1 Commits
Author SHA1 Message Date
janhouse 9f6b0772f0 mapsyncer-paper 0.2.0: Paper 26.3
Build / build (push) Successful in 3m6s
Build against the 26.3 dev bundle (26.3.build.151-beta; no stable build
exists yet).

26.3 rewrote the block palette in region files: a default-state block is a
bare string, any other state is {id, properties}, and a list mixing both
wraps the strings as {"": name}. The parser cast every entry to a
compound with Name/Properties, so every chunk saved by 26.3 threw
ClassCastException and the whole region failed to render. Both formats are
read now (a region holds a mix until every chunk is re-saved), and a chunk
that fails to parse is skipped instead of aborting the region.
2026-10-04 18:18:09 +03:00
6 changed files with 111 additions and 9 deletions

No files matched your search

+5
View File
@@ -28,6 +28,11 @@ diffs stay readable:
- `mca/ChunkDataParser.java` — heightmap bit width uses vanilla's - `mca/ChunkDataParser.java` — heightmap bit width uses vanilla's
`ceillog2(height + 1)` (upstream under-counts by one for 256-high `ceillog2(height + 1)` (upstream under-counts by one for 256-high
dimensions); accepted chunk statuses include `initialize_light`. 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`. - `XaeroWriter.java` — moved from `com.mapsyncer.server` to `com.mapsyncer`.
- `render/BlockPropertyResolver.java`, `render/PlaceholderBlockGetter.java` — - `render/BlockPropertyResolver.java`, `render/PlaceholderBlockGetter.java` —
package moved; the cache-size constant is inlined. package moved; the cache-size constant is inlined.
+1 -1
View File
@@ -37,7 +37,7 @@ Two visibility modes (`config.yml`):
## Requirements ## 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). internals via paperweight; other versions need a rebuild).
- Clients: Xaero's World Map 1.40.11+ and the MapSyncer client mod for the - Clients: Xaero's World Map 1.40.11+ and the MapSyncer client mod for the
same Minecraft version. same Minecraft version.
+3 -3
View File
@@ -1,11 +1,11 @@
group = lv.janhouse group = lv.janhouse
version = 0.1.1 version = 0.2.0
# Paper dev bundle (Mojang-mapped server + API) the plugin compiles against. # 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 # needs a handful of server internals (block classes, render shape, tags), which
# is why this is a dev bundle and not plain paper-api. # 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 # 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. # be compiled for anything newer; --release also fences off newer JDK APIs.
@@ -263,8 +263,7 @@ public class ChunkSectionParser {
if (blockStates.contains("palette", Tag.TAG_LIST)) { if (blockStates.contains("palette", Tag.TAG_LIST)) {
Tag.ListTag paletteList = blockStates.getList("palette", Tag.TAG_COMPOUND); Tag.ListTag paletteList = blockStates.getList("palette", Tag.TAG_COMPOUND);
for (int i = 0; i < paletteList.items().size(); i++) { for (int i = 0; i < paletteList.items().size(); i++) {
Tag.Compound stateTag = (Tag.Compound) paletteList.items().get(i); BlockState blockState = parsePaletteEntry(paletteList.items().get(i));
BlockState blockState = parseBlockState(stateTag);
blockPalette.add(blockState); blockPalette.add(blockState);
blockNames.add(blockState.name()); 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.
*
* <p>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.</p>
*/
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 * 解析单个方块状态的NBT
* *
* <p>格式: {Name: "minecraft:grass_block", Properties: {snowy: "false"}}</p> * <p>格式: {Name: "minecraft:grass_block", Properties: {snowy: "false"}}
* (26.3+: {@code id} / {@code properties}, or {@code ""} for a wrapped bare name)</p>
* *
* @param stateTag 方块状态的NBT复合标签 * @param stateTag 方块状态的NBT复合标签
* @return 解析后的BlockState对象 * @return 解析后的BlockState对象
*/ */
private static BlockState parseBlockState(Tag.Compound stateTag) { private static BlockState parseBlockState(Tag.Compound stateTag) {
String name = stateTag.getString("Name"); 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); return new BlockState(name, BlockState.EMPTY_PROPERTIES);
} }
Tag.Compound propsTag = stateTag.getCompound("Properties"); Tag.Compound propsTag = stateTag.getCompound(propsKey);
if (propsTag.children().isEmpty()) { if (propsTag.children().isEmpty()) {
return new BlockState(name, BlockState.EMPTY_PROPERTIES); return new BlockState(name, BlockState.EMPTY_PROPERTIES);
} }
@@ -136,7 +136,12 @@ public final class McaRegionLoader {
if (nbt == null) { if (nbt == null) {
continue; continue;
} }
try {
grid[localX][localZ] = ChunkDataParser.parseChunk(localX, localZ, nbt, worldHeightRange); 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; return grid;
@@ -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<Tag> palette) {
Map<String, Tag> blockStates = new LinkedHashMap<>();
blockStates.put("palette", new Tag.ListTag("palette", Tag.TAG_COMPOUND, palette));
Map<String, Tag> 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<String, Tag> children) {
return new Tag.Compound("", children);
}
}