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.
This commit is contained in:
Cailin Smith
2023-07-02 21:41:14 +02:00
parent 368541b09c
commit 649cd5bbe8
9 changed files with 216 additions and 61 deletions
@@ -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"));
}
}
}
@@ -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;
@@ -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)
@@ -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;
@@ -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();
}
@@ -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);
}
@@ -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;
@@ -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);
}
@@ -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);
}