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
This commit is contained in:
Brian Neumann-Fopiano
2026-09-29 06:50:24 -04:00
parent dd4633ad3a
commit ea516b4a59
5 changed files with 87 additions and 80 deletions
@@ -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;
}
}
@@ -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;
}
@@ -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<NativeBlockState> vblocks, Hunk<NativeBiome> 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<Matter> 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<Matter> 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;
@@ -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<NativeBlockState> blocks = mock(Hunk.class);
Hunk<NativeBiome> 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);
}
}