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() public static void resetDetection()
{ {
isImmersivePortalsPresent = null; synchronized (ImmersivePortalsCompat.class)
isImmersivePortalsActive = null; {
isImmersivePortalsPresent = null;
isImmersivePortalsActive = null;
}
} }
} }
@@ -661,15 +661,21 @@ public class ChunkWrapper implements IChunkWrapper
@Override @Override
public String toString() { return this.chunk.getClass().getSimpleName() + this.chunk.getPos(); } public String toString() { return this.chunk.getClass().getSimpleName() + this.chunk.getPos(); }
//@Override //@Override
//public int hashCode() //public int hashCode()
//{ //{
// if (this.blockBiomeHashCode == 0) // if (this.blockBiomeHashCode == 0)
// { // {
// this.blockBiomeHashCode = this.getBlockBiomeHashCode(); // this.blockBiomeHashCode = this.getBlockBiomeHashCode();
// } // }
// //
// return this.blockBiomeHashCode; // 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; 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.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager; import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.multiplayer.ClientLevel;
import org.jetbrains.annotations.Nullable; 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 class KeyedClientLevelManager implements IKeyedClientLevelManager
{ {
public static final KeyedClientLevelManager INSTANCE = new KeyedClientLevelManager(); public static final KeyedClientLevelManager INSTANCE = new KeyedClientLevelManager();
/** This is set and managed by the ClientApi for servers with support for DH. */ private static class KeyInfo {
@Nullable public final String serverKey;
private IServerKeyedClientLevel serverKeyedLevel = null; 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 */ /** 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 @Override
@Nullable @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 @Override
public IServerKeyedClientLevel setServerKeyedLevel(IClientLevelWrapper clientLevel, String serverKey, String levelKey) public IServerKeyedClientLevel setServerKeyedLevel(IClientLevelWrapper clientLevel, String serverKey, String levelKey)
{ {
IServerKeyedClientLevel keyedLevel = new ServerKeyedClientLevelWrapper((ClientLevel) clientLevel.getWrappedMcObject(), serverKey, levelKey); // 1. Determine the target dimension name
this.serverKeyedLevel = keyedLevel; 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; 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 @Override
public void clearKeyedLevel() { this.serverKeyedLevel = null; } public void clearKeyedLevel()
{
synchronized (this.keyedLevelsCache)
{
this.keyedLevelsCache.clear();
this.keysByDimensionName.clear();
}
}
@Override @Override
public boolean isEnabled() { return this.enabled; } 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.BlockStateWrapper;
import com.seibel.distanthorizons.common.wrappers.block.ClientBlockStateColorCache; import com.seibel.distanthorizons.common.wrappers.block.ClientBlockStateColorCache;
import com.seibel.distanthorizons.common.wrappers.chunk.ChunkWrapper; 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.api.internal.ClientApi;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
@@ -104,7 +105,7 @@ public class ClientLevelWrapper implements IClientLevelWrapper
public synchronized void markRendered() { public synchronized void markRendered() {
this.lastRenderTime = System.currentTimeMillis(); this.lastRenderTime = System.currentTimeMillis();
} }
public long getLastRenderTime() { return this.lastRenderTime; } public synchronized long getLastRenderTime() { return this.lastRenderTime; }
public boolean isDhLevelLoaded() { public boolean isDhLevelLoaded() {
return this.dhLevel != null; return this.dhLevel != null;
} }
@@ -125,6 +126,7 @@ public class ClientLevelWrapper implements IClientLevelWrapper
ClientLevelWrapper wrapper = ref.get(); ClientLevelWrapper wrapper = ref.get();
if (wrapper != null && wrapper.isDhLevelLoaded() && wrapper.level != MINECRAFT.level) 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) if (currentTime - wrapper.getLastRenderTime() > timeout)
{ {
toUnload.add(wrapper); 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 @Nullable
public static IClientLevelWrapper getWrapperIfDifferent(@Nullable IClientLevelWrapper levelWrapper, @NotNull ClientLevel level) 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; 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 // 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) if (overrideLevel != null)
{ {
return overrideLevel; return overrideLevel;
@@ -419,6 +464,22 @@ public class ClientLevelWrapper implements IClientLevelWrapper
return this.dimMinHeight; 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 @Override
public ClientLevel getWrappedMcObject() { return this.level; } 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; import net.minecraft.world.level.chunk.status.ChunkStatus;
#endif #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 com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -223,6 +229,41 @@ public class ServerLevelWrapper implements IServerLevelWrapper
#endif #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 @Override
public ServerLevel getWrappedMcObject() { return this.level; } public ServerLevel getWrappedMcObject() { return this.level; }
@@ -143,6 +143,16 @@ public class FabricServerProxy implements AbstractModInitializer.IEventProxy
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> ServerPlayConnectionEvents.JOIN.register((handler, sender, server) ->
{ {
ServerApi.INSTANCE.serverPlayerJoinEvent(this.getServerPlayerWrapper(handler.player)); 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) -> ServerPlayConnectionEvents.DISCONNECT.register((handler, server) ->
{ {