From 184a04d97f1dd9ded6721d239c954149074f3d46 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:11:28 -0400 Subject: [PATCH] Snapshot cave terrain per chunk - sCave copies the chunk's cavern and hydrology terrain view once (journal originals win) instead of per-voxel monitor, journal and slice lookups - hydrology lookups answer null without work in chunks with no cave cells - decorateNaturalCaves reuses the sCave snapshot through the chunk context --- .../iris/generation/context/ChunkContext.java | 10 + .../mantle/CaveTerrainSnapshot.java | 220 ++++++++++++++++++ .../generation/mantle/TerrainMatterView.java | 12 + .../iris/generation/mode/ModeOverworld.java | 2 +- .../generation/stage/IrisCarveModifier.java | 210 ++++++++--------- .../mantle/CaveTerrainSnapshotTest.java | 175 ++++++++++++++ .../stage/IrisCarveBoundaryFacesTest.java | 6 +- .../IrisCarveModifierBoundarySupportTest.java | 25 +- ...isCarveModifierInferenceIsolationTest.java | 5 +- .../stage/IrisNaturalCaveScanTest.java | 14 +- 10 files changed, 542 insertions(+), 137 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshot.java create mode 100644 core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java b/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java index 8135a3c9c..62270dc8a 100644 --- a/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java +++ b/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java @@ -8,6 +8,7 @@ import art.arcane.iris.generation.runtime.EngineMetrics; import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.generation.biome.FloatingIslandBoundarySampler; import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.world.history.FloatingBiomeOverlay; import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; @@ -23,6 +24,7 @@ public class ChunkContext { private boolean naturalTerrain = true; private Hunk terrainBiomes; private FloatingBiomeOverlay floatingBiomes; + private CaveTerrainSnapshot caveTerrain; private final int x; private final int z; private final IrisComplex complex; @@ -157,6 +159,14 @@ public class ChunkContext { return floatingBiomes; } + public CaveTerrainSnapshot getCaveTerrain() { + return caveTerrain; + } + + public void setCaveTerrain(CaveTerrainSnapshot caveTerrain) { + this.caveTerrain = caveTerrain; + } + public void setNaturalBiome(int localX, int y, int localZ, NativeBiome biome) { if (terrainBiomes != null) { terrainBiomes.setRaw(localX, y, localZ, biome); diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshot.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshot.java new file mode 100644 index 000000000..743b978d5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshot.java @@ -0,0 +1,220 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; +import art.arcane.iris.world.storage.matter.PreObjectMatterCell; +import art.arcane.volmlib.util.hunk.bits.DataContainer; +import art.arcane.volmlib.util.hunk.storage.MappedHunk; +import art.arcane.volmlib.util.hunk.storage.PaletteOrHunk; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; + +import java.util.Map; +import java.util.Objects; +import java.util.function.IntFunction; + +/** + * The terrain view of one mantle chunk's cavern and hydrology cells (journaled pre-object values win), + * copied once under the chunk monitor. Terrain components finish before any content write reaches a + * chunk and every later write journals the value it replaces, so this view never goes stale while the + * chunk generates. + */ +public final class CaveTerrainSnapshot { + private static final int SECTION_VOLUME = 4096; + + private final int chunkX; + private final int chunkZ; + private final MatterCavern[][] caverns; + private final HydrologyCaveCell[][] hydrology; + private boolean hydrologyPresent; + + private CaveTerrainSnapshot(int chunkX, int chunkZ, int sections) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + caverns = new MatterCavern[sections][]; + hydrology = new HydrologyCaveCell[sections][]; + } + + public static CaveTerrainSnapshot capture(MantleChunk chunk, int chunkX, int chunkZ) { + Objects.requireNonNull(chunk, "Terrain mantle chunk"); + synchronized (chunk) { + int sections = chunk.sectionCount(); + CaveTerrainSnapshot snapshot = new CaveTerrainSnapshot(chunkX, chunkZ, sections); + for (int section = 0; section < sections; section++) { + if (!chunk.exists(section)) { + continue; + } + Matter matter = chunk.get(section); + if (matter != null) { + snapshot.captureSection(section, matter); + } + } + return snapshot; + } + } + + public boolean covers(int chunkX, int chunkZ) { + return this.chunkX == chunkX && this.chunkZ == chunkZ; + } + + public boolean hasHydrology() { + return hydrologyPresent; + } + + public MatterCavern cavern(int x, int y, int z) { + if (y < 0) { + return null; + } + int section = y >> 4; + if (section >= caverns.length) { + return null; + } + MatterCavern[] values = caverns[section]; + return values == null ? null : values[index(x, y, z)]; + } + + public HydrologyCaveCell hydrology(int x, int y, int z) { + if (!hydrologyPresent || y < 0) { + return null; + } + int section = y >> 4; + if (section >= hydrology.length) { + return null; + } + HydrologyCaveCell[] values = hydrology[section]; + return values == null ? null : values[index(x, y, z)]; + } + + public MatterCavern composedCavern(int x, int y, int z) { + HydrologyCaveCell cell = hydrology(x, y, z); + return cell != null ? cell.asCavern() : cavern(x, y, z); + } + + /** + * Visits every cell holding a cavern or a hydrology cell, section by section in slice position order. + */ + public void forEachCell(CellConsumer consumer) { + for (int section = 0; section < caverns.length; section++) { + MatterCavern[] sectionCaverns = caverns[section]; + HydrologyCaveCell[] sectionHydrology = hydrologyPresent ? hydrology[section] : null; + if (sectionCaverns == null && sectionHydrology == null) { + continue; + } + int baseY = section << 4; + for (int position = 0; position < SECTION_VOLUME; position++) { + MatterCavern cavern = sectionCaverns == null ? null : sectionCaverns[position]; + HydrologyCaveCell cell = sectionHydrology == null ? null : sectionHydrology[position]; + if (cavern != null || cell != null) { + consumer.accept(position & 15, baseY + ((position >> 4) & 15), position >> 8, cavern, cell); + } + } + } + } + + public MatterCavern[] composedFace(TerrainMatterView.Face face, int height) { + Objects.requireNonNull(face, "Terrain face"); + if (height < 0) { + throw new IllegalArgumentException("Terrain face height must be nonnegative"); + } + MatterCavern[] result = new MatterCavern[Math.multiplyExact(height, 16)]; + for (int y = 0; y < height; y++) { + for (int offset = 0; offset < 16; offset++) { + result[y * 16 + offset] = composedCavern(face.x(offset), y, face.z(offset)); + } + } + return result; + } + + private void captureSection(int section, Matter matter) { + MatterCavern[] sectionCaverns = copy(matter.getSlice(MatterCavern.class), MatterCavern[]::new); + HydrologyCaveCell[] sectionHydrology = copy(matter.getSlice(HydrologyCaveCell.class), HydrologyCaveCell[]::new); + MatterSlice journal = matter.getSlice(PreObjectMatterCell.class); + if (journal instanceof MappedHunk mapped) { + @SuppressWarnings("unchecked") + Map entries = (Map) mapped.getData(); + for (Map.Entry entry : entries.entrySet()) { + int position = entry.getKey(); + PreObjectMatterCell cell = entry.getValue(); + if (cell.cavernCaptured() && (sectionCaverns != null || cell.cavern() != null)) { + if (sectionCaverns == null) { + sectionCaverns = new MatterCavern[SECTION_VOLUME]; + } + sectionCaverns[position] = cell.cavern(); + } + if (cell.hydrologyCaptured() && (sectionHydrology != null || cell.hydrology() != null)) { + if (sectionHydrology == null) { + sectionHydrology = new HydrologyCaveCell[SECTION_VOLUME]; + } + sectionHydrology[position] = cell.hydrology(); + } + } + } else if (journal != null) { + for (int position = 0; position < SECTION_VOLUME; position++) { + PreObjectMatterCell cell = journal.get(position & 15, (position >> 4) & 15, position >> 8); + if (cell == null) { + continue; + } + if (cell.cavernCaptured() && (sectionCaverns != null || cell.cavern() != null)) { + if (sectionCaverns == null) { + sectionCaverns = new MatterCavern[SECTION_VOLUME]; + } + sectionCaverns[position] = cell.cavern(); + } + if (cell.hydrologyCaptured() && (sectionHydrology != null || cell.hydrology() != null)) { + if (sectionHydrology == null) { + sectionHydrology = new HydrologyCaveCell[SECTION_VOLUME]; + } + sectionHydrology[position] = cell.hydrology(); + } + } + } + caverns[section] = sectionCaverns; + if (sectionHydrology != null && containsAny(sectionHydrology)) { + hydrology[section] = sectionHydrology; + hydrologyPresent = true; + } + } + + @SuppressWarnings("unchecked") + private static T[] copy(MatterSlice slice, IntFunction allocator) { + if (slice == null) { + return null; + } + T[] values = allocator.apply(SECTION_VOLUME); + if (slice instanceof PaletteOrHunk storage && storage.isPalette() + && slice.getWidth() == 16 && slice.getHeight() == 16 && slice.getDepth() == 16) { + ((DataContainer) storage.palette()).copyAll(values); + return values; + } + int width = Math.min(16, slice.getWidth()); + int height = Math.min(16, slice.getHeight()); + int depth = Math.min(16, slice.getDepth()); + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + for (int z = 0; z < depth; z++) { + values[index(x, y, z)] = slice.get(x, y, z); + } + } + } + return values; + } + + private static boolean containsAny(Object[] values) { + for (Object value : values) { + if (value != null) { + return true; + } + } + return false; + } + + private static int index(int x, int y, int z) { + return ((z & 15) << 8) | ((y & 15) << 4) | (x & 15); + } + + @FunctionalInterface + public interface CellConsumer { + void accept(int x, int y, int z, MatterCavern cavern, HydrologyCaveCell hydrology); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/TerrainMatterView.java b/core/src/main/java/art/arcane/iris/generation/mantle/TerrainMatterView.java index f463252b8..f77f9c009 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/TerrainMatterView.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/TerrainMatterView.java @@ -134,8 +134,12 @@ public final class TerrainMatterView { SOUTH(false, 15); private final int[] positions = new int[256]; + private final boolean fixedX; + private final int coordinate; Face(boolean fixedX, int coordinate) { + this.fixedX = fixedX; + this.coordinate = coordinate; for (int y = 0; y < 16; y++) { for (int offset = 0; offset < 16; offset++) { int x = fixedX ? coordinate : offset; @@ -144,6 +148,14 @@ public final class TerrainMatterView { } } } + + int x(int offset) { + return fixedX ? coordinate : offset; + } + + int z(int offset) { + return fixedX ? offset : coordinate; + } } public static void iterate(MantleChunk chunk, Class type, 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 ea61380f6..d0fe437c9 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 @@ -109,7 +109,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { registerTerrainStage(sCave); registerTerrainStage(sPost); registerTerrainStage(sFloatingTerrainSolid); - registerStage((x, z, k, p, m, c) -> cave.decorateNaturalCaves(x, z, k)); + registerStage((x, z, k, p, m, c) -> cave.decorateNaturalCaves(x, z, k, c)); // Never burst these three: all of them write the same block hunk (and sDecorant reads // the surface sInsertMatter writes), so parallel order is scheduler-dependent. The // production path already runs them inline in this order; sequential registration diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java index 64bf27b3e..d58ab98eb 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java @@ -29,6 +29,7 @@ import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.iris.generation.runtime.EngineAssignedModifier; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.mantle.TerrainMatterView; import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.biome.IrisBiome; @@ -126,6 +127,10 @@ public class IrisCarveModifier extends EngineAssignedModifier MantleChunk mantleChunk = mantle.getChunk(x, z).use(); try { PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); + CaveTerrainSnapshot terrain = CaveTerrainSnapshot.capture(mantleChunk, x, z); + if (context != null) { + context.setCaveTerrain(terrain); + } int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight(); CarveResolutionContext resolutionContext = new CarveResolutionContext( @@ -142,32 +147,20 @@ public class IrisCarveModifier extends EngineAssignedModifier chunkBlockZ ); CarveResolver carveResolver = new CarveResolver(resolutionContext); - TerrainMatterView.iterate(mantleChunk, MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply( - xx, - yy, - zz, - cavern, - dataIfPresent(mantleChunk, xx, yy, zz, HydrologyCaveCell.class) - )); - TerrainMatterView.iterate(mantleChunk, HydrologyCaveCell.class, (xx, yy, zz, hydrology) -> { - if (dataIfPresent(mantleChunk, xx, yy, zz, MatterCavern.class) == null) { - carveResolver.apply(xx, yy, zz, null, hydrology); - } - }); + terrain.forEachCell(carveResolver::apply); if (scratch.customCaveBiomePresent) { - addInternalWallsFromMantle(mantleChunk, walls, columnMasks); + addInternalWallsFromMantle(terrain, walls, columnMasks); } else { addInternalWallsFromMasks(walls, columnMasks); } - addCrossChunkBoundaryWalls(mantle, mantleChunk, walls, boundaryMasks, x, z, surfaceHeights); + addCrossChunkBoundaryWalls(mantle, terrain, walls, boundaryMasks, x, z, surfaceHeights); getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start(); try { IrisData wallData = getData(); walls.forEach((rx, yy, rz, cavern) -> { - HydrologyCaveCell hydrology = dataIfPresent( - mantleChunk, rx, yy, rz, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(rx, yy, rz); if (hydrology != null && hydrology.protectsPlacement()) { return; } @@ -191,8 +184,7 @@ public class IrisCarveModifier extends EngineAssignedModifier for (int columnIndex = 0; columnIndex < 256; columnIndex++) { processColumnFromMask( output, - mantleChunk, - mantle, + terrain, columnMasks[columnIndex], columnIndex, x, @@ -209,7 +201,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } processBoundaryColumnFromMask( output, - mantleChunk, + terrain, boundaryMasks[columnIndex], walls, columnIndex, @@ -472,7 +464,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } } - private void addInternalWallsFromMantle(MantleChunk mc, CarveWallBuffer walls, CarveColumnMask[] columnMasks) { + private void addInternalWallsFromMantle(CaveTerrainSnapshot terrain, CarveWallBuffer walls, CarveColumnMask[] columnMasks) { for (int columnIndex = 0; columnIndex < 256; columnIndex++) { CarveColumnMask columnMask = columnMasks[columnIndex]; if (columnMask.isEmpty()) { @@ -483,18 +475,18 @@ public class IrisCarveModifier extends EngineAssignedModifier int rz = columnIndex & 15; int yy = columnMask.nextSetBit(0); while (yy >= 0) { - MatterCavern cavern = composedCavernAt(mc, rx, yy, rz); + MatterCavern cavern = terrain.composedCavern(rx, yy, rz); if (cavern != null) { - if (rz < 15 && composedCavernAt(mc, rx, yy, rz + 1) == null) { + if (rz < 15 && terrain.composedCavern(rx, yy, rz + 1) == null) { walls.put(rx, yy, rz + 1, cavern); } - if (rx < 15 && composedCavernAt(mc, rx + 1, yy, rz) == null) { + if (rx < 15 && terrain.composedCavern(rx + 1, yy, rz) == null) { walls.put(rx + 1, yy, rz, cavern); } - if (rz > 0 && composedCavernAt(mc, rx, yy, rz - 1) == null) { + if (rz > 0 && terrain.composedCavern(rx, yy, rz - 1) == null) { walls.put(rx, yy, rz - 1, cavern); } - if (rx > 0 && composedCavernAt(mc, rx - 1, yy, rz) == null) { + if (rx > 0 && terrain.composedCavern(rx - 1, yy, rz) == null) { walls.put(rx - 1, yy, rz, cavern); } } @@ -505,7 +497,7 @@ public class IrisCarveModifier extends EngineAssignedModifier private void addCrossChunkBoundaryWalls( Mantle mantle, - MantleChunk mc, + CaveTerrainSnapshot terrain, CarveWallBuffer walls, CarveColumnMask[] boundaryMasks, int chunkX, @@ -534,10 +526,10 @@ public class IrisCarveModifier extends EngineAssignedModifier } int height = maxY + 1; - MatterCavern[] ownWest = west == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.WEST, height); - MatterCavern[] ownEast = east == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.EAST, height); - MatterCavern[] ownNorth = north == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.NORTH, height); - MatterCavern[] ownSouth = south == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.SOUTH, height); + MatterCavern[] ownWest = west == null ? null : terrain.composedFace(TerrainMatterView.Face.WEST, height); + MatterCavern[] ownEast = east == null ? null : terrain.composedFace(TerrainMatterView.Face.EAST, height); + MatterCavern[] ownNorth = north == null ? null : terrain.composedFace(TerrainMatterView.Face.NORTH, height); + MatterCavern[] ownSouth = south == null ? null : terrain.composedFace(TerrainMatterView.Face.SOUTH, height); MatterCavern[] fromWest = west == null ? null : TerrainMatterView.getComposedFace(west, TerrainMatterView.Face.EAST, height); MatterCavern[] fromEast = east == null ? null : TerrainMatterView.getComposedFace(east, TerrainMatterView.Face.WEST, height); MatterCavern[] fromNorth = north == null ? null : TerrainMatterView.getComposedFace(north, TerrainMatterView.Face.SOUTH, height); @@ -586,18 +578,9 @@ public class IrisCarveModifier extends EngineAssignedModifier return plate.get(chunkX & 31, chunkZ & 31); } - private MatterCavern composedCavernAt(MantleChunk mantleChunk, int x, int y, int z) { - return TerrainMatterView.getComposedCavern(mantleChunk, x, y, z); - } - - private static T dataIfPresent(MantleChunk mantleChunk, int x, int y, int z, Class type) { - return TerrainMatterView.get(mantleChunk, x, y, z, type); - } - private void processColumnFromMask( Hunk output, - MantleChunk mc, - Mantle mantle, + CaveTerrainSnapshot terrain, CarveColumnMask columnMask, int columnIndex, int chunkX, @@ -631,7 +614,7 @@ public class IrisCarveModifier extends EngineAssignedModifier zone.ceiling = buf; } else { if (zone.isValid(resolverState.height)) { - processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, + processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache); } zone = new CaveZone(); @@ -644,14 +627,14 @@ public class IrisCarveModifier extends EngineAssignedModifier } if (zone.isValid(resolverState.height)) { - processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, + processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache); } } private void processBoundaryColumnFromMask( Hunk output, - MantleChunk mantleChunk, + CaveTerrainSnapshot terrain, CarveColumnMask boundaryMask, CarveWallBuffer walls, int columnIndex, @@ -678,7 +661,7 @@ public class IrisCarveModifier extends EngineAssignedModifier if (y == zoneCeiling + 1) { zoneCeiling = y; } else { - paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, + paintBoundaryZone(output, terrain, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache); zoneFloor = y; zoneCeiling = y; @@ -686,13 +669,13 @@ public class IrisCarveModifier extends EngineAssignedModifier y = boundaryMask.nextSetBit(y + 1); } - paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, + paintBoundaryZone(output, terrain, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache); } private void paintBoundaryZone( Hunk output, - MantleChunk mantleChunk, + CaveTerrainSnapshot terrain, CarveWallBuffer walls, int rx, int rz, @@ -715,8 +698,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } if (floorBiome != null) { - HydrologyCaveCell floorHydrology = dataIfPresent( - mantleChunk, rx, zoneFloor, rz, HydrologyCaveCell.class); + HydrologyCaveCell floorHydrology = terrain.hydrology(rx, zoneFloor, rz); IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial(); KList floorLayers = floorBiome.generateLayers( resolverState.dimension, worldX, worldZ, rng, 3, zoneFloor, resolverState.data, resolverState.complex); @@ -728,8 +710,7 @@ public class IrisCarveModifier extends EngineAssignedModifier if (floorY < 0) { break; } - HydrologyCaveCell hydrology = dataIfPresent( - mantleChunk, rx, floorY, rz, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(rx, floorY, rz); if (hydrology != null && hydrology.protectsPlacement() && hydrology.action() != HydrologyCaveAction.SEAL_GUARD) { @@ -764,8 +745,7 @@ public class IrisCarveModifier extends EngineAssignedModifier if (ceilingY >= worldMaxY) { break; } - HydrologyCaveCell hydrology = dataIfPresent( - mantleChunk, rx, ceilingY, rz, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(rx, ceilingY, rz); if (hydrology != null && hydrology.protectsPlacement() && hydrology.action() != HydrologyCaveAction.SEAL_GUARD) { @@ -800,7 +780,7 @@ public class IrisCarveModifier extends EngineAssignedModifier return (h & 15L) == 0L; } - private void processZone(Hunk output, MantleChunk mc, Mantle mantle, + private void processZone(Hunk output, CaveTerrainSnapshot terrain, CaveZone zone, int rx, int rz, int xx, int zz, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, @@ -815,16 +795,15 @@ public class IrisCarveModifier extends EngineAssignedModifier output.setRaw(rx, zone.ceiling, rz, AIR); } - IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); - IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); + IrisBiome floorBiome = resolveCaveBoundaryBiome(terrain, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); + IrisBiome ceilingBiome = resolveCaveBoundaryBiome(terrain, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); if (floorBiome == null && ceilingBiome == null) { - normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz); + normalizeCaveZoneWaterlogging(output, terrain, zone, rx, rz, xx, zz); return; } if (floorBiome != null) { - HydrologyCaveCell floorHydrology = dataIfPresent( - mc, rx, zone.floor, rz, HydrologyCaveCell.class); + HydrologyCaveCell floorHydrology = terrain.hydrology(rx, zone.floor, rz); IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial(); KList floorBlocks = floorBiome.generateLayers(resolverState.dimension, xx, zz, rng, 3, zone.floor, resolverState.data, resolverState.complex); for (int i = 0; i < zone.floor - 1; i++) { @@ -832,7 +811,7 @@ public class IrisCarveModifier extends EngineAssignedModifier break; } int y = zone.floor - i - 1; - HydrologyCaveCell hydrology = dataIfPresent(mc, rx, y, rz, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(rx, y, rz); if (hydrology != null && hydrology.protectsPlacement() && hydrology.action() != HydrologyCaveAction.SEAL_GUARD) { @@ -865,7 +844,7 @@ public class IrisCarveModifier extends EngineAssignedModifier if (cy >= maxY) { break; } - HydrologyCaveCell hydrology = dataIfPresent(mc, rx, cy, rz, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(rx, cy, rz); if (hydrology != null && hydrology.protectsPlacement() && hydrology.action() != HydrologyCaveAction.SEAL_GUARD) { @@ -884,64 +863,71 @@ public class IrisCarveModifier extends EngineAssignedModifier } } - normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz); + normalizeCaveZoneWaterlogging(output, terrain, zone, rx, rz, xx, zz); } - public void decorateNaturalCaves(int blockX, int blockZ, Hunk output) { + public void decorateNaturalCaves(int blockX, int blockZ, Hunk output, ChunkContext context) { Mantle mantle = getEngine().getMantle().getMantle(); - MantleChunk chunk = mantle.getChunk(blockX >> 4, blockZ >> 4).use(); + CaveTerrainSnapshot terrain = context == null ? null : context.getCaveTerrain(); + if (context != null) { + context.setCaveTerrain(null); + } + if (terrain == null || !terrain.covers(blockX >> 4, blockZ >> 4)) { + MantleChunk chunk = mantle.getChunk(blockX >> 4, blockZ >> 4).use(); + try { + terrain = CaveTerrainSnapshot.capture(chunk, blockX >> 4, blockZ >> 4); + } finally { + chunk.release(); + } + } CaveInputs resolver = new CaveInputs(getEngine()); Long2ObjectOpenHashMap caveBiomes = new Long2ObjectOpenHashMap<>(256); Map customBiomes = new HashMap<>(); - try { - int width = output.getWidth(); - int depth = output.getDepth(); - int height = output.getHeight(); - ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; - for (int localX = 0; localX < width; localX++) { - for (int localZ = 0; localZ < depth; localZ++) { - int worldX = blockX + localX; - int worldZ = blockZ + localZ; - int floor = -1; - int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ); - for (int y = 1; y < height; y++) { - if (y > top) { - break; - } - NativeBlockState state = output.getRaw(localX, y, localZ); - if (B.isSolid(state)) { - if (floor >= 0) { - CaveZone zone = new CaveZone(); - zone.setFloor(floor); - zone.setCeiling(y - 1); - if (zone.isValid(resolver.height) - && !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) { - if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) { - mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING); - } - if (markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) { - mantle.set(worldX, zone.floor, worldZ, MarkerMatter.CAVE_FLOOR); - } - IrisBiome floorBiome = resolveCaveBoundaryBiome(chunk, localX, floor, localZ, - worldX, worldZ, resolver, caveBiomes, customBiomes); - IrisBiome ceilingBiome = resolveCaveBoundaryBiome(chunk, localX, y - 1, localZ, - worldX, worldZ, resolver, caveBiomes, customBiomes); - decorateZone(output, zone, localX, localZ, worldX, worldZ, - floorBiome, ceilingBiome); - if (extent != null) { - top = extent.highestStoredY(localX, localZ); - } + int width = output.getWidth(); + int depth = output.getDepth(); + int height = output.getHeight(); + ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; + for (int localX = 0; localX < width; localX++) { + for (int localZ = 0; localZ < depth; localZ++) { + int worldX = blockX + localX; + int worldZ = blockZ + localZ; + int floor = -1; + int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ); + for (int y = 1; y < height; y++) { + if (y > top) { + break; + } + NativeBlockState state = output.getRaw(localX, y, localZ); + if (B.isSolid(state)) { + if (floor >= 0) { + CaveZone zone = new CaveZone(); + zone.setFloor(floor); + zone.setCeiling(y - 1); + if (zone.isValid(resolver.height) + && !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) { + if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) { + mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING); + } + if (markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) { + mantle.set(worldX, zone.floor, worldZ, MarkerMatter.CAVE_FLOOR); + } + IrisBiome floorBiome = resolveCaveBoundaryBiome(terrain, localX, floor, localZ, + worldX, worldZ, resolver, caveBiomes, customBiomes); + IrisBiome ceilingBiome = resolveCaveBoundaryBiome(terrain, localX, y - 1, localZ, + worldX, worldZ, resolver, caveBiomes, customBiomes); + decorateZone(output, zone, localX, localZ, worldX, worldZ, + floorBiome, ceilingBiome); + if (extent != null) { + top = extent.highestStoredY(localX, localZ); } - floor = -1; } - } else if (floor < 0 && B.isSolid(output.getRaw(localX, y - 1, localZ))) { - floor = y; + floor = -1; } + } else if (floor < 0 && B.isSolid(output.getRaw(localX, y - 1, localZ))) { + floor = y; } } } - } finally { - chunk.release(); } } @@ -966,7 +952,7 @@ public class IrisCarveModifier extends EngineAssignedModifier private void normalizeCaveZoneWaterlogging( Hunk output, - MantleChunk mantleChunk, + CaveTerrainSnapshot terrain, CaveZone zone, int localX, int localZ, @@ -976,13 +962,11 @@ public class IrisCarveModifier extends EngineAssignedModifier int minimumY = Math.max(0, zone.floor - 1); int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1); for (int y = minimumY; y <= maximumY; y++) { - HydrologyCaveCell hydrology = dataIfPresent( - mantleChunk, localX, y, localZ, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(localX, y, localZ); if (hydrology == null) { continue; } - MatterCavern baseline = dataIfPresent( - mantleChunk, localX, y, localZ, MatterCavern.class); + MatterCavern baseline = terrain.cavern(localX, y, localZ); NativeBlockState current = output.getRaw(localX, y, localZ); NativeBlockState columnFluid = getComplex().resolveHydrologyFluid( hydrology.fluidProfileKey(), @@ -1001,8 +985,8 @@ public class IrisCarveModifier extends EngineAssignedModifier } } - IrisBiome resolveCaveBoundaryBiome(MantleChunk mantleChunk, int x, int y, int z, int worldX, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { - MatterCavern cavern = composedCavernAt(mantleChunk, x, y, z); + IrisBiome resolveCaveBoundaryBiome(CaveTerrainSnapshot terrain, int x, int y, int z, int worldX, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { + MatterCavern cavern = terrain == null ? null : terrain.composedCavern(x, y, z); return resolveCaveBoundaryBiome( cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache); } diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java new file mode 100644 index 000000000..95c64d0c2 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java @@ -0,0 +1,175 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction; +import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; +import art.arcane.iris.world.storage.matter.IrisMatterSupport; +import art.arcane.iris.world.storage.matter.PreObjectMatterCell; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.IrisMatter; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CaveTerrainSnapshotTest { + private static final int SECTIONS = 5; + + @BeforeClass + public static void registerMatter() { + IrisMatterSupport.ensureRegistered(); + } + + @Test + public void snapshotMatchesTerrainViewForEveryCellFaceAndResolverInput() { + for (long seed = 1; seed <= 2; seed++) { + MantleChunk chunk = randomChunk(new Random(seed), seed % 2 == 0); + CaveTerrainSnapshot snapshot = CaveTerrainSnapshot.capture(chunk, 3, -4); + assertTrue(snapshot.covers(3, -4)); + assertFalse(snapshot.covers(3, 4)); + boolean anyHydrology = false; + for (int x = 0; x < 16; x++) { + for (int y = -2; y < SECTIONS * 16 + 3; y++) { + for (int z = 0; z < 16; z++) { + String cell = "seed=" + seed + " " + x + "," + y + "," + z; + assertSame(cell, TerrainMatterView.get(chunk, x, y, z, MatterCavern.class), snapshot.cavern(x, y, z)); + HydrologyCaveCell hydrology = TerrainMatterView.get(chunk, x, y, z, HydrologyCaveCell.class); + anyHydrology |= hydrology != null; + assertSame(cell, hydrology, snapshot.hydrology(x, y, z)); + assertSame(cell, TerrainMatterView.getComposedCavern(chunk, x, y, z), snapshot.composedCavern(x, y, z)); + } + } + } + assertEquals(anyHydrology, snapshot.hasHydrology()); + for (TerrainMatterView.Face face : TerrainMatterView.Face.values()) { + for (int height : new int[]{0, 1, 37, SECTIONS * 16, SECTIONS * 16 + 9}) { + MatterCavern[] expected = TerrainMatterView.getComposedFace(chunk, face, height); + MatterCavern[] actual = snapshot.composedFace(face, height); + assertEquals(expected.length, actual.length); + for (int index = 0; index < expected.length; index++) { + assertSame(face + " " + index, expected[index], actual[index]); + } + } + } + assertEquals(legacyResolverInputs(chunk), snapshotResolverInputs(snapshot)); + } + } + + @Test + public void chunksWithoutHydrologyAnswerNullWithoutCells() { + Matter[] sections = new Matter[SECTIONS]; + sections[1] = new IrisMatter(16, 16, 16); + MatterCavern cavern = new MatterCavern(true, "", (byte) 0); + sections[1].slice(MatterCavern.class).set(4, 5, 6, cavern); + sections[1].slice(PreObjectMatterCell.class).set(4, 6, 6, PreObjectMatterCell.hydrology(null)); + CaveTerrainSnapshot snapshot = CaveTerrainSnapshot.capture(chunk(sections), 0, 0); + assertFalse(snapshot.hasHydrology()); + assertNull(snapshot.hydrology(4, 22, 6)); + assertSame(cavern, snapshot.composedCavern(4, 21, 6)); + assertNull(snapshot.cavern(4, 22, 6)); + } + + private static Map legacyResolverInputs(MantleChunk chunk) { + Map inputs = new HashMap<>(); + for (int x = 0; x < 16; x++) { + for (int y = 0; y < SECTIONS * 16; y++) { + for (int z = 0; z < 16; z++) { + MatterCavern cavern = TerrainMatterView.get(chunk, x, y, z, MatterCavern.class); + HydrologyCaveCell hydrology = TerrainMatterView.get(chunk, x, y, z, HydrologyCaveCell.class); + if (cavern != null || hydrology != null) { + inputs.put(x + ":" + y + ":" + z, identity(cavern) + "/" + identity(hydrology)); + } + } + } + } + return inputs; + } + + private static Map snapshotResolverInputs(CaveTerrainSnapshot snapshot) { + Map inputs = new HashMap<>(); + snapshot.forEachCell((x, y, z, cavern, hydrology) -> assertNull(inputs.put(x + ":" + y + ":" + z, + identity(cavern) + "/" + identity(hydrology)))); + return inputs; + } + + private static String identity(Object value) { + return value == null ? "null" : Integer.toHexString(System.identityHashCode(value)); + } + + private static MantleChunk randomChunk(Random random, boolean manyCaverns) { + MatterCavern[] caverns = new MatterCavern[manyCaverns ? 40 : 4]; + for (int index = 0; index < caverns.length; index++) { + caverns[index] = new MatterCavern(index % 3 != 0, "biome-" + index, (byte) (index % 4)); + } + HydrologyCaveCell[] hydrology = new HydrologyCaveCell[HydrologyCaveAction.values().length]; + for (int index = 0; index < hydrology.length; index++) { + hydrology[index] = new HydrologyCaveCell(HydrologyCaveAction.values()[index], "river", "flooded-" + index); + } + Matter[] sections = new Matter[SECTIONS]; + for (int section = 0; section < SECTIONS; section++) { + if (section == 2) { + continue; + } + Matter matter = new IrisMatter(16, 16, 16); + sections[section] = matter; + int mode = section % 4; + for (int x = 0; x < 16; x++) { + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + boolean aligned = ((z << 8 | y << 4 | x) & 0x333) == 0; + int roll = random.nextInt(100); + if (mode == 1 ? aligned && roll < 50 : roll < 40) { + matter.slice(MatterCavern.class).set(x, y, z, caverns[random.nextInt(caverns.length)]); + } + if (mode != 3 && random.nextInt(100) < 6) { + matter.slice(HydrologyCaveCell.class).set(x, y, z, hydrology[random.nextInt(hydrology.length)]); + } + int journal = random.nextInt(100); + if (journal < 3) { + matter.slice(PreObjectMatterCell.class).set(x, y, z, PreObjectMatterCell.cavern(null)); + } else if (journal < 6) { + matter.slice(PreObjectMatterCell.class).set(x, y, z, + PreObjectMatterCell.cavern(caverns[random.nextInt(caverns.length)])); + } else if (journal < 8) { + matter.slice(PreObjectMatterCell.class).set(x, y, z, PreObjectMatterCell.hydrology(null)); + } else if (journal < 10) { + matter.slice(PreObjectMatterCell.class).set(x, y, z, + PreObjectMatterCell.block(null).captureHydrology(hydrology[random.nextInt(hydrology.length)])); + } else if (journal < 13) { + matter.slice(PreObjectMatterCell.class).set(x, y, z, PreObjectMatterCell.string("marker")); + } + } + } + } + } + return chunk(sections); + } + + @SuppressWarnings("unchecked") + private static MantleChunk chunk(Matter[] sections) { + MantleChunk chunk = mock(MantleChunk.class); + when(chunk.sectionCount()).thenReturn(sections.length); + when(chunk.exists(anyInt())).thenAnswer(call -> { + int section = call.getArgument(0); + return section >= 0 && section < sections.length && sections[section] != null; + }); + when(chunk.get(anyInt())).thenAnswer(call -> { + assertTrue(Thread.holdsLock(chunk)); + int section = call.getArgument(0); + return section >= 0 && section < sections.length ? sections[section] : null; + }); + return chunk; + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java index 85c9eca15..f43fe5bf1 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java @@ -1,6 +1,7 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.mantle.TerrainMatterView; import art.arcane.iris.world.IrisWorld; import art.arcane.iris.world.storage.matter.IrisMatterSupport; @@ -56,6 +57,7 @@ public class IrisCarveBoundaryFacesTest { return true; }); when(chunks[side].get(anyInt())).thenAnswer(call -> matter[selected][(int) call.getArgument(0)]); + when(chunks[side].sectionCount()).thenReturn(2); if (side == 0) { continue; } @@ -87,10 +89,10 @@ public class IrisCarveBoundaryFacesTest { int[] surfaces = new int[256]; Arrays.fill(surfaces, 30); Method method = IrisCarveModifier.class.getDeclaredMethod("addCrossChunkBoundaryWalls", - Mantle.class, MantleChunk.class, CarveWallBuffer.class, CarveColumnMask[].class, + Mantle.class, CaveTerrainSnapshot.class, CarveWallBuffer.class, CarveColumnMask[].class, int.class, int.class, int[].class); method.setAccessible(true); - method.invoke(modifier, mantle, chunks[0], walls, masks, 0, 0, surfaces); + method.invoke(modifier, mantle, CaveTerrainSnapshot.capture(chunks[0], 0, 0), walls, masks, 0, 0, surfaces); CarveWallBuffer expectedWalls = new CarveWallBuffer(64); for (int y = 1; y < 32; y++) { for (int offset = 0; offset < 16; offset++) { diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java index 31cbad1d9..c32ea2a51 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java @@ -2,6 +2,7 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.ResourceLoader; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.biome.IrisBiome; @@ -12,9 +13,10 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.iris.world.storage.matter.IrisMatterSupport; +import art.arcane.volmlib.util.matter.IrisMatter; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; -import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.volmlib.util.math.RNG; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import org.junit.Test; @@ -55,30 +57,27 @@ public class IrisCarveModifierBoundarySupportTest { rng.setAccessible(true); rng.set(modifier, new RNG(71L)); + IrisMatterSupport.ensureRegistered(); + Matter floorMatter = new IrisMatter(16, 16, 16); + Matter ceilingMatter = new IrisMatter(16, 16, 16); + floorMatter.slice(MatterCavern.class).set(1, 6, 2, new MatterCavern(true, "custom/floor", (byte) 0)); + ceilingMatter.slice(MatterCavern.class).set(1, 11, 2, new MatterCavern(true, "custom/other", (byte) 0)); MantleChunk mantleChunk = mock(MantleChunk.class); - Matter floorMatter = mock(Matter.class); - Matter ceilingMatter = mock(Matter.class); - MatterSlice floorSlice = mock(MatterSlice.class); - MatterSlice ceilingSlice = mock(MatterSlice.class); + doReturn(3).when(mantleChunk).sectionCount(); doReturn(true).when(mantleChunk).exists(0); doReturn(true).when(mantleChunk).exists(2); doReturn(floorMatter).when(mantleChunk).get(0); doReturn(ceilingMatter).when(mantleChunk).get(2); - doReturn(true).when(floorMatter).hasSlice(MatterCavern.class); - doReturn(true).when(ceilingMatter).hasSlice(MatterCavern.class); - doReturn(floorSlice).when(floorMatter).getSlice(MatterCavern.class); - doReturn(ceilingSlice).when(ceilingMatter).getSlice(MatterCavern.class); - doReturn(new MatterCavern(true, "custom/floor", (byte) 0)).when(floorSlice).get(1, 6, 2); - doReturn(null).when(ceilingSlice).get(1, 10, 2); + CaveTerrainSnapshot terrain = CaveTerrainSnapshot.capture(mantleChunk, 2, 2); Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(); Map customBiomeCache = new HashMap<>(); IrisCarveModifier.CaveInputs resolverState = new IrisCarveModifier.CaveInputs(engine); IrisBiome floor = modifier.resolveCaveBoundaryBiome( - mantleChunk, 1, 6, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache); + terrain, 1, 6, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache); IrisBiome ceiling = modifier.resolveCaveBoundaryBiome( - mantleChunk, 1, 42, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache); + terrain, 1, 42, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache); assertSame(customFloor, floor); assertSame(resolvedCeiling, ceiling); diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java index 433a55dd5..ca9f39398 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java @@ -1,6 +1,7 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.terrain.InferredType; @@ -56,7 +57,7 @@ public class IrisCarveModifierInferenceIsolationTest { Method paintBoundaryZone = IrisCarveModifier.class.getDeclaredMethod( "paintBoundaryZone", Hunk.class, - MantleChunk.class, + CaveTerrainSnapshot.class, CarveWallBuffer.class, int.class, int.class, @@ -72,7 +73,7 @@ public class IrisCarveModifierInferenceIsolationTest { paintBoundaryZone.invoke( modifier, mock(Hunk.class), - mock(MantleChunk.class), + CaveTerrainSnapshot.capture(mock(MantleChunk.class), 0, 0), walls, 0, 0, diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java index 9b16e3bcd..128eb48ff 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java @@ -5,6 +5,7 @@ import art.arcane.iris.generation.decoration.IrisCeilingDecorator; import art.arcane.iris.generation.decoration.IrisSurfaceDecorator; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.SeedManager; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.mantle.EngineMantle; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.decoration.IrisDecorationPart; @@ -38,6 +39,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.doAnswer; @@ -78,7 +80,7 @@ public class IrisNaturalCaveScanTest { throw failure; }); assertSame(failure, assertThrows(IllegalStateException.class, - () -> fixture.modifier.decorateNaturalCaves(-32, 48, output))); + () -> fixture.modifier.decorateNaturalCaves(-32, 48, output, null))); verify(fixture.chunk).release(); } @@ -92,7 +94,7 @@ public class IrisNaturalCaveScanTest { IllegalStateException failure = new IllegalStateException("height unavailable"); doAnswer(call -> { throw failure; }).when(output).getHeight(); assertSame(failure, assertThrows(IllegalStateException.class, - () -> fixture.modifier.decorateNaturalCaves(-32, 48, output))); + () -> fixture.modifier.decorateNaturalCaves(-32, 48, output, null))); verify(fixture.chunk).release(); } @@ -104,7 +106,7 @@ public class IrisNaturalCaveScanTest { Hunk fullOutput = full.sparseOutput(Hunk.newArrayHunk(16, 32, 16)); List fullWrites = new ArrayList<>(); full.modifier.decorateNaturalCaves(-32, 48, - fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key()))); + fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key())), null); Fixture bounded = new Fixture(true); ChunkGenerator.ChunkData chunkData = mock(ChunkGenerator.ChunkData.class); @@ -114,7 +116,7 @@ public class IrisNaturalCaveScanTest { bounded.sparseOutput(holder); List boundedWrites = new ArrayList<>(); bounded.modifier.decorateNaturalCaves(-32, 48, new ColumnExtentListeningHunk<>(holder, - (x, y, z, state) -> boundedWrites.add(x + ":" + y + ":" + z + ":" + state.key()))); + (x, y, z, state) -> boundedWrites.add(x + ":" + y + ":" + z + ":" + state.key())), null); assertTrue("fixture must decorate a zone above the initial column top", full.calls.contains("floor:0:0:27:1")); @@ -141,7 +143,7 @@ public class IrisNaturalCaveScanTest { Hunk output = fixture.output(width, depth); List writes = new ArrayList<>(); fixture.modifier.decorateNaturalCaves(-32, 48, - output.listen((x, y, z, state) -> writes.add(x + ":" + y + ":" + z + ":" + state.key()))); + output.listen((x, y, z, state) -> writes.add(x + ":" + y + ":" + z + ":" + state.key())), null); List expectedCalls = new ArrayList<>(); List expectedMarkers = new ArrayList<>(); List expectedWrites = new ArrayList<>(); @@ -239,7 +241,7 @@ public class IrisNaturalCaveScanTest { IrisBiome biome = mock(IrisBiome.class); doReturn(new IrisDecorator[]{new IrisDecorator()}).when(biome).getDecoratorBucket(IrisDecorationPart.NONE); doReturn(new IrisDecorator[]{new IrisDecorator()}).when(biome).getDecoratorBucket(IrisDecorationPart.CEILING); - doReturn(biome).when(modifier).resolveCaveBoundaryBiome(eq(chunk), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), any(), any()); + doReturn(biome).when(modifier).resolveCaveBoundaryBiome(argThat((CaveTerrainSnapshot terrain) -> terrain != null && terrain.covers(-2, 3)), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), any(), any()); IrisDecorantActuator decorant = mock(IrisDecorantActuator.class); IrisSurfaceDecorator surface = mock(IrisSurfaceDecorator.class); IrisCeilingDecorator roof = mock(IrisCeilingDecorator.class);