From 8757355174c0fd8be7385114ce40015ca7d44fa8 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 13 Oct 2026 11:11:02 -0400 Subject: [PATCH] Removed empty floating work - Objectless floating placements no longer expand mantle generation windows - Empty floating object passes finish without column sampling - Tests cover placement reach, reload, failure fallback and completion flags --- .../mantle/MantleFloatingObjectComponent.java | 17 +- ...antleFloatingObjectComponentEmptyTest.java | 275 ++++++++++++++++++ 2 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponentEmptyTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java index 172478b39..40e050675 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java @@ -53,6 +53,7 @@ import java.util.Set; @ComponentFlag(ReservedFlag.FLOATING_OBJECT) public class MantleFloatingObjectComponent extends IrisMantleComponent { + private static final int MINIMUM_PLACEMENT_RADIUS = 16; private static final int INVERTED_PICK_ATTEMPTS = 8; private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0); @@ -62,6 +63,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent { @Override public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + if (getRadius() == 0) { + return; + } IrisComplex complex = context.getComplex(); if (!complex.allowsNewGenerationChunk(x, z)) { return; @@ -593,7 +597,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent { @Override protected int computeRadius() { - int maxObjectExtent = 16; + int maxObjectExtent = 0; Map sizeCache = new HashMap<>(); Set warnedLargeObjects = new HashSet<>(); try { @@ -613,16 +617,20 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent { if (target != null) { maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveTopObjects(target), data, sizeCache, warnedLargeObjects)); maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects)); + } else { + maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS); } } catch (Throwable e) { GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); + maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS); } } } } catch (Throwable e) { GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); + maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS); } return maxObjectExtent; } @@ -630,10 +638,13 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent { private int computePlacementRadius(KList placements, IrisData data, Map sizeCache, Set warnedLargeObjects) { - int radius = 0; if (placements == null) { - return radius; + return MINIMUM_PLACEMENT_RADIUS; } + if (placements.isEmpty()) { + return 0; + } + int radius = MINIMUM_PLACEMENT_RADIUS; for (IrisObjectPlacement placement : placements) { if (placement == null || placement.getPlace() == null) { continue; diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponentEmptyTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponentEmptyTest.java new file mode 100644 index 000000000..9d743a033 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponentEmptyTest.java @@ -0,0 +1,275 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.generation.biome.FloatingIslandBoundarySampler; +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.biome.IrisFloatingChildBiomes; +import art.arcane.iris.generation.context.ChunkContext; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.runtime.SeedManager; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.pack.loading.ResourceLoader; +import art.arcane.iris.structure.object.IrisObject; +import art.arcane.iris.structure.object.IrisObjectPlacement; +import art.arcane.iris.testsupport.PlatformBinding; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.flag.ReservedFlag; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +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 org.junit.ClassRule; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public class MantleFloatingObjectComponentEmptyTest { + @ClassRule + public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform(); + + @Test + public void absentFloatingObjectsDoNotReadColumnsOrWriteMatter() { + Fixture fixture = new Fixture(new IrisBiome()); + MantleWriter writer = mock(MantleWriter.class); + ChunkContext context = mock(ChunkContext.class); + + assertEquals(0, fixture.component.getRadius()); + fixture.component.generateLayer(writer, 4, -6, context); + + verifyNoInteractions(writer, context); + } + + @Test + public void floatingTerrainWithoutObjectsHasNoObjectReach() { + Fixture fixture = new Fixture(floatingBiome(new IrisFloatingChildBiomes())); + + assertEquals(0, fixture.component.getRadius()); + } + + @Test + public void explicitPlacementListsKeepTheirMinimumReachWhenObjectsAreMissing() { + IrisObjectPlacement placement = new IrisObjectPlacement().setPlace(new KList<>("test/missing")); + List entries = List.of( + new IrisFloatingChildBiomes().setFloatingObjects(new KList<>(placement)), + new IrisFloatingChildBiomes().setExtraObjects(new KList<>(placement)), + new IrisFloatingChildBiomes().setTopObjectOverrides(new KList<>(placement)), + new IrisFloatingChildBiomes().setBottomObjectOverrides(new KList<>(placement)) + ); + + for (IrisFloatingChildBiomes entry : entries) { + Fixture fixture = new Fixture(floatingBiome(entry)); + assertEquals(16, fixture.component.getRadius()); + } + } + + @Test + public void inheritedParentObjectsKeepTheirMinimumReach() { + IrisBiome parent = floatingBiome(new IrisFloatingChildBiomes()) + .setObjects(new KList<>(new IrisObjectPlacement())); + Fixture fixture = new Fixture(parent); + + assertEquals(16, fixture.component.getRadius()); + } + + @Test + public void inheritedTargetObjectsKeepTheirMinimumReach() { + IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes().setBiome("test/target"); + Fixture fixture = new Fixture(floatingBiome(entry)); + IrisBiome target = new IrisBiome().setObjects(new KList<>(new IrisObjectPlacement())); + when(fixture.biomes.load("test/target")).thenReturn(target); + + assertEquals(16, fixture.component.getRadius()); + } + + @Test + public void disabledInheritanceLeavesObjectlessFloatingTerrainEmpty() { + IrisBiome parent = floatingBiome(new IrisFloatingChildBiomes().setInheritObjects(false)) + .setObjects(new KList<>(new IrisObjectPlacement())); + Fixture fixture = new Fixture(parent); + + assertEquals(0, fixture.component.getRadius()); + } + + @Test + public void hotloadRechecksObjectsAcrossPackChanges() { + Fixture fixture = new Fixture(new IrisBiome()); + assertEquals(0, fixture.component.getRadius()); + + IrisBiome active = floatingBiome(new IrisFloatingChildBiomes() + .setExtraObjects(new KList<>(new IrisObjectPlacement()))); + when(fixture.dimension.getReachableBiomes(any())).thenReturn(new KList<>(active)); + fixture.component.hotload(); + assertEquals(16, fixture.component.getRadius()); + + when(fixture.dimension.getReachableBiomes(any())).thenReturn(new KList<>(new IrisBiome())); + fixture.component.hotload(); + assertEquals(0, fixture.component.getRadius()); + assertEquals(0, fixture.component.getRadius()); + verify(fixture.dimension, times(3)).getReachableBiomes(any()); + } + + @Test + public void failedReachabilityScanKeepsTheComponentActive() { + Fixture fixture = new Fixture(new IrisBiome()); + when(fixture.dimension.getReachableBiomes(any())).thenThrow(new IllegalStateException("Unavailable biome")); + + assertEquals(16, fixture.component.getRadius()); + } + + @Test + public void failedTargetResolutionKeepsTheComponentActive() { + IrisFloatingChildBiomes entry = spy(new IrisFloatingChildBiomes()); + Fixture fixture = new Fixture(floatingBiome(entry)); + doThrow(new IllegalStateException("Unavailable floating biome")).when(entry).getRealBiome(any(), any()); + + assertEquals(16, fixture.component.getRadius()); + } + + @Test + public void unresolvedTargetKeepsTheComponentActive() { + IrisFloatingChildBiomes entry = spy(new IrisFloatingChildBiomes()); + Fixture fixture = new Fixture(floatingBiome(entry)); + doReturn(null).when(entry).getRealBiome(any(), any()); + + assertEquals(16, fixture.component.getRadius()); + } + + @Test + public void unresolvedPlacementListKeepsTheComponentActive() { + Fixture fixture = new Fixture(floatingBiome(new IrisFloatingChildBiomes().setExtraObjects(null))); + + assertEquals(16, fixture.component.getRadius()); + } + + @Test + public void emptyComponentShrinksTheWindowAndPreservesCompletionFlags() { + Fixture compact = new Fixture(new IrisBiome()); + Fixture conservative = new Fixture(new IrisBiome()); + MantleFloatingObjectComponent conservativeComponent = new ConservativeFloatingObjects(conservative.engineMantle); + + GenerationResult compactResult = compact.generate(compact.component); + GenerationResult conservativeResult = conservative.generate(conservativeComponent); + + assertEquals(9, compactResult.carveCalls()); + assertEquals(25, conservativeResult.carveCalls()); + assertEquals(0, compact.columnReads.get()); + assertEquals(2304, conservative.columnReads.get()); + assertEquals(9, compactResult.loadedChunks()); + assertEquals(25, conservativeResult.loadedChunks()); + for (MantleFlag flag : List.of(ReservedFlag.CARVED, ReservedFlag.FLOATING_OBJECT, MantleFlag.PLANNED)) { + assertTrue(compactResult.center().isFlagged(flag)); + assertEquals(conservativeResult.center().isFlagged(flag), compactResult.center().isFlagged(flag)); + } + } + + private static IrisBiome floatingBiome(IrisFloatingChildBiomes entry) { + return new IrisBiome().setFloatingChildBiomes(new KList<>(entry)); + } + + private static final class Fixture { + private final IrisDimension dimension = mock(IrisDimension.class); + private final Engine engine = mock(Engine.class); + private final EngineMantle engineMantle = mock(EngineMantle.class); + private final IrisComplex complex = mock(IrisComplex.class); + private final ChunkContext context = mock(ChunkContext.class); + private final ResourceLoader biomes; + private final AtomicInteger columnReads = new AtomicInteger(); + private final MantleFloatingObjectComponent component; + + @SuppressWarnings("unchecked") + private Fixture(IrisBiome biome) { + biomes = mock(ResourceLoader.class); + ResourceLoader objects = mock(ResourceLoader.class); + IrisData data = mock(IrisData.class); + when(data.getBiomeLoader()).thenReturn(biomes); + when(data.getObjectLoader()).thenReturn(objects); + when(dimension.isUseMantle()).thenReturn(true); + when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome)); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getHeight()).thenReturn(256); + when(engine.getSeedManager()).thenReturn(mock(SeedManager.class)); + when(engine.getMantle()).thenReturn(engineMantle); + when(engineMantle.getEngine()).thenReturn(engine); + when(engineMantle.getData()).thenReturn(data); + when(engineMantle.getComplex()).thenReturn(complex); + when(complex.allowsNewGenerationChunk(anyInt(), anyInt())).thenReturn(true); + when(complex.allowsMantleChunkWrite(anyInt(), anyInt())).thenReturn(true); + when(context.getComplex()).thenReturn(complex); + when(context.getFloatingIslandBoundarySampler()).thenReturn(new FloatingIslandBoundarySampler((x, z) -> { + columnReads.incrementAndGet(); + return biome; + })); + component = new MantleFloatingObjectComponent(engineMantle); + } + + @SuppressWarnings("unchecked") + private GenerationResult generate(MantleFloatingObjectComponent floating) { + Map> chunks = new HashMap<>(); + MantleDataAdapter adapter = mock(MantleDataAdapter.class); + Mantle mantle = mock(Mantle.class); + when(mantle.useChunk(anyInt(), anyInt())).thenAnswer(invocation -> { + int x = invocation.getArgument(0); + int z = invocation.getArgument(1); + long key = ((long) x << 32) ^ (z & 0xffffffffL); + return chunks.computeIfAbsent(key, ignored -> new MantleChunk<>(16, x, z, adapter, MantleHooks.NONE)).use(); + }); + MantleComponent carving = mock(MantleComponent.class); + when(carving.isEnabled()).thenReturn(true); + when(carving.getFlag()).thenReturn(ReservedFlag.CARVED); + when(carving.getOutputRadius()).thenReturn(1); + when(carving.getGenerationPhase()).thenReturn(MatterGenerationPhase.TERRAIN); + when(carving.getPrerequisiteFlags()).thenReturn(MantleComponent.EMPTY_PREREQUISITES); + AtomicInteger carveCalls = new AtomicInteger(); + doAnswer(invocation -> { + carveCalls.incrementAndGet(); + return null; + }).when(carving).generateLayer(any(), anyInt(), anyInt(), any()); + MatterGenerator generator = mock(MatterGenerator.class, CALLS_REAL_METHODS); + when(generator.getEngine()).thenReturn(engine); + when(generator.getMantle()).thenReturn(mantle); + when(generator.getComponents()).thenReturn(List.of( + new MantlePass(List.of(carving), 0, 0), + new MantlePass(List.of(floating), 0, 0) + )); + + generator.generateMatter(0, 0, false, context); + + return new GenerationResult(chunks.get(0L), carveCalls.get(), chunks.size()); + } + } + + private static final class ConservativeFloatingObjects extends MantleFloatingObjectComponent { + private ConservativeFloatingObjects(EngineMantle engineMantle) { + super(engineMantle); + } + + @Override + protected int computeRadius() { + return Math.max(16, super.computeRadius()); + } + } + + private record GenerationResult(MantleChunk center, int carveCalls, int loadedChunks) { + } +}