From e36880daf6646a2f0ed0a921d0bf06f87b8a7280 Mon Sep 17 00:00:00 2001 From: Koding Date: Thu, 23 Jul 2020 17:30:19 +1000 Subject: [PATCH] :sparkles: Finished schematics & refactor --- .../zerite/craftlib/commons/world/Block.kt | 40 ++++ .../kotlin/dev/zerite/craftlib/nbt/DSL.kt | 34 +++ .../craftlib/protocol/data/world/Chunk.kt | 33 +-- .../protocol/data/world/ChunkColumn.kt | 2 + .../craftlib/protocol/data/world/World.kt | 2 + .../server/world/ServerPlayChunkDataTest.kt | 2 +- .../world/ServerPlayMapChunkBulkTest.kt | 2 +- .../schematic/{data => }/Schematic.kt | 40 ++-- .../zerite/craftlib/schematic/SchematicIO.kt | 199 +++++++++++++++--- .../craftlib/schematic/SchematicIOTest.kt | 44 ++++ 10 files changed, 310 insertions(+), 88 deletions(-) create mode 100644 craftlib-commons/src/main/kotlin/dev/zerite/craftlib/commons/world/Block.kt rename craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/{data => }/Schematic.kt (84%) create mode 100644 craftlib-schematic/src/test/kotlin/dev/zerite/craftlib/schematic/SchematicIOTest.kt diff --git a/craftlib-commons/src/main/kotlin/dev/zerite/craftlib/commons/world/Block.kt b/craftlib-commons/src/main/kotlin/dev/zerite/craftlib/commons/world/Block.kt new file mode 100644 index 0000000..3ec04a8 --- /dev/null +++ b/craftlib-commons/src/main/kotlin/dev/zerite/craftlib/commons/world/Block.kt @@ -0,0 +1,40 @@ +package dev.zerite.craftlib.commons.world + +/** + * Stores information about a single block in the world + * + * @author Koding + * @since 0.1.0-SNAPSHOT + */ +data class Block( + val id: Int, + val metadata: Int, + val blockLight: Int = 0, + val skyLight: Int = 0 +) { + + companion object { + /** + * Constant value for an air block. + */ + @JvmField + val AIR = Block(0, 0) + } + + /** + * The location of this block. + */ + lateinit var location: BlockLocation +} + +/** + * Vector for storing a 3D block location. + * + * @author Koding + * @since 0.1.0-SNAPSHOT + */ +data class BlockLocation( + val x: Int, + val y: Int, + val z: Int +) diff --git a/craftlib-nbt/src/main/kotlin/dev/zerite/craftlib/nbt/DSL.kt b/craftlib-nbt/src/main/kotlin/dev/zerite/craftlib/nbt/DSL.kt index f801280..4a9b987 100644 --- a/craftlib-nbt/src/main/kotlin/dev/zerite/craftlib/nbt/DSL.kt +++ b/craftlib-nbt/src/main/kotlin/dev/zerite/craftlib/nbt/DSL.kt @@ -3,6 +3,8 @@ package dev.zerite.craftlib.nbt import dev.zerite.craftlib.nbt.impl.* +import java.io.InputStream +import java.io.OutputStream /** * Builds a compound tag. @@ -151,3 +153,35 @@ val Any?.asTag: NBTTag is LongArray -> LongArrayTag(this) else -> error("Don't know how to convert $this into a NBT tag") } + +/** + * Writes a NBT tag to the provided output stream. + * + * @param stream The stream to write to. + * @param compressed Whether the tag we are writing should be compressed. + * + * @author Koding + * @since 0.1.3 + */ +suspend fun NBTTag.write(stream: OutputStream, compressed: Boolean = false) = + if (compressed) NBTIO.writeCompressed(this, stream) + else NBTIO.write(this, stream) + +/** + * Reads a NBT tag from this input stream. + * + * @param compressed Whether this tag we are reading is compressed. + * @author Koding + * @since 0.1.3 + */ +suspend fun InputStream.readTag(compressed: Boolean = false) = + if (compressed) NBTIO.readCompressed(this) + else NBTIO.read(this) + +/** + * Converts a NBT tag into a named NBT tag. + * + * @author Koding + * @since 0.1.3 + */ +fun T.named(name: String) = named(name, this) diff --git a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/Chunk.kt b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/Chunk.kt index f1cbf94..3cfecf4 100644 --- a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/Chunk.kt +++ b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/Chunk.kt @@ -1,5 +1,8 @@ package dev.zerite.craftlib.protocol.data.world +import dev.zerite.craftlib.commons.world.Block +import dev.zerite.craftlib.commons.world.BlockLocation + /** * Stores a chunk's full data, including the block information and * lighting data. @@ -69,36 +72,6 @@ data class Chunk(internal var blocks: Array = arrayOfNulls(DESIRED_BLOCK override fun hashCode() = blocks.contentHashCode() } -/** - * Stores information about a single block in the world - * - * @author Koding - * @since 0.1.0-SNAPSHOT - */ -data class Block( - val id: Int, - val metadata: Int, - val blockLight: Int, - val skyLight: Int -) { - /** - * The location of this block. - */ - lateinit var location: BlockLocation -} - -/** - * Vector for storing a 3D block location. - * - * @author Koding - * @since 0.1.0-SNAPSHOT - */ -data class BlockLocation( - val x: Int, - val y: Int, - val z: Int -) - /** * Stores information about a chunk. * diff --git a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/ChunkColumn.kt b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/ChunkColumn.kt index 6b539bd..d2063ab 100644 --- a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/ChunkColumn.kt +++ b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/ChunkColumn.kt @@ -1,6 +1,8 @@ package dev.zerite.craftlib.protocol.data.world import dev.zerite.craftlib.commons.io.ByteNibbleArray +import dev.zerite.craftlib.commons.world.Block +import dev.zerite.craftlib.commons.world.BlockLocation import dev.zerite.craftlib.protocol.util.ext.toByteArray import dev.zerite.craftlib.protocol.util.ext.toShortArray import dev.zerite.craftlib.protocol.util.ext.trim diff --git a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/World.kt b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/World.kt index b672c1d..35e3929 100644 --- a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/World.kt +++ b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/World.kt @@ -1,5 +1,7 @@ package dev.zerite.craftlib.protocol.data.world +import dev.zerite.craftlib.commons.world.Block + /** * Simple utility class which allows you to set any coordinate in a world with * a block and automatically update the chunks map. diff --git a/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/packet/play/server/world/ServerPlayChunkDataTest.kt b/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/packet/play/server/world/ServerPlayChunkDataTest.kt index ece0b61..b020ce1 100644 --- a/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/packet/play/server/world/ServerPlayChunkDataTest.kt +++ b/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/packet/play/server/world/ServerPlayChunkDataTest.kt @@ -1,6 +1,6 @@ package dev.zerite.craftlib.protocol.packet.play.server.world -import dev.zerite.craftlib.protocol.data.world.Block +import dev.zerite.craftlib.commons.world.Block import dev.zerite.craftlib.protocol.data.world.Chunk import dev.zerite.craftlib.protocol.data.world.ChunkColumn import dev.zerite.craftlib.protocol.packet.PacketTest diff --git a/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/packet/play/server/world/ServerPlayMapChunkBulkTest.kt b/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/packet/play/server/world/ServerPlayMapChunkBulkTest.kt index 226c75d..1a5063d 100644 --- a/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/packet/play/server/world/ServerPlayMapChunkBulkTest.kt +++ b/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/packet/play/server/world/ServerPlayMapChunkBulkTest.kt @@ -1,6 +1,6 @@ package dev.zerite.craftlib.protocol.packet.play.server.world -import dev.zerite.craftlib.protocol.data.world.Block +import dev.zerite.craftlib.commons.world.Block import dev.zerite.craftlib.protocol.data.world.Chunk import dev.zerite.craftlib.protocol.data.world.ChunkColumn import dev.zerite.craftlib.protocol.packet.PacketTest diff --git a/craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/data/Schematic.kt b/craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/Schematic.kt similarity index 84% rename from craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/data/Schematic.kt rename to craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/Schematic.kt index 8494464..a14b2f6 100644 --- a/craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/data/Schematic.kt +++ b/craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/Schematic.kt @@ -1,4 +1,7 @@ -package dev.zerite.craftlib.schematic.data +package dev.zerite.craftlib.schematic + +import dev.zerite.craftlib.commons.world.Block +import dev.zerite.craftlib.nbt.impl.CompoundTag /** * Houses all the valid schematic values which have either been parsed from a @@ -7,12 +10,20 @@ package dev.zerite.craftlib.schematic.data * @author Koding * @since 0.1.3 */ -data class Schematic( +data class Schematic @JvmOverloads constructor( var width: Short, var height: Short, var length: Short, var materials: SchematicMaterials, - var blocks: Array + var blocks: Array = Array(width * height * length) { Block.AIR }, + var entities: List = arrayListOf(), + var tileEntities: List = arrayListOf(), + var originX: Int = 0, + var originY: Int = 0, + var originZ: Int = 0, + var offsetX: Int = 0, + var offsetY: Int = 0, + var offsetZ: Int = 0 ) { companion object { @@ -44,7 +55,8 @@ data class Schematic( * @since 0.1.3 */ @Suppress("UNUSED") - fun index(x: Int, y: Int, z: Int) = index(x, y, z, length, width) + fun index(x: Int, y: Int, z: Int) = + index(x, y, z, length, width) /** * Retrieves a block at the given coordinate, otherwise returning @@ -58,7 +70,7 @@ data class Schematic( * @author Koding * @since 0.1.3 */ - operator fun get(x: Int, y: Int, z: Int) = blocks.getOrElse(index(x, y, z)) { SchematicBlock.AIR } + operator fun get(x: Int, y: Int, z: Int) = blocks.getOrElse(index(x, y, z)) { Block.AIR } /** * Sets a block at the given coordinate. All positions are relative @@ -72,7 +84,7 @@ data class Schematic( * @author Koding * @since 0.1.3 */ - operator fun set(x: Int, y: Int, z: Int, value: SchematicBlock) { + operator fun set(x: Int, y: Int, z: Int, value: Block) { blocks[index(x, y, z)] = value } @@ -101,22 +113,6 @@ data class Schematic( } } -/** - * Stores a pair of a block ID and metadata value. - * - * @author Koding - * @since 0.1.3 - */ -data class SchematicBlock( - var id: Int, - var metadata: Byte -) { - companion object { - @JvmField - val AIR = SchematicBlock(0, 0) - } -} - /** * Stores the possible value for the schematic materials string. * diff --git a/craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/SchematicIO.kt b/craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/SchematicIO.kt index 1ce8f25..db73a9c 100644 --- a/craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/SchematicIO.kt +++ b/craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/SchematicIO.kt @@ -1,14 +1,17 @@ +@file:JvmName("SchematicUtil") + package dev.zerite.craftlib.schematic import dev.zerite.craftlib.commons.io.ByteNibbleArray -import dev.zerite.craftlib.nbt.NBTIO -import dev.zerite.craftlib.nbt.impl.ByteArrayTag -import dev.zerite.craftlib.nbt.impl.ShortTag -import dev.zerite.craftlib.nbt.impl.StringTag -import dev.zerite.craftlib.schematic.data.Schematic -import dev.zerite.craftlib.schematic.data.SchematicBlock -import dev.zerite.craftlib.schematic.data.SchematicMaterials +import dev.zerite.craftlib.commons.world.Block +import dev.zerite.craftlib.nbt.compound +import dev.zerite.craftlib.nbt.impl.* +import dev.zerite.craftlib.nbt.readTag +import dev.zerite.craftlib.nbt.write +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.future.future import java.io.InputStream +import java.io.OutputStream /** * Utility relating to IO operations which involve the schematic format. @@ -19,6 +22,53 @@ import java.io.InputStream @Suppress("UNUSED") object SchematicIO { + /** + * Parses a schematic from the provided compound tag. + * + * @param tag The tag input data we're parsing. + * @author Koding + * @since 0.1.3 + */ + @OptIn(ExperimentalUnsignedTypes::class) + @JvmStatic + fun readTag(tag: CompoundTag) = tag.let { + val rawBlocks = it["Blocks", ByteArrayTag(ByteArray(0))].value + val addBlocks = ByteNibbleArray(it["AddBlocks", ByteArrayTag(ByteArray(0))].value, flipped = true) + val data = it["Data", ByteArrayTag(ByteArray(0))].value + + val width = it["Width", ShortTag(0)].value + val length = it["Length", ShortTag(0)].value + val height = it["Height", ShortTag(0)].value + + val blocks = Array(rawBlocks.size) { Block.AIR } + + for (i in blocks.indices) { + val id = rawBlocks[i] + val add = addBlocks[i, 0] + val meta = if (data.size <= i) 0 else data[i] + blocks[i] = Block( + (add shl 8) or id.toUByte().toInt(), + meta.toInt() and 0xF + ) + } + + Schematic( + width, + height, + length, + SchematicMaterials[it["Materials", StringTag("Alpha")].value], + blocks = blocks, + entities = it["Entities", ListTag()].value, + tileEntities = it["TileEntities", ListTag()].value, + originX = it["WEOriginX", IntTag(0)].value, + originY = it["WEOriginY", IntTag(0)].value, + originZ = it["WEOriginZ", IntTag(0)].value, + offsetX = it["WEOffsetX", IntTag(0)].value, + offsetY = it["WEOffsetY", IntTag(0)].value, + offsetZ = it["WEOffsetZ", IntTag(0)].value + ) + } + /** * Reads a NBT compound from the provided input stream and parses it * into a schematic class. @@ -29,36 +79,117 @@ object SchematicIO { * @author Koding * @since 0.1.3 */ - @OptIn(ExperimentalUnsignedTypes::class) + @Suppress("UNUSED") suspend fun read(input: InputStream, compressed: Boolean = true) = - (if (compressed) NBTIO.readCompressed(input) else NBTIO.read(input)).tag.let { - val rawBlocks = it["Blocks", ByteArrayTag(ByteArray(0))].value - val addBlocks = ByteNibbleArray(it["AddBlocks", ByteArrayTag(ByteArray(0))].value, flipped = true) - val data = it["Data", ByteArrayTag(ByteArray(0))].value - - val width = it["Width", ShortTag(0)].value - val length = it["Length", ShortTag(0)].value - val height = it["Height", ShortTag(0)].value - - val blocks = Array(rawBlocks.size) { SchematicBlock.AIR } - - for (i in blocks.indices) { - val id = rawBlocks[i] - val add = addBlocks[i, 0] - val meta = if (data.size <= i) 0 else data[i] - blocks[i] = SchematicBlock( - (add shl 4) or id.toUByte().toInt(), - (meta.toInt() and 0xF).toByte() - ) + readTag(input.readTag(compressed).tag) + + /** + * Reads a NBT compound from the provided input stream and parses it + * into a schematic class, returning a future with the result. + * + * @param input The input stream which we are reading the compound from. + * @param compressed Whether the input stream contains compressed NBT data. + * + * @author Koding + * @since 0.1.3 + */ + @JvmStatic + fun readFuture(input: InputStream, compressed: Boolean = true) = + GlobalScope.future { read(input, compressed) } + + /** + * Writes a schematic into a NBT tag. + * + * @param schematic The schematic we're writing to a tag. + * @author Koding + * @since 0.1.3 + */ + @JvmStatic + @Suppress("UNUSED") + fun writeTag(schematic: Schematic): CompoundTag { + val size = schematic.width * schematic.height * schematic.length + val blocks = ByteArray(size) + val addBlocks = ByteNibbleArray(ByteArray(size / 2), flipped = true) + val data = ByteArray(size) + var add = false + + schematic.blocks.forEachIndexed { index, it -> + blocks[index] = (it.id and 0xFF).toByte() + data[index] = (it.metadata and 0xF).toByte() + + ((it.id shr 8) and 0xF).takeIf { it != 0 }?.let { + add = true + addBlocks[index] = it } + } - Schematic( - width, - height, - length, - SchematicMaterials[it["Materials", StringTag("Alpha")].value], - blocks - ) + return compound { + "Width" to schematic.width + "Height" to schematic.height + "Length" to schematic.length + "Materials" to schematic.materials.key + + "Blocks" to blocks + "Data" to data + if (add) "AddBlocks" to addBlocks.data + + "Entities" to schematic.entities + "TileEntities" to schematic.tileEntities + + "WEOriginX" to schematic.originX + "WEOriginY" to schematic.originY + "WEOriginZ" to schematic.originZ + "WEOffsetX" to schematic.offsetX + "WEOffsetY" to schematic.offsetY + "WEOffsetZ" to schematic.offsetZ } + } + + /** + * Writes a schematic into the provided {@code OutputStream}, optionally + * compressing it with GZIP. + * + * @param schematic The schematic we're writing. + * @param output The output stream which we're writing to. + * @param compressed Whether we should compress the written schematic. + * + * @author Koding + * @since 0.1.3 + */ + @Suppress("UNUSED") + suspend fun write(schematic: Schematic, output: OutputStream, compressed: Boolean = true) = + writeTag(schematic).write(output, compressed) + + /** + * Writes a schematic into the provided {@code OutputStream}, optionally + * compressing it with GZIP. Returns a {@code Future} for handling + * the completion. + * + * @param schematic The schematic we're writing. + * @param output The output stream which we're writing to. + * @param compressed Whether we should compress the written schematic. + * + * @author Koding + * @since 0.1.3 + */ + @JvmStatic + fun writeFuture(schematic: Schematic, output: OutputStream, compressed: Boolean = true) = + GlobalScope.future { write(schematic, output, compressed) } } + +/** + * Reads a schematic from the compound tag. + * + * @author Koding + * @since 0.1.3 + */ +@Suppress("UNUSED") +fun CompoundTag.readSchematic() = SchematicIO.readTag(this) + +/** + * Converts this schematic to a NBT compound tag. + */ +@Suppress("UNUSED") +val Schematic.compound + get() = SchematicIO.writeTag(this) diff --git a/craftlib-schematic/src/test/kotlin/dev/zerite/craftlib/schematic/SchematicIOTest.kt b/craftlib-schematic/src/test/kotlin/dev/zerite/craftlib/schematic/SchematicIOTest.kt new file mode 100644 index 0000000..6e17be6 --- /dev/null +++ b/craftlib-schematic/src/test/kotlin/dev/zerite/craftlib/schematic/SchematicIOTest.kt @@ -0,0 +1,44 @@ +package dev.zerite.craftlib.schematic + +import dev.zerite.craftlib.commons.world.Block +import dev.zerite.craftlib.nbt.named +import dev.zerite.craftlib.nbt.readTag +import dev.zerite.craftlib.nbt.write +import kotlinx.coroutines.runBlocking +import java.io.ByteArrayOutputStream +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Tests the IO operations relating to schematics. + * + * @author Koding + * @since 0.1.3 + */ +class SchematicIOTest { + + /** + * The example schematic we're comparing against. + */ + private val schematic = Schematic(4, 4, 4, SchematicMaterials.ALPHA).apply { + this[0, 2, 3] = Block(10, 10) + this[1, 3, 2] = Block(4, 6) + } + + @Test + fun `Test IO`() = runBlocking { + val output = ByteArrayOutputStream() + schematic.compound.named("Schematic").write(output) + val read = output.toByteArray().inputStream().readTag().tag.readSchematic() + assertEquals(schematic, read) + } + + @Test + fun `Test Compressed IO`() = runBlocking { + val output = ByteArrayOutputStream() + schematic.compound.named("Schematic").write(output, compressed = true) + val read = output.toByteArray().inputStream().readTag(compressed = true).tag.readSchematic() + assertEquals(schematic, read) + } + +}