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 4a835f2..e4b1458 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 @@ -11,6 +11,7 @@ import gjum.minecraft.mapsync.mod.net.CloseContext; import gjum.minecraft.mapsync.mod.net.Packet; import gjum.minecraft.mapsync.mod.net.SyncClient; +import gjum.minecraft.mapsync.mod.net.packet.ServerboundDimensionChangePacket; import gjum.minecraft.mapsync.mod.sync.DimensionState; import gjum.minecraft.mapsync.mod.sync.GameContext; import gjum.minecraft.mapsync.mod.net.UnexpectedPacketException; @@ -31,6 +32,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientChunkEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; @@ -132,14 +134,36 @@ public static void handleSyncPacket( } } - /// @param clientLevel This is the *new* dimension. + public static void handleWelcomed( + final @NotNull SyncClient client + ) { + if (client.gameContext.getDimensionState().orElse(null) instanceof final DimensionState dimensionState) { + client.send(new ServerboundDimensionChangePacket( + dimensionState.dimension.identifier() + )); + } + } + + public static void handleGameConnection( + final @NotNull Minecraft minecraft, + final @NotNull GameContext gameContext + ) { + if (gameContext.getGameConfig().shouldAutoConnect()) { + gameContext.getSyncConnections().setAll(Set.copyOf( + gameContext.getGameConfig().getSyncServerAddresses() + )); + } + } + + /// @param level This is the *new* dimension. public static void handleDimensionChange( final @NotNull Minecraft minecraft, - final @NotNull ClientLevel clientLevel, + final @NotNull ClientLevel level, final @NotNull GameContext gameContext ) { - debugLog("handleDimensionChange"); - // TODO tell sync server to only send chunks for this dimension now + gameContext.getSyncConnections().broadcast(new ServerboundDimensionChangePacket( + level.dimension().identifier() + )); } /** diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/config/ServerConfig.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/config/ServerConfig.java index 5b4bb2c..345f9be 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/config/ServerConfig.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/config/ServerConfig.java @@ -12,6 +12,9 @@ public final class ServerConfig extends JsonConfig { @Expose private ArrayList syncServerAddresses = new ArrayList<>(); + @Expose + private boolean autoConnect = false; + public @NotNull List<@NotNull String> getSyncServerAddresses() { return this.syncServerAddresses.stream() .map(String::trim) @@ -27,6 +30,16 @@ public void setSyncServerAddresses( this.syncServerAddresses = new ArrayList<>(syncAddresses); } + public boolean shouldAutoConnect() { + return this.autoConnect; + } + + public void setAutoConnect( + final boolean autoConnect + ) { + this.autoConnect = autoConnect; + } + @Override public void resetToDefaults() { this.setSyncServerAddresses(List.of( diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/config/gui/SyncConnectionsGui.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/config/gui/SyncConnectionsGui.java index 2a6fa53..abbec27 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/config/gui/SyncConnectionsGui.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/config/gui/SyncConnectionsGui.java @@ -8,6 +8,7 @@ import java.util.Set; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Checkbox; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.CommonComponents; @@ -40,13 +41,6 @@ protected void init() { final int offsetRight = this.width / 2 + innerWidth / 2; this.offsetTop = this.height / 3; - this.addRenderableWidget( - Button.builder(Component.literal("Close"), (button) -> this.minecraft.setScreen(this.parentScreen)) - .pos(offsetRight - 100, this.offsetTop) - .width(100) - .build() - ); - final EditBox addressField = this.addRenderableWidget(new EditBox( this.font, this.offsetLeft, @@ -58,6 +52,17 @@ protected void init() { addressField.setValue(this.addressFieldValue); addressField.setResponder((value) -> this.addressFieldValue = value); + this.addRenderableWidget( + Checkbox.builder(Component.literal("Auto-connect"), this.font) + .pos(offsetRight - 100, this.offsetTop + 18) + .selected(this.gameContext.getGameConfig().shouldAutoConnect()) + .onValueChange((checkbox, value) -> { + this.gameContext.getGameConfig().setAutoConnect(value); + this.gameContext.getGameConfig().save(); + }) + .build() + ); + this.addRenderableWidget( Button .builder( diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/Packet.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/Packet.java index 547a9cc..2472f67 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/Packet.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/Packet.java @@ -9,6 +9,7 @@ import gjum.minecraft.mapsync.mod.net.packet.ClientboundWelcomePacket; import gjum.minecraft.mapsync.mod.net.packet.ServerboundCatchupRequestPacket; import gjum.minecraft.mapsync.mod.net.packet.ServerboundChunkTimestampsRequestPacket; +import gjum.minecraft.mapsync.mod.net.packet.ServerboundDimensionChangePacket; import gjum.minecraft.mapsync.mod.net.packet.ServerboundHandshakePacket; import gjum.minecraft.mapsync.mod.net.packet.ServerboundIdentityResponsePacket; import org.apache.commons.lang3.NotImplementedException; @@ -43,6 +44,7 @@ public static void encodePacket( case ChunkTilePacket $ -> ChunkTilePacket.PACKET_ID; case ServerboundHandshakePacket $ -> ServerboundHandshakePacket.PACKET_ID; case ServerboundIdentityResponsePacket $ -> ServerboundIdentityResponsePacket.PACKET_ID; + case ServerboundDimensionChangePacket $ -> ServerboundDimensionChangePacket.PACKET_ID; case ServerboundChunkTimestampsRequestPacket $ -> ServerboundChunkTimestampsRequestPacket.PACKET_ID; case ServerboundCatchupRequestPacket $ -> ServerboundCatchupRequestPacket.PACKET_ID; default -> throw new UnexpectedPacketException(packet); diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/auth/AuthProcess.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/auth/AuthProcess.java index 6c3514a..eba5efb 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/auth/AuthProcess.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/auth/AuthProcess.java @@ -1,6 +1,6 @@ package gjum.minecraft.mapsync.mod.net.auth; -import gjum.minecraft.mapsync.mod.sync.DimensionState; +import gjum.minecraft.mapsync.mod.MapSyncMod; import gjum.minecraft.mapsync.mod.net.SyncClient; import gjum.minecraft.mapsync.mod.net.UnexpectedPacketException; import gjum.minecraft.mapsync.mod.net.packet.ClientboundIdentityRequestPacket; @@ -21,17 +21,12 @@ private record AwaitingIdentityRequest() implements AuthState {} public static void sendHandshake( final @NotNull SyncClient client ) throws Exception { - final DimensionState dimensionState = client.gameContext.getDimensionState().orElse(null); - if (dimensionState == null) { - throw new IllegalStateException("no dimension state"); - } if (!client.authState.setIf(Objects::isNull, AwaitingIdentityRequest::new)) { throw new IllegalStateException("already authenticated"); } client.send(new ServerboundHandshakePacket( MagicValues.VERSION, - client.gameContext.getGameAddress(), - dimensionState.dimension.identifier().toString() + client.gameContext.getGameAddress() )); } @@ -74,5 +69,6 @@ public static void handleWelcome( if (!client.authState.setIf((state) -> state instanceof AwaitingWelcome, Welcomed::new)) { throw new UnexpectedPacketException(packet); } + MapSyncMod.handleWelcomed(client); } } diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ClientboundWelcomePacket.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ClientboundWelcomePacket.java index 25ad26a..46bc0ea 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ClientboundWelcomePacket.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ClientboundWelcomePacket.java @@ -4,11 +4,12 @@ import gjum.minecraft.mapsync.mod.net.buffers.BufferReader; import org.jetbrains.annotations.NotNull; -/// This is sent by the server to indicate a successful connection: that the client can begin sending chunk data. The -/// server will immediately follow up this packet with a [ClientboundRegionTimestampsPacket]. +/// This is sent by the server to indicate a successful connection. The client should then inform the server of its +/// current dimension via [ServerboundDimensionChangePacket], after which the client can begin sending chunk data for +/// that dimension. /// /// - Prev: [ServerboundIdentityResponsePacket] -/// - Next: [ClientboundRegionTimestampsPacket] +/// - Next: [ServerboundDimensionChangePacket] public record ClientboundWelcomePacket() implements Packet { public static final int PACKET_ID = 9; diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundDimensionChangePacket.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundDimensionChangePacket.java new file mode 100644 index 0000000..d493a5b --- /dev/null +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/net/packet/ServerboundDimensionChangePacket.java @@ -0,0 +1,31 @@ +package gjum.minecraft.mapsync.mod.net.packet; + +import gjum.minecraft.mapsync.mod.net.Packet; +import gjum.minecraft.mapsync.mod.net.buffers.BufferWriter; +import gjum.minecraft.mapsync.mod.utils.Assertions; +import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.NotNull; + +/// The client should send this to the server: +/// +/// 1. Whenever the player changes dimension (such as going through a portal) +/// 2. Whenever a new sync connection is made while the player is already in-game. +/// +/// - Prev: [ClientboundWelcomePacket] +/// - Next: [ClientboundRegionTimestampsPacket] +public record ServerboundDimensionChangePacket( + @NotNull Identifier dimension +) implements Packet { + public static final int PACKET_ID = 10; + + public ServerboundDimensionChangePacket { + Assertions.assertNotNull(dimension); + } + + @Override + public void write( + final @NotNull BufferWriter writer + ) throws Exception { + writer.writeString(this.dimension().toString()); + } +} 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 49c5ada..78ca465 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 @@ -12,15 +12,13 @@ /// - Next: [ClientboundIdentityRequestPacket] public record ServerboundHandshakePacket( @NotNull String modVersion, - @NotNull GameAddress gameAddress, - @NotNull String dimension + @NotNull GameAddress gameAddress ) implements Packet { public static final int PACKET_ID = 1; public ServerboundHandshakePacket { Assertions.assertNotNull(modVersion); Assertions.assertNotNull(gameAddress); - Assertions.assertNotNull(dimension); } @Override @@ -29,6 +27,5 @@ public void write( ) throws Exception { writer.writeString(this.modVersion()); writer.writeString(this.gameAddress().address()); - writer.writeString(this.dimension()); } } diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/sync/GameContext.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/sync/GameContext.java index ad0b59e..4be6a70 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/sync/GameContext.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/sync/GameContext.java @@ -121,6 +121,11 @@ public static void initEvents() { } } }); + ClientPlayConnectionEvents.JOIN.register((gameConnection, packetSender, minecraft) -> { + if (instance instanceof final GameContext context) { + MapSyncMod.handleGameConnection(minecraft, context); + } + }); ClientPlayConnectionEvents.DISCONNECT.register((gameConnection, minecraft) -> { if (INSTANCE.getAndSet((Object) null) instanceof final GameContext context) { context.shutdown(); diff --git a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/sync/SyncConnections.java b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/sync/SyncConnections.java index e28ff19..6fc665d 100644 --- a/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/sync/SyncConnections.java +++ b/mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/sync/SyncConnections.java @@ -1,6 +1,7 @@ package gjum.minecraft.mapsync.mod.sync; import gjum.minecraft.mapsync.mod.MapSyncMod; +import gjum.minecraft.mapsync.mod.net.Packet; import gjum.minecraft.mapsync.mod.net.SyncClient; import java.util.Iterator; import java.util.Map; @@ -87,4 +88,12 @@ public void closeAll( return true; }); } + + public void broadcast( + final @NotNull Packet packet + ) { + for (final SyncClient syncClient : this) { + syncClient.send(packet); + } + } } diff --git a/mapsync-server/src/main.ts b/mapsync-server/src/main.ts index ecd6ed7..8bb7294 100644 --- a/mapsync-server/src/main.ts +++ b/mapsync-server/src/main.ts @@ -16,6 +16,7 @@ import { ClientboundWelcomePacket, UnexpectedPacketError, ClientboundIdentityRequestPacket, + ServerboundDimensionChangePacket, } from "./packets.ts"; import { AwaitingHandshake, @@ -73,6 +74,8 @@ export class ProtocolHandler { return this.handleCatchupRequest(client, pkt); case pkt instanceof ChunkTilePacket: return this.handleChunkTilePacket(client, pkt); + case pkt instanceof ServerboundDimensionChangePacket: + return this.handleDimensionChange(client, pkt); default: throw new Error( `Unknown packet [${node_utils.inspect(pkt)}] from client ${client.id}`, @@ -94,8 +97,6 @@ export class ProtocolHandler { } // TODO: Check whether the game address is supported client.gameAddress = packet.gameAddress; - // TODO: Make this its own packet - client.dimension = packet.dimension; const serverSalt: Buffer = this.config.auth ? node_crypto.randomBytes(32) : Buffer.allocUnsafe(0); @@ -172,6 +173,18 @@ export class ProtocolHandler { // TODO check version, mc server, user access client.send(new ClientboundWelcomePacket()); + } + + private async handleDimensionChange( + client: WSClient, + pkt: ServerboundDimensionChangePacket, + ) { + if (client.isInDimension(pkt.dimension)) { + return; + } + // TODO: Stop any sync process of the previous dimension + + client.dimension = pkt.dimension; for (const region of await database.getRegionTimestamps( client.dimension!, @@ -193,6 +206,13 @@ export class ProtocolHandler { ) { const welcome = client.requireWelcomed(); + if (!client.isInDimension(pkt.dimension)) { + client.warn( + `Client send chunk data for [${pkt.dimension}] when their dimension is [${client.dimension}]!`, + ); + return; + } + // TODO ignore if same chunk hash exists in db await database @@ -225,6 +245,13 @@ export class ProtocolHandler { ) { const welcome = client.requireWelcomed(); + if (!client.isInDimension(pkt.dimension)) { + client.warn( + `Client requested catchup for [${pkt.dimension}] when their dimension is [${client.dimension}]!`, + ); + return; + } + for (const req of pkt.chunks) { let chunk = await database.getChunkData( pkt.dimension, @@ -262,6 +289,13 @@ export class ProtocolHandler { ) { const welcome = client.requireWelcomed(); + if (!client.isInDimension(pkt.dimension)) { + client.warn( + `Client requested chunk timestamps for [${pkt.dimension}] when their dimension is [${client.dimension}]!`, + ); + return; + } + const chunks = await database.getChunkTimestamps( pkt.dimension, pkt.regionX, diff --git a/mapsync-server/src/packets.ts b/mapsync-server/src/packets.ts index 89a5b92..07ac0ff 100644 --- a/mapsync-server/src/packets.ts +++ b/mapsync-server/src/packets.ts @@ -15,6 +15,7 @@ import { export type ServerboundPacket = | ServerboundHandshakePacket | ServerboundIdentityResponsePacket + | ServerboundDimensionChangePacket | ServerboundChunkTimestampsRequestPacket | ServerboundCatchupRequestPacket | ChunkTilePacket; @@ -48,7 +49,6 @@ export class ServerboundHandshakePacket extends Packet { public constructor( public readonly modVersion: string, public readonly gameAddress: string, - public readonly dimension: string, ) { super(ServerboundHandshakePacket.PACKET_ID); } @@ -57,7 +57,6 @@ export class ServerboundHandshakePacket extends Packet { return new ServerboundHandshakePacket( reader.readString(), reader.readString(), - reader.readString(), ); } } @@ -107,6 +106,20 @@ export class ClientboundWelcomePacket extends Packet { public encode(writer: BufferWriter) {} } +export class ServerboundDimensionChangePacket extends Packet { + public static readonly PACKET_ID = asUnt8(10); + + public constructor(public readonly dimension: string) { + super(ServerboundDimensionChangePacket.PACKET_ID); + } + + public static decode( + reader: BufferReader, + ): ServerboundDimensionChangePacket { + return new ServerboundDimensionChangePacket(reader.readString()); + } +} + export class ClientboundRegionTimestampsPacket extends Packet { public static readonly PACKET_ID = asUnt8(7); @@ -196,7 +209,9 @@ export class ServerboundCatchupRequestPacket extends Packet { const dimension = reader.readString(); const anchorChunkX = reader.readInt16() << 5n; const anchorChunkZ = reader.readInt16() << 5n; - const chunks: CatchupChunk[] = new Array(Number(reader.readUnt10()) + 1); + const chunks: CatchupChunk[] = new Array( + Number(reader.readUnt10()) + 1, + ); for (let i = 0; i < chunks.length; i++) { chunks[i] = { chunkX: asInt32(anchorChunkX + reader.readUnt5()), @@ -253,6 +268,8 @@ export function decodePacket(reader: BufferReader): ServerboundPacket { return ServerboundHandshakePacket.decode(reader); case ServerboundIdentityResponsePacket.PACKET_ID: return ServerboundIdentityResponsePacket.decode(reader); + case ServerboundDimensionChangePacket.PACKET_ID: + return ServerboundDimensionChangePacket.decode(reader); case ServerboundChunkTimestampsRequestPacket.PACKET_ID: return ServerboundChunkTimestampsRequestPacket.decode(reader); case ServerboundCatchupRequestPacket.PACKET_ID: diff --git a/mapsync-server/src/server.ts b/mapsync-server/src/server.ts index 4bf6975..cc40606 100644 --- a/mapsync-server/src/server.ts +++ b/mapsync-server/src/server.ts @@ -110,6 +110,10 @@ export class WSClient { throw new Error("Client is not authenticated!"); } + public isInDimension(dimension: string): boolean { + return this.dimension === dimension; + } + public kick(internalReason: string) { this.log("Kicking:", internalReason); this.ws.close(1000);