Bind generation context

- Resolve first-generation engine binding from authoritative published level context.
- Validate generator identity and recover canonical Overworld binding from stale snapshots.
- Add shared loader regression tests and their Mockito test dependency.
This commit is contained in:
Brian Neumann-Fopiano
2026-08-28 16:30:11 -04:00
parent fd8c55076e
commit 27799039d1
5 changed files with 243 additions and 12 deletions
+1
View File
@@ -114,6 +114,7 @@ dependencies {
minecraft("com.mojang:minecraft:${minecraftVersion}")
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
// registrySync and resourceLoader are LOAD-BEARING despite zero imports anywhere in Iris:
// fabric-registry-sync-v0 delays registry freeze past mod init, which is what lets
// IrisFabricBootstrap register the CHUNK_GENERATOR codec in
+1
View File
@@ -131,6 +131,7 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
dependencies {
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly(libs.spigot) {
transitive = false
@@ -331,34 +331,113 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
if (server == null) {
return null;
}
// Snapshot, never server.getAllLevels(): this runs off the server thread from data queries.
for (ServerLevel level : ModdedServerLevels.levels(server)) {
ServerLevel resolved = resolveBoundLevel(server, ModdedServerLevels.levels(server));
if (resolved != null) {
boundLevel = resolved;
}
return resolved;
}
ServerLevel resolveBoundLevel(MinecraftServer server, List<ServerLevel> snapshot) {
for (ServerLevel level : snapshot) {
if (level.getChunkSource().getGenerator() == this) {
boundLevel = level;
return level;
}
}
return null;
ServerLevel overworld = server.getLevel(Level.OVERWORLD);
return overworld != null && overworld.getChunkSource().getGenerator() == this ? overworld : null;
}
Engine engine() {
requireBindingAllowed();
Engine cached = engine;
requireCompletedShutdown(cached);
if (cached != null && !cached.isClosed()) {
Engine cached = readyEngine();
if (cached != null) {
return cached;
}
ServerLevel level = boundLevel();
if (level == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
}
return bindEngine(level);
return bindGenerationLevel(level);
}
private Engine engine(ResourceKey<Level> levelKey) {
Engine cached = readyEngine();
if (cached != null) {
return cached;
}
ServerLevel level = boundLevel;
if (level == null) {
level = requirePublishedLevel(ModdedEngineBootstrap.currentServer(), levelKey);
} else {
requireGeneratorLevel(level, levelKey);
}
return bindGenerationLevel(level);
}
private Engine engine(ServerLevel generationLevel) {
Engine cached = readyEngine();
if (cached != null) {
return cached;
}
ServerLevel level = boundLevel == null ? generationLevel : boundLevel;
requireGeneratorLevel(level, generationLevel.dimension());
return bindGenerationLevel(level);
}
private Engine readyEngine() {
requireBindingAllowed();
Engine cached = engine;
requireCompletedShutdown(cached);
if (cached != null && !cached.isClosed()) {
return cached;
}
return null;
}
private Engine bindGenerationLevel(ServerLevel level) {
bindLevel(level);
Engine bound = readyEngine();
if (bound == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' completed generation binding without a ready engine");
}
return bound;
}
ServerLevel requirePublishedLevel(MinecraftServer server, ResourceKey<Level> levelKey) {
if (server == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' cannot resolve level '" + levelKey.identifier() + "': server is unavailable");
}
ServerLevel level = server.getLevel(levelKey);
if (level == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' has no published ServerLevel for '" + levelKey.identifier() + "'");
}
requireGeneratorLevel(level, levelKey);
return level;
}
private void requireGeneratorLevel(ServerLevel level, ResourceKey<Level> levelKey) {
if (!levelKey.equals(level.dimension())) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' resolved level '"
+ level.dimension().identifier() + "' while binding '" + levelKey.identifier() + "'");
}
ChunkGenerator publishedGenerator = level.getChunkSource().getGenerator();
if (publishedGenerator != this) {
throw new IllegalStateException("Published ServerLevel '" + levelKey.identifier()
+ "' does not use Iris generator '" + dimensionKey + "'");
}
}
synchronized void bindLevel(ServerLevel level) {
if (level.getChunkSource().getGenerator() != this) {
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
}
Engine current = engineIfBound();
if (boundLevel == level && current != null && current.getComplex() != null) {
return;
}
requireCompletedShutdown(engine);
unloading = false;
Engine bound = bindEngine(level);
@@ -853,7 +932,6 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
Engine current = engine();
// Self-heal for an engine bound through a data-query path instead of bindLevel; a no-op once prepared.
importedFeatures.prepare(current);
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
@@ -867,7 +945,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
Engine current = engine();
Engine current = engine(levelKey);
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
@@ -891,7 +969,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
Engine current = engine();
Engine current = engine(level.getLevel());
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
NativeStructureReferenceRepair.createReferences(
@@ -0,0 +1,150 @@
package art.arcane.iris.modded;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.ChunkGenerator;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
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;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedGenerationBootstrapBindingTest {
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
@Test
public void publishedLevelRequiresCurrentGeneratorIdentity() {
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class, CALLS_REAL_METHODS);
MinecraftServer server = mock(MinecraftServer.class);
ServerLevel level = mock(ServerLevel.class);
ServerChunkCache chunkSource = mock(ServerChunkCache.class);
when(server.getLevel(Level.OVERWORLD)).thenReturn(level);
when(level.dimension()).thenReturn(Level.OVERWORLD);
when(level.getChunkSource()).thenReturn(chunkSource);
when(chunkSource.getGenerator()).thenReturn(generator);
assertSame(level, generator.requirePublishedLevel(server, Level.OVERWORLD));
ChunkGenerator otherGenerator = mock(ChunkGenerator.class);
when(chunkSource.getGenerator()).thenReturn(otherGenerator);
IllegalStateException mismatch = assertThrows(IllegalStateException.class,
() -> generator.requirePublishedLevel(server, Level.OVERWORLD));
assertTrue(mismatch.getMessage().contains("does not use Iris generator"));
}
@Test
public void publishedLevelRejectsMissingServerAndWorld() {
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class, CALLS_REAL_METHODS);
IllegalStateException missingServer = assertThrows(IllegalStateException.class,
() -> generator.requirePublishedLevel(null, Level.OVERWORLD));
assertTrue(missingServer.getMessage().contains("server is unavailable"));
MinecraftServer server = mock(MinecraftServer.class);
IllegalStateException missingWorld = assertThrows(IllegalStateException.class,
() -> generator.requirePublishedLevel(server, Level.OVERWORLD));
assertTrue(missingWorld.getMessage().contains("no published ServerLevel"));
}
@Test
public void staleSnapshotResolvesOnlyCanonicalOverworldOwnedByGenerator() {
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class, CALLS_REAL_METHODS);
MinecraftServer server = mock(MinecraftServer.class);
ServerLevel overworld = mock(ServerLevel.class);
ServerChunkCache chunkSource = mock(ServerChunkCache.class);
when(server.getLevel(Level.OVERWORLD)).thenReturn(overworld);
when(overworld.getChunkSource()).thenReturn(chunkSource);
when(chunkSource.getGenerator()).thenReturn(generator);
assertSame(overworld, generator.resolveBoundLevel(server, List.of()));
ChunkGenerator otherGenerator = mock(ChunkGenerator.class);
when(chunkSource.getGenerator()).thenReturn(otherGenerator);
assertNull(generator.resolveBoundLevel(server, List.of()));
verify(server, never()).getLevel(Level.NETHER);
}
@Test
public void dynamicLevelStillResolvesFromSnapshotWithoutCanonicalLookup() {
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class, CALLS_REAL_METHODS);
MinecraftServer server = mock(MinecraftServer.class);
ServerLevel dynamicLevel = mock(ServerLevel.class);
ServerChunkCache chunkSource = mock(ServerChunkCache.class);
when(dynamicLevel.getChunkSource()).thenReturn(chunkSource);
when(chunkSource.getGenerator()).thenReturn(generator);
assertSame(dynamicLevel, generator.resolveBoundLevel(server, List.of(dynamicLevel)));
verify(server, never()).getLevel(Level.OVERWORLD);
}
@Test
public void firstGenerationStagesProvideAuthoritativeLevelContext() throws IOException {
String source = source("IrisModdedChunkGenerator.java");
String structures = method(source, "public void createStructures(");
String references = method(source, "public void createReferences(");
assertTrue(structures.contains("Engine current = engine(levelKey);"));
assertTrue(references.contains("Engine current = engine(level.getLevel());"));
String publishedBinding = method(source, "private Engine engine(ResourceKey<Level> levelKey)");
assertTrue(publishedBinding.contains("requirePublishedLevel(ModdedEngineBootstrap.currentServer(), levelKey)"));
assertTrue(publishedBinding.contains("return bindGenerationLevel(level);"));
assertFalse(publishedBinding.contains("ModdedServerLevels.level("));
String fullBinding = method(source, "private Engine bindGenerationLevel(ServerLevel level)");
assertTrue(fullBinding.contains("bindLevel(level);"));
assertTrue(fullBinding.contains("Engine bound = readyEngine();"));
String genericBinding = method(source, "Engine engine()");
assertTrue(genericBinding.contains("return bindGenerationLevel(level);"));
}
@Test
public void constructorMixinIsNotPartOfGenerationBootstrap() throws IOException {
String sourceRoot = System.getProperty(SOURCE_ROOT_PROPERTY);
assertTrue(sourceRoot != null && !sourceRoot.isBlank());
Path root = Path.of(sourceRoot);
assertFalse(Files.exists(root.resolve(Path.of("art", "arcane", "iris", "modded", "mixin",
"ServerLevelBindingMixin.java"))));
String mixinConfig = Files.readString(root.getParent().resolve("resources/irisworldgen.entity.mixins.json"));
assertFalse(mixinConfig.contains("ServerLevelBindingMixin"));
}
private String source(String fileName) throws IOException {
String sourceRoot = System.getProperty(SOURCE_ROOT_PROPERTY);
assertTrue(sourceRoot != null && !sourceRoot.isBlank());
return Files.readString(Path.of(sourceRoot, "art", "arcane", "iris", "modded", fileName));
}
private String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue(start >= 0);
int body = source.indexOf('{', start);
assertTrue(body >= 0);
int depth = 0;
for (int index = body; index < source.length(); index++) {
char token = source.charAt(index);
if (token == '{') {
depth++;
} else if (token == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
+1
View File
@@ -105,6 +105,7 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
dependencies {
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
testRuntimeOnly('org.junit.platform:junit-platform-launcher:6.1.2')
testRuntimeOnly('org.junit.vintage:junit-vintage-engine:6.1.2')
compileOnly('org.slf4j:slf4j-api:2.0.17')