diff --git a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java index 93fc2b000..a91c1a792 100644 --- a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java +++ b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java @@ -31,7 +31,7 @@ public final class ModInfo public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial"; /** Incremented every time any packets are added, changed or removed, with a few exceptions. */ - public static final int PROTOCOL_VERSION = 13; + public static final int PROTOCOL_VERSION = 14; /** * The full plugin channel name (RESOURCE_NAMESPACE:WRAPPER_PACKET_PATH) 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 0c7637c7f..c5f20de66 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 @@ -41,6 +41,7 @@ import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IImmersivePortalsAccessor; import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IIrisAccessor; import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhMetaRenderer; import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhVanillaFadeRenderer; @@ -53,7 +54,6 @@ import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; -import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.DhClientWorld; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; @@ -65,9 +65,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; -import java.awt.*; import java.io.File; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; @@ -87,6 +87,12 @@ public class ClientApi private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); + /** Delayed accessing is necessary since this object will be created before the mod accessors are bound. */ + private static class DelayedAccessors + { + public static final IImmersivePortalsAccessor IMMERSIVE_PORTALS = ModAccessorInjector.INSTANCE.get(IImmersivePortalsAccessor.class); + } + /** this includes the is dev build message and low allocated memory warning */ private static final int MS_BETWEEN_STATIC_STARTUP_MESSAGES = 4_000; @@ -124,7 +130,7 @@ public class ClientApi public boolean rendererDisabledBecauseOfExceptions = false; - private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent); + private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(); /** Delay loading the first level to give the server some time to respond with level to actually load */ private Timer firstLevelLoadTimer; @@ -132,8 +138,8 @@ public class ClientApi /** Holds any levels that were loaded before the {@link ClientApi#onClientOnlyConnected} was fired. */ public final HashSet waitingClientLevels = new HashSet<>(); - /** Holds any chunks that were loaded before the {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} was fired. */ - public final HashMap, IChunkWrapper> waitingChunkByClientLevelAndPos = new HashMap<>(); + /** Holds any chunks that were found before the client levels are loaded. */ + public final Map, IChunkWrapper> waitingChunkByClientLevelAndPos = new ConcurrentHashMap<>(); /** publicly available so {@link F3Screen} can display the error */ @Nullable @@ -149,9 +155,10 @@ public class ClientApi * tracked should also be to keep the ratio roughly the same. * @see ClientApi#MIN_MS_BETWEEN_SPEED_CHECKS */ - public RollingAverage cameraSpeedRollingAverage = new RollingAverage(40); + private final RollingAverage cameraSpeedRollingAverage = new RollingAverage(40); private Vec3d lastCameraPosForSpeedCheck = new Vec3d(); private long msSinceLastSpeedCheck = 0L; + public double getAvgCameraSpeed() { return cameraSpeedRollingAverage.getAverage(); } /** * keeping track of this is necessary to fix @@ -179,7 +186,7 @@ public class ClientApi /** * May be fired slightly before or after the associated - * {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} event + * level is loaded * depending on how the host mod loader functions.

