mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-29 21:41:00 +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}")
|
minecraft("com.mojang:minecraft:${minecraftVersion}")
|
||||||
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||||
testImplementation('junit:junit:4.13.2')
|
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:
|
// 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
|
// fabric-registry-sync-v0 delays registry freeze past mod init, which is what lets
|
||||||
// IrisFabricBootstrap register the CHUNK_GENERATOR codec in
|
// IrisFabricBootstrap register the CHUNK_GENERATOR codec in
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
|
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||||
compileOnly(libs.spigot) {
|
compileOnly(libs.spigot) {
|
||||||
transitive = false
|
transitive = false
|
||||||
|
|||||||
+90
-12
@@ -331,34 +331,113 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
if (server == null) {
|
if (server == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Snapshot, never server.getAllLevels(): this runs off the server thread from data queries.
|
ServerLevel resolved = resolveBoundLevel(server, ModdedServerLevels.levels(server));
|
||||||
for (ServerLevel level : 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) {
|
if (level.getChunkSource().getGenerator() == this) {
|
||||||
boundLevel = level;
|
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
ServerLevel overworld = server.getLevel(Level.OVERWORLD);
|
||||||
|
return overworld != null && overworld.getChunkSource().getGenerator() == this ? overworld : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Engine engine() {
|
Engine engine() {
|
||||||
requireBindingAllowed();
|
Engine cached = readyEngine();
|
||||||
Engine cached = engine;
|
if (cached != null) {
|
||||||
requireCompletedShutdown(cached);
|
|
||||||
if (cached != null && !cached.isClosed()) {
|
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
ServerLevel level = boundLevel();
|
ServerLevel level = boundLevel();
|
||||||
if (level == null) {
|
if (level == null) {
|
||||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
|
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) {
|
synchronized void bindLevel(ServerLevel level) {
|
||||||
if (level.getChunkSource().getGenerator() != this) {
|
if (level.getChunkSource().getGenerator() != this) {
|
||||||
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
|
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
|
||||||
}
|
}
|
||||||
|
Engine current = engineIfBound();
|
||||||
|
if (boundLevel == level && current != null && current.getComplex() != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
requireCompletedShutdown(engine);
|
requireCompletedShutdown(engine);
|
||||||
unloading = false;
|
unloading = false;
|
||||||
Engine bound = bindEngine(level);
|
Engine bound = bindEngine(level);
|
||||||
@@ -853,7 +932,6 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
@Override
|
@Override
|
||||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
|
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
|
||||||
Engine current = engine();
|
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);
|
importedFeatures.prepare(current);
|
||||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
|
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
@@ -867,7 +945,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
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");
|
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
||||||
@@ -891,7 +969,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
|
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");
|
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
NativeStructureReferenceRepair.createReferences(
|
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 {
|
dependencies {
|
||||||
testImplementation('junit:junit:4.13.2')
|
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.platform:junit-platform-launcher:6.1.2')
|
||||||
testRuntimeOnly('org.junit.vintage:junit-vintage-engine:6.1.2')
|
testRuntimeOnly('org.junit.vintage:junit-vintage-engine:6.1.2')
|
||||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||||
|
|||||||
Reference in New Issue
Block a user