Merge branch 'distant-horizons-core-serverSupport'

This commit is contained in:
James Seibel
2023-07-04 13:39:49 -05:00
9 changed files with 270 additions and 90 deletions
@@ -0,0 +1,15 @@
package com.seibel.distanthorizons.api.methods.events.interfaces;
/**
* @author Cailin
*/
public interface IDhServerMessageReceived<T> extends IDhApiEvent<T>
{
/**
* Triggered when a plugin message is received from the server.
* @param channel The name of the channel this was received on.
* @param message The message sent from the server.
*/
void serverMessageReceived(String channel, byte[] message);
}
@@ -21,6 +21,9 @@ 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.world.*;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.config.Config;
@@ -36,64 +39,64 @@ 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;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import io.netty.buffer.ByteBuf;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.glfw.GLFW;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
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
*/
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;
//==============//
// constructors //
//==============//
private ClientApi()
{
}
//========//
// events //
//========//
public void onClientOnlyConnected()
{
// only continue if the client is connected to a different server
@@ -103,11 +106,11 @@ public class ClientApi
{
LOGGER.info("Client on ClientOnly mode connecting.");
}
SharedApi.setDhWorld(new DhClientWorld());
}
}
public void onClientOnlyDisconnected()
{
if (MC.clientConnectedToDedicatedServer())
@@ -119,13 +122,17 @@ public class ClientApi
{
LOGGER.info("Client on ClientOnly mode disconnecting.");
}
world.close();
SharedApi.setDhWorld(null);
}
this.isServerCommunicationEnabled = false;
this.serverIsMalformed = false;
KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(false);
KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(null);
}
}
public void clientChunkLoadEvent(IChunkWrapper chunk, IClientLevelWrapper level)
{
if (SharedApi.getEnvironment() == EWorldEnvironment.Client_Only)
@@ -137,7 +144,7 @@ public class ClientApi
}
}
}
public void clientChunkSaveEvent(IChunkWrapper chunk, IClientLevelWrapper level)
{
if (SharedApi.getEnvironment() == EWorldEnvironment.Client_Only)
@@ -149,14 +156,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,12 +171,22 @@ public class ClientApi
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.");
LOGGER.info("Client level " + level + " loading.");
}
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
@@ -179,40 +196,55 @@ public class ClientApi
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));
}
}
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 +253,7 @@ public class ClientApi
}
ConfigBasedLogger.updateAll();
ConfigBasedSpamLogger.updateAll(doFlush);
IDhClientWorld clientWorld = SharedApi.getIDhClientWorld();
if (clientWorld != null)
{
@@ -230,12 +262,75 @@ public class ClientApi
profiler.pop();
}
/** @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)
{
return;
}
short commandLength = byteBuf.readShort();
if (commandLength > 32)
{
LOGGER.error("Server sent command > 32");
ClientApi.INSTANCE.serverIsMalformed = true;
return;
}
String eventType = byteBuf.readCharSequence(commandLength, StandardCharsets.UTF_8).toString();
switch (eventType)
{
case "ServerCommsEnabled":
LOGGER.info("Server supports DH protocol.");
ClientApi.INSTANCE.isServerCommunicationEnabled = true;
KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(true);
MC.executeOnRenderThread(() -> {
// Go ahead and unload the current world, because it may be wrong. We expect
// a followup WorldChanged event from the server soon anyways.
this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
});
break;
case "WorldChanged":
short worldKeyLength = byteBuf.readShort();
if (worldKeyLength > 128)
{
LOGGER.error("Server sent worldKey > 128");
this.serverIsMalformed = true;
return;
}
String worldKey = byteBuf.readCharSequence(worldKeyLength, StandardCharsets.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.executeOnRenderThread(() -> {
if (MC.getWrappedClientWorld() != null)
{
this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
}
IServerKeyedClientLevel clientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(MC.getWrappedClientWorld(), worldKey);
KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel);
this.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 +341,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 +352,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 +375,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 +393,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 +411,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 +426,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 +444,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,9 +5,11 @@ 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.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.util.objects.ParsedIp;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import javax.annotation.Nullable;
@@ -25,17 +27,17 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
{
final File folder;
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
public static final String INVALID_FILE_CHARACTERS_REGEX = "[\\\\/:*?\"<>|]";
SubDimensionLevelMatcher fileMatcher = null;
final HashMap<ILevelWrapper, File> levelToFileMap = new HashMap<>();
SubDimensionLevelMatcher subDimMatcher = null;
final HashMap<ILevelWrapper, File> levelWrapperToFileMap = new HashMap<>();
public ClientOnlySaveStructure()
{
this.folder = new File(MC_CLIENT.getGameDirectory().getPath() +
File.separatorChar + "Distant_Horizons_server_data" + File.separatorChar + getServerFolderName());
this.folder = new File(getSaveStructureFolderPath());
if (!this.folder.exists())
{
@@ -54,34 +56,51 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
//================//
@Override
public File getLevelFolder(ILevelWrapper level)
public File getLevelFolder(ILevelWrapper levelWrapper)
{
return this.levelToFileMap.computeIfAbsent(level, (newLevel) ->
return this.levelWrapperToFileMap.computeIfAbsent(levelWrapper, (newLevelWrapper) ->
{
if (Config.Client.Advanced.Multiplayer.multiverseSimilarityRequiredPercent.get() == 0)
// Use the server provided key if one was provided
if (newLevelWrapper instanceof IServerKeyedClientLevel)
{
if (this.fileMatcher != null)
IServerKeyedClientLevel keyedClientLevel = (IServerKeyedClientLevel) newLevelWrapper;
LOGGER.info("Loading level "+newLevelWrapper.getDimensionType().getDimensionName()+" with key: "+keyedClientLevel.getServerLevelKey());
// This world was identified by the server directly, so we can know for sure which folder to use.
return new File(getSaveStructureFolderPath() + File.separatorChar + keyedClientLevel.getServerLevelKey());
}
// use multiverse matching if enabled
if (Config.Client.Advanced.Multiplayer.multiverseSimilarityRequiredPercent.get() != 0)
{
// create the matcher if one doesn't exist
if (this.subDimMatcher == null || !this.subDimMatcher.isFindingLevel(newLevelWrapper))
{
this.fileMatcher.close();
this.fileMatcher = null;
LOGGER.info("Loading level " + newLevelWrapper.getDimensionType().getDimensionName());
this.subDimMatcher = new SubDimensionLevelMatcher(newLevelWrapper, this.folder,
this.getMatchingLevelFolders(newLevelWrapper).toArray(new File[0] /* surprisingly we don't need to create an array of any specific size for this to work */));
}
return this.getLevelFolderWithoutSimilarityMatching(newLevel);
File levelFile = this.subDimMatcher.tryGetLevel();
if (levelFile != null)
{
this.subDimMatcher.close();
this.subDimMatcher = null;
}
return levelFile;
}
if (this.fileMatcher == null || !this.fileMatcher.isFindingLevel(newLevel))
// we aren't using multiverse matching, shut down the matcher
// TODO this additional call may not be needed
if (this.subDimMatcher != null)
{
LOGGER.info("Loading level for world " + newLevel.getDimensionType().getDimensionName());
this.fileMatcher = new SubDimensionLevelMatcher(newLevel, this.folder,
this.getMatchingLevelFolders(newLevel).toArray(new File[0] /* surprisingly we don't need to create an array of any specific size for this to work */));
this.subDimMatcher.close();
this.subDimMatcher = null;
}
File levelFile = this.fileMatcher.tryGetLevel();
if (levelFile != null)
{
this.fileMatcher.close();
this.fileMatcher = null;
}
return levelFile;
// get the default folder
return this.getLevelFolderWithoutSimilarityMatching(newLevelWrapper);
});
}
@@ -129,7 +148,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
@Override
public File getRenderCacheFolder(ILevelWrapper level)
{
File levelFolder = this.levelToFileMap.get(level);
File levelFolder = this.levelWrapperToFileMap.get(level);
if (levelFolder == null)
{
return null;
@@ -141,7 +160,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
@Override
public File getFullDataFolder(ILevelWrapper level)
{
File levelFolder = this.levelToFileMap.get(level);
File levelFolder = this.levelWrapperToFileMap.get(level);
if (levelFolder == null)
{
return null;
@@ -173,7 +192,16 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
// a valid level folder needs to have DH specific folders in it
return files != null && files.length != 0;
}
private static String getSaveStructureFolderPath()
{
String path = MC_SHARED.getInstallationDirectory().getPath() + File.separatorChar
+ "Distant_Horizons_server_data" + File.separatorChar
+ getServerFolderName();
return path;
}
/** Generated from the server the client is currently connected to. */
private static String getServerFolderName()
{
@@ -213,15 +241,15 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
// This fixes some issues when the server is named something in other languages
return new PercentEscaper("", true).escape(folderName);
}
//==================//
// override methods //
//==================//
@Override
public void close() { this.fileMatcher.close(); }
public void close() { this.subDimMatcher.close(); }
@Override
public String toString() { return "[" + this.getClass().getSimpleName() + "@" + this.folder.getName() + "]"; }
@@ -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,24 @@
package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
/**
* Handles level overrides initiated by servers that
* support differentiating between different levels.
*/
public interface IKeyedClientLevelManager extends IBindable
{
/** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */
void setServerKeyedLevel(IServerKeyedClientLevel clientLevel);
IServerKeyedClientLevel getOverrideWrapper();
/** Returns a new instance of a ServerEnhancedClientLevel. */
IServerKeyedClientLevel getServerKeyedLevel(ILevelWrapper level, String serverLevelKey);
/** Sets the LOD engine to use the override wrapper, if the server has communication enabled. */
void setUseOverrideWrapper(boolean useOverrideWrapper);
boolean getUseOverrideWrapper();
}
@@ -0,0 +1,11 @@
package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
/** Enhances a {@link IClientLevelWrapper} with server provided level information. */
public interface IServerKeyedClientLevel extends IClientLevelWrapper
{
/** Returns the level key, which is used to select the correct folder on the client. */
String getServerLevelKey();
}
@@ -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;
@@ -138,5 +138,8 @@ public interface IMinecraftClientWrapper extends IBindable
void crashMinecraft(String errorMessage, Throwable exception); //FIXME: Move to IMinecraftSharedWrapper
Object getOptionsObject();
/** Executes the given task on Minecraft's render thread. */
void executeOnRenderThread(Runnable runnable);
}