mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-29 13:30:50 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+90
-12
@@ -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(
|
||||
|
||||
+150
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user