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
This commit is contained in:
Brian Neumann-Fopiano
2026-09-28 13:41:08 -04:00
parent 08353e3bdd
commit f4a06efa21
13 changed files with 1028 additions and 386 deletions
@@ -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<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> surfaceOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> undergroundOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> surfaceOreBandsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> undergroundOreBandsCache = new AtomicCache<>();
private final transient AtomicCache<KSet<String>> surfaceOreReplaceableBlockData = new AtomicCache<>();
private final transient AtomicCache<EnumMap<IrisDecorationPart, IrisDecorator[]>> decoratorBuckets = new AtomicCache<>();
private final transient AtomicCache<Biome> 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<String> surfaceOreReplaceableBlocks) {
@@ -348,20 +348,12 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
return this;
}
public KList<IrisOreGenerator> getSurfaceOreGenerators() {
return IrisBiomeOres.getSurfaceOreGenerators(this);
public IrisOreBands getSurfaceOreBands() {
return IrisBiomeOres.getSurfaceOreBands(this);
}
public KList<IrisOreGenerator> 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() {
@@ -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<IrisOreGenerator> getSurfaceOreGenerators(IrisBiome biome) {
return getOres(biome, true);
}
static KList<IrisOreGenerator> 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<IrisOreGenerator> getSurfaceOres(IrisBiome biome) {
@@ -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<IrisOreGenerator> 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;
}
}
@@ -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<CNG> chanceCache = new AtomicCache<>();
private transient AtomicCache<OreChance> 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);
}
}
}
@@ -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<IrisOreGenerator> 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;
}
}
@@ -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<NativeBloc
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
@Getter
private final RNG rng;
@Getter
private int lastBedrock = -1;
public IrisTerrainNormalActuator(Engine engine) {
super(engine, "Terrain");
@@ -64,263 +61,25 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
@Override
public void onActuate(int x, int z, Hunk<NativeBlockState> 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
*/
@BlockCoordinates
public void terrainSliver(int x, int z, int xf, Hunk<NativeBlockState> h, ChunkContext context) {
int chunkHeight = h.getHeight();
int chunkDepth = h.getDepth();
IrisDimension dimension = getDimension();
IrisData data = getData();
IrisComplex complex = getComplex();
ProceduralStream<Double> riverWaterSurfaceStream = complex.getRiverWaterSurfaceStream();
RNG localRng = rng;
boolean bedrockEnabled = dimension.isBedrock();
boolean hideOres = dimension.isHideOresForHiddenOre();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
ChunkedDataCache<NativeBlockState> 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<IrisOreGenerator> dimensionSurfaceOres = hideOres ? null : dimension.getSurfaceOreGenerators();
KList<IrisOreGenerator> 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<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
KList<IrisOreGenerator> regionSurfaceOres = hideOres ? null : region.getSurfaceOreGenerators();
KList<IrisOreGenerator> biomeUndergroundOres = hideOres ? null : biome.getUndergroundOreGenerators();
KList<IrisOreGenerator> 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<NativeBlockState> blocks = null;
KList<NativeBlockState> ceilingBlocks = null;
KList<NativeBlockState> 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<NativeBlockState> 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<NativeBlockState> 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<NativeBloc
return painted == null ? layerBlock : painted;
}
private NativeBlockState generateOres(KList<IrisOreGenerator> 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<NativeBlockState> h;
private final ChunkContext context;
private final int chunkHeight;
private final IrisDimension dimension;
private final IrisData data;
private final IrisComplex complex;
private final ProceduralStream<Double> riverWaterSurfaceStream;
private final boolean bedrockEnabled;
private final boolean hideOres;
private final boolean transitionDisplaced;
private final ChunkedDataCache<IrisBiome> biomeCache;
private final ChunkedDataCache<IrisRegion> regionCache;
private final ChunkedDataCache<NativeBlockState> 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<NativeBlockState> 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);
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<NativeBlockState> blocks = null;
KList<NativeBlockState> ceilingBlocks = null;
KList<NativeBlockState> 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);
}
}
/**
* 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;
}
return null;
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<NativeBlockState> 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);
}
}
}
}
}
@@ -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<Map<String, IrisDimensionCarvingEntry>> carvingEntryIndex = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> surfaceOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> undergroundOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> surfaceOreBandsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> 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<IrisOreGenerator> getSurfaceOreGenerators() {
return getOres(true);
public IrisOreBands getSurfaceOreBands() {
return surfaceOreBandsCache.aquire(() -> IrisOreBands.of(getSurfaceOres()));
}
public KList<IrisOreGenerator> 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<IrisOreGenerator> getSurfaceOres() {
@@ -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<Color> cacheColor = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> surfaceOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> undergroundOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> surfaceOreBandsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> 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<IrisOreGenerator> getSurfaceOreGenerators() {
return getOres(true);
public IrisOreBands getSurfaceOreBands() {
return surfaceOreBandsCache.aquire(() -> IrisOreBands.of(getSurfaceOres()));
}
public KList<IrisOreGenerator> 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<IrisOreGenerator> getSurfaceOres() {
@@ -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<IrisOreGenerator> 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<IrisOreGenerator> 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<IrisOreGenerator> 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<IrisOreGenerator> 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);
}
}
@@ -291,7 +291,7 @@ public class DimensionTerrain3DCompositionTest {
when(context.getRock()).thenReturn(rocks);
when(context.getRoundedHeight(0, 0)).thenReturn(8);
Hunk<NativeBlockState> 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;
}
@@ -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<NativeBlockState> painted = Hunk.newArrayHunk(1, HEIGHT, 1);
new IrisTerrainNormalActuator(fixture.engine).paint(X, Z, painted, fixture.context);
Hunk<NativeBlockState> 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<String, NativeBlockState> 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<IrisOreGenerator> biomeSurface = new KList<>();
private final KList<IrisOreGenerator> biomeUnderground = new KList<>();
private final KList<IrisOreGenerator> regionSurface = new KList<>();
private final KList<IrisOreGenerator> regionUnderground = new KList<>();
private final KList<IrisOreGenerator> dimensionSurface = new KList<>();
private final KList<IrisOreGenerator> 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<IrisOreGenerator> 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<IrisOreGenerator> regionOres = new KList<>(regionSurface);
regionOres.addAll(regionUnderground);
region.setOres(regionOres);
KList<IrisOreGenerator> 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<NativeBlockState> 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<NativeBlockState> blocks = null;
KList<NativeBlockState> ceilingBlocks = null;
KList<NativeBlockState> 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<IrisOreGenerator> 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<NativeBlockState> layers(String name, int count) {
KList<NativeBlockState> 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<Integer> 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<IrisOreGenerator> 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;
}
}
}
@@ -263,13 +263,13 @@ public class IrisTerrainNormalActuatorHydrologyTest {
anyInt(), anyInt(), eq(data), eq(complex))).thenReturn(new KList<>(List.of(grass)));
Hunk<NativeBlockState> 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));
@@ -163,7 +163,7 @@ public class IrisTerrainNormalActuatorStrataCutTest {
private Hunk<NativeBlockState> actuate() {
Hunk<NativeBlockState> 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;
}
}