* * Synchronized shouldn't be necessary, but is present to match {@see onClientOnlyDisconnected} and prevent any unforeseen issues. @@ -216,14 +223,6 @@ public class ClientApi this.pluginChannelApi.onJoinServer(world.networkState.getSession()); world.networkState.sendConfigMessage(); - - LOGGER.info("Loading [" + this.waitingClientLevels.size() + "] waiting client level wrappers."); - for (IClientLevelWrapper level : this.waitingClientLevels) - { - this.clientLevelLoadEvent(level); - } - - this.waitingClientLevels.clear(); } } @@ -250,7 +249,6 @@ public class ClientApi // remove any waiting items this.waitingChunkByClientLevelAndPos.clear(); - this.waitingClientLevels.clear(); } //endregion @@ -262,44 +260,12 @@ public class ClientApi //==============// //region level events - public void clientLevelUnloadEvent(IClientLevelWrapper level) + /** + * used in conjunction with the server networking to + * handle level load requests. + */ + public boolean canLoadClientLevel(IClientLevelWrapper wrapper) { - try - { - LOGGER.info("Unloading client level [" + level.getClass().getSimpleName() + "]-[" + level.getDhIdentifier() + "]."); - - if (level instanceof IServerKeyedClientLevel) - { - this.pluginChannelApi.onClientLevelUnload(); - } - - AbstractDhWorld world = SharedApi.getAbstractDhWorld(); - if (world != null) - { - world.unloadLevel(level); - ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level)); - } - else - { - this.waitingClientLevels.remove(level); - } - } - catch (Exception e) - { - // handle errors here to prevent blowing up a mixin or API up stream - LOGGER.error("Unexpected error in ClientApi.clientLevelUnloadEvent(), error: "+e.getMessage(), e); - } - } - - public void clientLevelLoadEvent(@Nullable IClientLevelWrapper levelWrapper) - { - // can happen if there was an issue during level load - if (levelWrapper == null) - { - return; - } - - // wait a moment before loading the level to give the server a chance to handle the client's login request if (MC_CLIENT.clientConnectedToDedicatedServer()) { @@ -309,48 +275,41 @@ public class ClientApi this.firstLevelLoadTimer.schedule(new TimerTask() { @Override - public void run() { ClientApi.this.clientLevelLoadEvent(levelWrapper); } + public void run() { canLoadClientLevel(wrapper); } }, FIRST_LEVEL_LOAD_DELAY_IN_MS); - return; + return false; } + this.firstLevelLoadTimer.cancel(); } - - try + if (!this.pluginChannelApi.allowLevelLoading(wrapper)) { - LOGGER.info("Loading client level [" + levelWrapper + "]-[" + levelWrapper.getDhIdentifier() + "]."); - + LOGGER.debug("Client levels in this connection are managed by the server, skipping auto-load of: ["+wrapper+"]"); AbstractDhWorld world = SharedApi.getAbstractDhWorld(); - if (world != null) + if (world == null) { - if (!this.pluginChannelApi.allowLevelLoading(levelWrapper)) - { - LOGGER.info("Levels in this connection are managed by the server, skipping auto-load."); - - // Instead of attempting to load themselves, send the config and wait for a server provided level key. - ((DhClientWorld) world).networkState.sendConfigMessage(); - return; - } - - - world.getOrLoadLevel(levelWrapper); - ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(levelWrapper)); - - this.loadWaitingChunksForLevel(levelWrapper); - } - else - { - this.waitingClientLevels.add(levelWrapper); + return false; } + + // Instead of attempting to load themselves, send the config and wait for a server provided level key. + ((DhClientWorld) world).networkState.sendLevelInitRequest(wrapper.getDimensionName()); + return false; } - catch (Exception e) - { - // handle errors here to prevent blowing up a mixin or API up stream - LOGGER.error("Unexpected error in ClientApi.clientLevelLoadEvent(), error: "+e.getMessage(), e); - } + + return true; } - private void loadWaitingChunksForLevel(IClientLevelWrapper level) + + //endregion + + + + //==============// + // level events // + //==============// + //region + + public void loadWaitingChunksForLevel(IClientLevelWrapper level) { HashSet> keysToRemove = new HashSet<>(); for (Pair levelChunkPair : this.waitingChunkByClientLevelAndPos.keySet()) @@ -501,7 +460,10 @@ public class ClientApi //region long nowMs = System.currentTimeMillis(); - if (this.msSinceLastSpeedCheck + MIN_MS_BETWEEN_SPEED_CHECKS < nowMs) + if (this.msSinceLastSpeedCheck + MIN_MS_BETWEEN_SPEED_CHECKS < nowMs + // don't track camera speed for dimensions the player isn't in + && (DelayedAccessors.IMMERSIVE_PORTALS == null + || !DelayedAccessors.IMMERSIVE_PORTALS.isRenderingPortal())) { // calc time since last check double secSinceLastCheck = (nowMs - this.msSinceLastSpeedCheck) / 1_000.0; @@ -725,8 +687,7 @@ public class ClientApi // or if LOD-only mode is enabled (fading is used to remove the MC render pass) || Config.Client.Advanced.Debugging.lodOnlyMode.get() ) - // don't fade when Iris shaders are active, otherwise the rendering can get weird - && !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering()) + && shouldRenderFade()) { RENDER_PARAMS.update(EDhApiRenderPass.OPAQUE, RENDER_STATE); fadeRenderer.render(RENDER_PARAMS); @@ -755,8 +716,7 @@ public class ClientApi // or if LOD-only mode is enabled (fading is used to remove the MC render pass) || Config.Client.Advanced.Debugging.lodOnlyMode.get() ) - // don't fade when Iris shaders are active, otherwise the rendering can get weird - && !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering(); + && shouldRenderFade(); if (renderFade) { RENDER_PARAMS.update(EDhApiRenderPass.TRANSPARENT, RENDER_STATE); @@ -765,6 +725,25 @@ public class ClientApi } } + private static boolean shouldRenderFade() + { + // don't fade when Iris shaders are active, otherwise the rendering can get weird + if (DhApiRenderProxy.INSTANCE.getDeferTransparentRendering()) + { + return false; + } + + // Don't render fade through immersive portals, this causes the fade to apply incorrectly + IImmersivePortalsAccessor immersivePortals = ModAccessorInjector.INSTANCE.get(IImmersivePortalsAccessor.class); + if (immersivePortals != null + && immersivePortals.isRenderingPortal()) + { + return false; + } + + return true; + } + //endregion diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java index f70438f61..79902676d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java @@ -10,6 +10,7 @@ import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage; import com.seibel.distanthorizons.core.network.session.NetworkSession; import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler; +import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import org.jetbrains.annotations.NotNull; @@ -30,9 +31,6 @@ public class ClientPluginChannelApi private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class); - private final Consumer levelLoadHandler; - private final Consumer levelUnloadHandler; - @Nullable public NetworkSession networkSession; @@ -42,10 +40,8 @@ public class ClientPluginChannelApi // constructor // //=============// - public ClientPluginChannelApi(Consumer levelLoadHandler, Consumer levelUnloadHandler) + public ClientPluginChannelApi() { - this.levelLoadHandler = levelLoadHandler; - this.levelUnloadHandler = levelUnloadHandler; } @@ -94,24 +90,6 @@ public class ClientPluginChannelApi { IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true); IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(); - - if (existingKeyedClientLevel != null) - { - if (!existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey)) - { - LOGGER.info("Unloading previous level with key: [" + existingKeyedClientLevel.getServerLevelKey() + "]."); - this.levelUnloadHandler.accept(existingKeyedClientLevel); - } - else - { - LOGGER.info("Level key matches the previous level key, ignoring the message."); - } - } - else - { - LOGGER.info("Unloading non-keyed level: [" + clientLevel.getDhIdentifier() + "]."); - this.levelUnloadHandler.accept(clientLevel); - } if (existingKeyedClientLevel == null || !existingKeyedClientLevel.getServerKey().equals(msg.serverKey) @@ -119,7 +97,11 @@ public class ClientPluginChannelApi { LOGGER.info("Loading level with key: [" + msg.levelKey + "]."); IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.serverKey, msg.levelKey); - this.levelLoadHandler.accept(keyedLevel); + AbstractDhWorld world = SharedApi.getAbstractDhWorld(); + if (world != null) + { + world.getOrLoadLevel(keyedLevel); + } } }); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java index bbfa2536e..b06f678fe 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java @@ -19,13 +19,10 @@ package com.seibel.distanthorizons.core.api.internal; -import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent; -import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; import com.seibel.distanthorizons.core.network.messages.MessageRegistry; import com.seibel.distanthorizons.core.world.*; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; -import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; @@ -77,7 +74,6 @@ public class ServerApi } - //==============// // level events // //==============// @@ -90,7 +86,6 @@ public class ServerApi if (serverWorld != null) { serverWorld.getOrLoadLevel(levelWrapper); - ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(levelWrapper)); } } public void serverLevelUnloadEvent(IServerLevelWrapper level) @@ -101,12 +96,10 @@ public class ServerApi if (serverWorld != null) { serverWorld.unloadLevel(level); - ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level)); } } - //=======================// // chunk modified events // //=======================// diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java index 0bc9d726e..f814ab939 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java @@ -26,6 +26,7 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListCheckout; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.coreapi.util.ColorUtil; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil; @@ -62,6 +63,12 @@ public class ColumnBox // variable setup // //================// + IClientLevelWrapper clientLevelWrapper = clientLevel.getClientLevelWrapper(); + if (clientLevelWrapper == null) + { + LodUtil.assertNotReach("addBoxQuadsToBuilder getClientLevelWrapper should always succeed"); + } + short maxX = (short) (minX + blockWidth); short maxY = (short) (minY + yHeight); short maxZ = (short) (minZ + blockWidth); @@ -105,7 +112,7 @@ public class ColumnBox && !isTopTransparent; if (!skipTop) { - builder.addQuadUp(minX, maxY, minZ, blockWidth, ColorUtil.applyShade(color, MC_RENDER.getShade(EDhDirection.UP)), irisBlockMaterialId, skyLightTop, blockLight); + builder.addQuadUp(minX, maxY, minZ, blockWidth, ColorUtil.applyShade(color, clientLevelWrapper.getShade(EDhDirection.UP)), irisBlockMaterialId, skyLightTop, blockLight); } } @@ -116,7 +123,7 @@ public class ColumnBox && !isBottomTransparent; if (!skipBottom) { - builder.addQuadDown(minX, minY, minZ, blockWidth, ColorUtil.applyShade(color, MC_RENDER.getShade(EDhDirection.DOWN)), irisBlockMaterialId, skyLightBot, blockLight); + builder.addQuadDown(minX, minY, minZ, blockWidth, ColorUtil.applyShade(color, clientLevelWrapper.getShade(EDhDirection.DOWN)), irisBlockMaterialId, skyLightBot, blockLight); } } @@ -146,7 +153,7 @@ public class ColumnBox else { makeAdjVerticalQuad( - builder, phantomArrayCheckout, + builder, phantomArrayCheckout, clientLevelWrapper, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.NORTH, minX, minY, minZ, blockWidth, yHeight, color, irisBlockMaterialId, blockLight); @@ -171,7 +178,7 @@ public class ColumnBox else { makeAdjVerticalQuad( - builder, phantomArrayCheckout, + builder, phantomArrayCheckout, clientLevelWrapper, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.SOUTH, minX, minY, maxZ, blockWidth, yHeight, color, irisBlockMaterialId, blockLight); @@ -196,7 +203,7 @@ public class ColumnBox else { makeAdjVerticalQuad( - builder, phantomArrayCheckout, + builder, phantomArrayCheckout, clientLevelWrapper, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.WEST, minX, minY, minZ, blockWidth, yHeight, color, irisBlockMaterialId, blockLight); @@ -221,7 +228,7 @@ public class ColumnBox else { makeAdjVerticalQuad( - builder, phantomArrayCheckout, + builder, phantomArrayCheckout, clientLevelWrapper, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.EAST, maxX, minY, minZ, blockWidth, yHeight, color, irisBlockMaterialId, blockLight); @@ -230,7 +237,7 @@ public class ColumnBox } private static void makeAdjVerticalQuad( - LodQuadBuilder builder, PhantomArrayListCheckout phantomArrayCheckout, + LodQuadBuilder builder, PhantomArrayListCheckout phantomArrayCheckout, IClientLevelWrapper clientLevelWrapper, @NotNull ColumnRenderView adjColumnView, boolean adjacentIsSameDetailLevel, int caveCullingMaxY, EDhDirection direction, short x, short yMin, short z, short horizontalBlockWidth, short ySize, int color, byte irisBlockMaterialId, byte blockLight) @@ -246,7 +253,7 @@ public class ColumnBox // no adjacent data // //==================// - color = ColorUtil.applyShade(color, MC_RENDER.getShade(direction)); + color = ColorUtil.applyShade(color, clientLevelWrapper.getShade(direction)); if (adjColumnView.size == 0 || RenderDataPointUtil.hasZeroHeight(adjColumnView.get(0))) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java index ec56619cb..35322ae1f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java @@ -420,7 +420,7 @@ public class LodQuadBuilder implements AutoCloseable // for horizontal and bottom faces of grass blocks, use the dirt color to // prevent green cliff walls color = this.clientLevelWrapper.getDirtBlockColor(); - color = ColorUtil.applyShade(color, MC_RENDER.getShade(quad.direction)); + color = ColorUtil.applyShade(color, this.clientLevelWrapper.getShade(quad.direction)); } } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/queues/LodRequestModule.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/queues/LodRequestModule.java index c564137e2..c054efde2 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/queues/LodRequestModule.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/queues/LodRequestModule.java @@ -123,17 +123,6 @@ public class LodRequestModule implements Closeable // if the world is read only don't generate anything shouldDoWorldGen &= !DhApiWorldProxy.INSTANCE.tryGetReadOnly(); - // don't generate chunks for client levels that aren't being rendered - // (this can happen when moving between dimensions) - if (this.level instanceof IDhClientLevel) - { - boolean isRendering = ((IDhClientLevel) this.level).isRendering(); - if (!isRendering) - { - shouldDoWorldGen = false; - } - } - boolean isWorldGenRunning = this.isWorldGenRunning(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java index 2077ee056..d0ba42ed1 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java @@ -9,7 +9,6 @@ import com.seibel.distanthorizons.core.multiplayer.server.FullDataSourceRequestH import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState; import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerStateManager; import com.seibel.distanthorizons.core.network.exceptions.RequestOutOfRangeException; -import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException; import com.seibel.distanthorizons.core.network.exceptions.SectionRequiresSplittingException; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage; @@ -200,26 +199,6 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I LodUtil.assertTrue(message.getSession().serverPlayer != null); - // Check if the player is in this dimension, - // since handling multiple dimensions isn't allowed - if (message.getSession().serverPlayer.getLevel() != this.getLevelWrapper()) - { - // If the message can be replied to - reply with an error, otherwise just ignore - if (message instanceof AbstractTrackableMessage) - { - ((AbstractTrackableMessage) message).sendResponse( - new RequestRejectedException( - "Generation not allowed. " + - "Requested dimension: ["+((ILevelRelatedMessage) message).getLevelName()+"], " + - "player dimension: [" + message.getSession().serverPlayer.getLevel().getDhIdentifier() + "], " + - "handler dimension: [" + this.getLevelWrapper().getDhIdentifier() + "]" - ) - ); - } - - return false; - } - return true; } 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 600cfb91c..1826b8edf 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 @@ -126,7 +126,7 @@ public class ClientLevelModule implements Closeable, IDataSourceUpdateListenerFu ClientRenderState clientRenderState = new ClientRenderState(this.clientLevel, this.clientLevel.getFullDataProvider()); if (!this.ClientRenderStateRef.compareAndSet(null, clientRenderState)) { - LOGGER.warn("Renderer already started for ["+this+"]."); + LOGGER.warn("Renderer already started for ["+this.clientLevel.getClientLevelWrapper()+"]."); clientRenderState.close(); } } 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 aec84ceb1..f375c27f3 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 @@ -168,15 +168,17 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel } + // Check this before decoding data to prevent errors if multiple client levels + // are receiving data at once (Immersive Portals compatibility). + boolean isSameLevel = message.isSameLevelAs(this.levelWrapper); + //NETWORK_LOGGER.debug("Buffer ["+message.payload.dtoBufferId+"] isSameLevel: ["+isSameLevel+"]"); + if (!isSameLevel) + { + return; + } + try (FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSource(message.payload)) { - boolean isSameLevel = message.isSameLevelAs(this.levelWrapper); - NETWORK_LOGGER.debug("Buffer ["+message.payload.dtoBufferId+"] isSameLevel: ["+isSameLevel+"]"); - if (!isSameLevel) - { - return; - } - Executor executor = ThreadPoolUtil.getFileHandlerExecutor(); if (executor != null) @@ -220,6 +222,15 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel { try { + // only tick the level the player is currently in + // (done to prevent ticking LodQuadTree's for levels that aren't rendering) + IClientLevelWrapper clientLevelWrapper = MC_CLIENT.getWrappedClientLevel(); + if (clientLevelWrapper == null + || clientLevelWrapper.getDhLevel() != this) + { + return; + } + this.clientside.clientTick(); if (this.syncOnLoadRequestQueue != null) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java index bbca7f6b4..bb33e8a82 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java @@ -72,7 +72,19 @@ public class DhClientServerLevel extends AbstractDhServerLevel implements IDhCli //region @Override - public void clientTick() { this.clientside.clientTick(); } + public void clientTick() + { + // only tick the level the player is currently in + // (done to prevent ticking LodQuadTree's for levels that aren't rendering) + IClientLevelWrapper clientLevelWrapper = MC_CLIENT.getWrappedClientLevel(); + if (clientLevelWrapper == null + || clientLevelWrapper.getDhLevel() != this) + { + return; + } + + this.clientside.clientTick(); + } //endregion diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/ClientNetworkState.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/ClientNetworkState.java index 0a922fa9e..60fee8c85 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/ClientNetworkState.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/ClientNetworkState.java @@ -13,6 +13,7 @@ import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource; import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent; import com.seibel.distanthorizons.core.network.event.internal.IncompatibleMessageInternalEvent; import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage; +import com.seibel.distanthorizons.core.network.messages.base.RequestLevelInitMessage; import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage; @@ -164,7 +165,8 @@ public class ClientNetworkState implements Closeable // send message // //==============// - + public void sendLevelInitRequest(String clientLevelKey) + { this.getSession().sendMessage(new RequestLevelInitMessage(clientLevelKey)); } public void sendConfigMessage() { this.sendConfigMessage(true); } public void sendConfigMessage(boolean blocking) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java index b5b86050a..979c8969a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java @@ -2,6 +2,7 @@ package com.seibel.distanthorizons.core.multiplayer.server; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; +import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.level.AbstractDhServerLevel; import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig; import com.seibel.distanthorizons.core.multiplayer.fullData.FullDataPayloadSender; @@ -9,20 +10,27 @@ import com.seibel.distanthorizons.core.multiplayer.fullData.SharedBandwidthLimit import com.seibel.distanthorizons.core.network.event.internal.IncompatibleMessageInternalEvent; import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage; import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage; +import com.seibel.distanthorizons.core.network.messages.base.RequestLevelInitMessage; import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage; import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent; import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage; import com.seibel.distanthorizons.core.network.session.NetworkSession; +import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateAndConcurrencyLimiter; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import org.jetbrains.annotations.NotNull; import java.io.Closeable; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; public class ServerPlayerState implements Closeable { + private final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class); + private final ConfigChangeListener levelKeyPrefixChangeListener = new ConfigChangeListener<>(Config.Server.levelKeyPrefix, this::onLevelKeyPrefixConfigChanged); private final SessionConfig.AnyChangeListener configAnyChangeListener = new SessionConfig.AnyChangeListener(this::sendConfigMessage); @@ -66,6 +74,12 @@ public class ServerPlayerState implements Closeable this.sendConfigMessage(); }); + this.networkSession.registerHandler(RequestLevelInitMessage.class, (requestLevelInitMessage) -> + { + sendLevelKey(requestLevelInitMessage.dimensionResourceLocation); + }); + + this.networkSession.registerHandler(CloseInternalEvent.class, event -> { // No-op. prevents "Unhandled message" log entries }); @@ -85,12 +99,33 @@ public class ServerPlayerState implements Closeable //=================// private void onLevelKeyPrefixConfigChanged(String newLevelKey) { this.sendLevelKey(); } + + private void sendLevelKey(String dimensionResourceLocation) + { + sendLevelKey(() -> + { + IServerLevelWrapper serverLevelWrapper = MC_SHARED.getWrappedServerLevelWithDimensionResourceLocation(dimensionResourceLocation); + if (serverLevelWrapper == null) + { + LodUtil.assertNotReach("Unable to get server level from"); + } + + return serverLevelWrapper.getKeyedLevelDimensionName(); + }); + } private void sendLevelKey() + { + sendLevelKey(() -> + this.getServerPlayer() + .getLevel() + .getKeyedLevelDimensionName()); + } + private void sendLevelKey(Supplier levelKeySupplier) { if (Config.Server.sendLevelKeys.get()) { + String levelKey = levelKeySupplier.get(); // let the client's know about the change - String levelKey = this.getServerPlayer().getLevel().getKeyedLevelDimensionName(); if (!levelKey.equals(this.lastLevelKey)) { this.lastLevelKey = levelKey; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/MessageRegistry.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/MessageRegistry.java index 8e3748e8e..3eb230ed1 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/MessageRegistry.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/MessageRegistry.java @@ -21,12 +21,9 @@ package com.seibel.distanthorizons.core.network.messages; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; -import com.seibel.distanthorizons.core.network.messages.base.CodecCrashMessage; -import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage; -import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage; +import com.seibel.distanthorizons.core.network.messages.base.*; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage; import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage; -import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage; import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage; @@ -60,6 +57,7 @@ public class MessageRegistry // Level keys this.registerMessage(LevelInitMessage.class, LevelInitMessage::new); + this.registerMessage(RequestLevelInitMessage.class, RequestLevelInitMessage::new); // Config (for full DH support) this.registerMessage(SessionConfigMessage.class, SessionConfigMessage::new); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/RequestLevelInitMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/RequestLevelInitMessage.java new file mode 100644 index 000000000..e1cd277b1 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/RequestLevelInitMessage.java @@ -0,0 +1,65 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages.base; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import io.netty.buffer.ByteBuf; + +/** used for full DH support */ +public class RequestLevelInitMessage extends AbstractNetworkMessage +{ + public String dimensionResourceLocation; + + + + //=============// + // constructor // + //=============// + + public RequestLevelInitMessage() { } + public RequestLevelInitMessage(String dimensionResourceLocation) { this.dimensionResourceLocation = dimensionResourceLocation; } + + + + //===============// + // serialization // + //===============// + + @Override + public void encode(ByteBuf out) { this.writeString(this.dimensionResourceLocation, out); } + + @Override + public void decode(ByteBuf in) { this.dimensionResourceLocation = this.readString(in); } + + + + //================// + // base overrides // + //================// + + @Override + public MoreObjects.ToStringHelper toStringHelper() + { + return super.toStringHelper() + .add("dimensionResourceLocation", this.dimensionResourceLocation); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderParams.java b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderParams.java index e7949dc5f..55706608f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderParams.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderParams.java @@ -79,7 +79,7 @@ public class RenderParams extends DhApiRenderParam this.dhClientWorld = SharedApi.tryGetDhClientWorld(); if (this.dhClientWorld != null) { - this.dhClientLevel = (IDhClientLevel) this.dhClientWorld.getLevel(this.clientLevelWrapper); + this.dhClientLevel = this.dhClientWorld.getOrLoadClientLevel(clientLevelWrapper); if (this.dhClientLevel != null) { this.renderBufferHandler = this.dhClientLevel.getRenderBufferHandler(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java index 0cbbc7d6c..985afa7c4 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java @@ -179,7 +179,7 @@ public class RenderUtil if (Config.Client.Advanced.Graphics.Culling.reduceOverdrawWithFastMovement.get()) { - double avgSpeed = ClientApi.INSTANCE.cameraSpeedRollingAverage.getAverage(); + double avgSpeed = ClientApi.INSTANCE.getAvgCameraSpeed(); if (avgSpeed >= DynamicOverdraw.MIN_SPEED) { // if the player is moving fast enough, diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPoolUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPoolUtil.java index 0526e0a1a..357970ed3 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPoolUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPoolUtil.java @@ -170,7 +170,7 @@ public class ThreadPoolUtil */ public static boolean worldGenThreadsCanRun() { - double cameraSpeed = ClientApi.INSTANCE.cameraSpeedRollingAverage.getAverage(); + double cameraSpeed = ClientApi.INSTANCE.getAvgCameraSpeed(); // stop these threads if moving a little bit slower than max elytra speed double maxAllowedSpeed = (LodUtil.ROCKET_ELYTRA_SPEED_IN_BLOCKS_PER_SEC - 10.0); if (cameraSpeed > maxAllowedSpeed) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhServerWorld.java index adeb7dbcd..a774eacb9 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhServerWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhServerWorld.java @@ -1,5 +1,6 @@ package com.seibel.distanthorizons.core.world; +import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent; import com.seibel.distanthorizons.core.file.structure.LocalSaveStructure; import com.seibel.distanthorizons.core.level.AbstractDhServerLevel; import com.seibel.distanthorizons.core.level.IDhLevel; @@ -8,6 +9,7 @@ import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerStateManag import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; +import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -138,6 +140,7 @@ public abstract class AbstractDhServerWorld implements IDhClientWorld { - private final Set dhLevels = Collections.synchronizedSet(new HashSet<>()); + /** + * Having a set of level wrappers is done to handle an issue where the client + * level would get unloaded when jumping back and forth between dimensions.

+ * + * We might have more than one {@link ILevelWrapper} pointing to the same {@link IDhLevel} + * since they're not immediately unloaded, and we don't want to unload the {@link IDhLevel} + * until all the {@link ILevelWrapper} for that {@link IDhLevel} have been unloaded. + * Any stale {@link IDhLevel} references should disappear on their own after about + * 30 seconds or so thanks to the automatic cleanup. + */ + private final Map> clientLevelWrapperSetByDhLevel + = Collections.synchronizedMap(new HashMap<>()); private final Timer clientTickTimer = TimerUtil.CreateTimer("ClientTickTimer"); @@ -47,14 +62,14 @@ public class DhClientServerWorld extends AbstractDhServerWorld Collections.synchronizedSet(new HashSet<>())); + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(wrapper)); return level; } catch (Exception e) @@ -92,11 +108,22 @@ public class DhClientServerWorld extends AbstractDhServerWorld { + if (!(levelWrapper instanceof IClientLevelWrapper)) + { + LodUtil.assertNotReach("tryGetServerSideWrapper given a non-IClientLevelWrapper."); + } + IClientLevelWrapper clientLevelWrapper = (IClientLevelWrapper) levelWrapper; IServerLevelWrapper serverLevelWrapper = clientLevelWrapper.tryGetServerSideWrapper(); LodUtil.assertTrue(serverLevelWrapper != null); + if (!clientLevelWrapper.getDimensionType().equals(serverLevelWrapper.getDimensionType())) { LodUtil.assertNotReach("tryGetServerSideWrapper returned a level for a different dimension. ClientLevelWrapper dim: [" + clientLevelWrapper.getDhIdentifier() + "] ServerLevelWrapper dim: [" + serverLevelWrapper.getDhIdentifier() + "]."); @@ -111,13 +138,14 @@ public class DhClientServerWorld extends AbstractDhServerWorld wrappers = clientLevelWrapperSetByDhLevel.get(level); + if (wrappers != null) + { + wrappers.remove(wrapper); + } + + if ((wrappers == null || wrappers.isEmpty()) + && level.isRendering()) + { + level.stopRenderer(); + } + wrapper.onUnload(); // We still want to unload the wrapper though. } + + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(wrapper)); + return true; } + + return false; } @@ -152,16 +197,24 @@ public class DhClientServerWorld extends AbstractDhServerWorld> closeFutures = new ArrayList<>(); - synchronized (this.dhLevels) + synchronized (this.clientLevelWrapperSetByDhLevel) { // close each level - for (DhClientServerLevel level : this.dhLevels) + for (DhClientServerLevel level : this.clientLevelWrapperSetByDhLevel.keySet()) { // level wrapper shouldn't be null, but just in case IServerLevelWrapper serverLevelWrapper = level.getServerLevelWrapper(); if (serverLevelWrapper != null) { serverLevelWrapper.onUnload(); + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(serverLevelWrapper)); + } + + IClientLevelWrapper clientLevelWrapper = level.getClientLevelWrapper(); + if (clientLevelWrapper != null) + { + clientLevelWrapper.onUnload(); + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(clientLevelWrapper)); } // close levels asynchronously to speed up 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 70cd16942..e3f45c8a2 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 @@ -19,6 +19,8 @@ package com.seibel.distanthorizons.core.world; +import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent; +import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent; import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.enums.MinecraftTextFormat; import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; @@ -28,21 +30,32 @@ import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState; import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld { - private final ConcurrentHashMap levels; public final ClientOnlySaveStructure saveStructure; public final ClientNetworkState networkState = new ClientNetworkState(); + + private final ConcurrentHashMap clientLevelByDhId; + /** + * Having a set of level wrappers is done to handle an issue where the client + * level would get unloaded when jumping back and forth between dimensions.

+ * + * We might have more than one {@link ILevelWrapper} pointing to the same {@link IDhLevel} + * since they're not immediately unloaded, and we don't want to unload the {@link IDhLevel} + * until all the {@link ILevelWrapper} for that {@link IDhLevel} have been unloaded. + * Any stale {@link IDhLevel} references should disappear on their own after about + * 30 seconds or so thanks to the automatic cleanup. + */ + private final Map> clientLevelWrapperSetByDhId = new ConcurrentHashMap<>(); + private final Timer clientTickTimer = TimerUtil.CreateTimer("ClientTickTimer"); @@ -56,7 +69,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld super(EWorldEnvironment.CLIENT_ONLY); this.saveStructure = new ClientOnlySaveStructure(); - this.levels = new ConcurrentHashMap<>(); + this.clientLevelByDhId = new ConcurrentHashMap<>(); LOGGER.info("Started DhWorld of type " + this.environment); @@ -65,7 +78,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld @Override public void run() { - DhClientWorld.this.levels.values().forEach(DhClientLevel::clientTick); + DhClientWorld.this.clientLevelByDhId.values().forEach(DhClientLevel::clientTick); } }, 0, IDhClientWorld.TICK_RATE_IN_MS); } @@ -84,24 +97,51 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld return null; } - return this.levels.computeIfAbsent((IClientLevelWrapper) wrapper, - (clientLevelWrapper) -> + IClientLevelWrapper clientLevelWrapper = (IClientLevelWrapper) wrapper; + clientLevelWrapper.markAccessed(); + DhClientLevel storedLevel = this.clientLevelByDhId.computeIfAbsent(wrapper.getDhIdentifier(), + (key) -> createClientLevel(clientLevelWrapper) + ); + + if (storedLevel != null + && storedLevel.getClientLevelWrapper() != wrapper) + { + unloadLevel(storedLevel.getLevelWrapper()); + storedLevel = createClientLevel(clientLevelWrapper); + if (storedLevel != null) { - try - { - return new DhClientLevel(this.saveStructure, clientLevelWrapper, this.networkState); - } - catch (Exception e) - { - LOGGER.fatal("Failed to load client level, error: ["+e.getMessage()+"].", e); - - ClientApi.INSTANCE.showChatMessageNextFrame( - MinecraftTextFormat.RED + "Distant Horizons: Client level loading failed." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" + - "Unable to load level ["+clientLevelWrapper.getDhIdentifier()+"], LODs may not appear. See log for more information."); - - return null; - } - }); + this.clientLevelByDhId.put(wrapper.getDhIdentifier(), storedLevel); + } + } + return storedLevel; + } + private DhClientLevel createClientLevel(@NotNull IClientLevelWrapper clientLevelWrapper) + { + try + { + if (!ClientApi.INSTANCE.canLoadClientLevel(clientLevelWrapper)) + { + return null; + } + + DhClientLevel level = new DhClientLevel(this.saveStructure, clientLevelWrapper, this.networkState); + clientLevelWrapperSetByDhId.computeIfAbsent(clientLevelWrapper.getDhIdentifier(), (dhId) -> Collections.synchronizedSet(new HashSet<>())).add(clientLevelWrapper); + + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(clientLevelWrapper)); + ClientApi.INSTANCE.loadWaitingChunksForLevel(clientLevelWrapper); + + return level; + } + catch (Exception e) + { + LOGGER.fatal("Failed to load client level, error: ["+e.getMessage()+"].", e); + + ClientApi.INSTANCE.showChatMessageNextFrame( + MinecraftTextFormat.RED + "Distant Horizons: Client level loading failed." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" + + "Unable to load level ["+clientLevelWrapper.getDhIdentifier()+"], LODs may not appear. See log for more information."); + + return null; + } } @Override @@ -112,28 +152,39 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld return null; } - return this.levels.get(wrapper); + return this.clientLevelByDhId.get(wrapper.getDhIdentifier()); } @Override - public Iterable getAllLoadedLevels() { return this.levels.values(); } + public Iterable getAllLoadedLevels() { return this.clientLevelByDhId.values(); } @Override - public int getLoadedLevelCount() { return this.levels.size(); } + public int getLoadedLevelCount() { return this.clientLevelByDhId.size(); } @Override - public void unloadLevel(@NotNull ILevelWrapper wrapper) + public boolean unloadLevel(@NotNull ILevelWrapper wrapper) { if (!(wrapper instanceof IClientLevelWrapper)) { - return; + return false; } - if (this.levels.containsKey(wrapper)) + if (this.clientLevelByDhId.containsKey(wrapper.getDhIdentifier())) { - LOGGER.info("Unloading level " + this.levels.get(wrapper)); + LOGGER.info("Unloading level [" + this.clientLevelByDhId.get(wrapper.getDhIdentifier()) + "]."); wrapper.onUnload(); - this.levels.remove(wrapper).close(); + Set wrapperSet = this.clientLevelWrapperSetByDhId.get(wrapper.getDhIdentifier()); + wrapperSet.remove(wrapper); + if (wrapperSet.isEmpty()) + { + this.clientLevelByDhId.remove(wrapper.getDhIdentifier()).close(); + } + + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(wrapper)); + + return true; } + + return false; } @Override @@ -149,13 +200,14 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld this.networkState.close(); ArrayList> closeFutures = new ArrayList<>(); - for (DhClientLevel dhClientLevel : this.levels.values()) + for (DhClientLevel dhClientLevel : this.clientLevelByDhId.values()) { // level wrapper shouldn't be null, but just in case IClientLevelWrapper clientLevelWrapper = dhClientLevel.getClientLevelWrapper(); if (clientLevelWrapper != null) { clientLevelWrapper.onUnload(); + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(clientLevelWrapper)); } @@ -177,7 +229,8 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld future.join(); } - this.levels.clear(); + this.clientLevelByDhId.clear(); + this.clientLevelWrapperSetByDhId.clear(); this.clientTickTimer.cancel(); LOGGER.info("Closed DhWorld of type [" + this.environment + "]."); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java index 06375f163..f49095e55 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java @@ -19,12 +19,15 @@ package com.seibel.distanthorizons.core.world; +import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent; +import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent; import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.enums.MinecraftTextFormat; import com.seibel.distanthorizons.core.generation.PregenManager; import com.seibel.distanthorizons.core.level.DhServerLevel; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; +import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; @@ -64,7 +67,9 @@ public class DhServerWorld extends AbstractDhServerWorld { try { - return new DhServerLevel(this.saveStructure, (IServerLevelWrapper) serverLevelWrapper, this.getServerPlayerStateManager()); + DhServerLevel level = new DhServerLevel(this.saveStructure, (IServerLevelWrapper) serverLevelWrapper, this.getServerPlayerStateManager()); + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(wrapper)); + return level; } catch (Exception e) { @@ -80,19 +85,22 @@ public class DhServerWorld extends AbstractDhServerWorld } @Override - public void unloadLevel(@NotNull ILevelWrapper wrapper) + public boolean unloadLevel(@NotNull ILevelWrapper wrapper) { if (!(wrapper instanceof IServerLevelWrapper)) { - return; + return false; } if (this.dhLevelByLevelWrapper.containsKey(wrapper)) { - DhServerLevel level = this.dhLevelByLevelWrapper.get(wrapper); wrapper.onUnload(); this.dhLevelByLevelWrapper.remove(wrapper).close(); + ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(wrapper)); + return true; } + + return false; } @Override diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java index f99db3c2b..1bb412a12 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java @@ -41,7 +41,6 @@ import java.util.concurrent.CompletableFuture; */ public interface IDhWorld extends Closeable { - @Nullable IDhLevel getOrLoadLevel(@NotNull ILevelWrapper levelWrapper); @Nullable @@ -49,6 +48,14 @@ public interface IDhWorld extends Closeable Iterable getAllLoadedLevels(); int getLoadedLevelCount(); - void unloadLevel(@NotNull ILevelWrapper levelWrapper); + /** + * Returns + * true if the level was unloaded, + * false if the level isn't present in this world + * or couldn't be unloaded for some other reason + */ + boolean unloadLevel(@NotNull ILevelWrapper levelWrapper); + + } 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 d964c828b..4cf20be73 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 @@ -24,6 +24,7 @@ import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable; +import org.jetbrains.annotations.Nullable; public interface IMinecraftClientWrapper extends IBindable { @@ -64,11 +65,13 @@ public interface IMinecraftClientWrapper extends IBindable * Returns the level the client is currently in.
* Returns null if the client isn't in a level. */ + @Nullable IClientLevelWrapper getWrappedClientLevel(); /** * Returns the level the client is currently in.
* Returns null if the client isn't in a level. */ + @Nullable IClientLevelWrapper getWrappedClientLevel(boolean bypassLevelKeyManager); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java index 83ffa102e..82f0d93da 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java @@ -99,8 +99,6 @@ public interface IMinecraftRenderWrapper extends IBindable @Nullable ILightMapWrapper getLightmapWrapper(@NotNull ILevelWrapper level); - float getShade(EDhDirection lodDirection); - } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftSharedWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftSharedWrapper.java index a61c3af55..5c7437c9a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftSharedWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftSharedWrapper.java @@ -19,7 +19,9 @@ package com.seibel.distanthorizons.core.wrapperInterfaces.minecraft; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable; +import org.jetbrains.annotations.Nullable; import java.io.File; @@ -31,6 +33,9 @@ public interface IMinecraftSharedWrapper extends IBindable int getPlayerCount(); + /** If used on the client will only return a non-null object if the client is hosting a LAN server */ + @Nullable + IServerLevelWrapper getWrappedServerLevelWithDimensionResourceLocation(String dimensionResourceLocation); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/modAccessor/AbstractImmersivePortalsAccessor.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/modAccessor/AbstractImmersivePortalsAccessor.java new file mode 100644 index 000000000..04bddd9bf --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/modAccessor/AbstractImmersivePortalsAccessor.java @@ -0,0 +1,157 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor; + +import com.seibel.distanthorizons.api.DhApi; +import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiBeforeRenderEvent; +import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiCancelableEventParam; +import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; +import com.seibel.distanthorizons.core.logging.DhLogger; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import org.jetbrains.annotations.NotNull; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.function.Supplier; + +public abstract class AbstractImmersivePortalsAccessor implements IImmersivePortalsAccessor +{ + private static final DhLogger LOGGER = new DhLoggerBuilder().build(); + + private static MethodHandle isRenderingMethodHandle; + + + + //=============// + // constructor // + //=============// + //region + + public AbstractImmersivePortalsAccessor() + { + LOGGER.info("Immersive Portals detected: some DH features will be disabled or may only partially function."); + + BeforeRenderEvent event = new BeforeRenderEvent(this); + DhApi.events.bind(DhApiBeforeRenderEvent.class, event); + } + + //endregion + + + + //=====================// + // reflection handling // + //=====================// + //region + + private static Class getPortalRenderingClass() + { + try + { + return Class.forName("qouteall.imm_ptl.core.render.context_management.PortalRendering"); + } + catch (ClassNotFoundException first) + { + try + { + return Class.forName("com.qouteall.immersive_portals.render.context_management.PortalRendering"); // 1.16 + } + catch (ClassNotFoundException second) + { + RuntimeException err = new RuntimeException(first); + err.addSuppressed(second); + throw err; + } + } + } + + //endregion + + + + //===========// + // overrides // + //===========// + //region + + @Override + public String getModName() { return "Immersive Portals"; } + + @Override + public boolean isRenderingPortal() + { + try + { + if (isRenderingMethodHandle == null) + { + isRenderingMethodHandle = MethodHandles.lookup().findStatic( + getPortalRenderingClass(), + "isRendering", MethodType.methodType(Boolean.TYPE) + ); + } + + return (boolean) isRenderingMethodHandle.invoke(); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + //endregion + + + + //=======// + // event // + //=======// + //region + + private static class BeforeRenderEvent extends DhApiBeforeRenderEvent + { + @NotNull + private final IImmersivePortalsAccessor immersivePortals; + + + public BeforeRenderEvent(@NotNull IImmersivePortalsAccessor portalAccessor) { this.immersivePortals = portalAccessor; } + + + @Override + public void beforeRender(DhApiCancelableEventParam event) + { + // needed because otherwise DH doesn't render to the level anyway + // and will probably render the level the player is currently in instead + if (this.immersivePortals.isRenderingPortal()) + { + event.cancelEvent(); + } + } + + } + + //endregion + + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/modAccessor/IImmersivePortalsAccessor.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/modAccessor/IImmersivePortalsAccessor.java new file mode 100644 index 000000000..9e79ce687 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/modAccessor/IImmersivePortalsAccessor.java @@ -0,0 +1,77 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor; + +import com.seibel.distanthorizons.core.pos.DhChunkPos; +import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; +import com.seibel.distanthorizons.core.util.math.Vec3d; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; +import org.jetbrains.annotations.Nullable; + +public interface IImmersivePortalsAccessor extends IModAccessor +{ + /** + * Returns true if Immersive Portals is currently rendering a portal. + * This can be used to determine if the level currently being rendered + * is being seen through a portal if called on the render thread. + */ + boolean isRenderingPortal(); + + + + /** + * Returns the player's position for the level they're currently in. + *

+ * Necessary since Immersive Portals messes with vanilla MC's + * variables in order to render the camera in multiple dimensions. + */ + @Nullable + DhBlockPos getActualPlayerBlockPos(); + + /** + * Returns the player's position for the level they're currently in. + *

+ * Necessary since Immersive Portals messes with vanilla MC's + * variables in order to render the camera in multiple dimensions. + */ + @Nullable + DhChunkPos getActualPlayerChunkPos(); + + /** + * Returns the client level the player is currently in. + *

+ * Necessary since Immersive Portals messes with vanilla MC's + * variables in order to render the camera in multiple dimensions. + */ + @Nullable + IClientLevelWrapper getActualClientLevelWrapper(); + + /** + * Returns the camera position for the level the player is currently in. + *

+ * Necessary since Immersive Portals messes with vanilla MC's + * variables in order to render the camera in multiple dimensions. + */ + @Nullable + Vec3d getActualCameraPos(); + + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IClientLevelWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IClientLevelWrapper.java index 3e69a5aa3..cc8b2e379 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IClientLevelWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IClientLevelWrapper.java @@ -20,6 +20,7 @@ package com.seibel.distanthorizons.core.wrapperInterfaces.world; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import org.jetbrains.annotations.Nullable; @@ -29,6 +30,9 @@ import java.awt.*; public interface IClientLevelWrapper extends ILevelWrapper { + /** used to track when this level was last used for Immersive portals support */ + void markAccessed(); + @Nullable IServerLevelWrapper tryGetServerSideWrapper(); @@ -41,4 +45,6 @@ public interface IClientLevelWrapper extends ILevelWrapper Color getCloudColor(float tickDelta); + float getShade(EDhDirection lodDirection); + }