From 77430e18e74bfc8d56354fd173887ad347d060b2 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 13 Jun 2025 05:15:46 +0100 Subject: [PATCH 1/5] Prevent packets from having dangerously sized collections Copied near-verbatim from #110 --- .../minecraft/mapsync/mod/MapSyncMod.java | 55 +++++++++++++------ .../ClientboundRegionTimestampsPacket.java | 32 +++++------ ...rverboundChunkTimestampsRequestPacket.java | 14 ++--- mapsync-server/src/cli.ts | 16 ++++-- mapsync-server/src/database.ts | 48 +++++++--------- mapsync-server/src/main.ts | 20 ++++--- mapsync-server/src/model.ts | 6 ++ .../src/protocol/RegionCatchupPacket.ts | 20 +++---- .../src/protocol/RegionTimestampsPacket.ts | 18 +++--- mapsync-server/src/server.ts | 8 +-- 10 files changed, 123 insertions(+), 114 deletions(-) diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/MapSyncMod.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/MapSyncMod.java index 49b6e328..63f654ef 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/MapSyncMod.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/MapSyncMod.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; @@ -31,6 +32,7 @@ import net.minecraft.network.protocol.game.ClientboundLoginPacket; import net.minecraft.network.protocol.game.ClientboundRespawnPacket; import net.minecraft.resources.Identifier; +import net.minecraft.world.level.ChunkPos; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; @@ -272,23 +274,21 @@ public void handleRegionTimestamps(ClientboundRegionTimestampsPacket packet, Syn if (!dimension.dimension.identifier().toString().equals(packet.getDimension())) { return; } - var outdatedRegions = new ArrayList(); - for (var regionTs : packet.getTimestamps()) { - var regionPos = new RegionPos(regionTs.x(), regionTs.z()); - long oldestChunkTs = dimension.getOldestChunkTsInRegion(regionPos); - boolean requiresUpdate = regionTs.timestamp() > oldestChunkTs; - - debugLog("region " + regionPos - + (requiresUpdate ? " requires update." : " is up to date.") - + " oldest client chunk ts: " + oldestChunkTs - + ", newest server chunk ts: " + regionTs.timestamp()); - - if (requiresUpdate) { - outdatedRegions.add(regionPos); - } - } - client.send(new ServerboundChunkTimestampsRequestPacket(packet.getDimension(), outdatedRegions)); + var regionTs = packet.getTimestamp(); + + var regionPos = new RegionPos(regionTs.x(), regionTs.z()); + long oldestChunkTs = dimension.getOldestChunkTsInRegion(regionPos); + boolean requiresUpdate = regionTs.timestamp() > oldestChunkTs; + + debugLog("region " + regionPos + + (requiresUpdate ? " requires update." : " is up to date.") + + " oldest client chunk ts: " + oldestChunkTs + + ", newest server chunk ts: " + regionTs.timestamp()); + + if (requiresUpdate) { + client.send(new ServerboundChunkTimestampsRequestPacket(packet.getDimension(), regionPos)); + } } public void handleSharedChunk(ChunkTile chunkTile) { @@ -322,8 +322,27 @@ public void requestCatchupData(List chunks) { list.add(chunk); } for (List chunksForServer : byServer.values()) { - SyncClient client = chunksForServer.get(0).syncClient; - client.send(new ServerboundCatchupRequestPacket(chunksForServer)); + final SyncClient client = chunksForServer.getFirst().syncClient; + + final Map> regionChunkRequests = new HashMap<>(); + for (final CatchupChunk chunk : chunksForServer) { + final ChunkPos chunkPos = chunk.chunkPos(); + final Map regionChunks = regionChunkRequests.computeIfAbsent( + RegionPos.forChunkPos(chunkPos), + (regionPos) -> new HashMap<>() + ); + final CatchupChunk existingCatchup = regionChunks.get(chunkPos); + if (existingCatchup != null && existingCatchup.timestamp() > chunk.timestamp()) { + continue; + } + regionChunks.put(chunkPos, chunk); + } + + for (final Map catchupChunks : regionChunkRequests.values()) { + client.send(new ServerboundCatchupRequestPacket( + new ArrayList<>(catchupChunks.values()) + )); + } } } diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ClientboundRegionTimestampsPacket.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ClientboundRegionTimestampsPacket.java index 93f7b1a2..c8738ba0 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ClientboundRegionTimestampsPacket.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ClientboundRegionTimestampsPacket.java @@ -14,35 +14,29 @@ public class ClientboundRegionTimestampsPacket implements Packet { private final String dimension; - private final RegionTimestamp[] timestamps; + private final RegionTimestamp timestamp; - public ClientboundRegionTimestampsPacket(String dimension, RegionTimestamp[] timestamps) { + public ClientboundRegionTimestampsPacket(String dimension, RegionTimestamp timestamp) { this.dimension = dimension; - this.timestamps = timestamps; + this.timestamp = timestamp; } public String getDimension() { return dimension; } - public RegionTimestamp[] getTimestamps() { - return timestamps; + public RegionTimestamp getTimestamp() { + return timestamp; } public static Packet read(ByteBuf buf) { - String dimension = Packet.readUtf8String(buf); - - short totalRegions = buf.readShort(); - RegionTimestamp[] timestamps = new RegionTimestamp[totalRegions]; - // row = x - for (short i = 0; i < totalRegions; i++) { - short regionX = buf.readShort(); - short regionZ = buf.readShort(); - - long timestamp = buf.readLong(); - timestamps[i] = new RegionTimestamp(regionX, regionZ, timestamp); - } - - return new ClientboundRegionTimestampsPacket(dimension, timestamps); + return new ClientboundRegionTimestampsPacket( + Packet.readUtf8String(buf), + new RegionTimestamp( + buf.readShort(), + buf.readShort(), + buf.readLong() + ) + ); } } diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundChunkTimestampsRequestPacket.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundChunkTimestampsRequestPacket.java index 51e455da..b77a6b24 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundChunkTimestampsRequestPacket.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundChunkTimestampsRequestPacket.java @@ -3,7 +3,6 @@ import gjum.minecraft.mapsync.mod.data.RegionPos; import gjum.minecraft.mapsync.mod.net.Packet; import io.netty.buffer.ByteBuf; -import java.util.List; import org.jetbrains.annotations.NotNull; /** @@ -15,20 +14,17 @@ public class ServerboundChunkTimestampsRequestPacket implements Packet { public static final int PACKET_ID = 8; private final String dimension; - private final List regions; + private final RegionPos region; - public ServerboundChunkTimestampsRequestPacket(String dimension, List regions) { + public ServerboundChunkTimestampsRequestPacket(String dimension, RegionPos region) { this.dimension = dimension; - this.regions = regions; + this.region = region; } @Override public void write(@NotNull ByteBuf buf) { Packet.writeUtf8String(buf, dimension); - buf.writeShort(regions.size()); - for (var region : regions) { - buf.writeShort(region.x()); - buf.writeShort(region.z()); - } + buf.writeShort(region.x()); + buf.writeShort(region.z()); } } diff --git a/mapsync-server/src/cli.ts b/mapsync-server/src/cli.ts index e9ca26ea..4f517d86 100644 --- a/mapsync-server/src/cli.ts +++ b/mapsync-server/src/cli.ts @@ -149,12 +149,16 @@ async function handle_input(input: string): Promise { return; } - const regions = await database.getRegionTimestamps(world); - await client.send({ - type: "RegionTimestamps", - world, - regions, - }); + const regions = await database.getRegionTimestamps(client.world!); + await Promise.allSettled( + regions.map((region) => + client.send({ + type: "RegionTimestamps", + world: client.world!, + region, + }), + ), + ); } else if (command === "kick") { const target = extras.trim(); // IGN or UUID diff --git a/mapsync-server/src/database.ts b/mapsync-server/src/database.ts index 0b073c05..80caf225 100644 --- a/mapsync-server/src/database.ts +++ b/mapsync-server/src/database.ts @@ -1,7 +1,6 @@ import * as kysely from "kysely"; import sqlite from "better-sqlite3"; import { DATA_FOLDER } from "./metadata"; -import { type Pos2D } from "./model"; let database: kysely.Kysely | null = null; @@ -98,33 +97,28 @@ export function getRegionTimestamps(dimension: string) { /** * Converts an array of region coords into an array of timestamped chunk coords. */ -export async function getChunkTimestamps(dimension: string, regions: Pos2D[]) { +export async function getChunkTimestamps( + dimension: string, + regionX: number, + regionZ: number, +) { + const minChunkX = regionX << 4, + maxChunkX = minChunkX + 16; + const minChunkZ = regionZ << 4, + maxChunkZ = minChunkZ + 16; return get() - .with("regions", (db) => - db - .selectFrom("player_chunk") - .select([ - (eb) => - kysely.sql`(cast(floor(${eb.ref( - "chunk_x", - )} / 32.0) as int) || '_' || cast(floor(${eb.ref( - "chunk_z", - )} / 32.0) as int))`.as("region"), - "chunk_x as x", - "chunk_z as z", - (eb) => eb.fn.max("ts").as("timestamp"), - ]) - .where("world", "=", dimension) - .groupBy(["x", "z"]), - ) - .selectFrom("regions") - .select(["x as chunkX", "z as chunkZ", "timestamp"]) - .where( - "region", - "in", - regions.map((region) => region.x + "_" + region.z), - ) - .orderBy("timestamp", "desc") + .selectFrom("player_chunk") + .select([ + "chunk_x as chunkX", + "chunk_z as chunkZ", + (eb) => eb.fn.max("ts").as("timestamp"), + ]) + .where("world", "=", dimension) + .where("chunk_x", ">=", minChunkX) + .where("chunk_x", "<", maxChunkX) + .where("chunk_z", ">=", minChunkZ) + .where("chunk_z", "<", maxChunkZ) + .orderBy("ts", "desc") .execute(); } diff --git a/mapsync-server/src/main.ts b/mapsync-server/src/main.ts index f7fdb953..e09dbd4e 100644 --- a/mapsync-server/src/main.ts +++ b/mapsync-server/src/main.ts @@ -7,6 +7,7 @@ import { CatchupRequestPacket } from "./protocol/CatchupRequestPacket"; import { ChunkTilePacket } from "./protocol/ChunkTilePacket"; import { TcpClient, TcpServer } from "./server"; import { RegionCatchupPacket } from "./protocol/RegionCatchupPacket"; +import { CatchupChunk, RegionChunkTimestamps } from "./model"; let config: metadata.Config = null!; let main: Main = null!; @@ -50,12 +51,16 @@ export class Main { // TODO check version, mc server, user access - const timestamps = await database.getRegionTimestamps(client.world!); - client.send({ - type: "RegionTimestamps", - world: client.world!, - regions: timestamps, - }); + const regions = await database.getRegionTimestamps(client.world!); + await Promise.allSettled( + regions.map((region) => + client.send({ + type: "RegionTimestamps", + world: client.world!, + region, + }), + ), + ); } handleClientDisconnected(client: ProtocolClient) {} @@ -152,7 +157,8 @@ export class Main { const chunks = await database.getChunkTimestamps( pkt.world, - pkt.regions, + pkt.regionX, + pkt.regionZ, ); if (chunks.length) client.send({ type: "Catchup", world: pkt.world, chunks }); diff --git a/mapsync-server/src/model.ts b/mapsync-server/src/model.ts index dc990c2f..879c202a 100644 --- a/mapsync-server/src/model.ts +++ b/mapsync-server/src/model.ts @@ -14,3 +14,9 @@ export interface Pos2D { readonly x: number; readonly z: number; } + +export interface RegionChunkTimestamps { + readonly regionX: number; + readonly regionZ: number; + readonly chunks: Array; +} diff --git a/mapsync-server/src/protocol/RegionCatchupPacket.ts b/mapsync-server/src/protocol/RegionCatchupPacket.ts index 13890d9b..a7ebf13d 100644 --- a/mapsync-server/src/protocol/RegionCatchupPacket.ts +++ b/mapsync-server/src/protocol/RegionCatchupPacket.ts @@ -1,23 +1,19 @@ import { BufReader } from "./BufReader"; -import { type Pos2D } from "../model"; export interface RegionCatchupPacket { type: "RegionCatchup"; world: string; - regions: Pos2D[]; + regionX: number; + regionZ: number; } export namespace RegionCatchupPacket { export function decode(reader: BufReader): RegionCatchupPacket { - let world = reader.readString(); - const len = reader.readInt16(); - const regions: Pos2D[] = []; - for (let i = 0; i < len; i++) { - regions.push({ - x: reader.readInt16(), - z: reader.readInt16(), - }); - } - return { type: "RegionCatchup", world, regions }; + return { + type: "RegionCatchup", + world: reader.readString(), + regionX: reader.readInt16(), + regionZ: reader.readInt16(), + }; } } diff --git a/mapsync-server/src/protocol/RegionTimestampsPacket.ts b/mapsync-server/src/protocol/RegionTimestampsPacket.ts index e99a151c..fcf6b9b7 100644 --- a/mapsync-server/src/protocol/RegionTimestampsPacket.ts +++ b/mapsync-server/src/protocol/RegionTimestampsPacket.ts @@ -4,19 +4,15 @@ import { CatchupRegion } from "../model"; export interface RegionTimestampsPacket { type: "RegionTimestamps"; world: string; - regions: Array; + region: CatchupRegion; } export namespace RegionTimestampsPacket { - export function encode(pkt: RegionTimestampsPacket, writer: BufWriter) { - writer.writeString(pkt.world); - writer.writeInt16(pkt.regions.length); - console.log("Sending regions " + JSON.stringify(pkt.regions)); - for (let i = 0; i < pkt.regions.length; i++) { - let region = pkt.regions[i]; - writer.writeInt16(region.regionX); - writer.writeInt16(region.regionZ); - writer.writeInt64(region.timestamp); - } + export function encode(packet: RegionTimestampsPacket, writer: BufWriter) { + writer.writeString(packet.world); + console.log(`Sending region for [${packet.world}]`, packet); + writer.writeInt16(packet.region.regionX); + writer.writeInt16(packet.region.regionZ); + writer.writeInt64(packet.region.timestamp); } } diff --git a/mapsync-server/src/server.ts b/mapsync-server/src/server.ts index 4f4c90c6..cd20eecc 100644 --- a/mapsync-server/src/server.ts +++ b/mapsync-server/src/server.ts @@ -109,12 +109,10 @@ export class TcpClient { // prevent Out of Memory if (frameSize > this.maxFrameSize) { - return this.kick( - "Frame too large: " + - frameSize + - " have " + - accBuf.length, + this.kick( + `Frame's length [${frameSize}] is too large, max is [${this.maxFrameSize}]!`, ); + return; } if (accBuf.length < 4 + frameSize) return; // wait for more data From 96c3cf05ac169ccd1d87fa1be32eccf2c66e0e15 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 24 Mar 2026 08:30:06 +0000 Subject: [PATCH 2/5] Classify server packets These had a weird mixture of interface and namespace. They've now just been replaced with classes and each consolidated into one file. --- mapsync-server/src/cli.ts | 20 +- mapsync-server/src/main.ts | 106 ++++--- mapsync-server/src/model.ts | 11 - mapsync-server/src/protocol/CatchupPacket.ts | 22 -- .../src/protocol/CatchupRequestPacket.ts | 23 -- .../src/protocol/ChunkTilePacket.ts | 39 --- .../src/protocol/EncryptionRequestPacket.ts | 23 -- .../src/protocol/EncryptionResponsePacket.ts | 25 -- .../src/protocol/HandshakePacket.ts | 22 -- .../src/protocol/RegionCatchupPacket.ts | 19 -- .../src/protocol/RegionTimestampsPacket.ts | 18 -- mapsync-server/src/protocol/index.ts | 291 ++++++++++++++---- mapsync-server/src/server.ts | 60 ++-- 13 files changed, 329 insertions(+), 350 deletions(-) delete mode 100644 mapsync-server/src/protocol/CatchupPacket.ts delete mode 100644 mapsync-server/src/protocol/CatchupRequestPacket.ts delete mode 100644 mapsync-server/src/protocol/ChunkTilePacket.ts delete mode 100644 mapsync-server/src/protocol/EncryptionRequestPacket.ts delete mode 100644 mapsync-server/src/protocol/EncryptionResponsePacket.ts delete mode 100644 mapsync-server/src/protocol/HandshakePacket.ts delete mode 100644 mapsync-server/src/protocol/RegionCatchupPacket.ts delete mode 100644 mapsync-server/src/protocol/RegionTimestampsPacket.ts diff --git a/mapsync-server/src/cli.ts b/mapsync-server/src/cli.ts index 4f517d86..085bd2f3 100644 --- a/mapsync-server/src/cli.ts +++ b/mapsync-server/src/cli.ts @@ -5,6 +5,7 @@ import * as metadata from "./metadata"; import { TcpServer } from "./server"; import * as database from "./database"; +import { ClientboundRegionTimestampsPacket } from "./protocol"; //idk where these come from lol interface TerminalExtras { @@ -143,20 +144,23 @@ async function handle_input(input: string): Promise { return; } - const world = client.world; - if (!world) { + const dimension = client.dimension; + if (!dimension) { console.log("Client has no world yet"); return; } - const regions = await database.getRegionTimestamps(client.world!); + const regions = await database.getRegionTimestamps(client.dimension!); await Promise.allSettled( regions.map((region) => - client.send({ - type: "RegionTimestamps", - world: client.world!, - region, - }), + client.send( + new ClientboundRegionTimestampsPacket( + client.dimension!, + region.regionX, + region.regionZ, + region.timestamp, + ), + ), ), ); } else if (command === "kick") { diff --git a/mapsync-server/src/main.ts b/mapsync-server/src/main.ts index e09dbd4e..26f97da6 100644 --- a/mapsync-server/src/main.ts +++ b/mapsync-server/src/main.ts @@ -1,13 +1,17 @@ +import node_utils from "node:util"; import "./cli"; import { setServer } from "./cli"; import * as database from "./database"; import * as metadata from "./metadata"; -import { ClientPacket } from "./protocol"; -import { CatchupRequestPacket } from "./protocol/CatchupRequestPacket"; -import { ChunkTilePacket } from "./protocol/ChunkTilePacket"; import { TcpClient, TcpServer } from "./server"; -import { RegionCatchupPacket } from "./protocol/RegionCatchupPacket"; -import { CatchupChunk, RegionChunkTimestamps } from "./model"; +import { + ChunkTilePacket, + ClientboundChunkTimestampsResponsePacket, + ClientboundRegionTimestampsPacket, + ServerboundCatchupRequestPacket, + ServerboundChunkTimestampsRequestPacket, + ServerboundPacket, +} from "./protocol"; let config: metadata.Config = null!; let main: Main = null!; @@ -51,34 +55,38 @@ export class Main { // TODO check version, mc server, user access - const regions = await database.getRegionTimestamps(client.world!); + const regions = await database.getRegionTimestamps(client.dimension!); await Promise.allSettled( regions.map((region) => - client.send({ - type: "RegionTimestamps", - world: client.world!, - region, - }), + client.send( + new ClientboundRegionTimestampsPacket( + client.dimension!, + region.regionX, + region.regionZ, + region.timestamp, + ), + ), ), ); } handleClientDisconnected(client: ProtocolClient) {} - handleClientPacketReceived(client: ProtocolClient, pkt: ClientPacket) { - client.debug(client.mcName + " <- " + pkt.type); - switch (pkt.type) { - case "ChunkTile": - return this.handleChunkTilePacket(client, pkt); - case "CatchupRequest": + async handleClientPacketReceived( + client: ProtocolClient, + pkt: ServerboundPacket, + ) { + client.debug(client.mcName + " <- " + pkt.name); + switch (true) { + case pkt instanceof ServerboundChunkTimestampsRequestPacket: + return this.handleChunkTimestampsRequest(client, pkt); + case pkt instanceof ServerboundCatchupRequestPacket: return this.handleCatchupRequest(client, pkt); - case "RegionCatchup": - return this.handleRegionCatchupPacket(client, pkt); + case pkt instanceof ChunkTilePacket: + return this.handleChunkTilePacket(client, pkt); default: throw new Error( - `Unknown packet '${(pkt as any).type}' from client ${ - client.id - }`, + `Unknown packet [${node_utils.inspect(pkt)}] from client ${client.id}`, ); } } @@ -91,14 +99,14 @@ export class Main { await database .storeChunkData( - pkt.world, - pkt.chunk_x, - pkt.chunk_z, + pkt.dimension, + pkt.chunkX, + pkt.chunkZ, client.uuid, - pkt.ts, - pkt.data.version, - pkt.data.hash, - pkt.data.data, + pkt.timestamp, + pkt.dataVersion, + pkt.dataHash, + pkt.data, ) .catch(console.error); @@ -111,20 +119,20 @@ export class Main { async handleCatchupRequest( client: ProtocolClient, - pkt: CatchupRequestPacket, + pkt: ServerboundCatchupRequestPacket, ) { if (!client.uuid) throw new Error(`${client.name} is not authenticated`); for (const req of pkt.chunks) { let chunk = await database.getChunkData( - pkt.world, + pkt.dimension, req.chunkX, req.chunkZ, ); if (!chunk) { console.error(`${client.name} requested unavailable chunk`, { - world: pkt.world, + world: pkt.dimension, ...req, }); continue; @@ -133,24 +141,23 @@ export class Main { if (chunk.ts > req.timestamp) continue; // someone sent a new chunk, which presumably got relayed to the client if (chunk.ts < req.timestamp) continue; // the client already has a chunk newer than this - client.send({ - type: "ChunkTile", - world: pkt.world, - chunk_x: req.chunkX, - chunk_z: req.chunkZ, - ts: req.timestamp, - data: { - hash: chunk.hash, - data: chunk.data, - version: chunk.version, - }, - }); + client.send( + new ChunkTilePacket( + pkt.dimension, + req.chunkX, + req.chunkZ, + req.timestamp, + chunk.version, + chunk.hash, + chunk.data, + ), + ); } } - async handleRegionCatchupPacket( + async handleChunkTimestampsRequest( client: ProtocolClient, - pkt: RegionCatchupPacket, + pkt: ServerboundChunkTimestampsRequestPacket, ) { if (!client.uuid) throw new Error(`${client.name} is not authenticated`); @@ -160,7 +167,10 @@ export class Main { pkt.regionX, pkt.regionZ, ); - if (chunks.length) - client.send({ type: "Catchup", world: pkt.world, chunks }); + if (chunks.length) { + client.send( + new ClientboundChunkTimestampsResponsePacket(pkt.world, chunks), + ); + } } } diff --git a/mapsync-server/src/model.ts b/mapsync-server/src/model.ts index 879c202a..5fe5558a 100644 --- a/mapsync-server/src/model.ts +++ b/mapsync-server/src/model.ts @@ -9,14 +9,3 @@ export interface CatchupChunk { readonly chunkZ: number; readonly timestamp: number; } - -export interface Pos2D { - readonly x: number; - readonly z: number; -} - -export interface RegionChunkTimestamps { - readonly regionX: number; - readonly regionZ: number; - readonly chunks: Array; -} diff --git a/mapsync-server/src/protocol/CatchupPacket.ts b/mapsync-server/src/protocol/CatchupPacket.ts deleted file mode 100644 index d05f839b..00000000 --- a/mapsync-server/src/protocol/CatchupPacket.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { type CatchupChunk } from "../model"; -import { BufWriter } from "./BufWriter"; - -export interface CatchupPacket { - type: "Catchup"; - world: string; - chunks: CatchupChunk[]; -} - -export namespace CatchupPacket { - export function encode(pkt: CatchupPacket, writer: BufWriter) { - if (pkt.chunks.length < 1) - throw new Error(`Catchup chunks must not be empty`); - writer.writeString(pkt.world); - writer.writeUInt32(pkt.chunks.length); - for (const row of pkt.chunks) { - writer.writeInt32(row.chunkX); - writer.writeInt32(row.chunkZ); - writer.writeUInt64(row.timestamp); - } - } -} diff --git a/mapsync-server/src/protocol/CatchupRequestPacket.ts b/mapsync-server/src/protocol/CatchupRequestPacket.ts deleted file mode 100644 index a14ddc86..00000000 --- a/mapsync-server/src/protocol/CatchupRequestPacket.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type CatchupChunk } from "../model"; -import { BufReader } from "./BufReader"; - -export interface CatchupRequestPacket { - type: "CatchupRequest"; - world: string; - chunks: CatchupChunk[]; -} - -export namespace CatchupRequestPacket { - export function decode(reader: BufReader): CatchupRequestPacket { - const world = reader.readString(); - const chunks: CatchupChunk[] = new Array(reader.readUInt32()); - for (let i = 0; i < chunks.length; i++) { - chunks[i] = { - chunkX: reader.readInt32(), - chunkZ: reader.readInt32(), - timestamp: reader.readUInt64(), - }; - } - return { type: "CatchupRequest", world, chunks }; - } -} diff --git a/mapsync-server/src/protocol/ChunkTilePacket.ts b/mapsync-server/src/protocol/ChunkTilePacket.ts deleted file mode 100644 index eee9f326..00000000 --- a/mapsync-server/src/protocol/ChunkTilePacket.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { BufReader } from "./BufReader"; -import { BufWriter } from "./BufWriter"; -import { SHA1_HASH_LENGTH } from "../constants"; - -export interface ChunkTilePacket { - type: "ChunkTile"; - world: string; - chunk_x: number; - chunk_z: number; - ts: number; - data: { version: number; hash: Buffer; data: Buffer }; -} - -export namespace ChunkTilePacket { - export function decode(reader: BufReader): ChunkTilePacket { - return { - type: "ChunkTile", - world: reader.readString(), - chunk_x: reader.readInt32(), - chunk_z: reader.readInt32(), - ts: reader.readUInt64(), - data: { - version: reader.readUInt16(), - hash: reader.readBufLen(SHA1_HASH_LENGTH), - data: reader.readRemainder(), - }, - }; - } - - export function encode(pkt: ChunkTilePacket, writer: BufWriter) { - writer.writeString(pkt.world); - writer.writeInt32(pkt.chunk_x); - writer.writeInt32(pkt.chunk_z); - writer.writeUInt64(pkt.ts); - writer.writeUInt16(pkt.data.version); - writer.writeBufRaw(pkt.data.hash); - writer.writeBufRaw(pkt.data.data); // XXX do we need to prefix with length? - } -} diff --git a/mapsync-server/src/protocol/EncryptionRequestPacket.ts b/mapsync-server/src/protocol/EncryptionRequestPacket.ts deleted file mode 100644 index 148e4212..00000000 --- a/mapsync-server/src/protocol/EncryptionRequestPacket.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BufReader } from "./BufReader"; -import { BufWriter } from "./BufWriter"; - -export interface EncryptionRequestPacket { - type: "EncryptionRequest"; - publicKey: Buffer; - verifyToken: Buffer; -} - -export namespace EncryptionRequestPacket { - export function decode(reader: BufReader): EncryptionRequestPacket { - return { - type: "EncryptionRequest", - publicKey: reader.readBufWithLen(), - verifyToken: reader.readBufWithLen(), - }; - } - - export function encode(pkt: EncryptionRequestPacket, writer: BufWriter) { - writer.writeBufWithLen(pkt.publicKey); - writer.writeBufWithLen(pkt.verifyToken); - } -} diff --git a/mapsync-server/src/protocol/EncryptionResponsePacket.ts b/mapsync-server/src/protocol/EncryptionResponsePacket.ts deleted file mode 100644 index e17adc5f..00000000 --- a/mapsync-server/src/protocol/EncryptionResponsePacket.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BufReader } from "./BufReader"; -import { BufWriter } from "./BufWriter"; - -export interface EncryptionResponsePacket { - type: "EncryptionResponse"; - /** encrypted with server's public key */ - sharedSecret: Buffer; - /** encrypted with server's public key */ - verifyToken: Buffer; -} - -export namespace EncryptionResponsePacket { - export function decode(reader: BufReader): EncryptionResponsePacket { - return { - type: "EncryptionResponse", - sharedSecret: reader.readBufWithLen(), - verifyToken: reader.readBufWithLen(), - }; - } - - export function encode(pkt: EncryptionResponsePacket, writer: BufWriter) { - writer.writeBufWithLen(pkt.sharedSecret); - writer.writeBufWithLen(pkt.verifyToken); - } -} diff --git a/mapsync-server/src/protocol/HandshakePacket.ts b/mapsync-server/src/protocol/HandshakePacket.ts deleted file mode 100644 index 32bd4b82..00000000 --- a/mapsync-server/src/protocol/HandshakePacket.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BufReader } from "./BufReader"; -import { BufWriter } from "./BufWriter"; - -export interface HandshakePacket { - type: "Handshake"; - modVersion: string; - mojangName: string; - gameAddress: string; - world: string; -} - -export namespace HandshakePacket { - export function decode(reader: BufReader): HandshakePacket { - return { - type: "Handshake", - modVersion: reader.readString(), - mojangName: reader.readString(), - gameAddress: reader.readString(), - world: reader.readString(), - }; - } -} diff --git a/mapsync-server/src/protocol/RegionCatchupPacket.ts b/mapsync-server/src/protocol/RegionCatchupPacket.ts deleted file mode 100644 index a7ebf13d..00000000 --- a/mapsync-server/src/protocol/RegionCatchupPacket.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BufReader } from "./BufReader"; - -export interface RegionCatchupPacket { - type: "RegionCatchup"; - world: string; - regionX: number; - regionZ: number; -} - -export namespace RegionCatchupPacket { - export function decode(reader: BufReader): RegionCatchupPacket { - return { - type: "RegionCatchup", - world: reader.readString(), - regionX: reader.readInt16(), - regionZ: reader.readInt16(), - }; - } -} diff --git a/mapsync-server/src/protocol/RegionTimestampsPacket.ts b/mapsync-server/src/protocol/RegionTimestampsPacket.ts deleted file mode 100644 index fcf6b9b7..00000000 --- a/mapsync-server/src/protocol/RegionTimestampsPacket.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BufWriter } from "./BufWriter"; -import { CatchupRegion } from "../model"; - -export interface RegionTimestampsPacket { - type: "RegionTimestamps"; - world: string; - region: CatchupRegion; -} - -export namespace RegionTimestampsPacket { - export function encode(packet: RegionTimestampsPacket, writer: BufWriter) { - writer.writeString(packet.world); - console.log(`Sending region for [${packet.world}]`, packet); - writer.writeInt16(packet.region.regionX); - writer.writeInt16(packet.region.regionZ); - writer.writeInt64(packet.region.timestamp); - } -} diff --git a/mapsync-server/src/protocol/index.ts b/mapsync-server/src/protocol/index.ts index da615fb8..2312399e 100644 --- a/mapsync-server/src/protocol/index.ts +++ b/mapsync-server/src/protocol/index.ts @@ -1,75 +1,236 @@ import { BufReader } from "./BufReader"; import { BufWriter } from "./BufWriter"; -import { ChunkTilePacket } from "./ChunkTilePacket"; -import { EncryptionRequestPacket } from "./EncryptionRequestPacket"; -import { EncryptionResponsePacket } from "./EncryptionResponsePacket"; -import { HandshakePacket } from "./HandshakePacket"; -import { CatchupPacket } from "./CatchupPacket"; -import { CatchupRequestPacket } from "./CatchupRequestPacket"; -import { RegionTimestampsPacket } from "./RegionTimestampsPacket"; -import { RegionCatchupPacket } from "./RegionCatchupPacket"; - -export type ClientPacket = - | ChunkTilePacket - | EncryptionResponsePacket - | HandshakePacket - | CatchupRequestPacket - | RegionCatchupPacket; - -export type ServerPacket = - | ChunkTilePacket - | EncryptionRequestPacket - | CatchupPacket - | RegionTimestampsPacket; - -export const packetIds = [ - "ERROR:pkt0", - "Handshake", - "EncryptionRequest", - "EncryptionResponse", - "ChunkTile", - "Catchup", - "CatchupRequest", - "RegionTimestamps", - "RegionCatchup", -]; - -export function getPacketId(type: ServerPacket["type"]) { - const id = packetIds.indexOf(type); - if (id === -1) throw new Error(`Unknown packet type ${type}`); - return id; +import type { CatchupChunk } from "../model"; +import { SHA1_HASH_LENGTH } from "../constants"; + +export type ServerboundPacket = + | ServerboundHandshakePacket + | ServerboundEncryptionResponsePacket + | ServerboundChunkTimestampsRequestPacket + | ServerboundCatchupRequestPacket + | ChunkTilePacket; + +export type ClientboundPacket = + | ClientboundEncryptionRequestPacket + | ClientboundRegionTimestampsPacket + | ClientboundChunkTimestampsResponsePacket + | ChunkTilePacket; + +abstract class Packet { + protected constructor(public readonly packetId: number) {} + + public get name(): string { + return this.constructor.name ?? `Packet[${this.packetId}]`; + } } -export function decodePacket(reader: BufReader): ClientPacket { - const packetType = reader.readUInt8(); - switch (packetIds[packetType]) { - case "ChunkTile": - return ChunkTilePacket.decode(reader); - case "Handshake": - return HandshakePacket.decode(reader); - case "EncryptionResponse": - return EncryptionResponsePacket.decode(reader); - case "CatchupRequest": - return CatchupRequestPacket.decode(reader); - case "RegionCatchup": - return RegionCatchupPacket.decode(reader); - default: - throw new Error(`Unknown packet type ${packetType}`); +export class ServerboundHandshakePacket extends Packet { + public static readonly PACKET_ID = 1; + + public constructor( + public readonly modVersion: string, + public readonly mojangName: string, + public readonly gameAddress: string, + public readonly world: string, + ) { + super(ServerboundHandshakePacket.PACKET_ID); + } + + public static decode(reader: BufReader): ServerboundHandshakePacket { + return new ServerboundHandshakePacket( + reader.readString(), + reader.readString(), + reader.readString(), + reader.readString(), + ); } } -export function encodePacket(pkt: ServerPacket, writer: BufWriter): void { - writer.writeUInt8(getPacketId(pkt.type)); - switch (pkt.type) { - case "ChunkTile": - return ChunkTilePacket.encode(pkt, writer); - case "Catchup": - return CatchupPacket.encode(pkt, writer); - case "EncryptionRequest": - return EncryptionRequestPacket.encode(pkt, writer); - case "RegionTimestamps": - return RegionTimestampsPacket.encode(pkt, writer); +export class ClientboundEncryptionRequestPacket extends Packet { + public static readonly PACKET_ID = 2; + + public constructor( + public readonly publicKey: Buffer, + public readonly verifyToken: Buffer, + ) { + super(ClientboundEncryptionRequestPacket.PACKET_ID); + } + + public encode(writer: BufWriter) { + writer.writeBufWithLen(this.publicKey); + writer.writeBufWithLen(this.verifyToken); + } +} + +export class ServerboundEncryptionResponsePacket extends Packet { + public static readonly PACKET_ID = 3; + + public constructor( + /** encrypted with server's public key */ + public readonly sharedSecret: Buffer, + /** encrypted with server's public key */ + public readonly verifyToken: Buffer, + ) { + super(ServerboundEncryptionResponsePacket.PACKET_ID); + } + + public static decode( + reader: BufReader, + ): ServerboundEncryptionResponsePacket { + return new ServerboundEncryptionResponsePacket( + reader.readBufWithLen(), + reader.readBufWithLen(), + ); + } +} + +export class ClientboundRegionTimestampsPacket extends Packet { + public static readonly PACKET_ID = 7; + + public constructor( + public readonly world: string, + public readonly regionX: number, + public readonly regionZ: number, + public readonly timestamp: number, + ) { + super(ClientboundRegionTimestampsPacket.PACKET_ID); + } + + public encode(writer: BufWriter) { + writer.writeString(this.world); + console.log(`Sending region for [${this.world}]`, this); + writer.writeInt16(this.regionX); + writer.writeInt16(this.regionZ); + writer.writeInt64(this.timestamp); + } +} + +export class ServerboundChunkTimestampsRequestPacket extends Packet { + public static readonly PACKET_ID = 8; + + public constructor( + public readonly world: string, + public readonly regionX: number, + public readonly regionZ: number, + ) { + super(ServerboundChunkTimestampsRequestPacket.PACKET_ID); + } + + public static decode( + reader: BufReader, + ): ServerboundChunkTimestampsRequestPacket { + return new ServerboundChunkTimestampsRequestPacket( + reader.readString(), + reader.readInt16(), + reader.readInt16(), + ); + } +} + +export class ClientboundChunkTimestampsResponsePacket extends Packet { + public static readonly PACKET_ID = 5; + + public constructor( + public readonly world: string, + public readonly chunks: CatchupChunk[], + ) { + super(ClientboundChunkTimestampsResponsePacket.PACKET_ID); + if (this.chunks.length < 1) + throw new Error(`Catchup chunks must not be empty`); + } + + public encode(writer: BufWriter) { + writer.writeString(this.world); + writer.writeUInt32(this.chunks.length); + for (const row of this.chunks) { + writer.writeInt32(row.chunkX); + writer.writeInt32(row.chunkZ); + writer.writeInt64(row.timestamp); + } + } +} + +export class ServerboundCatchupRequestPacket extends Packet { + public static readonly PACKET_ID = 6; + + public constructor( + public readonly dimension: string, + public readonly chunks: CatchupChunk[], + ) { + super(ServerboundCatchupRequestPacket.PACKET_ID); + } + + public static decode(reader: BufReader): ServerboundCatchupRequestPacket { + const dimension = reader.readString(); + const chunks: CatchupChunk[] = new Array(reader.readUInt32()); + for (let i = 0; i < chunks.length; i++) { + chunks[i] = { + chunkX: reader.readInt32(), + chunkZ: reader.readInt32(), + timestamp: reader.readInt64(), + }; + } + return new ServerboundCatchupRequestPacket(dimension, chunks); + } +} + +export class ChunkTilePacket extends Packet { + public static readonly PACKET_ID = 4; + + public constructor( + public readonly dimension: string, + public readonly chunkX: number, + public readonly chunkZ: number, + public readonly timestamp: number, + public readonly dataVersion: number, + public readonly dataHash: Buffer, + public readonly data: Buffer, + ) { + super(ChunkTilePacket.PACKET_ID); + } + + public static decode(reader: BufReader): ChunkTilePacket { + return new ChunkTilePacket( + reader.readString(), + reader.readInt32(), + reader.readInt32(), + reader.readInt64(), + reader.readUInt16(), + reader.readBufLen(SHA1_HASH_LENGTH), + reader.readRemainder(), + ); + } + + public encode(writer: BufWriter) { + writer.writeString(this.dimension); + writer.writeInt32(this.chunkX); + writer.writeInt32(this.chunkZ); + writer.writeInt64(this.timestamp); + writer.writeUInt16(this.dataVersion); + writer.writeBufRaw(this.dataHash); + writer.writeBufRaw(this.data); // XXX do we need to prefix with length? + } +} + +export function decodePacket(reader: BufReader): ServerboundPacket { + const packetId = reader.readUInt8(); + switch (packetId) { + case ServerboundHandshakePacket.PACKET_ID: + return ServerboundHandshakePacket.decode(reader); + case ServerboundEncryptionResponsePacket.PACKET_ID: + return ServerboundEncryptionResponsePacket.decode(reader); + case ServerboundChunkTimestampsRequestPacket.PACKET_ID: + return ServerboundChunkTimestampsRequestPacket.decode(reader); + case ServerboundCatchupRequestPacket.PACKET_ID: + return ServerboundCatchupRequestPacket.decode(reader); + case ChunkTilePacket.PACKET_ID: + return ChunkTilePacket.decode(reader); default: - throw new Error(`Unknown packet type ${(pkt as any).type}`); + throw new Error(`Unknown packet type ${packetId}`); } } + +export function encodePacket(pkt: ClientboundPacket, writer: BufWriter): void { + writer.writeUInt8(pkt.packetId); + pkt.encode(writer); +} diff --git a/mapsync-server/src/server.ts b/mapsync-server/src/server.ts index cd20eecc..4370f6ca 100644 --- a/mapsync-server/src/server.ts +++ b/mapsync-server/src/server.ts @@ -1,12 +1,17 @@ import crypto from "crypto"; import net from "net"; import { Main } from "./main"; -import type { ClientPacket, ServerPacket } from "./protocol"; -import { decodePacket, encodePacket } from "./protocol"; +import { + decodePacket, + encodePacket, + type ServerboundPacket, + type ClientboundPacket, + ServerboundHandshakePacket, + ServerboundEncryptionResponsePacket, + ClientboundEncryptionRequestPacket, +} from "./protocol"; import { BufReader } from "./protocol/BufReader"; import { BufWriter } from "./protocol/BufWriter"; -import { EncryptionResponsePacket } from "./protocol/EncryptionResponsePacket"; -import { HandshakePacket } from "./protocol/HandshakePacket"; import { SUPPORTED_VERSIONS } from "./constants"; import * as metadata from "./metadata"; @@ -66,7 +71,7 @@ export class TcpClient { gameAddress: string | undefined; uuid: string | undefined; mcName: string | undefined; - world: string | undefined; + dimension: string | undefined; /** prevent Out of Memory when client sends a large packet */ maxFrameSize = 2 ** 21; @@ -161,18 +166,18 @@ export class TcpClient { }); } - private async handlePacketReceived(pkt: ClientPacket) { + private async handlePacketReceived(pkt: ServerboundPacket) { if (!this.uuid) { - // not authenticated yet - switch (pkt.type) { - case "Handshake": + switch (true) { + case pkt instanceof ServerboundHandshakePacket: return await this.handleHandshakePacket(pkt); - case "EncryptionResponse": + case pkt instanceof ServerboundEncryptionResponsePacket: return await this.handleEncryptionResponsePacket(pkt); + default: + throw new Error( + `Packet ${pkt.name} from unauth'd client ${this.id}`, + ); } - throw new Error( - `Packet ${pkt.type} from unauth'd client ${this.id}`, - ); } else { return await this.handler.handleClientPacketReceived(this, pkt); } @@ -183,16 +188,16 @@ export class TcpClient { this.socket.destroy(); } - async send(pkt: ServerPacket) { + async send(pkt: ClientboundPacket) { if (!this.cryptoPromise) { - this.debug("Not encrypted, dropping packet", pkt.type); + this.debug("Not encrypted, dropping packet", pkt); return; } if (!this.uuid) { - this.debug("Not authenticated, dropping packet", pkt.type); + this.debug("Not authenticated, dropping packet", pkt); return; } - this.debug(this.mcName + " -> " + pkt.type); + this.debug(this.mcName + " -> " + pkt.name); await this.sendInternal(pkt, true); } @@ -208,9 +213,9 @@ export class TcpClient { * - If encryption is enabled, waits for the handshake to complete and encrypts the buffer. * - Drops the packet if the socket is not writable. */ - private async sendInternal(pkt: ServerPacket, doCrypto = false) { + private async sendInternal(pkt: ClientboundPacket, doCrypto = false) { if (!this.socket.writable) - return this.debug("Socket closed, dropping", pkt.type); + return this.debug("Socket closed, dropping", pkt); if (doCrypto && !this.cryptoPromise) throw new Error(`Can't encrypt: handshake not finished`); @@ -228,7 +233,7 @@ export class TcpClient { this.socket.write(buf); } - private async handleHandshakePacket(packet: HandshakePacket) { + private async handleHandshakePacket(packet: ServerboundHandshakePacket) { if (this.cryptoPromise) throw new Error(`Already authenticated`); if (this.verifyToken) throw new Error(`Encryption already started`); @@ -243,18 +248,19 @@ export class TcpClient { this.gameAddress = packet.gameAddress; this.claimedMojangName = packet.mojangName; - this.world = packet.world; + this.dimension = packet.world; this.verifyToken = crypto.randomBytes(4); - await this.sendInternal({ - type: "EncryptionRequest", - publicKey: this.server.publicKeyBuffer, - verifyToken: this.verifyToken, - }); + await this.sendInternal( + new ClientboundEncryptionRequestPacket( + this.server.publicKeyBuffer, + this.verifyToken, + ), + ); } private async handleEncryptionResponsePacket( - pkt: EncryptionResponsePacket, + pkt: ServerboundEncryptionResponsePacket, ) { if (this.cryptoPromise) throw new Error(`Already authenticated`); if (!this.claimedMojangName) From f68b728ef8add6fefa48fcdb0c0ea269cbfef761 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 24 Mar 2026 09:50:57 +0000 Subject: [PATCH 3/5] Fixup server packet printing --- mapsync-server/src/main.ts | 1 - mapsync-server/src/protocol/index.ts | 1 - mapsync-server/src/server.ts | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mapsync-server/src/main.ts b/mapsync-server/src/main.ts index 26f97da6..0832e2f6 100644 --- a/mapsync-server/src/main.ts +++ b/mapsync-server/src/main.ts @@ -76,7 +76,6 @@ export class Main { client: ProtocolClient, pkt: ServerboundPacket, ) { - client.debug(client.mcName + " <- " + pkt.name); switch (true) { case pkt instanceof ServerboundChunkTimestampsRequestPacket: return this.handleChunkTimestampsRequest(client, pkt); diff --git a/mapsync-server/src/protocol/index.ts b/mapsync-server/src/protocol/index.ts index 2312399e..0d73cbed 100644 --- a/mapsync-server/src/protocol/index.ts +++ b/mapsync-server/src/protocol/index.ts @@ -98,7 +98,6 @@ export class ClientboundRegionTimestampsPacket extends Packet { public encode(writer: BufWriter) { writer.writeString(this.world); - console.log(`Sending region for [${this.world}]`, this); writer.writeInt16(this.regionX); writer.writeInt16(this.regionZ); writer.writeInt64(this.timestamp); diff --git a/mapsync-server/src/server.ts b/mapsync-server/src/server.ts index 4370f6ca..8b333ff4 100644 --- a/mapsync-server/src/server.ts +++ b/mapsync-server/src/server.ts @@ -167,6 +167,7 @@ export class TcpClient { } private async handlePacketReceived(pkt: ServerboundPacket) { + this.debug(`Received ${pkt.name}:`, pkt); if (!this.uuid) { switch (true) { case pkt instanceof ServerboundHandshakePacket: @@ -197,7 +198,6 @@ export class TcpClient { this.debug("Not authenticated, dropping packet", pkt); return; } - this.debug(this.mcName + " -> " + pkt.name); await this.sendInternal(pkt, true); } @@ -218,7 +218,7 @@ export class TcpClient { return this.debug("Socket closed, dropping", pkt); if (doCrypto && !this.cryptoPromise) throw new Error(`Can't encrypt: handshake not finished`); - + this.debug(`Sending ${pkt.name}:`, pkt); const writer = new BufWriter(); // TODO size hint writer.writeUInt32(0); // set later, but reserve space in buffer encodePacket(pkt, writer); From 2b5a09ddbea2a19a550ab02d4ca868c2a4f1c593 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 24 Mar 2026 09:55:19 +0000 Subject: [PATCH 4/5] Rename remaining "world" to "dimension" --- .../mod/net/packet/ServerboundHandshakePacket.java | 8 ++++---- mapsync-server/src/main.ts | 6 +++--- mapsync-server/src/protocol/index.ts | 12 ++++++------ mapsync-server/src/server.ts | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundHandshakePacket.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundHandshakePacket.java index a4b970da..80b85036 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundHandshakePacket.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundHandshakePacket.java @@ -14,13 +14,13 @@ public class ServerboundHandshakePacket implements Packet { public final @NotNull String modVersion; public final @NotNull String username; public final @NotNull String gameAddress; - public final @NotNull String world; + public final @NotNull String dimension; - public ServerboundHandshakePacket(@NotNull String modVersion, @NotNull String username, @NotNull String gameAddress, @NotNull String world) { + public ServerboundHandshakePacket(@NotNull String modVersion, @NotNull String username, @NotNull String gameAddress, @NotNull String dimension) { this.modVersion = modVersion; this.username = username; this.gameAddress = gameAddress; - this.world = world; + this.dimension = dimension; } @Override @@ -28,7 +28,7 @@ public void write(@NotNull ByteBuf out) { Packet.writeUtf8String(out, modVersion); Packet.writeUtf8String(out, username); Packet.writeUtf8String(out, gameAddress); - Packet.writeUtf8String(out, world); + Packet.writeUtf8String(out, dimension); } @Override diff --git a/mapsync-server/src/main.ts b/mapsync-server/src/main.ts index 0832e2f6..cdb59202 100644 --- a/mapsync-server/src/main.ts +++ b/mapsync-server/src/main.ts @@ -131,7 +131,7 @@ export class Main { ); if (!chunk) { console.error(`${client.name} requested unavailable chunk`, { - world: pkt.dimension, + dimension: pkt.dimension, ...req, }); continue; @@ -162,13 +162,13 @@ export class Main { throw new Error(`${client.name} is not authenticated`); const chunks = await database.getChunkTimestamps( - pkt.world, + pkt.dimension, pkt.regionX, pkt.regionZ, ); if (chunks.length) { client.send( - new ClientboundChunkTimestampsResponsePacket(pkt.world, chunks), + new ClientboundChunkTimestampsResponsePacket(pkt.dimension, chunks), ); } } diff --git a/mapsync-server/src/protocol/index.ts b/mapsync-server/src/protocol/index.ts index 0d73cbed..ee4dff8a 100644 --- a/mapsync-server/src/protocol/index.ts +++ b/mapsync-server/src/protocol/index.ts @@ -31,7 +31,7 @@ export class ServerboundHandshakePacket extends Packet { public readonly modVersion: string, public readonly mojangName: string, public readonly gameAddress: string, - public readonly world: string, + public readonly dimension: string, ) { super(ServerboundHandshakePacket.PACKET_ID); } @@ -88,7 +88,7 @@ export class ClientboundRegionTimestampsPacket extends Packet { public static readonly PACKET_ID = 7; public constructor( - public readonly world: string, + public readonly dimension: string, public readonly regionX: number, public readonly regionZ: number, public readonly timestamp: number, @@ -97,7 +97,7 @@ export class ClientboundRegionTimestampsPacket extends Packet { } public encode(writer: BufWriter) { - writer.writeString(this.world); + writer.writeString(this.dimension); writer.writeInt16(this.regionX); writer.writeInt16(this.regionZ); writer.writeInt64(this.timestamp); @@ -108,7 +108,7 @@ export class ServerboundChunkTimestampsRequestPacket extends Packet { public static readonly PACKET_ID = 8; public constructor( - public readonly world: string, + public readonly dimension: string, public readonly regionX: number, public readonly regionZ: number, ) { @@ -130,7 +130,7 @@ export class ClientboundChunkTimestampsResponsePacket extends Packet { public static readonly PACKET_ID = 5; public constructor( - public readonly world: string, + public readonly dimension: string, public readonly chunks: CatchupChunk[], ) { super(ClientboundChunkTimestampsResponsePacket.PACKET_ID); @@ -139,7 +139,7 @@ export class ClientboundChunkTimestampsResponsePacket extends Packet { } public encode(writer: BufWriter) { - writer.writeString(this.world); + writer.writeString(this.dimension); writer.writeUInt32(this.chunks.length); for (const row of this.chunks) { writer.writeInt32(row.chunkX); diff --git a/mapsync-server/src/server.ts b/mapsync-server/src/server.ts index 8b333ff4..b6647324 100644 --- a/mapsync-server/src/server.ts +++ b/mapsync-server/src/server.ts @@ -248,7 +248,7 @@ export class TcpClient { this.gameAddress = packet.gameAddress; this.claimedMojangName = packet.mojangName; - this.dimension = packet.world; + this.dimension = packet.dimension; this.verifyToken = crypto.randomBytes(4); await this.sendInternal( From 37be1cfc6081750744009d6bc3aa267a096b9472 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 24 Mar 2026 10:19:35 +0000 Subject: [PATCH 5/5] Fix getChunkTimestamps query Turns out that doing an aggregate function like MAX without having a group by, in this case, is a *bad* idea. Also I seemed to have forgotten that regions are 32x32, not 16x16 :s --- mapsync-server/src/database.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mapsync-server/src/database.ts b/mapsync-server/src/database.ts index 80caf225..5affc1a3 100644 --- a/mapsync-server/src/database.ts +++ b/mapsync-server/src/database.ts @@ -102,10 +102,10 @@ export async function getChunkTimestamps( regionX: number, regionZ: number, ) { - const minChunkX = regionX << 4, - maxChunkX = minChunkX + 16; - const minChunkZ = regionZ << 4, - maxChunkZ = minChunkZ + 16; + const minChunkX = regionX << 5, + maxChunkX = minChunkX + 32; + const minChunkZ = regionZ << 5, + maxChunkZ = minChunkZ + 32; return get() .selectFrom("player_chunk") .select([ @@ -118,6 +118,7 @@ export async function getChunkTimestamps( .where("chunk_x", "<", maxChunkX) .where("chunk_z", ">=", minChunkZ) .where("chunk_z", "<", maxChunkZ) + .groupBy(["chunk_x", "chunk_z"]) .orderBy("ts", "desc") .execute(); }