From 04a988bb4e6fb772fd1a14304f1824dc99685c18 Mon Sep 17 00:00:00 2001 From: Koding Date: Wed, 22 Jul 2020 22:35:08 +1000 Subject: [PATCH 1/3] :construction: WIP schematic parsing --- build.gradle | 2 +- craftlib-commons/build.gradle | 1 + .../craftlib/commons/io}/ByteNibbleArray.kt | 16 +- craftlib-protocol/build.gradle | 2 + .../protocol/data/world/ChunkColumn.kt | 24 ++- .../craftlib/protocol/data/world/World.kt | 49 ++++++ craftlib-schematic/build.gradle | 6 + .../zerite/craftlib/schematic/SchematicIO.kt | 64 ++++++++ .../craftlib/schematic/data/Schematic.kt | 142 ++++++++++++++++++ settings.gradle | 2 +- 10 files changed, 297 insertions(+), 11 deletions(-) create mode 100644 craftlib-commons/build.gradle rename {craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world => craftlib-commons/src/main/kotlin/dev/zerite/craftlib/commons/io}/ByteNibbleArray.kt (77%) create mode 100644 craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/World.kt create mode 100644 craftlib-schematic/build.gradle create mode 100644 craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/SchematicIO.kt create mode 100644 craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/data/Schematic.kt diff --git a/build.gradle b/build.gradle index 3b19b03..1a031d9 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ ext { } group 'dev.zerite.craftlib' -version '0.1.2' +version '0.1.3' subprojects { apply plugin: 'java' diff --git a/craftlib-commons/build.gradle b/craftlib-commons/build.gradle new file mode 100644 index 0000000..3e15725 --- /dev/null +++ b/craftlib-commons/build.gradle @@ -0,0 +1 @@ +description = 'Miscellaneous utilities which are shared between modules' diff --git a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/ByteNibbleArray.kt b/craftlib-commons/src/main/kotlin/dev/zerite/craftlib/commons/io/ByteNibbleArray.kt similarity index 77% rename from craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/ByteNibbleArray.kt rename to craftlib-commons/src/main/kotlin/dev/zerite/craftlib/commons/io/ByteNibbleArray.kt index f26f4eb..1516fba 100644 --- a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/ByteNibbleArray.kt +++ b/craftlib-commons/src/main/kotlin/dev/zerite/craftlib/commons/io/ByteNibbleArray.kt @@ -1,23 +1,23 @@ -package dev.zerite.craftlib.protocol.data.world +package dev.zerite.craftlib.commons.io /** * Stores an array of bytes, with each storing two integer values spanning * 4 bits. * * @author Koding - * @since 0.1.0-SNAPSHOT + * @since 0.1.3 */ -data class ByteNibbleArray(var data: ByteArray) { +data class ByteNibbleArray @JvmOverloads constructor(var data: ByteArray, var flipped: Boolean = false) { /** * Gets a nibble byte from the array with the given index. * * @param index The index of the nibble byte. * @author Koding - * @since 0.1.0-SNAPSHOT + * @since 0.1.3 */ operator fun get(index: Int) = - data[index / 2].toInt() ushr (if (index % 2 == 0) 0 else 4) and 0x0F + data[index / 2].toInt() ushr (if (index % 2 == if (flipped) 1 else 0) 0 else 4) and 0x0F /** * Gets a nibble byte from the array, otherwise falling back @@ -27,7 +27,7 @@ data class ByteNibbleArray(var data: ByteArray) { * @param default The fallback value if the index is out of bounds. * * @author Koding - * @since 0.1.0-SNAPSHOT + * @since 0.1.3 */ operator fun get(index: Int, default: Int) = if (index < 0 || (index / 2) >= data.size || data.isEmpty()) default else this[index] @@ -39,11 +39,11 @@ data class ByteNibbleArray(var data: ByteArray) { * @param value The value which we are setting. * * @author Koding - * @since 0.1.0-SNAPSHOT + * @since 0.1.3 */ operator fun set(index: Int, value: Int) { val i = index shr 1 - if (index and 1 == 0) data[i] = ((data[i].toInt() and 240) or (value and 15)).toByte() + if (index and 1 == if (flipped) 1 else 0) data[i] = ((data[i].toInt() and 240) or (value and 15)).toByte() else data[i] = ((data[i].toInt() and 15) or ((value and 15) shl 4)).toByte() } diff --git a/craftlib-protocol/build.gradle b/craftlib-protocol/build.gradle index 6399572..9a4675f 100644 --- a/craftlib-protocol/build.gradle +++ b/craftlib-protocol/build.gradle @@ -4,6 +4,8 @@ dependencies { api "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion" api "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" api 'io.netty:netty-all:4.1.51.Final' + api project(':craftlib-chat') api project(':craftlib-nbt') + api project(':craftlib-commons') } 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 833bc90..6b539bd 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,5 +1,6 @@ package dev.zerite.craftlib.protocol.data.world +import dev.zerite.craftlib.commons.io.ByteNibbleArray import dev.zerite.craftlib.protocol.util.ext.toByteArray import dev.zerite.craftlib.protocol.util.ext.toShortArray import dev.zerite.craftlib.protocol.util.ext.trim @@ -11,7 +12,12 @@ import dev.zerite.craftlib.protocol.util.ext.trim * @author Koding * @since 0.1.0-SNAPSHOT */ -data class ChunkColumn(val x: Int, val z: Int, val chunks: Array, private var biomes: ByteArray) { +data class ChunkColumn @JvmOverloads constructor( + val x: Int, + val z: Int, + val chunks: Array = Array(16) { Chunk() }, + var biomes: ByteArray = ByteArray(16 * 16) { 0 } +) { companion object { /** @@ -402,6 +408,22 @@ data class ChunkColumn(val x: Int, val z: Int, val chunks: Array, private return this[chunkY][x, y - chunkY * 16, z] } + /** + * Gets a block in this chunk column and return it. + * + * @param x The x coordinate in this chunk. Max 16. + * @param y The y coordinate in this chunk. Max 256. + * @param z The z coordinate in this chunk. Max 16. + * @param block The new block at this coordinate. + * + * @author Koding + * @since 0.1.0-SNAPSHOT + */ + operator fun set(x: Int, y: Int, z: Int, block: Block) { + val chunkY = y / 16 + this[chunkY][x, y - chunkY * 16, z] = block + } + /** * Gets the biome at the specific coordinate within the chunk. * 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 new file mode 100644 index 0000000..b672c1d --- /dev/null +++ b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/data/world/World.kt @@ -0,0 +1,49 @@ +package dev.zerite.craftlib.protocol.data.world + +/** + * Simple utility class which allows you to set any coordinate in a world with + * a block and automatically update the chunks map. + * + * @author Koding + * @since 0.1.3 + */ +@Suppress("UNUSED") +class World @JvmOverloads constructor(var chunks: MutableMap, ChunkColumn> = hashMapOf()) { + + /** + * Sets a block a the given position in the world. + * + * @param x The x position to set the block at. + * @param y The y position to set the block at. + * @param z The z position to set the block at. + * @param block The new block for the position to have set. + * + * @author Koding + * @since 0.1.3 + */ + operator fun set(x: Int, y: Int, z: Int, block: Block) { + val chunkX = x shr 4 + val chunkZ = z shr 4 + val column = chunks.getOrPut(chunkX to chunkZ) { ChunkColumn(chunkX, chunkZ) } + column[x and 0xF, y, z and 0xF] = block + } + + /** + * Gets a block at the given position or null if there + * is no block present. + * + * @param x The x position to set the block at. + * @param y The y position to set the block at. + * @param z The z position to set the block at. + * + * @author Koding + * @since 0.1.3 + */ + operator fun get(x: Int, y: Int, z: Int): Block? { + val chunkX = x shr 4 + val chunkZ = z shr 4 + val column = chunks[chunkX to chunkZ] ?: return null + return column[x and 0xF, y, z and 0xF] + } + +} diff --git a/craftlib-schematic/build.gradle b/craftlib-schematic/build.gradle new file mode 100644 index 0000000..e6bc796 --- /dev/null +++ b/craftlib-schematic/build.gradle @@ -0,0 +1,6 @@ +description = 'Reads the schematic file format' + +dependencies { + api project(':craftlib-commons') + api project(':craftlib-nbt') +} 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 new file mode 100644 index 0000000..1ce8f25 --- /dev/null +++ b/craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/SchematicIO.kt @@ -0,0 +1,64 @@ +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 java.io.InputStream + +/** + * Utility relating to IO operations which involve the schematic format. + * + * @author Koding + * @since 0.1.3 + */ +@Suppress("UNUSED") +object SchematicIO { + + /** + * Reads a NBT compound from the provided input stream and parses it + * into a schematic class. + * + * @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 + */ + @OptIn(ExperimentalUnsignedTypes::class) + 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() + ) + } + + Schematic( + width, + height, + length, + SchematicMaterials[it["Materials", StringTag("Alpha")].value], + blocks + ) + } + +} 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/data/Schematic.kt new file mode 100644 index 0000000..8494464 --- /dev/null +++ b/craftlib-schematic/src/main/kotlin/dev/zerite/craftlib/schematic/data/Schematic.kt @@ -0,0 +1,142 @@ +package dev.zerite.craftlib.schematic.data + +/** + * Houses all the valid schematic values which have either been parsed from a + * schematic file or created programmatically. + * + * @author Koding + * @since 0.1.3 + */ +data class Schematic( + var width: Short, + var height: Short, + var length: Short, + var materials: SchematicMaterials, + var blocks: Array +) { + + companion object { + /** + * Calculates the array index with the given coordinates + * and sizing values. + * + * @param x The x position to calculate with. + * @param y The y position to calculate with. + * @param z The z position to calculate with. + * @param length The length of the schematic. + * @param width The width of the schematic. + * + * @author Koding + * @since 0.1.3 + */ + fun index(x: Int, y: Int, z: Int, length: Short, width: Short) = (y * length + z) * width + x + } + + /** + * Finds the array index with the given coordinates whilst passing + * in this schematics {@code length} and {@code width} values automatically. + * + * @param x The x position to calculate with. + * @param y The y position to calculate with. + * @param z The z position to calculate with. + * + * @author Koding + * @since 0.1.3 + */ + @Suppress("UNUSED") + fun index(x: Int, y: Int, z: Int) = index(x, y, z, length, width) + + /** + * Retrieves a block at the given coordinate, otherwise returning + * the default air block. All positions are relative to this schematic and + * begin from 0 to the maximum of that axis. + * + * @param x The x position which we are looking up. + * @param y The y position which we are looking up. + * @param z The z position which we are looking up. + * + * @author Koding + * @since 0.1.3 + */ + operator fun get(x: Int, y: Int, z: Int) = blocks.getOrElse(index(x, y, z)) { SchematicBlock.AIR } + + /** + * Sets a block at the given coordinate. All positions are relative + * to this schematic and begin from 0 to the maximum of that axis. + * + * @param x The x position which we are writing to. + * @param y The y position which we are writing to. + * @param z The z position which we are writing to. + * @param value The block to write in this schematic. + * + * @author Koding + * @since 0.1.3 + */ + operator fun set(x: Int, y: Int, z: Int, value: SchematicBlock) { + blocks[index(x, y, z)] = value + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Schematic + + if (width != other.width) return false + if (height != other.height) return false + if (length != other.length) return false + if (materials != other.materials) return false + if (!blocks.contentEquals(other.blocks)) return false + + return true + } + + override fun hashCode(): Int { + var result = width.toInt() + result = 31 * result + height + result = 31 * result + length + result = 31 * result + materials.hashCode() + result = 31 * result + blocks.contentHashCode() + return result + } +} + +/** + * 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. + * + * @author Koding + * @since 0.1.3 + */ +@Suppress("UNUSED") +enum class SchematicMaterials(val key: String) { + ALPHA("Alpha"), + CLASSIC("Classic"), + POCKET("Pocket"); + + companion object { + /** + * Finds a schematic material with the given key. + * + * @param key The key to find a material with. + * @author Koding + * @since 0.1.3 + */ + operator fun get(key: String) = values().first { it.key == key } + } +} diff --git a/settings.gradle b/settings.gradle index fc89213..fac95de 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = 'CraftLib' -include ':craftlib-chat', ':craftlib-nbt', ':craftlib-protocol' +include ':craftlib-chat', ':craftlib-commons', ':craftlib-nbt', ':craftlib-protocol', ':craftlib-schematic' From 53c46017afc2dc8ad4356dff30abb44fa44e1afd Mon Sep 17 00:00:00 2001 From: Koding Date: Thu, 23 Jul 2020 16:12:55 +1000 Subject: [PATCH 2/3] :zap: Improving protocol performance --- .../dev/zerite/craftlib/protocol/PacketIO.kt | 7 ++++++ .../craftlib/protocol/util/ext/ArrayExt.kt | 1 + .../protocol/version/ProtocolState.kt | 24 ++++--------------- .../protocol/version/ProtocolStateTest.kt | 4 ---- 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/PacketIO.kt b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/PacketIO.kt index 8cf3d33..0db47cc 100644 --- a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/PacketIO.kt +++ b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/PacketIO.kt @@ -1,5 +1,6 @@ package dev.zerite.craftlib.protocol +import com.google.gson.reflect.TypeToken import dev.zerite.craftlib.protocol.connection.NettyConnection import dev.zerite.craftlib.protocol.version.ProtocolVersion @@ -13,6 +14,12 @@ import dev.zerite.craftlib.protocol.version.ProtocolVersion @Suppress("UNUSED") interface PacketIO { + /** + * Retrieves the class for the packet generic. + */ + val type: Class + get() = (object : TypeToken() {}).rawType + /** * Reads a packet from the provided protocol buffer into an object. * diff --git a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/util/ext/ArrayExt.kt b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/util/ext/ArrayExt.kt index dc0f741..44fd78b 100644 --- a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/util/ext/ArrayExt.kt +++ b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/util/ext/ArrayExt.kt @@ -1,4 +1,5 @@ @file:JvmName("ArrayUtil") + package dev.zerite.craftlib.protocol.util.ext import io.netty.buffer.Unpooled diff --git a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/version/ProtocolState.kt b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/version/ProtocolState.kt index 23e02b7..e7b3129 100644 --- a/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/version/ProtocolState.kt +++ b/craftlib-protocol/src/main/kotlin/dev/zerite/craftlib/protocol/version/ProtocolState.kt @@ -1,7 +1,6 @@ package dev.zerite.craftlib.protocol.version import dev.zerite.craftlib.protocol.PacketIO -import kotlin.reflect.jvm.javaType /** * Stores details about packet mappings for a specific connection state. @@ -138,29 +137,14 @@ data class ProtocolState(val name: String, val id: Any) { * @since 0.1.0-SNAPSHOT */ @ProtocolStateDSL - operator fun PacketIO<*>.invoke(block: IdListBuilder.() -> Unit) = + operator fun PacketIO<*>.invoke(block: IdListBuilder.() -> Unit) { + val packetType = type runForAllProtocols(IdListBuilder().apply(block).ids.toTypedArray()) { version, id -> val data = PacketData(id, this) - val type = javaClass.typeParameter ?: return@runForAllProtocols - - classToData.getOrPut(version) { hashMapOf() }[type] = data + classToData.getOrPut(version) { hashMapOf() }[packetType] = data idToData.getOrPut(version) { hashMapOf() }[id] = data } - - /** - * Finds the first type parameter for a class. - */ - val Class<*>.typeParameter: Class<*>? - get() = try { - kotlin.supertypes.firstOrNull() - ?.arguments?.firstOrNull() - ?.type?.javaType?.typeName - ?.let { - Class.forName(it) - } ?: error("Failed to get packet class!") - } catch (e: Throwable) { - error("Failed to get packet class!") - } + } /** diff --git a/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/version/ProtocolStateTest.kt b/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/version/ProtocolStateTest.kt index 2bb97bf..7d777b6 100644 --- a/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/version/ProtocolStateTest.kt +++ b/craftlib-protocol/src/test/kotlin/dev/zerite/craftlib/protocol/version/ProtocolStateTest.kt @@ -2,7 +2,6 @@ package dev.zerite.craftlib.protocol.version import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFails import kotlin.test.assertNull class ProtocolStateTest { @@ -19,9 +18,6 @@ class ProtocolStateTest { val state = ProtocolState("Example", 1) assertNull(state[PacketDirection.CLIENTBOUND][ProtocolVersion.UNKNOWN, -1]) assertNull(state[PacketDirection.CLIENTBOUND][ProtocolVersion.UNKNOWN, Any()]) - assertFails { - state[PacketDirection.CLIENTBOUND].apply { Any::class.java.typeParameter } - } } @Test From e36880daf6646a2f0ed0a921d0bf06f87b8a7280 Mon Sep 17 00:00:00 2001 From: Koding Date: Thu, 23 Jul 2020 17:30:19 +1000 Subject: [PATCH 3/3] :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) + } + +}