Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions mapsync-mod/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ dependencies {
}
modImplementation(libs.fabricLoader)
modImplementation(libs.fabricApi)

project(":dep-websockets", configuration = "shadedElements").also {
implementation(it)
include(it)
}

modLocalDep(libs.fixChat)
modImplementation(libs.modmenu)

Expand Down
35 changes: 35 additions & 0 deletions mapsync-mod/dep-websockets/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
plugins {
id("java-library")
alias(libs.plugins.shadow)
}

version = libs.java.ws.get().version!!

dependencies {
implementation(libs.java.ws)
}

repositories {
mavenCentral()
}

tasks {
shadowJar {
include("org/java_websocket/**")
include("META-INF/LICENSE.txt")
rename("LICENSE.txt", "LICENSE_JavaWebSockets")
relocate(
"org.java_websocket",
"gjum.minecraft.mapsync.mod.deps.websockets"
)
}
}

val shadedElements by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}

artifacts {
add(shadedElements.name, tasks.shadowJar)
}
4 changes: 4 additions & 0 deletions mapsync-mod/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ voxelmap = { group = "maven.modrinth", name = "voxelmap-updated", version = "KgO
journeymap = { group = "maven.modrinth", name = "journeymap", version = "6vu2HyQi" }
# https://modrinth.com/mod/xaeros-world-map/versions?l=fabric
xaerosmap = { group = "maven.modrinth", name = "xaeros-world-map", version = "CkZVhVE0" }
# https://mvnrepository.com/artifact/org.java-websocket/Java-WebSocket
java-ws = { group = "org.java-websocket", name = "Java-WebSocket", version = "1.6.0" }

[plugins]
fabricLoom = { id = "fabric-loom", version.ref = "fabricLoom" }
# https://plugins.gradle.org/plugin/com.gradleup.shadow
shadow = { id = "com.gradleup.shadow", version = "9.4.1" }
2 changes: 2 additions & 0 deletions mapsync-mod/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ pluginManagement {
}

rootProject.name = "MapSync"

include(":dep-websockets");
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import gjum.minecraft.mapsync.mod.data.ChunkTile;
import gjum.minecraft.mapsync.mod.net.buffers.BufferWriter;
import gjum.minecraft.mapsync.mod.utils.Shortcuts;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.io.ByteArrayOutputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import net.minecraft.client.Minecraft;
Expand Down Expand Up @@ -36,10 +35,10 @@ public static ChunkTile chunkTileFromLevel(Level level, int cx, int cz) {

// TODO speedup: don't serialize twice (once here, once later when writing to network)
final byte[] dataHash; {
final ByteBuf columnsBuf = Unpooled.buffer();
Failable.run(() -> ChunkTile.writeColumns(columns, new BufferWriter(columnsBuf)));
final ByteArrayOutputStream os = new ByteArrayOutputStream();
Failable.run(() -> ChunkTile.writeColumns(columns, new BufferWriter(os)));
final MessageDigest md = Shortcuts.shaHash();
md.update(columnsBuf.nioBuffer());
md.update(os.toByteArray());
dataHash = md.digest();
}

Expand Down
135 changes: 51 additions & 84 deletions mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/MapSyncMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,32 @@
import gjum.minecraft.mapsync.mod.data.CatchupChunk;
import gjum.minecraft.mapsync.mod.data.ChunkTile;
import gjum.minecraft.mapsync.mod.data.RegionPos;
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.SyncClients;
import gjum.minecraft.mapsync.mod.net.UnexpectedPacketException;
import gjum.minecraft.mapsync.mod.net.auth.AuthProcess;
import gjum.minecraft.mapsync.mod.net.packet.ChunkTilePacket;
import gjum.minecraft.mapsync.mod.net.packet.ClientboundChunkTimestampsResponsePacket;
import gjum.minecraft.mapsync.mod.net.packet.ClientboundIdentityRequestPacket;
import gjum.minecraft.mapsync.mod.net.packet.ClientboundRegionTimestampsPacket;
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 it.unimi.dsi.fastutil.objects.Object2LongArrayMap;
import it.unimi.dsi.fastutil.objects.Object2LongMap;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
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;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ServerData;
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;
Expand All @@ -43,19 +44,6 @@
import org.lwjgl.glfw.GLFW;

public final class MapSyncMod implements ClientModInitializer {
public static final String VERSION; static {
final InputStream in = MapSyncMod.class.getResourceAsStream("/mapsync.version.const");
if (in == null) {
throw new ExceptionInInitializerError(new NullPointerException("'mapsync.version.const' const is missing!"));
}
try (in) {
VERSION = new String(in.readAllBytes(), StandardCharsets.UTF_8).trim();
}
catch (final IOException e) {
throw new ExceptionInInitializerError(e);
}
}

private static final Minecraft mc = Minecraft.getInstance();

public static final Logger logger = LogManager.getLogger(MapSyncMod.class);
Expand All @@ -81,8 +69,6 @@ public static MapSyncMod getMod() {
//"category.map-sync"
);

private @NotNull List<SyncClient> syncClients = new ArrayList<>();

/**
* Tracks state and render thread for current mc dimension.
* Never access this directly; always go through `getDimensionState()`.
Expand Down Expand Up @@ -115,18 +101,7 @@ public void onInitializeClient() {
e.printStackTrace();
}
});
}


/**
* for example: 1.0.0+forge
*/
public String getVersion() {
return VERSION + "+fabric";
}

public boolean isDevMode() {
return FabricLoader.getInstance().isDevelopmentEnvironment();
SyncClients.initEvents();
}

public void handleTick(
Expand All @@ -140,8 +115,36 @@ public void handleTick(
if (dimensionState != null) dimensionState.onTick();
}

public void handleConnectedToServer(ClientboundLoginPacket packet) {
getSyncClients();
public void handleSyncConnection(
final @NotNull SyncClient client
) throws Exception {
client.authState.set(null);
AuthProcess.sendHandshake(
client,
this.getDimensionState()
);
}

public void handleSyncDisconnection(
final @NotNull SyncClient client,
final @NotNull CloseContext context
) {
client.authState.set(null);
}

/// BEWARE: This is called from whatever thread the given SyncClient websocket is using for reads.
public void handleSyncPacket(
final @NotNull SyncClient client,
final @NotNull Packet received
) throws Exception {
switch (received) {
case ChunkTilePacket(ChunkTile chunkTile) -> handleSharedChunk(client, chunkTile);
case ClientboundIdentityRequestPacket packet -> AuthProcess.handleIdentityRequest(client, packet);
case ClientboundWelcomePacket packet -> AuthProcess.handleWelcome(client, packet);
case ClientboundRegionTimestampsPacket packet -> handleRegionTimestamps(client, packet);
case ClientboundChunkTimestampsResponsePacket packet -> handleCatchupData(client, packet);
default -> throw new UnexpectedPacketException(received);
}
}

public void handleRespawn(ClientboundRespawnPacket packet) {
Expand All @@ -167,48 +170,6 @@ public void handleRespawn(ClientboundRespawnPacket packet) {
return serverConfig;
}

public @NotNull List<SyncClient> getSyncClients() {
var serverConfig = getServerConfig();
if (serverConfig == null) return shutDownSyncClients();

var syncServerAddresses = serverConfig.getSyncServerAddresses();
if (syncServerAddresses.isEmpty()) return shutDownSyncClients();

// will be filled with clients that are still wanted (address) and are still connected
var existingClients = new HashMap<String, SyncClient>();

for (SyncClient client : syncClients) {
if (client.isShutDown) continue;
// avoid reconnecting to same sync server, to keep shared state (expensive to resync)
if (!client.gameAddress.equals(serverConfig.gameAddress)) {
debugLog("Disconnecting sync client; different game server");
client.shutDown();
} else if (!syncServerAddresses.contains(client.address)) {
debugLog("Disconnecting sync client; different sync address");
client.shutDown();
} else {
existingClients.put(client.address, client);
}
}

syncClients = syncServerAddresses.stream().map(address -> {
var client = existingClients.get(address);
if (client == null) client = new SyncClient(address, serverConfig.gameAddress);
client.autoReconnect = true;
return client;
}).collect(Collectors.toList());

return syncClients;
}

public List<SyncClient> shutDownSyncClients() {
for (SyncClient client : syncClients) {
client.shutDown();
}
syncClients.clear();
return Collections.emptyList();
}

/**
* for current dimension
*/
Expand Down Expand Up @@ -253,7 +214,7 @@ public void handleMcFullChunk(int cx, int cz) {
if (RenderQueue.areAllMapModsMapping()) {
dimensionState.setChunkTimestamp(chunkTile.chunkPos(), chunkTile.timestamp());
}
for (SyncClient client : getSyncClients()) {
for (SyncClient client : SyncClients.get().orElseThrow()) {
client.sendChunkTile(chunkTile);
}
}
Expand All @@ -271,7 +232,8 @@ public void handleSyncServerEncryptionSuccess() {
// TODO tell server our current dimension
}

public void handleRegionTimestamps(ClientboundRegionTimestampsPacket packet, SyncClient client) {
public void handleRegionTimestamps(SyncClient client, ClientboundRegionTimestampsPacket packet) {
client.authState.requireWelcomed();
DimensionState dimension = getDimensionState();
if (dimension == null) return;
if (!dimension.dimension.identifier().toString().equals(packet.dimension())) {
Expand All @@ -294,9 +256,10 @@ public void handleRegionTimestamps(ClientboundRegionTimestampsPacket packet, Syn
}
}

public void handleSharedChunk(ChunkTile chunkTile) {
public void handleSharedChunk(SyncClient client, ChunkTile chunkTile) {
client.authState.requireWelcomed();
debugLog("received shared chunk: " + chunkTile.chunkPos());
for (SyncClient syncClient : getSyncClients()) {
for (SyncClient syncClient : SyncClients.get().orElseThrow()) {
syncClient.setServerKnownChunkHash(chunkTile.chunkPos(), chunkTile.dataHash());
}

Expand All @@ -305,10 +268,14 @@ public void handleSharedChunk(ChunkTile chunkTile) {
dimensionState.processSharedChunk(chunkTile);
}

public void handleCatchupData(ClientboundChunkTimestampsResponsePacket packet) {
public void handleCatchupData(SyncClient client, ClientboundChunkTimestampsResponsePacket packet) {
client.authState.requireWelcomed();
for (CatchupChunk chunk : packet.chunks()) {
chunk.syncClient = client;
}
var dimensionState = getDimensionState();
if (dimensionState == null) return;
debugLog("received catchup: " + packet.chunks().size() + " " + packet.chunks().get(0).syncClient.address);
debugLog("received catchup: " + packet.chunks().size() + " " + client.syncAddress);
dimensionState.addCatchupChunks(packet.chunks());
}

Expand Down
43 changes: 27 additions & 16 deletions mapsync-mod/src/main/java/gjum/minecraft/mapsync/mod/ModGui.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import static gjum.minecraft.mapsync.mod.MapSyncMod.getMod;

import gjum.minecraft.mapsync.mod.config.ServerConfig;
import gjum.minecraft.mapsync.mod.net.SyncClients;
import java.util.HashSet;
import java.util.List;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
Expand Down Expand Up @@ -98,10 +100,9 @@ protected void init() {
public void connectClicked(Button btn) {
try {
if (syncServerAddressField == null) return;
var addresses = List.of(syncServerAddressField.getValue().split("[^-_.:A-Za-z0-9]+"));
var addresses = List.of(syncServerAddressField.getValue().split("[^-_.:A-Za-z0-9/]+"));
serverConfig.setSyncServerAddresses(addresses);
getMod().shutDownSyncClients();
getMod().getSyncClients();
SyncClients.get().orElseThrow().setAll(new HashSet<>(addresses));
btn.active = false;
syncServerDisconnectBtn.active = true;
} catch (Throwable e) {
Expand All @@ -112,7 +113,7 @@ public void connectClicked(Button btn) {
// TODO: not working
public void disconnectClicked(Button btn) {
if (syncServerAddressField == null) return;
getMod().shutDownSyncClients();
SyncClients.get().orElseThrow().closeAll(true);
btn.active = false;
}

Expand Down Expand Up @@ -148,21 +149,31 @@ public void render(@NotNull GuiGraphics guiGraphics, int i, int j, float f) {
}

int msgY = syncServerAddressField.getY() + 25;
var syncClients = getMod().getSyncClients();
for (var client : syncClients) {
for (var client : SyncClients.get().orElseThrow()) {
int statusColor;
String statusText;
if (client.isEncrypted()) {
statusColor = 0xFF008800;
statusText = "Connected";
} else if (client.getError() != null) {
statusColor = 0xFFff8888;
statusText = client.getError();
} else {
statusColor = 0xFFffffff;
statusText = "Connecting...";

var connectionState = client.state();
switch (connectionState) {
case DISCONNECTED -> {
statusColor = 0xFFff8888;
statusText = "Disconnected";
}
case CONNECTED -> {
statusColor = 0xFF8888ff;
statusText = "Connected (not authed)";
}
case WELCOMED -> {
statusColor = 0xFF88ff88;
statusText = "Connected and authed";
}
default -> {
statusColor = 0xFFFFFF00;
statusText = "Unknown state: " + connectionState;
}
}
statusText = client.address + " " + statusText;

statusText = client.syncAddress + " " + statusText;
guiGraphics.drawString(font, statusText, left, msgY, statusColor);
msgY += 10;
}
Expand Down
Loading