Fixed Structure Crash

- Don't carash anymore when you disable structures in the server.properties.
This commit is contained in:
Brian Neumann-Fopiano
2026-10-04 15:32:54 -04:00
parent 65ae3f1b45
commit ee5aaec87c
6 changed files with 12 additions and 74 deletions
@@ -195,8 +195,6 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
if (!nativeGenerator.represents(level)) {
throw new IllegalArgumentException("NativeWorld does not use Iris generator '" + dimensionKey + "'");
}
requireGlobalStructureGeneration(
NativeModdedServer.forWorld(level).generateStructures(), dimensionKey);
Engine activeEngine = engineIfBound();
Engine replacement = ModdedWorldEngines.prepareReplacement(
level,
@@ -462,13 +460,6 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
// A recorded failure stands until repoint or unbind resets the binding; re-running the bind would repeat the
// history verification on every chunk call and fail the same way.
engineBinding.throwIfFailed(dimensionKey);
try {
requireGlobalStructureGeneration(
NativeModdedServer.forWorld(level).generateStructures(), dimensionKey);
} catch (RuntimeException error) {
recordBindFailure(level, error);
throw error;
}
// Cache the owning level so hot paths never scan the level map to find themselves.
boundLevel = level;
Engine cached = engine;
@@ -575,29 +566,6 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
}
}
static void requireGlobalStructureGeneration(boolean enabled, String dimensionKey) {
if (enabled) {
return;
}
String remedy = integratedEnvironment()
? "enable 'Generate Structures' for this world; Iris requires it, then deny families through "
+ "importedStructures.disabled or complete keys through importedStructures.disabledExact"
: "set generate-structures=true in server.properties, restart the server, "
+ "then deny families through importedStructures.disabled or complete keys through "
+ "importedStructures.disabledExact";
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' cannot bind while generate-structures=false; " + remedy);
}
private static boolean integratedEnvironment() {
try {
return ModdedEngineBootstrap.loader().clientEnvironment();
} catch (Throwable e) {
// No loader bound (unit tests, very early boot): assume dedicated wording.
return false;
}
}
/**
* Null only while no level uses this generator (world creation screens, codec validation). A bound level whose
* engine is refused, unloading or stopping throws, so no stage can mistake it for an unbound generator.
@@ -122,17 +122,6 @@ final class ModdedNativeStructurePolicy implements NativeModdedStructureStage.Po
return ((IrisMaterialPalette) source).get(rng, x, y, z, generator.engine().getData());
}
@Override
public RuntimeException structuresDisabled(int chunkX, int chunkZ) {
return new IllegalStateException("Iris cannot generate native structures in chunk "
+ chunkX + "," + chunkZ
+ " because generate-structures=false disables them outside the pack. That flag is fixed when "
+ "the world is created (server.properties generate-structures, or the Generate Structures "
+ "toggle in singleplayer), so it cannot be changed for this world: create a new world with "
+ "structures enabled, then deny families through importedStructures.disabled or complete keys "
+ "through importedStructures.disabledExact");
}
@Override
public String generationLabel(String structureId) {
return "Iris native structure " + structureId;
@@ -57,8 +57,11 @@ public class ModdedBindFailureTest {
when(server.generateStructures()).thenReturn(false);
try (MockedStatic<NativeModdedServer> servers = mockStatic(NativeModdedServer.class);
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class);
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
servers.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
engines.when(() -> ModdedWorldEngines.get(eq(level), any(), any(), anyLong(), any()))
.thenThrow(new IllegalStateException("broken world binding"));
IllegalStateException recorded = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
IllegalStateException second = assertThrows(IllegalStateException.class, generator::engine);
@@ -66,9 +69,9 @@ public class ModdedBindFailureTest {
assertSame(recorded, second.getCause());
assertSame(recorded, third.getCause());
servers.verify(() -> NativeModdedServer.forWorld(level), times(1));
engines.verify(() -> ModdedWorldEngines.get(eq(level), any(), any(), anyLong(), any()), times(1));
log.verify(() -> ModdedIrisLog.error(argThat((String line) -> line != null && line.startsWith("Cause: ")
&& line.contains("generate-structures=false"))), times(1));
&& line.contains("broken world binding"))), times(1));
}
}
@@ -117,13 +120,15 @@ public class ModdedBindFailureTest {
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class);
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
servers.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
engines.when(() -> ModdedWorldEngines.get(eq(level), any(), any(), anyLong(), any()))
.thenAnswer(invocation -> { throw new IllegalStateException("broken world binding"); });
IllegalStateException first = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
generator.unbindEngine(level);
IllegalStateException second = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
assertNotSame(first, second.getCause());
servers.verify(() -> NativeModdedServer.forWorld(level), times(2));
engines.verify(() -> ModdedWorldEngines.get(eq(level), any(), any(), anyLong(), any()), times(2));
engines.verify(() -> ModdedWorldEngines.evictOrThrow(level));
}
}
@@ -37,7 +37,7 @@ public class ModdedStructureBindingOrderTest {
}
@Test
public void initialBindingPublishesReadyEngineBeforeStartingStructureSearches() throws Exception {
public void disabledStructuresAllowInitialBindingBeforeStartingStructureSearches() throws Exception {
NativeModdedChunkGenerator<?, ?, ?> nativeGenerator = mock(NativeModdedChunkGenerator.class);
ModdedEngineBinding<Engine> binding = new ModdedEngineBinding<>(1L, TimeUnit.MILLISECONDS);
IrisModdedChunkGenerator generator = generator(nativeGenerator, binding);
@@ -45,7 +45,7 @@ public class ModdedStructureBindingOrderTest {
NativeModdedServer server = mock(NativeModdedServer.class);
Engine engine = readyEngine();
when(nativeGenerator.represents(world)).thenReturn(true);
when(server.generateStructures()).thenReturn(true);
when(server.generateStructures()).thenReturn(false);
doAnswer(invocation -> {
assertSame(engine, generator.awaitStructureEngine());
assertSame(engine, binding.await("overworld"));
@@ -88,7 +88,7 @@ public class ModdedStructureBindingOrderTest {
}
@Test
public void repointPublishesReplacementBindingBeforeStartingStructureSearches() throws Exception {
public void disabledStructuresAllowReplacementBindingBeforeStartingStructureSearches() throws Exception {
NativeModdedChunkGenerator<?, ?, ?> nativeGenerator = mock(NativeModdedChunkGenerator.class);
ModdedEngineBinding<Engine> binding = new ModdedEngineBinding<>(1L, TimeUnit.MILLISECONDS);
IrisModdedChunkGenerator generator = generator(nativeGenerator, binding);
@@ -96,7 +96,7 @@ public class ModdedStructureBindingOrderTest {
NativeModdedServer server = mock(NativeModdedServer.class);
Engine replacement = readyEngine();
when(nativeGenerator.represents(world)).thenReturn(true);
when(server.generateStructures()).thenReturn(true);
when(server.generateStructures()).thenReturn(false);
doAnswer(invocation -> {
assertSame(replacement, generator.awaitStructureEngine());
assertSame(replacement, binding.await("overworld"));
@@ -7,7 +7,6 @@ import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructureFailureContractTest {
@@ -17,21 +16,6 @@ public class NativeStructureFailureContractTest {
Bootstrap.bootStrap();
}
@Test
public void globalStructureDisableRefusesGeneratorBinding() {
IrisModdedChunkGenerator.requireGlobalStructureGeneration(true, "overworld:overworld");
IllegalStateException error = assertThrows(
IllegalStateException.class,
() -> IrisModdedChunkGenerator.requireGlobalStructureGeneration(
false, "overworld:overworld"));
assertTrue(error.getMessage().contains("overworld:overworld"));
assertTrue(error.getMessage().contains("generate-structures=false"));
assertTrue(error.getMessage().contains("importedStructures.disabled"));
assertTrue(error.getMessage().contains("importedStructures.disabledExact"));
}
@Test
public void structureFailurePreservesPhaseIdentityChunkAndCause() {
IllegalArgumentException cause = new IllegalArgumentException("broken placement");
@@ -118,14 +118,6 @@ public final class BukkitStructureStagePolicy implements StructureStagePolicy<
return ((IrisMaterialPalette) source).get(rng, x, y, z, engine.getData());
}
@Override
public RuntimeException structuresDisabled(int chunkX, int chunkZ) {
return new IllegalStateException("Iris cannot generate native structures in chunk "
+ chunkX + "," + chunkZ
+ " because structure generation is disabled outside the pack; enable native structure generation "
+ "and deny families through importedStructures.disabled or complete keys through importedStructures.disabledExact");
}
@Override
public String generationLabel(String structureId) {
return "Iris native structure " + structureId;