add server support for immersive portals

This commit is contained in:
Michael Harvey
2025-12-20 19:43:00 +01:00
committed by Acuadragon100
parent e65b1e2dfc
commit 2e1a2367bd
6 changed files with 247 additions and 20 deletions
@@ -86,7 +86,10 @@ public class ImmersivePortalsCompat
*/
public static void resetDetection()
{
isImmersivePortalsPresent = null;
isImmersivePortalsActive = null;
synchronized (ImmersivePortalsCompat.class)
{
isImmersivePortalsPresent = null;
isImmersivePortalsActive = null;
}
}
}
@@ -672,4 +672,10 @@ public class ChunkWrapper implements IChunkWrapper
// return this.blockBiomeHashCode;
//}
@Override
public IChunkWrapper copy()
{
return new ChunkWrapper(this.chunk, this.wrappedLevel, false);
}
}
@@ -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<String, KeyInfo> keysByDimensionName = new ConcurrentHashMap<>();
/** Cache for already keyed level wrappers to maintain object identity. */
private final Map<ClientLevel, IServerKeyedClientLevel> 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();
}
}
@@ -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<ClientLevelWrapper> 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; }
@@ -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; }
@@ -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) ->
{