From 23bad0e1a27f6aac9720d64fa64ccbb647c88bbf Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 6 Sep 2026 03:15:57 -0400 Subject: [PATCH] Refined terrain assembly - Ceiling layers use their own thickness generators - Pack validation accepts independent ceiling layer counts - Locked strata repeat across the reference height - Surface and cave palettes share layer emission - Cave passes release retained chunk sections - Nested carving preserves outer blend buffers - Profile blending reuses bounded numeric buffers - Terrain actuators remove unused state and duplicate checks - Regression tests cover layers and carving scratch lifetimes --- .../core/pack/PackBiomeLayerValidator.java | 36 +--- .../arcane/iris/core/pack/PackValidator.java | 2 +- .../engine/actuator/IrisBiomeActuator.java | 2 - .../actuator/IrisTerrainNormalActuator.java | 9 - .../IrisTransitionGeometryActuator.java | 4 +- .../mantle/components/CaveCarveScratch.java | 7 + .../mantle/components/IrisCaveCarver3D.java | 5 +- .../components/MantleCarvingComponent.java | 93 ++++++---- .../arcane/iris/engine/object/IrisBiome.java | 1 + .../object/IrisBiomeLayerGenerator.java | 109 ++++------- .../pack/PackBiomeLayerValidatorTest.java | 21 +-- .../IrisCaveCarver3DNearParityTest.java | 42 +++++ .../MantleCarvingComponentScratchTest.java | 170 ++++++++++++++++++ .../IrisBiomeCeilingLayerGuardTest.java | 55 ------ .../object/IrisBiomeCeilingLayerTest.java | 78 ++++++++ .../object/IrisBiomeLockedLayerTest.java | 35 ++++ 16 files changed, 446 insertions(+), 223 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentScratchTest.java delete mode 100644 core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerGuardTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/object/IrisBiomeLockedLayerTest.java diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java index 17297aa22..a9e5a0406 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java @@ -22,24 +22,15 @@ import art.arcane.volmlib.util.json.JSONArray; import art.arcane.volmlib.util.json.JSONObject; import java.io.File; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -/** - * Validates biome layer stacks. caveCeilingLayers reuses the height generators built from layers, - * so a biome with more ceiling entries than surface entries has no generator for the extras; the - * engine skips them, and this validator surfaces the mistake to the author at validate time. - */ final class PackBiomeLayerValidator { - private static final int DEFAULT_LAYER_COUNT = 1; - private PackBiomeLayerValidator() { } - static List validateCeilingLayerCounts(File biomesFolder) { + static List validateLayers(File biomesFolder) { List blockingErrors = new ArrayList<>(); if (biomesFolder == null || !biomesFolder.isDirectory()) { return blockingErrors; @@ -49,24 +40,13 @@ final class PackBiomeLayerValidator { biomeFiles.sort(Comparator.comparing(File::getPath)); for (File biomeFile : biomeFiles) { String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile); - JSONObject biome; - try { - biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8)); - } catch (Throwable e) { - // Invalid JSON is reported by the graph validators; layer counts have nothing to add. + JSONObject biome = PackValidationIo.readJson(biomeFile); + if (biome == null) { continue; } - Integer layers = arrayLength(biome, "layers", biomeKey, DEFAULT_LAYER_COUNT, blockingErrors); - Integer ceiling = arrayLength(biome, "caveCeilingLayers", biomeKey, 0, blockingErrors); - if (layers == null || ceiling == null) { - continue; - } - - if (ceiling > layers) { - blockingErrors.add("Biome '" + biomeKey + "' declares " + ceiling + " caveCeilingLayers but only " - + layers + " layers. caveCeilingLayers reuses the layers height generators and must not have more entries."); - } + validateLayerArray(biome, "layers", biomeKey, blockingErrors); + validateLayerArray(biome, "caveCeilingLayers", biomeKey, blockingErrors); } return blockingErrors; } @@ -126,17 +106,15 @@ final class PackBiomeLayerValidator { } } - private static Integer arrayLength(JSONObject biome, String field, String biomeKey, int defaultCount, + private static void validateLayerArray(JSONObject biome, String field, String biomeKey, List blockingErrors) { if (!biome.has(field) || biome.isNull(field)) { - return defaultCount; + return; } JSONArray array = biome.optJSONArray(field); if (array == null) { blockingErrors.add("Biome '" + biomeKey + "' " + field + " must be an array."); - return null; } - return array.length(); } } diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java index e09d62545..5cd39ebd5 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java @@ -150,7 +150,7 @@ public final class PackValidator { new File(packFolder, "spawners"), new File(packFolder, "entities"))); blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns( new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory)); - blockingErrors.addAll(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(packFolder, "biomes"))); + blockingErrors.addAll(PackBiomeLayerValidator.validateLayers(new File(packFolder, "biomes"))); blockingErrors.addAll(PackBiomeLayerValidator.validateDecoratorPalettes( new File(packFolder, "biomes"), new File(packFolder, "snippet/decorator"))); PackStyledRangeDefaultValidator.Validation styledRanges = PackStyledRangeDefaultValidator.validate(packFolder); diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java index 9ec321a06..a4a5083fc 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java @@ -41,7 +41,6 @@ import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterBiomeInject; import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter; -import art.arcane.volmlib.util.scheduling.ChronoLatch; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.util.List; @@ -49,7 +48,6 @@ import java.util.Objects; public class IrisBiomeActuator extends EngineAssignedActuator { private final RNG rng; - private final ChronoLatch cl = new ChronoLatch(5000); private final KMap resolvedBiomes = new KMap<>(); private final KMap resolvedPhysicalBiomes = new KMap<>(); diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java index fa05afcba..1407e285f 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java @@ -46,11 +46,7 @@ import art.arcane.iris.util.common.data.B; import lombok.Getter; public class IrisTerrainNormalActuator extends EngineAssignedActuator { - private static final PlatformBlockState AIR = B.getState("AIR"); private static final PlatformBlockState BEDROCK = B.getState("BEDROCK"); - private static final PlatformBlockState LAVA = B.getState("LAVA"); - private static final PlatformBlockState GLASS = B.getState("GLASS"); - private static final PlatformBlockState CAVE_AIR = B.getState("CAVE_AIR"); @Getter private final RNG rng; @Getter @@ -83,11 +79,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator h, ChunkContext context) { - terrainSliverOptimized(x, z, xf, h, context); - } - - @BlockCoordinates - private void terrainSliverOptimized(int x, int z, int xf, Hunk h, ChunkContext context) { int chunkHeight = h.getHeight(); int chunkDepth = h.getDepth(); IrisDimension dimension = getDimension(); diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuator.java index b0be83ec5..1b95915fc 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuator.java @@ -42,9 +42,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen return; } GeometrySource source = new GeometrySource(blocks, biomes, getEngine().getMinHeight(), context); - if (plan != null && plan.hasTransitionAtChunk(x >> 4, z >> 4)) { - blendGeometry(x, z, blocks, context, source, plan); - } + blendGeometry(x, z, blocks, context, source, plan); } public static SavedTerrainChunk capture(int x, int z, Hunk blocks, diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java index 912663a19..5be04aff5 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java @@ -22,6 +22,8 @@ import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterSlice; +import java.util.Arrays; + final class CaveCarveScratch { final int[] columnMaxY = new int[256]; final int[] fluidMaxY = new int[256]; @@ -70,4 +72,9 @@ final class CaveCarveScratch { int adaptiveGeometryStep = -1; int adaptiveGeometryAxisCells = -1; boolean fullWeightsInitialized; + + void releaseSections() { + Arrays.fill(sectionMatter, null); + Arrays.fill(sectionSlices, null); + } } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index f7f6483f1..b3391898a 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -436,6 +436,7 @@ public class IrisCaveCarver3D { return carved; } finally { + scratchCache.get().releaseSections(); carving.set(nested); if (nested) { scratchCache.set(previousScratch); @@ -2695,10 +2696,6 @@ public class IrisCaveCarver3D { return weight; } - private double signed(double value) { - return (value * 2D) - 1D; - } - private double applySurfaceCeilingFade( CaveCarveScratch scratch, double threshold, diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index 808ebb13d..2d2416d88 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -19,6 +19,7 @@ package art.arcane.iris.engine.mantle.components; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.UpperDimensionContext; import art.arcane.iris.engine.mantle.MatterGenerationPhase; import art.arcane.iris.engine.mantle.ComponentFlag; @@ -36,7 +37,6 @@ import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.utility.ChunkFillableDoubleStream2D; -import art.arcane.iris.util.simd.SimdKernels; import art.arcane.iris.util.simd.SimdSupport; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; @@ -58,6 +58,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { private static final double MIN_WEIGHT = 0.08D; private static final double THRESHOLD_PENALTY = 0.24D; private static final int MAX_BLENDED_PROFILE_PASSES = 2; + private static final int MAX_POOLED_WEIGHT_BUFFERS = 64; private static final int KERNEL_WIDTH = (BLEND_RADIUS * 2) + 1; private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH; private static final int[] KERNEL_DX = new int[KERNEL_SIZE]; @@ -107,8 +108,25 @@ public class MantleCarvingComponent extends IrisMantleComponent { if (!complex.allowsMantleChunkWrite(x, z)) { return; } + BlendScratch previous = BLEND_SCRATCH.get(); + BlendScratch scratch = previous.inUse ? new BlendScratch() : previous; + if (scratch != previous) { + BLEND_SCRATCH.set(scratch); + } + scratch.inUse = true; + try { + generateLayer(writer, x, z, context, scratch); + } finally { + scratch.release(); + if (scratch != previous) { + BLEND_SCRATCH.set(previous); + } + } + } + + private void generateLayer(MantleWriter writer, int x, int z, ChunkContext context, BlendScratch blendScratch) { + IrisComplex complex = context.getComplex(); IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); - BlendScratch blendScratch = BLEND_SCRATCH.get(); int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights); PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); List weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState); @@ -217,12 +235,12 @@ public class MantleCarvingComponent extends IrisMantleComponent { BlendScratch blendScratch = BLEND_SCRATCH.get(); IrisCaveProfile[] profileField = blendScratch.profileField; Map columnProfileWeights = blendScratch.columnProfileWeights; - IdentityHashMap activeProfiles = blendScratch.activeProfiles; List profileOrder = blendScratch.profileOrder; IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles; double[] kernelProfileWeights = blendScratch.kernelProfileWeights; - activeProfiles.clear(); + columnProfileWeights.clear(); profileOrder.clear(); + blendScratch.nextWeightBuffer = 0; fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, blendScratch); for (int localX = 0; localX < CHUNK_SIZE; localX++) { @@ -270,12 +288,8 @@ public class MantleCarvingComponent extends IrisMantleComponent { double[] weights = columnProfileWeights.get(profile); if (weights == null) { - weights = new double[CHUNK_AREA]; + weights = blendScratch.acquireWeights(); columnProfileWeights.put(profile, weights); - } else if (!activeProfiles.containsKey(profile)) { - Arrays.fill(weights, 0D); - } - if (activeProfiles.put(profile, Boolean.TRUE) == null) { profileOrder.add(profile); } weights[columnIndex] = columnWeight; @@ -316,28 +330,27 @@ public class MantleCarvingComponent extends IrisMantleComponent { return weightedProfiles; } - Map dimensionColumnPlans = blendScratch.dimensionColumnPlans; - dimensionColumnPlans.clear(); + Engine engine = getEngineMantle().getEngine(); + int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX); + int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ); for (IrisDimensionCarvingEntry entry : entries) { if (entry == null || !entry.isEnabled()) { continue; } - IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), entry, resolverState); + IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, entry, resolverState); if (rootBiome == null) { continue; } - IrisDimensionCarvingEntry[] columnPlan = dimensionColumnPlans.computeIfAbsent(entry, key -> new IrisDimensionCarvingEntry[CHUNK_AREA]); - buildDimensionColumnPlan(columnPlan, chunkX, chunkZ, entry, resolverState); - Map rootProfileColumnWeights = new IdentityHashMap<>(); List rootProfileOrder = new ArrayList<>(); IrisRange worldYRange = entry.getWorldYRange(); for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) { - IrisDimensionCarvingEntry resolvedEntry = columnPlan[columnIndex]; - IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState); + IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot( + engine, entry, baseX + (columnIndex >> 4), baseZ + (columnIndex & 15), resolverState); + IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, resolvedEntry, resolverState); if (resolvedBiome == null) { continue; } @@ -349,7 +362,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { double[] columnWeights = rootProfileColumnWeights.get(profile); if (columnWeights == null) { - columnWeights = new double[CHUNK_AREA]; + columnWeights = blendScratch.acquireWeights(); rootProfileColumnWeights.put(profile, columnWeights); rootProfileOrder.add(profile); } @@ -364,19 +377,6 @@ public class MantleCarvingComponent extends IrisMantleComponent { return weightedProfiles; } - private void buildDimensionColumnPlan(IrisDimensionCarvingEntry[] columnPlan, int chunkX, int chunkZ, IrisDimensionCarvingEntry entry, IrisDimensionCarvingResolver.State resolverState) { - int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX); - int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ); - for (int localX = 0; localX < CHUNK_SIZE; localX++) { - int worldX = baseX + localX; - for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { - int worldZ = baseZ + localZ; - int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); - columnPlan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState); - } - } - } - private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) { int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - BLEND_RADIUS; int startZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ) - BLEND_RADIUS; @@ -698,8 +698,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { private final IrisCaveProfile[] kernelProfiles = new IrisCaveProfile[KERNEL_SIZE]; private final double[] kernelProfileWeights = new double[KERNEL_SIZE]; private final IdentityHashMap columnProfileWeights = new IdentityHashMap<>(); - private final IdentityHashMap dimensionColumnPlans = new IdentityHashMap<>(); - private final IdentityHashMap activeProfiles = new IdentityHashMap<>(); + private final List weightBuffers = new ArrayList<>(); private final List profileOrder = new ArrayList<>(); private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE]; private final double[] fieldFluidHeights = new double[FIELD_SIZE * FIELD_SIZE]; @@ -710,5 +709,33 @@ public class MantleCarvingComponent extends IrisMantleComponent { private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE]; private final int[] chunkSurfaceHeights = new int[CHUNK_AREA]; private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA]; + private int nextWeightBuffer; + private boolean inUse; + + private double[] acquireWeights() { + if (nextWeightBuffer < weightBuffers.size()) { + double[] weights = weightBuffers.get(nextWeightBuffer++); + Arrays.fill(weights, 0D); + return weights; + } + double[] weights = new double[CHUNK_AREA]; + if (weightBuffers.size() < MAX_POOLED_WEIGHT_BUFFERS) { + weightBuffers.add(weights); + nextWeightBuffer++; + } + return weights; + } + + private void release() { + Arrays.fill(profileField, null); + Arrays.fill(kernelProfiles, null); + Arrays.fill(fieldRegions, null); + Arrays.fill(fieldSurfaceBiomes, null); + Arrays.fill(fieldCaveBiomes, null); + columnProfileWeights.clear(); + profileOrder.clear(); + nextWeightBuffer = 0; + inUse = false; + } } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index 2580475c4..94dcbef30 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -92,6 +92,7 @@ public class IrisBiome extends IrisRegistrant implements IRare { private final transient AtomicCache realCarveBiome = new AtomicCache<>(); private final transient AtomicCache> realChildren = new AtomicCache<>(); private final transient AtomicCache> layerHeightGenerators = new AtomicCache<>(); + private final transient AtomicCache> layerCeilingHeightGenerators = new AtomicCache<>(); private final transient AtomicCache> layerSeaHeightGenerators = new AtomicCache<>(); private final transient AtomicCache> surfaceOreCache = new AtomicCache<>(); private final transient AtomicCache> undergroundOreCache = new AtomicCache<>(); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java index e551f1237..fb41a1a75 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java @@ -74,7 +74,7 @@ final class IrisBiomeLayerGenerator { IrisBiomePaletteLayer layer = layers.get(i); double zoom = layer.getZoom(); CNG hgen = heightGenerators.get(i); - double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); IrisSlopeClip sc = layer.getSlopeCondition(); @@ -88,17 +88,7 @@ final class IrisBiomeLayerGenerator { continue; } - for (int j = 0; j < d; j++) { - if (data.size() >= maxDepth) { - break; - } - - try { - data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); - } catch (Throwable e) { - IrisLogging.reportError(e); - } - } + appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata); if (data.size() >= maxDepth) { break; @@ -132,31 +122,20 @@ final class IrisBiomeLayerGenerator { return data; } - KList heightGenerators = getLayerHeightGenerators(biome, random, rdata); - // Ceiling layers reuse the surface-layer height generators, so entries beyond layers.size() have no generator; skip them. - int usableLayers = Math.min(layerCount, heightGenerators.size()); + KList heightGenerators = getHeightGenerators( + biome.getLayerCeilingHeightGenerators(), ceilingLayers, 7235, random, rdata); - for (int i = 0; i < usableLayers; i++) { + for (int i = 0; i < layerCount; i++) { IrisBiomePaletteLayer layer = ceilingLayers.get(i); double zoom = layer.getZoom(); CNG hgen = heightGenerators.get(i); - double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); if (d <= 0) { continue; } - for (int j = 0; j < d; j++) { - if (data.size() >= maxDepth) { - break; - } - - try { - data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); - } catch (Throwable e) { - IrisLogging.reportError(e); - } - } + appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata); if (data.size() >= maxDepth) { break; @@ -198,7 +177,7 @@ final class IrisBiomeLayerGenerator { IrisBiomePaletteLayer layer = layers.get(i); double zoom = layer.getZoom(); CNG hgen = heightGenerators.get(i); - double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); IrisSlopeClip sc = layer.getSlopeCondition(); @@ -212,13 +191,7 @@ final class IrisBiomeLayerGenerator { continue; } - for (int j = 0; j < d; j++) { - try { - data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); - } catch (Throwable e) { - IrisLogging.reportError(e); - } - } + appendLayer(data, layer, i, d, wx, wz, random, Integer.MAX_VALUE, rdata); } } @@ -227,9 +200,8 @@ final class IrisBiomeLayerGenerator { } for (int i = 0; i < maxDepth; i++) { - int offset = (512 - height) - i; - int index = offset % data.size(); - real.add(data.get(Math.max(index, 0))); + long offset = 512L - height - i; + real.add(data.get(Math.floorMod(offset, data.size()))); } return real; @@ -264,17 +236,7 @@ final class IrisBiomeLayerGenerator { continue; } - for (int j = 0; j < d; j++) { - if (data.size() >= maxDepth) { - break; - } - - try { - data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); - } catch (Throwable e) { - IrisLogging.reportError(e); - } - } + appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata); if (data.size() >= maxDepth) { break; @@ -285,7 +247,16 @@ final class IrisBiomeLayerGenerator { } static KList getLayerHeightGenerators(IrisBiome biome, RNG rng, IrisData rdata) { - AtomicCache> cache = biome.getLayerHeightGenerators(); + return getHeightGenerators(biome.getLayerHeightGenerators(), biome.getLayers(), 7235, rng, rdata); + } + + static KList getLayerSeaHeightGenerators(IrisBiome biome, RNG rng, IrisData data) { + return getHeightGenerators(biome.getLayerSeaHeightGenerators(), biome.getSeaLayers(), 7735, rng, data); + } + + private static KList getHeightGenerators(AtomicCache> cache, + KList layers, + int seedOffset, RNG rng, IrisData data) { KList cached = cache.getIfPresent(); if (cached != null) { @@ -296,36 +267,28 @@ final class IrisBiomeLayerGenerator { { KList layerHeightGenerators = new KList<>(); - int m = 7235; + int m = seedOffset; - for (IrisBiomePaletteLayer i : biome.getLayers()) { - layerHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), rdata)); + for (IrisBiomePaletteLayer layer : layers) { + layerHeightGenerators.add(layer.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data)); } return layerHeightGenerators; }); } - static KList getLayerSeaHeightGenerators(IrisBiome biome, RNG rng, IrisData data) { - AtomicCache> cache = biome.getLayerSeaHeightGenerators(); - KList cached = cache.getIfPresent(); - - if (cached != null) { - return cached; - } - - return cache.aquire(() -> - { - KList layerSeaHeightGenerators = new KList<>(); - - int m = 7735; - - for (IrisBiomePaletteLayer i : biome.getSeaLayers()) { - layerSeaHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data)); + private static void appendLayer(KList blocks, IrisBiomePaletteLayer layer, + int layerIndex, int thickness, double x, double z, + RNG random, int maxDepth, IrisData data) { + double zoom = layer.getZoom(); + for (int offset = 0; offset < thickness && blocks.size() < maxDepth; offset++) { + try { + blocks.add(layer.get(random, layerIndex + offset, + (x + offset) / zoom, offset, (z - offset) / zoom, data)); + } catch (Throwable error) { + IrisLogging.reportError(error); } - - return layerSeaHeightGenerators; - }); + } } static PlatformBlockState getSurfaceBlock(IrisBiome biome, int x, int z, RNG rng, IrisData idm) { diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackBiomeLayerValidatorTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackBiomeLayerValidatorTest.java index a95fee979..d2d92ad19 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackBiomeLayerValidatorTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackBiomeLayerValidatorTest.java @@ -11,7 +11,6 @@ import java.nio.file.Path; import java.util.List; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class PackBiomeLayerValidatorTest { @@ -19,14 +18,12 @@ public class PackBiomeLayerValidatorTest { public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void rejectsMoreCeilingLayersThanLayers() throws Exception { + public void acceptsMoreCeilingLayersThanSurfaceLayers() throws Exception { File pack = temporaryFolder.newFolder("pack"); write(pack, "biomes/nested/cave.json", "{\"layers\":[{}],\"caveCeilingLayers\":[{},{},{}]}"); - assertEquals(List.of( - "Biome 'nested/cave' declares 3 caveCeilingLayers but only 1 layers. caveCeilingLayers reuses the layers height generators and must not have more entries." - ), PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes"))); + assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty()); } @Test @@ -35,9 +32,7 @@ public class PackBiomeLayerValidatorTest { write(pack, "biomes/defaulted.json", "{\"name\":\"Defaulted\"}"); write(pack, "biomes/implicit.json", "{\"caveCeilingLayers\":[{},{}]}"); - assertEquals(List.of( - "Biome 'implicit' declares 2 caveCeilingLayers but only 1 layers. caveCeilingLayers reuses the layers height generators and must not have more entries." - ), PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes"))); + assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty()); } @Test @@ -45,7 +40,7 @@ public class PackBiomeLayerValidatorTest { File pack = temporaryFolder.newFolder("pack"); write(pack, "biomes/ok.json", "{\"layers\":[{},{},{}],\"caveCeilingLayers\":[{},{}]}"); - assertTrue(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes")).isEmpty()); + assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty()); } @Test @@ -56,11 +51,11 @@ public class PackBiomeLayerValidatorTest { assertEquals(List.of( "Biome 'bad' layers must be an array.", "Biome 'bad' caveCeilingLayers must be an array." - ), PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes"))); + ), PackBiomeLayerValidator.validateLayers(new File(pack, "biomes"))); } @Test - public void ceilingLayerOverflowBlocksFullPackValidation() throws Exception { + public void independentCeilingLayersPassFullPackValidation() throws Exception { File pack = temporaryFolder.newFolder("pack"); write(pack, "dimensions/main.json", "{\"regions\":[\"region\"]}"); write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}"); @@ -68,9 +63,7 @@ public class PackBiomeLayerValidatorTest { PackValidationResult result = PackValidator.validate(pack); - assertFalse(result.isLoadable()); - assertTrue(result.getBlockingErrors().contains( - "Biome 'biome' declares 2 caveCeilingLayers but only 1 layers. caveCeilingLayers reuses the layers height generators and must not have more entries.")); + assertTrue(result.getBlockingErrors().toString(), result.isLoadable()); } @Test diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java index 387e865cd..96cdf4aab 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java @@ -35,12 +35,15 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -114,6 +117,45 @@ public class IrisCaveCarver3DNearParityTest { carveForcedAirField.setAccessible(true); } + @Test + public void carvingReleasesSectionReferencesAfterSuccessAndFailure() throws Exception { + for (boolean fail : new boolean[]{false, true}) { + Engine engine = createEngine(128, 110); + IrisCaveProfile profile = createProfile(false, false).setAdaptiveSampling(false) + .setAllowFluid(false).setDetailWeight(0D) + .setDensityThreshold(new IrisStyledRange(2D, 2D, new IrisGeneratorStyle(NoiseStyle.FLAT))); + IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile); + WriterCapture capture = createWriterCapture(128); + if (fail) { + AtomicInteger samples = new AtomicInteger(); + CNG density = mock(CNG.class); + doAnswer(invocation -> { + if (samples.incrementAndGet() > 300) { + throw new IllegalStateException("Density sample failed"); + } + return -1D; + }).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble()); + baseDensityField.set(carver, density); + assertThrows(IllegalStateException.class, () -> carver.carve(capture.writer, 0, 0, + fullWeights(), 0D, 0D, null, filledHeights(110))); + } else { + carver.carve(capture.writer, 0, 0, fullWeights(), 0D, 0D, null, filledHeights(110)); + } + assertFalse(capture.carvedCells.isEmpty()); + Field scratchField = IrisCaveCarver3D.class.getDeclaredField("scratchCache"); + scratchField.setAccessible(true); + ThreadLocal scratchCache = (ThreadLocal) scratchField.get(carver); + CaveCarveScratch scratch = (CaveCarveScratch) scratchCache.get(); + assertTrue(scratch.sectionMatter.length > 0); + for (Matter matter : scratch.sectionMatter) { + assertNull(matter); + } + for (MatterSlice slice : scratch.sectionSlices) { + assertNull(slice); + } + } + } + @Test public void parallelDensityPreservesExactAndAdaptiveCavernsAndLiquids() throws Exception { try (ForkJoinPool pool = new ForkJoinPool(4)) { diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentScratchTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentScratchTest.java new file mode 100644 index 000000000..4ce06a67b --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentScratchTest.java @@ -0,0 +1,170 @@ +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineMetrics; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisCaveProfile; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.matter.Matter; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +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.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MantleCarvingComponentScratchTest { + private MantleCarvingComponent component; + private IrisDimension dimension; + private ChunkContext context; + private MantleWriter writer; + + @Before + @SuppressWarnings("unchecked") + public void prepare() { + Engine engine = mock(Engine.class); + EngineMantle mantle = mock(EngineMantle.class); + when(mantle.getEngine()).thenReturn(engine); + component = new MantleCarvingComponent(mantle); + dimension = new IrisDimension(); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getWorld()).thenReturn(mock(IrisWorld.class)); + when(engine.getMetrics()).thenReturn(new EngineMetrics(16)); + IrisComplex complex = mock(IrisComplex.class); + when(complex.allowsMantleChunkWrite(anyInt(), anyInt())).thenReturn(true); + ProceduralStream heights = mock(ProceduralStream.class); + when(heights.getDouble(anyDouble(), anyDouble())).thenAnswer(invocation -> { + double x = invocation.getArgument(0); + return x < 0D ? 90D : 70D; + }); + when(complex.getNaturalHeightStream()).thenReturn(heights); + ProceduralStream regions = mock(ProceduralStream.class); + ProceduralStream biomes = mock(ProceduralStream.class); + ProceduralStream fluids = mock(ProceduralStream.class); + when(regions.get(anyDouble(), anyDouble())).thenReturn(new IrisRegion()); + when(biomes.get(anyDouble(), anyDouble())).thenReturn(new IrisBiome()); + when(complex.getRegionStream()).thenReturn(regions); + when(complex.getNaturalTrueBiomeStream()).thenReturn(biomes); + when(complex.getCaveBiomeStream()).thenReturn(biomes); + when(complex.getFluidStream()).thenReturn(fluids); + context = mock(ChunkContext.class); + when(context.getComplex()).thenReturn(complex); + writer = mock(MantleWriter.class); + Mantle storage = mock(Mantle.class); + when(storage.getWorldHeight()).thenReturn(128); + when(writer.getMantle()).thenReturn(storage); + } + + @Test + public void nestedGenerationPreservesOuterColumnHeights() throws Exception { + IrisCaveCarver3D carver = installProfile(); + AtomicBoolean entered = new AtomicBoolean(); + AtomicInteger calls = new AtomicInteger(); + doAnswer(invocation -> { + int chunkX = invocation.getArgument(1); + int[] heights = invocation.getArgument(7); + int expectedHeight = chunkX < 0 ? 90 : 70; + assertEquals(expectedHeight, heights[0]); + calls.incrementAndGet(); + if (entered.compareAndSet(false, true)) { + component.generateLayer(writer, -3, -3, context); + } + for (int height : heights) { + assertEquals(expectedHeight, height); + } + return 0; + }).when(carver).carve(any(), anyInt(), anyInt(), any(), anyDouble(), anyDouble(), + any(), any(), any(), any(), any()); + + component.generateLayer(writer, 2, 2, context); + + assertEquals(2, calls.get()); + assertReleased(); + } + + @Test + public void generationReleasesConfigurationReferencesWhenCarvingFails() throws Exception { + IrisCaveCarver3D carver = installProfile(); + doAnswer(invocation -> { + throw new IllegalStateException("Carving failed"); + }).when(carver).carve(any(), anyInt(), anyInt(), any(), anyDouble(), anyDouble(), + any(), any(), any(), any(), any()); + assertThrows(IllegalStateException.class, () -> component.generateLayer(writer, 2, 2, context)); + assertReleased(); + } + + @Test + public void changedProfilesReuseNumericBuffersWithoutKeepingProfileKeys() throws Exception { + Object firstBuffer = null; + for (int index = 0; index < 4; index++) { + installProfile(); + component.generateLayer(writer, 2, 2, context); + Object scratch = assertReleased(); + List buffers = (List) field(scratch, "weightBuffers"); + assertEquals(1, buffers.size()); + if (firstBuffer == null) { + firstBuffer = buffers.get(0); + } else { + assertSame(firstBuffer, buffers.get(0)); + } + } + } + + private IrisCaveCarver3D installProfile() throws Exception { + IrisCaveProfile profile = new IrisCaveProfile().setEnabled(true); + dimension.setCaveProfile(profile); + IrisCaveCarver3D carver = mock(IrisCaveCarver3D.class); + Map carvers = new IdentityHashMap<>(); + carvers.put(profile, carver); + Field cache = MantleCarvingComponent.class.getDeclaredField("profileCarvers"); + cache.setAccessible(true); + cache.set(component, carvers); + return carver; + } + + private Object assertReleased() throws Exception { + Field scratchField = MantleCarvingComponent.class.getDeclaredField("BLEND_SCRATCH"); + scratchField.setAccessible(true); + ThreadLocal scratchCache = (ThreadLocal) scratchField.get(null); + Object scratch = scratchCache.get(); + for (String name : List.of("profileField", "kernelProfiles", "fieldRegions", "fieldSurfaceBiomes", "fieldCaveBiomes")) { + for (Object value : (Object[]) field(scratch, name)) { + assertNull(value); + } + } + assertEquals(0, ((Map) field(scratch, "columnProfileWeights")).size()); + assertEquals(0, ((List) field(scratch, "profileOrder")).size()); + assertFalse((boolean) field(scratch, "inUse")); + return scratch; + } + + private Object field(Object object, String name) throws Exception { + Field field = object.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(object); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerGuardTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerGuardTest.java deleted file mode 100644 index a97e7938d..000000000 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerGuardTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.math.RNG; -import org.junit.Test; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - -public class IrisBiomeCeilingLayerGuardTest { - private static IrisBiome biome(int layers, int ceilingLayers) { - IrisBiome biome = new IrisBiome(); - KList surface = new KList<>(); - for (int i = 0; i < layers; i++) { - surface.add(new IrisBiomePaletteLayer().zero()); - } - KList ceiling = new KList<>(); - for (int i = 0; i < ceilingLayers; i++) { - IrisBiomePaletteLayer layer = new IrisBiomePaletteLayer().zero(); - layer.setMinHeight(0); - layer.setMaxHeight(0); - ceiling.add(layer); - } - biome.setLayers(surface); - biome.setCaveCeilingLayers(ceiling); - return biome; - } - - @Test - public void moreCeilingLayersThanLayersMustNotThrow() { - IrisBiome biome = biome(1, 3); - - KList result = biome.generateCeilingLayers( - new IrisDimension(), 0, 0, new RNG(1), 8, 64, mock(IrisData.class), null); - - assertTrue("extra ceiling layers are skipped, not crashed on", result.isEmpty()); - } - - @Test - public void inRangeCeilingLayersKeepWorking() { - IrisBiome biome = biome(3, 2); - - KList result = biome.generateCeilingLayers( - new IrisDimension(), 0, 0, new RNG(1), 8, 64, mock(IrisData.class), null); - - assertTrue("zeroed palettes with zero heights produce no blocks", result.isEmpty()); - } - - @Test - public void omittedCeilingLayersLeaveCavesUnchanged() { - assertTrue(new IrisBiome().getCaveCeilingLayers().isEmpty()); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerTest.java new file mode 100644 index 000000000..cc46a9c82 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerTest.java @@ -0,0 +1,78 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class IrisBiomeCeilingLayerTest { + @Test + public void emitsEveryCeilingLayerWithoutSurfaceLayers() { + PlatformBlockState first = mock(PlatformBlockState.class); + PlatformBlockState second = mock(PlatformBlockState.class); + PlatformBlockState third = mock(PlatformBlockState.class); + IrisBiome biome = new IrisBiome().setLayers(new KList<>()).setCaveCeilingLayers( + new KList<>(layer(first, 2), layer(second, 1), layer(third, 2))); + + KList result = biome.generateCeilingLayers( + new IrisDimension(), 13, -27, new RNG(1), 8, 64, null, null); + + assertEquals(List.of(first, first, second, third, third), result); + assertNull(biome.getLayerHeightGenerators().getIfPresent()); + } + + @Test + public void ceilingThicknessDoesNotDependOnSurfaceDefinitionOrWarmOrder() { + IrisBiome first = variableBiome(1, 1); + IrisBiome second = variableBiome(7, 31); + RNG rng = new RNG(37); + second.getLayerHeightGenerators(rng, null); + for (int x = -200; x <= 200; x += 7) { + KList firstCeiling = first.generateCeilingLayers( + new IrisDimension(), x, -x, rng, 100, 120, null, null); + KList secondCeiling = second.generateCeilingLayers( + new IrisDimension(), x, -x, rng, 100, 120, null, null); + assertEquals(firstCeiling.size(), secondCeiling.size()); + } + first.getLayerHeightGenerators(rng, null); + assertNotSame(first.getLayerHeightGenerators().getIfPresent().get(0), + first.getLayerCeilingHeightGenerators().getIfPresent().get(0)); + } + + @Test + public void capsCeilingLayersAtAvailableDepth() { + PlatformBlockState first = mock(PlatformBlockState.class); + PlatformBlockState second = mock(PlatformBlockState.class); + IrisBiome biome = new IrisBiome().setCaveCeilingLayers(new KList<>(layer(first, 2), layer(second, 3))); + assertEquals(List.of(first, first, second), biome.generateCeilingLayers( + new IrisDimension(), 0, 0, new RNG(1), 3, 64, null, null)); + assertTrue(biome.generateCeilingLayers( + new IrisDimension(), 0, 0, new RNG(1), 0, 64, null, null).isEmpty()); + } + + @Test + public void omittedCeilingLayersLeaveCavesUnchanged() { + assertTrue(new IrisBiome().getCaveCeilingLayers().isEmpty()); + } + + static IrisBiomePaletteLayer layer(PlatformBlockState block, int thickness) { + IrisBiomePaletteLayer layer = new IrisBiomePaletteLayer().setMinHeight(thickness).setMaxHeight(thickness); + layer.getBlockData().aquire(() -> new KList<>(block)); + return layer; + } + + private static IrisBiome variableBiome(int surfaceMinimum, int surfaceMaximum) { + PlatformBlockState block = mock(PlatformBlockState.class); + return new IrisBiome() + .setLayers(new KList<>(layer(block, surfaceMinimum).setMaxHeight(surfaceMaximum))) + .setCaveCeilingLayers(new KList<>(layer(block, 1).setMaxHeight(9))); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeLockedLayerTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeLockedLayerTest.java new file mode 100644 index 000000000..e2d0a604c --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeLockedLayerTest.java @@ -0,0 +1,35 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +public class IrisBiomeLockedLayerTest { + @Test + public void repeatsLockedLayersAcrossTheReferenceHeight() { + PlatformBlockState first = mock(PlatformBlockState.class); + PlatformBlockState second = mock(PlatformBlockState.class); + PlatformBlockState third = mock(PlatformBlockState.class); + IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(8).setLayers(new KList<>( + IrisBiomeCeilingLayerTest.layer(first, 1), + IrisBiomeCeilingLayerTest.layer(second, 1), + IrisBiomeCeilingLayerTest.layer(third, 1))); + List cycle = List.of(first, second, third); + int[] heights = {1, 509, 512, 513, 1000, Integer.MAX_VALUE}; + int[][] expectedCycles = {{1, 0, 2}, {0, 2, 1}, {0, 2, 1}, {2, 1, 0}, {1, 0, 2}, {1, 0, 2}}; + for (int index = 0; index < heights.length; index++) { + KList blocks = biome.generateLockedLayers( + 13, -27, new RNG(37), 8, heights[index], null, null); + assertEquals(8, blocks.size()); + for (int depth = 0; depth < blocks.size(); depth++) { + assertEquals(cycle.get(expectedCycles[index][depth % 3]), blocks.get(depth)); + } + } + } +}