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/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 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/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))) {