From 649cd5bbe85d8b5a513eb13bd65a61e75bbb20e6 Mon Sep 17 00:00:00 2001 From: Cailin Smith Date: Sun, 2 Jul 2023 21:41:14 +0200 Subject: [PATCH] Add ability for servers to communicate with the client to set the world. This prevents the client from accidentally selected the wrong world folder to load LODs from, since levels of the same dimension can't naturally be distinguished from each other. With level similarity detection, this can sometimes work, but in general is not reliable. This mechanism instead allows servers to send a packet to the client on load, enabling the override system, and then a second packet on world change, which specifically sets the world key, based on knowledge that only the server has, leading to a reliable way of detecting the correct world. --- .../core/api/internal/ClientApi.java | 204 ++++++++++++------ .../core/api/internal/SharedApi.java | 2 + .../structure/ClientOnlySaveStructure.java | 9 + .../core/level/DhClientLevel.java | 1 + .../level/IServerEnhancedClientLevel.java | 15 ++ .../core/level/IServerEnhancedManager.java | 26 +++ .../core/world/DhClientWorld.java | 1 + .../minecraft/IFriendlyByteBuf.java | 13 ++ .../minecraft/IMinecraftClientWrapper.java | 6 + 9 files changed, 216 insertions(+), 61 deletions(-) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/level/IServerEnhancedClientLevel.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/level/IServerEnhancedManager.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IFriendlyByteBuf.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 cef0c1e8f..a0695cbb3 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,6 +21,11 @@ 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.IServerEnhancedClientLevel; +import com.seibel.distanthorizons.core.level.IServerEnhancedManager; +import com.seibel.distanthorizons.core.world.*; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IFriendlyByteBuf; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.config.Config; @@ -36,10 +41,6 @@ import com.seibel.distanthorizons.coreapi.util.math.Mat4f; import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.renderer.TestRenderer; import com.seibel.distanthorizons.core.util.RenderUtil; -import com.seibel.distanthorizons.core.world.DhClientWorld; -import com.seibel.distanthorizons.core.world.AbstractDhWorld; -import com.seibel.distanthorizons.core.world.IDhClientWorld; -import com.seibel.distanthorizons.core.world.EWorldEnvironment; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; @@ -49,13 +50,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.lwjgl.glfw.GLFW; +import java.nio.charset.Charset; import java.util.concurrent.TimeUnit; /** * This holds the methods that should be called * by the host mod loader (Fabric, Forge, etc.). * Specifically for the client. - * + * * @author James Seibel * @version 2022-9-16 */ @@ -64,36 +66,41 @@ 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 IServerEnhancedManager SERVER_ENHANCED_MANAGER + = SingletonInjector.INSTANCE.get(IServerEnhancedManager.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; + + //==============// // constructors // //==============// - + private ClientApi() { - + } - - - + + + //========// // events // //========// - + public void onClientOnlyConnected() { // only continue if the client is connected to a different server @@ -103,11 +110,11 @@ public class ClientApi { LOGGER.info("Client on ClientOnly mode connecting."); } - + SharedApi.setDhWorld(new DhClientWorld()); } } - + public void onClientOnlyDisconnected() { if (MC.clientConnectedToDedicatedServer()) @@ -119,13 +126,17 @@ public class ClientApi { LOGGER.info("Client on ClientOnly mode disconnecting."); } - + world.close(); SharedApi.setDhWorld(null); } + this.isServerCommunicationEnabled = false; + this.serverIsMalformed = false; + SERVER_ENHANCED_MANAGER.setUseOverrideWrapper(false); + SERVER_ENHANCED_MANAGER.registerServerEnhancedLevel(null); } } - + public void clientChunkLoadEvent(IChunkWrapper chunk, IClientLevelWrapper level) { if (SharedApi.getEnvironment() == EWorldEnvironment.Client_Only) @@ -137,7 +148,7 @@ public class ClientApi } } } - + public void clientChunkSaveEvent(IChunkWrapper chunk, IClientLevelWrapper level) { if (SharedApi.getEnvironment() == EWorldEnvironment.Client_Only) @@ -149,14 +160,14 @@ 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) { @@ -164,14 +175,18 @@ public class ClientApi ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level)); } } - + public void clientLevelLoadEvent(IClientLevelWrapper level) { - if (ENABLE_EVENT_LOGGING) - { - LOGGER.info("Client level "+level+" loading."); + if (ENABLE_EVENT_LOGGING) { + if (this.isServerCommunicationEnabled) { + LOGGER.info("Server supports communication, deferring loading."); + return; + } + + LOGGER.info("Client level " + level + " loading."); } - + AbstractDhWorld world = SharedApi.getAbstractDhWorld(); if (world != null) { @@ -179,40 +194,55 @@ public class ClientApi ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); } } - + + public void serverLevelLoadEvent(IServerEnhancedClientLevel level) + { + if (ENABLE_EVENT_LOGGING) + { + LOGGER.info("Server level " + level + " (" + level.getServerWorldKey() + ") loading."); + } + + AbstractDhWorld world = SharedApi.getAbstractDhWorld(); + if (world != null) + { + world.getOrLoadLevel(level); + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); + } + } + public void rendererShutdownEvent() { if (ENABLE_EVENT_LOGGING) { 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."); } - + IProfilerWrapper profiler = MC.getProfiler(); profiler.push("DH-RendererStartup"); - + // make sure the GLProxy is created before the LodBufferBuilder needs it GLProxy.getInstance(); profiler.pop(); } - + public void clientTickEvent() { IProfilerWrapper profiler = MC.getProfiler(); profiler.push("DH-ClientTick"); - + boolean doFlush = System.nanoTime() - this.lastFlushNanoTime >= SPAM_LOGGER_FLUSH_NS; if (doFlush) { @@ -221,7 +251,7 @@ public class ClientApi } ConfigBasedLogger.updateAll(); ConfigBasedSpamLogger.updateAll(doFlush); - + IDhClientWorld clientWorld = SharedApi.getIDhClientWorld(); if (clientWorld != null) { @@ -229,13 +259,65 @@ public class ClientApi } profiler.pop(); } - - - + + public void serverMessageReceived(IFriendlyByteBuf buf) + { + // It is important to ensure malicious server input is ignored. + if(this.serverIsMalformed) { + return; + } + short commandLength = buf.readShort(); + if(commandLength > 32) { + LOGGER.error("Server sent command > 32"); + ClientApi.INSTANCE.serverIsMalformed = true; + return; + } + String eventType = buf.readCharSequence(commandLength, Charset.forName("UTF-8")).toString(); + switch(eventType) { + case "ServerCommsEnabled": + LOGGER.info("Server supports DH protocol."); + ClientApi.INSTANCE.isServerCommunicationEnabled = true; + SERVER_ENHANCED_MANAGER.setUseOverrideWrapper(true); + MC.execute(() -> { + // Go ahead and unload the current world, because it may be wrong. We expect + // a followup WorldChanged event from the server soon anyways. + clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld()); + }); + break; + case "WorldChanged": + short worldKeyLength = buf.readShort(); + if(worldKeyLength > 128) { + LOGGER.error("Server sent worldKey > 128"); + this.serverIsMalformed = true; + return; + } + String worldKey = buf.readCharSequence(worldKeyLength, Charset.forName("UTF-8")).toString(); + if(!worldKey.matches("[a-zA-Z0-9_]+")) { + LOGGER.error("Server sent invalid world key name, and is being ignored."); + this.isServerCommunicationEnabled = false; + this.serverIsMalformed = true; + return; + } + LOGGER.info("Server sent world change event: " + worldKey); + MC.execute(() -> { + if(MC.getWrappedClientWorld() != null) { + clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld()); + } + IServerEnhancedClientLevel clientLevel + = SERVER_ENHANCED_MANAGER.getServerEnhancedLevel(MC.getWrappedClientWorld(), worldKey); + SERVER_ENHANCED_MANAGER.registerServerEnhancedLevel(clientLevel); + serverLevelLoadEvent(clientLevel); + }); + break; + } + } + + + //===========// // rendering // //===========// - + public void renderLods(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks) { if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC.playerExists()) @@ -246,8 +328,8 @@ public class ClientApi MC.sendChatMessage("Here be dragons!"); this.configOverrideReminderPrinted = true; } - - + + IProfilerWrapper profiler = MC.getProfiler(); profiler.pop(); // get out of "terrain" profiler.push("DH-RenderLevel"); @@ -257,20 +339,20 @@ public class ClientApi { return; } - - + + //FIXME: Improve class hierarchy of DhWorld, IClientWorld, IServerWorld to fix all this hard casting // (also in RenderUtil) IDhClientWorld dhClientWorld = SharedApi.getIDhClientWorld(); IDhClientLevel level = dhClientWorld.getOrLoadClientLevel(levelWrapper); - + if (prefLoggerEnabled) { level.dumpRamUsage(); } - - - + + + profiler.push("Render" + (Config.Client.Advanced.Debugging.rendererMode.get() == ERendererMode.DEFAULT ? "-lods" : "-debug")); try { @@ -280,7 +362,7 @@ public class ClientApi new DhApiRenderParam(mcProjectionMatrix, mcModelViewMatrix, RenderUtil.createLodProjectionMatrix(mcProjectionMatrix, partialTicks), RenderUtil.createLodModelViewMatrix(mcModelViewMatrix), partialTicks); - + boolean renderingCanceled = ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderEvent.class, new DhApiBeforeRenderEvent.EventParam(renderEventParam)); if (!this.rendererDisabledBecauseOfExceptions && !renderingCanceled) { @@ -298,7 +380,7 @@ public class ClientApi { this.rendererDisabledBecauseOfExceptions = true; LOGGER.error("Renderer thrown an uncaught exception: ", e); - + MC.sendChatMessage("\u00A74\u00A7l\u00A7uERROR: Distant Horizons" + " renderer has encountered an exception!"); MC.sendChatMessage("\u00A74Renderer is now disabled to prevent further issues."); @@ -316,13 +398,13 @@ public class ClientApi profiler.push("terrain"); // go back into "terrain" } } - - - + + + //=================// // DEBUG USE // //=================// - + /** Trigger once on key press, with CLIENT PLAYER. */ public void keyPressedEvent(int glfwKey) { @@ -331,8 +413,8 @@ public class ClientApi // keybindings are disabled return; } - - + + if (glfwKey == GLFW.GLFW_KEY_F8) { Config.Client.Advanced.Debugging.debugRendering.set(EDebugRendering.next(Config.Client.Advanced.Debugging.debugRendering.get())); @@ -349,6 +431,6 @@ public class ClientApi MC.sendChatMessage("P: Debug Pref Logger is " + (prefLoggerEnabled ? "enabled" : "disabled")); } } - - + + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java index c6a063850..a16764ae9 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java @@ -5,6 +5,7 @@ import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnR import com.seibel.distanthorizons.core.dataObjects.transformers.DataRenderTransformer; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataFileHandler; import com.seibel.distanthorizons.core.generation.WorldGenerationQueue; +import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.world.*; import com.seibel.distanthorizons.core.world.*; @@ -12,6 +13,7 @@ import com.seibel.distanthorizons.core.world.*; public class SharedApi { private static AbstractDhWorld currentWorld; + diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java b/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java index 3ad53c5ca..285009d32 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java @@ -5,6 +5,7 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.file.subDimMatching.SubDimensionLevelMatcher; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.api.enums.config.EServerFolderNameMode; +import com.seibel.distanthorizons.core.level.IServerEnhancedClientLevel; import com.seibel.distanthorizons.core.util.objects.ParsedIp; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; @@ -58,6 +59,14 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure { return this.levelToFileMap.computeIfAbsent(level, (newLevel) -> { + if (newLevel instanceof IServerEnhancedClientLevel) { + IServerEnhancedClientLevel secl = (IServerEnhancedClientLevel) newLevel; + // This world was identified by the server directly, so we can know for sure which folder to use. + File seclFolder = new File(this.folder.getParent(), MC_CLIENT.getCurrentServerIp().toString()); + seclFolder = new File(seclFolder, secl.getServerWorldKey()); + return seclFolder; + } + if (Config.Client.Advanced.Multiplayer.multiverseSimilarityRequiredPercent.get() == 0) { if (this.fileMatcher != null) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java index 9e63773a9..06fb790cc 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java @@ -4,6 +4,7 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedF import com.seibel.distanthorizons.core.file.fullDatafile.IFullDataSourceProvider; import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataFileHandler; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; +import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/IServerEnhancedClientLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/IServerEnhancedClientLevel.java new file mode 100644 index 000000000..7f50aad1d --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/IServerEnhancedClientLevel.java @@ -0,0 +1,15 @@ +package com.seibel.distanthorizons.core.level; + +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; + +/** + * Enhances an IClientLevelWrapper with server provided world information. + */ +public interface IServerEnhancedClientLevel extends IClientLevelWrapper +{ + /** + * Returns the world key, which is used to select the correct folder on the client. + * @return + */ + String getServerWorldKey(); +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/IServerEnhancedManager.java b/core/src/main/java/com/seibel/distanthorizons/core/level/IServerEnhancedManager.java new file mode 100644 index 000000000..bd252e959 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/IServerEnhancedManager.java @@ -0,0 +1,26 @@ +package com.seibel.distanthorizons.core.level; + +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable; + +public interface IServerEnhancedManager extends IBindable { + /** + * Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. + * @param clientLevel + */ + void registerServerEnhancedLevel(IServerEnhancedClientLevel clientLevel); + + /** + * Returns a new instance of a ServerEnhancedClientLevel. + * @param level + * @param worldKey + * @return + */ + IServerEnhancedClientLevel getServerEnhancedLevel(ILevelWrapper level, String worldKey); + + /** + * Sets the LOD engine to use the override wrapper, if the server has communication enabled. + * @param useOverrideWrapper + */ + void setUseOverrideWrapper(boolean useOverrideWrapper); +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java index 1c9126f19..553731fe8 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java @@ -45,6 +45,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld return this.levels.computeIfAbsent((IClientLevelWrapper) wrapper, (clientLevelWrapper) -> { File file = this.saveStructure.getLevelFolder(wrapper); + if (file == null) { return null; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IFriendlyByteBuf.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IFriendlyByteBuf.java new file mode 100644 index 000000000..fac5fac2b --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IFriendlyByteBuf.java @@ -0,0 +1,13 @@ +package com.seibel.distanthorizons.core.wrapperInterfaces.minecraft; + +import java.nio.charset.Charset; + +/** + * Interface that wraps the net.minecraft.network.FriendlyByteBuffer. + */ +public interface IFriendlyByteBuf { + + short readShort(); + + CharSequence readCharSequence(int length, Charset charset); +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java index 252a2bea6..57f8b573a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java @@ -138,5 +138,11 @@ public interface IMinecraftClientWrapper extends IBindable void crashMinecraft(String errorMessage, Throwable exception); //FIXME: Move to IMinecraftSharedWrapper Object getOptionsObject(); + + /** + * Executes a task on the Minecraft render thread. + * @param runnable + */ + void execute(Runnable runnable); }