protocol: hello carries five fields; the released client reads no serverName
Build / build (push) Successful in 1m18s

Upstream's git HEAD appends serverName to server_installed, but the shipped
1.0.4 client (fabric-26.2) decodes exactly version/lastGen/interval/mode/
ticks and vanilla disconnects the player with "8 bytes extra" on the first
hello. Verified with javap on the release jar; the other three codecs match.
Drops the server-name option (a leftover key now only warns).

Claude-Session: https://claude.ai/code/session_011FePLXwBsCGLTzaSkk1Z6V
This commit is contained in:
2026-09-07 00:24:43 +03:00
parent 93164378e2
commit 8d4b7df1ce
6 changed files with 25 additions and 11 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
group = lv.janhouse
version = 0.1.0
version = 0.1.1
# 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
@@ -34,8 +34,6 @@ public final class Settings {
public final int clientPollIntervalTicks;
public final int helloDelayTicks;
public final String serverName;
public final WorldSettings defaultWorld;
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);
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");
WorldSettings def = new WorldSettings(true, TrackedWorld.Layers.AUTO);
@@ -74,16 +74,20 @@ public final class Protocol {
// -------------------------------------------------------------- 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,
int autoSyncIntervalMinutes, UpdateMode mode,
int incrementalUpdateIntervalTicks, String serverName) {
int incrementalUpdateIntervalTicks) {
return new PacketWriter(64)
.writeUtf(version)
.writeLong(lastGenerationTimestamp)
.writeInt(autoSyncIntervalMinutes)
.writeByte(mode.ordinal())
.writeInt(incrementalUpdateIntervalTicks)
.writeUtf(serverName == null ? "" : serverName)
.toByteArray();
}
@@ -199,7 +199,7 @@ public final class SyncService implements PluginMessageListener, Listener {
int minutes = Math.max(1, ticks / 20 / 60);
byte[] payload = Protocol.encodeServerInstalled(
"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);
s.helloSent = true;
helloCount.incrementAndGet();
-4
View File
@@ -58,10 +58,6 @@ sync:
# cache, otherwise it forgets what it already has and re-syncs on every join.
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
# "default". `layers` controls what is rendered:
# auto surface for worlds with a sky, cave layers for worlds with a ceiling
@@ -138,6 +138,20 @@ class ProtocolTest {
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
void hashValidity() {
assertTrue(Protocol.isValidHash("a1b2c3d4"));