diff --git a/common/src/main/java/com/seibel/distanthorizons/common/ImmersivePortalsCompat.java b/common/src/main/java/com/seibel/distanthorizons/common/ImmersivePortalsCompat.java index b1b9b343e..62b54c132 100644 --- a/common/src/main/java/com/seibel/distanthorizons/common/ImmersivePortalsCompat.java +++ b/common/src/main/java/com/seibel/distanthorizons/common/ImmersivePortalsCompat.java @@ -86,7 +86,10 @@ public class ImmersivePortalsCompat */ public static void resetDetection() { - isImmersivePortalsPresent = null; - isImmersivePortalsActive = null; + synchronized (ImmersivePortalsCompat.class) + { + isImmersivePortalsPresent = null; + isImmersivePortalsActive = null; + } } } diff --git a/common/src/main/java/com/seibel/distanthorizons/common/wrappers/chunk/ChunkWrapper.java b/common/src/main/java/com/seibel/distanthorizons/common/wrappers/chunk/ChunkWrapper.java index 022c3fb93..bb0b00d5b 100644 --- a/common/src/main/java/com/seibel/distanthorizons/common/wrappers/chunk/ChunkWrapper.java +++ b/common/src/main/java/com/seibel/distanthorizons/common/wrappers/chunk/ChunkWrapper.java @@ -661,15 +661,21 @@ public class ChunkWrapper implements IChunkWrapper @Override public String toString() { return this.chunk.getClass().getSimpleName() + this.chunk.getPos(); } - //@Override + //@Override //public int hashCode() //{ // if (this.blockBiomeHashCode == 0) // { // this.blockBiomeHashCode = this.getBlockBiomeHashCode(); // } - // + // // return this.blockBiomeHashCode; //} - + + @Override + public IChunkWrapper copy() + { + return new ChunkWrapper(this.chunk, this.wrappedLevel, false); + } + } diff --git a/common/src/main/java/com/seibel/distanthorizons/common/wrappers/level/KeyedClientLevelManager.java b/common/src/main/java/com/seibel/distanthorizons/common/wrappers/level/KeyedClientLevelManager.java index 0f0ecc4c3..22e6714ca 100644 --- a/common/src/main/java/com/seibel/distanthorizons/common/wrappers/level/KeyedClientLevelManager.java +++ b/common/src/main/java/com/seibel/distanthorizons/common/wrappers/level/KeyedClientLevelManager.java @@ -1,20 +1,39 @@ package com.seibel.distanthorizons.common.wrappers.level; +import com.seibel.distanthorizons.common.wrappers.world.ClientLevelWrapper; import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; +import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; import org.jetbrains.annotations.Nullable; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + public class KeyedClientLevelManager implements IKeyedClientLevelManager { public static final KeyedClientLevelManager INSTANCE = new KeyedClientLevelManager(); - /** This is set and managed by the ClientApi for servers with support for DH. */ - @Nullable - private IServerKeyedClientLevel serverKeyedLevel = null; + private static class KeyInfo { + public final String serverKey; + public final String levelKey; + public KeyInfo(String serverKey, String levelKey) { + this.serverKey = serverKey; + this.levelKey = levelKey; + } + } + + /** Stores the server-provided keys indexed by dimension name for persistence. */ + private final Map keysByDimensionName = new ConcurrentHashMap<>(); + + /** Cache for already keyed level wrappers to maintain object identity. */ + private final Map keyedLevelsCache = Collections.synchronizedMap(new WeakHashMap<>()); + /** Allows to keep level manager enabled between loading different keyed levels */ - private boolean enabled = false; + private volatile boolean enabled = false; @@ -33,23 +52,110 @@ public class KeyedClientLevelManager implements IKeyedClientLevelManager @Override @Nullable - public IServerKeyedClientLevel getServerKeyedLevel() { return this.serverKeyedLevel; } + public IServerKeyedClientLevel getServerKeyedLevel() + { + return this.getServerKeyedLevel(Minecraft.getInstance().level); + } + + @Nullable + public IServerKeyedClientLevel getServerKeyedLevel(@Nullable ClientLevel level) + { + if (level == null) + { + return null; + } + + // We synchronize on the cache map to ensure atomicity of the lookup-and-populate sequence. + // This prevents multiple threads from creating duplicate wrappers for the same level. + synchronized (this.keyedLevelsCache) + { + // Check the cache first + IServerKeyedClientLevel cached = this.keyedLevelsCache.get(level); + if (cached != null) + { + return cached; + } + + // Determine the dimension name for this level + // We use bypassLevelKeyManager=true to avoid recursion back into this manager + IClientLevelWrapper wrappedLevel = ClientLevelWrapper.getWrapper(level, true); + if (wrappedLevel == null) + { + return null; + } + + String dimensionName = wrappedLevel.getDimensionName(); + KeyInfo info = this.keysByDimensionName.get(dimensionName); + if (info == null) + { + return null; + } + + // Create and cache a new keyed wrapper + IServerKeyedClientLevel keyedLevel = new ServerKeyedClientLevelWrapper(level, info.serverKey, info.levelKey); + this.keyedLevelsCache.put(level, keyedLevel); + return keyedLevel; + } + } @Override public IServerKeyedClientLevel setServerKeyedLevel(IClientLevelWrapper clientLevel, String serverKey, String levelKey) { - IServerKeyedClientLevel keyedLevel = new ServerKeyedClientLevelWrapper((ClientLevel) clientLevel.getWrappedMcObject(), serverKey, levelKey); - this.serverKeyedLevel = keyedLevel; + // 1. Determine the target dimension name + String targetDimensionName = clientLevel.getDimensionName(); + int separatorIndex = levelKey.lastIndexOf("@"); + if (separatorIndex != -1) + { + targetDimensionName = levelKey.substring(separatorIndex + 1); + } + + final String finalTargetDimensionName = targetDimensionName; + + // 2. Store the key for this dimension + this.keysByDimensionName.put(finalTargetDimensionName, new KeyInfo(serverKey, levelKey)); this.enabled = true; - return keyedLevel; + + // 3. Clear the cache for this dimension to ensure new wrappers are created with the new key + // (though in practice keys shouldn't change mid-session) + // + // We synchronize manually on the map to ensure atomicity of the compound removal operation + // and to prevent race conditions or deadlocks with other threads accessing the map. + // We avoid calling ClientLevelWrapper.getWrapper() inside the lock to prevent circular lock dependencies. + synchronized (this.keyedLevelsCache) + { + this.keyedLevelsCache.keySet().removeIf(level -> { + #if MC_VER <= MC_1_21_10 + String levelDim = level.dimension().location().toString(); + #else + String levelDim = level.dimension().identifier().toString(); + #endif + return levelDim.equals(finalTargetDimensionName); + }); + } + + // 4. Return the keyed wrapper for whatever level the core passed us, + // but only if it matches the dimension we just keyed. + return this.getServerKeyedLevel((ClientLevel) clientLevel.getWrappedMcObject()); } @Override - public void clearKeyedLevel() { this.serverKeyedLevel = null; } + public void clearKeyedLevel() + { + synchronized (this.keyedLevelsCache) + { + this.keyedLevelsCache.clear(); + this.keysByDimensionName.clear(); + } + } + @Override public boolean isEnabled() { return this.enabled; } - @Override - public void disable() { this.enabled = false; } + @Override + public void disable() + { + this.enabled = false; + this.clearKeyedLevel(); + } } diff --git a/common/src/main/java/com/seibel/distanthorizons/common/wrappers/world/ClientLevelWrapper.java b/common/src/main/java/com/seibel/distanthorizons/common/wrappers/world/ClientLevelWrapper.java index 57554854a..f38507bb0 100644 --- a/common/src/main/java/com/seibel/distanthorizons/common/wrappers/world/ClientLevelWrapper.java +++ b/common/src/main/java/com/seibel/distanthorizons/common/wrappers/world/ClientLevelWrapper.java @@ -6,6 +6,7 @@ import com.seibel.distanthorizons.common.wrappers.block.BiomeWrapper; import com.seibel.distanthorizons.common.wrappers.block.BlockStateWrapper; import com.seibel.distanthorizons.common.wrappers.block.ClientBlockStateColorCache; import com.seibel.distanthorizons.common.wrappers.chunk.ChunkWrapper; +import com.seibel.distanthorizons.common.wrappers.level.KeyedClientLevelManager; import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; @@ -104,7 +105,7 @@ public class ClientLevelWrapper implements IClientLevelWrapper public synchronized void markRendered() { this.lastRenderTime = System.currentTimeMillis(); } - public long getLastRenderTime() { return this.lastRenderTime; } + public synchronized long getLastRenderTime() { return this.lastRenderTime; } public boolean isDhLevelLoaded() { return this.dhLevel != null; } @@ -125,6 +126,7 @@ public class ClientLevelWrapper implements IClientLevelWrapper ClientLevelWrapper wrapper = ref.get(); if (wrapper != null && wrapper.isDhLevelLoaded() && wrapper.level != MINECRAFT.level) { + // We use the synchronized getter to prevent race conditions with markRendered() if (currentTime - wrapper.getLastRenderTime() > timeout) { toUnload.add(wrapper); @@ -148,6 +150,23 @@ public class ClientLevelWrapper implements IClientLevelWrapper } } + @Nullable + public static ClientLevelWrapper getWrapperByDimensionName(String dimensionName) + { + synchronized (LEVEL_WRAPPER_REF_BY_CLIENT_LEVEL) + { + for (WeakReference ref : LEVEL_WRAPPER_REF_BY_CLIENT_LEVEL.values()) + { + ClientLevelWrapper wrapper = ref.get(); + if (wrapper != null && wrapper.getDimensionName().equals(dimensionName)) + { + return wrapper; + } + } + } + return null; + } + /** @@ -157,9 +176,24 @@ public class ClientLevelWrapper implements IClientLevelWrapper @Nullable public static IClientLevelWrapper getWrapperIfDifferent(@Nullable IClientLevelWrapper levelWrapper, @NotNull ClientLevel level) { - if (KEYED_CLIENT_LEVEL_MANAGER.isEnabled() && KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel() != levelWrapper) + if (KEYED_CLIENT_LEVEL_MANAGER.isEnabled()) { - return getWrapper(level); + IServerKeyedClientLevel keyedLevel = null; + if (KEYED_CLIENT_LEVEL_MANAGER instanceof KeyedClientLevelManager) + { + keyedLevel = ((KeyedClientLevelManager) KEYED_CLIENT_LEVEL_MANAGER).getServerKeyedLevel(level); + } + else + { + // FIXME: If the implementation is not KeyedClientLevelManager, + // this fallback may return the key for the wrong dimension in multiverse scenarios. + keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(); + } + + if (keyedLevel != levelWrapper) + { + return getWrapper(level); + } } ClientLevelWrapper clientLevelWrapper = (ClientLevelWrapper)levelWrapper; @@ -186,7 +220,18 @@ public class ClientLevelWrapper implements IClientLevelWrapper } // used if the client is connected to a server that defines the currently loaded level - IServerKeyedClientLevel overrideLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(); + IServerKeyedClientLevel overrideLevel = null; + if (KEYED_CLIENT_LEVEL_MANAGER instanceof KeyedClientLevelManager) + { + overrideLevel = ((KeyedClientLevelManager) KEYED_CLIENT_LEVEL_MANAGER).getServerKeyedLevel(level); + } + else + { + // FIXME: If the implementation is not KeyedClientLevelManager, + // this fallback may return the key for the wrong dimension in multiverse scenarios. + overrideLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(); + } + if (overrideLevel != null) { return overrideLevel; @@ -419,6 +464,22 @@ public class ClientLevelWrapper implements IClientLevelWrapper return this.dimMinHeight; } + public IChunkWrapper tryGetChunk(DhChunkPos pos) + { + if (!this.level.hasChunk(pos.getX(), pos.getZ())) + { + return null; + } + + ChunkAccess chunk = this.level.getChunk(pos.getX(), pos.getZ(), ChunkStatus.EMPTY, false); + if (chunk == null) + { + return null; + } + + return new ChunkWrapper(chunk, this); + } + @Override public ClientLevel getWrappedMcObject() { return this.level; } diff --git a/common/src/main/java/com/seibel/distanthorizons/common/wrappers/world/ServerLevelWrapper.java b/common/src/main/java/com/seibel/distanthorizons/common/wrappers/world/ServerLevelWrapper.java index e400dcf76..4ae16db28 100644 --- a/common/src/main/java/com/seibel/distanthorizons/common/wrappers/world/ServerLevelWrapper.java +++ b/common/src/main/java/com/seibel/distanthorizons/common/wrappers/world/ServerLevelWrapper.java @@ -49,6 +49,12 @@ import net.minecraft.world.level.chunk.ChunkStatus; import net.minecraft.world.level.chunk.status.ChunkStatus; #endif +#if MC_VER <= MC_1_21_10 +#else +import net.minecraft.world.level.ChunkPos; +import net.minecraft.server.level.ChunkHolder; +#endif + import com.seibel.distanthorizons.core.logging.DhLogger; import org.jetbrains.annotations.Nullable; @@ -223,6 +229,41 @@ public class ServerLevelWrapper implements IServerLevelWrapper #endif } + public IChunkWrapper tryGetChunk(DhChunkPos pos) + { + #if MC_VER < MC_1_21_11 + if (!this.level.hasChunk(pos.getX(), pos.getZ())) + { + return null; + } + + ChunkAccess chunk = this.level.getChunk(pos.getX(), pos.getZ(), ChunkStatus.FULL, false); + if (chunk == null) + { + return null; + } + + return new ChunkWrapper(chunk, this); + #else + + // directly hitting the chunkMap is required otherwise MC will run this on the main server thread, + // causing lag + ChunkHolder chunkHolder = this.level.getChunkSource().chunkMap.getVisibleChunkIfPresent(new ChunkPos(pos.getX(), pos.getZ()).toLong()); + if (chunkHolder == null) + { + return null; + } + + ChunkAccess chunk = chunkHolder.getChunkIfPresent(ChunkStatus.FULL); + if (chunk == null) + { + return null; + } + + return new ChunkWrapper(chunk, this); + #endif + } + @Override public ServerLevel getWrappedMcObject() { return this.level; } diff --git a/fabric/src/main/java/com/seibel/distanthorizons/fabric/FabricServerProxy.java b/fabric/src/main/java/com/seibel/distanthorizons/fabric/FabricServerProxy.java index cf0568fb5..06d517149 100644 --- a/fabric/src/main/java/com/seibel/distanthorizons/fabric/FabricServerProxy.java +++ b/fabric/src/main/java/com/seibel/distanthorizons/fabric/FabricServerProxy.java @@ -143,6 +143,16 @@ public class FabricServerProxy implements AbstractModInitializer.IEventProxy ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { ServerApi.INSTANCE.serverPlayerJoinEvent(this.getServerPlayerWrapper(handler.player)); + + // Send identification for all loaded levels to the joining player + // This is necessary for Immersive Portals which can render multiple dimensions at once + for (ServerLevel level : server.getAllLevels()) + { + if (level != handler.player.level()) + { + ServerApi.INSTANCE.serverLevelLoadEvent(this.getServerLevelWrapper(level)); + } + } }); ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> {