mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Merged small stages
- Single-pass deposit footprints with balanced preparation, cached perfection and placement state checks, run-scanned history receipts
This commit is contained in:
commit
45e00f4794
25 files changed
+1376
-270
No files matched your search
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+235
-72
@@ -58,6 +58,7 @@ import lombok.experimental.Accessors;
|
||||
public class IrisDepositGenerator {
|
||||
private static final int FOOTPRINT_MAXIMUM_SIZE = 64;
|
||||
private static final ThreadLocal<FootprintCells> 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<NativeBlockState> 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<NativeBlockState> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<NativeBlockState> {
|
||||
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<NativeBlockState
|
||||
BurstExecutor burst = burst().burst(multicore);
|
||||
long seed = x * 341873128712L + z * 132897987541L;
|
||||
int highestSurface = highestTerrainSurface(x, z, null, context);
|
||||
PreparedDeposit[] prepared = new PreparedDeposit[PREPARATION_BATCH_COUNT];
|
||||
int pending = 0;
|
||||
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine
|
||||
&& irisEngine.hasGenerationRuntimeScope() ? irisEngine : null;
|
||||
PreparationContext preparation = new PreparationContext(context, generationEngine,
|
||||
generationEngine == null ? null : generationEngine.captureGenerationRuntimeBinding(), new Throwable[PREPARATION_BATCH_COUNT]);
|
||||
MantleChunk chunk = getEngine().getMantle().getMantle().useChunk(x, z);
|
||||
PreparationContext preparation = null;
|
||||
Throwable callerFailure = null;
|
||||
try {
|
||||
List<DepositPlan> 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<NativeBlockState
|
||||
// submitted burst tasks must never write into a released chunk.
|
||||
try {
|
||||
burst.complete();
|
||||
if (callerFailure != null) {
|
||||
if (callerFailure != null && preparation != null) {
|
||||
preparation.takeFailure(callerFailure);
|
||||
}
|
||||
} finally {
|
||||
@@ -130,28 +168,22 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
}
|
||||
}
|
||||
|
||||
private void completeBatches(BurstExecutor burst, PreparedDeposit[] prepared, MantleChunk chunk,
|
||||
Hunk<NativeBlockState> 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<DepositPlan> plans, int first, int last, int cx, int cz,
|
||||
ChunkContext context, int highestSurface) {
|
||||
List<PreparedDeposit> 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<NativeBlockState
|
||||
|
||||
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<NativeBlockState> 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<NativeBlockState
|
||||
int engineHeight = getEngine().getHeight();
|
||||
List<PreparedClump> 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<NativeBlockState
|
||||
private record DepositPlan(IrisDepositGenerator generator, long seed, boolean ore, int attempts) {
|
||||
}
|
||||
|
||||
private static final class HostStates {
|
||||
private static final int SLOTS = 32;
|
||||
private static final byte BEDROCK_KNOWN = 1;
|
||||
private static final byte BEDROCK = 2;
|
||||
private static final byte REPLACEABLE_KNOWN = 4;
|
||||
private static final byte REPLACEABLE = 8;
|
||||
private static final byte TARGET_KNOWN = 16;
|
||||
private static final byte TARGET = 32;
|
||||
|
||||
private final IrisDepositGenerator generator;
|
||||
private final NativeBlockState[] states = new NativeBlockState[SLOTS];
|
||||
private final byte[] flags = new byte[SLOTS];
|
||||
|
||||
private HostStates(IrisDepositGenerator generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
private boolean target(NativeBlockState state) {
|
||||
int slot = slot(state);
|
||||
byte known = flags[slot];
|
||||
if ((known & TARGET_KNOWN) == 0) {
|
||||
boolean target = canReplaceDepositTarget(state);
|
||||
known = (byte) (known | TARGET_KNOWN | (target ? TARGET : 0));
|
||||
flags[slot] = known;
|
||||
}
|
||||
return (known & TARGET) != 0;
|
||||
}
|
||||
|
||||
private boolean bedrock(NativeBlockState state) {
|
||||
int slot = slot(state);
|
||||
byte known = flags[slot];
|
||||
if ((known & BEDROCK_KNOWN) == 0) {
|
||||
boolean bedrock = IrisProceduralBlocks.materialKey(state).equals("minecraft:bedrock");
|
||||
known = (byte) (known | BEDROCK_KNOWN | (bedrock ? BEDROCK : 0));
|
||||
flags[slot] = known;
|
||||
}
|
||||
return (known & BEDROCK) != 0;
|
||||
}
|
||||
|
||||
private boolean replaceable(NativeBlockState state) {
|
||||
int slot = slot(state);
|
||||
byte known = flags[slot];
|
||||
if ((known & REPLACEABLE_KNOWN) == 0) {
|
||||
boolean replaceable = generator.canReplace(state);
|
||||
known = (byte) (known | REPLACEABLE_KNOWN | (replaceable ? REPLACEABLE : 0));
|
||||
flags[slot] = known;
|
||||
}
|
||||
return (known & REPLACEABLE) != 0;
|
||||
}
|
||||
|
||||
private int slot(NativeBlockState state) {
|
||||
int slot = System.identityHashCode(state) & (SLOTS - 1);
|
||||
if (states[slot] != state) {
|
||||
states[slot] = state;
|
||||
flags[slot] = 0;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AboveTerrainHosts {
|
||||
private final Hunk<NativeBlockState> terrain;
|
||||
private final ChunkContext context;
|
||||
private int scannedClearance = -1;
|
||||
private boolean present;
|
||||
|
||||
private AboveTerrainHosts(Hunk<NativeBlockState> 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<NativeBlockState
|
||||
private final ChunkContext context;
|
||||
private final boolean variants;
|
||||
private final IrisDimension dimension;
|
||||
private final IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State();
|
||||
private IrisDimensionCarvingResolver.State carvingState;
|
||||
private final IrisDepositPlacementScope scope;
|
||||
private final int surfaceClearance;
|
||||
private final double discardChance;
|
||||
private final int height;
|
||||
private final boolean replaceBedrock;
|
||||
private final HostStates hosts;
|
||||
|
||||
private ClumpPlacement(IrisDepositGenerator generator, boolean ore, MantleChunk chunk,
|
||||
Hunk<NativeBlockState> data, int chunkX, int chunkZ, HeightMap heightMap,
|
||||
@@ -671,6 +816,17 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
this.surfaceClearance = generator.getSurfaceClearance();
|
||||
this.discardChance = generator.getDiscardChanceOnAirExposure();
|
||||
this.height = getEngine().getHeight();
|
||||
this.replaceBedrock = generator.isReplaceBedrock();
|
||||
this.hosts = new HostStates(generator);
|
||||
}
|
||||
|
||||
private IrisDimensionCarvingResolver.State carvingState() {
|
||||
IrisDimensionCarvingResolver.State state = carvingState;
|
||||
if (state == null) {
|
||||
state = new IrisDimensionCarvingResolver.State();
|
||||
carvingState = state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private void place(int nx, int ny, int nz, NativeBlockState block, RNG rng) {
|
||||
@@ -688,10 +844,10 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
}
|
||||
|
||||
NativeBlockState current = data.get(nx, ny, nz);
|
||||
if (!canReplaceDepositTarget(current)) {
|
||||
if (current == null || !hosts.target(current)) {
|
||||
return;
|
||||
}
|
||||
if (!generator.isReplaceBedrock() && IrisProceduralBlocks.materialKey(current).equals("minecraft:bedrock")) {
|
||||
if (!replaceBedrock && hosts.bedrock(current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -702,7 +858,9 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
exteriorSurface = generator.hasSurfaceReplaceableBlocks(candidateSurfaceBiome)
|
||||
&& isTerrainSurface(data, nx, ny, nz, columnSurface);
|
||||
}
|
||||
if (!canReplaceDepositHost(generator, current, candidateSurfaceBiome, exteriorSurface)) {
|
||||
if (exteriorSurface
|
||||
? !canReplaceDepositHost(generator, current, candidateSurfaceBiome, true)
|
||||
: !hosts.replaceable(current)) {
|
||||
return;
|
||||
}
|
||||
if (discardChance > 0D) {
|
||||
@@ -714,7 +872,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
|
||||
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
|
||||
NativeBlockState remapped = variants ? resolveDepositVariant(
|
||||
chunkX, chunkZ, nx, ny, nz, block, dimension, context, carvingState) : null;
|
||||
chunkX, chunkZ, nx, ny, nz, block, dimension, context, carvingState()) : null;
|
||||
data.set(nx, ny, nz, remapped != null ? remapped : B.toDeepSlateOre(current, block));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockState> {
|
||||
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<String, BoundBlockState> ORE_BASES = buildOreBases();
|
||||
@@ -87,70 +88,94 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
|
||||
if (getDimension().isHideOresForHiddenOre()) {
|
||||
hideOres(output, multicore);
|
||||
}
|
||||
int tasks = multicore ? SLICE_TASKS : 1;
|
||||
int slicesPerTask = 16 / tasks;
|
||||
BurstExecutor burst = burst().burst(multicore);
|
||||
for (int index = 0; index < 16; index++) {
|
||||
int columnX = index;
|
||||
burst.queue(() -> 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<NativeBlockState> output, int x) {
|
||||
private void perfectSlices(Hunk<NativeBlockState> 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<NativeBlockState> 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<NativeBlockState> 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<NativeBlockState> 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<NativeBlockSt
|
||||
burst.complete();
|
||||
}
|
||||
|
||||
private int getHeight(Hunk<NativeBlockState> output, int x, int z, StateClasses classes) {
|
||||
private static int getHeight(Hunk<NativeBlockState> 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<NativeBlockSt
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static final class StateClasses {
|
||||
private NativeBlockState last;
|
||||
private int lastFlags;
|
||||
private static final class StateFlags {
|
||||
private static final int SLOTS = 64;
|
||||
|
||||
private final NativeBlockState[] states = new NativeBlockState[SLOTS];
|
||||
private final byte[] flags = new byte[SLOTS];
|
||||
|
||||
private int of(NativeBlockState state) {
|
||||
if (state == null) {
|
||||
return 0;
|
||||
int slot = System.identityHashCode(state) & (SLOTS - 1);
|
||||
if (states[slot] == state) {
|
||||
return flags[slot];
|
||||
}
|
||||
if (state != last) {
|
||||
last = state;
|
||||
lastFlags = (state.isAir() || state.isFluid() ? 0 : SOLID) | (IrisSpeleothems.isSpike(state) ? SPIKE : 0);
|
||||
}
|
||||
return lastFlags;
|
||||
int computed = (state.isAir() || state.isFluid() ? 0 : SOLID) | (IrisSpeleothems.isSpike(state) ? SPIKE : 0);
|
||||
states[slot] = state;
|
||||
flags[slot] = (byte) computed;
|
||||
return computed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +153,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
private final int chunkHeight;
|
||||
private final IrisDimension dimension;
|
||||
private final IrisData data;
|
||||
private final Engine oreEngine;
|
||||
private final IrisComplex complex;
|
||||
private final ProceduralStream<Double> riverWaterSurfaceStream;
|
||||
private final boolean bedrockEnabled;
|
||||
@@ -195,6 +196,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
chunkHeight = h.getHeight();
|
||||
dimension = getDimension();
|
||||
data = getData();
|
||||
oreEngine = data == null ? null : data.getEngine();
|
||||
complex = getComplex();
|
||||
riverWaterSurfaceStream = complex.getRiverWaterSurfaceStream();
|
||||
bedrockEnabled = dimension.isBedrock();
|
||||
@@ -459,13 +461,13 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
private NativeBlockState surfaceOre(int realX, int y, int realZ) {
|
||||
NativeBlockState ore = null;
|
||||
if (biomeSurfaceOres.contains(y)) {
|
||||
ore = biomeSurfaceOres.generate(realX, y, realZ, rng, data);
|
||||
ore = biomeSurfaceOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
if (ore == null && regionSurfaceOres.contains(y)) {
|
||||
ore = regionSurfaceOres.generate(realX, y, realZ, rng, data);
|
||||
ore = regionSurfaceOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
if (ore == null && dimensionSurfaceOres.contains(y)) {
|
||||
ore = dimensionSurfaceOres.generate(realX, y, realZ, rng, data);
|
||||
ore = dimensionSurfaceOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
return ore;
|
||||
}
|
||||
@@ -473,13 +475,13 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
private NativeBlockState undergroundOre(int realX, int y, int realZ) {
|
||||
NativeBlockState ore = null;
|
||||
if (biomeUndergroundOres.contains(y)) {
|
||||
ore = biomeUndergroundOres.generate(realX, y, realZ, rng, data);
|
||||
ore = biomeUndergroundOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
if (ore == null && regionUndergroundOres.contains(y)) {
|
||||
ore = regionUndergroundOres.generate(realX, y, realZ, rng, data);
|
||||
ore = regionUndergroundOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
if (ore == null && dimensionUndergroundOres.contains(y)) {
|
||||
ore = dimensionUndergroundOres.generate(realX, y, realZ, rng, data);
|
||||
ore = dimensionUndergroundOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
return ore;
|
||||
}
|
||||
|
||||
+33
-6
@@ -120,13 +120,40 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
|
||||
|
||||
@Override
|
||||
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
|
||||
NativeBlockState state = blocks.getRaw(localX, worldY - minimumY, localZ);
|
||||
if (state == null) {
|
||||
if (air == null) {
|
||||
air = IrisPlatforms.get().registries().air();
|
||||
}
|
||||
state = air;
|
||||
return voxelOf(stateAt(localX, worldY - minimumY, localZ));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scanColumn(int localX, int localZ, int minimumY, int height, SavedTerrainChunk.VoxelRuns runs) {
|
||||
if (height <= 0) {
|
||||
return;
|
||||
}
|
||||
int base = minimumY - this.minimumY;
|
||||
NativeBlockState runState = stateAt(localX, base, localZ);
|
||||
int start = 0;
|
||||
for (int offset = 1; offset < height; offset++) {
|
||||
NativeBlockState state = stateAt(localX, base + offset, localZ);
|
||||
if (state != runState) {
|
||||
runs.add(start, offset, voxelOf(runState));
|
||||
start = offset;
|
||||
runState = state;
|
||||
}
|
||||
}
|
||||
runs.add(start, height, voxelOf(runState));
|
||||
}
|
||||
|
||||
private NativeBlockState stateAt(int localX, int offset, int localZ) {
|
||||
NativeBlockState state = blocks.getRaw(localX, offset, localZ);
|
||||
if (state != null) {
|
||||
return state;
|
||||
}
|
||||
if (air == null) {
|
||||
air = IrisPlatforms.get().registries().air();
|
||||
}
|
||||
return air;
|
||||
}
|
||||
|
||||
private BoundaryColumnGeometry.Voxel voxelOf(NativeBlockState state) {
|
||||
if (state != lastState) {
|
||||
lastVoxel = voxels.computeIfAbsent(state, GeometrySource::encode);
|
||||
lastState = state;
|
||||
|
||||
@@ -257,11 +257,16 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
}
|
||||
|
||||
void add(Voxel voxel) {
|
||||
add(voxel, 1);
|
||||
}
|
||||
|
||||
void add(Voxel voxel, int run) {
|
||||
Objects.requireNonNull(voxel, "Geometry voxel");
|
||||
if (count == 0 || !sameVoxel(voxels[count - 1], voxel)) {
|
||||
voxels[count++] = voxel;
|
||||
}
|
||||
ends[count - 1] = ++length;
|
||||
length += run;
|
||||
ends[count - 1] = length;
|
||||
}
|
||||
|
||||
BoundaryColumnGeometry build(int minimumY) {
|
||||
|
||||
@@ -16,6 +16,9 @@ import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainCell;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
|
||||
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.structure.placement.IrisStructurePlacement;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
@@ -170,13 +173,14 @@ public final class GenerationSemanticCapture {
|
||||
cavern == null ? null : cavern.getCustomBiome()
|
||||
);
|
||||
});
|
||||
IrisComplex complex = engine.getComplex();
|
||||
mantle.iterateChunk(chunkX, chunkZ, HydrologyCaveCell.class, (localX, y, localZ, cell) -> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -628,10 +628,16 @@ public final class SavedBiomeStore {
|
||||
private static byte[] encode(SavedBiomeChunk chunk) throws IOException {
|
||||
LinkedHashMap<SavedBiomeChunk.Cell, Integer> palette = new LinkedHashMap<>();
|
||||
LinkedHashMap<SavedBiomeChunk.Column, Integer> 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");
|
||||
|
||||
@@ -63,38 +63,32 @@ public final class SavedTerrainChunk {
|
||||
}
|
||||
Math.addExact(minimumY, height);
|
||||
List<TerrainBoundarySignature> 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<String> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> expected = new ArrayList<>();
|
||||
VectorMap<NativeBlockState>.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<String> 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+12
-13
@@ -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());
|
||||
|
||||
+21
-9
@@ -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<Matter> 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<IrisBiome> 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<String> firstCompletions() {
|
||||
List<String> 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++) {
|
||||
|
||||
+73
-1
@@ -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<B> blocks = mockStatic(B.class, withSettings().stubOnly().defaultAnswer(CALLS_REAL_METHODS))) {
|
||||
|
||||
@@ -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<SavedBiomeChunk.Cell> 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<SavedBiomeChunk.Column> 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<SavedBiomeChunk.Cell> cells,
|
||||
int minimumY, int height) {
|
||||
List<SavedBiomeChunk.Span> 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<SavedBiomeChunk.Cell, Integer> palette = new LinkedHashMap<>();
|
||||
LinkedHashMap<SavedBiomeChunk.Column, Integer> 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);
|
||||
}
|
||||
}
|
||||
+58
@@ -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];
|
||||
|
||||
Reference in new issue
Block a user