diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/DepositRandom.java b/core/src/main/java/art/arcane/iris/generation/decoration/DepositRandom.java
new file mode 100644
index 000000000..44a86ee45
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/generation/decoration/DepositRandom.java
@@ -0,0 +1,82 @@
+/*
+ * Iris is a World Generator for Minecraft Bukkit Servers
+ * Copyright (c) 2026 Arcane Arts (Volmit Software)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package art.arcane.iris.generation.decoration;
+
+import art.arcane.volmlib.util.math.RNG;
+
+public final class DepositRandom extends RNG {
+ private static final long MULTIPLIER = 0x5DEECE66DL;
+ private static final long ADDEND = 0xBL;
+ private static final long MASK = (1L << 48) - 1L;
+ private static final int JUMP_TABLE_SIZE = 48;
+ private static final long[] JUMP_MULTIPLIERS = new long[JUMP_TABLE_SIZE];
+ private static final long[] JUMP_ADDENDS = new long[JUMP_TABLE_SIZE];
+
+ static {
+ long multiplier = MULTIPLIER;
+ long addend = ADDEND;
+ for (int bit = 0; bit < JUMP_TABLE_SIZE; bit++) {
+ JUMP_MULTIPLIERS[bit] = multiplier;
+ JUMP_ADDENDS[bit] = addend;
+ addend = (addend * multiplier + addend) & MASK;
+ multiplier = (multiplier * multiplier) & MASK;
+ }
+ }
+
+ private long state;
+
+ public DepositRandom() {
+ super(0L);
+ }
+
+ public DepositRandom(long seed) {
+ super(seed);
+ }
+
+ @Override
+ public void setSeed(long seed) {
+ state = (seed ^ MULTIPLIER) & MASK;
+ }
+
+ @Override
+ protected int next(int bits) {
+ long advanced = (state * MULTIPLIER + ADDEND) & MASK;
+ state = advanced;
+ return (int) (advanced >>> (48 - bits));
+ }
+
+ public long state() {
+ return state;
+ }
+
+ public void restore(long state) {
+ this.state = state & MASK;
+ }
+
+ public void skip(long steps) {
+ long remaining = steps;
+ long advanced = state;
+ for (int bit = 0; remaining != 0L && bit < JUMP_TABLE_SIZE; bit++, remaining >>>= 1) {
+ if ((remaining & 1L) != 0L) {
+ advanced = (advanced * JUMP_MULTIPLIERS[bit] + JUMP_ADDENDS[bit]) & MASK;
+ }
+ }
+ state = advanced;
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java b/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java
index 1ab82efa5..bb1d8b36d 100644
--- a/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java
+++ b/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java
@@ -58,6 +58,7 @@ import lombok.experimental.Accessors;
public class IrisDepositGenerator {
private static final int FOOTPRINT_MAXIMUM_SIZE = 64;
private static final ThreadLocal FOOTPRINT_CELLS = ThreadLocal.withInitial(FootprintCells::new);
+ private static final double[][] NODE_SINES = nodeSines();
private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L;
private static final long FNV_PRIME = 0x100000001b3L;
@@ -283,28 +284,52 @@ public class IrisDepositGenerator {
* only for clumps it keeps. Returns null without drawing when the shape has no footprint path.
*/
public ClumpFootprint sampleClumpFootprint(RNG rng, IrisData rdata) {
- if (shape != IrisDepositShape.VANILLA_ELLIPSOID || minSize > FOOTPRINT_MAXIMUM_SIZE
- || maxSize > FOOTPRINT_MAXIMUM_SIZE) {
- return null;
- }
- int paletteSize = getBlockData(rdata).size();
- if (paletteSize == 0) {
+ if (!usesClumpFootprints(rdata)) {
return null;
}
+ KList resolvedPalette = getBlockData(rdata);
+ int paletteSize = resolvedPalette.size();
int size = rng.i(minSize, maxSize + 1);
if (size <= 0) {
return ClumpFootprint.EMPTY;
}
FootprintCells cells = FOOTPRINT_CELLS.get();
cells.reset();
- vanillaEllipsoidCells(rng, size, cells);
- if (cells.count == 0) {
- return ClumpFootprint.EMPTY;
+ if (paletteSize == 1) {
+ emitEllipsoidRows(rng, size, cells);
+ if (cells.count == 0) {
+ return ClumpFootprint.EMPTY;
+ }
+ skipUniformPicks(rng, cells.count);
+ } else {
+ emitEllipsoidCells(rng, size, cells, cells.scratch);
+ if (cells.count == 0) {
+ return ClumpFootprint.EMPTY;
+ }
+ for (int cell = 0; cell < cells.count; cell++) {
+ cells.picks[cell] = rng.i(0, paletteSize);
+ }
}
- for (int cell = 0; cell < cells.count; cell++) {
- cells.picks[cell] = rng.i(0, paletteSize);
+ return new ClumpFootprint(cells.extentX * 2 + 1, cells.minY, cells, cells.stamp, resolvedPalette);
+ }
+
+ public boolean usesClumpFootprints(IrisData rdata) {
+ return shape == IrisDepositShape.VANILLA_ELLIPSOID && minSize <= FOOTPRINT_MAXIMUM_SIZE
+ && maxSize <= FOOTPRINT_MAXIMUM_SIZE && !getBlockData(rdata).isEmpty();
+ }
+
+ public static int lowestFootprintOffset(int size) {
+ return -4 - (Math.max(0, size) + 15) / 16;
+ }
+
+ private static void skipUniformPicks(RNG rng, int count) {
+ if (rng instanceof DepositRandom random) {
+ random.skip(2L * count);
+ return;
+ }
+ for (int cell = 0; cell < count; cell++) {
+ rng.nextDouble();
}
- return new ClumpFootprint(cells.extentX * 2 + 1, cells.minY, cells, cells.stamp, getBlockData(rdata));
}
/**
@@ -313,15 +338,87 @@ public class IrisDepositGenerator {
* test is bit-identical to evaluating {@code nx * nx + ny * ny + nz * nz} in place.
*/
static void vanillaEllipsoidCells(RNG rng, int size, CellSink sink) {
+ emitEllipsoidCells(rng, size, sink, new EllipsoidScratch());
+ }
+
+ private static void emitEllipsoidCells(RNG rng, int size, CellSink sink, EllipsoidScratch scratch) {
+ double[] nodes = ellipsoidNodes(rng, size, scratch);
+ for (int node = 0; node < size; node++) {
+ if (!scratch.sphere(nodes, node)) {
+ continue;
+ }
+ double[] squaredY = scratch.squaredY;
+ double[] squaredZ = scratch.squaredZ;
+ for (int x = scratch.minX; x <= scratch.maxX; x++) {
+ double nx = (x + 0.5D - scratch.centerX) / scratch.radius;
+ double squaredX = nx * nx;
+ if (squaredX >= 1D) {
+ continue;
+ }
+ for (int y = 0; y < scratch.spanY; y++) {
+ double squaredXY = squaredX + squaredY[y];
+ if (squaredXY >= 1D) {
+ continue;
+ }
+ for (int z = 0; z < scratch.spanZ; z++) {
+ if (squaredXY + squaredZ[z] < 1D) {
+ sink.add(x, scratch.minY + y, scratch.minZ + z);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static void emitEllipsoidRows(RNG rng, int size, FootprintCells cells) {
+ EllipsoidScratch scratch = cells.scratch;
+ double[] nodes = ellipsoidNodes(rng, size, scratch);
+ for (int node = 0; node < size; node++) {
+ if (!scratch.sphere(nodes, node)) {
+ continue;
+ }
+ double[] squaredY = scratch.squaredY;
+ double[] squaredZ = scratch.squaredZ;
+ int zShift = scratch.minZ + FootprintCells.OFFSET;
+ for (int x = scratch.minX; x <= scratch.maxX; x++) {
+ double nx = (x + 0.5D - scratch.centerX) / scratch.radius;
+ double squaredX = nx * nx;
+ if (squaredX >= 1D) {
+ continue;
+ }
+ for (int y = 0; y < scratch.spanY; y++) {
+ double squaredXY = squaredX + squaredY[y];
+ if (squaredXY >= 1D) {
+ continue;
+ }
+ long row = 0L;
+ for (int z = 0; z < scratch.spanZ; z++) {
+ if (squaredXY + squaredZ[z] < 1D) {
+ row |= 1L << (zShift + z);
+ }
+ }
+ if (row != 0L) {
+ cells.addRow(x, scratch.minY + y, row);
+ }
+ }
+ }
+ }
+ cells.finishRows();
+ }
+
+ private static double[] ellipsoidNodes(RNG rng, int size, EllipsoidScratch scratch) {
float angle = rng.nextFloat() * (float) Math.PI;
float reach = size / 8F;
- double startX = Math.sin(angle) * reach;
- double endX = -Math.sin(angle) * reach;
- double startZ = Math.cos(angle) * reach;
- double endZ = -Math.cos(angle) * reach;
+ double sine = Math.sin(angle);
+ double cosine = Math.cos(angle);
+ double startX = sine * reach;
+ double endX = -sine * reach;
+ double startZ = cosine * reach;
+ double endZ = -cosine * reach;
double startY = rng.nextInt(3) - 2;
double endY = rng.nextInt(3) - 2;
- double[] nodes = new double[size * 4];
+ double[] nodes = scratch.nodes(size * 4);
+ double[] sines = size < NODE_SINES.length ? NODE_SINES[size] : null;
for (int i = 0; i < size; i++) {
float progress = (float) i / size;
@@ -329,7 +426,8 @@ public class IrisDepositGenerator {
nodes[i * 4] = startX + (endX - startX) * progress;
nodes[i * 4 + 1] = startY + (endY - startY) * progress;
nodes[i * 4 + 2] = startZ + (endZ - startZ) * progress;
- nodes[i * 4 + 3] = ((Math.sin(Math.PI * progress) + 1D) * radiusNoise + 1D) / 2D;
+ double arc = sines == null ? Math.sin(Math.PI * progress) : sines[i];
+ nodes[i * 4 + 3] = ((arc + 1D) * radiusNoise + 1D) / 2D;
}
for (int i = 0; i < size - 1; i++) {
@@ -354,59 +452,20 @@ public class IrisDepositGenerator {
}
}
}
+ return nodes;
+ }
- double[] squaredY = new double[16];
- double[] squaredZ = new double[16];
- for (int i = 0; i < size; i++) {
- double radius = nodes[i * 4 + 3];
- if (radius < 0D) {
- continue;
- }
- double centerX = nodes[i * 4];
- double centerY = nodes[i * 4 + 1];
- double centerZ = nodes[i * 4 + 2];
- int minX = (int) Math.floor(centerX - radius);
- int maxX = Math.max((int) Math.floor(centerX + radius), minX);
- int minY = (int) Math.floor(centerY - radius);
- int maxY = Math.max((int) Math.floor(centerY + radius), minY);
- int minZ = (int) Math.floor(centerZ - radius);
- int maxZ = Math.max((int) Math.floor(centerZ + radius), minZ);
- int spanY = maxY - minY + 1;
- int spanZ = maxZ - minZ + 1;
- if (spanY > squaredY.length) {
- squaredY = new double[spanY];
- }
- if (spanZ > squaredZ.length) {
- squaredZ = new double[spanZ];
- }
- for (int y = 0; y < spanY; y++) {
- double ny = (minY + y + 0.5D - centerY) / radius;
- squaredY[y] = ny * ny;
- }
- for (int z = 0; z < spanZ; z++) {
- double nz = (minZ + z + 0.5D - centerZ) / radius;
- squaredZ[z] = nz * nz;
- }
-
- for (int x = minX; x <= maxX; x++) {
- double nx = (x + 0.5D - centerX) / radius;
- double squaredX = nx * nx;
- if (squaredX >= 1D) {
- continue;
- }
- for (int y = 0; y < spanY; y++) {
- double squaredXY = squaredX + squaredY[y];
- if (squaredXY >= 1D) {
- continue;
- }
- for (int z = 0; z < spanZ; z++) {
- if (squaredXY + squaredZ[z] < 1D) {
- sink.add(x, minY + y, minZ + z);
- }
- }
- }
+ private static double[][] nodeSines() {
+ double[][] sines = new double[FOOTPRINT_MAXIMUM_SIZE + 1][];
+ for (int size = 1; size <= FOOTPRINT_MAXIMUM_SIZE; size++) {
+ double[] values = new double[size];
+ for (int i = 0; i < size; i++) {
+ float progress = (float) i / size;
+ values[i] = Math.sin(Math.PI * progress);
}
+ sines[size] = values;
}
+ return sines;
}
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
@@ -672,14 +731,16 @@ public class IrisDepositGenerator {
}
public static final class ClumpCells {
- static final ClumpCells EMPTY = new ClumpCells(new int[0], new NativeBlockState[0]);
+ static final ClumpCells EMPTY = new ClumpCells(new int[0], new NativeBlockState[0], null);
private final int[] positions;
private final NativeBlockState[] blocks;
+ private final NativeBlockState uniform;
- private ClumpCells(int[] positions, NativeBlockState[] blocks) {
+ private ClumpCells(int[] positions, NativeBlockState[] blocks, NativeBlockState uniform) {
this.positions = positions;
this.blocks = blocks;
+ this.uniform = uniform;
}
public int size() {
@@ -699,7 +760,63 @@ public class IrisDepositGenerator {
}
public NativeBlockState block(int cell) {
- return blocks[cell];
+ return blocks == null ? uniform : blocks[cell];
+ }
+ }
+
+ private static final class EllipsoidScratch {
+ private double[] nodes = new double[256];
+ private double[] squaredY = new double[16];
+ private double[] squaredZ = new double[16];
+ private double centerX;
+ private double radius;
+ private int minX;
+ private int maxX;
+ private int minY;
+ private int spanY;
+ private int minZ;
+ private int spanZ;
+
+ private double[] nodes(int length) {
+ if (nodes.length < length) {
+ nodes = new double[length];
+ }
+ return nodes;
+ }
+
+ private boolean sphere(double[] values, int node) {
+ double sphereRadius = values[node * 4 + 3];
+ if (sphereRadius < 0D) {
+ return false;
+ }
+ double sphereX = values[node * 4];
+ double sphereY = values[node * 4 + 1];
+ double sphereZ = values[node * 4 + 2];
+ minX = (int) Math.floor(sphereX - sphereRadius);
+ maxX = Math.max((int) Math.floor(sphereX + sphereRadius), minX);
+ minY = (int) Math.floor(sphereY - sphereRadius);
+ int maxY = Math.max((int) Math.floor(sphereY + sphereRadius), minY);
+ minZ = (int) Math.floor(sphereZ - sphereRadius);
+ int maxZ = Math.max((int) Math.floor(sphereZ + sphereRadius), minZ);
+ spanY = maxY - minY + 1;
+ spanZ = maxZ - minZ + 1;
+ if (spanY > squaredY.length) {
+ squaredY = new double[spanY];
+ }
+ if (spanZ > squaredZ.length) {
+ squaredZ = new double[spanZ];
+ }
+ for (int y = 0; y < spanY; y++) {
+ double ny = (minY + y + 0.5D - sphereY) / sphereRadius;
+ squaredY[y] = ny * ny;
+ }
+ for (int z = 0; z < spanZ; z++) {
+ double nz = (minZ + z + 0.5D - sphereZ) / sphereRadius;
+ squaredZ[z] = nz * nz;
+ }
+ centerX = sphereX;
+ radius = sphereRadius;
+ return true;
}
}
@@ -708,6 +825,7 @@ public class IrisDepositGenerator {
private static final int AXIS = 64;
private final long[] occupied = new long[AXIS * AXIS * AXIS / 64];
private final int[] touched = new int[AXIS * AXIS * AXIS / 64];
+ private final EllipsoidScratch scratch = new EllipsoidScratch();
private int[] order = new int[256];
private int[] picks = new int[256];
private long[] sortKeys = new long[256];
@@ -715,6 +833,7 @@ public class IrisDepositGenerator {
private int count;
private int extentX;
private int minY;
+ private boolean uniform;
private long stamp;
private void reset() {
@@ -725,6 +844,7 @@ public class IrisDepositGenerator {
count = 0;
extentX = 0;
minY = Integer.MAX_VALUE;
+ uniform = false;
stamp++;
}
@@ -751,7 +871,35 @@ public class IrisDepositGenerator {
minY = Math.min(minY, y);
}
+ private void addRow(int x, int y, long row) {
+ int word = (x + OFFSET) * AXIS + (y + OFFSET);
+ long current = occupied[word];
+ if (current == 0L) {
+ touched[touchedCount++] = word;
+ }
+ occupied[word] = current | row;
+ }
+
+ private void finishRows() {
+ int cells = 0;
+ int extent = 0;
+ int lowest = Integer.MAX_VALUE;
+ for (int index = 0; index < touchedCount; index++) {
+ int word = touched[index];
+ cells += Long.bitCount(occupied[word]);
+ extent = Math.max(extent, Math.abs(word / AXIS - OFFSET));
+ lowest = Math.min(lowest, word % AXIS - OFFSET);
+ }
+ count = cells;
+ extentX = extent;
+ minY = lowest;
+ uniform = true;
+ }
+
private ClumpCells toCells(KList palette) {
+ if (uniform) {
+ return toUniformCells(palette.get(0));
+ }
for (int cell = 0; cell < count; cell++) {
sortKeys[cell] = ((long) order[cell] << 32) | picks[cell];
}
@@ -763,7 +911,22 @@ public class IrisDepositGenerator {
positions[cell] = (int) (key >>> 32);
blocks[cell] = palette.get((int) key);
}
- return new ClumpCells(positions, blocks);
+ return new ClumpCells(positions, blocks, null);
+ }
+
+ private ClumpCells toUniformCells(NativeBlockState block) {
+ Arrays.sort(touched, 0, touchedCount);
+ int[] positions = new int[count];
+ int cell = 0;
+ for (int index = 0; index < touchedCount; index++) {
+ int word = touched[index];
+ long row = occupied[word];
+ while (row != 0L) {
+ positions[cell++] = (word << 6) | Long.numberOfTrailingZeros(row);
+ row &= row - 1L;
+ }
+ }
+ return new ClumpCells(positions, null, block);
}
}
}
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
index a15ab4ca2..875e60ea2 100644
--- a/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreBands.java
+++ b/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreBands.java
@@ -1,5 +1,6 @@
package art.arcane.iris.generation.decoration;
+import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
@@ -94,10 +95,10 @@ public final class IrisOreBands {
/**
* 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) {
+ public NativeBlockState generate(int x, int y, int z, RNG rng, IrisData data, Engine engine) {
IrisOreGenerator[] candidates = byY == null ? generators : byY[y - minY];
for (IrisOreGenerator candidate : candidates) {
- NativeBlockState ore = candidate.generate(x, y, z, rng, data);
+ NativeBlockState ore = candidate.generate(x, y, z, rng, data, engine);
if (ore != null) {
return ore;
}
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 b5eeff86f..3c78c4c82 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
@@ -65,6 +65,10 @@ public class IrisOreGenerator {
}
public NativeBlockState generate(int x, int y, int z, RNG rng, IrisData data) {
+ return generate(x, y, z, rng, data, data == null ? null : data.getEngine());
+ }
+
+ public NativeBlockState generate(int x, int y, int z, RNG rng, IrisData data, Engine engine) {
if (threshold <= 0 || !range.contains(y)) {
return null;
}
@@ -73,7 +77,7 @@ public class IrisOreGenerator {
return null;
}
- if (chance(rng, data).sample(x, y, z) > threshold) {
+ if (chance(rng, data, engine).sample(x, y, z) > threshold) {
return null;
}
@@ -81,8 +85,11 @@ public class IrisOreGenerator {
}
private OreChance chance(RNG rng, IrisData data) {
+ return chance(rng, data, data == null ? null : data.getEngine());
+ }
+
+ private OreChance chance(RNG rng, IrisData data, Engine engine) {
long seed = rng.getSeed();
- Engine engine = data == null ? null : data.getEngine();
CachedChance recent = recentChance;
if (recent != null && recent.key.seed == seed && recent.key.data == data && recent.key.engine == engine) {
return recent.chance;
diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisDepositModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisDepositModifier.java
index a422b13a1..d5fe2fd07 100644
--- a/core/src/main/java/art/arcane/iris/generation/stage/IrisDepositModifier.java
+++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisDepositModifier.java
@@ -23,6 +23,8 @@ import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.EngineAssignedModifier;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.biome.IrisBiome;
+import art.arcane.iris.generation.chunk.ColumnExtent;
+import art.arcane.iris.generation.decoration.DepositRandom;
import art.arcane.iris.generation.decoration.IrisDepositBiomeScope;
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
@@ -55,8 +57,8 @@ import java.util.List;
import java.util.function.Predicate;
public class IrisDepositModifier extends EngineAssignedModifier {
- private static final int CLUMPS_PER_BATCH = 8;
- private static final int PREPARATION_BATCH_COUNT = 4;
+ private static final int PREPARATION_TASKS = 4;
+ private static final int MINIMUM_TASK_ATTEMPTS = 96;
private final RNG rng;
private volatile PackScan packScan;
@@ -88,31 +90,67 @@ public class IrisDepositModifier extends EngineAssignedModifier plans = new ArrayList<>(generators.size());
+ AboveTerrainHosts aboveTerrainHosts = new AboveTerrainHosts(terrain, context);
+ int attempts = 0;
for (int i = 0; i < generators.size(); i++) {
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)), context);
- for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
- int firstAttempt = first;
- int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
- int index = pending++;
- burst.queue(scopedDepositTask(
- () -> prepared[index] = prepare(plan, firstAttempt, limit, x, z, null, context, highestSurface),
- preparation, index));
- if (pending == prepared.length) {
- completeBatches(burst, prepared, chunk, terrain, x, z, preparation);
- pending = 0;
+ if (plan.attempts() == 0 || footprintsMissTerrain(plan, highestSurface)
+ || plan.generator().getPlacementScope() == IrisDepositPlacementScope.ABOVE_TERRAIN
+ && !aboveTerrainHosts.present(plan.generator().getSurfaceClearance())) {
+ continue;
+ }
+ plans.add(plan);
+ attempts += plan.attempts();
+ }
+ if (attempts == 0) {
+ return;
+ }
+ int tasks = multicore
+ ? Math.max(1, Math.min(PREPARATION_TASKS, Math.ceilDiv(attempts, MINIMUM_TASK_ATTEMPTS)))
+ : 1;
+ int attemptsPerTask = Math.ceilDiv(attempts, tasks);
+ preparation = new PreparationContext(context, generationEngine,
+ generationEngine == null ? null : generationEngine.captureGenerationRuntimeBinding(), new Throwable[tasks]);
+ PreparedDeposit[][] prepared = new PreparedDeposit[tasks][];
+ for (int task = 0; task < tasks; task++) {
+ int first = task * attemptsPerTask;
+ int last = Math.min(attempts, first + attemptsPerTask);
+ if (first >= last) {
+ break;
+ }
+ int index = task;
+ burst.queue(scopedDepositTask(
+ () -> prepared[index] = prepareSlice(plans, first, last, x, z, context, highestSurface),
+ preparation, index));
+ }
+ burst.complete();
+ Throwable failure = preparation.takeFailure(null);
+ if (failure instanceof Error error) {
+ throw error;
+ }
+ if (failure instanceof RuntimeException exception) {
+ throw exception;
+ }
+ if (failure != null) {
+ throw new IllegalStateException("Deposit preparation failed", failure);
+ }
+ try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) {
+ for (PreparedDeposit[] slice : prepared) {
+ if (slice == null) {
+ continue;
+ }
+ for (PreparedDeposit deposit : slice) {
+ place(deposit, chunk, terrain, x, z, null, context);
}
}
}
- completeBatches(burst, prepared, chunk, terrain, x, z, preparation);
} catch (Throwable failure) {
callerFailure = failure;
throw failure;
@@ -121,7 +159,7 @@ public class IrisDepositModifier extends EngineAssignedModifier terrain, int x, int z, PreparationContext preparation) {
- burst.complete();
- Throwable failure = preparation.takeFailure(null);
- if (failure instanceof Error error) {
- throw error;
- }
- if (failure instanceof RuntimeException exception) {
- throw exception;
- }
- if (failure != null) {
- throw new IllegalStateException("Deposit preparation failed", failure);
- }
- try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), preparation.chunk().getGenerationSessionId(), preparation.chunk())) {
- for (int i = 0; i < prepared.length; i++) {
- PreparedDeposit deposit = prepared[i];
- prepared[i] = null;
- if (deposit != null) {
- place(deposit, chunk, terrain, x, z, null, preparation.chunk());
- }
+ private PreparedDeposit[] prepareSlice(List plans, int first, int last, int cx, int cz,
+ ChunkContext context, int highestSurface) {
+ List deposits = new ArrayList<>();
+ int offset = 0;
+ for (DepositPlan plan : plans) {
+ int start = Math.max(first, offset);
+ int end = Math.min(last, offset + plan.attempts());
+ if (start < end) {
+ deposits.add(prepare(plan, start - offset, end - offset, cx, cz, null, context, highestSurface));
+ }
+ offset += plan.attempts();
+ if (offset >= last) {
+ break;
}
}
+ return deposits.toArray(new PreparedDeposit[0]);
}
private Runnable scopedDepositTask(Runnable task, PreparationContext preparation, int index) {
@@ -172,10 +204,25 @@ public class IrisDepositModifier extends EngineAssignedModifier data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) {
DepositPlan plan = plan(k, rng, context);
- for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
- int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
- place(prepare(plan, first, limit, cx, cz, he, context, Integer.MIN_VALUE), chunk, data, cx, cz, he, context);
+ if (plan.attempts() == 0) {
+ return;
}
+ place(prepare(plan, 0, plan.attempts(), cx, cz, he, context, Integer.MIN_VALUE), chunk, data, cx, cz, he, context);
+ }
+
+ private boolean footprintsMissTerrain(DepositPlan plan, int highestSurface) {
+ IrisDepositGenerator generator = plan.generator();
+ if (highestSurface == Integer.MIN_VALUE
+ || generator.getPlacementScope() != IrisDepositPlacementScope.TERRAIN
+ || !generator.usesClumpFootprints(getData())
+ || plan.ore() && packScan().oreScaling()) {
+ return false;
+ }
+ int minimum = generator.getHeightDistribution() == IrisDepositHeightDistribution.CLIPPED_UNIFORM
+ ? Math.max(0, generator.getMinHeight())
+ : generator.getMinHeight();
+ long lowest = (long) minimum + IrisDepositGenerator.lowestFootprintOffset(generator.getMaxSize());
+ return lowest > (long) highestSurface - Math.max(0, generator.getSurfaceClearance());
}
private DepositPlan plan(IrisDepositGenerator generator, RNG rng, ChunkContext context) {
@@ -227,7 +274,7 @@ public class IrisDepositModifier extends EngineAssignedModifier clumps = new ArrayList<>(limit - first);
for (int l = first; l < limit; l++) {
- RNG clumpRng = new RNG(clumpSeed(plan.seed(), l));
+ DepositRandom clumpRng = new DepositRandom(clumpSeed(plan.seed(), l));
if (k.getPerClumpSpawnChance() < clumpRng.d()) {
continue;
}
@@ -628,6 +675,102 @@ public class IrisDepositModifier extends EngineAssignedModifier terrain;
+ private final ChunkContext context;
+ private int scannedClearance = -1;
+ private boolean present;
+
+ private AboveTerrainHosts(Hunk terrain, ChunkContext context) {
+ this.terrain = terrain;
+ this.context = context;
+ }
+
+ private boolean present(int surfaceClearance) {
+ int clearance = Math.max(0, surfaceClearance);
+ if (clearance != scannedClearance) {
+ present = scan(clearance);
+ scannedClearance = clearance;
+ }
+ return present;
+ }
+
+ private boolean scan(int clearance) {
+ int height = terrain.getHeight();
+ for (int z = 0; z < terrain.getDepth(); z++) {
+ for (int x = 0; x < terrain.getWidth(); x++) {
+ long floor = Math.max(-1L, (long) context.getRoundedHeight(x, z) + clearance);
+ for (int y = Math.min(height - 1, ColumnExtent.highestStoredY(terrain, x, z)); y > floor; y--) {
+ if (canReplaceDepositTarget(terrain.get(x, y, z))) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ }
+
private record PackScan(IrisData data, boolean variants, boolean oreScaling) {
}
@@ -648,11 +791,13 @@ public class IrisDepositModifier extends EngineAssignedModifier data, int chunkX, int chunkZ, HeightMap heightMap,
@@ -671,6 +816,17 @@ public class IrisDepositModifier extends EngineAssignedModifier 0D) {
@@ -714,7 +872,7 @@ public class IrisDepositModifier extends EngineAssignedModifier {
private static final int SOLID = 1;
private static final int SPIKE = 2;
+ private static final int SLICE_TASKS = 16;
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
private static final BoundBlockState WATER = BoundBlockState.of("WATER");
private static final Map ORE_BASES = buildOreBases();
@@ -87,70 +88,94 @@ public class IrisPerfectionModifier extends EngineAssignedModifier perfectSlice(output, columnX));
+ for (int task = 0; task < tasks; task++) {
+ int firstSlice = task * slicesPerTask;
+ burst.queue(() -> perfectSlices(output, firstSlice, firstSlice + slicesPerTask));
}
burst.complete();
getEngine().getMetrics().getPerfection().put(p.getMilliseconds());
}
- private void perfectSlice(Hunk output, int x) {
+ private void perfectSlices(Hunk output, int firstSlice, int lastSlice) {
IntArrayList surfaces = new IntArrayList();
- StateClasses classes = new StateClasses();
- for (int z = 0; z < 16; z++) {
- boolean changed;
- do {
- surfaces.clear();
- int top = getHeight(output, x, z, classes);
- boolean inside = true;
- surfaces.add(top);
- for (int y = top; y >= 0; y--) {
- NativeBlockState block = output.get(x, y, z);
- int flags = classes.of(block);
- if ((flags & SPIKE) != 0) {
- block = normalizeSpike(block, output, x, z, y, AIR.get(), WATER.get());
- flags = classes.of(block);
- }
- boolean now = (flags & SOLID) != 0;
- if (now != inside) {
- inside = now;
- if (inside) {
- surfaces.add(y);
- }
- }
- }
- changed = false;
- for (int index = 0; index < surfaces.size(); index++) {
- int y = surfaces.getInt(index);
- NativeBlockState tip = output.get(x, y, z);
- if (!B.isDecorant(tip)) {
- continue;
- }
- NativeBlockState below = output.get(x, y - 1, z);
- boolean remove = below == null || !B.canPlaceOnto(tip, below);
- boolean removeBelow = false;
- if (!remove && IrisProceduralBlocks.hasProperty(below, "half")) {
- NativeBlockState support = output.get(x, y - 2, z);
- if (support == null || !B.canPlaceOnto(below, support)) {
- remove = true;
- removeBelow = true;
- }
- }
- if (remove) {
- changed = true;
- output.set(x, y, z, AIR.get());
- if (removeBelow) {
- output.set(x, y - 1, z, AIR.get());
- }
- }
- }
- } while (changed);
+ StateFlags states = new StateFlags();
+ NativeBlockState air = AIR.get();
+ NativeBlockState water = WATER.get();
+ for (int x = firstSlice; x < lastSlice; x++) {
+ for (int z = 0; z < 16; z++) {
+ perfectColumn(output, x, z, surfaces, states, air, water);
+ }
}
}
+ private static void perfectColumn(Hunk output, int x, int z, IntArrayList surfaces,
+ StateFlags states, NativeBlockState air, NativeBlockState water) {
+ do {
+ surfaces.clear();
+ int top = getHeight(output, x, z, states);
+ boolean inside = true;
+ surfaces.add(top);
+ NativeBlockState classified = null;
+ int classifiedFlags = 0;
+ for (int y = top; y >= 0; y--) {
+ NativeBlockState block = output.get(x, y, z);
+ int flags = 0;
+ if (block != null) {
+ if (block != classified) {
+ classified = block;
+ classifiedFlags = states.of(block);
+ }
+ flags = classifiedFlags;
+ if ((flags & SPIKE) != 0) {
+ block = normalizeSpike(block, output, x, z, y, air, water);
+ flags = block == null ? 0 : states.of(block);
+ }
+ }
+ boolean now = (flags & SOLID) != 0;
+ if (now != inside) {
+ inside = now;
+ if (inside) {
+ surfaces.add(y);
+ }
+ }
+ }
+ } while (removeUnsupportedDecorants(output, x, z, surfaces, air));
+ }
+
+ private static boolean removeUnsupportedDecorants(Hunk output, int x, int z,
+ IntArrayList surfaces, NativeBlockState air) {
+ boolean changed = false;
+ for (int index = 0; index < surfaces.size(); index++) {
+ int y = surfaces.getInt(index);
+ NativeBlockState tip = output.get(x, y, z);
+ if (!B.isDecorant(tip)) {
+ continue;
+ }
+ NativeBlockState below = output.get(x, y - 1, z);
+ boolean remove = below == null || !B.canPlaceOnto(tip, below);
+ boolean removeBelow = false;
+ if (!remove && IrisProceduralBlocks.hasProperty(below, "half")) {
+ NativeBlockState support = output.get(x, y - 2, z);
+ if (support == null || !B.canPlaceOnto(below, support)) {
+ remove = true;
+ removeBelow = true;
+ }
+ }
+ if (remove) {
+ changed = true;
+ output.set(x, y, z, air);
+ if (removeBelow) {
+ output.set(x, y - 1, z, air);
+ }
+ }
+ }
+ return changed;
+ }
+
static NativeBlockState normalizeSpike(NativeBlockState state, Hunk output,
int x, int z, int y, NativeBlockState air, NativeBlockState water) {
if (IrisSpeleothems.isSupported(state, output, x, z, y)) {
@@ -208,9 +233,19 @@ public class IrisPerfectionModifier extends EngineAssignedModifier output, int x, int z, StateClasses classes) {
+ private static int getHeight(Hunk output, int x, int z, StateFlags states) {
+ NativeBlockState classified = null;
+ int classifiedFlags = 0;
for (int i = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); i >= 0; i--) {
- if ((classes.of(output.get(x, i, z)) & SOLID) != 0) {
+ NativeBlockState state = output.get(x, i, z);
+ if (state == null) {
+ continue;
+ }
+ if (state != classified) {
+ classified = state;
+ classifiedFlags = states.of(state);
+ }
+ if ((classifiedFlags & SOLID) != 0) {
return i;
}
}
@@ -218,19 +253,21 @@ public class IrisPerfectionModifier extends EngineAssignedModifier riverWaterSurfaceStream;
private final boolean bedrockEnabled;
@@ -195,6 +196,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator {
if (!caveSpace.test(localX, y, localZ)) {
return;
}
int blockX = minimumX + localX;
int blockZ = minimumZ + localZ;
- if (!hasFullHydrologyWeight(engine.getComplex(), blockX, blockZ)) {
+ if (!hasFullHydrologyWeight(complex, blockX, blockZ)) {
return;
}
captureCave(
@@ -199,12 +203,8 @@ public final class GenerationSemanticCapture {
Math.addExact(minimumZ, localZ))));
}
});
- mantle.iterateChunk(chunkX, chunkZ, String.class, (localX, y, localZ, marker) -> {
- StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
- if (decoded != null) {
- semantics.addObject(decoded.objectKey());
- }
- });
+ ObjectMarkers objects = new ObjectMarkers(semantics);
+ mantle.iterateChunk(chunkX, chunkZ, String.class, (localX, y, localZ, marker) -> objects.add(marker));
}
private static boolean hasFullHydrologyWeight(IrisComplex complex, int blockX, int blockZ) {
@@ -273,10 +273,76 @@ public final class GenerationSemanticCapture {
}
}
+ private static final class ObjectMarkers {
+ private final ChunkGenerationSemantics.Builder semantics;
+ private String lastMarker;
+
+ private ObjectMarkers(ChunkGenerationSemantics.Builder semantics) {
+ this.semantics = semantics;
+ }
+
+ private void add(String marker) {
+ if (marker != null && marker.equals(lastMarker)) {
+ return;
+ }
+ StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
+ if (decoded != null) {
+ semantics.addObject(decoded.objectKey());
+ }
+ lastMarker = marker;
+ }
+ }
+
+ private static final class SubterrainBiomes {
+ private final Engine engine;
+ private final boolean live;
+ private final SubterrainPlanner planner;
+ private final int minimumY;
+
+ private SubterrainBiomes(Engine engine, boolean live, SubterrainPlanner planner, int minimumY) {
+ this.engine = engine;
+ this.live = live;
+ this.planner = planner;
+ this.minimumY = minimumY;
+ }
+
+ private static SubterrainBiomes of(Engine engine, int minimumY) {
+ if (engine.getClass() != IrisEngine.class) {
+ return new SubterrainBiomes(engine, false, null, 0);
+ }
+ IrisEngine irisEngine = (IrisEngine) engine;
+ if (!irisEngine.hasGenerationRuntimeScope() && irisEngine.getGenerationHistoryRuntimeRouter().isPresent()) {
+ return new SubterrainBiomes(engine, false, null, 0);
+ }
+ IrisComplex complex = engine.getComplex();
+ IrisDimension dimension = engine.getDimension();
+ SubterrainPlanner planner = complex == null || dimension == null || !dimension.allowsSubterrainFeatures()
+ || dimension.getSubterrainFeatures() == null || dimension.getSubterrainFeatures().isEmpty()
+ ? null
+ : complex.getSubterrainPlanner();
+ return new SubterrainBiomes(engine, true, planner, minimumY);
+ }
+
+ private IrisBiome resolve(int x, int y, int z) {
+ if (!live) {
+ return engine.getSubterrainBiome(x, y, z);
+ }
+ if (planner == null) {
+ return null;
+ }
+ SubterrainCell cell = planner.sample(x, y + minimumY, z);
+ if (!cell.occupied() || cell.room().biome().isEmpty()) {
+ return null;
+ }
+ return engine.getSubterrainBiome(x, y, z);
+ }
+ }
+
private static final class CaveBiomes {
private final Engine engine;
private final boolean scoped;
private final CaveFallback fallback;
+ private SubterrainBiomes subterrain;
private final IrisDimensionCarvingResolver.State fallbackState = new IrisDimensionCarvingResolver.State();
private IrisDimensionCarvingResolver.Snapshot snapshot;
private int minimumY;
@@ -305,8 +371,9 @@ public final class GenerationSemanticCapture {
if (snapshot == null) {
snapshot = IrisDimensionCarvingResolver.snapshot(engine);
minimumY = engine.getWorld().minHeight();
+ subterrain = SubterrainBiomes.of(engine, minimumY);
}
- IrisBiome featureBiome = engine.getSubterrainBiome(x, y, z);
+ IrisBiome featureBiome = subterrain.resolve(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java
index 78459d1dc..22d0240e8 100644
--- a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java
+++ b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java
@@ -628,10 +628,16 @@ public final class SavedBiomeStore {
private static byte[] encode(SavedBiomeChunk chunk) throws IOException {
LinkedHashMap palette = new LinkedHashMap<>();
LinkedHashMap columns = new LinkedHashMap<>();
+ byte[] columnIndices = new byte[256];
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
SavedBiomeChunk.Column column = chunk.column(x, z);
- columns.computeIfAbsent(column, ignored -> columns.size());
+ int known = columns.size();
+ int index = columns.computeIfAbsent(column, ignored -> columns.size());
+ columnIndices[z * 16 + x] = (byte) index;
+ if (index < known) {
+ continue;
+ }
palette.computeIfAbsent(column.surface(), ignored -> palette.size());
palette.computeIfAbsent(column.caveBase(), ignored -> palette.size());
for (SavedBiomeChunk.Span span : column.vertical()) {
@@ -666,11 +672,7 @@ public final class SavedBiomeStore {
writeUnsigned(output, palette.get(span.cell()));
}
}
- for (int z = 0; z < 16; z++) {
- for (int x = 0; x < 16; x++) {
- output.writeByte(columns.get(chunk.column(x, z)));
- }
- }
+ output.write(columnIndices);
}
if (raw.size() > MAXIMUM_DECODED_BYTES) {
throw new IOException("Saved biome chunk exceeds its decoded size limit");
diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java b/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java
index c04c36f0d..a50b110ce 100644
--- a/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java
+++ b/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java
@@ -63,38 +63,32 @@ public final class SavedTerrainChunk {
}
Math.addExact(minimumY, height);
List columns = new ArrayList<>(256);
- BoundaryColumnGeometry.Runs runs = new BoundaryColumnGeometry.Runs(height);
+ ColumnScan scan = new ColumnScan(height);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (boundaryOnly && x != 0 && x != 15 && z != 0 && z != 15) {
columns.add(null);
continue;
}
- runs.clear();
+ scan.reset();
+ source.scanColumn(x, z, minimumY, height, scan);
+ scan.requireCovered();
List biomePalette = new ArrayList<>(4);
short[] biomeIndices = new short[height / 4];
- int surface = 0;
- int floor = 0;
- int fluid = -1;
- for (int offset = 0; offset < height; offset++) {
- int y = minimumY + offset;
- BoundaryColumnGeometry.Voxel voxel = Objects.requireNonNull(source.voxel(x, y, z), "voxel");
- runs.add(voxel);
- if (voxel.phase() != BoundaryColumnGeometry.Phase.AIR) {
- surface = offset;
- }
- if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID) {
- floor = offset;
- }
- if (!voxel.fluidStateKey().isEmpty()) {
- fluid = offset;
- }
- if (offset % 4 == 0) {
- String biome = Objects.requireNonNull(source.biome(x, y, z), "physical biome");
- biomeIndices[offset / 4] = paletteIndex(biomePalette, biome);
+ String lastBiome = null;
+ short lastIndex = 0;
+ for (int offset = 0; offset < height; offset += 4) {
+ String biome = Objects.requireNonNull(source.biome(x, minimumY + offset, z), "physical biome");
+ if (biome != lastBiome) {
+ lastIndex = paletteIndex(biomePalette, biome);
+ lastBiome = biome;
}
+ biomeIndices[offset / 4] = lastIndex;
}
- BoundaryColumnGeometry geometry = runs.build(minimumY);
+ int surface = scan.surface;
+ int floor = scan.floor;
+ int fluid = scan.fluid;
+ BoundaryColumnGeometry geometry = scan.runs.build(minimumY);
OptionalInt groundSurface = source.groundSurface(x, z);
if (groundSurface.isPresent()) {
floor = geometry.surfaceOffsetNear(groundSurface.getAsInt());
@@ -209,5 +203,64 @@ public final class SavedTerrainChunk {
BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) throws IOException;
String biome(int localX, int worldY, int localZ) throws IOException;
+
+ default void scanColumn(int localX, int localZ, int minimumY, int height, VoxelRuns runs) throws IOException {
+ for (int offset = 0; offset < height; offset++) {
+ runs.add(offset, offset + 1, voxel(localX, minimumY + offset, localZ));
+ }
+ }
+ }
+
+ @FunctionalInterface
+ public interface VoxelRuns {
+ void add(int startOffset, int endOffset, BoundaryColumnGeometry.Voxel voxel);
+ }
+
+ private static final class ColumnScan implements VoxelRuns {
+ private final BoundaryColumnGeometry.Runs runs;
+ private final int height;
+ private int covered;
+ private int surface;
+ private int floor;
+ private int fluid;
+
+ private ColumnScan(int height) {
+ this.runs = new BoundaryColumnGeometry.Runs(height);
+ this.height = height;
+ }
+
+ private void reset() {
+ runs.clear();
+ covered = 0;
+ surface = 0;
+ floor = 0;
+ fluid = -1;
+ }
+
+ @Override
+ public void add(int startOffset, int endOffset, BoundaryColumnGeometry.Voxel voxel) {
+ Objects.requireNonNull(voxel, "voxel");
+ if (startOffset != covered || endOffset <= startOffset || endOffset > height) {
+ throw new IllegalArgumentException("Voxel runs must cover the column in order");
+ }
+ runs.add(voxel, endOffset - startOffset);
+ covered = endOffset;
+ int last = endOffset - 1;
+ if (voxel.phase() != BoundaryColumnGeometry.Phase.AIR) {
+ surface = last;
+ }
+ if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID) {
+ floor = last;
+ }
+ if (!voxel.fluidStateKey().isEmpty()) {
+ fluid = last;
+ }
+ }
+
+ private void requireCovered() {
+ if (covered != height) {
+ throw new IllegalStateException("Voxel runs must cover the whole column");
+ }
+ }
}
}
diff --git a/core/src/test/java/art/arcane/iris/generation/decoration/DepositRandomTest.java b/core/src/test/java/art/arcane/iris/generation/decoration/DepositRandomTest.java
new file mode 100644
index 000000000..e58a80e44
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/generation/decoration/DepositRandomTest.java
@@ -0,0 +1,65 @@
+package art.arcane.iris.generation.decoration;
+
+import art.arcane.volmlib.util.math.RNG;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class DepositRandomTest {
+ @Test
+ public void everyDrawMatchesTheRngSequence() {
+ int[] bounds = {1, 2, 3, 5, 7, 16, 100, 1 << 20, Integer.MAX_VALUE};
+ for (long seed : new long[]{0L, 1L, -1L, 42L, 0x9E3779B97F4A7C15L, Long.MIN_VALUE}) {
+ RNG reference = new RNG(seed);
+ DepositRandom random = new DepositRandom(seed);
+ assertEquals(reference.getSeed(), random.getSeed());
+ for (int draw = 0; draw < 4000; draw++) {
+ int bound = bounds[draw % bounds.length];
+ switch (draw % 9) {
+ case 0 -> assertEquals(reference.nextInt(), random.nextInt());
+ case 1 -> assertEquals(reference.nextInt(bound), random.nextInt(bound));
+ case 2 -> assertEquals(reference.nextLong(), random.nextLong());
+ case 3 -> assertEquals(Double.doubleToRawLongBits(reference.nextDouble()),
+ Double.doubleToRawLongBits(random.nextDouble()));
+ case 4 -> assertEquals(Float.floatToRawIntBits(reference.nextFloat()),
+ Float.floatToRawIntBits(random.nextFloat()));
+ case 5 -> assertEquals(reference.nextBoolean(), random.nextBoolean());
+ case 6 -> assertEquals(Double.doubleToRawLongBits(reference.d()),
+ Double.doubleToRawLongBits(random.d()));
+ case 7 -> assertEquals(reference.i(-bound / 2, bound), random.i(-bound / 2, bound));
+ default -> assertEquals(reference.i(0, 1), random.i(0, 1));
+ }
+ }
+ }
+ }
+
+ @Test
+ public void skipAdvancesExactlyLikeRepeatedSteps() {
+ for (long seed : new long[]{3L, -11L, 0x5DEECE66DL}) {
+ for (long steps : new long[]{0L, 1L, 2L, 3L, 7L, 64L, 65L, 1000L, 123457L}) {
+ DepositRandom stepped = new DepositRandom(seed);
+ DepositRandom jumped = new DepositRandom(seed);
+ for (long step = 0L; step < steps; step++) {
+ stepped.nextInt();
+ }
+ jumped.skip(steps);
+ assertEquals("seed " + seed + " steps " + steps, stepped.state(), jumped.state());
+ assertEquals(stepped.nextLong(), jumped.nextLong());
+ }
+ }
+ }
+
+ @Test
+ public void reseedingAndRestoringReproduceTheSequence() {
+ DepositRandom random = new DepositRandom();
+ for (long seed : new long[]{5L, 77L, -9001L}) {
+ random.setSeed(seed);
+ RNG reference = new RNG(seed);
+ long saved = random.state();
+ long first = random.nextLong();
+ assertEquals(reference.nextLong(), first);
+ random.restore(saved);
+ assertEquals(first, random.nextLong());
+ }
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositTuningTest.java b/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositTuningTest.java
index a9d5fb79a..8dbe4df6a 100644
--- a/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositTuningTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositTuningTest.java
@@ -260,6 +260,56 @@ public class IrisDepositTuningTest {
assertNull(generator.sampleClumpFootprint(new RNG(1L), data));
}
+ @Test
+ public void uniformEllipsoidFootprintCellsMatchTheBuiltClumpForEitherRandom() {
+ IrisData data = mock(IrisData.class);
+ IrisDepositGenerator generator = generatorWithState(data, true);
+ generator.setShape(IrisDepositShape.VANILLA_ELLIPSOID);
+ for (int size : new int[]{0, 1, 2, 3, 4, 9, 17, 20, 25, 48, 64}) {
+ generator.setMinSize(size).setMaxSize(size);
+ for (long seed = 0; seed < 300; seed++) {
+ RNG built = new RNG(seed);
+ IrisObject clump = generator.getClump(null, built, data);
+ long expectedNext = built.nextLong();
+ List expected = new ArrayList<>();
+ VectorMap.Cursor cursor = clump.getBlocks().cursor();
+ while (cursor.next()) {
+ expected.add(cursor.key().getBlockX() + "," + cursor.key().getBlockY() + ","
+ + cursor.key().getBlockZ() + "=" + cursor.value().key());
+ }
+ for (RNG sampled : new RNG[]{new RNG(seed), new DepositRandom(seed)}) {
+ IrisDepositGenerator.ClumpFootprint footprint = generator.sampleClumpFootprint(sampled, data);
+ assertEquals(clump.getW(), footprint.width());
+ IrisDepositGenerator.ClumpCells cells = footprint.cells();
+ List actual = new ArrayList<>();
+ for (int cell = 0; cell < cells.size(); cell++) {
+ actual.add(cells.x(cell) + "," + cells.y(cell) + "," + cells.z(cell) + "="
+ + cells.block(cell).key());
+ }
+ assertEquals("size " + size + " seed " + seed, expected, actual);
+ assertEquals("size " + size + " seed " + seed, expectedNext, sampled.nextLong());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void footprintOffsetBoundNeverExceedsTheLowestEllipsoidCell() {
+ IrisData data = mock(IrisData.class);
+ IrisDepositGenerator generator = generatorWithState(data, true);
+ generator.setShape(IrisDepositShape.VANILLA_ELLIPSOID);
+ for (int size = 1; size <= 64; size++) {
+ generator.setMinSize(size).setMaxSize(size);
+ int bound = IrisDepositGenerator.lowestFootprintOffset(size);
+ for (long seed = 0; seed < 400; seed++) {
+ IrisDepositGenerator.ClumpFootprint footprint = generator.sampleClumpFootprint(new DepositRandom(seed), data);
+ if (!footprint.empty()) {
+ assertTrue("size " + size + " seed " + seed, footprint.minY() >= bound);
+ }
+ }
+ }
+ }
+
@Test
public void ellipsoidFootprintCellsMatchTheBuiltClumpInPlacementOrder() {
IrisData data = mock(IrisData.class);
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
index aab4c31f2..f9ca00b29 100644
--- a/core/src/test/java/art/arcane/iris/generation/decoration/IrisOreBandsTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/decoration/IrisOreBandsTest.java
@@ -48,7 +48,7 @@ public class IrisOreBandsTest {
}
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));
+ scan(ores, x, y, 5 - x), bands.generate(x, y, 5 - x, RNG_SOURCE, null, null));
}
}
}
@@ -110,10 +110,10 @@ public class IrisOreBandsTest {
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);
+ assertSame(scan(ores, 0, 0, 0), bands.generate(0, 0, 0, RNG_SOURCE, null, null));
+ assertSame(scan(ores, 0, 30, 0), bands.generate(0, 30, 0, RNG_SOURCE, null, null));
+ assertSame(scan(ores, 0, 58, 0), bands.generate(0, 58, 0, RNG_SOURCE, null, null));
+ assertNull(bands.contains(59) ? bands.generate(0, 59, 0, RNG_SOURCE, null, null) : null);
}
private static final class FixedPalette extends IrisMaterialPalette {
private final NativeBlockState state;
diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierContextTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierContextTest.java
index bd75deeae..bc7b6b18b 100644
--- a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierContextTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierContextTest.java
@@ -54,8 +54,8 @@ public class IrisDepositModifierContextTest {
try (Fixture fixture = new Fixture(engine, true)) {
fixture.generateAndCheckCleanup();
verify(engine, times(1)).captureGenerationRuntimeBinding();
- verify(engine, times(4)).openGenerationRuntimeScope(fixture.binding);
- assertEquals(4, fixture.closedScopes.get());
+ verify(engine, times(1)).openGenerationRuntimeScope(fixture.binding);
+ assertEquals(1, fixture.closedScopes.get());
}
}
@@ -84,13 +84,13 @@ public class IrisDepositModifierContextTest {
assertSame(failure, assertThrows(IllegalStateException.class, () ->
fixture.modifier.modify(32, -48, Hunk.newArrayHunk(16, 16, 16), true, fixture.context)));
verify(fixture.chunk).release();
- assertEquals(4, fixture.closedScopes.get());
+ assertEquals(1, fixture.closedScopes.get());
assertNull(IrisContext.get());
}
}
@Test
- public void callerPlanFailureStillDrainsWorkersAndRestoresContext() throws Exception {
+ public void callerPlanFailureQueuesNoWorkAndRestoresContext() throws Exception {
try (Fixture fixture = new Fixture(mock(IrisEngine.class, RETURNS_DEEP_STUBS), true)) {
IllegalStateException failure = new IllegalStateException("caller deposit plan failed");
doThrow(failure).when(fixture.biomeDeposit).getSpawnChance();
@@ -102,25 +102,24 @@ public class IrisDepositModifierContextTest {
assertEquals(19L, IrisContext.require().getGenerationSessionId());
}
verify(fixture.chunk).release();
- assertEquals(3, fixture.closedScopes.get());
- assertEquals(10, fixture.completed.get());
+ assertEquals(0, fixture.closedScopes.get());
+ assertEquals(0, fixture.completed.get());
assertNull(IrisContext.get());
}
}
@Test
- public void callerFailureRetainsDistinctWorkerFailuresAfterDraining() throws Exception {
+ public void distinctWorkerFailuresAreRetainedAfterDraining() throws Exception {
try (Fixture fixture = new Fixture(mock(IrisEngine.class, RETURNS_DEEP_STUBS), true)) {
- IllegalStateException caller = new IllegalStateException("caller failure");
IllegalArgumentException firstWorker = new IllegalArgumentException("first worker failure");
UnsupportedOperationException secondWorker = new UnsupportedOperationException("second worker failure");
+ when(fixture.dimensionDeposit.getMinPerChunk()).thenReturn(200);
+ when(fixture.dimensionDeposit.getMaxPerChunk()).thenReturn(200);
doThrow(firstWorker, secondWorker).when(fixture.dimensionDeposit).getClump(any(), any(), any());
- doThrow(caller).when(fixture.biomeDeposit).getSpawnChance();
- assertSame(caller, assertThrows(IllegalStateException.class, () ->
+ assertSame(firstWorker, assertThrows(IllegalArgumentException.class, () ->
fixture.modifier.modify(32, -48, Hunk.newArrayHunk(16, 16, 16), true, fixture.context)));
- assertEquals(2, caller.getSuppressed().length);
- assertSame(firstWorker, caller.getSuppressed()[0]);
- assertSame(secondWorker, caller.getSuppressed()[1]);
+ assertEquals(1, firstWorker.getSuppressed().length);
+ assertSame(secondWorker, firstWorker.getSuppressed()[0]);
verify(fixture.chunk).release();
assertEquals(3, fixture.closedScopes.get());
assertNull(IrisContext.get());
diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierOrderingTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierOrderingTest.java
index 64c6314b0..3479b4a56 100644
--- a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierOrderingTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierOrderingTest.java
@@ -79,16 +79,16 @@ public class IrisDepositModifierOrderingTest {
@Test
public void reverseWorkerCompletionKeepsDimensionRegionBiomePlacementOrder() throws Exception {
- try (Fixture fixture = new Fixture(true, 1)) {
+ try (Fixture fixture = new Fixture(true, 96, true)) {
fixture.generate();
- assertEquals(List.of("biome", "region", "dimension"), fixture.completed);
+ assertEquals(List.of("biome", "region", "dimension"), fixture.firstCompletions());
fixture.assertBiomeWins();
}
}
@Test
public void inlinePreparationUsesTheSameOverlappingHostRules() throws Exception {
- try (Fixture fixture = new Fixture(false, 1)) {
+ try (Fixture fixture = new Fixture(false, 1, false)) {
fixture.generate();
assertEquals(List.of("dimension", "region", "biome"), fixture.completed);
fixture.assertBiomeWins();
@@ -107,7 +107,7 @@ public class IrisDepositModifierOrderingTest {
Collections.sort(expectedSamples);
for (boolean parallel : new boolean[]{false, true}) {
- try (Fixture fixture = new Fixture(parallel, 25)) {
+ try (Fixture fixture = new Fixture(parallel, 25, false)) {
fixture.generate();
Collections.sort(fixture.samples);
assertEquals(expectedSamples, fixture.samples);
@@ -290,11 +290,13 @@ public class IrisDepositModifierOrderingTest {
private final MantleChunk chunk;
private final boolean parallel;
private final int attempts;
+ private final boolean reverse;
@SuppressWarnings("unchecked")
- private Fixture(boolean parallel, int attempts) {
+ private Fixture(boolean parallel, int attempts, boolean reverse) {
this.parallel = parallel;
this.attempts = attempts;
+ this.reverse = reverse;
IrisBiome biome = mock(IrisBiome.class, withSettings().stubOnly());
IrisRegion region = mock(IrisRegion.class, withSettings().stubOnly());
ChunkedDataCache biomes = new ChunkedDataCache<>(
@@ -348,11 +350,9 @@ public class IrisDepositModifierOrderingTest {
}
}
when(generator.getClump(any(), any(), any())).thenAnswer(invocation -> {
- assertTrue("Placement must release the first clump window before more are prepared",
- samples.size() < 32 || output.get(0, 4, 0) != stone);
- if (parallel && attempts == 1 && name.equals("dimension")) {
+ if (reverse && name.equals("dimension")) {
assertTrue(regionReady.await(10, TimeUnit.SECONDS));
- } else if (parallel && attempts == 1 && name.equals("region")) {
+ } else if (reverse && name.equals("region")) {
assertTrue(biomeReady.await(10, TimeUnit.SECONDS));
}
RNG rng = invocation.getArgument(1);
@@ -376,6 +376,18 @@ public class IrisDepositModifierOrderingTest {
verify(chunk).release();
}
+ private List firstCompletions() {
+ List order = new ArrayList<>();
+ synchronized (completed) {
+ for (String name : completed) {
+ if (!order.contains(name)) {
+ order.add(name);
+ }
+ }
+ }
+ return order;
+ }
+
private void assertBiomeWins() {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java
index 68176479b..bd11ff5ac 100644
--- a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java
@@ -8,6 +8,7 @@ import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisDepositBiomeScope;
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
+import art.arcane.iris.generation.decoration.IrisDepositPlacementScope;
import art.arcane.iris.generation.decoration.IrisDepositShape;
import art.arcane.iris.generation.decoration.IrisDepositVariant;
import art.arcane.iris.generation.terrain.IrisDimension;
@@ -129,6 +130,56 @@ public class IrisDepositModifierParityTest {
assertEquals(0, clumps.get());
}
+ @Test
+ public void terrainFootprintsAboveTheHighestSurfaceAreSkippedWithoutChangingOutput() {
+ long[] expected = {-681153659952315909L, 784L, 2118L, 53L, 67L, 10L};
+ for (boolean multicore : new boolean[]{false, true}) {
+ AtomicInteger skipped = new AtomicInteger();
+ long[] withSkipped = generateConfigured(101L, multicore, true, false, new AtomicInteger(), null, true,
+ skyDeposit(47, skipped));
+ assertEquals(Arrays.toString(expected), Arrays.toString(withSkipped));
+ assertEquals(0, skipped.get());
+ AtomicInteger sampled = new AtomicInteger();
+ long[] withSampled = generateConfigured(101L, multicore, true, false, new AtomicInteger(), null, true,
+ skyDeposit(46, sampled));
+ assertEquals(Arrays.toString(expected), Arrays.toString(withSampled));
+ assertTrue(sampled.get() > 0);
+ }
+ }
+
+ @Test
+ public void aboveTerrainDepositsWithoutHostsAreSkippedWithoutChangingOutput() {
+ long[] expected = {-681153659952315909L, 784L, 2118L, 53L, 67L, 10L};
+ for (boolean multicore : new boolean[]{false, true}) {
+ AtomicInteger vacant = new AtomicInteger();
+ long[] withoutHosts = generateConfigured(101L, multicore, true, false, new AtomicInteger(), null, true,
+ floatingDeposit(vacant), false);
+ assertEquals(Arrays.toString(expected), Arrays.toString(withoutHosts));
+ assertEquals(0, vacant.get());
+ AtomicInteger hosted = new AtomicInteger();
+ generateConfigured(101L, multicore, true, false, new AtomicInteger(), null, true,
+ floatingDeposit(hosted), true);
+ assertTrue(hosted.get() > 0);
+ }
+ }
+
+ private IrisDepositGenerator floatingDeposit(AtomicInteger clumps) {
+ return new CountingPaletteGenerator(state("andesite"), clumps)
+ .setShape(IrisDepositShape.VANILLA_ELLIPSOID).setMinSize(9).setMaxSize(9)
+ .setMinPerChunk(40).setMaxPerChunk(40).setMinHeight(0).setMaxHeight(47)
+ .setHeightDistribution(IrisDepositHeightDistribution.UNIFORM).setSurfaceClearance(0)
+ .setPlacementScope(IrisDepositPlacementScope.ABOVE_TERRAIN)
+ .setPerClumpSpawnChance(1D).setReplaceableBlocks(new KList<>("stone"));
+ }
+
+ private IrisDepositGenerator skyDeposit(int minimumHeight, AtomicInteger clumps) {
+ return new CountingPaletteGenerator(state("andesite"), clumps)
+ .setShape(IrisDepositShape.VANILLA_ELLIPSOID).setMinSize(17).setMaxSize(17)
+ .setMinPerChunk(60).setMaxPerChunk(60).setMinHeight(minimumHeight).setMaxHeight(400)
+ .setHeightDistribution(IrisDepositHeightDistribution.UNIFORM).setSurfaceClearance(0)
+ .setPerClumpSpawnChance(1D).setReplaceableBlocks(new KList<>("stone"));
+ }
+
private long[] generateConfigured(long seed, boolean multicore) {
return generateConfigured(seed, multicore, true, false, new AtomicInteger());
}
@@ -143,10 +194,25 @@ public class IrisDepositModifierParityTest {
return generateConfigured(seed, multicore, caveVariant, enumerablePack, caveBiomeLookups, unmatchableClumps, true);
}
- @SuppressWarnings("unchecked")
private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack,
AtomicInteger caveBiomeLookups, AtomicInteger unmatchableClumps,
boolean oreScaling) {
+ return generateConfigured(seed, multicore, caveVariant, enumerablePack, caveBiomeLookups, unmatchableClumps,
+ oreScaling, null, false);
+ }
+
+ private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack,
+ AtomicInteger caveBiomeLookups, AtomicInteger unmatchableClumps,
+ boolean oreScaling, IrisDepositGenerator extraSurfaceDeposit) {
+ return generateConfigured(seed, multicore, caveVariant, enumerablePack, caveBiomeLookups, unmatchableClumps,
+ oreScaling, extraSurfaceDeposit, false);
+ }
+
+ @SuppressWarnings("unchecked")
+ private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack,
+ AtomicInteger caveBiomeLookups, AtomicInteger unmatchableClumps,
+ boolean oreScaling, IrisDepositGenerator extraSurfaceDeposit,
+ boolean floatingHost) {
Engine engine = mock(Engine.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS));
ChunkContext context = mock(ChunkContext.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS));
IrisDimension dimension = new IrisDimension();
@@ -189,6 +255,9 @@ public class IrisDepositModifierParityTest {
.setIncludedBiomes(new KList<>("elsewhere"));
surface.getDeposits().add(unmatchable);
}
+ if (extraSurfaceDeposit != null) {
+ surface.getDeposits().add(extraSurfaceDeposit);
+ }
when(engine.getHeight()).thenReturn(48);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getSeedManager().getDeposit()).thenReturn(seed);
@@ -241,6 +310,9 @@ public class IrisDepositModifierParityTest {
}
}
}
+ if (floatingHost) {
+ terrain.set(8, 44, 8, state("stone"));
+ }
MultiBurst burst = new MultiBurst("Deposit parity", () -> 4);
when(engine.burst()).thenReturn(burst);
try (MockedStatic blocks = mockStatic(B.class, withSettings().stubOnly().defaultAnswer(CALLS_REAL_METHODS))) {
diff --git a/core/src/test/java/art/arcane/iris/world/history/SavedBiomeStoreEncodingTest.java b/core/src/test/java/art/arcane/iris/world/history/SavedBiomeStoreEncodingTest.java
new file mode 100644
index 000000000..4722891d8
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/world/history/SavedBiomeStoreEncodingTest.java
@@ -0,0 +1,140 @@
+package art.arcane.iris.world.history;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Random;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+import static org.junit.Assert.assertArrayEquals;
+
+public class SavedBiomeStoreEncodingTest {
+ @Test
+ public void encodedRecordsMatchTheReferenceEncoding() throws Exception {
+ Method encode = SavedBiomeStore.class.getDeclaredMethod("encode", SavedBiomeChunk.class);
+ encode.setAccessible(true);
+ Random random = new Random(20261022L);
+ for (int sample = 0; sample < 40; sample++) {
+ SavedBiomeChunk chunk = randomChunk(random, sample);
+ assertArrayEquals("sample " + sample, referenceEncode(chunk), (byte[]) encode.invoke(null, chunk));
+ }
+ }
+
+ private static SavedBiomeChunk randomChunk(Random random, int sample) {
+ long activation = 1L + random.nextInt(9);
+ int minimumY = -64;
+ int height = 384;
+ List cells = new ArrayList<>();
+ int distinctCells = 1 + random.nextInt(sample % 4 == 0 ? 3 : 24);
+ for (int index = 0; index < distinctCells; index++) {
+ cells.add(random.nextInt(9) == 0
+ ? SavedBiomeChunk.Cell.unresolved(activation)
+ : new SavedBiomeChunk.Cell(activation, "biome_" + random.nextInt(12), "region_" + random.nextInt(3)));
+ }
+ List shared = new ArrayList<>();
+ int distinctColumns = 1 + random.nextInt(sample % 3 == 0 ? 4 : 40);
+ for (int index = 0; index < distinctColumns; index++) {
+ shared.add(randomColumn(random, cells, minimumY, height));
+ }
+ SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(
+ new SavedBiomeChunk.Header(3, -7, activation, minimumY, height));
+ for (int z = 0; z < 16; z++) {
+ for (int x = 0; x < 16; x++) {
+ SavedBiomeChunk.Column column = shared.get(random.nextInt(shared.size()));
+ if (random.nextInt(4) == 0) {
+ column = new SavedBiomeChunk.Column(column.surface(), column.caveBase(), new ArrayList<>(column.vertical()));
+ }
+ builder.column(x, z, column);
+ }
+ }
+ return builder.build();
+ }
+
+ private static SavedBiomeChunk.Column randomColumn(Random random, List cells,
+ int minimumY, int height) {
+ List spans = new ArrayList<>();
+ int start = minimumY;
+ int end = minimumY + height;
+ while (start < end) {
+ int top = Math.min(end, start + 1 + random.nextInt(96));
+ spans.add(new SavedBiomeChunk.Span(start, top, cells.get(random.nextInt(cells.size()))));
+ start = top;
+ }
+ return new SavedBiomeChunk.Column(cells.get(random.nextInt(cells.size())),
+ cells.get(random.nextInt(cells.size())), spans);
+ }
+
+ private static byte[] referenceEncode(SavedBiomeChunk chunk) throws IOException {
+ LinkedHashMap palette = new LinkedHashMap<>();
+ LinkedHashMap columns = new LinkedHashMap<>();
+ for (int z = 0; z < 16; z++) {
+ for (int x = 0; x < 16; x++) {
+ SavedBiomeChunk.Column column = chunk.column(x, z);
+ columns.computeIfAbsent(column, ignored -> columns.size());
+ palette.computeIfAbsent(column.surface(), ignored -> palette.size());
+ palette.computeIfAbsent(column.caveBase(), ignored -> palette.size());
+ for (SavedBiomeChunk.Span span : column.vertical()) {
+ palette.computeIfAbsent(span.cell(), ignored -> palette.size());
+ }
+ }
+ }
+ ByteArrayOutputStream raw = new ByteArrayOutputStream();
+ try (DataOutputStream output = new DataOutputStream(raw)) {
+ output.writeLong(chunk.activationId());
+ output.writeInt(chunk.header().minimumY());
+ output.writeInt(chunk.header().height());
+ output.writeInt(palette.size());
+ for (SavedBiomeChunk.Cell cell : palette.keySet()) {
+ output.writeLong(cell.activationId());
+ output.writeByte(cell.isResolved() ? 0 : 1);
+ if (cell.isResolved()) {
+ output.writeUTF(cell.biomeKey());
+ output.writeUTF(cell.regionKey());
+ }
+ }
+ output.writeInt(columns.size());
+ for (SavedBiomeChunk.Column column : columns.keySet()) {
+ writeUnsigned(output, palette.get(column.surface()));
+ writeUnsigned(output, palette.get(column.caveBase()));
+ writeUnsigned(output, column.vertical().size());
+ for (SavedBiomeChunk.Span span : column.vertical()) {
+ writeUnsigned(output, span.maximumYExclusive() - chunk.header().minimumY());
+ writeUnsigned(output, palette.get(span.cell()));
+ }
+ }
+ for (int z = 0; z < 16; z++) {
+ for (int x = 0; x < 16; x++) {
+ output.writeByte(columns.get(chunk.column(x, z)));
+ }
+ }
+ }
+ ByteArrayOutputStream encoded = new ByteArrayOutputStream();
+ DataOutputStream output = new DataOutputStream(encoded);
+ output.writeInt(chunk.chunkX());
+ output.writeInt(chunk.chunkZ());
+ output.writeInt(raw.size());
+ Deflater deflater = new Deflater(Deflater.BEST_SPEED);
+ try (DeflaterOutputStream compressed = new DeflaterOutputStream(output, deflater)) {
+ raw.writeTo(compressed);
+ } finally {
+ deflater.end();
+ }
+ return encoded.toByteArray();
+ }
+
+ private static void writeUnsigned(DataOutputStream output, int value) throws IOException {
+ int remaining = value;
+ while ((remaining & ~0x7f) != 0) {
+ output.writeByte((remaining & 0x7f) | 0x80);
+ remaining >>>= 7;
+ }
+ output.writeByte(remaining);
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/world/history/SavedTerrainCaptureEquivalenceTest.java b/core/src/test/java/art/arcane/iris/world/history/SavedTerrainCaptureEquivalenceTest.java
index 0bdd9c171..be2d60d4d 100644
--- a/core/src/test/java/art/arcane/iris/world/history/SavedTerrainCaptureEquivalenceTest.java
+++ b/core/src/test/java/art/arcane/iris/world/history/SavedTerrainCaptureEquivalenceTest.java
@@ -56,6 +56,24 @@ public class SavedTerrainCaptureEquivalenceTest {
}
}
+ @Test
+ public void runScanningSourcesMatchThePerVoxelReference() throws Exception {
+ Random random = new Random(20261022L);
+ for (int sample = 0; sample < 32; sample++) {
+ RandomSource voxels = new RandomSource(random, sample);
+ SavedTerrainChunk.VoxelSource runs = new RunScanningSource(voxels, new Random(sample));
+ for (boolean boundaryOnly : new boolean[]{true, false}) {
+ SavedTerrainChunk expected = referenceCapture(-3, 7, voxels, boundaryOnly);
+ SavedTerrainChunk actual = boundaryOnly
+ ? SavedTerrainChunk.captureBoundary(-3, 7, MINIMUM_Y, HEIGHT, "minecraft:noise", runs)
+ : SavedTerrainChunk.capture(-3, 7, MINIMUM_Y, HEIGHT, "minecraft:noise", runs);
+ assertSameTerrain(expected, actual);
+ assertArrayEquals(NativeTerrainReceipt.encode(expected, 1L, "epoch"),
+ NativeTerrainReceipt.encode(actual, 1L, "epoch"));
+ }
+ }
+ }
+
@Test
public void geometryMatchesTheReferenceOnWidePalettes() {
Random random = new Random(7L);
@@ -184,6 +202,46 @@ public class SavedTerrainCaptureEquivalenceTest {
return new BoundaryColumnGeometry.Voxel(state, phase, fluid, protectedContent);
}
+ private static final class RunScanningSource implements SavedTerrainChunk.VoxelSource {
+ private final RandomSource source;
+ private final Random splits;
+
+ private RunScanningSource(RandomSource source, Random splits) {
+ this.source = source;
+ this.splits = splits;
+ }
+
+ @Override
+ public OptionalInt groundSurface(int localX, int localZ) {
+ return source.groundSurface(localX, localZ);
+ }
+
+ @Override
+ public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
+ return source.voxel(localX, worldY, localZ);
+ }
+
+ @Override
+ public String biome(int localX, int worldY, int localZ) {
+ return source.biome(localX, worldY, localZ);
+ }
+
+ @Override
+ public void scanColumn(int localX, int localZ, int minimumY, int height, SavedTerrainChunk.VoxelRuns runs) {
+ int start = 0;
+ BoundaryColumnGeometry.Voxel current = source.voxel(localX, minimumY, localZ);
+ for (int offset = 1; offset < height; offset++) {
+ BoundaryColumnGeometry.Voxel next = source.voxel(localX, minimumY + offset, localZ);
+ if (next != current || splits.nextInt(16) == 0) {
+ runs.add(start, offset, current);
+ start = offset;
+ current = next;
+ }
+ }
+ runs.add(start, height, current);
+ }
+ }
+
private static final class RandomSource implements SavedTerrainChunk.VoxelSource {
private final BoundaryColumnGeometry.Voxel[][] voxels = new BoundaryColumnGeometry.Voxel[256][HEIGHT];
private final String[][] biomes = new String[256][HEIGHT];
diff --git a/probe/build.gradle b/probe/build.gradle
index 926526037..66fa6b246 100644
--- a/probe/build.gradle
+++ b/probe/build.gradle
@@ -180,6 +180,7 @@ tasks.register('genProbe', JavaExec) {
systemProperty('iris.probe.pregenProfile', providers.gradleProperty('probePregenProfile').orElse('false').get())
systemProperty('iris.probe.hydrologyPreplan', providers.gradleProperty('probeHydrologyPreplan').orElse('false').get())
systemProperty('iris.probe.historyRouter', providers.gradleProperty('probeHistoryRouter').orElse('false').get())
+ systemProperty('iris.probe.historyDigest', providers.gradleProperty('probeHistoryDigest').orElse('false').get())
systemProperty('iris.probe.productionBuffers', providers.gradleProperty('probeProductionBuffers').orElse('false').get())
String probeCounters = providers.gradleProperty('probeCounters').orElse('false').get()
systemProperty('iris.generation.counters', probeCounters)
diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
index 81b78ae28..c1014314a 100644
--- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
+++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
@@ -177,7 +177,7 @@ public final class GenerationProbe {
}
record ProbeOptions(int threads, Path dumpFile, boolean pregenProfile, boolean hydrologyPreplan,
- boolean historyRouter, boolean productionBuffers, boolean counters) {
+ boolean historyRouter, boolean productionBuffers, boolean counters, boolean historyDigest) {
ProbeOptions {
if (threads < 1) {
throw new IllegalArgumentException("Thread count must be at least 1.");
@@ -196,7 +196,8 @@ public final class GenerationProbe {
booleanProperty("iris.probe.hydrologyPreplan"),
booleanProperty("iris.probe.historyRouter"),
booleanProperty("iris.probe.productionBuffers"),
- booleanProperty("iris.generation.counters"));
+ booleanProperty("iris.generation.counters"),
+ booleanProperty("iris.probe.historyDigest"));
}
private static boolean booleanProperty(String key) {
@@ -437,7 +438,8 @@ public final class GenerationProbe {
+ " timeMs=" + String.format(Locale.ROOT, "%.3f", nanosToMillis(engineReadyNanos)));
System.out.println("IRIS_GENPROBE_MANTLE version=1 radius=" + engine.getMantle().getRadius()
+ " real_radius=" + engine.getMantle().getRealRadius());
- ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers());
+ ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers(),
+ options.historyDigest());
long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount();
GenerationResult generation = generateWithProfile(generator, configuration, options);
verifyBackend(nativeSamplesBefore);
@@ -445,6 +447,9 @@ public final class GenerationProbe {
System.out.println("IRIS_GENPROBE_BUFFERS version=1 mode=production biome_expanded_chunks="
+ generator.expandedBiomeHunks());
}
+ if (generator.historySummary() != null) {
+ System.out.println(generator.historySummary());
+ }
String status = generation.failedChunks() == 0 ? "PASS" : "FAIL";
printGenerationFailures(generation);
return new ProbeResult(
diff --git a/probe/src/main/java/art/arcane/iris/probe/HistoryDigest.java b/probe/src/main/java/art/arcane/iris/probe/HistoryDigest.java
new file mode 100644
index 000000000..ca0724703
--- /dev/null
+++ b/probe/src/main/java/art/arcane/iris/probe/HistoryDigest.java
@@ -0,0 +1,86 @@
+/*
+ * Iris is a World Generator for Minecraft Servers
+ * Copyright (c) 2026 Arcane Arts (Volmit Software)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package art.arcane.iris.probe;
+
+import art.arcane.iris.world.history.ChunkGenerationSemantics;
+import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
+import art.arcane.iris.world.history.SavedBiomeChunk;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HexFormat;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+final class HistoryDigest {
+ private final GenerationHistoryRuntimeRouter router;
+ private final Map chunks = new ConcurrentSkipListMap<>();
+
+ HistoryDigest(GenerationHistoryRuntimeRouter router) {
+ this.router = router;
+ }
+
+ void record(int chunkX, int chunkZ) throws IOException {
+ StringBuilder canonical = new StringBuilder(4096);
+ Optional semantics = router.history().semantics(chunkX, chunkZ);
+ canonical.append(semantics.map(value -> value + "|" + value.pointsOfInterest()).orElse("none")).append('\n');
+ Optional biomes = router.biomes().snapshot(chunkX, chunkZ);
+ if (biomes.isEmpty()) {
+ canonical.append("none");
+ } else {
+ SavedBiomeChunk chunk = biomes.get();
+ canonical.append(chunk.header());
+ for (int localZ = 0; localZ < 16; localZ++) {
+ for (int localX = 0; localX < 16; localX++) {
+ canonical.append('\n').append(chunk.column(localX, localZ));
+ }
+ }
+ }
+ chunks.put(((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL), sha256(canonical.toString().getBytes(StandardCharsets.UTF_8)));
+ }
+
+ String summary() {
+ MessageDigest digest = newDigest();
+ ByteBuffer key = ByteBuffer.allocate(Long.BYTES);
+ for (Map.Entry entry : chunks.entrySet()) {
+ key.clear();
+ key.putLong(entry.getKey());
+ digest.update(key.array());
+ digest.update(entry.getValue());
+ }
+ return "IRIS_GENPROBE_HISTORY version=1 chunks=" + chunks.size()
+ + " digest=" + HexFormat.of().formatHex(digest.digest(), 0, 8);
+ }
+
+ private static byte[] sha256(byte[] bytes) {
+ return newDigest().digest(bytes);
+ }
+
+ private static MessageDigest newDigest() {
+ try {
+ return MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException missing) {
+ throw new IllegalStateException("SHA-256 is unavailable", missing);
+ }
+ }
+}
diff --git a/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
index 64db59352..5e3b7d6d4 100644
--- a/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
+++ b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
@@ -36,12 +36,19 @@ final class ProbeChunkGenerator {
private final Engine engine;
private final GenerationHistoryRuntimeRouter router;
private final boolean productionBuffers;
+ private final HistoryDigest historyDigest;
private final AtomicInteger expandedBiomeHunks = new AtomicInteger();
- ProbeChunkGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers) {
+ ProbeChunkGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers,
+ boolean historyDigest) {
this.engine = engine;
this.router = router;
this.productionBuffers = productionBuffers;
+ this.historyDigest = historyDigest && router != null ? new HistoryDigest(router) : null;
+ }
+
+ String historySummary() {
+ return historyDigest == null ? null : historyDigest.summary();
}
Engine engine() {
@@ -64,6 +71,13 @@ final class ProbeChunkGenerator {
failures.add(e);
}
long elapsed = System.nanoTime() - started;
+ if (historyDigest != null && failures.isEmpty()) {
+ try {
+ historyDigest.record(coordinate.x(), coordinate.z());
+ } catch (Throwable e) {
+ failures.add(e);
+ }
+ }
if (biomes instanceof ProductionBiomeHunk production && production.expanded()) {
expandedBiomeHunks.incrementAndGet();
}
diff --git a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java
index 54d240378..ee72fe4f9 100644
--- a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java
+++ b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java
@@ -134,7 +134,7 @@ public final class GenerationProbeTest {
assertEquals(1, defaults.threads());
assertNull(defaults.dumpFile());
assertThrows(IllegalArgumentException.class,
- () -> new GenerationProbe.ProbeOptions(0, null, false, false, false, false, false));
+ () -> new GenerationProbe.ProbeOptions(0, null, false, false, false, false, false, false));
assertFalse(defaults.hydrologyPreplan());
assertFalse(defaults.historyRouter());
assertFalse(defaults.productionBuffers());