diff --git a/build.gradle.kts b/build.gradle.kts index 950500d..33996af 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { testImplementation(platform("org.junit:junit-bom:5.9.1")) testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(libs.bundles.logback) } tasks.test { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 47d666b..d6dc792 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,14 @@ metadata.format.version = "1.1" [versions] -minestom = "e9d0098418" +minestom = "8715f4305d" +logback = "1.4.5" # For tests only [libraries] minestom = { group = "dev.hollowcube", name = "minestom-ce", version.ref = "minestom" } + +logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = "logback" } +logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } + +[bundles] +logback = ["logback-core", "logback-classic"] diff --git a/src/main/java/net/hollowcube/schem/SchematicReader.java b/src/main/java/net/hollowcube/schem/SchematicReader.java index 6c9b239..183c94e 100644 --- a/src/main/java/net/hollowcube/schem/SchematicReader.java +++ b/src/main/java/net/hollowcube/schem/SchematicReader.java @@ -2,6 +2,7 @@ import net.minestom.server.command.builder.arguments.minecraft.ArgumentBlockState; +import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import net.minestom.server.utils.validate.Check; @@ -43,16 +44,33 @@ private SchematicReader() { try { NBTCompound tag = (NBTCompound) reader.read(); - Short width = tag.getShort("Width"); - Check.notNull(width, "Missing required field 'Width'"); - Short height = tag.getShort("Height"); - Check.notNull(height, "Missing required field 'Height'"); - Short length = tag.getShort("Length"); - Check.notNull(length, "Missing required field 'Length'"); + // If it has a Schematic tag is sponge v2 or 3 + var schematicTag = tag.getCompound("Schematic"); + if (schematicTag != null) { + Integer version = schematicTag.getInt("Version"); + Check.notNull(version, "Missing required field 'Schematic.Version'"); + return read(schematicTag, version); + } - NBTCompound metadata = tag.getCompound("Metadata"); - Check.notNull(metadata, "Missing required field 'Metadata'"); + // Otherwise it is hopefully v1 + return read(tag, 1); + } catch (Exception e) { + throw new SchematicReadException("Invalid schematic file", e); + } + } + + private static @NotNull Schematic read(@NotNull NBTCompound tag, int version) { + Short width = tag.getShort("Width"); + Check.notNull(width, "Missing required field 'Width'"); + Short height = tag.getShort("Height"); + Check.notNull(height, "Missing required field 'Height'"); + Short length = tag.getShort("Length"); + Check.notNull(length, "Missing required field 'Length'"); + + NBTCompound metadata = tag.getCompound("Metadata"); + var offset = Vec.ZERO; + if (metadata != null && metadata.containsKey("WEOffsetX")) { Integer offsetX = metadata.getInt("WEOffsetX"); Check.notNull(offsetX, "Missing required field 'Metadata.WEOffsetX'"); Integer offsetY = metadata.getInt("WEOffsetY"); @@ -60,31 +78,44 @@ private SchematicReader() { Integer offsetZ = metadata.getInt("WEOffsetZ"); Check.notNull(offsetZ, "Missing required field 'Metadata.WEOffsetZ'"); - NBTCompound palette = tag.getCompound("Palette"); + offset = new Vec(offsetX, offsetY, offsetZ); + } //todo handle sponge Offset + + NBTCompound palette; + ImmutableByteArray blockArray; + Integer paletteSize; + if (version == 1) { + palette = tag.getCompound("Palette"); Check.notNull(palette, "Missing required field 'Palette'"); - ImmutableByteArray blockArray = tag.getByteArray("BlockData"); + blockArray = tag.getByteArray("BlockData"); Check.notNull(blockArray, "Missing required field 'BlockData'"); - - Integer paletteSize = tag.getInt("PaletteMax"); + paletteSize = tag.getInt("PaletteMax"); Check.notNull(paletteSize, "Missing required field 'PaletteMax'"); + } else { + var blockEntries = tag.getCompound("Blocks"); + Check.notNull(blockEntries, "Missing required field 'Blocks'"); + + palette = blockEntries.getCompound("Palette"); + Check.notNull(palette, "Missing required field 'Blocks.Palette'"); + blockArray = blockEntries.getByteArray("Data"); + Check.notNull(blockArray, "Missing required field 'Blocks.Data'"); + paletteSize = palette.getSize(); + } - Block[] paletteBlocks = new Block[paletteSize]; + Block[] paletteBlocks = new Block[paletteSize]; - palette.forEach((key, value) -> { - int assigned = ((NBTInt) value).getValue(); - Block block = ArgumentBlockState.staticParse(key); - paletteBlocks[assigned] = block; - }); + palette.forEach((key, value) -> { + int assigned = ((NBTInt) value).getValue(); + Block block = ArgumentBlockState.staticParse(key); + paletteBlocks[assigned] = block; + }); - return new Schematic( - new Vec(width, height, length), - new Vec(offsetX, offsetY, offsetZ), - paletteBlocks, - blockArray.copyArray() - ); - } catch (Exception e) { - throw new SchematicReadException("Invalid schematic file", e); - } + return new Schematic( + new Vec(width, height, length), + offset, + paletteBlocks, + blockArray.copyArray() + ); } } diff --git a/src/test/java/net/hollowcube/schem/TestSchematicReaderRegressions.java b/src/test/java/net/hollowcube/schem/TestSchematicReaderRegressions.java new file mode 100644 index 0000000..33361d5 --- /dev/null +++ b/src/test/java/net/hollowcube/schem/TestSchematicReaderRegressions.java @@ -0,0 +1,33 @@ +package net.hollowcube.schem; + +import net.minestom.server.coordinate.Vec; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class TestSchematicReaderRegressions { + + @Test + public void testReadFail1_20_1() { + var schem = assertReadSchematic("/regression/1_20_1_read_fail.schem"); + assertEquals(new Vec(15, 16, 20), schem.size()); + } + + @Test + public void testSpongeV1() { + var schem = assertReadSchematic("/regression/sponge_1.schem"); + assertEquals(new Vec(217, 70, 173), schem.size()); + } + + private @NotNull Schematic assertReadSchematic(@NotNull String path) { + try (var is = getClass().getResourceAsStream(path)) { + assertNotNull(is, "Failed to load resource: " + path); + return SchematicReader.read(is); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/test/java/net/hollowcube/schem/demo/DemoServer.java b/src/test/java/net/hollowcube/schem/demo/DemoServer.java new file mode 100644 index 0000000..6033e31 --- /dev/null +++ b/src/test/java/net/hollowcube/schem/demo/DemoServer.java @@ -0,0 +1,61 @@ +package net.hollowcube.schem.demo; + +import net.hollowcube.schem.Rotation; +import net.minestom.server.MinecraftServer; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.CommandContext; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerLoginEvent; +import net.minestom.server.event.player.PlayerSpawnEvent; +import net.minestom.server.instance.LightingChunk; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +public class DemoServer { + public static void main(String[] args) { + var server = MinecraftServer.init(); + + var instances = MinecraftServer.getInstanceManager(); + var instance = instances.createInstanceContainer(); + instance.setChunkSupplier(LightingChunk::new); + instance.setGenerator(unit -> unit.modifier().fillHeight(0, 39, Block.STONE)); + + var events = MinecraftServer.getGlobalEventHandler(); + events.addListener(PlayerLoginEvent.class, event -> { + event.setSpawningInstance(instance); + event.getPlayer().setRespawnPoint(new Pos(0, 40, 0)); + }); + events.addListener(PlayerSpawnEvent.class, event -> { + var player = event.getPlayer(); + + player.setGameMode(GameMode.CREATIVE); + }); + + var commands = MinecraftServer.getCommandManager(); + commands.register(new Command("paste") { + { + addSyntax(this::execute, ArgumentType.StringArray("path")); + } + + public void execute(@NotNull CommandSender sender, @NotNull CommandContext context) { + var player = (Player) sender; + + try (var is = getClass().getResourceAsStream("/" + String.join(" ", context.get("path")) + ".schem")) { + var schem = net.hollowcube.schem.SchematicReader.read(is); + schem.build(Rotation.NONE, false).apply(instance, player.getPosition(), () -> { + player.sendMessage("Done!"); + }); + } catch (Exception e) { + player.sendMessage("Failed to paste schematic: " + e.getMessage()); + e.printStackTrace(); + } + } + }); + + server.start("localhost", 25565); + } +} diff --git a/src/test/resources/regression/1_20_1_read_fail.schem b/src/test/resources/regression/1_20_1_read_fail.schem new file mode 100644 index 0000000..8cbf746 Binary files /dev/null and b/src/test/resources/regression/1_20_1_read_fail.schem differ diff --git a/src/test/resources/regression/sponge_1.schem b/src/test/resources/regression/sponge_1.schem new file mode 100644 index 0000000..eae6671 Binary files /dev/null and b/src/test/resources/regression/sponge_1.schem differ