From f4a06efa21f07bf3fb06fc1787c59e0ab049a7e9 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:41:08 -0400 Subject: [PATCH] Terrain column painter - Paint chunks z-major so neighbouring columns share block-buffer cache lines - Fill the solid run below layers, river paint and cave ceilings with ores or rock in one loop - Resolve chunk constants, air and bedrock once per chunk; drop the unread shared lastBedrock write - IrisOreBands replaces IrisOreGeneratorBounds: per-Y candidate generators in list order - Resolve a constant (FLAT) ore chance once instead of sampling it per block - Column parity test against the per-block algorithm over randomized columns --- .../iris/generation/biome/IrisBiome.java | 26 +- .../iris/generation/biome/IrisBiomeOres.java | 22 +- .../generation/decoration/IrisOreBands.java | 99 +++ .../decoration/IrisOreGenerator.java | 25 +- .../decoration/IrisOreGeneratorBounds.java | 45 -- .../stage/IrisTerrainNormalActuator.java | 574 ++++++++++-------- .../generation/terrain/IrisDimension.java | 26 +- .../iris/generation/terrain/IrisRegion.java | 26 +- .../decoration/IrisOreBandsTest.java | 134 ++++ .../DimensionTerrain3DCompositionTest.java | 2 +- ...TerrainNormalActuatorColumnParityTest.java | 425 +++++++++++++ ...risTerrainNormalActuatorHydrologyTest.java | 8 +- ...risTerrainNormalActuatorStrataCutTest.java | 2 +- 13 files changed, 1028 insertions(+), 386 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/generation/decoration/IrisOreBands.java delete mode 100644 core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGeneratorBounds.java create mode 100644 core/src/test/java/art/arcane/iris/generation/decoration/IrisOreBandsTest.java create mode 100644 core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorColumnParityTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java index 266f8d3b9..b1b448b9a 100644 --- a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java @@ -24,7 +24,7 @@ import art.arcane.iris.generation.decoration.IrisDecorator; import art.arcane.iris.generation.decoration.IrisDepositGenerator; import art.arcane.iris.generation.decoration.IrisDepositVariant; import art.arcane.iris.generation.decoration.IrisOreGenerator; -import art.arcane.iris.generation.decoration.IrisOreGeneratorBounds; +import art.arcane.iris.generation.decoration.IrisOreBands; import art.arcane.iris.generation.decoration.IrisProceduralObjects; import art.arcane.iris.generation.hydrology.IrisRiverPolicy; import art.arcane.iris.generation.noise.IrisGeneratorStyle; @@ -120,8 +120,8 @@ public class IrisBiome extends IrisRegistrant implements Rarity { private final transient IrisBiomeLayerHeightCache layerSeaHeightGenerators = new IrisBiomeLayerHeightCache(); private final transient AtomicCache> surfaceOreCache = new AtomicCache<>(); private final transient AtomicCache> undergroundOreCache = new AtomicCache<>(); - private final transient AtomicCache surfaceOreBoundsCache = new AtomicCache<>(); - private final transient AtomicCache undergroundOreBoundsCache = new AtomicCache<>(); + private final transient AtomicCache surfaceOreBandsCache = new AtomicCache<>(); + private final transient AtomicCache undergroundOreBandsCache = new AtomicCache<>(); private final transient AtomicCache> surfaceOreReplaceableBlockData = new AtomicCache<>(); private final transient AtomicCache> decoratorBuckets = new AtomicCache<>(); private final transient AtomicCache derivativeResolved = new AtomicCache<>(); @@ -338,8 +338,8 @@ public class IrisBiome extends IrisRegistrant implements Rarity { this.ores = ores == null ? new KList<>() : ores; surfaceOreCache.reset(); undergroundOreCache.reset(); - surfaceOreBoundsCache.reset(); - undergroundOreBoundsCache.reset(); + surfaceOreBandsCache.reset(); + undergroundOreBandsCache.reset(); } public IrisBiome setSurfaceOreReplaceableBlocks(KList surfaceOreReplaceableBlocks) { @@ -348,20 +348,12 @@ public class IrisBiome extends IrisRegistrant implements Rarity { return this; } - public KList getSurfaceOreGenerators() { - return IrisBiomeOres.getSurfaceOreGenerators(this); + public IrisOreBands getSurfaceOreBands() { + return IrisBiomeOres.getSurfaceOreBands(this); } - public KList getUndergroundOreGenerators() { - return IrisBiomeOres.getUndergroundOreGenerators(this); - } - - public IrisOreGeneratorBounds getSurfaceOreGeneratorBounds() { - return IrisBiomeOres.getSurfaceOreGeneratorBounds(this); - } - - public IrisOreGeneratorBounds getUndergroundOreGeneratorBounds() { - return IrisBiomeOres.getUndergroundOreGeneratorBounds(this); + public IrisOreBands getUndergroundOreBands() { + return IrisBiomeOres.getUndergroundOreBands(this); } public Biome getDerivative() { diff --git a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeOres.java b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeOres.java index 6fe084f33..2b1691b8d 100644 --- a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeOres.java +++ b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeOres.java @@ -19,7 +19,7 @@ package art.arcane.iris.generation.biome; import art.arcane.iris.generation.decoration.IrisOreGenerator; -import art.arcane.iris.generation.decoration.IrisOreGeneratorBounds; +import art.arcane.iris.generation.decoration.IrisOreBands; import art.arcane.iris.generation.decoration.IrisProceduralBlocks; import art.arcane.iris.pack.loading.IrisData; @@ -73,30 +73,22 @@ final class IrisBiomeOres { .contains(IrisProceduralBlocks.materialKey(state)); } - static KList getSurfaceOreGenerators(IrisBiome biome) { - return getOres(biome, true); - } - - static KList getUndergroundOreGenerators(IrisBiome biome) { - return getOres(biome, false); - } - - static IrisOreGeneratorBounds getSurfaceOreGeneratorBounds(IrisBiome biome) { + static IrisOreBands getSurfaceOreBands(IrisBiome biome) { // getIfPresent fast path: aquire allocates a capturing lambda even on a hit, and this // runs per column on the terrain hot path. - IrisOreGeneratorBounds cached = biome.getSurfaceOreBoundsCache().getIfPresent(); + IrisOreBands cached = biome.getSurfaceOreBandsCache().getIfPresent(); if (cached != null) { return cached; } - return biome.getSurfaceOreBoundsCache().aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres(biome))); + return biome.getSurfaceOreBandsCache().aquire(() -> IrisOreBands.of(getSurfaceOres(biome))); } - static IrisOreGeneratorBounds getUndergroundOreGeneratorBounds(IrisBiome biome) { - IrisOreGeneratorBounds cached = biome.getUndergroundOreBoundsCache().getIfPresent(); + static IrisOreBands getUndergroundOreBands(IrisBiome biome) { + IrisOreBands cached = biome.getUndergroundOreBandsCache().getIfPresent(); if (cached != null) { return cached; } - return biome.getUndergroundOreBoundsCache().aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres(biome))); + return biome.getUndergroundOreBandsCache().aquire(() -> IrisOreBands.of(getUndergroundOres(biome))); } private static KList getSurfaceOres(IrisBiome biome) { diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreBands.java b/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreBands.java new file mode 100644 index 000000000..b70f0681e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreBands.java @@ -0,0 +1,99 @@ +package art.arcane.iris.generation.decoration; + +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.pack.value.IrisRange; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +import java.util.Arrays; + +/** + * One ore list (a biome's, region's or dimension's surface or underground ores) indexed by Y. Each Y + * holds the generators whose range contains it, in list order, so resolving a block tries exactly + * the generators that could place there and still returns the first one that does. + */ +public final class IrisOreBands { + public static final IrisOreBands EMPTY = new IrisOreBands(new IrisOreGenerator[0], 0, -1, null); + private static final int MAXIMUM_INDEXED_HEIGHT = 8192; + + private final IrisOreGenerator[] generators; + private final int minY; + private final int maxY; + private final IrisOreGenerator[][] byY; + + private IrisOreBands(IrisOreGenerator[] generators, int minY, int maxY, IrisOreGenerator[][] byY) { + this.generators = generators; + this.minY = minY; + this.maxY = maxY; + this.byY = byY; + } + + public static IrisOreBands of(KList ores) { + if (ores == null || ores.isEmpty()) { + return EMPTY; + } + + IrisOreGenerator[] generators = ores.toArray(new IrisOreGenerator[0]); + double min = Double.POSITIVE_INFINITY; + double max = Double.NEGATIVE_INFINITY; + for (IrisOreGenerator generator : generators) { + IrisRange range = generator.getRange(); + min = Math.min(min, range.getMin()); + max = Math.max(max, range.getMax()); + } + if (!(min <= max)) { + return EMPTY; + } + + int minY = (int) Math.ceil(min); + int maxY = (int) Math.floor(max); + if (minY > maxY) { + return EMPTY; + } + long height = (long) maxY - minY + 1L; + return new IrisOreBands(generators, minY, maxY, + height > MAXIMUM_INDEXED_HEIGHT ? null : index(generators, minY, (int) height)); + } + + private static IrisOreGenerator[][] index(IrisOreGenerator[] generators, int minY, int height) { + IrisOreGenerator[][] byY = new IrisOreGenerator[height][]; + IrisOreGenerator[] scratch = new IrisOreGenerator[generators.length]; + IrisOreGenerator[] previous = null; + for (int offset = 0; offset < height; offset++) { + int y = minY + offset; + int count = 0; + for (IrisOreGenerator generator : generators) { + if (generator.getRange().contains(y)) { + scratch[count++] = generator; + } + } + IrisOreGenerator[] candidates = Arrays.copyOf(scratch, count); + byY[offset] = previous != null && Arrays.equals(previous, candidates) ? previous : candidates; + previous = byY[offset]; + } + return byY; + } + + public boolean hasOres() { + return minY <= maxY; + } + + public boolean contains(int y) { + return y >= minY && y <= maxY; + } + + /** + * The ore placed at a Y inside {@link #contains(int)}, or null when no generator places one. + */ + public NativeBlockState generate(int x, int y, int z, RNG rng, IrisData data) { + IrisOreGenerator[] candidates = byY == null ? generators : byY[y - minY]; + for (IrisOreGenerator candidate : candidates) { + NativeBlockState ore = candidate.generate(x, y, z, rng, data); + if (ore != null) { + return ore; + } + } + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGenerator.java b/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGenerator.java index b4e7dd711..a5cd15539 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGenerator.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGenerator.java @@ -55,10 +55,10 @@ public class IrisOreGenerator { @Description("Vertical band (min, max) this ore can generate in, in engine-local Y where 0 is the bottom of the dimension, not world Y.") private IrisRange range = new IrisRange(30, 80); - private transient AtomicCache chanceCache = new AtomicCache<>(); + private transient AtomicCache chanceCache = new AtomicCache<>(); public void warm(RNG rng, IrisData data) { - chanceCache.aquire(() -> chanceStyle.create(rng, data)); + chanceCache.aquire(() -> OreChance.of(chanceStyle.create(rng, data))); palette.getLayerGenerator(rng, data); } @@ -71,15 +71,30 @@ public class IrisOreGenerator { return null; } - CNG chance = chanceCache.getIfPresent(); + OreChance chance = chanceCache.getIfPresent(); if (chance == null) { - chance = chanceCache.aquire(() -> chanceStyle.create(rng, data)); + chance = chanceCache.aquire(() -> OreChance.of(chanceStyle.create(rng, data))); } - if (chance.noise(x, y, z) > threshold) { + if (chance.sample(x, y, z) > threshold) { return null; } return palette.get(rng, x, y, z, data); } + + /** + * The chance field, with its single value resolved once when it is constant (a FLAT style), + * which bands such as a full deepslate layer sample at every block. + */ + private record OreChance(CNG noise, boolean constant, double value) { + private static OreChance of(CNG noise) { + boolean constant = noise.isConstant(); + return new OreChance(noise, constant, constant ? noise.noise(0D, 0D, 0D) : 0D); + } + + private double sample(int x, int y, int z) { + return constant ? value : noise.noise(x, y, z); + } + } } diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGeneratorBounds.java b/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGeneratorBounds.java deleted file mode 100644 index eb44f3e94..000000000 --- a/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGeneratorBounds.java +++ /dev/null @@ -1,45 +0,0 @@ -package art.arcane.iris.generation.decoration; - -import art.arcane.iris.pack.value.IrisRange; - -import art.arcane.volmlib.util.collection.KList; - -public final class IrisOreGeneratorBounds { - public static final IrisOreGeneratorBounds EMPTY = new IrisOreGeneratorBounds(false, 0D, 0D); - - private final boolean hasOres; - private final double min; - private final double max; - - private IrisOreGeneratorBounds(boolean hasOres, double min, double max) { - this.hasOres = hasOres; - this.min = min; - this.max = max; - } - - public static IrisOreGeneratorBounds of(KList ores) { - if (ores == null || ores.isEmpty()) { - return EMPTY; - } - - double min = Double.POSITIVE_INFINITY; - double max = Double.NEGATIVE_INFINITY; - int oreCount = ores.size(); - for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { - IrisOreGenerator oreGenerator = ores.get(oreIndex); - IrisRange range = oreGenerator.getRange(); - min = Math.min(min, range.getMin()); - max = Math.max(max, range.getMax()); - } - - return new IrisOreGeneratorBounds(true, min, max); - } - - public boolean hasOres() { - return hasOres; - } - - public boolean contains(int y) { - return hasOres && y >= min && y <= max; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java index 03d8c0ecb..9e5bf6754 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java @@ -30,8 +30,7 @@ import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.decoration.IrisProceduralBlocks; import art.arcane.iris.generation.hydrology.IrisSurfaceRiverBedConfig; import art.arcane.iris.generation.terrain.IrisDimension; -import art.arcane.iris.generation.decoration.IrisOreGenerator; -import art.arcane.iris.generation.decoration.IrisOreGeneratorBounds; +import art.arcane.iris.generation.decoration.IrisOreBands; import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.hydrology.IrisRiverMaterialConfig; import art.arcane.iris.generation.hydrology.IrisSurfaceRiverBankConfig; @@ -52,8 +51,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator h, boolean multicore, ChunkContext context) { PrecisionStopwatch p = PrecisionStopwatch.start(); - - for (int xf = 0; xf < h.getWidth(); xf++) { - terrainSliver(x, z, xf, h, context); - } - + paint(x, z, h, context); getEngine().getMetrics().getTerrain().put(p.getMilliseconds()); } /** - * This is calling 1/16th of a chunk x/z slice. It is a plane from sky to bedrock 1 thick in the x direction. + * Paints every column of the chunk from its top down to bedrock. Columns are walked z-major so + * consecutive columns share the cache lines of the x-minor block buffer. * - * @param x the chunk x in blocks - * @param z the chunk z in blocks - * @param xf the current x slice - * @param h the blockdata + * @param x the chunk x in blocks + * @param z the chunk z in blocks */ @BlockCoordinates - public void terrainSliver(int x, int z, int xf, Hunk h, ChunkContext context) { - int chunkHeight = h.getHeight(); - int chunkDepth = h.getDepth(); - IrisDimension dimension = getDimension(); - IrisData data = getData(); - IrisComplex complex = getComplex(); - ProceduralStream riverWaterSurfaceStream = complex.getRiverWaterSurfaceStream(); - RNG localRng = rng; - boolean bedrockEnabled = dimension.isBedrock(); - boolean hideOres = dimension.isHideOresForHiddenOre(); - ChunkedDataCache biomeCache = context.getBiome(); - ChunkedDataCache regionCache = context.getRegion(); - ChunkedDataCache rockCache = context.getRock(); - int realX = xf + x; - UpperDimensionContext upperContext = getEngine().getUpperContext(); - // Dimension-level ore lookups are chunk-invariant; resolving them per column paid - // four accessor chains times 256 per chunk for constants. - KList dimensionSurfaceOres = hideOres ? null : dimension.getSurfaceOreGenerators(); - KList dimensionUndergroundOres = hideOres ? null : dimension.getUndergroundOreGenerators(); - IrisOreGeneratorBounds dimensionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getSurfaceOreGeneratorBounds(); - IrisOreGeneratorBounds dimensionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getUndergroundOreGeneratorBounds(); - IrisSurfaceRiverBankConfig riverBanks = dimension.getHydrology() == null - ? null - : dimension.getHydrology().getRivers().getSurface().getBanks(); - boolean exposeCutStrata = riverBanks != null && riverBanks.isExposeCutStrata(); - IrisSurfaceRiverBedConfig riverBed = dimension.getHydrology() == null - ? null - : dimension.getHydrology().getRivers().getSurface().getBed(); - boolean padRiverBed = riverBed != null && !riverBed.isAllowGravityBlocks(); - IrisRiverMaterialConfig bedMaterial = riverBed == null ? null : riverBed.getMaterial(); - IrisRiverMaterialConfig shoreMaterial = riverBanks == null ? null : riverBanks.getShoreMaterial(); - IrisRiverMaterialConfig bankMaterial = riverBanks == null ? null : riverBanks.getBankMaterial(); - - for (int zf = 0; zf < chunkDepth; zf++) { - int realZ = zf + z; - IrisBiome biome = biomeCache.get(xf, zf); - IrisRegion region = regionCache.get(xf, zf); - int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf)); - int surfaceFluidHeight = Math.min( - chunkHeight, - (int) Math.round(riverWaterSurfaceStream.getDouble(realX, realZ)) - ); - int hf = Math.max(surfaceFluidHeight, he); - if (hf < 0) { - continue; - } - - int topY = Math.min(hf, chunkHeight - 1); - HydrologyColumnSample hydrology = complex.sampleHydrologyColumn(realX, realZ); - HydrologyColumnLayer hydrologyFluid = hydrology == null - ? null - : hydrology.primarySurfaceFluidLayerOrNull(); - HydrologyColumnLayer hydrologyTerrain = hydrology == null - ? null - : hydrology.primarySurfaceLayerOrNull(); - Terrain3DColumn terrainColumn = complex.terrainColumn(realX, realZ, hydrology); - int terrainSpan = terrainColumn == null ? -1 : terrainColumn.spanCount() - 1; - int layerSurfaceY = he; - int layerCeilingY = 0; - boolean exposeRiverStrata = exposeCutStrata - && hydrologyTerrain != null - && hydrologyTerrain.terrainOwned() - && !hydrologyTerrain.channel(); - Terrain3DColumn naturalColumn = exposeRiverStrata ? complex.naturalTerrainColumn(realX, realZ) : null; - int cut = 0; - boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned(); - IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial( - hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial); - NativeBlockState fluid = hydrologyFluid == null || complex.getTransitionDisplacement() != null - ? complex.resolveSurfaceFluid(realX, realZ) - : complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ); - NativeBlockState rock = rockCache.get(xf, zf); - NativeBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ); - KList biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators(); - KList regionSurfaceOres = hideOres ? null : region.getSurfaceOreGenerators(); - KList biomeUndergroundOres = hideOres ? null : biome.getUndergroundOreGenerators(); - KList regionUndergroundOres = hideOres ? null : region.getUndergroundOreGenerators(); - IrisOreGeneratorBounds biomeSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : biome.getSurfaceOreGeneratorBounds(); - IrisOreGeneratorBounds regionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : region.getSurfaceOreGeneratorBounds(); - IrisOreGeneratorBounds biomeUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : biome.getUndergroundOreGeneratorBounds(); - IrisOreGeneratorBounds regionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : region.getUndergroundOreGeneratorBounds(); - boolean hasSurfaceOres = biomeSurfaceOreBounds.hasOres() || regionSurfaceOreBounds.hasOres() || dimensionSurfaceOreBounds.hasOres(); - boolean hasUndergroundOres = biomeUndergroundOreBounds.hasOres() || regionUndergroundOreBounds.hasOres() || dimensionUndergroundOreBounds.hasOres(); - KList blocks = null; - KList ceilingBlocks = null; - KList fblocks = null; - - for (int i = topY; i >= 0; i--) { - if (i == 0 && bedrockEnabled) { - h.setRaw(xf, i, zf, BEDROCK.get()); - lastBedrock = i; - continue; - } - - if (terrainColumn != null && i <= he) { - while (terrainSpan >= 0 && i < terrainColumn.ceiling(terrainSpan)) { - terrainSpan--; - blocks = null; - ceilingBlocks = null; - } - if (terrainSpan < 0 || i > terrainColumn.floor(terrainSpan)) { - h.setRaw(xf, i, zf, AIR.get()); - continue; - } - layerSurfaceY = terrainColumn.floor(terrainSpan); - layerCeilingY = terrainColumn.ceiling(terrainSpan); - } - - NativeBlockState ore = null; - if (hasSurfaceOres) { - if (biomeSurfaceOreBounds.contains(i)) { - ore = generateOres(biomeSurfaceOres, realX, i, realZ, localRng, data); - } - if (ore == null && regionSurfaceOreBounds.contains(i)) { - ore = generateOres(regionSurfaceOres, realX, i, realZ, localRng, data); - } - if (ore == null && dimensionSurfaceOreBounds.contains(i)) { - ore = generateOres(dimensionSurfaceOres, realX, i, realZ, localRng, data); - } - } - if (ore != null) { - h.setRaw(xf, i, zf, ore); - continue; - } - - if (i > he && i <= hf) { - int fdepth = hf - i; - if (hydrologyFluid == null && fblocks == null) { - fblocks = biome.generateSeaLayers(realX, realZ, localRng, hf - he, data); - } - h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select( - fblocks, - fdepth, - fluid, - hydrologyFluid != null - )); - continue; - } - - if (i <= he) { - int depth = layerSurfaceY - i; - if (depth == 0 && mappedSurfaceBlock != null) { - h.setRaw(xf, i, zf, mappedSurfaceBlock); - continue; - } - if (blocks == null) { - if (exposeRiverStrata) { - int naturalSurfaceY = naturalColumn == null ? hydrology.naturalHeight() : naturalColumn.surfaceY(layerSurfaceY); - cut = Math.max(0, naturalSurfaceY - layerSurfaceY); - } - blocks = biome.generateLayers(dimension, realX, realZ, localRng, - layerSurfaceY + cut, layerSurfaceY + cut, data, complex); - } - - if (layerCeilingY > 0 && depth >= 2 && i - layerCeilingY < 2) { - if (ceilingBlocks == null) { - ceilingBlocks = biome.generateCeilingLayers(dimension, realX, realZ, localRng, - 2, layerCeilingY, data, complex); - } - int ceilingDepth = i - layerCeilingY; - h.setRaw(xf, i, zf, ceilingBlocks.hasIndex(ceilingDepth) - ? ceilingBlocks.get(ceilingDepth) : rock); - continue; - } - - int strataIndex = strataIndex(depth, cut, blocks.size()); - NativeBlockState layerBlock = paintHydrologyMaterial( - blocks.hasIndex(strataIndex) ? blocks.get(strataIndex) : null, - roleMaterial, depth, localRng, realX, i, realZ, data); - if (layerBlock != null) { - if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) { - layerBlock = riverBed.getPaddingPalette().get(localRng, realX, i, realZ, data); - } - h.setRaw(xf, i, zf, layerBlock); - continue; - } - - if (hasUndergroundOres) { - if (biomeUndergroundOreBounds.contains(i)) { - ore = generateOres(biomeUndergroundOres, realX, i, realZ, localRng, data); - } - if (ore == null && regionUndergroundOreBounds.contains(i)) { - ore = generateOres(regionUndergroundOres, realX, i, realZ, localRng, data); - } - if (ore == null && dimensionUndergroundOreBounds.contains(i)) { - ore = generateOres(dimensionUndergroundOres, realX, i, realZ, localRng, data); - } - } - - if (ore != null) { - h.setRaw(xf, i, zf, ore); - } else { - h.setRaw(xf, i, zf, rock); - } - } - } - - if (upperContext != null) { - UpperDimensionContext.Column upperColumn = upperContext.sampleColumn(realX, realZ); - int upperSurfaceY = upperColumn.surfaceY(); - - if (upperSurfaceY < chunkHeight - 1) { - IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ); - NativeBlockState upperRock = upperContext.getRockBlock(realX, realZ); - NativeBlockState upperMappedSurface = upperContext.getSurfaceBlock(realX, realZ); - KList upperBlocks = null; - int paletteSourceY = -1; - - for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) { - if (y == chunkHeight - 1 && bedrockEnabled) { - h.setRaw(xf, y, zf, BEDROCK.get()); - continue; - } - if (!upperColumn.isSolid(y)) { - h.setRaw(xf, y, zf, AIR.get()); - continue; - } - int faceY = upperColumn.faceY(y); - int sourceSurfaceY = upperColumn.height() - 1 - faceY; - if (sourceSurfaceY != paletteSourceY) { - paletteSourceY = sourceSurfaceY; - upperBlocks = null; - } - if (y == faceY && upperMappedSurface != null) { - h.setRaw(xf, y, zf, upperMappedSurface); - continue; - } - int depthFromFace = y - faceY; - if (upperBlocks == null && upperBiome != null) { - upperBlocks = upperBiome.generateLayersWithSlope(upperContext.getDimension(), - realX, realZ, localRng, sourceSurfaceY, sourceSurfaceY, - upperContext.getData(), upperContext.getSurfaceSlopeStream(sourceSurfaceY)); - } - if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) { - h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace)); - } else { - h.setRaw(xf, y, zf, upperRock); - } - } - } + public void paint(int x, int z, Hunk h, ChunkContext context) { + ColumnPainter painter = new ColumnPainter(x, z, h, context); + int width = h.getWidth(); + int depth = h.getDepth(); + for (int zf = 0; zf < depth; zf++) { + for (int xf = 0; xf < width; xf++) { + painter.paint(xf, zf); } } } @@ -385,20 +144,307 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator oreGenerators, int x, int y, int z, RNG rng, IrisData data) { - if (oreGenerators == null || oreGenerators.isEmpty()) { - return null; + private final class ColumnPainter { + private final int x; + private final int z; + private final Hunk h; + private final ChunkContext context; + private final int chunkHeight; + private final IrisDimension dimension; + private final IrisData data; + private final IrisComplex complex; + private final ProceduralStream riverWaterSurfaceStream; + private final boolean bedrockEnabled; + private final boolean hideOres; + private final boolean transitionDisplaced; + private final ChunkedDataCache biomeCache; + private final ChunkedDataCache regionCache; + private final ChunkedDataCache rockCache; + private final UpperDimensionContext upperContext; + private final IrisOreBands dimensionSurfaceOres; + private final IrisOreBands dimensionUndergroundOres; + private final boolean exposeCutStrata; + private final IrisSurfaceRiverBedConfig riverBed; + private final boolean padRiverBed; + private final IrisRiverMaterialConfig bedMaterial; + private final IrisRiverMaterialConfig shoreMaterial; + private final IrisRiverMaterialConfig bankMaterial; + private final NativeBlockState air; + private final NativeBlockState bedrock; + private IrisOreBands biomeSurfaceOres; + private IrisOreBands regionSurfaceOres; + private IrisOreBands biomeUndergroundOres; + private IrisOreBands regionUndergroundOres; + + private ColumnPainter(int x, int z, Hunk h, ChunkContext context) { + this.x = x; + this.z = z; + this.h = h; + this.context = context; + chunkHeight = h.getHeight(); + dimension = getDimension(); + data = getData(); + complex = getComplex(); + riverWaterSurfaceStream = complex.getRiverWaterSurfaceStream(); + bedrockEnabled = dimension.isBedrock(); + hideOres = dimension.isHideOresForHiddenOre(); + transitionDisplaced = complex.getTransitionDisplacement() != null; + biomeCache = context.getBiome(); + regionCache = context.getRegion(); + rockCache = context.getRock(); + upperContext = getEngine().getUpperContext(); + dimensionSurfaceOres = hideOres ? IrisOreBands.EMPTY : dimension.getSurfaceOreBands(); + dimensionUndergroundOres = hideOres ? IrisOreBands.EMPTY : dimension.getUndergroundOreBands(); + IrisSurfaceRiverBankConfig riverBanks = dimension.getHydrology() == null + ? null + : dimension.getHydrology().getRivers().getSurface().getBanks(); + exposeCutStrata = riverBanks != null && riverBanks.isExposeCutStrata(); + riverBed = dimension.getHydrology() == null + ? null + : dimension.getHydrology().getRivers().getSurface().getBed(); + padRiverBed = riverBed != null && !riverBed.isAllowGravityBlocks(); + bedMaterial = riverBed == null ? null : riverBed.getMaterial(); + shoreMaterial = riverBanks == null ? null : riverBanks.getShoreMaterial(); + bankMaterial = riverBanks == null ? null : riverBanks.getBankMaterial(); + air = AIR.get(); + bedrock = BEDROCK.get(); } - int oreCount = oreGenerators.size(); - for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { - IrisOreGenerator oreGenerator = oreGenerators.get(oreIndex); - NativeBlockState ore = oreGenerator.generate(x, y, z, rng, data); - if (ore != null) { - return ore; + private void paint(int xf, int zf) { + int realX = xf + x; + int realZ = zf + z; + IrisBiome biome = biomeCache.get(xf, zf); + IrisRegion region = regionCache.get(xf, zf); + int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf)); + int surfaceFluidHeight = Math.min( + chunkHeight, + (int) Math.round(riverWaterSurfaceStream.getDouble(realX, realZ)) + ); + int hf = Math.max(surfaceFluidHeight, he); + if (hf < 0) { + return; + } + + int topY = Math.min(hf, chunkHeight - 1); + HydrologyColumnSample hydrology = complex.sampleHydrologyColumn(realX, realZ); + HydrologyColumnLayer hydrologyFluid = hydrology == null + ? null + : hydrology.primarySurfaceFluidLayerOrNull(); + HydrologyColumnLayer hydrologyTerrain = hydrology == null + ? null + : hydrology.primarySurfaceLayerOrNull(); + Terrain3DColumn terrainColumn = complex.terrainColumn(realX, realZ, hydrology); + int terrainSpan = terrainColumn == null ? -1 : terrainColumn.spanCount() - 1; + int layerSurfaceY = he; + int layerCeilingY = 0; + boolean exposeRiverStrata = exposeCutStrata + && hydrologyTerrain != null + && hydrologyTerrain.terrainOwned() + && !hydrologyTerrain.channel(); + Terrain3DColumn naturalColumn = exposeRiverStrata ? complex.naturalTerrainColumn(realX, realZ) : null; + int cut = 0; + boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned(); + IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial( + hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial); + int paintDepth = roleMaterial == null ? 0 : roleMaterial.getDepth(); + NativeBlockState fluid = hydrologyFluid == null || transitionDisplaced + ? complex.resolveSurfaceFluid(realX, realZ) + : complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ); + NativeBlockState rock = rockCache.get(xf, zf); + NativeBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ); + biomeSurfaceOres = hideOres ? IrisOreBands.EMPTY : biome.getSurfaceOreBands(); + regionSurfaceOres = hideOres ? IrisOreBands.EMPTY : region.getSurfaceOreBands(); + biomeUndergroundOres = hideOres ? IrisOreBands.EMPTY : biome.getUndergroundOreBands(); + regionUndergroundOres = hideOres ? IrisOreBands.EMPTY : region.getUndergroundOreBands(); + boolean hasSurfaceOres = biomeSurfaceOres.hasOres() || regionSurfaceOres.hasOres() || dimensionSurfaceOres.hasOres(); + boolean hasUndergroundOres = biomeUndergroundOres.hasOres() || regionUndergroundOres.hasOres() || dimensionUndergroundOres.hasOres(); + int bedrockFloor = bedrockEnabled ? 1 : 0; + KList blocks = null; + KList ceilingBlocks = null; + KList fblocks = null; + + for (int i = topY; i >= 0; i--) { + if (i == 0 && bedrockEnabled) { + h.setRaw(xf, i, zf, bedrock); + continue; + } + + if (terrainColumn != null && i <= he) { + while (terrainSpan >= 0 && i < terrainColumn.ceiling(terrainSpan)) { + terrainSpan--; + blocks = null; + ceilingBlocks = null; + } + if (terrainSpan < 0 || i > terrainColumn.floor(terrainSpan)) { + h.setRaw(xf, i, zf, air); + continue; + } + layerSurfaceY = terrainColumn.floor(terrainSpan); + layerCeilingY = terrainColumn.ceiling(terrainSpan); + } + + NativeBlockState ore = hasSurfaceOres ? surfaceOre(realX, i, realZ) : null; + if (ore != null) { + h.setRaw(xf, i, zf, ore); + continue; + } + + if (i > he && i <= hf) { + int fdepth = hf - i; + if (hydrologyFluid == null && fblocks == null) { + fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, data); + } + h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select( + fblocks, + fdepth, + fluid, + hydrologyFluid != null + )); + continue; + } + + if (i <= he) { + int depth = layerSurfaceY - i; + if (depth == 0 && mappedSurfaceBlock != null) { + h.setRaw(xf, i, zf, mappedSurfaceBlock); + continue; + } + if (blocks == null) { + if (exposeRiverStrata) { + int naturalSurfaceY = naturalColumn == null ? hydrology.naturalHeight() : naturalColumn.surfaceY(layerSurfaceY); + cut = Math.max(0, naturalSurfaceY - layerSurfaceY); + } + blocks = biome.generateLayers(dimension, realX, realZ, rng, + layerSurfaceY + cut, layerSurfaceY + cut, data, complex); + } + + int deepFloor = Math.max(bedrockFloor, layerCeilingY > 0 ? layerCeilingY + 2 : 0); + if (i >= deepFloor && depth >= Math.max(blocks.size(), paintDepth)) { + paintDeep(xf, zf, realX, realZ, i, deepFloor, rock, hasSurfaceOres, hasUndergroundOres); + i = deepFloor; + continue; + } + + if (layerCeilingY > 0 && depth >= 2 && i - layerCeilingY < 2) { + if (ceilingBlocks == null) { + ceilingBlocks = biome.generateCeilingLayers(dimension, realX, realZ, rng, + 2, layerCeilingY, data, complex); + } + int ceilingDepth = i - layerCeilingY; + h.setRaw(xf, i, zf, ceilingBlocks.hasIndex(ceilingDepth) + ? ceilingBlocks.get(ceilingDepth) : rock); + continue; + } + + int strataIndex = strataIndex(depth, cut, blocks.size()); + NativeBlockState layerBlock = paintHydrologyMaterial( + blocks.hasIndex(strataIndex) ? blocks.get(strataIndex) : null, + roleMaterial, depth, rng, realX, i, realZ, data); + if (layerBlock != null) { + if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) { + layerBlock = riverBed.getPaddingPalette().get(rng, realX, i, realZ, data); + } + h.setRaw(xf, i, zf, layerBlock); + continue; + } + + ore = hasUndergroundOres ? undergroundOre(realX, i, realZ) : null; + h.setRaw(xf, i, zf, ore != null ? ore : rock); + } + } + + if (upperContext != null) { + paintUpper(xf, zf, realX, realZ); } } - return null; + /** + * Paints the solid run from {@code top} down to {@code floor}, which lies below every biome + * layer, river material, mapped surface and cave ceiling of its span: only ores or rock land there. + */ + private void paintDeep(int xf, int zf, int realX, int realZ, int top, int floor, NativeBlockState rock, + boolean hasSurfaceOres, boolean hasUndergroundOres) { + for (int y = top; y >= floor; y--) { + NativeBlockState ore = hasSurfaceOres ? surfaceOre(realX, y, realZ) : null; + if (ore == null && hasUndergroundOres) { + ore = undergroundOre(realX, y, realZ); + } + h.setRaw(xf, y, zf, ore != null ? ore : rock); + } + } + + private NativeBlockState surfaceOre(int realX, int y, int realZ) { + NativeBlockState ore = null; + if (biomeSurfaceOres.contains(y)) { + ore = biomeSurfaceOres.generate(realX, y, realZ, rng, data); + } + if (ore == null && regionSurfaceOres.contains(y)) { + ore = regionSurfaceOres.generate(realX, y, realZ, rng, data); + } + if (ore == null && dimensionSurfaceOres.contains(y)) { + ore = dimensionSurfaceOres.generate(realX, y, realZ, rng, data); + } + return ore; + } + + private NativeBlockState undergroundOre(int realX, int y, int realZ) { + NativeBlockState ore = null; + if (biomeUndergroundOres.contains(y)) { + ore = biomeUndergroundOres.generate(realX, y, realZ, rng, data); + } + if (ore == null && regionUndergroundOres.contains(y)) { + ore = regionUndergroundOres.generate(realX, y, realZ, rng, data); + } + if (ore == null && dimensionUndergroundOres.contains(y)) { + ore = dimensionUndergroundOres.generate(realX, y, realZ, rng, data); + } + return ore; + } + + private void paintUpper(int xf, int zf, int realX, int realZ) { + UpperDimensionContext.Column upperColumn = upperContext.sampleColumn(realX, realZ); + int upperSurfaceY = upperColumn.surfaceY(); + if (upperSurfaceY >= chunkHeight - 1) { + return; + } + + IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ); + NativeBlockState upperRock = upperContext.getRockBlock(realX, realZ); + NativeBlockState upperMappedSurface = upperContext.getSurfaceBlock(realX, realZ); + KList upperBlocks = null; + int paletteSourceY = -1; + + for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) { + if (y == chunkHeight - 1 && bedrockEnabled) { + h.setRaw(xf, y, zf, bedrock); + continue; + } + if (!upperColumn.isSolid(y)) { + h.setRaw(xf, y, zf, air); + continue; + } + int faceY = upperColumn.faceY(y); + int sourceSurfaceY = upperColumn.height() - 1 - faceY; + if (sourceSurfaceY != paletteSourceY) { + paletteSourceY = sourceSurfaceY; + upperBlocks = null; + } + if (y == faceY && upperMappedSurface != null) { + h.setRaw(xf, y, zf, upperMappedSurface); + continue; + } + int depthFromFace = y - faceY; + if (upperBlocks == null && upperBiome != null) { + upperBlocks = upperBiome.generateLayersWithSlope(upperContext.getDimension(), + realX, realZ, rng, sourceSurfaceY, sourceSurfaceY, + upperContext.getData(), upperContext.getSurfaceSlopeStream(sourceSurfaceY)); + } + if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) { + h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace)); + } else { + h.setRaw(xf, y, zf, upperRock); + } + } + } } } diff --git a/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimension.java b/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimension.java index 1daa6777c..ec55287a0 100644 --- a/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimension.java @@ -27,7 +27,7 @@ import art.arcane.iris.generation.cave.IrisCaveProfile; import art.arcane.iris.generation.decoration.IrisDepositGenerator; import art.arcane.iris.generation.decoration.IrisDepositVariant; import art.arcane.iris.generation.decoration.IrisOreGenerator; -import art.arcane.iris.generation.decoration.IrisOreGeneratorBounds; +import art.arcane.iris.generation.decoration.IrisOreBands; import art.arcane.iris.generation.decoration.tree.IrisTreeSettings; import art.arcane.iris.generation.hydrology.IrisDeepFluidConfig; import art.arcane.iris.generation.hydrology.IrisHydrology; @@ -137,8 +137,8 @@ public class IrisDimension extends IrisRegistrant { private final transient AtomicCache> carvingEntryIndex = new AtomicCache<>(); private final transient AtomicCache> surfaceOreCache = new AtomicCache<>(); private final transient AtomicCache> undergroundOreCache = new AtomicCache<>(); - private final transient AtomicCache surfaceOreBoundsCache = new AtomicCache<>(); - private final transient AtomicCache undergroundOreBoundsCache = new AtomicCache<>(); + private final transient AtomicCache surfaceOreBandsCache = new AtomicCache<>(); + private final transient AtomicCache undergroundOreBandsCache = new AtomicCache<>(); @MinNumber(2) @Required @Description("The human readable name of this dimension") @@ -484,24 +484,16 @@ public class IrisDimension extends IrisRegistrant { this.ores = ores == null ? new KList<>() : ores; surfaceOreCache.reset(); undergroundOreCache.reset(); - surfaceOreBoundsCache.reset(); - undergroundOreBoundsCache.reset(); + surfaceOreBandsCache.reset(); + undergroundOreBandsCache.reset(); } - public KList getSurfaceOreGenerators() { - return getOres(true); + public IrisOreBands getSurfaceOreBands() { + return surfaceOreBandsCache.aquire(() -> IrisOreBands.of(getSurfaceOres())); } - public KList getUndergroundOreGenerators() { - return getOres(false); - } - - public IrisOreGeneratorBounds getSurfaceOreGeneratorBounds() { - return surfaceOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres())); - } - - public IrisOreGeneratorBounds getUndergroundOreGeneratorBounds() { - return undergroundOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres())); + public IrisOreBands getUndergroundOreBands() { + return undergroundOreBandsCache.aquire(() -> IrisOreBands.of(getUndergroundOres())); } private KList getSurfaceOres() { diff --git a/core/src/main/java/art/arcane/iris/generation/terrain/IrisRegion.java b/core/src/main/java/art/arcane/iris/generation/terrain/IrisRegion.java index e37ef3dfd..56ef088e4 100644 --- a/core/src/main/java/art/arcane/iris/generation/terrain/IrisRegion.java +++ b/core/src/main/java/art/arcane/iris/generation/terrain/IrisRegion.java @@ -23,7 +23,7 @@ import art.arcane.iris.generation.cave.IrisCaveProfile; import art.arcane.iris.generation.decoration.IrisDepositGenerator; import art.arcane.iris.generation.decoration.IrisDepositVariant; import art.arcane.iris.generation.decoration.IrisOreGenerator; -import art.arcane.iris.generation.decoration.IrisOreGeneratorBounds; +import art.arcane.iris.generation.decoration.IrisOreBands; import art.arcane.iris.generation.decoration.IrisProceduralObjects; import art.arcane.iris.generation.hydrology.IrisRiverPolicy; import art.arcane.iris.pack.validation.CompatPools; @@ -88,8 +88,8 @@ public class IrisRegion extends IrisRegistrant implements Rarity { private final transient AtomicCache cacheColor = new AtomicCache<>(); private final transient AtomicCache> surfaceOreCache = new AtomicCache<>(); private final transient AtomicCache> undergroundOreCache = new AtomicCache<>(); - private final transient AtomicCache surfaceOreBoundsCache = new AtomicCache<>(); - private final transient AtomicCache undergroundOreBoundsCache = new AtomicCache<>(); + private final transient AtomicCache surfaceOreBandsCache = new AtomicCache<>(); + private final transient AtomicCache undergroundOreBandsCache = new AtomicCache<>(); @MinNumber(2) @Required @Description("The name of the region") @@ -217,24 +217,16 @@ public class IrisRegion extends IrisRegistrant implements Rarity { this.ores = ores == null ? new KList<>() : ores; surfaceOreCache.reset(); undergroundOreCache.reset(); - surfaceOreBoundsCache.reset(); - undergroundOreBoundsCache.reset(); + surfaceOreBandsCache.reset(); + undergroundOreBandsCache.reset(); } - public KList getSurfaceOreGenerators() { - return getOres(true); + public IrisOreBands getSurfaceOreBands() { + return surfaceOreBandsCache.aquire(() -> IrisOreBands.of(getSurfaceOres())); } - public KList getUndergroundOreGenerators() { - return getOres(false); - } - - public IrisOreGeneratorBounds getSurfaceOreGeneratorBounds() { - return surfaceOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres())); - } - - public IrisOreGeneratorBounds getUndergroundOreGeneratorBounds() { - return undergroundOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres())); + public IrisOreBands getUndergroundOreBands() { + return undergroundOreBandsCache.aquire(() -> IrisOreBands.of(getUndergroundOres())); } private KList getSurfaceOres() { diff --git a/core/src/test/java/art/arcane/iris/generation/decoration/IrisOreBandsTest.java b/core/src/test/java/art/arcane/iris/generation/decoration/IrisOreBandsTest.java new file mode 100644 index 000000000..afe6ccba6 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/decoration/IrisOreBandsTest.java @@ -0,0 +1,134 @@ +package art.arcane.iris.generation.decoration; + +import art.arcane.iris.generation.block.IrisBlockData; +import art.arcane.iris.generation.noise.IrisGeneratorStyle; +import art.arcane.iris.generation.noise.NoiseStyle; +import art.arcane.iris.generation.terrain.IrisMaterialPalette; +import art.arcane.iris.pack.value.IrisRange; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.noise.CNG; +import org.junit.Test; + +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IrisOreBandsTest { + private static final RNG RNG_SOURCE = new RNG(77L); + + @Test + public void bandsResolveTheSameOreAsScanningEveryGeneratorInOrder() { + Random random = new Random(1337L); + for (int trial = 0; trial < 200; trial++) { + KList ores = new KList<>(); + int count = 1 + random.nextInt(12); + for (int index = 0; index < count; index++) { + double min = random.nextInt(90) - 10 + (random.nextBoolean() ? 0D : random.nextDouble()); + double max = min + random.nextInt(40) + (random.nextBoolean() ? 0D : random.nextDouble()); + ores.add(generator(min, max, 0.2D + random.nextDouble() * 0.8D, 31L * trial + index)); + } + IrisOreBands bands = IrisOreBands.of(ores); + double lowest = Double.POSITIVE_INFINITY; + double highest = Double.NEGATIVE_INFINITY; + for (IrisOreGenerator ore : ores) { + lowest = Math.min(lowest, ore.getRange().getMin()); + highest = Math.max(highest, ore.getRange().getMax()); + } + for (int y = -20; y <= 140; y++) { + assertEquals("trial " + trial + " y " + y, y >= lowest && y <= highest, bands.contains(y)); + if (!bands.contains(y)) { + continue; + } + for (int x = -3; x <= 3; x++) { + assertSame("trial " + trial + " y " + y + " x " + x, + scan(ores, x, y, 5 - x), bands.generate(x, y, 5 - x, RNG_SOURCE, null)); + } + } + } + } + + @Test + public void emptyAndUnboundedListsHaveNoBands() { + assertFalse(IrisOreBands.of(null).hasOres()); + assertFalse(IrisOreBands.of(new KList<>()).contains(0)); + KList inverted = new KList<>(); + inverted.add(generator(40D, 10D, 1D, 3L)); + assertFalse(IrisOreBands.of(inverted).contains(20)); + } + + @Test + public void constantChanceMatchesSamplingTheFlatField() { + IrisOreGenerator flat = new IrisOreGenerator(); + IrisMaterialPalette palette = mock(IrisMaterialPalette.class); + NativeBlockState ore = mock(NativeBlockState.class); + when(palette.getPalette()).thenReturn(new KList<>(mock(IrisBlockData.class))); + when(palette.get(any(), anyDouble(), anyDouble(), anyDouble(), isNull())).thenReturn(ore); + IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.FLAT).setZoom(2.5D).setExponent(1.3D) + .setFracture(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(9D)); + flat.setPalette(palette).setChanceStyle(style).setRange(new IrisRange(0, 100)); + CNG reference = style.create(RNG_SOURCE, null); + for (double threshold : new double[]{0.1D, 0.5D, 0.99D, 1D}) { + flat.setThreshold(threshold); + for (int index = 0; index < 100; index++) { + int x = index * 37 - 900; + int y = index % 101; + int z = 400 - index * 11; + NativeBlockState expected = reference.noise(x, y, z) > threshold ? null : ore; + assertSame(expected, flat.generate(x, y, z, RNG_SOURCE, null)); + } + } + } + + private static NativeBlockState scan(KList ores, int x, int y, int z) { + for (IrisOreGenerator ore : ores) { + NativeBlockState state = ore.generate(x, y, z, RNG_SOURCE, null); + if (state != null) { + return state; + } + } + return null; + } + + private static IrisOreGenerator generator(double min, double max, double threshold, long seed) { + IrisOreGenerator generator = new IrisOreGenerator(); + IrisMaterialPalette palette = mock(IrisMaterialPalette.class); + NativeBlockState state = mock(NativeBlockState.class); + when(palette.getPalette()).thenReturn(new KList<>(mock(IrisBlockData.class))); + when(palette.get(any(), anyDouble(), anyDouble(), anyDouble(), isNull())).thenReturn(state); + CNG chance = mock(CNG.class); + when(chance.noise(anyDouble(), anyDouble(), anyDouble())).thenAnswer(invocation -> { + long hash = seed; + hash = hash * 31 + Double.doubleToLongBits(invocation.getArgument(0)); + hash = hash * 31 + Double.doubleToLongBits(invocation.getArgument(1)); + hash = hash * 31 + Double.doubleToLongBits(invocation.getArgument(2)); + return new Random(hash).nextDouble(); + }); + IrisGeneratorStyle style = mock(IrisGeneratorStyle.class); + when(style.create(any(), isNull())).thenReturn(chance); + return generator.setPalette(palette).setChanceStyle(style).setThreshold(threshold) + .setRange(new IrisRange(min, max)); + } + + @Test + public void generatorsWithDisjointRangesDoNotShadowEachOther() { + KList ores = new KList<>(); + ores.add(generator(0D, 0D, 1D, 1L)); + ores.add(generator(1D, 57D, 1D, 2L)); + ores.add(generator(58D, 58D, 1D, 3L)); + IrisOreBands bands = IrisOreBands.of(ores); + assertSame(scan(ores, 0, 0, 0), bands.generate(0, 0, 0, RNG_SOURCE, null)); + assertSame(scan(ores, 0, 30, 0), bands.generate(0, 30, 0, RNG_SOURCE, null)); + assertSame(scan(ores, 0, 58, 0), bands.generate(0, 58, 0, RNG_SOURCE, null)); + assertNull(bands.contains(59) ? bands.generate(0, 59, 0, RNG_SOURCE, null) : null); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/DimensionTerrain3DCompositionTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/DimensionTerrain3DCompositionTest.java index 58b74e61c..decd6087b 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/DimensionTerrain3DCompositionTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/DimensionTerrain3DCompositionTest.java @@ -291,7 +291,7 @@ public class DimensionTerrain3DCompositionTest { when(context.getRock()).thenReturn(rocks); when(context.getRoundedHeight(0, 0)).thenReturn(8); Hunk blocks = Hunk.newArrayHunk(1, 64, 1); - new IrisTerrainNormalActuator(engine).terrainSliver(0, 0, 0, blocks, context); + new IrisTerrainNormalActuator(engine).paint(0, 0, blocks, context); return blocks; } diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorColumnParityTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorColumnParityTest.java new file mode 100644 index 000000000..0534903c4 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorColumnParityTest.java @@ -0,0 +1,425 @@ +package art.arcane.iris.generation.stage; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.block.B; +import art.arcane.iris.generation.block.IrisBlockData; +import art.arcane.iris.generation.decoration.IrisProceduralBlocks; +import art.arcane.iris.generation.context.ChunkContext; +import art.arcane.iris.generation.context.ChunkedDataCache; +import art.arcane.iris.generation.decoration.IrisOreBands; +import art.arcane.iris.generation.decoration.IrisOreGenerator; +import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; +import art.arcane.iris.generation.hydrology.HydrologyColumnSample; +import art.arcane.iris.generation.hydrology.HydrologyFeatureRef; +import art.arcane.iris.generation.hydrology.HydrologyFeatureType; +import art.arcane.iris.generation.hydrology.IrisHydrology; +import art.arcane.iris.generation.hydrology.IrisRiverMaterialConfig; +import art.arcane.iris.generation.hydrology.IrisSurfaceRiverBedConfig; +import art.arcane.iris.generation.image.IrisImageMapRuntime; +import art.arcane.iris.generation.noise.IrisGeneratorStyle; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.runtime.SeedManager; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisMaterialPalette; +import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.iris.generation.terrain.Terrain3DColumn; +import art.arcane.iris.generation.terrain.Terrain3DColumnFixtures; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.pack.value.IrisRange; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.testsupport.PlatformBinding; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.hunk.Hunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.noise.CNG; +import art.arcane.volmlib.util.stream.ProceduralStream; +import art.arcane.volmlib.util.stream.interpolation.Interpolated; +import org.junit.Rule; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Paints randomized columns (Terrain3D spans with cave ceilings, biome layers, river materials, a + * mapped surface, bedrock, fluid, and surface and underground ores at every level) and compares + * each block with the per-block algorithm the painter replaced. + */ +public class IrisTerrainNormalActuatorColumnParityTest { + private static final int HEIGHT = 96; + private static final int X = 21; + private static final int Z = -7; + + @Rule + public final PlatformBinding platform = PlatformBinding.of(IrisTerrainNormalActuatorColumnParityTest::distinctBlocks); + + @Test + public void paintedColumnsMatchThePerBlockAlgorithm() { + Random random = new Random(20260928L); + for (int trial = 0; trial < 600; trial++) { + Fixture fixture = new Fixture(random, trial); + Hunk painted = Hunk.newArrayHunk(1, HEIGHT, 1); + new IrisTerrainNormalActuator(fixture.engine).paint(X, Z, painted, fixture.context); + Hunk expected = Hunk.newArrayHunk(1, HEIGHT, 1); + fixture.reference(expected); + for (int y = 0; y < HEIGHT; y++) { + assertEquals("trial " + trial + " y " + y + " " + fixture, + key(expected.getRaw(0, y, 0)), key(painted.getRaw(0, y, 0))); + } + } + } + + private static String key(NativeBlockState state) { + return state == null ? "null" : state.key(); + } + + private static IrisPlatform distinctBlocks() { + Map blocks = new HashMap<>(); + PlatformRegistries registries = mock(PlatformRegistries.class); + when(registries.block(anyString())).thenAnswer(invocation -> + blocks.computeIfAbsent(invocation.getArgument(0), Fixture::state)); + IrisPlatform platform = mock(IrisPlatform.class); + when(platform.registries()).thenReturn(registries); + return platform; + } + + private static final class Fixture { + private final Engine engine = mock(Engine.class); + private final ChunkContext context = mock(ChunkContext.class); + private final IrisComplex complex = mock(IrisComplex.class); + private final IrisData data = mock(IrisData.class); + private final IrisBiome biome = mock(IrisBiome.class); + private final IrisRegion region = new IrisRegion(); + private final IrisDimension dimension; + private final RNG rng = new RNG(0L); + private final NativeBlockState rock = state("rock"); + private final NativeBlockState fluid = state("water"); + private final NativeBlockState mapped; + private final Terrain3DColumn terrainColumn; + private final HydrologyColumnSample hydrology; + private final int he; + private final int surfaceFluid; + private final KList biomeSurface = new KList<>(); + private final KList biomeUnderground = new KList<>(); + private final KList regionSurface = new KList<>(); + private final KList regionUnderground = new KList<>(); + private final KList dimensionSurface = new KList<>(); + private final KList dimensionUnderground = new KList<>(); + private final String description; + + private Fixture(Random random, int trial) { + he = 3 + random.nextInt(HEIGHT); + surfaceFluid = random.nextInt(HEIGHT + 4); + boolean bedrock = random.nextBoolean(); + mapped = random.nextInt(4) == 0 ? state("mapped") : null; + int layerCount = random.nextInt(6); + int seaCount = random.nextInt(3); + int ceilingCount = random.nextInt(3); + terrainColumn = random.nextBoolean() ? null : spans(random, Math.min(he, HEIGHT - 1)); + IrisRiverMaterialConfig material = null; + if (random.nextInt(3) == 0) { + IrisMaterialPalette palette = mock(IrisMaterialPalette.class); + NativeBlockState riverBlock = state("river"); + when(palette.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), eq(data))).thenReturn(riverBlock); + material = new IrisRiverMaterialConfig().setEnabled(true).setDepth(random.nextInt(8)).setPalette(palette); + } + hydrology = material == null ? null : new HydrologyColumnSample(X, Z, he + 5, 0, false, "parent", + List.of(bankLayer(random.nextBoolean()))); + IrisHydrology rivers = new IrisHydrology(); + if (material != null) { + rivers.getRivers().getSurface().getBanks().setShoreMaterial(material).setBankMaterial(material); + } + for (int level = 0; level < 6; level++) { + KList target = List.of(biomeSurface, biomeUnderground, regionSurface, + regionUnderground, dimensionSurface, dimensionUnderground).get(level); + int count = random.nextInt(3) == 0 ? 0 : random.nextInt(4); + for (int index = 0; index < count; index++) { + double min = random.nextInt(HEIGHT); + target.add(ore(min, min + random.nextInt(30), 0.3D + random.nextDouble() * 0.7D, + level % 2 == 0, 97L * trial + 13L * level + index)); + } + } + KList regionOres = new KList<>(regionSurface); + regionOres.addAll(regionUnderground); + region.setOres(regionOres); + KList dimensionOres = new KList<>(dimensionSurface); + dimensionOres.addAll(dimensionUnderground); + dimension = new IrisDimension().setHydrology(rivers).setBedrock(bedrock).setHideOresForHiddenOre(false); + dimension.setOres(dimensionOres); + + when(engine.getDimension()).thenReturn(dimension); + when(engine.getComplex()).thenReturn(complex); + when(engine.getData()).thenReturn(data); + when(engine.getSeedManager()).thenReturn(mock(SeedManager.class)); + IrisImageMapRuntime imageMap = mock(IrisImageMapRuntime.class); + when(imageMap.sampleSurfaceBlock(anyDouble(), anyDouble())).thenReturn(mapped); + when(complex.getImageMapRuntime()).thenReturn(imageMap); + when(complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> (double) surfaceFluid)); + when(complex.sampleHydrologyColumn(X, Z)).thenReturn(hydrology); + when(complex.terrainColumn(eq(X), eq(Z), hydrology == null ? isNull() : any(HydrologyColumnSample.class))) + .thenReturn(terrainColumn); + when(complex.naturalTerrainColumn(X, Z)).thenReturn(terrainColumn); + when(complex.resolveSurfaceFluid(anyDouble(), anyDouble())).thenReturn(fluid); + when(context.getRoundedHeight(0, 0)).thenReturn(he); + when(context.getBiome()).thenReturn(new ChunkedDataCache<>( + ProceduralStream.of((x, z) -> biome, Interpolated.of(value -> 0D, value -> biome)), X, Z, false)); + when(context.getRegion()).thenReturn(new ChunkedDataCache<>( + ProceduralStream.of((x, z) -> region, Interpolated.of(value -> 0D, value -> region)), X, Z, false)); + when(context.getRock()).thenReturn(new ChunkedDataCache<>( + ProceduralStream.of((x, z) -> rock, Interpolated.of(value -> 0D, value -> rock)), X, Z, false)); + when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class), + anyInt(), anyInt(), eq(data), eq(complex))).thenAnswer(invocation -> layers("layer", layerCount)); + when(biome.generateSeaLayers(anyDouble(), anyDouble(), any(RNG.class), anyInt(), eq(data))) + .thenAnswer(invocation -> layers("sea", seaCount)); + when(biome.generateCeilingLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class), + anyInt(), anyInt(), eq(data), eq(complex))).thenAnswer(invocation -> layers("ceiling", ceilingCount)); + when(biome.getSurfaceOreBands()).thenReturn(IrisOreBands.of(biomeSurface)); + when(biome.getUndergroundOreBands()).thenReturn(IrisOreBands.of(biomeUnderground)); + description = "he=" + he + " fluid=" + surfaceFluid + " bedrock=" + bedrock + " mapped=" + (mapped != null) + + " layers=" + layerCount + " column=" + terrainColumn + " material=" + (material == null ? -1 : material.getDepth()); + } + + @Override + public String toString() { + return description; + } + + private void reference(Hunk h) { + int chunkHeight = h.getHeight(); + int xf = 0; + int zf = 0; + int realX = X; + int realZ = Z; + NativeBlockState bedrockState = B.getState("BEDROCK"); + NativeBlockState airState = B.getState("AIR"); + IrisSurfaceRiverBedConfig riverBed = dimension.getHydrology().getRivers().getSurface().getBed(); + int he = Math.min(chunkHeight, this.he); + int surfaceFluidHeight = Math.min(chunkHeight, this.surfaceFluid); + int hf = Math.max(surfaceFluidHeight, he); + if (hf < 0) { + return; + } + int topY = Math.min(hf, chunkHeight - 1); + HydrologyColumnLayer hydrologyFluid = hydrology == null ? null : hydrology.primarySurfaceFluidLayerOrNull(); + HydrologyColumnLayer hydrologyTerrain = hydrology == null ? null : hydrology.primarySurfaceLayerOrNull(); + int terrainSpan = terrainColumn == null ? -1 : terrainColumn.spanCount() - 1; + int layerSurfaceY = he; + int layerCeilingY = 0; + boolean exposeRiverStrata = dimension.getHydrology().getRivers().getSurface().getBanks().isExposeCutStrata() + && hydrologyTerrain != null && hydrologyTerrain.terrainOwned() && !hydrologyTerrain.channel(); + Terrain3DColumn naturalColumn = exposeRiverStrata ? terrainColumn : null; + int cut = 0; + boolean riverOwned = !riverBed.isAllowGravityBlocks() + && hydrologyTerrain != null && hydrologyTerrain.terrainOwned(); + IrisRiverMaterialConfig roleMaterial = IrisTerrainNormalActuator.hydrologyRoleMaterial(hydrologyTerrain, + riverBed.getMaterial(), + dimension.getHydrology().getRivers().getSurface().getBanks().getShoreMaterial(), + dimension.getHydrology().getRivers().getSurface().getBanks().getBankMaterial()); + OldBounds biomeSurfaceBounds = new OldBounds(biomeSurface); + OldBounds regionSurfaceBounds = new OldBounds(regionSurface); + OldBounds dimensionSurfaceBounds = new OldBounds(dimensionSurface); + OldBounds biomeUndergroundBounds = new OldBounds(biomeUnderground); + OldBounds regionUndergroundBounds = new OldBounds(regionUnderground); + OldBounds dimensionUndergroundBounds = new OldBounds(dimensionUnderground); + boolean hasSurfaceOres = biomeSurfaceBounds.hasOres || regionSurfaceBounds.hasOres || dimensionSurfaceBounds.hasOres; + boolean hasUndergroundOres = biomeUndergroundBounds.hasOres || regionUndergroundBounds.hasOres || dimensionUndergroundBounds.hasOres; + KList blocks = null; + KList ceilingBlocks = null; + KList fblocks = null; + + for (int i = topY; i >= 0; i--) { + if (i == 0 && dimension.isBedrock()) { + h.setRaw(xf, i, zf, bedrockState); + continue; + } + if (terrainColumn != null && i <= he) { + while (terrainSpan >= 0 && i < terrainColumn.ceiling(terrainSpan)) { + terrainSpan--; + blocks = null; + ceilingBlocks = null; + } + if (terrainSpan < 0 || i > terrainColumn.floor(terrainSpan)) { + h.setRaw(xf, i, zf, airState); + continue; + } + layerSurfaceY = terrainColumn.floor(terrainSpan); + layerCeilingY = terrainColumn.ceiling(terrainSpan); + } + NativeBlockState ore = null; + if (hasSurfaceOres) { + if (biomeSurfaceBounds.contains(i)) { + ore = scan(biomeSurface, realX, i, realZ); + } + if (ore == null && regionSurfaceBounds.contains(i)) { + ore = scan(regionSurface, realX, i, realZ); + } + if (ore == null && dimensionSurfaceBounds.contains(i)) { + ore = scan(dimensionSurface, realX, i, realZ); + } + } + if (ore != null) { + h.setRaw(xf, i, zf, ore); + continue; + } + if (i > he && i <= hf) { + int fdepth = hf - i; + if (hydrologyFluid == null && fblocks == null) { + fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, data); + } + h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select(fblocks, fdepth, fluid, hydrologyFluid != null)); + continue; + } + if (i <= he) { + int depth = layerSurfaceY - i; + if (depth == 0 && mapped != null) { + h.setRaw(xf, i, zf, mapped); + continue; + } + if (blocks == null) { + if (exposeRiverStrata) { + int naturalSurfaceY = naturalColumn == null ? hydrology.naturalHeight() : naturalColumn.surfaceY(layerSurfaceY); + cut = Math.max(0, naturalSurfaceY - layerSurfaceY); + } + blocks = biome.generateLayers(dimension, realX, realZ, rng, + layerSurfaceY + cut, layerSurfaceY + cut, data, complex); + } + if (layerCeilingY > 0 && depth >= 2 && i - layerCeilingY < 2) { + if (ceilingBlocks == null) { + ceilingBlocks = biome.generateCeilingLayers(dimension, realX, realZ, rng, 2, layerCeilingY, data, complex); + } + int ceilingDepth = i - layerCeilingY; + h.setRaw(xf, i, zf, ceilingBlocks.hasIndex(ceilingDepth) ? ceilingBlocks.get(ceilingDepth) : rock); + continue; + } + int strataIndex = IrisTerrainNormalActuator.strataIndex(depth, cut, blocks.size()); + NativeBlockState layerBlock = IrisTerrainNormalActuator.paintHydrologyMaterial( + blocks.hasIndex(strataIndex) ? blocks.get(strataIndex) : null, + roleMaterial, depth, rng, realX, i, realZ, data); + if (layerBlock != null) { + if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) { + layerBlock = riverBed.getPaddingPalette().get(rng, realX, i, realZ, data); + } + h.setRaw(xf, i, zf, layerBlock); + continue; + } + if (hasUndergroundOres) { + if (biomeUndergroundBounds.contains(i)) { + ore = scan(biomeUnderground, realX, i, realZ); + } + if (ore == null && regionUndergroundBounds.contains(i)) { + ore = scan(regionUnderground, realX, i, realZ); + } + if (ore == null && dimensionUndergroundBounds.contains(i)) { + ore = scan(dimensionUnderground, realX, i, realZ); + } + } + h.setRaw(xf, i, zf, ore != null ? ore : rock); + } + } + } + + private NativeBlockState scan(KList ores, int x, int y, int z) { + for (IrisOreGenerator ore : ores) { + NativeBlockState state = ore.generate(x, y, z, rng, data); + if (state != null) { + return state; + } + } + return null; + } + + private static KList layers(String name, int count) { + KList layers = new KList<>(); + for (int index = 0; index < count; index++) { + layers.add(state(name + index)); + } + return layers; + } + + private static Terrain3DColumn spans(Random random, int top) { + List boundaries = new ArrayList<>(); + boundaries.add(0); + int cursor = random.nextInt(Math.max(1, top / 3) + 1); + boundaries.add(cursor); + while (cursor + 3 < top && random.nextBoolean()) { + int ceiling = cursor + 2 + random.nextInt(Math.max(1, (top - cursor) / 2)); + if (ceiling >= top) { + break; + } + int floor = ceiling + random.nextInt(top - ceiling + 1); + boundaries.add(ceiling); + boundaries.add(floor); + cursor = floor; + } + return Terrain3DColumnFixtures.spans(top, boundaries.stream().mapToInt(Integer::intValue).toArray()); + } + + private IrisOreGenerator ore(double min, double max, double threshold, boolean surface, long seed) { + IrisOreGenerator generator = new IrisOreGenerator(); + IrisMaterialPalette palette = mock(IrisMaterialPalette.class); + NativeBlockState state = state("ore" + seed); + when(palette.getPalette()).thenReturn(new KList<>(mock(IrisBlockData.class))); + when(palette.get(any(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(state); + CNG chance = mock(CNG.class); + when(chance.noise(anyDouble(), anyDouble(), anyDouble())).thenAnswer(invocation -> { + long hash = seed * 31 + Double.doubleToLongBits(invocation.getArgument(1)); + return new Random(hash).nextDouble(); + }); + IrisGeneratorStyle style = mock(IrisGeneratorStyle.class); + when(style.create(any(), any())).thenReturn(chance); + return generator.setPalette(palette).setChanceStyle(style).setThreshold(threshold) + .setRange(new IrisRange(min, max)).setGenerateSurface(surface); + } + + private static HydrologyColumnLayer bankLayer(boolean shore) { + return new HydrologyColumnLayer( + new HydrologyFeatureRef(21L, HydrologyFeatureType.RIFFLE, 22L, 23L, X, 63, Z, 1, 0, false), + 60, 63, 63, false, shore, !shore, false, false, false, true, false, false, + "default", "river", "mouth", "shore", "bank", "cave"); + } + + private static NativeBlockState state(String key) { + NativeBlockState state = mock(NativeBlockState.class); + when(state.key()).thenReturn("minecraft:" + key); + return state; + } + } + + private static final class OldBounds { + private final boolean hasOres; + private final double min; + private final double max; + + private OldBounds(KList ores) { + double low = Double.POSITIVE_INFINITY; + double high = Double.NEGATIVE_INFINITY; + for (IrisOreGenerator ore : ores) { + low = Math.min(low, ore.getRange().getMin()); + high = Math.max(high, ore.getRange().getMax()); + } + hasOres = !ores.isEmpty(); + min = low; + max = high; + } + + private boolean contains(int y) { + return hasOres && y >= min && y <= max; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorHydrologyTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorHydrologyTest.java index 1ebd8f411..3b9577f2b 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorHydrologyTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorHydrologyTest.java @@ -263,13 +263,13 @@ public class IrisTerrainNormalActuatorHydrologyTest { anyInt(), anyInt(), eq(data), eq(complex))).thenReturn(new KList<>(List.of(grass))); Hunk output = Hunk.newArrayHunk(1, 64, 1); - new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context); + new IrisTerrainNormalActuator(engine).paint(11, -4, output, context); assertSame(painted, output.get(0, 60, 0)); assertSame(painted, output.get(0, 59, 0)); assertSame(rock, output.get(0, 58, 0)); material.setEnabled(false); - new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context); + new IrisTerrainNormalActuator(engine).paint(11, -4, output, context); assertSame(grass, output.get(0, 60, 0)); Terrain3DColumn carved = mock(Terrain3DColumn.class); @@ -285,11 +285,11 @@ public class IrisTerrainNormalActuatorHydrologyTest { when(complex.naturalTerrainColumn(11, -4)).thenReturn(natural); when(biome.generateCeilingLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class), anyInt(), anyInt(), eq(data), eq(complex))).thenReturn(new KList<>()); - new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context); + new IrisTerrainNormalActuator(engine).paint(11, -4, output, context); assertSame(grass, output.get(0, 60, 0)); assertSame(grass, output.get(0, 40, 0)); material.setEnabled(true); - new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context); + new IrisTerrainNormalActuator(engine).paint(11, -4, output, context); assertSame(painted, output.get(0, 60, 0)); assertSame(painted, output.get(0, 40, 0)); assertSame(rock, output.get(0, 38, 0)); diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorStrataCutTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorStrataCutTest.java index a8d51aa9e..e1a350da8 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorStrataCutTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuatorStrataCutTest.java @@ -163,7 +163,7 @@ public class IrisTerrainNormalActuatorStrataCutTest { private Hunk actuate() { Hunk output = Hunk.newArrayHunk(1, 64, 1); - new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context); + new IrisTerrainNormalActuator(engine).paint(11, -4, output, context); return output; } }