From 5cadd9b8c4fa4a57304624a86b3159d6c4f06a2e Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 29 Sep 2026 06:04:19 -0400 Subject: [PATCH] Deferred saved biome updates - Defers a chunk update pass while saved biome information is loading and retries it on a later pass - Forgets only the updates already applied so a retry never refills a chest - Propagates engine failures from loot application instead of marking the chunk done --- .../service/ModdedChunkUpdateService.java | 44 ++++++++-- .../service/ModdedChunkUpdateServiceTest.java | 88 +++++++++++++++++++ 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java index e29deb1c7..884174eb3 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java @@ -30,6 +30,7 @@ import art.arcane.iris.studio.view.PregeneratorJob; import art.arcane.iris.integration.Identifier; import art.arcane.iris.generation.cache.Cache; import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.GenerationFailures; import art.arcane.iris.generation.block.TileData; import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.ModdedBlockResolution; @@ -38,6 +39,7 @@ import art.arcane.iris.modded.ModdedTileData; import art.arcane.iris.modded.api.ModdedCustomContentRegistry; import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement; import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.world.history.SavedBiomeUnavailableException; import art.arcane.iris.world.storage.matter.TileWrapper; import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.runtime.Mantle; @@ -47,6 +49,7 @@ import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterUpdate; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer; +import it.unimi.dsi.fastutil.ints.IntArrayList; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -195,6 +198,11 @@ public final class ModdedChunkUpdateService implements ModdedTickableService { chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, () -> runCustomPass(engine, level, chunkX, chunkZ, chunk)); chunk.raiseFlagUnchecked(MantleFlag.UPDATE, () -> runUpdatePass(engine, level, chunkX, chunkZ, chunk)); }); + } catch (SavedBiomeUnavailableException unavailable) { + if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) { + throw unavailable; + } + IrisLogging.debug("Iris chunk update deferred at " + chunkX + "," + chunkZ + ": saved biomes loading"); } finally { chunk.release(); } @@ -315,15 +323,40 @@ public final class ModdedChunkUpdateService implements ModdedTickableService { } } - chunk.iterate(MatterUpdate.class, (Integer x, Integer yf, Integer z, MatterUpdate v) -> { - if (v != null && v.isUpdate()) { - update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk); - } - }); + IntArrayList completed = new IntArrayList(); + try { + chunk.iterate(MatterUpdate.class, (Integer x, Integer yf, Integer z, MatterUpdate v) -> { + if (v != null && v.isUpdate()) { + update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk); + completed.add((yf << 8) | ((x & 15) << 4) | (z & 15)); + } + }); + } catch (RuntimeException | Error failure) { + forgetCompletedUpdates(chunk, completed, failure); + throw failure; + } chunk.deleteSlices(MatterUpdate.class); engine.getMetrics().getUpdates().put(stopwatch.getMilliseconds()); } + private static void forgetCompletedUpdates(MantleChunk chunk, IntArrayList completed, Throwable failure) { + try { + for (int index = 0; index < completed.size(); index++) { + int position = completed.getInt(index); + int y = position >>> 8; + Matter section = chunk.get(y >> 4); + if (section != null && section.hasSlice(MatterUpdate.class)) { + section.getSlice(MatterUpdate.class) + .set((position >> 4) & 15, y & 15, position & 15, null); + } + } + } catch (RuntimeException | Error cleanupFailure) { + if (cleanupFailure != failure) { + failure.addSuppressed(cleanupFailure); + } + } + } + private void update(Engine engine, NativeWorld level, int x, int y, int z, int baseX, int baseZ, MantleChunk chunk) { if (y < level.minHeight() || y >= level.maxHeight() - 1) { return; @@ -338,6 +371,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService { try { ModdedLootApplier.apply(engine, level, pos, state, chunk); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } } else { diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedChunkUpdateServiceTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedChunkUpdateServiceTest.java index 0e0279392..5b69b0b62 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedChunkUpdateServiceTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedChunkUpdateServiceTest.java @@ -4,13 +4,17 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedPlatformWorld; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.EngineMetrics; +import art.arcane.iris.modded.ModdedLootApplier; import art.arcane.iris.world.IrisWorld; +import art.arcane.iris.world.history.SavedBiomeUnavailableException; import art.arcane.iris.world.storage.matter.TileWrapper; +import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint; import art.arcane.volmlib.util.function.Consumer4; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter; import art.arcane.volmlib.util.mantle.runtime.MantleHooks; import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.volmlib.util.matter.MatterUpdate; import art.arcane.volmlib.util.matter.slices.UpdateMatter; import net.minecraft.SharedConstants; @@ -24,6 +28,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.FlowingFluid; import org.junit.Test; import org.mockito.InOrder; +import org.mockito.MockedStatic; import java.lang.reflect.Proxy; import java.util.ArrayList; @@ -31,12 +36,17 @@ import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class ModdedChunkUpdateServiceTest { @@ -70,6 +80,44 @@ public class ModdedChunkUpdateServiceTest { assertTrue(chunk.deleted()); } + @Test + public void loadingSavedBiomeDefersThePassAndForgetsOnlyCompletedLoot() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + IrisWorld world = mock(IrisWorld.class); + when(world.minHeight()).thenReturn(-64); + Engine engine = mock(Engine.class); + when(engine.getWorld()).thenReturn(world); + when(engine.getMetrics()).thenReturn(new EngineMetrics(8)); + ServerLevel level = mock(ServerLevel.class); + when(level.getMinY()).thenReturn(-64); + when(level.getMaxY()).thenReturn(319); + when(level.getHeight()).thenReturn(384); + when(level.getBlockState(any(BlockPos.class))).thenReturn(Blocks.CHEST.defaultBlockState()); + ChestUpdateMantleChunk chunk = new ChestUpdateMantleChunk(); + SavedBiomeUnavailableException loading = new SavedBiomeUnavailableException( + "Saved biome information is loading at chunk 0,0. Try again shortly.", true); + + try (MockedStatic loot = mockStatic(ModdedLootApplier.class)) { + loot.when(() -> ModdedLootApplier.apply(any(), any(), any(), any(), any())).thenAnswer(invocation -> { + NativeBlockPoint position = invocation.getArgument(2); + if (position.x() == 2) { + throw loading; + } + return null; + }); + + SavedBiomeUnavailableException thrown = assertThrows(SavedBiomeUnavailableException.class, + () -> new ModdedChunkUpdateService().runUpdatePass( + engine, new ModdedPlatformWorld(level), 0, 0, chunk)); + + assertSame(loading, thrown); + } + assertFalse(chunk.deleted()); + verify(chunk.slice).set(1, 5, 1, null); + verify(chunk.slice, never()).set(2, 5, 2, null); + } + @Test public void scansWhenPlayersArePresent() { assertTrue(ModdedChunkUpdateService.hasUpdateTargets(true, false)); @@ -166,6 +214,46 @@ public class ModdedChunkUpdateServiceTest { } } + private static final class ChestUpdateMantleChunk extends MantleChunk { + @SuppressWarnings("unchecked") + private final MatterSlice slice = mock(MatterSlice.class); + private final Matter section = mock(Matter.class); + private boolean deleted; + + private ChestUpdateMantleChunk() { + super(1, 0, 0, emptyAdapter(), MantleHooks.NONE); + when(section.hasSlice(MatterUpdate.class)).thenReturn(true); + when(section.getSlice(MatterUpdate.class)).thenReturn(slice); + } + + @Override + public Matter get(int index) { + return section; + } + + @Override + public void iterate( + Class type, + Consumer4 iterator + ) { + if (type == MatterUpdate.class) { + iterator.accept(1, 5, 1, type.cast(UpdateMatter.ON)); + iterator.accept(2, 5, 2, type.cast(UpdateMatter.ON)); + } + } + + @Override + public void deleteSlices(Class type) { + if (type == MatterUpdate.class) { + deleted = true; + } + } + + private boolean deleted() { + return deleted; + } + } + @SuppressWarnings("unchecked") private static MantleDataAdapter emptyAdapter() { return (MantleDataAdapter) Proxy.newProxyInstance(