From fd8c55076e71d0eb6cd3ae396f22c15a56726fdd Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Fri, 28 Aug 2026 16:30:05 -0400 Subject: [PATCH] Bridge host levels - Publish and remove dynamic levels through host lifecycle methods when available. - Attach hybrid host level data before engine binding and publication. - Preserve native loader behavior when host extensions are absent. --- .../iris/modded/ModdedDimensionManager.java | 1 + .../iris/modded/ModdedServerAccess.java | 2 + .../iris/modded/ModdedServerLevels.java | 152 +++++++++++++++++- .../modded/ModdedLevelDataAttachmentTest.java | 94 +++++++++++ .../iris/modded/ModdedServerLevelsTest.java | 73 +++++++++ 5 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedLevelDataAttachmentTest.java create mode 100644 adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedServerLevelsTest.java diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java index ac835d03e..b57943039 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java @@ -590,6 +590,7 @@ public final class ModdedDimensionManager { boolean loadEventStarted = false; try { + serverAccess.initializeLevelData(server, level); generator.bindLevel(level); Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator); ServerLevel previous = serverAccess.putLevelIfAbsent(server, key, level); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerAccess.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerAccess.java index 050a56369..e4fa27511 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerAccess.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerAccess.java @@ -31,6 +31,8 @@ public interface ModdedServerAccess { LevelStorageSource.LevelStorageAccess levelStorage(MinecraftServer server); + void initializeLevelData(MinecraftServer server, ServerLevel level); + ServerLevel putLevel(MinecraftServer server, ResourceKey key, ServerLevel level); ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey key, ServerLevel level); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerLevels.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerLevels.java index 3e0680af0..66e6b7a1a 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerLevels.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerLevels.java @@ -23,7 +23,11 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.PrimaryLevelData; +import net.minecraft.world.level.storage.WorldData; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ConcurrentModificationException; import java.util.LinkedHashMap; import java.util.List; @@ -34,6 +38,18 @@ import java.util.function.Consumer; public final class ModdedServerLevels implements ModdedServerAccess { private static final int CAPTURE_ATTEMPTS = 16; + private static final ClassValue HOST_LEVEL_PUBLICATIONS = new ClassValue<>() { + @Override + protected HostLevelPublication computeValue(Class type) { + return HostLevelPublication.detect(type, ServerLevel.class); + } + }; + private static final ClassValue HOST_LEVEL_DATA_ATTACHMENTS = new ClassValue<>() { + @Override + protected HostLevelDataAttachment computeValue(Class type) { + return HostLevelDataAttachment.detect(type, PrimaryLevelData.class, ResourceKey.class); + } + }; private static volatile Snapshot snapshot; private final Consumer levelCacheInvalidator; @@ -43,8 +59,8 @@ public final class ModdedServerLevels implements ModdedServerAccess { } /** - * Immutable view of the loaded levels. {@code server.levels} is a plain map mutated on the server - * thread, so every off-server-thread reader must iterate this snapshot instead of + * Immutable view of the loaded levels. {@code server.levels} is loader-owned and published on the + * server thread, so every off-server-thread reader must iterate this snapshot instead of * {@code server.getAllLevels()} to avoid ConcurrentModificationException. */ public static List levels(MinecraftServer server) { @@ -137,8 +153,32 @@ public final class ModdedServerLevels implements ModdedServerAccess { return server.storageSource; } + @Override + public void initializeLevelData(MinecraftServer server, ServerLevel level) { + Object levelData = level.getLevelData(); + HostLevelDataAttachment attachment = HOST_LEVEL_DATA_ATTACHMENTS.get(levelData.getClass()); + if (!attachment.supported()) { + return; + } + WorldData worldData = server.getWorldData(); + if (!(worldData instanceof PrimaryLevelData rootData)) { + throw new IllegalStateException("Host level data attachment requires PrimaryLevelData, found " + + worldData.getClass().getName()); + } + attachment.attach(levelData, rootData, level.dimension()); + } + @Override public ServerLevel putLevel(MinecraftServer server, ResourceKey key, ServerLevel level) { + HostLevelPublication hostPublication = HOST_LEVEL_PUBLICATIONS.get(server.getClass()); + if (hostPublication.supported()) { + ServerLevel previous = server.levels.get(key); + if (previous != level) { + hostPublication.add(server, level); + capture(server); + } + return previous; + } ServerLevel previous = server.levels.put(key, level); if (previous != level) { capture(server); @@ -149,6 +189,15 @@ public final class ModdedServerLevels implements ModdedServerAccess { @Override public ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey key, ServerLevel level) { + HostLevelPublication hostPublication = HOST_LEVEL_PUBLICATIONS.get(server.getClass()); + if (hostPublication.supported()) { + ServerLevel previous = server.levels.get(key); + if (previous == null) { + hostPublication.add(server, level); + capture(server); + } + return previous; + } ServerLevel previous = server.levels.putIfAbsent(key, level); if (previous == null) { capture(server); @@ -159,6 +208,15 @@ public final class ModdedServerLevels implements ModdedServerAccess { @Override public ServerLevel removeLevel(MinecraftServer server, ResourceKey key) { + HostLevelPublication hostPublication = HOST_LEVEL_PUBLICATIONS.get(server.getClass()); + if (hostPublication.supported()) { + ServerLevel removed = server.levels.get(key); + if (removed != null) { + hostPublication.remove(server, removed); + capture(server); + } + return removed; + } ServerLevel removed = server.levels.remove(key); if (removed != null) { capture(server); @@ -175,4 +233,94 @@ public final class ModdedServerLevels implements ModdedServerAccess { private record Snapshot(MinecraftServer server, List levels, Map, ServerLevel> byKey) { } + + static final class HostLevelPublication { + private final Method addLevel; + private final Method removeLevel; + + private HostLevelPublication(Method addLevel, Method removeLevel) { + this.addLevel = addLevel; + this.removeLevel = removeLevel; + } + + static HostLevelPublication detect(Class serverType, Class levelType) { + try { + Method addLevel = serverType.getMethod("addLevel", levelType); + Method removeLevel = serverType.getMethod("removeLevel", levelType); + return new HostLevelPublication(addLevel, removeLevel); + } catch (NoSuchMethodException e) { + return new HostLevelPublication(null, null); + } + } + + boolean supported() { + return addLevel != null && removeLevel != null; + } + + void add(Object server, Object level) { + invoke(addLevel, server, level); + } + + void remove(Object server, Object level) { + invoke(removeLevel, server, level); + } + + private void invoke(Method method, Object server, Object level) { + try { + method.invoke(server, level); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Iris cannot access host level publication method " + + method.getName(), e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (cause instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Host level publication method " + method.getName() + + " failed", cause); + } + } + } + + static final class HostLevelDataAttachment { + private final Method attach; + + private HostLevelDataAttachment(Method attach) { + this.attach = attach; + } + + static HostLevelDataAttachment detect(Class levelDataType, Class rootDataType, + Class dimensionKeyType) { + try { + return new HostLevelDataAttachment(levelDataType.getMethod( + "attach", rootDataType, dimensionKeyType)); + } catch (NoSuchMethodException e) { + return new HostLevelDataAttachment(null); + } + } + + boolean supported() { + return attach != null; + } + + void attach(Object levelData, Object rootData, Object dimensionKey) { + try { + attach.invoke(levelData, rootData, dimensionKey); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Iris cannot access host level data attachment method", e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (cause instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Host level data attachment failed", cause); + } + } + } } diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedLevelDataAttachmentTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedLevelDataAttachmentTest.java new file mode 100644 index 000000000..541337a32 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedLevelDataAttachmentTest.java @@ -0,0 +1,94 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ModdedLevelDataAttachmentTest { + @Test + public void detectsAndInvokesHostLevelDataAttachment() { + ModdedServerLevels.HostLevelDataAttachment attachment = + ModdedServerLevels.HostLevelDataAttachment.detect( + AttachedLevelData.class, RootData.class, DimensionKey.class); + AttachedLevelData levelData = new AttachedLevelData(); + RootData rootData = new RootData(); + DimensionKey dimensionKey = new DimensionKey(); + + assertTrue(attachment.supported()); + attachment.attach(levelData, rootData, dimensionKey); + assertSame(rootData, levelData.rootData); + assertSame(dimensionKey, levelData.dimensionKey); + } + + @Test + public void ignoresLevelDataWithoutAttachmentCapability() { + ModdedServerLevels.HostLevelDataAttachment attachment = + ModdedServerLevels.HostLevelDataAttachment.detect( + StandardLevelData.class, RootData.class, DimensionKey.class); + + assertFalse(attachment.supported()); + } + + @Test + public void propagatesHostAttachmentFailureWithoutReflectionWrapper() { + ModdedServerLevels.HostLevelDataAttachment attachment = + ModdedServerLevels.HostLevelDataAttachment.detect( + FailingLevelData.class, RootData.class, DimensionKey.class); + IllegalStateException failure = assertThrows(IllegalStateException.class, + () -> attachment.attach(new FailingLevelData(), new RootData(), new DimensionKey())); + + assertNull(failure.getCause()); + } + + @Test + public void initializesLevelDataBeforeBindingAndPublication() throws IOException { + String source = Files.readString(sourcePath("ModdedDimensionManager.java")); + int construction = source.indexOf("ServerLevel level = new ServerLevel("); + int initialization = source.indexOf("serverAccess.initializeLevelData(server, level);", construction); + int binding = source.indexOf("generator.bindLevel(level);", initialization); + int publication = source.indexOf("serverAccess.putLevelIfAbsent(server, key, level);", binding); + + assertTrue(construction >= 0); + assertTrue(initialization > construction); + assertTrue(binding > initialization); + assertTrue(publication > binding); + } + + private static Path sourcePath(String fileName) { + return Path.of(System.getProperty("iris.moddedCommonSources"), + "art/arcane/iris/modded", fileName); + } + + public static final class AttachedLevelData { + private RootData rootData; + private DimensionKey dimensionKey; + + public void attach(RootData rootData, DimensionKey dimensionKey) { + this.rootData = rootData; + this.dimensionKey = dimensionKey; + } + } + + public static final class FailingLevelData { + public void attach(RootData rootData, DimensionKey dimensionKey) { + throw new IllegalStateException("attachment failed"); + } + } + + public static final class StandardLevelData { + } + + public static final class RootData { + } + + public static final class DimensionKey { + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedServerLevelsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedServerLevelsTest.java new file mode 100644 index 000000000..0b11aafd0 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedServerLevelsTest.java @@ -0,0 +1,73 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ModdedServerLevelsTest { + @Test + public void detectsAndInvokesHostLevelPublicationPair() { + ModdedServerLevels.HostLevelPublication publication = + ModdedServerLevels.HostLevelPublication.detect(PublishingServer.class, PublishedLevel.class); + PublishingServer server = new PublishingServer(); + PublishedLevel level = new PublishedLevel(); + + assertTrue(publication.supported()); + publication.add(server, level); + assertSame(level, server.added); + publication.remove(server, level); + assertSame(level, server.removed); + } + + @Test + public void rejectsPartialHostLevelPublicationCapability() { + ModdedServerLevels.HostLevelPublication publication = + ModdedServerLevels.HostLevelPublication.detect(AddOnlyServer.class, PublishedLevel.class); + + assertFalse(publication.supported()); + } + + @Test + public void propagatesHostPublicationFailureWithoutReflectionWrapper() { + ModdedServerLevels.HostLevelPublication publication = + ModdedServerLevels.HostLevelPublication.detect(FailingServer.class, PublishedLevel.class); + IllegalStateException failure = assertThrows(IllegalStateException.class, + () -> publication.add(new FailingServer(), new PublishedLevel())); + + assertNull(failure.getCause()); + } + + public static final class PublishingServer { + private PublishedLevel added; + private PublishedLevel removed; + + public void addLevel(PublishedLevel level) { + added = level; + } + + public void removeLevel(PublishedLevel level) { + removed = level; + } + } + + public static final class AddOnlyServer { + public void addLevel(PublishedLevel level) { + } + } + + public static final class FailingServer { + public void addLevel(PublishedLevel level) { + throw new IllegalStateException("publication failed"); + } + + public void removeLevel(PublishedLevel level) { + } + } + + public static final class PublishedLevel { + } +}