diff --git a/NOTICE.md b/NOTICE.md index 9a79b7e..412e10e 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -28,6 +28,11 @@ diffs stay readable: - `mca/ChunkDataParser.java` — heightmap bit width uses vanilla's `ceillog2(height + 1)` (upstream under-counts by one for 256-high dimensions); accepted chunk statuses include `initialize_light`. +- `mca/ChunkSectionParser.java` — reads the 26.3 block palette format (bare + string entries, `{id, properties}`, `{"": name}`) alongside the old + `{Name, Properties}` one. Same approach as upstream PR #26. +- `mca/convert/io/McaRegionLoader.java` — a chunk that fails to parse is + skipped instead of aborting the region. - `XaeroWriter.java` — moved from `com.mapsyncer.server` to `com.mapsyncer`. - `render/BlockPropertyResolver.java`, `render/PlaceholderBlockGetter.java` — package moved; the cache-size constant is inlined. diff --git a/README.md b/README.md index f5ffc86..2998cb0 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Two visibility modes (`config.yml`): ## Requirements -- Paper 26.2 on Java 25 (the build compiles against Paper's Mojang-mapped +- Paper 26.3 on Java 25 (the build compiles against Paper's Mojang-mapped internals via paperweight; other versions need a rebuild). - Clients: Xaero's World Map 1.40.11+ and the MapSyncer client mod for the same Minecraft version. diff --git a/gradle.properties b/gradle.properties index ef56560..7f84193 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,11 @@ group = lv.janhouse -version = 0.1.1 +version = 0.2.0 # Paper dev bundle (Mojang-mapped server + API) the plugin compiles against. -# Must match the server it is deployed on: Paper 26.2 on JDK 25. The renderer +# Must match the server it is deployed on: Paper 26.3 on JDK 25. The renderer # needs a handful of server internals (block classes, render shape, tags), which # is why this is a dev bundle and not plain paper-api. -paperDevBundleVersion = 26.2.build.121-stable +paperDevBundleVersion = 26.3.build.151-beta # Class-file target. The server container runs OpenJDK 25, so the jar must not # be compiled for anything newer; --release also fences off newer JDK APIs. diff --git a/src/main/java/com/mapsyncer/mca/ChunkSectionParser.java b/src/main/java/com/mapsyncer/mca/ChunkSectionParser.java index 4a8d06c..a36c784 100644 --- a/src/main/java/com/mapsyncer/mca/ChunkSectionParser.java +++ b/src/main/java/com/mapsyncer/mca/ChunkSectionParser.java @@ -263,8 +263,7 @@ public class ChunkSectionParser { if (blockStates.contains("palette", Tag.TAG_LIST)) { Tag.ListTag paletteList = blockStates.getList("palette", Tag.TAG_COMPOUND); for (int i = 0; i < paletteList.items().size(); i++) { - Tag.Compound stateTag = (Tag.Compound) paletteList.items().get(i); - BlockState blockState = parseBlockState(stateTag); + BlockState blockState = parsePaletteEntry(paletteList.items().get(i)); blockPalette.add(blockState); blockNames.add(blockState.name()); } @@ -345,22 +344,57 @@ public class ChunkSectionParser { ); } + /** + * One {@code block_states.palette} entry, in any of the shapes a region file + * can hold. + * + *

Up to 26.2 every entry is {@code {Name: "...", Properties: {...}}}. From + * 26.3 a block in its default state is written as a bare string + * ({@code "minecraft:stone"}) and any other state as + * {@code {id: "...", properties: {...}}}; a list mixing both shapes has to be + * all compounds, so the bare strings are wrapped as {@code {"": "..."}}. + * A region keeps chunks in both formats until every chunk has been re-saved, + * so the old shape stays supported. No properties means the default state, + * which is also how Xaero reads a state written without them.

+ */ + private static BlockState parsePaletteEntry(Tag entry) { + if (entry instanceof Tag.StringTag str) { + return new BlockState(str.value(), BlockState.EMPTY_PROPERTIES); + } + if (entry instanceof Tag.Compound compound) { + return parseBlockState(compound); + } + // Keeps palette indices aligned with the data array. + return new BlockState("minecraft:air", BlockState.EMPTY_PROPERTIES); + } + /** * 解析单个方块状态的NBT * - *

格式: {Name: "minecraft:grass_block", Properties: {snowy: "false"}}

+ *

格式: {Name: "minecraft:grass_block", Properties: {snowy: "false"}} + * (26.3+: {@code id} / {@code properties}, or {@code ""} for a wrapped bare name)

* * @param stateTag 方块状态的NBT复合标签 * @return 解析后的BlockState对象 */ private static BlockState parseBlockState(Tag.Compound stateTag) { String name = stateTag.getString("Name"); + if (name.isEmpty()) { + name = stateTag.getString("id"); + } + if (name.isEmpty()) { + name = stateTag.getString(""); + } + if (name.isEmpty()) { + name = "minecraft:air"; + } - if (!stateTag.contains("Properties", Tag.TAG_COMPOUND)) { + String propsKey = stateTag.contains("Properties", Tag.TAG_COMPOUND) ? "Properties" : "properties"; + if (!stateTag.contains(propsKey, Tag.TAG_COMPOUND)) { return new BlockState(name, BlockState.EMPTY_PROPERTIES); } - Tag.Compound propsTag = stateTag.getCompound("Properties"); + Tag.Compound propsTag = stateTag.getCompound(propsKey); if (propsTag.children().isEmpty()) { return new BlockState(name, BlockState.EMPTY_PROPERTIES); } diff --git a/src/main/java/com/mapsyncer/mca/convert/io/McaRegionLoader.java b/src/main/java/com/mapsyncer/mca/convert/io/McaRegionLoader.java index 5c19fc3..e7424e5 100644 --- a/src/main/java/com/mapsyncer/mca/convert/io/McaRegionLoader.java +++ b/src/main/java/com/mapsyncer/mca/convert/io/McaRegionLoader.java @@ -136,7 +136,12 @@ public final class McaRegionLoader { if (nbt == null) { continue; } - grid[localX][localZ] = ChunkDataParser.parseChunk(localX, localZ, nbt, worldHeightRange); + try { + grid[localX][localZ] = ChunkDataParser.parseChunk(localX, localZ, nbt, worldHeightRange); + } catch (RuntimeException e) { + // One malformed chunk must not cost the whole region. + LOGGER.warn("Failed to parse chunk ({}, {}), skipping: {}", localX, localZ, e.toString()); + } } } return grid; diff --git a/src/test/java/com/mapsyncer/mca/ChunkSectionParserTest.java b/src/test/java/com/mapsyncer/mca/ChunkSectionParserTest.java new file mode 100644 index 0000000..dcbad71 --- /dev/null +++ b/src/test/java/com/mapsyncer/mca/ChunkSectionParserTest.java @@ -0,0 +1,58 @@ +package com.mapsyncer.mca; + +import com.mapsyncer.nbt.Tag; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** Palette shapes as written by 26.2 and 26.3 servers (DataVersion 4790 / 5023). */ +class ChunkSectionParserTest { + + @Test + void readsPre263Compounds() { + var section = parse(List.of( + compound(Map.of("Name", str("minecraft:stone"))), + compound(Map.of("Name", str("minecraft:oak_log"), + "Properties", compound(Map.of("axis", str("x"))))))); + assertEquals(List.of("minecraft:stone", "minecraft:oak_log"), section.blockNames()); + assertEquals(Map.of("axis", "x"), section.blockPalette().get(1).properties()); + } + + @Test + void readsBareStrings() { + var section = parse(List.of(str("minecraft:bedrock"), str("minecraft:deepslate"))); + assertEquals(List.of("minecraft:bedrock", "minecraft:deepslate"), section.blockNames()); + assertEquals(Map.of(), section.blockPalette().get(1).properties()); + } + + @Test + void readsMixed263Compounds() { + var section = parse(List.of( + compound(Map.of("", str("minecraft:deepslate"))), + compound(Map.of("id", str("minecraft:pointed_dripstone"), + "properties", compound(Map.of("vertical_direction", str("down"))))))); + assertEquals(List.of("minecraft:deepslate", "minecraft:pointed_dripstone"), section.blockNames()); + assertEquals(Map.of("vertical_direction", "down"), section.blockPalette().get(1).properties()); + } + + private static ChunkSectionParser.SectionData parse(List palette) { + Map blockStates = new LinkedHashMap<>(); + blockStates.put("palette", new Tag.ListTag("palette", Tag.TAG_COMPOUND, palette)); + Map section = new LinkedHashMap<>(); + section.put("Y", new Tag.Byte("Y", (byte) 0)); + section.put("block_states", new Tag.Compound("block_states", blockStates)); + return ChunkSectionParser.parseSection(new Tag.Compound("", section)); + } + + private static Tag.StringTag str(String value) { + return new Tag.StringTag("", value); + } + + private static Tag.Compound compound(Map children) { + return new Tag.Compound("", children); + } +}