Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f6b0772f0 | ||
|
|
8d4b7df1ce |
No files matched your search
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -1,11 +1,11 @@
|
|||||||
group = lv.janhouse
|
group = lv.janhouse
|
||||||
version = 0.1.0
|
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;
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ public final class Settings {
|
|||||||
public final int clientPollIntervalTicks;
|
public final int clientPollIntervalTicks;
|
||||||
public final int helloDelayTicks;
|
public final int helloDelayTicks;
|
||||||
|
|
||||||
public final String serverName;
|
|
||||||
|
|
||||||
public final WorldSettings defaultWorld;
|
public final WorldSettings defaultWorld;
|
||||||
public final Map<String, WorldSettings> worlds;
|
public final Map<String, WorldSettings> worlds;
|
||||||
|
|
||||||
@@ -66,7 +64,9 @@ public final class Settings {
|
|||||||
clientPollIntervalTicks = clamp(c.getInt("sync.client-poll-interval-ticks", 6000), 1200, 72000, "sync.client-poll-interval-ticks", warn);
|
clientPollIntervalTicks = clamp(c.getInt("sync.client-poll-interval-ticks", 6000), 1200, 72000, "sync.client-poll-interval-ticks", warn);
|
||||||
helloDelayTicks = clamp(c.getInt("sync.hello-delay-ticks", 100), 1, 1200, "sync.hello-delay-ticks", warn);
|
helloDelayTicks = clamp(c.getInt("sync.hello-delay-ticks", 100), 1, 1200, "sync.hello-delay-ticks", warn);
|
||||||
|
|
||||||
serverName = c.getString("server-name", "");
|
if (c.contains("server-name")) {
|
||||||
|
warn.accept("server-name is ignored: the released MapSyncer client cannot read that hello field.");
|
||||||
|
}
|
||||||
|
|
||||||
ConfigurationSection ws = c.getConfigurationSection("worlds");
|
ConfigurationSection ws = c.getConfigurationSection("worlds");
|
||||||
WorldSettings def = new WorldSettings(true, TrackedWorld.Layers.AUTO);
|
WorldSettings def = new WorldSettings(true, TrackedWorld.Layers.AUTO);
|
||||||
|
|||||||
@@ -74,16 +74,20 @@ public final class Protocol {
|
|||||||
|
|
||||||
// -------------------------------------------------------------- encoders
|
// -------------------------------------------------------------- encoders
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Five fields, no more. Upstream's git HEAD appends a sixth (serverName), but
|
||||||
|
* the released 1.0.4 client reads exactly five and vanilla disconnects on
|
||||||
|
* "8 bytes extra" if anything follows — verified against the shipped jar.
|
||||||
|
*/
|
||||||
public static byte[] encodeServerInstalled(String version, long lastGenerationTimestamp,
|
public static byte[] encodeServerInstalled(String version, long lastGenerationTimestamp,
|
||||||
int autoSyncIntervalMinutes, UpdateMode mode,
|
int autoSyncIntervalMinutes, UpdateMode mode,
|
||||||
int incrementalUpdateIntervalTicks, String serverName) {
|
int incrementalUpdateIntervalTicks) {
|
||||||
return new PacketWriter(64)
|
return new PacketWriter(64)
|
||||||
.writeUtf(version)
|
.writeUtf(version)
|
||||||
.writeLong(lastGenerationTimestamp)
|
.writeLong(lastGenerationTimestamp)
|
||||||
.writeInt(autoSyncIntervalMinutes)
|
.writeInt(autoSyncIntervalMinutes)
|
||||||
.writeByte(mode.ordinal())
|
.writeByte(mode.ordinal())
|
||||||
.writeInt(incrementalUpdateIntervalTicks)
|
.writeInt(incrementalUpdateIntervalTicks)
|
||||||
.writeUtf(serverName == null ? "" : serverName)
|
|
||||||
.toByteArray();
|
.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ public final class SyncService implements PluginMessageListener, Listener {
|
|||||||
int minutes = Math.max(1, ticks / 20 / 60);
|
int minutes = Math.max(1, ticks / 20 / 60);
|
||||||
byte[] payload = Protocol.encodeServerInstalled(
|
byte[] payload = Protocol.encodeServerInstalled(
|
||||||
"mapsyncer-paper/" + plugin.getPluginMeta().getVersion(),
|
"mapsyncer-paper/" + plugin.getPluginMeta().getVersion(),
|
||||||
lastGen, minutes, Protocol.UpdateMode.TICK, ticks, settings.serverName);
|
lastGen, minutes, Protocol.UpdateMode.TICK, ticks);
|
||||||
player.sendPluginMessage(plugin, Protocol.SERVER_INSTALLED, payload);
|
player.sendPluginMessage(plugin, Protocol.SERVER_INSTALLED, payload);
|
||||||
s.helloSent = true;
|
s.helloSent = true;
|
||||||
helloCount.incrementAndGet();
|
helloCount.incrementAndGet();
|
||||||
|
|||||||
@@ -58,10 +58,6 @@ sync:
|
|||||||
# cache, otherwise it forgets what it already has and re-syncs on every join.
|
# cache, otherwise it forgets what it already has and re-syncs on every join.
|
||||||
hello-delay-ticks: 100
|
hello-delay-ticks: 100
|
||||||
|
|
||||||
# Optional stable name sent to clients. The client mod uses it to recognise this
|
|
||||||
# server when it is reached under a different address. Leave empty to disable.
|
|
||||||
server-name: ""
|
|
||||||
|
|
||||||
# Per-world settings. Keys are Bukkit world names. Anything not listed uses
|
# Per-world settings. Keys are Bukkit world names. Anything not listed uses
|
||||||
# "default". `layers` controls what is rendered:
|
# "default". `layers` controls what is rendered:
|
||||||
# auto surface for worlds with a sky, cave layers for worlds with a ceiling
|
# auto surface for worlds with a sky, cave layers for worlds with a ceiling
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,6 +138,20 @@ class ProtocolTest {
|
|||||||
assertFalse(Protocol.split(0, 0, "null", new byte[100], 1, Protocol.SURFACE_LAYER).get(0).isSplit());
|
assertFalse(Protocol.split(0, 0, "null", new byte[100], 1, Protocol.SURFACE_LAYER).get(0).isSplit());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void helloHasExactlyFiveFields() {
|
||||||
|
// The released 1.0.4 client reads utf, long, int, byte, int and nothing
|
||||||
|
// else; a trailing byte disconnects the player.
|
||||||
|
byte[] hello = Protocol.encodeServerInstalled("v", 123L, 5, Protocol.UpdateMode.TICK, 6000);
|
||||||
|
PacketReader r = new PacketReader(hello);
|
||||||
|
assertEquals("v", r.readUtf());
|
||||||
|
assertEquals(123L, r.readLong());
|
||||||
|
assertEquals(5, r.readInt());
|
||||||
|
assertEquals(1, r.readByte());
|
||||||
|
assertEquals(6000, r.readInt());
|
||||||
|
assertEquals(0, r.readableBytes());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void hashValidity() {
|
void hashValidity() {
|
||||||
assertTrue(Protocol.isValidHash("a1b2c3d4"));
|
assertTrue(Protocol.isValidHash("a1b2c3d4"));
|
||||||
|
|||||||
Reference in new issue
Block a user