diff --git a/build.gradle.kts b/build.gradle.kts index 213e3aa5..b152a5c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,14 +17,15 @@ dependencies { val minestomVersion = "aebf72de90" val loggingVersion = "3.0.5" val mockkVersion = "1.13.4" - val coroutinesCoreVersion = "1.6.4" + val coroutinesVersion = "1.6.4" val kotlinSerializationVersion = "1.5.0" val commonsNetVersion = "3.9.0" val icu4jVersion = "72.1" api(kotlin("stdlib")) api(kotlin("reflect")) - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesCoreVersion") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion") api("com.github.Minestom.Minestom:Minestom:$minestomVersion") api("commons-net:commons-net:$commonsNetVersion") api("com.ibm.icu:icu4j:$icu4jVersion") @@ -37,7 +38,7 @@ dependencies { testImplementation(kotlin("test-junit5")) testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesCoreVersion") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") testImplementation("io.mockk:mockk:$mockkVersion") testImplementation("com.github.Minestom.Minestom:testing:$minestomVersion") } diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt new file mode 100644 index 00000000..d375335b --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt @@ -0,0 +1,254 @@ +package com.github.rushyverse.api.image + +import com.github.rushyverse.api.image.exception.ImageAlreadyLoadedException +import com.github.rushyverse.api.image.exception.ImageNotLoadedException +import com.github.rushyverse.api.image.exception.ItemFramesAlreadyExistException +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.future.asDeferred +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Entity +import net.minestom.server.entity.EntityType +import net.minestom.server.entity.metadata.other.ItemFrameMeta +import net.minestom.server.instance.Instance +import net.minestom.server.item.ItemStack +import net.minestom.server.item.Material +import net.minestom.server.item.metadata.MapMeta +import net.minestom.server.map.framebuffers.LargeGraphics2DFramebuffer +import net.minestom.server.network.packet.server.SendablePacket +import org.jetbrains.annotations.Blocking +import java.awt.geom.AffineTransform +import java.awt.image.BufferedImage +import java.io.InputStream +import javax.imageio.ImageIO +import kotlin.properties.Delegates + +/** + * Read an image from the resources and build the packets to send to the players. + * @see loadImageAsPacketsFromInputStream + * @receiver Object to display image on the server. + * @param resourceImage Path of the image in the resources. + * @param modifyTransform Function to modify the transform of the image. + * @return The packets list to send to players. + */ +@Blocking +public fun MapImage.loadImageAsPacketsFromResources( + resourceImage: String, + modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} +): Array { + val inputStream = MapImage::class.java.getResourceAsStream("/$resourceImage") + ?: error("Unable to retrieve the image $resourceImage in resources.") + + return inputStream.buffered().use { loadImageAsPacketsFromInputStream(it, modifyTransform) } +} + +/** + * Read an image from an input stream and build the packets to send to the players. + * **This method does not close the provided [inputStream] after the read operation has completed. + * It is the responsibility of the caller to close the stream, if desired.** + * @see [MapImage.loadImageAsPackets] + * @receiver Object to display image on the server. + * @param inputStream Input stream to retrieve the image's data. + * @param modifyTransform Function to modify the transform of the image. + * @return The packets list to send to players. + */ +@Blocking +public fun MapImage.loadImageAsPacketsFromInputStream( + inputStream: InputStream, + modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} +): Array { + val image = ImageIO.read(inputStream) + return loadImageAsPackets(image, modifyTransform) +} + +/** + * A class that allows you to create an Image as Map Item Frame on the server. + * @property packets The packets list to send to new players. + * @property itemFramesPerLine The width blocks size desired for the item frame. The value define the number of item frames by line. + * @property itemFramesPerColumn The height blocks size desired for the item frame. The value define the number of item frames by column. + * @property numberOfItemFrames The number of item frames needed to display the image. + * @property imageLoaded `true` if the image is loaded, `false` otherwise. + * @property itemFrames The list of item frames created. + */ +public class MapImage { + + public companion object { + + /** + * The number of pixels per item frame is 128x128. + */ + public const val MAP_ITEM_FRAME_PIXELS: Int = 128 + + /** + * The number of pixels per item frame is 128. + * So to improve the performance, we will use the bitwise operator to divide by 128. + */ + private const val MAP_ITEM_FRAME_PIXELS_BITWISE = 7 + } + + public var packets: Array? = null + private set + + public var itemFramesPerLine: Int by Delegates.notNull() + private set + + public var itemFramesPerColumn: Int by Delegates.notNull() + private set + + public val imageLoaded: Boolean + get() = packets != null + + private var _itemFrames: List? = null + + public val itemFrames: List? + get() = _itemFrames + + private val numberOfItemFrames: Int + get() = itemFramesPerLine * itemFramesPerColumn + + /** + * Create the packets list to send to new players. + * The result is stored in the [packets] property. + * + * **This method does not close the provided [inputStream] after the read operation has completed. + * It is the responsibility of the caller to close the stream, if desired.** + * + * @param image The image to display. + * @param modifyTransform The function to apply transformation to the image. By default, the image is turned upside down. + * For example, to rotate the image of 90° clockwise, you can use the following code: + * ``` + * // 'this' is the AffineTransform instance. + * // 'it' is the Image instance. + * rotate(Math.toRadians(90.0), it.width / 2.0, it.height / 2.0) + * ``` + * @return The packets list to send to players. + */ + public fun loadImageAsPackets( + image: BufferedImage, + modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} + ): Array { + if (imageLoaded) { + throw ImageAlreadyLoadedException("An image is already loaded using this instance.") + } + + val imageWidth = image.width + val imageHeight = image.height + // We need to round the value to the nearest integer. + // For example : + // If the image is 1x1, we need 1 item frame by line and 1 item frame by column. + // If the image is 129x129, we need 2 item frames by line and 2 item frames by column. + // If the image is 129x128, we need 2 item frames by line and 1 item frame by column. + itemFramesPerLine = (imageWidth + MAP_ITEM_FRAME_PIXELS - 1) ushr MAP_ITEM_FRAME_PIXELS_BITWISE + itemFramesPerColumn = (imageHeight + MAP_ITEM_FRAME_PIXELS - 1) ushr MAP_ITEM_FRAME_PIXELS_BITWISE + + val transform = AffineTransform.getScaleInstance(1.0, 1.0).apply { + modifyTransform(image) + } + + val framebuffer = LargeGraphics2DFramebuffer(imageWidth, imageHeight).apply { + renderer.drawRenderedImage(image, transform) + } + + return createPackets(framebuffer).also { packets = it } + } + + /** + * Creates packets from the image. + * @param framebuffer The frame buffer to convert as packets. + * @return The list of packets. + */ + private fun createPackets(framebuffer: LargeGraphics2DFramebuffer): Array { + val itemFramesPerLine = itemFramesPerLine + return Array(numberOfItemFrames) { + val x = it % itemFramesPerLine + val y = it / itemFramesPerLine + framebuffer.createSubView( + x shl MAP_ITEM_FRAME_PIXELS_BITWISE, + y shl MAP_ITEM_FRAME_PIXELS_BITWISE + ).preparePacket(it) + } + } + + /** + * Create necessary item frames on which the image will be displayed. + * + * **Before calling this method, you must have loaded an image using [loadImageAsPackets].** + * @param instance The instance where you want to create the frame. + * @param pos The position of the frame. + * @param orientation The orientation of the frame. + * @param metaModifier The function to modify the item frame meta. + */ + public suspend fun createItemFrames( + instance: Instance, + pos: Pos, + orientation: ItemFrameMeta.Orientation, + metaModifier: ItemFrameMeta.() -> Unit = { + isInvisible = true + } + ): List { + if (!imageLoaded) { + throw ImageNotLoadedException("An image must be loaded before creating the item frames.") + } + if (atLeastOneItemFrameIsPresent()) { + throw ItemFramesAlreadyExistException("The item frames are already present in the instance.") + } + if (numberOfItemFrames == 0) { + return emptyList().also { _itemFrames = it } + } + + val imageMath = MapImageMath.getFromOrientation(orientation) + val beginX = pos.blockX() + val beginY = pos.blockY() + val beginZ = pos.blockZ() + + // Workaround to avoid unpredictable rotation of the item frames. + val yaw = imageMath.yaw + val pitch = imageMath.pitch + + val entities = List(numberOfItemFrames) { frameNumber -> + Entity(EntityType.ITEM_FRAME).apply { + with(entityMeta as ItemFrameMeta) { + setNotifyAboutChanges(false) + + item = ItemStack.builder(Material.FILLED_MAP) + .meta(MapMeta::class.java) { it.mapId(frameNumber) } + .build() + + this.orientation = orientation + metaModifier() + + setNotifyAboutChanges(true) + } + } + } + + entities.mapIndexed { frameNumber, entity -> + val x = imageMath.computeX(beginX, frameNumber, itemFramesPerLine) + val y = imageMath.computeY(beginY, frameNumber, itemFramesPerLine) + val z = imageMath.computeZ(beginZ, frameNumber, itemFramesPerLine) + entity.setInstance(instance, Pos(x.toDouble(), y.toDouble(), z.toDouble(), yaw, pitch)).asDeferred() + }.awaitAll() + + return entities.also { _itemFrames = it } + } + + /** + * Remove all item frames linked to the image. + * Do nothing if the item frames are not present. + * Will set the [itemFrames] property to `null`. + * @see [Entity.remove] + */ + public fun removeItemFrames() { + val itemFrames = itemFrames ?: return + itemFrames.forEach(Entity::remove) + _itemFrames = null + } + + /** + * Check if all item frames are present. + * If at least one item frame is not present, the function will return `false`. + * @return `true` if at least one item frame is present, `false` otherwise. + */ + private fun atLeastOneItemFrameIsPresent(): Boolean { + return itemFrames?.any { !it.isRemoved } ?: return false + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt new file mode 100644 index 00000000..c18cded3 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt @@ -0,0 +1,173 @@ +package com.github.rushyverse.api.image + +import net.minestom.server.entity.metadata.other.ItemFrameMeta + +/** + * This class is used to calculate the position of the map image. + * The [yaw] and [pitch] values are used to fix the orientation of the item frame. + * Check https://github.com/Minestom/Minestom/issues/760. + * @property yaw Get the pitch for the orientation. + * This is a workaround to fix the orientation of the item frame. + * Without these values, the item frame will be rotated in another direction after several seconds. + * @property pitch Get the pitch for the orientation. + * This is a workaround to fix the orientation of the item frame. + * Without these values, the item frame will be rotated in another direction after several seconds. + */ +public sealed interface MapImageMath { + + public companion object { + + /** + * Link the item frame orientation to the [MapImageMath] instance. + */ + private val orientations = mapOf( + ItemFrameMeta.Orientation.DOWN to Down, + ItemFrameMeta.Orientation.UP to Up, + ItemFrameMeta.Orientation.NORTH to North, + ItemFrameMeta.Orientation.SOUTH to South, + ItemFrameMeta.Orientation.WEST to West, + ItemFrameMeta.Orientation.EAST to East + ) + + /** + * Get the [MapImageMath] linked to the orientation. + * @param orientation The orientation of the item frame. + * @return The [MapImageMath] for the orientation. + */ + public fun getFromOrientation(orientation: ItemFrameMeta.Orientation): MapImageMath { + return orientations[orientation] ?: throw IllegalArgumentException("Unsupported orientation: $orientation") + } + } + + public val yaw: Float + public val pitch: Float + + /** + * Compute the x position of the item frame. + * @param beginX Initial x position. + * @param frameNumber Number of the item frame. + * @param itemFramesPerLine Number of blocks by line. + * @return The x position of the item frame. + */ + public fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int + + /** + * Compute the y position of the item frame. + * @param beginY Initial y position. + * @param frameNumber Number of the item frame. + * @param itemFramesPerLine Number of blocks by line. + * @return The y position of the item frame. + */ + public fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int + + /** + * Compute the z position of the item frame. + * @param beginZ Initial z position. + * @param frameNumber Number of the item frame. + * @param itemFramesPerLine Number of blocks by line. + * @return The z position of the item frame. + */ + public fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int + + /** + * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.UP]. + */ + public object Up : MapImageMath { + override val yaw: Float = 0f + override val pitch: Float = 270f + + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginX + (frameNumber % itemFramesPerLine) + + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginY + + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginZ + (frameNumber / itemFramesPerLine) + } + + /** + * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.DOWN]. + */ + public object Down : MapImageMath { + override val yaw: Float = 0f + override val pitch: Float = 90f + + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginX + (frameNumber % itemFramesPerLine) + + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginY + + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginZ - (frameNumber / itemFramesPerLine) + } + + /** + * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.NORTH]. + */ + public object North : MapImageMath { + override val yaw: Float = 180f + override val pitch: Float = 0f + + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginX - (frameNumber % itemFramesPerLine) + + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginY - (frameNumber / itemFramesPerLine) + + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginZ + } + + /** + * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.SOUTH]. + */ + public object South : MapImageMath { + override val yaw: Float = 0f + override val pitch: Float = 0f + + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginX + (frameNumber % itemFramesPerLine) + + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginY - (frameNumber / itemFramesPerLine) + + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginZ + } + + /** + * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.EAST]. + */ + public object East : MapImageMath { + override val yaw: Float = 270f + override val pitch: Float = 0f + + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginX + + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginY - (frameNumber / itemFramesPerLine) + + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginZ - (frameNumber % itemFramesPerLine) + } + + /** + * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.WEST]. + */ + public object West : MapImageMath { + override val yaw: Float = 90f + override val pitch: Float = 0f + + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginX + + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginY - (frameNumber / itemFramesPerLine) + + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginZ + (frameNumber % itemFramesPerLine) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/image/exception/ImageNotLoadedException.kt b/src/main/kotlin/com/github/rushyverse/api/image/exception/ImageNotLoadedException.kt new file mode 100644 index 00000000..b8c5a38d --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/image/exception/ImageNotLoadedException.kt @@ -0,0 +1,16 @@ +package com.github.rushyverse.api.image.exception + +/** + * Exception when an issue occurs with an image. + */ +public open class ImageException(message: String) : Exception(message) + +/** + * Exception thrown when an image is already loaded. + */ +public open class ImageAlreadyLoadedException(message: String) : ImageException(message) + +/** + * Exception thrown when an image is not loaded. + */ +public open class ImageNotLoadedException(message: String) : ImageException(message) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/image/exception/ItemFramesAlreadyExistException.kt b/src/main/kotlin/com/github/rushyverse/api/image/exception/ItemFramesAlreadyExistException.kt new file mode 100644 index 00000000..0d495f69 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/image/exception/ItemFramesAlreadyExistException.kt @@ -0,0 +1,6 @@ +package com.github.rushyverse.api.image.exception + +/** + * Exception thrown when item frames are already loaded and present in an instance. + */ +public open class ItemFramesAlreadyExistException(message: String) : Exception(message) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/ItemFrameMetaOrientationSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/ItemFrameMetaOrientationSerializer.kt new file mode 100644 index 00000000..c967f8a3 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/ItemFrameMetaOrientationSerializer.kt @@ -0,0 +1,30 @@ +package com.github.rushyverse.api.serializer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.minestom.server.entity.metadata.other.ItemFrameMeta + +/** + * Serializer for [ItemFrameMeta.Orientation]. + * To deserialize the orientation, it will be case-insensitive. + */ +public object ItemFrameMetaOrientationSerializer : KSerializer { + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("orientation", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: ItemFrameMeta.Orientation) { + encoder.encodeString(value.name) + } + + override fun deserialize(decoder: Decoder): ItemFrameMeta.Orientation { + val decodeString = decoder.decodeString() + return ItemFrameMeta.Orientation.values() + .find { it.name.equals(decodeString, true) } + ?: throw SerializationException("Invalid orientation") + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt new file mode 100644 index 00000000..abd40b28 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt @@ -0,0 +1,806 @@ +package com.github.rushyverse.api.image + +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class MapImageMathTest { + + @Nested + inner class Up { + + private lateinit var instance: MapImageMath + + @BeforeTest + fun onBefore() { + instance = MapImageMath.Up + } + + @Test + fun `should have the correct yaw`() { + assertEquals(0f, instance.yaw) + } + + @Test + fun `should have the correct pitch`() { + assertEquals(270f, instance.pitch) + } + + @Nested + inner class ComputeX { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct x with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeX(begin, 0, itemFramesPerLine)) + assertEquals(1, instance.computeX(begin, 1, itemFramesPerLine)) + assertEquals(2, instance.computeX(begin, 2, itemFramesPerLine)) + + assertEquals(0, instance.computeX(begin, 3, itemFramesPerLine)) + assertEquals(1, instance.computeX(begin, 4, itemFramesPerLine)) + assertEquals(2, instance.computeX(begin, 5, itemFramesPerLine)) + } + + @Test + fun `should compute the correct x with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeX(begin, 0, itemFramesPerLine)) + assertEquals(6, instance.computeX(begin, 1, itemFramesPerLine)) + assertEquals(7, instance.computeX(begin, 2, itemFramesPerLine)) + + assertEquals(5, instance.computeX(begin, 3, itemFramesPerLine)) + assertEquals(6, instance.computeX(begin, 4, itemFramesPerLine)) + assertEquals(7, instance.computeX(begin, 5, itemFramesPerLine)) + } + + } + + @Nested + inner class ComputeY { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) + } + } + + @ParameterizedTest + @ValueSource(ints = [-1, 0, 1]) + fun `should stay on the same y`(beginY: Int) { + repeat(10) { + assertEquals(0, instance.computeY(0, it, 5)) + } + } + + } + + @Nested + inner class ComputeZ { + + @Test + fun `should throws exception with no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct z with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeZ(begin, 0, itemFramesPerLine)) + assertEquals(0, instance.computeZ(begin, 1, itemFramesPerLine)) + assertEquals(0, instance.computeZ(begin, 2, itemFramesPerLine)) + + assertEquals(1, instance.computeZ(begin, 3, itemFramesPerLine)) + assertEquals(1, instance.computeZ(begin, 4, itemFramesPerLine)) + assertEquals(1, instance.computeZ(begin, 5, itemFramesPerLine)) + + assertEquals(2, instance.computeZ(begin, 6, itemFramesPerLine)) + assertEquals(2, instance.computeZ(begin, 7, itemFramesPerLine)) + assertEquals(2, instance.computeZ(begin, 8, itemFramesPerLine)) + } + + @Test + fun `should compute the correct z with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeZ(begin, 0, itemFramesPerLine)) + assertEquals(5, instance.computeZ(begin, 1, itemFramesPerLine)) + assertEquals(5, instance.computeZ(begin, 2, itemFramesPerLine)) + + assertEquals(6, instance.computeZ(begin, 3, itemFramesPerLine)) + assertEquals(6, instance.computeZ(begin, 4, itemFramesPerLine)) + assertEquals(6, instance.computeZ(begin, 5, itemFramesPerLine)) + + assertEquals(7, instance.computeZ(begin, 6, itemFramesPerLine)) + assertEquals(7, instance.computeZ(begin, 7, itemFramesPerLine)) + assertEquals(7, instance.computeZ(begin, 8, itemFramesPerLine)) + } + + } + } + + @Nested + inner class Down { + + private lateinit var instance: MapImageMath + + @BeforeTest + fun onBefore() { + instance = MapImageMath.Down + } + + @Test + fun `should have the correct yaw`() { + assertEquals(0f, instance.yaw) + } + + @Test + fun `should have the correct pitch`() { + assertEquals(90f, instance.pitch) + } + + @Nested + inner class ComputeX { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct x with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeX(begin, 0, itemFramesPerLine)) + assertEquals(1, instance.computeX(begin, 1, itemFramesPerLine)) + assertEquals(2, instance.computeX(begin, 2, itemFramesPerLine)) + + assertEquals(0, instance.computeX(begin, 3, itemFramesPerLine)) + assertEquals(1, instance.computeX(begin, 4, itemFramesPerLine)) + assertEquals(2, instance.computeX(begin, 5, itemFramesPerLine)) + } + + @Test + fun `should compute the correct x with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeX(begin, 0, itemFramesPerLine)) + assertEquals(6, instance.computeX(begin, 1, itemFramesPerLine)) + assertEquals(7, instance.computeX(begin, 2, itemFramesPerLine)) + + assertEquals(5, instance.computeX(begin, 3, itemFramesPerLine)) + assertEquals(6, instance.computeX(begin, 4, itemFramesPerLine)) + assertEquals(7, instance.computeX(begin, 5, itemFramesPerLine)) + } + + } + + @Nested + inner class ComputeY { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) + } + } + + @ParameterizedTest + @ValueSource(ints = [-1, 0, 1]) + fun `should stay on the same y`(beginY: Int) { + repeat(10) { + assertEquals(0, instance.computeY(0, it, 5)) + } + } + + } + + @Nested + inner class ComputeZ { + + @Test + fun `should throws exception with no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct z with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeZ(begin, 0, itemFramesPerLine)) + assertEquals(0, instance.computeZ(begin, 1, itemFramesPerLine)) + assertEquals(0, instance.computeZ(begin, 2, itemFramesPerLine)) + + assertEquals(-1, instance.computeZ(begin, 3, itemFramesPerLine)) + assertEquals(-1, instance.computeZ(begin, 4, itemFramesPerLine)) + assertEquals(-1, instance.computeZ(begin, 5, itemFramesPerLine)) + + assertEquals(-2, instance.computeZ(begin, 6, itemFramesPerLine)) + assertEquals(-2, instance.computeZ(begin, 7, itemFramesPerLine)) + assertEquals(-2, instance.computeZ(begin, 8, itemFramesPerLine)) + } + + @Test + fun `should compute the correct z with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeZ(begin, 0, itemFramesPerLine)) + assertEquals(5, instance.computeZ(begin, 1, itemFramesPerLine)) + assertEquals(5, instance.computeZ(begin, 2, itemFramesPerLine)) + + assertEquals(4, instance.computeZ(begin, 3, itemFramesPerLine)) + assertEquals(4, instance.computeZ(begin, 4, itemFramesPerLine)) + assertEquals(4, instance.computeZ(begin, 5, itemFramesPerLine)) + + assertEquals(3, instance.computeZ(begin, 6, itemFramesPerLine)) + assertEquals(3, instance.computeZ(begin, 7, itemFramesPerLine)) + assertEquals(3, instance.computeZ(begin, 8, itemFramesPerLine)) + } + + } + + } + + @Nested + inner class North { + + private lateinit var instance: MapImageMath + + @BeforeTest + fun onBefore() { + instance = MapImageMath.North + } + + @Test + fun `should have the correct yaw`() { + assertEquals(180f, instance.yaw) + } + + @Test + fun `should have the correct pitch`() { + assertEquals(0f, instance.pitch) + } + + @Nested + inner class ComputeX { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct x with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeX(begin, 0, itemFramesPerLine)) + assertEquals(-1, instance.computeX(begin, 1, itemFramesPerLine)) + assertEquals(-2, instance.computeX(begin, 2, itemFramesPerLine)) + + assertEquals(0, instance.computeX(begin, 3, itemFramesPerLine)) + assertEquals(-1, instance.computeX(begin, 4, itemFramesPerLine)) + assertEquals(-2, instance.computeX(begin, 5, itemFramesPerLine)) + } + + @Test + fun `should compute the correct x with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeX(begin, 0, itemFramesPerLine)) + assertEquals(4, instance.computeX(begin, 1, itemFramesPerLine)) + assertEquals(3, instance.computeX(begin, 2, itemFramesPerLine)) + + assertEquals(5, instance.computeX(begin, 3, itemFramesPerLine)) + assertEquals(4, instance.computeX(begin, 4, itemFramesPerLine)) + assertEquals(3, instance.computeX(begin, 5, itemFramesPerLine)) + } + + } + + @Nested + inner class ComputeY { + + @Test + fun `should throws exception with no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct y with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeY(begin, 0, itemFramesPerLine)) + assertEquals(0, instance.computeY(begin, 1, itemFramesPerLine)) + assertEquals(0, instance.computeY(begin, 2, itemFramesPerLine)) + + assertEquals(-1, instance.computeY(begin, 3, itemFramesPerLine)) + assertEquals(-1, instance.computeY(begin, 4, itemFramesPerLine)) + assertEquals(-1, instance.computeY(begin, 5, itemFramesPerLine)) + + assertEquals(-2, instance.computeY(begin, 6, itemFramesPerLine)) + assertEquals(-2, instance.computeY(begin, 7, itemFramesPerLine)) + assertEquals(-2, instance.computeY(begin, 8, itemFramesPerLine)) + } + + @Test + fun `should compute the correct y with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeY(begin, 0, itemFramesPerLine)) + assertEquals(5, instance.computeY(begin, 1, itemFramesPerLine)) + assertEquals(5, instance.computeY(begin, 2, itemFramesPerLine)) + + assertEquals(4, instance.computeY(begin, 3, itemFramesPerLine)) + assertEquals(4, instance.computeY(begin, 4, itemFramesPerLine)) + assertEquals(4, instance.computeY(begin, 5, itemFramesPerLine)) + + assertEquals(3, instance.computeY(begin, 6, itemFramesPerLine)) + assertEquals(3, instance.computeY(begin, 7, itemFramesPerLine)) + assertEquals(3, instance.computeY(begin, 8, itemFramesPerLine)) + } + } + + @Nested + inner class ComputeZ { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) + } + } + + @ParameterizedTest + @ValueSource(ints = [-1, 0, 1]) + fun `should stay on the same z`(beginZ: Int) { + repeat(10) { + assertEquals(0, instance.computeZ(0, it, 5)) + } + } + + } + + } + + @Nested + inner class South { + + private lateinit var instance: MapImageMath + + @BeforeTest + fun onBefore() { + instance = MapImageMath.South + } + + @Test + fun `should have the correct yaw`() { + assertEquals(0f, instance.yaw) + } + + @Test + fun `should have the correct pitch`() { + assertEquals(0f, instance.pitch) + } + + @Nested + inner class ComputeX { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct x with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeX(begin, 0, itemFramesPerLine)) + assertEquals(1, instance.computeX(begin, 1, itemFramesPerLine)) + assertEquals(2, instance.computeX(begin, 2, itemFramesPerLine)) + + assertEquals(0, instance.computeX(begin, 3, itemFramesPerLine)) + assertEquals(1, instance.computeX(begin, 4, itemFramesPerLine)) + assertEquals(2, instance.computeX(begin, 5, itemFramesPerLine)) + } + + @Test + fun `should compute the correct x with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeX(begin, 0, itemFramesPerLine)) + assertEquals(6, instance.computeX(begin, 1, itemFramesPerLine)) + assertEquals(7, instance.computeX(begin, 2, itemFramesPerLine)) + + assertEquals(5, instance.computeX(begin, 3, itemFramesPerLine)) + assertEquals(6, instance.computeX(begin, 4, itemFramesPerLine)) + assertEquals(7, instance.computeX(begin, 5, itemFramesPerLine)) + } + + } + + @Nested + inner class ComputeY { + + @Test + fun `should throws exception with no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct y with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeY(begin, 0, itemFramesPerLine)) + assertEquals(0, instance.computeY(begin, 1, itemFramesPerLine)) + assertEquals(0, instance.computeY(begin, 2, itemFramesPerLine)) + + assertEquals(-1, instance.computeY(begin, 3, itemFramesPerLine)) + assertEquals(-1, instance.computeY(begin, 4, itemFramesPerLine)) + assertEquals(-1, instance.computeY(begin, 5, itemFramesPerLine)) + + assertEquals(-2, instance.computeY(begin, 6, itemFramesPerLine)) + assertEquals(-2, instance.computeY(begin, 7, itemFramesPerLine)) + assertEquals(-2, instance.computeY(begin, 8, itemFramesPerLine)) + } + + @Test + fun `should compute the correct y with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeY(begin, 0, itemFramesPerLine)) + assertEquals(5, instance.computeY(begin, 1, itemFramesPerLine)) + assertEquals(5, instance.computeY(begin, 2, itemFramesPerLine)) + + assertEquals(4, instance.computeY(begin, 3, itemFramesPerLine)) + assertEquals(4, instance.computeY(begin, 4, itemFramesPerLine)) + assertEquals(4, instance.computeY(begin, 5, itemFramesPerLine)) + + assertEquals(3, instance.computeY(begin, 6, itemFramesPerLine)) + assertEquals(3, instance.computeY(begin, 7, itemFramesPerLine)) + assertEquals(3, instance.computeY(begin, 8, itemFramesPerLine)) + } + } + + @Nested + inner class ComputeZ { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) + } + } + + @ParameterizedTest + @ValueSource(ints = [-1, 0, 1]) + fun `should stay on the same z`(beginZ: Int) { + repeat(10) { + assertEquals(0, instance.computeZ(0, it, 5)) + } + } + } + } + + @Nested + inner class West { + + private lateinit var instance: MapImageMath + + @BeforeTest + fun onBefore() { + instance = MapImageMath.West + } + + @Test + fun `should have the correct yaw`() { + assertEquals(90f, instance.yaw) + } + + @Test + fun `should have the correct pitch`() { + assertEquals(0f, instance.pitch) + } + + @Nested + inner class ComputeX { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) + } + } + + @ParameterizedTest + @ValueSource(ints = [-1, 0, 1]) + fun `should stay on the same x`(beginZ: Int) { + repeat(10) { + assertEquals(0, instance.computeX(0, it, 5)) + } + } + + } + + @Nested + inner class ComputeY { + + @Test + fun `should throws exception with no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct y with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeY(begin, 0, itemFramesPerLine)) + assertEquals(0, instance.computeY(begin, 1, itemFramesPerLine)) + assertEquals(0, instance.computeY(begin, 2, itemFramesPerLine)) + + assertEquals(-1, instance.computeY(begin, 3, itemFramesPerLine)) + assertEquals(-1, instance.computeY(begin, 4, itemFramesPerLine)) + assertEquals(-1, instance.computeY(begin, 5, itemFramesPerLine)) + + assertEquals(-2, instance.computeY(begin, 6, itemFramesPerLine)) + assertEquals(-2, instance.computeY(begin, 7, itemFramesPerLine)) + assertEquals(-2, instance.computeY(begin, 8, itemFramesPerLine)) + } + + @Test + fun `should compute the correct y with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeY(begin, 0, itemFramesPerLine)) + assertEquals(5, instance.computeY(begin, 1, itemFramesPerLine)) + assertEquals(5, instance.computeY(begin, 2, itemFramesPerLine)) + + assertEquals(4, instance.computeY(begin, 3, itemFramesPerLine)) + assertEquals(4, instance.computeY(begin, 4, itemFramesPerLine)) + assertEquals(4, instance.computeY(begin, 5, itemFramesPerLine)) + + assertEquals(3, instance.computeY(begin, 6, itemFramesPerLine)) + assertEquals(3, instance.computeY(begin, 7, itemFramesPerLine)) + assertEquals(3, instance.computeY(begin, 8, itemFramesPerLine)) + } + } + + @Nested + inner class ComputeZ { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct z with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeZ(begin, 0, itemFramesPerLine)) + assertEquals(1, instance.computeZ(begin, 1, itemFramesPerLine)) + assertEquals(2, instance.computeZ(begin, 2, itemFramesPerLine)) + + assertEquals(0, instance.computeZ(begin, 3, itemFramesPerLine)) + assertEquals(1, instance.computeZ(begin, 4, itemFramesPerLine)) + assertEquals(2, instance.computeZ(begin, 5, itemFramesPerLine)) + } + + @Test + fun `should compute the correct z with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeZ(begin, 0, itemFramesPerLine)) + assertEquals(6, instance.computeZ(begin, 1, itemFramesPerLine)) + assertEquals(7, instance.computeZ(begin, 2, itemFramesPerLine)) + + assertEquals(5, instance.computeZ(begin, 3, itemFramesPerLine)) + assertEquals(6, instance.computeZ(begin, 4, itemFramesPerLine)) + assertEquals(7, instance.computeZ(begin, 5, itemFramesPerLine)) + } + } + } + + @Nested + inner class East { + + private lateinit var instance: MapImageMath + + @BeforeTest + fun onBefore() { + instance = MapImageMath.East + } + + @Test + fun `should have the correct yaw`() { + assertEquals(270f, instance.yaw) + } + + @Test + fun `should have the correct pitch`() { + assertEquals(0f, instance.pitch) + } + + @Nested + inner class ComputeX { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) + } + } + + @ParameterizedTest + @ValueSource(ints = [-1, 0, 1]) + fun `should stay on the same x`(beginZ: Int) { + repeat(10) { + assertEquals(0, instance.computeX(0, it, 5)) + } + } + + } + + @Nested + inner class ComputeY { + + @Test + fun `should throws exception with no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct y with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeY(begin, 0, itemFramesPerLine)) + assertEquals(0, instance.computeY(begin, 1, itemFramesPerLine)) + assertEquals(0, instance.computeY(begin, 2, itemFramesPerLine)) + + assertEquals(-1, instance.computeY(begin, 3, itemFramesPerLine)) + assertEquals(-1, instance.computeY(begin, 4, itemFramesPerLine)) + assertEquals(-1, instance.computeY(begin, 5, itemFramesPerLine)) + + assertEquals(-2, instance.computeY(begin, 6, itemFramesPerLine)) + assertEquals(-2, instance.computeY(begin, 7, itemFramesPerLine)) + assertEquals(-2, instance.computeY(begin, 8, itemFramesPerLine)) + } + + @Test + fun `should compute the correct y with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeY(begin, 0, itemFramesPerLine)) + assertEquals(5, instance.computeY(begin, 1, itemFramesPerLine)) + assertEquals(5, instance.computeY(begin, 2, itemFramesPerLine)) + + assertEquals(4, instance.computeY(begin, 3, itemFramesPerLine)) + assertEquals(4, instance.computeY(begin, 4, itemFramesPerLine)) + assertEquals(4, instance.computeY(begin, 5, itemFramesPerLine)) + + assertEquals(3, instance.computeY(begin, 6, itemFramesPerLine)) + assertEquals(3, instance.computeY(begin, 7, itemFramesPerLine)) + assertEquals(3, instance.computeY(begin, 8, itemFramesPerLine)) + } + + } + + @Nested + inner class ComputeZ { + + @Test + fun `should always return 0 if no element per line`() { + repeat(10) { + assertThrows { + assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) + } + } + } + + @Test + fun `should compute the correct z with begin equals to 0`() { + val begin = 0 + val itemFramesPerLine = 3 + + assertEquals(0, instance.computeZ(begin, 0, itemFramesPerLine)) + assertEquals(-1, instance.computeZ(begin, 1, itemFramesPerLine)) + assertEquals(-2, instance.computeZ(begin, 2, itemFramesPerLine)) + + assertEquals(0, instance.computeZ(begin, 3, itemFramesPerLine)) + assertEquals(-1, instance.computeZ(begin, 4, itemFramesPerLine)) + assertEquals(-2, instance.computeZ(begin, 5, itemFramesPerLine)) + } + + @Test + fun `should compute the correct z with begin greater than 0`() { + val begin = 5 + val itemFramesPerLine = 3 + + assertEquals(5, instance.computeZ(begin, 0, itemFramesPerLine)) + assertEquals(4, instance.computeZ(begin, 1, itemFramesPerLine)) + assertEquals(3, instance.computeZ(begin, 2, itemFramesPerLine)) + + assertEquals(5, instance.computeZ(begin, 3, itemFramesPerLine)) + assertEquals(4, instance.computeZ(begin, 4, itemFramesPerLine)) + assertEquals(3, instance.computeZ(begin, 5, itemFramesPerLine)) + } + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt new file mode 100644 index 00000000..33c541b5 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt @@ -0,0 +1,722 @@ +package com.github.rushyverse.api.image + +import com.github.rushyverse.api.image.exception.ImageAlreadyLoadedException +import com.github.rushyverse.api.image.exception.ImageNotLoadedException +import com.github.rushyverse.api.image.exception.ItemFramesAlreadyExistException +import io.mockk.every +import io.mockk.spyk +import kotlinx.coroutines.test.runTest +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.metadata.other.ItemFrameMeta +import net.minestom.server.item.Material +import net.minestom.server.item.metadata.MapMeta +import net.minestom.server.network.packet.server.SendablePacket +import net.minestom.server.network.packet.server.play.MapDataPacket +import net.minestom.server.utils.Rotation +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import java.awt.Color +import java.awt.image.BufferedImage +import java.awt.image.BufferedImage.TYPE_INT_ARGB +import kotlin.test.* + +class MapImageTest { + + companion object { + private const val BLACK_COLOR_PACKET = 119.toByte() + } + + @Nested + @EnvTest + inner class CreateItemFrames { + + @Nested + inner class Position { + + @Test + fun `should spawn item frame at the target position`(env: Env) = runTest { + val instance = env.createFlatInstance() + val orientation = ItemFrameMeta.Orientation.NORTH + val math = MapImageMath.getFromOrientation(orientation) + val image = BufferedImage(128, 128, TYPE_INT_ARGB) + + repeat(10) { x -> + repeat(10) { y -> + repeat(10) { z -> + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames( + instance, + Pos(x.toDouble(), y.toDouble(), z.toDouble()), + orientation + ) + assertEquals( + Pos(x.toDouble(), y.toDouble(), z.toDouble(), math.yaw, math.pitch), + mapImage.itemFrames!!.first().position + ) + } + } + } + } + + @Test + fun `should spawn item by following the north orientation`(env: Env) = runTest { + val instance = env.createFlatInstance() + val orientation = ItemFrameMeta.Orientation.NORTH + val math = MapImageMath.getFromOrientation(orientation) + val image = BufferedImage(256, 256, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) + + val itemFrames = mapImage.itemFrames!! + assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) + assertEquals(Pos(-1.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[1].position) + assertEquals(Pos(0.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[2].position) + assertEquals(Pos(-1.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[3].position) + } + + @Test + fun `should spawn item by following the east orientation`(env: Env) = runTest { + val instance = env.createFlatInstance() + val orientation = ItemFrameMeta.Orientation.EAST + val math = MapImageMath.getFromOrientation(orientation) + val image = BufferedImage(256, 256, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) + + val itemFrames = mapImage.itemFrames!! + assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) + assertEquals(Pos(0.0, 0.0, -1.0, math.yaw, math.pitch), itemFrames[1].position) + assertEquals(Pos(0.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[2].position) + assertEquals(Pos(0.0, -1.0, -1.0, math.yaw, math.pitch), itemFrames[3].position) + } + + @Test + fun `should spawn item by following the south orientation`(env: Env) = runTest { + val instance = env.createFlatInstance() + val orientation = ItemFrameMeta.Orientation.SOUTH + val math = MapImageMath.getFromOrientation(orientation) + val image = BufferedImage(256, 256, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) + + val itemFrames = mapImage.itemFrames!! + assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) + assertEquals(Pos(1.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[1].position) + assertEquals(Pos(0.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[2].position) + assertEquals(Pos(1.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[3].position) + } + + @Test + fun `should spawn item by following the west orientation`(env: Env) = runTest { + val instance = env.createFlatInstance() + val orientation = ItemFrameMeta.Orientation.WEST + val math = MapImageMath.getFromOrientation(orientation) + val image = BufferedImage(256, 256, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) + + val itemFrames = mapImage.itemFrames!! + assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) + assertEquals(Pos(0.0, 0.0, 1.0, math.yaw, math.pitch), itemFrames[1].position) + assertEquals(Pos(0.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[2].position) + assertEquals(Pos(0.0, -1.0, 1.0, math.yaw, math.pitch), itemFrames[3].position) + } + + @Test + fun `should spawn item by following the up orientation`(env: Env) = runTest { + val instance = env.createFlatInstance() + val orientation = ItemFrameMeta.Orientation.UP + val math = MapImageMath.getFromOrientation(orientation) + val image = BufferedImage(256, 256, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) + + val itemFrames = mapImage.itemFrames!! + assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) + assertEquals(Pos(1.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[1].position) + assertEquals(Pos(0.0, 0.0, 1.0, math.yaw, math.pitch), itemFrames[2].position) + assertEquals(Pos(1.0, 0.0, 1.0, math.yaw, math.pitch), itemFrames[3].position) + } + + @Test + fun `should spawn item by following the down orientation`(env: Env) = runTest { + val instance = env.createFlatInstance() + val orientation = ItemFrameMeta.Orientation.DOWN + val math = MapImageMath.getFromOrientation(orientation) + val image = BufferedImage(256, 256, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) + + val itemFrames = mapImage.itemFrames!! + assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) + assertEquals(Pos(1.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[1].position) + assertEquals(Pos(0.0, 0.0, -1.0, math.yaw, math.pitch), itemFrames[2].position) + assertEquals(Pos(1.0, 0.0, -1.0, math.yaw, math.pitch), itemFrames[3].position) + } + } + + @Nested + inner class MetaInformation { + + @Test + fun `should custom meta of item frame if needed`(env: Env) = runTest { + val instance = env.createFlatInstance() + val image = BufferedImage(128, 128, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + + val invisible = false + val rotation = Rotation.values().random() + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) { + this.isInvisible = false + this.rotation = rotation + } + + val itemFrame = mapImage.itemFrames!!.first() + val itemFrameMeta = itemFrame.entityMeta as ItemFrameMeta + assertEquals(rotation, itemFrameMeta.rotation) + assertEquals(invisible, itemFrameMeta.isInvisible) + } + + @Test + fun `should spawn item frame with the target orientation`(env: Env) = runTest { + val instance = env.createFlatInstance() + val image = BufferedImage(128, 128, TYPE_INT_ARGB) + + ItemFrameMeta.Orientation.values().forEach { orientation -> + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) + val itemFrame = mapImage.itemFrames!!.first() + assertEquals(orientation, (itemFrame.entityMeta as ItemFrameMeta).orientation) + } + } + + @Test + fun `should spawn item frame with invisibility by default`(env: Env) = runTest { + val instance = env.createFlatInstance() + val image = BufferedImage(128, 128, TYPE_INT_ARGB) + + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) + val itemFrame = mapImage.itemFrames!!.first() + assertEquals(true, itemFrame.isInvisible) + } + + @Test + fun `should spawn item frame with map item in meta`(env: Env) = runTest { + val instance = env.createFlatInstance() + val image = BufferedImage(128, 128, TYPE_INT_ARGB) + + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) + val itemFrame = mapImage.itemFrames!!.first() + val meta = itemFrame.entityMeta as ItemFrameMeta + + val metaItem = meta.item + assertNotNull(metaItem) + assertEquals(Material.FILLED_MAP, metaItem.material()) + } + + @Test + fun `should spawn item frame with map id`(env: Env) = runTest { + val instance = env.createFlatInstance() + val image = BufferedImage(256, 256, TYPE_INT_ARGB) + + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) + + val itemFrames = mapImage.itemFrames + assertNotNull(itemFrames) + assertEquals(4, itemFrames.size) + + itemFrames.forEachIndexed { index, entity -> + val meta = entity.entityMeta as ItemFrameMeta + val metaItem = meta.item + val metaOfMetaItem = metaItem.meta(MapMeta::class.java) + assertEquals(index, metaOfMetaItem.mapId) + } + } + + } + + @Nested + inner class WithImageNotLoaded { + + @Test + fun `should throw exception if image is not loaded`(env: Env) = runTest { + val instance = env.createFlatInstance() + val mapImage = MapImage() + val ex = assertThrows { + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) + } + assertEquals("An image must be loaded before creating the item frames.", ex.message) + } + } + + @Nested + inner class ItemFramesAlreadyExist { + + @Test + fun `should throw exception if all item frames already exist`(env: Env) = runTest { + val instance = env.createFlatInstance() + val image = BufferedImage(128, 128, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) + + val ex = assertThrows { + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) + } + + assertEquals("The item frames are already present in the instance.", ex.message) + } + + @Test + fun `should throw exception if at least one item frames already exist`(env: Env) = runTest { + val instance = env.createFlatInstance() + val image = BufferedImage(1024, 1024, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) + + val itemFrames = mapImage.itemFrames + assertNotNull(itemFrames) + itemFrames.drop(1).forEach { it.remove() } + assertTrue { itemFrames.drop(1).all { it.isRemoved } } + assertFalse { itemFrames.first().isRemoved } + + val ex = assertThrows { + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) + } + + assertEquals("The item frames are already present in the instance.", ex.message) + } + + } + + @Test + fun `should throw exception when image size is 0x0`(env: Env) = runTest { + val instance = env.createFlatInstance() + val mapImage = MapImage() + val image = BufferedImage(1, 1, TYPE_INT_ARGB) + mapImage.loadImageAsPackets(image) + + val spyMapImage = spyk(mapImage) { + every { itemFramesPerLine } returns 0 + every { itemFramesPerColumn } returns 0 + } + val returnedFrame = + spyMapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) + assertTrue { returnedFrame.isEmpty() } + assertTrue { spyMapImage.itemFrames!!.isEmpty() } + } + + @Test + fun `should return and set the property of item frames`(env: Env) = runTest { + val instance = env.createFlatInstance() + val mapImage = MapImage() + val image = BufferedImage(512, 1024, TYPE_INT_ARGB) + mapImage.loadImageAsPackets(image) + + val returnedFrame = mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) + assertEquals(returnedFrame, mapImage.itemFrames) + } + + @Test + fun `should create one item frame if image is between 1x1 and 128x128`(env: Env) = runTest { + val instance = env.createFlatInstance() + (1..128).forEach { width -> + val mapImage = MapImage() + mapImage.loadImageAsPackets(BufferedImage(width, width, TYPE_INT_ARGB)) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) + + val itemFrames = mapImage.itemFrames + assertNotNull(itemFrames) + assertEquals(1, itemFrames.size) + } + } + + @Test + fun `should create two item frame if image is 129x128`(env: Env) = runTest { + val instance = env.createFlatInstance() + val mapImage = MapImage() + val orientation = ItemFrameMeta.Orientation.NORTH + mapImage.loadImageAsPackets(BufferedImage(129, 128, TYPE_INT_ARGB)) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) + val math = MapImageMath.getFromOrientation(orientation) + + val itemFrames = mapImage.itemFrames + assertNotNull(itemFrames) + assertEquals(2, itemFrames.size) + + val (first, second) = itemFrames + assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), first.position) + assertEquals(Pos(-1.0, 0.0, 0.0, math.yaw, math.pitch), second.position) + } + + @Test + fun `should create two item frame if image is 128x129`(env: Env) = runTest { + val instance = env.createFlatInstance() + val mapImage = MapImage() + val orientation = ItemFrameMeta.Orientation.NORTH + mapImage.loadImageAsPackets(BufferedImage(128, 129, TYPE_INT_ARGB)) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) + val math = MapImageMath.getFromOrientation(orientation) + + val itemFrames = mapImage.itemFrames + assertNotNull(itemFrames) + assertEquals(2, itemFrames.size) + + val (first, second) = itemFrames + assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), first.position) + assertEquals(Pos(0.0, -1.0, 0.0, math.yaw, math.pitch), second.position) + } + + @Test + fun `should spawn item frame at the target instance`(env: Env) = runTest { + val instance = env.createFlatInstance() + val mapImage = MapImage() + mapImage.loadImageAsPackets(BufferedImage(128, 128, TYPE_INT_ARGB)) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) + assertEquals(instance, mapImage.itemFrames!!.first().instance) + } + } + + @Nested + inner class LoadImageAsPackets { + + @Nested + inner class ItemFramesPerLine { + + @Test + fun `should set property according to the width`() { + fun assertWidthPixelWithItemFramesPerLine(widths: IntRange, expectedFramesPerLine: Int) { + widths.forEach { + val image = BufferedImage(it, 128, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + assertEquals(expectedFramesPerLine, mapImage.itemFramesPerLine) + } + } + assertWidthPixelWithItemFramesPerLine(1..128, 1) + assertWidthPixelWithItemFramesPerLine(129..256, 2) + assertWidthPixelWithItemFramesPerLine(257..384, 3) + assertWidthPixelWithItemFramesPerLine(385..512, 4) + assertWidthPixelWithItemFramesPerLine(513..640, 5) + assertWidthPixelWithItemFramesPerLine(641..768, 6) + assertWidthPixelWithItemFramesPerLine(769..896, 7) + assertWidthPixelWithItemFramesPerLine(897..1024, 8) + } + + } + + @Nested + inner class ItemFramesPerColumn { + + @Test + fun `should set property according to the height`() { + fun assertHeightPixelWithItemFramesPerColumn(heights: IntRange, expectedFramesPerColumn: Int) { + heights.forEach { + val image = BufferedImage(128, it, TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.loadImageAsPackets(image) + assertEquals(expectedFramesPerColumn, mapImage.itemFramesPerColumn) + } + } + assertHeightPixelWithItemFramesPerColumn(1..128, 1) + assertHeightPixelWithItemFramesPerColumn(129..256, 2) + assertHeightPixelWithItemFramesPerColumn(257..384, 3) + assertHeightPixelWithItemFramesPerColumn(385..512, 4) + assertHeightPixelWithItemFramesPerColumn(513..640, 5) + assertHeightPixelWithItemFramesPerColumn(641..768, 6) + assertHeightPixelWithItemFramesPerColumn(769..896, 7) + assertHeightPixelWithItemFramesPerColumn(897..1024, 8) + } + + } + + @Test + fun `should throw exception if an image is already loaded`() { + val mapImage = MapImage() + mapImage.loadImageAsPackets(BufferedImage(128, 128, TYPE_INT_ARGB)) + assertThrows { + mapImage.loadImageAsPackets(BufferedImage(128, 128, TYPE_INT_ARGB)) + } + } + + @Test + fun `should load map data packets`() { + val mapImage = MapImage() + mapImage.loadImageAsPackets(BufferedImage(1000, 1000, TYPE_INT_ARGB)) + + val packets = assertNotNull(mapImage.packets) + packets.forEachIndexed { index, packet -> + assertTrue(packet is MapDataPacket) + assertEquals(index, packet.mapId) + } + } + + @Test + fun `should create one packet if the image is between 1x1 and 128x128`() { + (1..128).forEach { width -> + (1..128).forEach { height -> + val mapImage = MapImage() + mapImage.loadImageAsPackets(BufferedImage(width, height, TYPE_INT_ARGB)) + val packets = assertNotNull(mapImage.packets) + assertEquals(1, packets.size) + } + } + } + + @Test + fun `should create two packets if the image width is between 129 and 256`() { + (129..256).forEach { width -> + val mapImage = MapImage() + mapImage.loadImageAsPackets(BufferedImage(width, 1, TYPE_INT_ARGB)) + val packets = assertNotNull(mapImage.packets) + assertEquals(2, packets.size) + } + } + + @Test + fun `should create two packets if the image height is between 129 and 256`() { + (129..256).forEach { height -> + val mapImage = MapImage() + mapImage.loadImageAsPackets(BufferedImage(1, height, TYPE_INT_ARGB)) + val packets = assertNotNull(mapImage.packets) + assertEquals(2, packets.size) + } + } + + @Test + fun `should create four packets if the image is between 129x129 and 256x256`() { + fun assertNumberPackets(width: Int, height: Int, expectedNumberPackets: Int) { + val mapImage = MapImage() + mapImage.loadImageAsPackets(BufferedImage(width, height, TYPE_INT_ARGB)) + val packets = assertNotNull(mapImage.packets) + assertEquals(expectedNumberPackets, packets.size) + } + assertNumberPackets(129, 129, 4) + assertNumberPackets(256, 129, 4) + assertNumberPackets(129, 256, 4) + assertNumberPackets(200, 200, 4) + assertNumberPackets(256, 256, 4) + } + + @Test + fun `should have same content than the image for one packet`() { + val mapImage = MapImage() + val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_RGB) + + val colors = listOf( + Color.PINK.rgb to (-110).toByte(), + Color.BLUE.rgb to (49).toByte(), + Color.GREEN.rgb to (-122).toByte() + ) + + for (y in 0 until image.height) { + for (x in 0 until image.width) { + val colorIndex = x % colors.size + image.setRGB(x, y, colors[colorIndex].first) + } + } + + mapImage.loadImageAsPackets(image) + val packets = assertNotNull(mapImage.packets) + assertEquals(1, packets.size) + val packet = packets[0] as MapDataPacket + + val data = packet.colorContent!!.data + assertEquals(128 * 128, data.size) + + for (y in 0 until 128) { + for (x in 0 until 128) { + val colorIndex = x % colors.size + assertEquals(colors[colorIndex].second, data[y * 128 + x]) + } + } + } + + @Test + fun `should have same content than the image for two packets`() { + val mapImage = MapImage() + val image = BufferedImage(256, 128, BufferedImage.TYPE_INT_RGB) + + val colorsFirstFrame = listOf( + Color.PINK.rgb to (-110).toByte(), + Color.BLUE.rgb to (49).toByte(), + Color.GREEN.rgb to (-122).toByte() + ) + + val colorsSecondFrame = listOf( + Color.DARK_GRAY.rgb to (85).toByte(), + Color.YELLOW.rgb to (74).toByte(), + Color.CYAN.rgb to (126).toByte() + ) + + // horizontal stripes + for (y in 0 until image.height) { + for (x in 0 until 128) { + val colorIndex = x % colorsFirstFrame.size + image.setRGB(x, y, colorsFirstFrame[colorIndex].first) + } + } + + // vertical stripes + for (x in 128 until 256) { + for (y in 0 until image.height) { + val colorIndex = x % colorsSecondFrame.size + image.setRGB(x, y, colorsSecondFrame[colorIndex].first) + } + } + + mapImage.loadImageAsPackets(image) + val packets = assertNotNull(mapImage.packets) + assertEquals(2, packets.size) + + val packet1 = packets[0] as MapDataPacket + val data1 = packet1.colorContent!!.data + assertTrue { data1.all { it != BLACK_COLOR_PACKET } } + assertEquals(128 * 128, data1.size) + + for (y in 0 until 128) { + for (x in 0 until image.height) { + val colorIndex = x % colorsFirstFrame.size + assertEquals(colorsFirstFrame[colorIndex].second, data1[y * 128 + x]) + } + } + + val packet2 = packets[1] as MapDataPacket + val data2 = packet2.colorContent!!.data + assertTrue { data2.all { it != BLACK_COLOR_PACKET } } + assertEquals(128 * 128, data2.size) + + for (x in 128 until 256) { + for (y in 0 until image.height) { + val colorIndex = x % colorsSecondFrame.size + assertEquals(colorsSecondFrame[colorIndex].second, data2[y * 128 + (x - 128)]) + } + } + } + + @Test + fun `should apply transformation on packets`() { + val mapImage = MapImage() + val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_RGB) + + val colors = listOf( + Color.PINK.rgb to (-110).toByte(), + Color.BLUE.rgb to (49).toByte(), + Color.GREEN.rgb to (-122).toByte() + ) + + for (y in 0 until image.height) { + for (x in 0 until image.width) { + val colorIndex = x % colors.size + image.setRGB(x, y, colors[colorIndex].first) + } + } + + mapImage.loadImageAsPackets(image) { + rotate(Math.toRadians(90.0), it.width / 2.0, it.height / 2.0) + } + + val packets = assertNotNull(mapImage.packets) + assertEquals(1, packets.size) + val packet = packets[0] as MapDataPacket + + val data = packet.colorContent!!.data + assertEquals(128 * 128, data.size) + + for (x in 0 until 128) { + for (y in 0 until 128) { + // Use Y due to rotation + val colorIndex = y % colors.size + assertEquals(colors[colorIndex].second, data[y * 128 + x]) + } + } + } + + @Test + fun `should load image from resources`() { + val mapImage = MapImage() + val packets = mapImage.loadImageAsPacketsFromResources("map_image.png") + assertImagePacket(packets) + } + + @Test + fun `should load image from input stream`() { + val mapImage = MapImage() + MapImageTest::class.java.getResourceAsStream("/map_image.png")!!.buffered().use { + val packets = mapImage.loadImageAsPacketsFromInputStream(it) + assertImagePacket(packets) + } + } + + private fun assertImagePacket(packets: Array) { + val colors = listOf( + Color(84, 70, 162) to (23).toByte(), + Color(55, 215, 61) to (-122).toByte(), + Color(215, 55, 214) to (66).toByte(), + Color(49, 61, 50) to (84).toByte(), + ) + + assertNotNull(packets) + assertEquals(4, packets.size) + packets.zip(colors).forEach { (packet, color) -> + val data = (packet as MapDataPacket).colorContent!!.data + data.forEach { + assertEquals(color.second, it) + } + } + } + + } + + @Nested + @EnvTest + inner class RemoveItemFrames { + + @Test + fun `should do nothing if no item frames`(env: Env) { + val mapImage = MapImage() + assertNull(mapImage.itemFrames) + mapImage.removeItemFrames() + assertNull(mapImage.itemFrames) + } + + @Test + fun `should remove item frames`(env: Env) = runTest { + val instance = env.createFlatInstance() + val mapImage = MapImage() + mapImage.loadImageAsPackets(BufferedImage(512, 512, BufferedImage.TYPE_INT_RGB)) + mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) + + val frames = assertNotNull(mapImage.itemFrames) + + assertTrue { frames.all { !it.isRemoved } } + mapImage.removeItemFrames() + assertTrue { frames.all { it.isRemoved } } + + assertNull(mapImage.itemFrames) + } + } + + @Test + fun `constant value should be correct`() { + assertEquals(128, MapImage.MAP_ITEM_FRAME_PIXELS) + } +} \ No newline at end of file diff --git a/src/test/resources/map_image.png b/src/test/resources/map_image.png new file mode 100644 index 00000000..c0a4aae4 Binary files /dev/null and b/src/test/resources/map_image.png differ