From 829d8a6cdf6ec3f0031985a551ad0765030667ec Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Wed, 21 Jan 2026 23:33:45 +0100 Subject: [PATCH] Initial ChaosSchematicReader Signed-off-by: Chaoscaot --- .../de/steamwar/core/ChaosSchematicReader.kt | 355 ++++++++++++++++++ .../de/steamwar/core/WorldEditWrapper14.java | 4 +- .../de/steamwar/core/WorldEditWrapper21.java | 31 +- 3 files changed, 367 insertions(+), 23 deletions(-) create mode 100644 SpigotCore/SpigotCore_14/src/de/steamwar/core/ChaosSchematicReader.kt diff --git a/SpigotCore/SpigotCore_14/src/de/steamwar/core/ChaosSchematicReader.kt b/SpigotCore/SpigotCore_14/src/de/steamwar/core/ChaosSchematicReader.kt new file mode 100644 index 00000000..a709da31 --- /dev/null +++ b/SpigotCore/SpigotCore_14/src/de/steamwar/core/ChaosSchematicReader.kt @@ -0,0 +1,355 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.core + +import com.sk89q.jnbt.ByteArrayTag +import com.sk89q.jnbt.ByteTag +import com.sk89q.jnbt.CompoundTag +import com.sk89q.jnbt.DoubleTag +import com.sk89q.jnbt.EndTag +import com.sk89q.jnbt.FloatTag +import com.sk89q.jnbt.IntArrayTag +import com.sk89q.jnbt.IntTag +import com.sk89q.jnbt.ListTag +import com.sk89q.jnbt.LongArrayTag +import com.sk89q.jnbt.LongTag +import com.sk89q.jnbt.ShortTag +import com.sk89q.jnbt.StringTag +import com.sk89q.jnbt.Tag +import com.sk89q.worldedit.WorldEdit +import com.sk89q.worldedit.WorldEditException +import com.sk89q.worldedit.extension.input.ParserContext +import com.sk89q.worldedit.extension.platform.Capability +import com.sk89q.worldedit.extension.platform.Platform +import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard +import com.sk89q.worldedit.math.BlockVector3 +import com.sk89q.worldedit.regions.CuboidRegion +import com.sk89q.worldedit.world.DataFixer +import com.sk89q.worldedit.world.block.BaseBlock +import com.sk89q.worldedit.world.block.BlockState +import com.sk89q.worldedit.world.block.BlockTypes +import java.io.DataInputStream +import java.io.IOException +import kotlin.collections.map +import kotlin.collections.set + +class ChaosSchematicReader( + val stream: DataInputStream, + val wrapper: WorldEditWrapper14 +) { + init { + println("ChaosSchematicReader init") + } + + /** + * In reinen Unit-Tests (ohne Bukkit/WorldEdit-Plugin-Lifecycle) ist keine WorldEdit-Platform registriert. + * Daher darf der Reader beim Laden nicht hart daran scheitern. + */ + private val platform: Platform? = runCatching { + WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING) + }.getOrNull() + + private val liveDataVersion: Int? = platform?.dataVersion + + private var wrapped = false + + var schematicVersion: Int? = null + var dataVersion: Int? = null + var metadata: Map? = null + + var width: Short? = null + var height: Short? = null + var length: Short? = null + + var offset: IntArray? = null + + val blocks: Blocks = Blocks(null, null, null) + val entities = mutableListOf() + + var fixer: DataFixer? = null + + fun read(): BlockArrayClipboard { + require(stream.readByte() == 0x0A.toByte()) + val tagName = stream.readUTF() + if (tagName != "Schematic") { + wrapped = true + stream.readByte() + stream.readUTF() + } + + while (stream.readByte() != 0.toByte()) { + val name = stream.readUTF() + when (name) { + "Version" -> schematicVersion = stream.readInt() + "DataVersion" -> { + dataVersion = stream.readInt() + val targetVersion = liveDataVersion + if (targetVersion != null && dataVersion!! < targetVersion) { + fixer = platform?.dataFixer + } + } + "Metadata" -> metadata = readCompoundTag() + "Width" -> width = stream.readShort() + "Height" -> height = stream.readShort() + "Length" -> length = stream.readShort() + "Blocks" -> { + while (stream.readByte() != 0.toByte()) { + val n = stream.readUTF() + when (n) { + "Palette" -> blocks.palette = readCompoundTag() as Map + "Data" -> blocks.data = readTag(7.toByte()) as ByteArray + "BlockEntities" -> blocks.blockEntities = readTagCompoundList() + } + } + } + "PaletteMax" -> stream.readInt() + "Palette" -> blocks.palette = readCompoundTag() as Map + "BlockEntities" -> blocks.blockEntities = readTagCompoundList() + "BlockData" -> blocks.data = readTag(7.toByte()) as ByteArray + "Offset" -> offset = readTag(11.toByte()) as IntArray + "Entities" -> entities.addAll(readTagCompoundList() as List) + "BiomePaletteMax" -> stream.readInt() + "BiomePalette" -> readCompoundTag() + "BiomeData" -> readTag(7.toByte()) + "Biomes" -> { + while (stream.readByte() != 0.toByte()) { + val n = stream.readUTF() + when (n) { + "Palette" -> readCompoundTag() + "Data" -> readTag(7.toByte()) as ByteArray + } + } + } + else -> { + println(this) + error("Unknown tag $name") + } + } + } + + + + if (wrapped) { + stream.readByte() + } + + val parserContext = ParserContext() + parserContext.setRestricted(false) + parserContext.setTryLegacy(false) + parserContext.setPreferringWildcard(false) + + val dv = dataVersion + val blockStates = blocks.palette!!.map { + val fixed = if (fixer != null && dv != null) { + fixer!!.fixUp(DataFixer.FixTypes.BLOCK_STATE, it.key, dv) + } else { + it.key + } + + val blockstate = try { + WorldEdit.getInstance().blockFactory.parseFromInput(fixed, parserContext).toImmutableState() + } catch (e: Exception) { + BlockTypes.AIR!!.defaultState + } + + it.value to blockstate + }.toMap() + + val tileEntities = blocks.blockEntities!!.map { + val entity = it as? Map ?: error("Invalid entity") + val pos = entity["Pos"] as? IntArray ?: error("Invalid pos") + val x = pos[0] + val y = pos[1] + val z = pos[2] + + val data = mutableMapOf() + data.putAll(entity) + if (entity.containsKey("Data")) { + //data.putAll(entity["Data"] as Map) + } + + val fixed = if (fixer != null) { + wrapper.applyDataFixer(fixer!!, dataVersion!!, (wrapNbt(data) as CompoundTag).value) + } else (wrapNbt(data) as CompoundTag).value + + BlockVector3.at(x, y, z) to fixed + }.toMap() + + val offset = BlockVector3.at(offset!![0], offset!![1], offset!![2]) + val region = CuboidRegion(offset, offset.add(width!! - 1, height!! - 1, length!! - 1)) + + val clipboard = BlockArrayClipboard(region) + clipboard.setOrigin(offset) + + var index = 0 + var i = 0 + var value: Int + var varintLength: Int + while (i < blocks.data!!.size) { + value = 0 + varintLength = 0 + + while (true) { + value = value or ((blocks.data!![i].toInt() and 127) shl (varintLength++ * 7)) + if (varintLength > 5) { + throw IOException("VarInt too big (probably corrupted data)") + } + if ((blocks.data!![i].toInt() and 128) != 128) { + i++ + break + } + i++ + } + // index = (y * length * width) + (z * width) + x + val y = index / (width!! * length!!) + val z = (index % (width!! * length!!)) / width!! + val x = (index % (width!! * length!!)) % width!! + val state: BlockState = blockStates[value] ?: error("Unknown block state $value") + val pt = BlockVector3.at(x, y, z) + try { + if (tileEntities.containsKey(pt)) { + clipboard.setBlock( + clipboard.minimumPoint.add(pt), + state.toBaseBlock(CompoundTag(tileEntities[pt]) + )) + } else { + clipboard.setBlock(clipboard.minimumPoint.add(pt), state) + } + } catch (e: WorldEditException) { + throw IOException("Failed to load a block in the schematic") + } + + index++ + } + + return clipboard + } + + private fun readCompoundTag(): Map { + var tagId = 0.toByte() + val map = mutableMapOf() + + while (stream.readByte().also { tagId = it } != 0.toByte()) { + val name = stream.readUTF() + map[name] = readTag(tagId)!! + } + return map + } + + private fun readTagCompoundList(): List { + val typeId = stream.readByte() + val length = stream.readInt() + + if (typeId == 0.toByte()) { + if (length != 0) { + throw IOException("Invalid TAG_List: typeId=TAG_End but length=$length") + } + return emptyList() + } + + if (typeId != 10.toByte()) { + throw IOException("Invalid TAG_List element type for (Block)Entities: expected TAG_Compound(10) but got $typeId") + } + + val list = ArrayList(length) + repeat(length) { list.add(readCompoundTag()) } + return list + } + + private fun readTag(tagId: Byte): Any? = when (tagId.toInt()) { + 0 -> null + 1 -> stream.readByte() + 2 -> stream.readShort() + 3 -> stream.readInt() + 4 -> stream.readLong() + 5 -> stream.readFloat() + 6 -> stream.readDouble() + 7 -> { + val length = stream.readInt() + val ba = ByteArray(length) + stream.readFully(ba) + ba + } + 8 -> stream.readUTF() + 9 -> { + val typeId = stream.readByte() + val length = stream.readInt() + if (typeId == 0.toByte() && length != 0) { + throw IOException("Invalid TAG_List: typeId=TAG_End but length=$length") + } + val list = mutableListOf() + repeat(length) { list.add(readTag(typeId)) } + list + } + 10 -> readCompoundTag() + 11 -> { + val length = stream.readInt() + IntArray(length) { stream.readInt() } + } + 12 -> { + val length = stream.readInt() + LongArray(length) { stream.readLong() } + } + else -> error("Unknown tag type $tagId") + } + + private fun wrapNbt(data: Any): Tag = when (data) { + is Byte -> ByteTag(data) + is Short -> ShortTag(data) + is Int -> IntTag(data) + is Long -> LongTag(data) + is Float -> FloatTag(data) + is Double -> DoubleTag(data) + is String -> StringTag(data) + is ByteArray -> ByteArrayTag(data) + is IntArray -> IntArrayTag(data) + is LongArray -> LongArrayTag(data) + is Map<*, *> -> CompoundTag(data.map { it.key as String to wrapNbt(it.value!!) }.toMap()) + is List<*> -> ListTag(data.firstOrNull()?.let { nbtClass(it) } ?: EndTag::class.java, data.map { wrapNbt(it!!) }) + else -> error("Unknown tag type $data") + } + + private fun nbtClass(data: Any): Class = when (data) { + is Byte -> ByteTag::class.java + is Short -> ShortTag::class.java + is Int -> IntTag::class.java + is Long -> LongTag::class.java + is Float -> FloatTag::class.java + is Double -> DoubleTag::class.java + is String -> StringTag::class.java + is ByteArray -> ByteArrayTag::class.java + is IntArray -> IntArrayTag::class.java + is LongArray -> LongArrayTag::class.java + is Map<*, *> -> CompoundTag::class.java + is List<*> -> ListTag::class.java + else -> error("Unknown tag type $data, ${data::class.java.simpleName}") + } + + override fun toString(): String { + return "ChaosSchematicReader(platform=$platform, liveDataVersion=$liveDataVersion, wrapped=$wrapped, schematicVersion=$schematicVersion, dataVersion=$dataVersion, metadata=$metadata, width=$width, height=$height, length=$length, offset=${offset.contentToString()}, blocks=$blocks, entities=$entities, fixer=$fixer)" + } + + + data class Blocks( + var data: ByteArray?, + var palette: Map?, + var blockEntities: List? + ) +} \ No newline at end of file diff --git a/SpigotCore/SpigotCore_14/src/de/steamwar/core/WorldEditWrapper14.java b/SpigotCore/SpigotCore_14/src/de/steamwar/core/WorldEditWrapper14.java index 60e4758d..08ad6a1a 100644 --- a/SpigotCore/SpigotCore_14/src/de/steamwar/core/WorldEditWrapper14.java +++ b/SpigotCore/SpigotCore_14/src/de/steamwar/core/WorldEditWrapper14.java @@ -89,7 +89,7 @@ public class WorldEditWrapper14 implements WorldEditWrapper { switch (schemFormat) { case SPONGE_V2: case SPONGE_V3: - return new SpongeSchematicReader(new NBTInputStream(is), this).read(); + return new ChaosSchematicReader(new DataInputStream(is), this).read(); case MCEDIT: return new MCEditSchematicReader(new NBTInputStream(is)).read(); default: @@ -576,8 +576,6 @@ public class WorldEditWrapper14 implements WorldEditWrapper { values.putIfAbsent("z", new IntTag(pt.getBlockZ())); } values.putIfAbsent("id", values.get("Id")); - values.remove("Id"); - values.remove("Pos"); if (fixer != null) { tileEntity = wrapper.applyDataFixer(fixer, dataVersion, values); } else { diff --git a/SpigotCore/SpigotCore_21/src/de/steamwar/core/WorldEditWrapper21.java b/SpigotCore/SpigotCore_21/src/de/steamwar/core/WorldEditWrapper21.java index 15256d90..14f78b07 100644 --- a/SpigotCore/SpigotCore_21/src/de/steamwar/core/WorldEditWrapper21.java +++ b/SpigotCore/SpigotCore_21/src/de/steamwar/core/WorldEditWrapper21.java @@ -20,29 +20,32 @@ package de.steamwar.core; import com.sk89q.jnbt.NBTInputStream; +import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.extent.clipboard.io.BuiltInClipboardFormat; import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter; import com.sk89q.worldedit.extent.clipboard.io.MCEditSchematicReader; -import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV2Reader; -import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV3Reader; import com.sk89q.worldedit.math.Vector3; import com.sk89q.worldedit.math.transform.Transform; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.session.ClipboardHolder; +import com.sk89q.worldedit.world.DataFixer; import de.steamwar.sql.NodeData; import org.bukkit.entity.Player; import org.bukkit.util.Vector; -import org.enginehub.linbus.stream.LinBinaryIO; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinTag; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; -public class WorldEditWrapper21 implements WorldEditWrapper { +public class WorldEditWrapper21 extends WorldEditWrapper18 { @Override public InputStream getPlayerClipboard(Player player) { @@ -72,22 +75,10 @@ public class WorldEditWrapper21 implements WorldEditWrapper { @Override @SuppressWarnings("removal") public Clipboard getClipboard(InputStream is, NodeData.SchematicFormat ignored) throws IOException { - ResetableInputStream ris = new ResetableInputStream(is); - for (NodeData.SchematicFormat schemFormat : NodeData.SchematicFormat.values()) { - try { - Clipboard clipboard = switch (schemFormat) { - case MCEDIT -> new MCEditSchematicReader(new NBTInputStream(ris)).read(); - case SPONGE_V2 -> new SpongeSchematicV2Reader(LinBinaryIO.read(new DataInputStream(ris))).read(); - case SPONGE_V3 -> new SpongeSchematicV3Reader(LinBinaryIO.read(new DataInputStream(ris))).read(); - }; - ris.close(); - return clipboard; - } catch (Exception e) { - // Ignore - } - ris.reset(); - } - throw new IOException("No clipboard found"); + return switch (ignored) { + case MCEDIT -> new MCEditSchematicReader(new NBTInputStream(is)).read(); + case SPONGE_V2, SPONGE_V3 -> new ChaosSchematicReader(new DataInputStream(is), this).read(); + }; } private class ResetableInputStream extends InputStream {