diff --git a/api/src/main/java/com/seibel/distanthorizons/api/methods/events/interfaces/IDhServerMessageReceived.java b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/interfaces/IDhServerMessageReceived.java new file mode 100644 index 000000000..378f8fe3d --- /dev/null +++ b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/interfaces/IDhServerMessageReceived.java @@ -0,0 +1,15 @@ +package com.seibel.distanthorizons.api.methods.events.interfaces; + +/** + * @author Cailin + */ +public interface IDhServerMessageReceived extends IDhApiEvent +{ + /** + * 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); + +} 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..d2fd8fb9f 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,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")); } } - - + + } 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..a7ec08d8d 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,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 levelToFileMap = new HashMap<>(); + SubDimensionLevelMatcher subDimMatcher = null; + final HashMap 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() + "]"; } 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 7dd641339..0f692235b 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/IKeyedClientLevelManager.java b/core/src/main/java/com/seibel/distanthorizons/core/level/IKeyedClientLevelManager.java new file mode 100644 index 000000000..b8e6a324e --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/IKeyedClientLevelManager.java @@ -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(); + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/IServerKeyedClientLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/IServerKeyedClientLevel.java new file mode 100644 index 000000000..24577e59e --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/IServerKeyedClientLevel.java @@ -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(); + +} 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/IMinecraftClientWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java index 252a2bea6..05f4e8979 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,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); }