From 895a0db54291403b50c1aade87a08af986895abf Mon Sep 17 00:00:00 2001 From: James Seibel Date: Thu, 27 Jul 2023 21:35:03 -0500 Subject: [PATCH] Fix Forge client-side multiplayer --- .../core/api/internal/ClientApi.java | 287 +++++++++++------- .../renderfile/RenderSourceFileHandler.java | 2 +- .../core/level/ClientLevelModule.java | 4 +- .../core/util/objects/Pair.java | 23 ++ 4 files changed, 200 insertions(+), 116 deletions(-) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/util/objects/Pair.java diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java index 644d47172..b97e8596e 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java @@ -21,9 +21,10 @@ package com.seibel.distanthorizons.core.api.internal; import com.seibel.distanthorizons.api.methods.events.abstractEvents.*; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; -import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager; +import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.pos.DhChunkPos; +import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.world.*; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.core.level.IDhClientLevel; @@ -52,6 +53,8 @@ import org.lwjgl.glfw.GLFW; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.HashSet; import java.util.concurrent.TimeUnit; /** @@ -62,53 +65,71 @@ import java.util.concurrent.TimeUnit; public class ClientApi { private static final Logger LOGGER = LogManager.getLogger(); - public static final boolean ENABLE_EVENT_LOGGING = true; + public static boolean prefLoggerEnabled = false; - + public static final ClientApi INSTANCE = new ClientApi(); public static TestRenderer testRenderer = new TestRenderer(); + private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class); - + public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS); - + private boolean configOverrideReminderPrinted = false; public boolean rendererDisabledBecauseOfExceptions = false; - + private long lastFlushNanoTime = 0; - - private boolean isServerCommunicationEnabled = true; - - private boolean serverIsMalformed = false; - - + + private boolean isServerCommunicationEnabled = false; + + /** set to true if any unexpected responses are received from the server */ + private boolean serverNetworkingIsMalformed = false; + + /** Holds any levels that were loaded before the {@link ClientApi#onClientOnlyConnected} was fired. */ + private final HashSet waitingClientLevels = new HashSet<>(); + /** Holds any chunks that were loaded before the {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} was fired. */ + private final HashMap, IChunkWrapper> waitingChunkByClientLevelAndPos = new HashMap<>(); + + + //==============// // constructors // //==============// - - private ClientApi() - { - - } - - - - //========// - // events // - //========// - + + private ClientApi() { } + + + + //==============// + // world events // + //==============// + + /** + * May be fired slightly before or after the associated + * {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} event + * depending on how the host mod loader functions. + */ public void onClientOnlyConnected() { // only continue if the client is connected to a different server if (MC.clientConnectedToDedicatedServer()) { - if (ENABLE_EVENT_LOGGING) - { - LOGGER.info("Client on ClientOnly mode connecting."); - } - + LOGGER.info("Client on ClientOnly mode connecting."); + + // firing after clientLevelLoadEvent + // TODO if level has prepped to load it should fire level load event SharedApi.setDhWorld(new DhClientWorld()); + + + LOGGER.info("Loading ["+this.waitingClientLevels.size()+"] waiting client level wrappers."); + for (IClientLevelWrapper level : this.waitingClientLevels) + { + this.clientLevelLoadEvent(level); + } + + this.waitingClientLevels.clear(); } } @@ -119,21 +140,94 @@ public class ClientApi AbstractDhWorld world = SharedApi.getAbstractDhWorld(); if (world != null) { - if (ENABLE_EVENT_LOGGING) - { - LOGGER.info("Client on ClientOnly mode disconnecting."); - } + LOGGER.info("Client on ClientOnly mode disconnecting."); world.close(); SharedApi.setDhWorld(null); } + + // clear the previous server's information this.isServerCommunicationEnabled = false; - this.serverIsMalformed = false; + this.serverNetworkingIsMalformed = false; KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(false); KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(null); + + // remove any waiting items + this.waitingChunkByClientLevelAndPos.clear(); + this.waitingClientLevels.clear(); } } - + + + + //==============// + // level events // + //==============// + + public void clientLevelUnloadEvent(IClientLevelWrapper level) + { + LOGGER.info("Client level "+level+" unloading."); + + AbstractDhWorld world = SharedApi.getAbstractDhWorld(); + if (world != null) + { + world.unloadLevel(level); + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level)); + } + } + + public void clientLevelLoadEvent(IClientLevelWrapper level) + { + if (this.isServerCommunicationEnabled) + { + LOGGER.info("Server supports communication, deferring loading."); + return; + } + + + LOGGER.info("Client level " + level + " loading."); + + AbstractDhWorld world = SharedApi.getAbstractDhWorld(); + if (world != null) + { + world.getOrLoadLevel(level); + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); + + this.loadWaitingChunksForLevel(level); + } + else + { + this.waitingClientLevels.add(level); + } + } + private void loadWaitingChunksForLevel(IClientLevelWrapper level) + { + HashSet> keysToRemove = new HashSet<>(); + for (Pair levelChunkPair : this.waitingChunkByClientLevelAndPos.keySet()) + { + // only load chunks that came from this level + IClientLevelWrapper levelWrapper = levelChunkPair.first; + if (levelWrapper.equals(level)) + { + IChunkWrapper chunkWrapper = this.waitingChunkByClientLevelAndPos.get(levelChunkPair); + this.applyChunkUpdate(chunkWrapper, levelWrapper); + keysToRemove.add(levelChunkPair); + } + } + LOGGER.info("Loaded ["+keysToRemove.size()+"] waiting chunk wrappers."); + + for (Pair keyToRemove : keysToRemove) + { + this.waitingChunkByClientLevelAndPos.remove(keyToRemove); + } + } + + + + //=======================// + // chunk modified events // + //=======================// + public void clientChunkLoadEvent(IChunkWrapper chunk, IClientLevelWrapper level) { this.applyChunkUpdate(chunk, level); } public void clientChunkSaveEvent(IChunkWrapper chunk, IClientLevelWrapper level) { this.applyChunkUpdate(chunk, level); } private void applyChunkUpdate(IChunkWrapper chunk, IClientLevelWrapper level) @@ -144,10 +238,14 @@ public class ClientApi return; } - // only continue if the level is still loaded + // only continue if the level is loaded IDhLevel dhLevel = SharedApi.getAbstractDhWorld().getLevel(level); if (dhLevel == null) { + // If the level isn't loaded yet, keep track of which chunks were loaded so we can use them later. + // This may happen if the world and level load events happen out of order + this.waitingChunkByClientLevelAndPos.replace(new Pair<>(level, chunk.getChunkPos()), chunk); + return; } @@ -169,81 +267,25 @@ public class ClientApi } } - - public void clientLevelUnloadEvent(IClientLevelWrapper level) - { - if (ENABLE_EVENT_LOGGING) - { - LOGGER.info("Client level "+level+" unloading."); - } - - AbstractDhWorld world = SharedApi.getAbstractDhWorld(); - if (world != null) - { - world.unloadLevel(level); - ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level)); - } - } - - public void clientLevelLoadEvent(IClientLevelWrapper level) - { - if (this.isServerCommunicationEnabled) - { - if (ENABLE_EVENT_LOGGING) - { - LOGGER.info("Server supports communication, deferring loading."); - } - return; - } - - - if (ENABLE_EVENT_LOGGING) - { - LOGGER.info("Client level " + level + " loading."); - } - - AbstractDhWorld world = SharedApi.getAbstractDhWorld(); - if (world != null) - { - world.getOrLoadLevel(level); - ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); - } - } - - public void serverLevelLoadEvent(IServerKeyedClientLevel level) - { - if (ENABLE_EVENT_LOGGING) - { - LOGGER.info("Server level " + level + " (" + level.getServerLevelKey() + ") loading."); - } - - AbstractDhWorld world = SharedApi.getAbstractDhWorld(); - if (world != null) - { - world.getOrLoadLevel(level); - ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); - } - } - + + + //===============// + // render events // + //===============// + public void rendererShutdownEvent() { - if (ENABLE_EVENT_LOGGING) - { - LOGGER.info("Renderer shutting down."); - } + LOGGER.info("Renderer shutting down."); IProfilerWrapper profiler = MC.getProfiler(); profiler.push("DH-RendererShutdown"); profiler.pop(); } - + public void rendererStartupEvent() { - if (ENABLE_EVENT_LOGGING) - { - LOGGER.info("Renderer starting up."); - } + LOGGER.info("Renderer starting up."); IProfilerWrapper profiler = MC.getProfiler(); profiler.push("DH-RendererStartup"); @@ -252,7 +294,7 @@ public class ClientApi GLProxy.getInstance(); profiler.pop(); } - + public void clientTickEvent() { IProfilerWrapper profiler = MC.getProfiler(); @@ -275,24 +317,41 @@ public class ClientApi profiler.pop(); } + + + //============// + // networking // + //============// + /** @param byteBuf is Netty's {@link ByteBuffer} wrapper. */ public void serverMessageReceived(ByteBuf byteBuf) { // It is important to ensure malicious server input is ignored. - if (this.serverIsMalformed) + if (this.serverNetworkingIsMalformed) { return; } short commandLength = byteBuf.readShort(); - if (commandLength > 32) // TODO 32 should be put into a constant somewhere + if (commandLength > 32) // TODO 32 should be put into a constant somewhere, what does it represent? { LOGGER.error("Server sent command > 32"); - ClientApi.INSTANCE.serverIsMalformed = true; + ClientApi.INSTANCE.serverNetworkingIsMalformed = true; + return; + } + + + String eventType = null; + try + { + eventType = byteBuf.readCharSequence(commandLength, StandardCharsets.UTF_8).toString(); + } + catch (Exception e) + { + LOGGER.error("Server sent un-parsable command. Error: "+e.getMessage()); return; } - String eventType = byteBuf.readCharSequence(commandLength, StandardCharsets.UTF_8).toString(); switch (eventType) { case "ServerCommsEnabled": @@ -305,13 +364,13 @@ public class ClientApi this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld()); }); break; - + case "WorldChanged": short worldKeyLength = byteBuf.readShort(); if (worldKeyLength > 128) // TODO 128 should be put into a constant somewhere { LOGGER.error("Server sent worldKey > 128"); - this.serverIsMalformed = true; + this.serverNetworkingIsMalformed = true; return; } @@ -320,7 +379,7 @@ public class ClientApi { LOGGER.error("Server sent invalid world key name, and is being ignored."); this.isServerCommunicationEnabled = false; - this.serverIsMalformed = true; + this.serverNetworkingIsMalformed = true; return; } @@ -332,18 +391,18 @@ public class ClientApi } IServerKeyedClientLevel clientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(MC.getWrappedClientWorld(), worldKey); KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel); - this.serverLevelLoadEvent(clientLevel); + this.clientLevelLoadEvent(clientLevel); }); break; } } - - - + + + //===========// // rendering // //===========// - + public void renderLods(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks) { if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC.playerExists()) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java index 3e238ba8e..61455e81a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java @@ -72,7 +72,7 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider { LOGGER.warn("Unable to create render data folder, file saving may fail."); } - fileHandlerThreadPool = ThreadUtil.makeSingleThreadPool("Render Source File Handler ["+this.level.getClientLevelWrapper().getDimensionType().getDimensionName()+"]"); + this.fileHandlerThreadPool = ThreadUtil.makeSingleThreadPool("Render Source File Handler ["+this.level.getLevelWrapper().getDimensionType().getDimensionName()+"]"); this.threadPoolMsg = new F3Screen.NestedMessage(this::f3Log); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java b/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java index ba0499ccd..9eae2bf57 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java @@ -257,7 +257,9 @@ public class ClientLevelModule implements Closeable { this.renderSourceFileHandler = new RenderSourceFileHandler(fullDataSourceProvider, dhClientLevel, saveStructure); this.quadtree = new LodQuadTree(dhClientLevel, Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistance.get() * LodUtil.CHUNK_WIDTH, - MC_CLIENT.getPlayerBlockPos().x, MC_CLIENT.getPlayerBlockPos().z, this.renderSourceFileHandler); + // initial position is (0,0) just in case the player hasn't loaded in yet, the tree will be moved once the level starts ticking + 0, 0, + this.renderSourceFileHandler); RenderBufferHandler renderBufferHandler = new RenderBufferHandler(this.quadtree); this.renderer = new LodRenderer(renderBufferHandler); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/Pair.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/Pair.java new file mode 100644 index 000000000..81850b34f --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/Pair.java @@ -0,0 +1,23 @@ +package com.seibel.distanthorizons.core.util.objects; + +import java.util.Objects; + +/** A simple way to hold 2 objects together */ +public final class Pair +{ + public final T first; + public final U second; + + public Pair(T first, U second) + { + this.second = second; + this.first = first; + } + + @Override + public String toString() { return "("+this.first+", "+this.second+")"; } + + @Override + public int hashCode() { return Objects.hash(this.first, this.second); } + +}