From ea516b4a590ec0f8d7f6468e100371d5e1da3ee4 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 29 Sep 2026 06:50:18 -0400 Subject: [PATCH] Refused maintenance generation - A chunk is refused while Folia world maintenance forbids mantle stages - Such chunks no longer end up missing objects, caves, deposits or their REAL flag - Refused chunks warn at most every 5 seconds - The overworld mode no longer bypasses mantle stages --- .../runtime/BukkitEnginePlatformHooks.java | 2 +- .../iris/generation/mode/ModeOverworld.java | 73 ++----------------- .../runtime/EnginePlatformHooks.java | 6 +- .../iris/generation/runtime/IrisEngine.java | 37 +++++++--- .../IrisEngineMaintenanceRefusalTest.java | 49 +++++++++++++ 5 files changed, 87 insertions(+), 80 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineMaintenanceRefusalTest.java diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/world/runtime/BukkitEnginePlatformHooks.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/world/runtime/BukkitEnginePlatformHooks.java index c1559165a..8933cdc93 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/world/runtime/BukkitEnginePlatformHooks.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/world/runtime/BukkitEnginePlatformHooks.java @@ -189,7 +189,7 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks { } @Override - public boolean shouldBypassMantleStages(Engine engine) { + public boolean shouldRefuseGeneration(Engine engine) { if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) { return false; } diff --git a/core/src/main/java/art/arcane/iris/generation/mode/ModeOverworld.java b/core/src/main/java/art/arcane/iris/generation/mode/ModeOverworld.java index d0fe437c9..61216e507 100644 --- a/core/src/main/java/art/arcane/iris/generation/mode/ModeOverworld.java +++ b/core/src/main/java/art/arcane/iris/generation/mode/ModeOverworld.java @@ -23,7 +23,6 @@ import art.arcane.iris.generation.stage.IrisDecorantActuator; import art.arcane.iris.generation.stage.IrisTerrainNormalActuator; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.EngineMode; -import art.arcane.iris.generation.runtime.EnginePlatformHooks; import art.arcane.iris.generation.runtime.EngineStage; import art.arcane.iris.generation.runtime.IrisEngineMode; import art.arcane.iris.generation.stage.IrisCarveModifier; @@ -32,17 +31,10 @@ import art.arcane.iris.generation.stage.IrisDepositModifier; import art.arcane.iris.generation.stage.IrisFloatingChildBiomeModifier; import art.arcane.iris.generation.stage.IrisPerfectionModifier; import art.arcane.iris.generation.stage.IrisPostModifier; -import art.arcane.iris.spi.IrisLogging; - -import java.util.concurrent.atomic.AtomicLong; public class ModeOverworld extends IrisEngineMode implements EngineMode { - private static final AtomicLong lastMaintenanceBypassLog = new AtomicLong(0L); - private final EnginePlatformHooks platformHooks; - public ModeOverworld(Engine engine) { super(engine); - platformHooks = engine.getPlatformHooks(); IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine()); IrisBiomeActuator biome = new IrisBiomeActuator(getEngine()); IrisDecorantActuator decorant = new IrisDecorantActuator(getEngine()); @@ -53,51 +45,17 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { IrisCustomModifier custom = new IrisCustomModifier(getEngine()); IrisFloatingChildBiomeModifier floatingChildBiomes = new IrisFloatingChildBiomeModifier(getEngine()); EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c); - EngineStage sGenMatter = (x, z, k, p, m, c) -> { - if (shouldBypassMantleStages()) { - return; - } - generateTerrainMatter( - x >> 4, - z >> 4, - m || getEngine().isStudio(), - c); - }; + EngineStage sGenMatter = (x, z, k, p, m, c) -> generateTerrainMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c); EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c); EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c); - EngineStage sCave = (x, z, k, p, m, c) -> { - if (shouldBypassMantleStages()) { - return; - } - cave.modify(x >> 4, z >> 4, k, m, c); - }; - EngineStage sDeposit = (x, z, k, p, m, c) -> { - if (shouldBypassMantleStages()) { - return; - } - deposit.modify(x, z, k, m, c); - }; - EngineStage sPost = (x, z, k, p, m, c) -> { - if (shouldBypassMantleStages()) { - return; - } - post.modify(x, z, k, m, c); - }; - EngineStage sInsertMatter = (x, z, K, p, m, c) -> { - if (shouldBypassMantleStages()) { - return; - } - getMantle().insertMatter(x >> 4, z >> 4, K, m, c); - }; + EngineStage sCave = (x, z, k, p, m, c) -> cave.modify(x >> 4, z >> 4, k, m, c); + EngineStage sDeposit = (x, z, k, p, m, c) -> deposit.modify(x, z, k, m, c); + EngineStage sPost = (x, z, k, p, m, c) -> post.modify(x, z, k, m, c); + EngineStage sInsertMatter = (x, z, K, p, m, c) -> getMantle().insertMatter(x >> 4, z >> 4, K, m, c); EngineStage sFloatingTerrainSolid = (x, z, k, p, m, c) -> floatingChildBiomes.modify(x, z, k, m, c); EngineStage sFloatingDecorate = (x, z, k, p, m, c) -> floatingChildBiomes.decorateColumns(x, z, k, m, c); EngineStage sPerfection = (x, z, k, p, m, c) -> perfection.modify(x, z, k, m, c); - EngineStage sCustom = (x, z, k, p, m, c) -> { - if (shouldBypassMantleStages()) { - return; - } - custom.modify(x, z, k, m, c); - }; + EngineStage sCustom = (x, z, k, p, m, c) -> custom.modify(x, z, k, m, c); // Matter runs on the calling thread so its window fans out across the burst pool (a pool // thread would run every chunk of the window inline) while biome and terrain run alongside. @@ -114,11 +72,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { // the surface sInsertMatter writes), so parallel order is scheduler-dependent. The // production path already runs them inline in this order; sequential registration // makes studio (the only multicore path) match production and the goldenhash baseline. - registerStage((x, z, k, p, m, c) -> { - if (!shouldBypassMantleStages()) { - generateContentMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c); - } - }); + registerStage((x, z, k, p, m, c) -> generateContentMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c)); registerStage(sDeposit); registerStage(sInsertMatter); registerStage(sDecorant); @@ -128,17 +82,4 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { registerStage(sCustom); } } - - private boolean shouldBypassMantleStages() { - boolean active = platformHooks.shouldBypassMantleStages(getEngine()); - if (active) { - long now = System.currentTimeMillis(); - long last = lastMaintenanceBypassLog.get(); - if (now - last >= 5000L && lastMaintenanceBypassLog.compareAndSet(last, now)) { - IrisLogging.info("Maintenance regen bypass: skipping mantle-backed overworld stages for Folia safety."); - } - } - return active; - } - } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EnginePlatformHooks.java b/core/src/main/java/art/arcane/iris/generation/runtime/EnginePlatformHooks.java index 44661074d..f8dabff8c 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/EnginePlatformHooks.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EnginePlatformHooks.java @@ -94,7 +94,11 @@ public interface EnginePlatformHooks { return false; } - default boolean shouldBypassMantleStages(Engine engine) { + /** + * True while world maintenance forbids the mantle-backed stages. Generating without them would persist a + * chunk with no objects, caves or deposits, so the engine refuses the chunk instead. + */ + default boolean shouldRefuseGeneration(Engine engine) { return false; } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java index b89324d35..905f4ff78 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java @@ -86,6 +86,8 @@ import java.util.function.Supplier; @Data public class IrisEngine implements Engine { static final long SESSION_DRAIN_TIMEOUT_MILLIS = 15000L; + private static final long MAINTENANCE_REFUSAL_WARNING_MILLIS = 5000L; + private static final AtomicLong lastMaintenanceRefusalWarning = new AtomicLong(); private final AtomicInteger bud; private final AtomicInteger buds; @@ -1485,6 +1487,9 @@ public class IrisEngine implements Engine { @Override public void generate(int x, int z, Hunk vblocks, Hunk vbiomes, boolean multicore) throws WrongEngineBroException { awaitGenerationCacheWarm(); + if (platformHooks.shouldRefuseGeneration(this)) { + refuseMaintenanceGeneration(x >> 4, z >> 4); + } try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openGenerationHistoryCoordinateScope(x, z); GenerationSessionLease lease = acquireGenerationLease("chunk_generate"); @@ -1505,20 +1510,17 @@ public class IrisEngine implements Engine { activeMode.generate(x, z, blocks, vbiomes, multicore, lease.sessionId()); } - boolean skipRealFlag = platformHooks.shouldBypassMantleStages(this); - if (!skipRealFlag) { - MantleChunk chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4); - try { - synchronized (chunk) { - Matter section = chunk.get(0); - if (section != null) { - section.deleteSlice(ObjectContinuationBundle.class); - } - chunk.flag(MantleFlag.REAL, true); + MantleChunk chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4); + try { + synchronized (chunk) { + Matter section = chunk.get(0); + if (section != null) { + section.deleteSlice(ObjectContinuationBundle.class); } - } finally { - chunk.release(); + chunk.flag(MantleFlag.REAL, true); } + } finally { + chunk.release(); } getMetrics().getTotal().put(p.getMilliseconds()); generated.incrementAndGet(); @@ -1538,6 +1540,17 @@ public class IrisEngine implements Engine { } } + private void refuseMaintenanceGeneration(int chunkX, int chunkZ) throws GenerationSessionException { + long now = System.currentTimeMillis(); + long last = lastMaintenanceRefusalWarning.get(); + if (now - last >= MAINTENANCE_REFUSAL_WARNING_MILLIS && lastMaintenanceRefusalWarning.compareAndSet(last, now)) { + IrisLogging.warn("Iris refused chunk " + chunkX + "," + chunkZ + " in " + getWorld().name() + + ": world maintenance forbids mantle generation, the chunk stays ungenerated"); + } + throw new GenerationSessionException("Iris refused chunk " + chunkX + "," + chunkZ + " in " + + getWorld().name() + " during world maintenance.", true); + } + @Override public GenerationSessionManager getGenerationSessions() { return generationSessions; diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineMaintenanceRefusalTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineMaintenanceRefusalTest.java new file mode 100644 index 000000000..e8937edbd --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineMaintenanceRefusalTest.java @@ -0,0 +1,49 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.world.IrisWorld; +import art.arcane.volmlib.nativelib.terrain.NativeBiome; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.util.hunk.Hunk; +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public class IrisEngineMaintenanceRefusalTest { + @Test + @SuppressWarnings("unchecked") + public void maintenanceThatCannotRunMantleStagesRefusesTheChunkInsteadOfGeneratingIt() throws Exception { + IrisEngine engine = mock(IrisEngine.class); + EnginePlatformHooks hooks = mock(EnginePlatformHooks.class); + when(hooks.shouldRefuseGeneration(engine)).thenReturn(true); + Field platformHooks = IrisEngine.class.getDeclaredField("platformHooks"); + platformHooks.setAccessible(true); + platformHooks.set(engine, hooks); + IrisWorld world = mock(IrisWorld.class); + when(world.name()).thenReturn("world"); + when(engine.getWorld()).thenReturn(world); + doCallRealMethod().when(engine).generate(anyInt(), anyInt(), any(), any(), anyBoolean()); + Hunk blocks = mock(Hunk.class); + Hunk biomes = mock(Hunk.class); + + GenerationSessionException refused = assertThrows(GenerationSessionException.class, + () -> engine.generate(32, 48, blocks, biomes, false)); + + assertTrue(refused.isExpectedTeardown()); + verify(engine, never()).acquireGenerationLease(anyString()); + verify(engine, never()).getMode(); + verifyNoInteractions(blocks, biomes); + } +}