From 10c281ad27b1a1bca1c67446f66e601553b07b43 Mon Sep 17 00:00:00 2001 From: Distractic Date: Sat, 4 Mar 2023 23:12:27 +0100 Subject: [PATCH 01/20] feat: Add image loader --- .../github/rushyverse/api/image/MapImage.kt | 144 +++++++++++++++ .../rushyverse/api/image/MapImageMath.kt | 167 ++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt create mode 100644 src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt 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..afa84807 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt @@ -0,0 +1,144 @@ +package com.github.rushyverse.api.image + +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 java.awt.geom.AffineTransform +import java.awt.image.BufferedImage +import javax.imageio.ImageIO +import kotlin.properties.Delegates + +/** + * A class that allows you to create an Image as Map Item Frame on the server. + * @property instance The instance where the item frames will be created. + * @property pos The position where the item frames will be created. + * The position is the top left corner of the image. + * @property orientation The orientation of the item frames. + * @property resourceImageName The resource that used to be printed as map. + * @property packets The packets list to send to new players. + * @property blocksPerLine The width blocks size desired for the item frame. The value define the number of item frames by line. + * @property blocksPerColumn 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. + */ +public class MapImage( + public val instance: Instance, + public val pos: Pos, + public val orientation: ItemFrameMeta.Orientation, + private val resourceImageName: String, +) { + + public companion object { + /** + * 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 lateinit var packets: Array + private set + + public var blocksPerLine: Int by Delegates.notNull() + private set + + public var blocksPerColumn: Int by Delegates.notNull() + private set + + private val numberOfItemFrames: Int + get() = blocksPerLine * blocksPerColumn + + /** + * Create the packets list to send to new players. + * The image data are loaded from the resource file [resourceImageName]. + * Item frames are created at the position [pos] with the orientation [orientation] to display the image. + * The result is stored in the [packets] property. + * @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 new players. + */ + public fun buildPackets( + modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} + ): Array { + val inputStream = MapImage::class.java.getResourceAsStream("/$resourceImageName") + ?: error("Unable to retrieve the image $resourceImageName in resources.") + + val image = inputStream.buffered().use { ImageIO.read(it) } + val imageWidth = image.width + val imageHeight = image.height + blocksPerLine = imageWidth shr MAP_ITEM_FRAME_PIXELS_BITWISE + blocksPerColumn = imageHeight shr 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) + } + + createItemFrames(instance, pos, orientation) + 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 { + return Array(numberOfItemFrames) { + val x = it % blocksPerLine + val y = it / blocksPerLine + 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. + * @param instance The instance where you want to create the frame. + * @param pos The position of the frame. + */ + private fun createItemFrames(instance: Instance, pos: Pos, orientation: ItemFrameMeta.Orientation) { + val imageMath = MapImageMath.getFromOrientation(orientation) + val beginX = pos.blockX() + val beginY = pos.blockY() + val beginZ = pos.blockZ() + val yaw = imageMath.yaw + val pitch = imageMath.pitch + + repeat(numberOfItemFrames) { i -> + // We need to calculate the position of the item frame. + // The position is calculated from the top left corner of the image. + // The item frames are place to the right and bottom of the beginning position. + val x = imageMath.computeX(beginX, i, blocksPerLine) + val y = imageMath.computeY(beginY, i, blocksPerLine) + val z = imageMath.computeZ(beginZ, i, blocksPerLine) + + val itemFrame = Entity(EntityType.ITEM_FRAME) + itemFrame.setInstance(instance, Pos(x.toDouble(), y.toDouble(), z.toDouble(), yaw, pitch)) + + with(itemFrame.entityMeta as ItemFrameMeta) { + setNotifyAboutChanges(false) + this.orientation = orientation + isInvisible = true + item = ItemStack.builder(Material.FILLED_MAP).meta(MapMeta::class.java) { it.mapId(i) }.build() + setNotifyAboutChanges(true) + } + } + } +} \ 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..fe48605d --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt @@ -0,0 +1,167 @@ +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 { + /** + * 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 when (orientation) { + ItemFrameMeta.Orientation.DOWN -> Down + ItemFrameMeta.Orientation.UP -> Up + ItemFrameMeta.Orientation.NORTH -> North + ItemFrameMeta.Orientation.SOUTH -> South + ItemFrameMeta.Orientation.WEST -> West + ItemFrameMeta.Orientation.EAST -> East + } + } + } + + 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 blocksPerLine Number of blocks by line. + * @return The x position of the item frame. + */ + public fun computeX(beginX: Int, frameNumber: Int, blocksPerLine: Int): Int + + /** + * Compute the y position of the item frame. + * @param beginY Initial y position. + * @param frameNumber Number of the item frame. + * @param blocksPerLine Number of blocks by line. + * @return The y position of the item frame. + */ + public fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int + + /** + * Compute the z position of the item frame. + * @param beginZ Initial z position. + * @param frameNumber Number of the item frame. + * @param blocksPerLine Number of blocks by line. + * @return The z position of the item frame. + */ + public fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int + + /** + * 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, blocksPerLine: Int): Int = + beginX + (frameNumber % blocksPerLine) + + override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginY + + override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginZ - (frameNumber / blocksPerLine) + } + + /** + * 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, blocksPerLine: Int): Int = + beginX + (frameNumber % blocksPerLine) + + override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginY + + override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginZ + (frameNumber / blocksPerLine) + } + + /** + * 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, blocksPerLine: Int): Int = + beginX - (frameNumber % blocksPerLine) + + override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginY - (frameNumber / blocksPerLine) + + override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: 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, blocksPerLine: Int): Int = + beginX + (frameNumber % blocksPerLine) + + override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginY - (frameNumber / blocksPerLine) + + override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginZ + } + + /** + * 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, blocksPerLine: Int): Int = + beginX + + override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginY - (frameNumber / blocksPerLine) + + override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginZ + (frameNumber % blocksPerLine) + } + + /** + * 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, blocksPerLine: Int): Int = + beginX + + override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginY - (frameNumber / blocksPerLine) + + override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = + beginZ - (frameNumber % blocksPerLine) + } +} \ No newline at end of file From 3db89c4da6b9e138a72f33e76992ae581c463abc Mon Sep 17 00:00:00 2001 From: Distractic Date: Sun, 5 Mar 2023 00:04:25 +0100 Subject: [PATCH 02/20] fix: Await set instance complete --- build.gradle.kts | 7 ++-- .../github/rushyverse/api/image/MapImage.kt | 35 ++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) 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 index afa84807..4f519c13 100644 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt @@ -1,5 +1,8 @@ package com.github.rushyverse.api.image +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.withContext import net.minestom.server.coordinate.Pos import net.minestom.server.entity.Entity import net.minestom.server.entity.EntityType @@ -13,6 +16,7 @@ import net.minestom.server.network.packet.server.SendablePacket import java.awt.geom.AffineTransform import java.awt.image.BufferedImage import javax.imageio.ImageIO +import kotlin.coroutines.CoroutineContext import kotlin.properties.Delegates /** @@ -68,13 +72,16 @@ public class MapImage( * ``` * @return The packets list to send to new players. */ - public fun buildPackets( + public suspend fun buildPackets( + loadImageCoroutineContext: CoroutineContext = Dispatchers.IO, modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} ): Array { - val inputStream = MapImage::class.java.getResourceAsStream("/$resourceImageName") - ?: error("Unable to retrieve the image $resourceImageName in resources.") + val image = withContext(loadImageCoroutineContext) { + val inputStream = MapImage::class.java.getResourceAsStream("/$resourceImageName") + ?: error("Unable to retrieve the image $resourceImageName in resources.") - val image = inputStream.buffered().use { ImageIO.read(it) } + inputStream.buffered().use { ImageIO.read(it) } + } val imageWidth = image.width val imageHeight = image.height blocksPerLine = imageWidth shr MAP_ITEM_FRAME_PIXELS_BITWISE @@ -113,7 +120,7 @@ public class MapImage( * @param instance The instance where you want to create the frame. * @param pos The position of the frame. */ - private fun createItemFrames(instance: Instance, pos: Pos, orientation: ItemFrameMeta.Orientation) { + private suspend fun createItemFrames(instance: Instance, pos: Pos, orientation: ItemFrameMeta.Orientation) { val imageMath = MapImageMath.getFromOrientation(orientation) val beginX = pos.blockX() val beginY = pos.blockY() @@ -121,24 +128,28 @@ public class MapImage( val yaw = imageMath.yaw val pitch = imageMath.pitch - repeat(numberOfItemFrames) { i -> + repeat(numberOfItemFrames) { numberOfFrame -> // We need to calculate the position of the item frame. // The position is calculated from the top left corner of the image. // The item frames are place to the right and bottom of the beginning position. - val x = imageMath.computeX(beginX, i, blocksPerLine) - val y = imageMath.computeY(beginY, i, blocksPerLine) - val z = imageMath.computeZ(beginZ, i, blocksPerLine) + val x = imageMath.computeX(beginX, numberOfFrame, blocksPerLine) + val y = imageMath.computeY(beginY, numberOfFrame, blocksPerLine) + val z = imageMath.computeZ(beginZ, numberOfFrame, blocksPerLine) val itemFrame = Entity(EntityType.ITEM_FRAME) - itemFrame.setInstance(instance, Pos(x.toDouble(), y.toDouble(), z.toDouble(), yaw, pitch)) - with(itemFrame.entityMeta as ItemFrameMeta) { setNotifyAboutChanges(false) + this.orientation = orientation isInvisible = true - item = ItemStack.builder(Material.FILLED_MAP).meta(MapMeta::class.java) { it.mapId(i) }.build() + item = ItemStack.builder(Material.FILLED_MAP) + .meta(MapMeta::class.java) { it.mapId(numberOfFrame) } + .build() + setNotifyAboutChanges(true) } + + itemFrame.setInstance(instance, Pos(x.toDouble(), y.toDouble(), z.toDouble(), yaw, pitch)).await() } } } \ No newline at end of file From fb83a9d42578c55d33594cf56930f983ce1c55f8 Mon Sep 17 00:00:00 2001 From: Distractic Date: Sun, 5 Mar 2023 00:06:33 +0100 Subject: [PATCH 03/20] feat: Add serializer for orientation --- .../ItemFrameMetaOrientationSerializer.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/kotlin/com/github/rushyverse/api/serializer/ItemFrameMetaOrientationSerializer.kt 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 From a343b54c7bc6635e64da4740d9cdb6e9c654d8a0 Mon Sep 17 00:00:00 2001 From: Distractic Date: Sun, 5 Mar 2023 08:57:31 +0100 Subject: [PATCH 04/20] chore: Refactor map image Add the possibility to send input stream as parameter to read image Store the item frames (in case of someone wants to remove them) --- .../github/rushyverse/api/image/MapImage.kt | 130 +++++++++++++----- .../rushyverse/api/image/MapImageMath.kt | 22 +-- 2 files changed, 106 insertions(+), 46 deletions(-) diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt index 4f519c13..2fffb3d8 100644 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt @@ -15,27 +15,73 @@ import net.minestom.server.map.framebuffers.LargeGraphics2DFramebuffer import net.minestom.server.network.packet.server.SendablePacket import java.awt.geom.AffineTransform import java.awt.image.BufferedImage +import java.io.InputStream import javax.imageio.ImageIO import kotlin.coroutines.CoroutineContext import kotlin.properties.Delegates +/** + * Read an image from the resources and build the packets to send to the players. + * @see buildPacketsFromInputStream + * @receiver Object to display image on the server. + * @param resourceImage Path of the image in the resources. + * @param loadImageCoroutineContext Coroutine context to load the image. We recommend to use Dispatchers.IO. + * @param modifyTransform Function to modify the transform of the image. + * @return The packets list to send to players. + */ +public suspend fun MapImage.buildPacketsFromResources( + resourceImage: String, + loadImageCoroutineContext: CoroutineContext = Dispatchers.IO, + modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} +): Array { + val inputStream = withContext(loadImageCoroutineContext) { + MapImage::class.java.getResourceAsStream("/$resourceImage") + ?: error("Unable to retrieve the image $resourceImage in resources.") + } + + return inputStream.buffered().use { buildPacketsFromInputStream(it, loadImageCoroutineContext, 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.buildPacketsFromImage] + * @receiver Object to display image on the server. + * @param inputStream Input stream to retrieve the image's data. + * @param loadImageCoroutineContext Coroutine context to load the image. We recommend to use Dispatchers.IO. + * @param modifyTransform Function to modify the transform of the image. + * @return The packets list to send to players. + */ +public suspend fun MapImage.buildPacketsFromInputStream( + inputStream: InputStream, + loadImageCoroutineContext: CoroutineContext = Dispatchers.IO, + modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} +): Array { + val image = withContext(loadImageCoroutineContext) { + @Suppress("BlockingMethodInNonBlockingContext") + ImageIO.read(inputStream) + } + return buildPacketsFromImage(image, modifyTransform) +} + /** * A class that allows you to create an Image as Map Item Frame on the server. * @property instance The instance where the item frames will be created. * @property pos The position where the item frames will be created. * The position is the top left corner of the image. * @property orientation The orientation of the item frames. - * @property resourceImageName The resource that used to be printed as map. * @property packets The packets list to send to new players. - * @property blocksPerLine The width blocks size desired for the item frame. The value define the number of item frames by line. - * @property blocksPerColumn The height blocks size desired for the item frame. The value define the number of item frames by column. + * @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 isLoaded `true` if the image is loaded, `false` otherwise. + * @property itemFrames The list of item frames created. */ public class MapImage( public val instance: Instance, public val pos: Pos, - public val orientation: ItemFrameMeta.Orientation, - private val resourceImageName: String, + public val orientation: ItemFrameMeta.Orientation ) { public companion object { @@ -49,20 +95,29 @@ public class MapImage( public lateinit var packets: Array private set - public var blocksPerLine: Int by Delegates.notNull() + public var itemFramesPerLine: Int by Delegates.notNull() private set - public var blocksPerColumn: Int by Delegates.notNull() + public var itemFramesPerColumn: Int by Delegates.notNull() private set + public val isLoaded: Boolean + get() = ::packets.isInitialized || ::itemFrames.isInitialized + + public lateinit var itemFrames: List + private val numberOfItemFrames: Int - get() = blocksPerLine * blocksPerColumn + get() = itemFramesPerLine * itemFramesPerColumn /** * Create the packets list to send to new players. * The image data are loaded from the resource file [resourceImageName]. * Item frames are created at the position [pos] with the orientation [orientation] to display the image. * 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 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: * ``` @@ -70,22 +125,18 @@ public class MapImage( * // '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 new players. + * @return The packets list to send to players. */ - public suspend fun buildPackets( - loadImageCoroutineContext: CoroutineContext = Dispatchers.IO, + public suspend fun buildPacketsFromImage( + image: BufferedImage, modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} ): Array { - val image = withContext(loadImageCoroutineContext) { - val inputStream = MapImage::class.java.getResourceAsStream("/$resourceImageName") - ?: error("Unable to retrieve the image $resourceImageName in resources.") + require(!isLoaded) { "An image is already loaded using this instance." } - inputStream.buffered().use { ImageIO.read(it) } - } val imageWidth = image.width val imageHeight = image.height - blocksPerLine = imageWidth shr MAP_ITEM_FRAME_PIXELS_BITWISE - blocksPerColumn = imageHeight shr MAP_ITEM_FRAME_PIXELS_BITWISE + itemFramesPerLine = imageWidth shr MAP_ITEM_FRAME_PIXELS_BITWISE + itemFramesPerColumn = imageHeight shr MAP_ITEM_FRAME_PIXELS_BITWISE val transform = AffineTransform.getScaleInstance(1.0, 1.0).apply { modifyTransform(image) @@ -106,8 +157,8 @@ public class MapImage( */ private fun createPackets(framebuffer: LargeGraphics2DFramebuffer): Array { return Array(numberOfItemFrames) { - val x = it % blocksPerLine - val y = it / blocksPerLine + val x = it % itemFramesPerLine + val y = it / itemFramesPerLine framebuffer.createSubView( x shl MAP_ITEM_FRAME_PIXELS_BITWISE, y shl MAP_ITEM_FRAME_PIXELS_BITWISE @@ -125,31 +176,34 @@ public class MapImage( 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 - repeat(numberOfItemFrames) { numberOfFrame -> + itemFrames = (0.. // We need to calculate the position of the item frame. // The position is calculated from the top left corner of the image. // The item frames are place to the right and bottom of the beginning position. - val x = imageMath.computeX(beginX, numberOfFrame, blocksPerLine) - val y = imageMath.computeY(beginY, numberOfFrame, blocksPerLine) - val z = imageMath.computeZ(beginZ, numberOfFrame, blocksPerLine) - - val itemFrame = Entity(EntityType.ITEM_FRAME) - with(itemFrame.entityMeta as ItemFrameMeta) { - setNotifyAboutChanges(false) - - this.orientation = orientation - isInvisible = true - item = ItemStack.builder(Material.FILLED_MAP) - .meta(MapMeta::class.java) { it.mapId(numberOfFrame) } - .build() - - setNotifyAboutChanges(true) - } + val x = imageMath.computeX(beginX, numberOfFrame, itemFramesPerLine) + val y = imageMath.computeY(beginY, numberOfFrame, itemFramesPerLine) + val z = imageMath.computeZ(beginZ, numberOfFrame, itemFramesPerLine) - itemFrame.setInstance(instance, Pos(x.toDouble(), y.toDouble(), z.toDouble(), yaw, pitch)).await() + Entity(EntityType.ITEM_FRAME).apply { + with(entityMeta as ItemFrameMeta) { + setNotifyAboutChanges(false) + + this.orientation = orientation + isInvisible = true + item = ItemStack.builder(Material.FILLED_MAP) + .meta(MapMeta::class.java) { it.mapId(numberOfFrame) } + .build() + + setNotifyAboutChanges(true) + } + + setInstance(instance, Pos(x.toDouble(), y.toDouble(), z.toDouble(), yaw, pitch)).await() + } } } } \ 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 index fe48605d..714c26d7 100644 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt @@ -16,20 +16,26 @@ import net.minestom.server.entity.metadata.other.ItemFrameMeta 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 when (orientation) { - ItemFrameMeta.Orientation.DOWN -> Down - ItemFrameMeta.Orientation.UP -> Up - ItemFrameMeta.Orientation.NORTH -> North - ItemFrameMeta.Orientation.SOUTH -> South - ItemFrameMeta.Orientation.WEST -> West - ItemFrameMeta.Orientation.EAST -> East - } + return orientations[orientation] ?: throw IllegalArgumentException("Unsupported orientation: $orientation") } } From e245603e6bd034d00f7fea81f419280787794506 Mon Sep 17 00:00:00 2001 From: Distractic Date: Sun, 5 Mar 2023 09:20:23 +0100 Subject: [PATCH 05/20] chore: Change variable name --- .../rushyverse/api/image/MapImageMath.kt | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt index 714c26d7..1a5dabdb 100644 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt @@ -46,28 +46,28 @@ public sealed interface MapImageMath { * Compute the x position of the item frame. * @param beginX Initial x position. * @param frameNumber Number of the item frame. - * @param blocksPerLine Number of blocks by line. + * @param itemFramesPerLine Number of blocks by line. * @return The x position of the item frame. */ - public fun computeX(beginX: Int, frameNumber: Int, blocksPerLine: Int): Int + 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 blocksPerLine Number of blocks by line. + * @param itemFramesPerLine Number of blocks by line. * @return The y position of the item frame. */ - public fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int + 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 blocksPerLine Number of blocks by line. + * @param itemFramesPerLine Number of blocks by line. * @return The z position of the item frame. */ - public fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int + 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.DOWN]. @@ -76,14 +76,14 @@ public sealed interface MapImageMath { override val yaw: Float = 0f override val pitch: Float = 90f - override fun computeX(beginX: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginX + (frameNumber % blocksPerLine) + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginX + (frameNumber % itemFramesPerLine) - override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = beginY - override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginZ - (frameNumber / blocksPerLine) + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginZ - (frameNumber / itemFramesPerLine) } /** @@ -93,14 +93,14 @@ public sealed interface MapImageMath { override val yaw: Float = 0f override val pitch: Float = 270f - override fun computeX(beginX: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginX + (frameNumber % blocksPerLine) + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginX + (frameNumber % itemFramesPerLine) - override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = beginY - override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginZ + (frameNumber / blocksPerLine) + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginZ + (frameNumber / itemFramesPerLine) } /** @@ -110,13 +110,13 @@ public sealed interface MapImageMath { override val yaw: Float = 180f override val pitch: Float = 0f - override fun computeX(beginX: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginX - (frameNumber % blocksPerLine) + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginX - (frameNumber % itemFramesPerLine) - override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginY - (frameNumber / blocksPerLine) + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginY - (frameNumber / itemFramesPerLine) - override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = beginZ } @@ -127,13 +127,13 @@ public sealed interface MapImageMath { override val yaw: Float = 0f override val pitch: Float = 0f - override fun computeX(beginX: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginX + (frameNumber % blocksPerLine) + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginX + (frameNumber % itemFramesPerLine) - override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginY - (frameNumber / blocksPerLine) + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginY - (frameNumber / itemFramesPerLine) - override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = beginZ } @@ -144,14 +144,14 @@ public sealed interface MapImageMath { override val yaw: Float = 90f override val pitch: Float = 0f - override fun computeX(beginX: Int, frameNumber: Int, blocksPerLine: Int): Int = + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = beginX - override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginY - (frameNumber / blocksPerLine) + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginY - (frameNumber / itemFramesPerLine) - override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginZ + (frameNumber % blocksPerLine) + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginZ + (frameNumber % itemFramesPerLine) } /** @@ -161,13 +161,13 @@ public sealed interface MapImageMath { override val yaw: Float = 270f override val pitch: Float = 0f - override fun computeX(beginX: Int, frameNumber: Int, blocksPerLine: Int): Int = + override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = beginX - override fun computeY(beginY: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginY - (frameNumber / blocksPerLine) + override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginY - (frameNumber / itemFramesPerLine) - override fun computeZ(beginZ: Int, frameNumber: Int, blocksPerLine: Int): Int = - beginZ - (frameNumber % blocksPerLine) + override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = + beginZ - (frameNumber % itemFramesPerLine) } } \ No newline at end of file From 9397d9d881b4b4f0fc70e8a99f6ca5103fd9eff0 Mon Sep 17 00:00:00 2001 From: Distractic Date: Sun, 5 Mar 2023 09:20:39 +0100 Subject: [PATCH 06/20] tests: Begin test for image math --- .../rushyverse/api/image/MapImageMathTest.kt | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt 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..5433e12a --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt @@ -0,0 +1,145 @@ +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 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + 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)) + } + + } + } +} \ No newline at end of file From 0489f86985e4efeff262da9224858ac9281dbd3e Mon Sep 17 00:00:00 2001 From: Distractic Date: Sun, 5 Mar 2023 10:05:37 +0100 Subject: [PATCH 07/20] tests: Add tests for MapImageMath orientation --- .../rushyverse/api/image/MapImageMathTest.kt | 663 +++++++++++++++++- 1 file changed, 662 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt index 5433e12a..abd40b28 100644 --- a/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt @@ -83,7 +83,7 @@ class MapImageMathTest { } @ParameterizedTest - @ValueSource(ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + @ValueSource(ints = [-1, 0, 1]) fun `should stay on the same y`(beginY: Int) { repeat(10) { assertEquals(0, instance.computeY(0, it, 5)) @@ -142,4 +142,665 @@ class MapImageMathTest { } } + + @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 From 9e68502cf9878bd4875fb4010c332b75a9bc2f07 Mon Sep 17 00:00:00 2001 From: Distractic Date: Sun, 5 Mar 2023 10:05:59 +0100 Subject: [PATCH 08/20] chore: Change order classes --- .../rushyverse/api/image/MapImageMath.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt index 1a5dabdb..c18cded3 100644 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt @@ -70,11 +70,11 @@ public sealed interface MapImageMath { 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.DOWN]. + * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.UP]. */ - public object Down : MapImageMath { + public object Up : MapImageMath { override val yaw: Float = 0f - override val pitch: Float = 90f + override val pitch: Float = 270f override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = beginX + (frameNumber % itemFramesPerLine) @@ -83,15 +83,15 @@ public sealed interface MapImageMath { beginY override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginZ - (frameNumber / itemFramesPerLine) + beginZ + (frameNumber / itemFramesPerLine) } /** - * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.UP]. + * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.DOWN]. */ - public object Up : MapImageMath { + public object Down : MapImageMath { override val yaw: Float = 0f - override val pitch: Float = 270f + override val pitch: Float = 90f override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = beginX + (frameNumber % itemFramesPerLine) @@ -100,7 +100,7 @@ public sealed interface MapImageMath { beginY override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginZ + (frameNumber / itemFramesPerLine) + beginZ - (frameNumber / itemFramesPerLine) } /** @@ -138,10 +138,10 @@ public sealed interface MapImageMath { } /** - * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.WEST]. + * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.EAST]. */ - public object West : MapImageMath { - override val yaw: Float = 90f + public object East : MapImageMath { + override val yaw: Float = 270f override val pitch: Float = 0f override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = @@ -151,14 +151,14 @@ public sealed interface MapImageMath { beginY - (frameNumber / itemFramesPerLine) override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginZ + (frameNumber % itemFramesPerLine) + beginZ - (frameNumber % itemFramesPerLine) } /** - * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.EAST]. + * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.WEST]. */ - public object East : MapImageMath { - override val yaw: Float = 270f + public object West : MapImageMath { + override val yaw: Float = 90f override val pitch: Float = 0f override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = @@ -168,6 +168,6 @@ public sealed interface MapImageMath { beginY - (frameNumber / itemFramesPerLine) override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginZ - (frameNumber % itemFramesPerLine) + beginZ + (frameNumber % itemFramesPerLine) } } \ No newline at end of file From cd862e259daffb2d1512d9d4f53cddbebc825f21 Mon Sep 17 00:00:00 2001 From: Distractic Date: Wed, 8 Mar 2023 08:41:54 +0100 Subject: [PATCH 09/20] fix: Allows smaller images --- .../github/rushyverse/api/image/MapImage.kt | 71 ++++++++++++++----- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt index 2fffb3d8..2b2425c1 100644 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt @@ -67,10 +67,6 @@ public suspend fun MapImage.buildPacketsFromInputStream( /** * A class that allows you to create an Image as Map Item Frame on the server. - * @property instance The instance where the item frames will be created. - * @property pos The position where the item frames will be created. - * The position is the top left corner of the image. - * @property orientation The orientation of the item frames. * @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. @@ -78,13 +74,15 @@ public suspend fun MapImage.buildPacketsFromInputStream( * @property isLoaded `true` if the image is loaded, `false` otherwise. * @property itemFrames The list of item frames created. */ -public class MapImage( - public val instance: Instance, - public val pos: Pos, - public val orientation: ItemFrameMeta.Orientation -) { +public class MapImage { public companion object { + + /** + * The number of pixels per item frame is 128x128. + */ + private const val MAP_ITEM_FRAME_PIXELS = 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. @@ -102,9 +100,9 @@ public class MapImage( private set public val isLoaded: Boolean - get() = ::packets.isInitialized || ::itemFrames.isInitialized + get() = ::packets.isInitialized - public lateinit var itemFrames: List + public var itemFrames: List? = null private val numberOfItemFrames: Int get() = itemFramesPerLine * itemFramesPerColumn @@ -127,7 +125,7 @@ public class MapImage( * ``` * @return The packets list to send to players. */ - public suspend fun buildPacketsFromImage( + public fun buildPacketsFromImage( image: BufferedImage, modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} ): Array { @@ -135,8 +133,13 @@ public class MapImage( val imageWidth = image.width val imageHeight = image.height - itemFramesPerLine = imageWidth shr MAP_ITEM_FRAME_PIXELS_BITWISE - itemFramesPerColumn = imageHeight shr MAP_ITEM_FRAME_PIXELS_BITWISE + // 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) @@ -146,7 +149,6 @@ public class MapImage( renderer.drawRenderedImage(image, transform) } - createItemFrames(instance, pos, orientation) return createPackets(framebuffer).also { packets = it } } @@ -170,8 +172,22 @@ public class MapImage( * Create necessary item frames on which the image will be displayed. * @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. */ - private suspend fun createItemFrames(instance: Instance, pos: Pos, orientation: ItemFrameMeta.Orientation) { + public suspend fun createItemFrames( + instance: Instance, + pos: Pos, + orientation: ItemFrameMeta.Orientation, + metaModifier: ItemFrameMeta.() -> Unit = { + isInvisible = true + } + ) { + require(!atLeastOneItemFrameIsPresent()) { "The item frames are already created." } + if(numberOfItemFrames == 0) { + itemFrames = emptyList() + return + } + val imageMath = MapImageMath.getFromOrientation(orientation) val beginX = pos.blockX() val beginY = pos.blockY() @@ -193,12 +209,13 @@ public class MapImage( with(entityMeta as ItemFrameMeta) { setNotifyAboutChanges(false) - this.orientation = orientation - isInvisible = true item = ItemStack.builder(Material.FILLED_MAP) .meta(MapMeta::class.java) { it.mapId(numberOfFrame) } .build() + this.orientation = orientation + metaModifier() + setNotifyAboutChanges(true) } @@ -206,4 +223,22 @@ public class MapImage( } } } + + /** + * 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) + this.itemFrames = null + } + + /** + * Check if at least one item frame is present in the [instance]. + * @return `true` if at least one item frame is present, `false` otherwise. + */ + private fun atLeastOneItemFrameIsPresent() = itemFrames?.any { it.isRemoved } == false } \ No newline at end of file From 9a88c586cfbf847831f37db885d95b9121f5ec7f Mon Sep 17 00:00:00 2001 From: Distractic Date: Wed, 8 Mar 2023 08:42:07 +0100 Subject: [PATCH 10/20] tests: Begin test about map image --- .../rushyverse/api/image/MapImageTest.kt | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt 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..beb55247 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt @@ -0,0 +1,302 @@ +package com.github.rushyverse.api.image + +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.utils.Rotation +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Nested +import java.awt.image.BufferedImage +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@EnvTest +class MapImageTest { + + @Nested + inner class CreateItemFrames { + + @Test + fun `should create one item frame if image is 128x128 max`(env: Env) = runTest { + val instance = env.createFlatInstance() + (1..128).forEach { width -> + val mapImage = MapImage() + mapImage.buildPacketsFromImage(BufferedImage(width, width, BufferedImage.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.buildPacketsFromImage(BufferedImage(129, 128, BufferedImage.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.buildPacketsFromImage(BufferedImage(128, 129, BufferedImage.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.buildPacketsFromImage(BufferedImage(128, 128, BufferedImage.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 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, BufferedImage.TYPE_INT_ARGB) + + repeat(10) { x -> + repeat(10) { y -> + repeat(10) { z -> + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + + ItemFrameMeta.Orientation.values().forEach { orientation -> + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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, BufferedImage.TYPE_INT_ARGB) + + val mapImage = MapImage() + mapImage.buildPacketsFromImage(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) + } + } + + } + } +} \ No newline at end of file From d3c074dfe50b4c1c8566631bbf033901db1c2ce7 Mon Sep 17 00:00:00 2001 From: Distractic Date: Wed, 8 Mar 2023 20:09:52 +0100 Subject: [PATCH 11/20] fix: Add security and remove coroutine argument Add security when try to load entities Remove coroutineContext to load resource (add blocking annotation instead) --- .../github/rushyverse/api/image/MapImage.kt | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt index 2b2425c1..d173a290 100644 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt @@ -1,8 +1,6 @@ package com.github.rushyverse.api.image -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.future.await -import kotlinx.coroutines.withContext import net.minestom.server.coordinate.Pos import net.minestom.server.entity.Entity import net.minestom.server.entity.EntityType @@ -13,56 +11,49 @@ 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.coroutines.CoroutineContext import kotlin.properties.Delegates /** * Read an image from the resources and build the packets to send to the players. - * @see buildPacketsFromInputStream + * @see loadImageAsPacketsFromInputStream * @receiver Object to display image on the server. * @param resourceImage Path of the image in the resources. - * @param loadImageCoroutineContext Coroutine context to load the image. We recommend to use Dispatchers.IO. * @param modifyTransform Function to modify the transform of the image. * @return The packets list to send to players. */ -public suspend fun MapImage.buildPacketsFromResources( +@Blocking +public fun MapImage.loadImageAsPacketsFromResources( resourceImage: String, - loadImageCoroutineContext: CoroutineContext = Dispatchers.IO, modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} ): Array { - val inputStream = withContext(loadImageCoroutineContext) { - MapImage::class.java.getResourceAsStream("/$resourceImage") - ?: error("Unable to retrieve the image $resourceImage in resources.") - } + val inputStream = MapImage::class.java.getResourceAsStream("/$resourceImage") + ?: error("Unable to retrieve the image $resourceImage in resources.") - return inputStream.buffered().use { buildPacketsFromInputStream(it, loadImageCoroutineContext, modifyTransform) } + 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.buildPacketsFromImage] + * @see [MapImage.loadImageAsPackets] * @receiver Object to display image on the server. * @param inputStream Input stream to retrieve the image's data. - * @param loadImageCoroutineContext Coroutine context to load the image. We recommend to use Dispatchers.IO. * @param modifyTransform Function to modify the transform of the image. * @return The packets list to send to players. */ -public suspend fun MapImage.buildPacketsFromInputStream( +@Blocking +public fun MapImage.loadImageAsPacketsFromInputStream( inputStream: InputStream, - loadImageCoroutineContext: CoroutineContext = Dispatchers.IO, modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} ): Array { - val image = withContext(loadImageCoroutineContext) { - @Suppress("BlockingMethodInNonBlockingContext") - ImageIO.read(inputStream) - } - return buildPacketsFromImage(image, modifyTransform) + val image = ImageIO.read(inputStream) + return loadImageAsPackets(image, modifyTransform) } /** @@ -71,7 +62,7 @@ public suspend fun MapImage.buildPacketsFromInputStream( * @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 isLoaded `true` if the image is loaded, `false` otherwise. + * @property imageLoaded `true` if the image is loaded, `false` otherwise. * @property itemFrames The list of item frames created. */ public class MapImage { @@ -90,7 +81,7 @@ public class MapImage { private const val MAP_ITEM_FRAME_PIXELS_BITWISE = 7 } - public lateinit var packets: Array + public var packets: Array? = null private set public var itemFramesPerLine: Int by Delegates.notNull() @@ -99,18 +90,19 @@ public class MapImage { public var itemFramesPerColumn: Int by Delegates.notNull() private set - public val isLoaded: Boolean - get() = ::packets.isInitialized + public val imageLoaded: Boolean + get() = packets != null - public var itemFrames: List? = 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 image data are loaded from the resource file [resourceImageName]. - * Item frames are created at the position [pos] with the orientation [orientation] to display the image. * The result is stored in the [packets] property. * * **This method does not close the provided [inputStream] after the read operation has completed. @@ -125,11 +117,11 @@ public class MapImage { * ``` * @return The packets list to send to players. */ - public fun buildPacketsFromImage( + public fun loadImageAsPackets( image: BufferedImage, modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} ): Array { - require(!isLoaded) { "An image is already loaded using this instance." } + require(!imageLoaded) { "An image is already loaded using this instance." } val imageWidth = image.width val imageHeight = image.height @@ -158,6 +150,7 @@ public class MapImage { * @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 @@ -170,9 +163,12 @@ public class MapImage { /** * 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, @@ -182,9 +178,10 @@ public class MapImage { isInvisible = true } ) { + require(imageLoaded) { "An image must be loaded before creating the item frames." } require(!atLeastOneItemFrameIsPresent()) { "The item frames are already created." } - if(numberOfItemFrames == 0) { - itemFrames = emptyList() + if (numberOfItemFrames == 0) { + _itemFrames = emptyList() return } @@ -197,7 +194,7 @@ public class MapImage { val yaw = imageMath.yaw val pitch = imageMath.pitch - itemFrames = (0.. + _itemFrames = (0.. // We need to calculate the position of the item frame. // The position is calculated from the top left corner of the image. // The item frames are place to the right and bottom of the beginning position. @@ -233,11 +230,12 @@ public class MapImage { public fun removeItemFrames() { val itemFrames = itemFrames ?: return itemFrames.forEach(Entity::remove) - this.itemFrames = null + _itemFrames = null } /** - * Check if at least one item frame is present in the [instance]. + * 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() = itemFrames?.any { it.isRemoved } == false From 0ce872901223ef1a8247f17ed1b94e096707f132 Mon Sep 17 00:00:00 2001 From: Distractic Date: Wed, 8 Mar 2023 20:10:14 +0100 Subject: [PATCH 12/20] chore: Change method name --- .../rushyverse/api/image/MapImageTest.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt index beb55247..12470400 100644 --- a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt @@ -25,7 +25,7 @@ class MapImageTest { val instance = env.createFlatInstance() (1..128).forEach { width -> val mapImage = MapImage() - mapImage.buildPacketsFromImage(BufferedImage(width, width, BufferedImage.TYPE_INT_ARGB)) + mapImage.loadImageAsPackets(BufferedImage(width, width, BufferedImage.TYPE_INT_ARGB)) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) val itemFrames = mapImage.itemFrames @@ -39,7 +39,7 @@ class MapImageTest { val instance = env.createFlatInstance() val mapImage = MapImage() val orientation = ItemFrameMeta.Orientation.NORTH - mapImage.buildPacketsFromImage(BufferedImage(129, 128, BufferedImage.TYPE_INT_ARGB)) + mapImage.loadImageAsPackets(BufferedImage(129, 128, BufferedImage.TYPE_INT_ARGB)) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) val math = MapImageMath.getFromOrientation(orientation) @@ -57,7 +57,7 @@ class MapImageTest { val instance = env.createFlatInstance() val mapImage = MapImage() val orientation = ItemFrameMeta.Orientation.NORTH - mapImage.buildPacketsFromImage(BufferedImage(128, 129, BufferedImage.TYPE_INT_ARGB)) + mapImage.loadImageAsPackets(BufferedImage(128, 129, BufferedImage.TYPE_INT_ARGB)) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) val math = MapImageMath.getFromOrientation(orientation) @@ -74,7 +74,7 @@ class MapImageTest { fun `should spawn item frame at the target instance`(env: Env) = runTest { val instance = env.createFlatInstance() val mapImage = MapImage() - mapImage.buildPacketsFromImage(BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB)) + mapImage.loadImageAsPackets(BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB)) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) assertEquals(instance, mapImage.itemFrames!!.first().instance) } @@ -93,7 +93,7 @@ class MapImageTest { repeat(10) { y -> repeat(10) { z -> val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + mapImage.loadImageAsPackets(image) mapImage.createItemFrames( instance, Pos(x.toDouble(), y.toDouble(), z.toDouble()), @@ -115,7 +115,7 @@ class MapImageTest { val math = MapImageMath.getFromOrientation(orientation) val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + mapImage.loadImageAsPackets(image) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) val itemFrames = mapImage.itemFrames!! @@ -132,7 +132,7 @@ class MapImageTest { val math = MapImageMath.getFromOrientation(orientation) val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + mapImage.loadImageAsPackets(image) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) val itemFrames = mapImage.itemFrames!! @@ -149,7 +149,7 @@ class MapImageTest { val math = MapImageMath.getFromOrientation(orientation) val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + mapImage.loadImageAsPackets(image) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) val itemFrames = mapImage.itemFrames!! @@ -166,7 +166,7 @@ class MapImageTest { val math = MapImageMath.getFromOrientation(orientation) val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + mapImage.loadImageAsPackets(image) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) val itemFrames = mapImage.itemFrames!! @@ -183,7 +183,7 @@ class MapImageTest { val math = MapImageMath.getFromOrientation(orientation) val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + mapImage.loadImageAsPackets(image) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) val itemFrames = mapImage.itemFrames!! @@ -200,7 +200,7 @@ class MapImageTest { val math = MapImageMath.getFromOrientation(orientation) val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + mapImage.loadImageAsPackets(image) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) val itemFrames = mapImage.itemFrames!! @@ -219,7 +219,7 @@ class MapImageTest { val instance = env.createFlatInstance() val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB) val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + mapImage.loadImageAsPackets(image) val invisible = false val rotation = Rotation.values().random() @@ -241,7 +241,7 @@ class MapImageTest { ItemFrameMeta.Orientation.values().forEach { orientation -> val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + 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) @@ -254,7 +254,7 @@ class MapImageTest { val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB) val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + 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) @@ -266,7 +266,7 @@ class MapImageTest { val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB) val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + 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 @@ -282,7 +282,7 @@ class MapImageTest { val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) val mapImage = MapImage() - mapImage.buildPacketsFromImage(image) + mapImage.loadImageAsPackets(image) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) val itemFrames = mapImage.itemFrames From 068a125d1d48343fff0780c4f1bac01410c699a1 Mon Sep 17 00:00:00 2001 From: Distractic Date: Wed, 8 Mar 2023 21:40:13 +0100 Subject: [PATCH 13/20] chore: Throws specific exception --- .../github/rushyverse/api/image/MapImage.kt | 31 +++++++++++++------ .../exception/ImageNotLoadedException.kt | 16 ++++++++++ .../ItemFramesAlreadyExistException.kt | 6 ++++ 3 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/com/github/rushyverse/api/image/exception/ImageNotLoadedException.kt create mode 100644 src/main/kotlin/com/github/rushyverse/api/image/exception/ItemFramesAlreadyExistException.kt diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt index d173a290..d89a57bc 100644 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt @@ -1,5 +1,8 @@ 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.future.await import net.minestom.server.coordinate.Pos import net.minestom.server.entity.Entity @@ -72,7 +75,7 @@ public class MapImage { /** * The number of pixels per item frame is 128x128. */ - private const val MAP_ITEM_FRAME_PIXELS = 128 + public const val MAP_ITEM_FRAME_PIXELS: Int = 128 /** * The number of pixels per item frame is 128. @@ -121,7 +124,9 @@ public class MapImage { image: BufferedImage, modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} ): Array { - require(!imageLoaded) { "An image is already loaded using this instance." } + if (imageLoaded) { + throw ImageAlreadyLoadedException("An image is already loaded using this instance.") + } val imageWidth = image.width val imageHeight = image.height @@ -177,12 +182,16 @@ public class MapImage { metaModifier: ItemFrameMeta.() -> Unit = { isInvisible = true } - ) { - require(imageLoaded) { "An image must be loaded before creating the item frames." } - require(!atLeastOneItemFrameIsPresent()) { "The item frames are already created." } + ): 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) { - _itemFrames = emptyList() - return + return emptyList().also { _itemFrames = it } } val imageMath = MapImageMath.getFromOrientation(orientation) @@ -194,7 +203,7 @@ public class MapImage { val yaw = imageMath.yaw val pitch = imageMath.pitch - _itemFrames = (0.. + return (0.. // We need to calculate the position of the item frame. // The position is calculated from the top left corner of the image. // The item frames are place to the right and bottom of the beginning position. @@ -218,7 +227,7 @@ public class MapImage { setInstance(instance, Pos(x.toDouble(), y.toDouble(), z.toDouble(), yaw, pitch)).await() } - } + }.also { _itemFrames = it } } /** @@ -238,5 +247,7 @@ public class MapImage { * 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() = itemFrames?.any { it.isRemoved } == false + 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/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 From 24f927343c52bdc2f2e96aecb0e15e3312c7336e Mon Sep 17 00:00:00 2001 From: Distractic Date: Wed, 8 Mar 2023 21:40:36 +0100 Subject: [PATCH 14/20] tests: Add tests for create item frame --- .../rushyverse/api/image/MapImageTest.kt | 134 +++++++++++++++--- 1 file changed, 113 insertions(+), 21 deletions(-) diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt index 12470400..03398d9d 100644 --- a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt @@ -1,5 +1,9 @@ package com.github.rushyverse.api.image +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 @@ -9,23 +13,51 @@ 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.image.BufferedImage -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull +import java.awt.image.BufferedImage.TYPE_INT_ARGB +import kotlin.test.* -@EnvTest class MapImageTest { @Nested + @EnvTest inner class CreateItemFrames { @Test - fun `should create one item frame if image is 128x128 max`(env: Env) = runTest { + 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, BufferedImage.TYPE_INT_ARGB)) + 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 @@ -39,7 +71,7 @@ class MapImageTest { val instance = env.createFlatInstance() val mapImage = MapImage() val orientation = ItemFrameMeta.Orientation.NORTH - mapImage.loadImageAsPackets(BufferedImage(129, 128, BufferedImage.TYPE_INT_ARGB)) + mapImage.loadImageAsPackets(BufferedImage(129, 128, TYPE_INT_ARGB)) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) val math = MapImageMath.getFromOrientation(orientation) @@ -57,7 +89,7 @@ class MapImageTest { val instance = env.createFlatInstance() val mapImage = MapImage() val orientation = ItemFrameMeta.Orientation.NORTH - mapImage.loadImageAsPackets(BufferedImage(128, 129, BufferedImage.TYPE_INT_ARGB)) + mapImage.loadImageAsPackets(BufferedImage(128, 129, TYPE_INT_ARGB)) mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) val math = MapImageMath.getFromOrientation(orientation) @@ -74,7 +106,7 @@ class MapImageTest { fun `should spawn item frame at the target instance`(env: Env) = runTest { val instance = env.createFlatInstance() val mapImage = MapImage() - mapImage.loadImageAsPackets(BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB)) + 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) } @@ -87,7 +119,7 @@ class MapImageTest { val instance = env.createFlatInstance() val orientation = ItemFrameMeta.Orientation.NORTH val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB) + val image = BufferedImage(128, 128, TYPE_INT_ARGB) repeat(10) { x -> repeat(10) { y -> @@ -113,7 +145,7 @@ class MapImageTest { val instance = env.createFlatInstance() val orientation = ItemFrameMeta.Orientation.NORTH val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) + 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) @@ -130,7 +162,7 @@ class MapImageTest { val instance = env.createFlatInstance() val orientation = ItemFrameMeta.Orientation.EAST val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) + 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) @@ -147,7 +179,7 @@ class MapImageTest { val instance = env.createFlatInstance() val orientation = ItemFrameMeta.Orientation.SOUTH val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) + 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) @@ -164,7 +196,7 @@ class MapImageTest { val instance = env.createFlatInstance() val orientation = ItemFrameMeta.Orientation.WEST val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) + 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) @@ -181,7 +213,7 @@ class MapImageTest { val instance = env.createFlatInstance() val orientation = ItemFrameMeta.Orientation.UP val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) + 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) @@ -198,7 +230,7 @@ class MapImageTest { val instance = env.createFlatInstance() val orientation = ItemFrameMeta.Orientation.DOWN val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) + 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) @@ -217,7 +249,7 @@ class MapImageTest { @Test fun `should custom meta of item frame if needed`(env: Env) = runTest { val instance = env.createFlatInstance() - val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB) + val image = BufferedImage(128, 128, TYPE_INT_ARGB) val mapImage = MapImage() mapImage.loadImageAsPackets(image) @@ -237,7 +269,7 @@ class MapImageTest { @Test fun `should spawn item frame with the target orientation`(env: Env) = runTest { val instance = env.createFlatInstance() - val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB) + val image = BufferedImage(128, 128, TYPE_INT_ARGB) ItemFrameMeta.Orientation.values().forEach { orientation -> val mapImage = MapImage() @@ -251,7 +283,7 @@ class MapImageTest { @Test fun `should spawn item frame with invisibility by default`(env: Env) = runTest { val instance = env.createFlatInstance() - val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB) + val image = BufferedImage(128, 128, TYPE_INT_ARGB) val mapImage = MapImage() mapImage.loadImageAsPackets(image) @@ -263,7 +295,7 @@ class MapImageTest { @Test fun `should spawn item frame with map item in meta`(env: Env) = runTest { val instance = env.createFlatInstance() - val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB) + val image = BufferedImage(128, 128, TYPE_INT_ARGB) val mapImage = MapImage() mapImage.loadImageAsPackets(image) @@ -279,7 +311,7 @@ class MapImageTest { @Test fun `should spawn item frame with map id`(env: Env) = runTest { val instance = env.createFlatInstance() - val image = BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB) + val image = BufferedImage(256, 256, TYPE_INT_ARGB) val mapImage = MapImage() mapImage.loadImageAsPackets(image) @@ -298,5 +330,65 @@ class MapImageTest { } } + + @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 `constant value should be correct`() { + assertEquals(128, MapImage.MAP_ITEM_FRAME_PIXELS) } } \ No newline at end of file From 8df874e8f36d88e8fcd1e14e7aae59508f876d16 Mon Sep 17 00:00:00 2001 From: Distractic Date: Wed, 8 Mar 2023 23:01:24 +0100 Subject: [PATCH 15/20] tests: Prepare tests for loadImage --- .../rushyverse/api/image/MapImageTest.kt | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt index 03398d9d..d8b6b7dd 100644 --- a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt @@ -387,6 +387,77 @@ class MapImageTest { } } + @Nested + @EnvTest + inner class LoadImageAsPackets { + + @Nested + inner class ItemFramesPerLine { + + @Test + fun `should set property according to the width`(env: Env) { + TODO() + } + + } + + @Nested + inner class ItemFramesPerColumn { + + @Test + fun `should set property according to the height`(env: Env) { + TODO() + } + + } + + @Test + fun `should throw exception if an image is already loaded`(env: Env) { + TODO() + } + + @Test + fun `should create one packet if the image is between 1x1 and 128x128`(env: Env) { + TODO() + } + + @Test + fun `should create two packets if the image width is between 129 and 256`(env: Env) { + TODO() + } + + @Test + fun `should create two packets if the image height is between 129 and 256`(env: Env) { + TODO() + } + + @Test + fun `should create four packets if the image is between 129x129 and 256x256`(env: Env) { + TODO() + } + + @Test + fun `should have same content than the image for one packet`(env: Env) { + TODO() + } + + @Test + fun `should have same content than the image for two packets`(env: Env) { + TODO() + } + + @Test + fun `should have same content than the image for four packets`(env: Env) { + TODO() + } + + @Test + fun `should apply transformation on packets`(env: Env) { + TODO() + } + + } + @Test fun `constant value should be correct`() { assertEquals(128, MapImage.MAP_ITEM_FRAME_PIXELS) From 8c6fda2ed503fea67f5ad3bf5cb66cec426dd6a8 Mon Sep 17 00:00:00 2001 From: Distractic Date: Thu, 9 Mar 2023 18:51:03 +0100 Subject: [PATCH 16/20] tests: Begin test for load image --- .../rushyverse/api/image/MapImageTest.kt | 292 +++++++++++------- 1 file changed, 188 insertions(+), 104 deletions(-) diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt index d8b6b7dd..df6a4200 100644 --- a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt @@ -1,5 +1,6 @@ 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 @@ -9,6 +10,7 @@ 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.play.MapDataPacket import net.minestom.server.utils.Rotation import net.minestom.testing.Env import net.minestom.testing.EnvTest @@ -24,93 +26,6 @@ class MapImageTest { @EnvTest inner class CreateItemFrames { - @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 Position { @@ -385,18 +300,119 @@ class MapImageTest { } } + + @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 - @EnvTest inner class LoadImageAsPackets { @Nested inner class ItemFramesPerLine { @Test - fun `should set property according to the width`(env: Env) { - TODO() + 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) } } @@ -405,54 +421,122 @@ class MapImageTest { inner class ItemFramesPerColumn { @Test - fun `should set property according to the height`(env: Env) { - TODO() + 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`(env: Env) { - TODO() + 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 create one packet if the image is between 1x1 and 128x128`(env: Env) { - TODO() + 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 two packets if the image width is between 129 and 256`(env: Env) { - TODO() + 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 create two packets if the image height is between 129 and 256`(env: Env) { + fun `should have same content than the image for one packet`() { TODO() } @Test - fun `should create four packets if the image is between 129x129 and 256x256`(env: Env) { + fun `should have same content than the image for two packets`() { TODO() } @Test - fun `should have same content than the image for one packet`(env: Env) { + fun `should have same content than the image for four packets`() { TODO() } @Test - fun `should have same content than the image for two packets`(env: Env) { + fun `should apply transformation on packets`() { TODO() } @Test - fun `should have same content than the image for four packets`(env: Env) { + fun `should load image from resources`() { TODO() } @Test - fun `should apply transformation on packets`(env: Env) { + fun `should load image from inputstream`() { TODO() } From 11a2c1ece2779b68d6d6f422317843609a011bd0 Mon Sep 17 00:00:00 2001 From: Distractic Date: Fri, 10 Mar 2023 00:07:18 +0100 Subject: [PATCH 17/20] tests: Add tests for image manipulation --- .../rushyverse/api/image/MapImageTest.kt | 164 ++++++++++++++++-- src/test/resources/map_image.png | Bin 0 -> 206 bytes 2 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 src/test/resources/map_image.png diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt index df6a4200..9ee56e14 100644 --- a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt @@ -10,18 +10,24 @@ 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 { @@ -512,32 +518,170 @@ class MapImageTest { @Test fun `should have same content than the image for one packet`() { - TODO() + 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`() { - TODO() - } + 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) + } + } - @Test - fun `should have same content than the image for four packets`() { - TODO() + // 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`() { - TODO() + 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`() { - TODO() + val mapImage = MapImage() + val packets = mapImage.loadImageAsPacketsFromResources("map_image.png") + assertImagePacket(packets) } @Test - fun `should load image from inputstream`() { - TODO() + 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) + } + } } } diff --git a/src/test/resources/map_image.png b/src/test/resources/map_image.png new file mode 100644 index 0000000000000000000000000000000000000000..c0a4aae43bd8481dc16ac510cb639d71c7f5731a GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58JL)Xl%kBoR}il_$lZzY=1HA;AcwQSBeIx* zfm;}a85w5HkpK$v1o(uwhPW*g^|cn)ls zc|=9c(W&0!f0|lU;9sBuT;LviS&0%ZNuc~6@vd$@?2>=$@ BD Date: Fri, 10 Mar 2023 08:37:52 +0100 Subject: [PATCH 18/20] tests: Add tests about remove item frames --- .../rushyverse/api/image/MapImageTest.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt index 9ee56e14..33c541b5 100644 --- a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt @@ -686,6 +686,35 @@ class MapImageTest { } + @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) From 62267a8437dc4f17377a938dd18ccd98c4731a24 Mon Sep 17 00:00:00 2001 From: Distractic Date: Fri, 10 Mar 2023 08:38:04 +0100 Subject: [PATCH 19/20] doc: Add missing documentation --- src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt index d89a57bc..6d4e5d53 100644 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt @@ -111,6 +111,7 @@ public class MapImage { * **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: * ``` From e3b1b01a6c1b57df3270b04cc2f41669f5a7f446 Mon Sep 17 00:00:00 2001 From: Distractic Date: Fri, 10 Mar 2023 09:10:02 +0100 Subject: [PATCH 20/20] build: Use awaitAll instead of await --- .../github/rushyverse/api/image/MapImage.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt index 6d4e5d53..d375335b 100644 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt +++ b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt @@ -3,7 +3,8 @@ 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.future.await +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 @@ -190,7 +191,6 @@ public class MapImage { if (atLeastOneItemFrameIsPresent()) { throw ItemFramesAlreadyExistException("The item frames are already present in the instance.") } - if (numberOfItemFrames == 0) { return emptyList().also { _itemFrames = it } } @@ -204,20 +204,13 @@ public class MapImage { val yaw = imageMath.yaw val pitch = imageMath.pitch - return (0.. - // We need to calculate the position of the item frame. - // The position is calculated from the top left corner of the image. - // The item frames are place to the right and bottom of the beginning position. - val x = imageMath.computeX(beginX, numberOfFrame, itemFramesPerLine) - val y = imageMath.computeY(beginY, numberOfFrame, itemFramesPerLine) - val z = imageMath.computeZ(beginZ, numberOfFrame, itemFramesPerLine) - + 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(numberOfFrame) } + .meta(MapMeta::class.java) { it.mapId(frameNumber) } .build() this.orientation = orientation @@ -225,10 +218,17 @@ public class MapImage { setNotifyAboutChanges(true) } - - setInstance(instance, Pos(x.toDouble(), y.toDouble(), z.toDouble(), yaw, pitch)).await() } - }.also { _itemFrames = it } + } + + 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 } } /**