diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java index 7df6777ae..5ceccec9f 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java @@ -474,7 +474,7 @@ final class DecoratorCore { && (!caveSkipFluid || !B.isFluid(lower) && !B.isFluid(upper)); } - private static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ, + static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ, IrisData irisData, int max) { int stack = decorator.getHeight(rng, realX, realZ, irisData); if (decorator.isScaleStack()) { diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java index 9de54ad7b..8067546da 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java @@ -39,6 +39,10 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator { @Override public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { + int limit = Math.min(max, Math.min(getEngine().getHeight(), data.getHeight())); + if (height < 0 || height >= limit) { + return; + } RNG rng = getRNG(realX, realZ); IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); @@ -51,34 +55,25 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator { && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { return; } - if (height >= 0 && height < getEngine().getHeight()) { - data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); + PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); + if (block != null) { + data.set(x, height, z, block); } return; } - int stack = decorator.getHeight(rng, realX, realZ, getData()); - if (decorator.isScaleStack()) { - stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100)); - } else { - stack = Math.min(stack, max - height); - } - - if (stack == 1) { - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } - - int engineHeight = getEngine().getHeight(); + int stack = Math.min(limit - height, + DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height)); for (int i = 0; i < stack; i++) { int h = height + i; - if (h > max || h > engineHeight) { - continue; - } - double threshold = ((double) i) / (stack - 1); - data.set(x, h, z, threshold >= decorator.getTopThreshold() + double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1); + PlatformBlockState block = threshold >= decorator.getTopThreshold() ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) - : decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); + : decorator.getBlockData100(biome, rng, realX, h, realZ, getData()); + if (block == null) { + break; + } + data.set(x, h, z, block); } } } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java index e310f29ea..03f2e8e0e 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java @@ -39,6 +39,10 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator { @Override public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { + int maxBaseY = Math.min(max, Math.min(getEngine().getHeight(), data.getHeight()) - 1); + if (height < 0 || height >= maxBaseY) { + return; + } RNG rng = getRNG(realX, realZ); IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); @@ -48,8 +52,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator { if (!decorator.isStacking()) { int targetY = height + 1; - if (height >= 0 && targetY < getEngine().getHeight() - && DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { + if (DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); if (block != null) { data.set(x, targetY, z, block); @@ -58,33 +61,15 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator { return; } - int stack = decorator.getHeight(rng, realX, realZ, getData()); - if (decorator.isScaleStack()) { - stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100)); - } - - if (stack == 1) { - int targetY = height + 1; - if (targetY >= data.getHeight() || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { - return; - } - - PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()); - if (block != null) { - data.set(x, targetY, z, block); - } - return; - } - - int engineHeight = getEngine().getHeight(); + int stack = Math.min(maxBaseY - height, + DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height)); for (int i = 0; i < stack; i++) { int h = height + i; int targetY = h + 1; - if (h >= max || targetY >= engineHeight - || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { + if (!DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { break; } - double threshold = ((double) i) / (stack - 1); + double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1); PlatformBlockState block = threshold >= decorator.getTopThreshold() ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : decorator.getBlockData100(biome, rng, realX, h, realZ, getData()); diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java index ff514cea7..6dbd40b16 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java @@ -40,11 +40,19 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.math.IrisBlockVector; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.data.VectorMap; + +import java.util.ArrayList; +import java.util.List; public class IrisDepositModifier extends EngineAssignedModifier { + private static final int CLUMPS_PER_BATCH = 8; + private static final int PREPARATION_BATCH_COUNT = 4; + private final RNG rng; public IrisDepositModifier(Engine engine) { @@ -62,29 +70,36 @@ public class IrisDepositModifier extends EngineAssignedModifier terrain, int x, int z, boolean multicore, ChunkContext context) { IrisRegion region = context.getRegion().get(7, 7); IrisBiome biome = context.getBiome().get(7, 7); - BurstExecutor burst = burst().burst(multicore); + List generators = new ArrayList<>( + getDimension().getDeposits().size() + region.getDeposits().size() + biome.getDeposits().size()); + generators.addAll(getDimension().getDeposits()); + generators.addAll(region.getDeposits()); + generators.addAll(biome.getDeposits()); + if (generators.isEmpty()) { + return; + } + BurstExecutor burst = burst().burst(multicore); long seed = x * 341873128712L + z * 132897987541L; - long mask = 0; + PreparedDeposit[] prepared = new PreparedDeposit[PREPARATION_BATCH_COUNT]; + int pending = 0; MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use(); try { - for (IrisDepositGenerator k : getDimension().getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(scopedDepositTask( - () -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); - } - - for (IrisDepositGenerator k : region.getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(scopedDepositTask( - () -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); - } - - for (IrisDepositGenerator k : biome.getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(scopedDepositTask( - () -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); + for (int i = 0; i < generators.size(); i++) { + DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L))); + 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), context)); + if (pending == prepared.length) { + completeBatches(burst, prepared, chunk, terrain, x, z, context); + pending = 0; + } + } } + completeBatches(burst, prepared, chunk, terrain, x, z, context); } finally { // complete() must run before release() even when queueing throws — already // submitted burst tasks must never write into a released chunk. @@ -96,6 +111,20 @@ public class IrisDepositModifier extends EngineAssignedModifier terrain, int x, int z, ChunkContext context) { + burst.complete(); + try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) { + 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, context); + } + } + } + } + private Runnable scopedDepositTask(Runnable task, ChunkContext context) { IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine && irisEngine.hasGenerationRuntimeScope() ? irisEngine : null; @@ -115,17 +144,36 @@ public class IrisDepositModifier extends EngineAssignedModifier data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) { - IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State(); - if (k.getSpawnChance() < rng.d()) - return; + DepositPlan plan = plan(k, rng); + 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), chunk, data, cx, cz, he, context); + } + } - boolean oreDeposit = k.isOre(getData()); + private DepositPlan plan(IrisDepositGenerator generator, RNG rng) { + if (generator.getSpawnChance() < rng.d()) { + return new DepositPlan(generator, rng, false, 0); + } + boolean ore = generator.isOre(getData()); + int attempts = rng.i(generator.getMinPerChunk(), generator.getMaxPerChunk() + 1); + return new DepositPlan(generator, rng, ore, attempts); + } - for (int l = 0; l < rng.i(k.getMinPerChunk(), k.getMaxPerChunk() + 1); l++) { - if (k.getPerClumpSpawnChance() < rng.d()) + private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) { + IrisDepositGenerator k = plan.generator(); + RNG rng = plan.rng(); + boolean oreDeposit = plan.ore(); + boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter(); + IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null; + List clumps = new ArrayList<>(limit - first); + for (int l = first; l < limit; l++) { + RNG clumpRng = rng.nextParallelRNG(l + 1L); + if (k.getPerClumpSpawnChance() < clumpRng.d()) { continue; + } - IrisObject clump = k.getClump(getEngine(), rng, getData()); + IrisObject clump = k.getClump(getEngine(), clumpRng, getData()); int dim = clump.getW(); int min = dim / 2; @@ -136,8 +184,8 @@ public class IrisDepositModifier extends EngineAssignedModifier height - 2) + if (clippedHeight && y > height - 2) { continue; + } int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y)); IrisBiome surfaceBiome = context.getBiome().get(x, z); - IrisBiome depositBiome = oreDeposit || k.usesCaveBiomeFilter() + IrisBiome depositBiome = needsCaveBiome ? getEngine().getCaveBiome( (cx << 4) + x, biomeY, (cz << 4) + z, carvingState) : null; @@ -172,13 +221,13 @@ public class IrisDepositModifier extends EngineAssignedModifier data, + int cx, int cz, HeightMap he, ChunkContext context) { + if (deposit.clumps().isEmpty()) { + return; + } + IrisDepositGenerator k = deposit.generator(); + boolean oreDeposit = deposit.ore(); + IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State(); + IrisDimension dimension = getDimension(); + for (PreparedClump prepared : deposit.clumps()) { + IrisObject clump = prepared.object(); + int x = prepared.x(); + int y = prepared.y(); + int z = prepared.z(); + RNG rng = prepared.rng(); + + VectorMap.Cursor cursor = clump.getBlocks().cursor(); + while (cursor.next()) { + IrisBlockVector j = cursor.key(); int nx = j.getBlockX() + x; int ny = j.getBlockY() + y; int nz = j.getBlockZ() + z; @@ -236,7 +306,7 @@ public class IrisDepositModifier extends EngineAssignedModifier clumps) { + } + + private record PreparedClump(IrisObject object, int x, int y, int z, RNG rng) { + } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/coral/CoralGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/coral/CoralGenerator.java index dde2938f5..26ea6a96e 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/coral/CoralGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/coral/CoralGenerator.java @@ -170,7 +170,9 @@ public final class CoralGenerator { int y = (int) Math.round(oy + dy * t); int z = (int) Math.round(oz + dz * t - sway); canvas.set(x, y, z, CoralCanvas.Role.STRUCTURE); - tip = new double[]{x, y, z}; + tip[0] = x; + tip[1] = y; + tip[2] = z; } return tip; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusGenerator.java index 1e63cdfa0..aace94dc7 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusGenerator.java @@ -63,11 +63,7 @@ public final class FungusGenerator { private static void buildUpright(Map roles, IrisFungus fungus, int height, RNG rng, long baseSeed) { int stemHeight = Math.max(1, height); - Map stemCells = new HashMap<>(); - double[] top = FungusStemBuilder.build(stemCells, fungus, stemHeight, baseSeed); - for (Vector3i v : stemCells.keySet()) { - roles.put(v, FungusCellRole.STEM); - } + double[] top = FungusStemBuilder.build(roles, fungus, stemHeight, baseSeed); int radius = pickRadius(fungus, rng); double cx = top[0]; diff --git a/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusStemBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusStemBuilder.java index 58d5120c0..321a5f9b8 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusStemBuilder.java +++ b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusStemBuilder.java @@ -20,7 +20,6 @@ package art.arcane.iris.engine.object.fungi; import art.arcane.iris.engine.object.IrisFungus; import art.arcane.iris.engine.object.tree.TreeFunctions; -import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.common.math.Vector3i; import java.util.Map; @@ -29,7 +28,7 @@ public final class FungusStemBuilder { private FungusStemBuilder() { } - public static double[] build(Map stemCells, IrisFungus fungus, int stemHeight, long seed) { + public static double[] build(Map roles, IrisFungus fungus, int stemHeight, long seed) { int width = Math.max(1, Math.min(3, fungus.getStemWidth())); double maxLean = stemHeight * Math.tan(Math.toRadians(Math.max(0.0, fungus.getStemCurve()))); double leanRad = Math.toRadians(fungus.getStemLeanAzimuth()); @@ -40,6 +39,7 @@ public final class FungusStemBuilder { double waveAzimuth = Math.toRadians(fungus.getStemLeanAzimuth() + 90.0); double waveX = Math.sin(waveAzimuth); double waveZ = Math.cos(waveAzimuth); + double wavePhase = TreeFunctions.valueNoise1D(seed, seed) * Math.PI * 2.0; double topCx = 0.0; double topCz = 0.0; @@ -48,12 +48,16 @@ public final class FungusStemBuilder { for (int y = 0; y < stemHeight; y++) { double t = stemHeight <= 1 ? 0.0 : y / (double) (stemHeight - 1); double lean = maxLean * (t * t); - double wave = waveAmp * Math.sin(2.0 * Math.PI * wavePeriods * t + TreeFunctions.valueNoise1D(seed, seed) * Math.PI * 2.0); + double wave = waveAmp * Math.sin(2.0 * Math.PI * wavePeriods * t + wavePhase); double cx = lean * leanX + wave * waveX; double cz = lean * leanZ + wave * waveZ; - for (int[] xz : squarePositions(cx, cz, width)) { - stemCells.put(new Vector3i(xz[0], y, xz[1]), null); + int ox = (width % 2 == 1 ? (int) Math.round(cx) : (int) Math.floor(cx) + 1) - width / 2; + int oz = (width % 2 == 1 ? (int) Math.round(cz) : (int) Math.floor(cz) + 1) - width / 2; + for (int dx = 0; dx < width; dx++) { + for (int dz = 0; dz < width; dz++) { + roles.put(new Vector3i(ox + dx, y, oz + dz), FungusCellRole.STEM); + } } topCx = cx; @@ -62,34 +66,4 @@ public final class FungusStemBuilder { return new double[]{topCx, topY, topCz}; } - - static int[][] squarePositions(double cx, double cz, int width) { - if (width % 2 == 1) { - int half = width / 2; - int icx = (int) Math.round(cx); - int icz = (int) Math.round(cz); - int[][] out = new int[width * width][2]; - int idx = 0; - for (int dx = -half; dx <= half; dx++) { - for (int dz = -half; dz <= half; dz++) { - out[idx][0] = icx + dx; - out[idx][1] = icz + dz; - idx++; - } - } - return out; - } - int ox = (int) Math.floor(cx) - width / 2 + 1; - int oz = (int) Math.floor(cz) - width / 2 + 1; - int[][] out = new int[width * width][2]; - int idx = 0; - for (int dx = 0; dx < width; dx++) { - for (int dz = 0; dz < width; dz++) { - out[idx][0] = ox + dx; - out[idx][1] = oz + dz; - idx++; - } - } - return out; - } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/ProceduralTreeGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/tree/ProceduralTreeGenerator.java index a6dbe7e36..cd72bf747 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/tree/ProceduralTreeGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/ProceduralTreeGenerator.java @@ -66,9 +66,10 @@ public final class ProceduralTreeGenerator { Map resolved = new HashMap<>(); Set trunkPositions = new HashSet<>(); Set leafPositions = new HashSet<>(); + RNG paletteRng = new RNG(tree.getSeed()); for (Map.Entry entry : canvas.getCells().entrySet()) { TreeBlockCanvas.Cell cell = entry.getValue(); - PlatformBlockState state = TreeBlockResolver.resolve(tree, data, cell, entry.getKey()); + PlatformBlockState state = TreeBlockResolver.resolve(tree, data, cell, entry.getKey(), paletteRng); if (state == null) { continue; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockResolver.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockResolver.java index 04c73e4af..d869aac20 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockResolver.java +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockResolver.java @@ -31,10 +31,7 @@ public final class TreeBlockResolver { private TreeBlockResolver() { } - public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos) { - RNG paletteRng = new RNG(tree.getSeed()); - RNG posRng = new RNG(tree.getSeed() ^ positionHash(pos)); - + public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos, RNG paletteRng) { switch (cell.role()) { case TRUNK -> { PlatformBlockState state = resolveBlock(tree.getTrunk(), tree.getTrunkPalette(), data, pos, paletteRng); @@ -51,7 +48,7 @@ public final class TreeBlockResolver { return resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng); } case SECONDARY_LEAF -> { - PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng, posRng); + PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng); if (state == null) { state = resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng); } @@ -94,12 +91,12 @@ public final class TreeBlockResolver { return state; } - private static PlatformBlockState resolveSecondaryLeaf(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng, RNG posRng) { + private static PlatformBlockState resolveSecondaryLeaf(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng) { if (TreeTrunkBuilder.paletteSet(tree.getSecondaryLeavesPalette())) { return tree.getSecondaryLeavesPalette().get(paletteRng, pos.x(), pos.y(), pos.z(), data); } if (tree.getWeightedSecondaryLeaves() != null && !tree.getWeightedSecondaryLeaves().isEmpty()) { - String picked = pickWeighted(tree, posRng); + String picked = pickWeighted(tree, new RNG(tree.getSeed() ^ positionHash(pos))); return picked == null ? null : B.getStateOrNull(picked, false); } if (tree.getSecondaryLeaves() != null && !tree.getSecondaryLeaves().isEmpty()) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplier.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplier.java index a4f34ffa4..6d36a0469 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplier.java +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplier.java @@ -67,11 +67,24 @@ public final class TreeDecoratorApplier { if (rng.nextDouble() > dec.getChance()) { continue; } - if (canvas.has(e[0], e[1], e[2])) { + int dx = e[0] - e[3]; + int dz = e[2] - e[4]; + boolean alongX = Math.abs(dx) >= Math.abs(dz); + int stepX = alongX ? (dx >= 0 ? 1 : -1) : 0; + int stepZ = alongX ? 0 : (dz >= 0 ? 1 : -1); + int x = e[0] + stepX; + int z = e[2] + stepZ; + TreeBlockCanvas.Cell cell = canvas.get(x, e[1], z); + while (cell != null && cell.role() != TreeBlockCanvas.Role.DECORATOR) { + x += stepX; + z += stepZ; + cell = canvas.get(x, e[1], z); + } + if (cell != null) { continue; } String facing = dec.isAxisAware() ? facingAway(e[3], e[4], e[0], e[2]) : null; - canvas.setDecor(e[0], e[1], e[2], idx, facing); + canvas.setDecor(x, e[1], z, idx, facing); } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreePlausibility.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreePlausibility.java index b9e6af329..00e1c6229 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/tree/TreePlausibility.java +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreePlausibility.java @@ -50,7 +50,9 @@ public final class TreePlausibility { return; } - Map distance = computeDistances(trunkPositions, realLeaves); + Map distance = tree.isPlausible() + ? computeDistances(trunkPositions, realLeaves) + : Map.of(); for (TreeBlockCanvas.Vec v : realLeaves) { PlatformBlockState leaf = resolved.get(v); diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeSupport.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeSupport.java index 6f24d9e16..bcb0f6035 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeSupport.java +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeSupport.java @@ -18,7 +18,6 @@ package art.arcane.iris.engine.object.tree; -import java.util.ArrayList; import java.util.Map; public final class TreeSupport { @@ -36,7 +35,7 @@ public final class TreeSupport { TreeBlockCanvas.Vec worstWood = null; int worstGap = -1; - for (TreeBlockCanvas.Vec leaf : new ArrayList<>(canvas.getLeaf())) { + for (TreeBlockCanvas.Vec leaf : canvas.getLeaf()) { Integer d = dist.get(leaf); if (d != null && d <= LEGAL_DISTANCE) { continue; diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeTrunkBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeTrunkBuilder.java index e69365a12..26a8a19a6 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeTrunkBuilder.java +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeTrunkBuilder.java @@ -121,7 +121,7 @@ public final class TreeTrunkBuilder { } private static void markExposedEnds(TreeBlockCanvas canvas) { - for (TreeBlockCanvas.Vec v : new ArrayList<>(canvas.getTrunk())) { + for (TreeBlockCanvas.Vec v : canvas.getTrunk()) { if (!canvas.has(v.x(), v.y() + 1, v.z()) || !canvas.has(v.x(), v.y() - 1, v.z())) { canvas.markExposed(v.x(), v.y(), v.z()); } diff --git a/core/src/test/java/art/arcane/iris/engine/decorator/IrisSeaDecoratorBoundsTest.java b/core/src/test/java/art/arcane/iris/engine/decorator/IrisSeaDecoratorBoundsTest.java new file mode 100644 index 000000000..064489641 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/decorator/IrisSeaDecoratorBoundsTest.java @@ -0,0 +1,147 @@ +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.hunk.Hunk; +import org.junit.Test; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IrisSeaDecoratorBoundsTest { + @Test + public void floorPercentageCannotGrowAboveTheWaterColumn() { + Fixture fixture = new Fixture(12, false); + fixture.stacking(200, 30); + + fixture.decorate(4, 8); + + assertSame(fixture.body, fixture.output.get(0, 4, 0)); + assertSame(fixture.top, fixture.output.get(0, 7, 0)); + assertNull(fixture.output.get(0, 8, 0)); + } + + @Test + public void floorPercentageRespectsAbsoluteMaximum() { + Fixture fixture = new Fixture(64, false); + fixture.stacking(100, 3); + + fixture.decorate(4, 60); + + assertSame(fixture.body, fixture.output.get(0, 4, 0)); + assertSame(fixture.top, fixture.output.get(0, 6, 0)); + assertNull(fixture.output.get(0, 7, 0)); + } + + @Test + public void surfacePercentageRespectsAbsoluteMaximum() { + Fixture fixture = new Fixture(64, true); + fixture.stacking(100, 3); + + fixture.decorate(4, 64); + + assertSame(fixture.body, fixture.output.get(0, 5, 0)); + assertSame(fixture.top, fixture.output.get(0, 7, 0)); + assertNull(fixture.output.get(0, 8, 0)); + } + + @Test + public void floorStackAtOutputCeilingUsesOneTopBlock() { + Fixture fixture = new Fixture(8, false); + fixture.stacking(200, 30); + + fixture.decorate(7, 20); + + assertSame(fixture.top, fixture.output.get(0, 7, 0)); + } + + @Test + public void unresolvedFloorPalettePreservesExistingWater() { + Fixture fixture = new Fixture(8, false); + fixture.stacking(100, 30); + PlatformBlockState water = mock(PlatformBlockState.class); + fixture.output.set(0, 3, 0, water); + when(fixture.decorator.getBlockData100(any(), any(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(null); + + fixture.decorate(3, 8); + + assertSame(water, fixture.output.get(0, 3, 0)); + assertNull(fixture.output.get(0, 4, 0)); + } + + @Test + public void surfaceStackUsesTheShorterOutputHeight() { + Fixture fixture = new Fixture(8, true); + when(fixture.engine.getHeight()).thenReturn(64); + fixture.stacking(200, 30); + + fixture.decorate(6, 64); + + assertSame(fixture.top, fixture.output.get(0, 7, 0)); + } + + @Test + public void surfaceSingleRejectsOutputCeiling() { + Fixture fixture = new Fixture(8, true); + when(fixture.engine.getHeight()).thenReturn(64); + + fixture.decorate(7, 64); + + assertNull(fixture.output.get(0, 7, 0)); + } + + @Test + public void surfaceSingleStackRejectsNegativeBaseAndEngineCeiling() { + Fixture fixture = new Fixture(16, true); + when(fixture.engine.getHeight()).thenReturn(8); + fixture.stacking(1, 30); + when(fixture.decorator.isScaleStack()).thenReturn(false); + + fixture.decorate(-1, 16); + fixture.decorate(7, 16); + + assertNull(fixture.output.get(0, 0, 0)); + assertNull(fixture.output.get(0, 8, 0)); + } + + private static final class Fixture { + private final IrisDecorator decorator = mock(IrisDecorator.class); + private final IrisBiome biome = mock(IrisBiome.class); + private final PlatformBlockState body = mock(PlatformBlockState.class); + private final PlatformBlockState top = mock(PlatformBlockState.class); + private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); + private final Hunk output; + private final IrisEngineDecorator placement; + + private Fixture(int height, boolean surface) { + when(engine.getHeight()).thenReturn(height); + output = Hunk.newArrayHunk(1, height, 1); + IrisDecorationPart part = surface ? IrisDecorationPart.SEA_SURFACE : IrisDecorationPart.SEA_FLOOR; + when(biome.getDecoratorBucket(part)).thenReturn(new IrisDecorator[]{decorator}); + when(decorator.passesChanceGate(any(), anyDouble(), anyDouble(), any())).thenReturn(true); + when(decorator.getBlockData100(any(), any(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(body); + when(decorator.getBlockDataForTop(any(), any(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(top); + when(decorator.getTopThreshold()).thenReturn(1D); + placement = surface ? new IrisSeaSurfaceDecorator(engine) : new IrisSeaFloorDecorator(engine); + } + + private void stacking(int percentage, int cap) { + when(decorator.isStacking()).thenReturn(true); + when(decorator.isScaleStack()).thenReturn(true); + when(decorator.getHeight(any(), anyDouble(), anyDouble(), any())).thenReturn(percentage); + when(decorator.getAbsoluteMaxStack()).thenReturn(cap); + } + + private void decorate(int height, int maximum) { + placement.decorate(0, 0, 0, 0, 0, 0, 0, 0, output, biome, height, maximum); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java index 97a057836..1dd7cb627 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java @@ -3,6 +3,9 @@ package art.arcane.iris.engine.modifier; import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDepositGenerator; +import art.arcane.iris.engine.object.IrisDepositHeightDistribution; +import art.arcane.iris.engine.object.IrisDepositPlacementScope; +import art.arcane.iris.engine.object.IrisObject; import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformBlockState; @@ -16,6 +19,7 @@ import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.math.RNG; import org.junit.Test; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -33,14 +37,12 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.same; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -90,6 +92,26 @@ public class IrisDepositModifierContextTest { } } + @Test + public void failedClumpChanceDoesNotResampleTheConfiguredAttemptCount() { + Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); + IrisDepositGenerator generator = mock(IrisDepositGenerator.class); + when(generator.getSpawnChance()).thenReturn(1D); + when(generator.getMinPerChunk()).thenReturn(0); + when(generator.getMaxPerChunk()).thenReturn(4); + when(generator.getPerClumpSpawnChance()).thenReturn(0D); + RNG rng = mock(RNG.class); + when(rng.d()).thenReturn(0.5D); + when(rng.i(0, 5)).thenReturn(4, 0); + when(rng.nextParallelRNG(anyLong())).thenReturn(rng); + + new IrisDepositModifier(engine).generate(generator, null, null, rng, 0, 0, false, null); + + verify(generator, times(4)).getPerClumpSpawnChance(); + verify(rng, times(1)).i(0, 5); + verify(generator, never()).getClump(any(), any(), any()); + } + private static final class Fixture implements AutoCloseable { private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final Engine engine; @@ -138,16 +160,27 @@ public class IrisDepositModifierContextTest { return null; }).when(scope).close(); } - modifier = spy(new IrisDepositModifier(engine)); - doAnswer(invocation -> { - assertNotSame(caller, Thread.currentThread()); - assertEquals(scoped, workerRuntime.get()); - assertSame(engine, IrisContext.require().getEngine()); - assertSame(context, IrisContext.require().getChunkContext()); - assertEquals(71L, IrisContext.require().getGenerationSessionId()); - completed.incrementAndGet(); - return null; - }).when(modifier).generate(any(), same(chunk), any(), any(), eq(2), eq(-3), eq(false), same(context)); + when(engine.getHeight()).thenReturn(16); + IrisObject clump = new IrisObject(1, 1, 1); + for (IrisDepositGenerator generator : new IrisDepositGenerator[]{dimensionDeposit, regionDeposit, biomeDeposit}) { + when(generator.getSpawnChance()).thenReturn(1D); + when(generator.getMinPerChunk()).thenReturn(1); + when(generator.getMaxPerChunk()).thenReturn(1); + when(generator.getPerClumpSpawnChance()).thenReturn(1D); + when(generator.getPlacementScope()).thenReturn(IrisDepositPlacementScope.FULL_HEIGHT); + when(generator.getHeightDistribution()).thenReturn(IrisDepositHeightDistribution.UNIFORM); + when(generator.matchesBiome(any(), any())).thenReturn(true); + doAnswer(invocation -> { + assertNotSame(caller, Thread.currentThread()); + assertEquals(scoped, workerRuntime.get()); + assertSame(engine, IrisContext.require().getEngine()); + assertSame(context, IrisContext.require().getChunkContext()); + assertEquals(71L, IrisContext.require().getGenerationSessionId()); + completed.incrementAndGet(); + return clump; + }).when(generator).getClump(any(), any(), any()); + } + modifier = new IrisDepositModifier(engine); doAnswer(invocation -> { assertEquals(3, completed.get()); assertEquals(scoped ? 3 : 0, closedScopes.get()); diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierOrderingTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierOrderingTest.java new file mode 100644 index 000000000..59f022235 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierOrderingTest.java @@ -0,0 +1,223 @@ +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisDepositGenerator; +import art.arcane.iris.engine.object.IrisDepositHeightDistribution; +import art.arcane.iris.engine.object.IrisDepositPlacementScope; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.math.RNG; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class IrisDepositModifierOrderingTest { + @Before + public void bindPlatform() { + IrisPlatforms.unbind(); + PlatformRegistries registries = mock(PlatformRegistries.class); + PlatformBlockState stone = state("stone"); + when(registries.block(anyString())).thenReturn(stone); + IrisPlatform platform = mock(IrisPlatform.class); + when(platform.registries()).thenReturn(registries); + IrisPlatforms.bind(platform); + } + + @After + public void unbindPlatform() { + IrisPlatforms.unbind(); + } + + @Test + public void reverseWorkerCompletionKeepsDimensionRegionBiomePlacementOrder() throws Exception { + try (Fixture fixture = new Fixture(true, 1)) { + fixture.generate(); + assertEquals(List.of("biome", "region", "dimension"), fixture.completed); + fixture.assertBiomeWins(); + } + } + + @Test + public void inlinePreparationUsesTheSameOverlappingHostRules() throws Exception { + try (Fixture fixture = new Fixture(false, 1)) { + fixture.generate(); + assertEquals(List.of("dimension", "region", "biome"), fixture.completed); + fixture.assertBiomeWins(); + } + } + + @Test + public void manyAttemptBatchesKeepEveryClumpSeedAndOrderedHostReplacement() throws Exception { + List expectedSamples = new ArrayList<>(); + RNG generatorRng = new RNG(0L).nextParallelRNG(0L); + for (String name : List.of("dimension", "region", "biome")) { + for (int i = 0; i < 25; i++) { + expectedSamples.add(name + ":" + generatorRng.nextParallelRNG(i + 1L).getSeed()); + } + } + Collections.sort(expectedSamples); + + for (boolean parallel : new boolean[]{false, true}) { + try (Fixture fixture = new Fixture(parallel, 25)) { + fixture.generate(); + Collections.sort(fixture.samples); + assertEquals(expectedSamples, fixture.samples); + assertEquals(75, fixture.completed.size()); + fixture.assertBiomeWins(); + } + } + } + + private static PlatformBlockState state(String material) { + PlatformBlockState state = mock(PlatformBlockState.class); + when(state.key()).thenReturn("minecraft:" + material); + when(state.materialKey()).thenReturn("minecraft:" + material); + return state; + } + + private static final class Fixture implements AutoCloseable { + private final ExecutorService executor = Executors.newFixedThreadPool(3); + private final List completed = Collections.synchronizedList(new ArrayList<>()); + private final List samples = Collections.synchronizedList(new ArrayList<>()); + private final CountDownLatch biomeReady = new CountDownLatch(1); + private final CountDownLatch regionReady = new CountDownLatch(1); + private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); + private final ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS); + private final PlatformBlockState stone = state("stone"); + private final PlatformBlockState granite = state("granite"); + private final PlatformBlockState diorite = state("diorite"); + private final PlatformBlockState diamond = state("diamond_ore"); + private final Hunk output = Hunk.newArrayHunk(16, 16, 16); + private final MantleChunk chunk; + private final boolean parallel; + private final int attempts; + + @SuppressWarnings("unchecked") + private Fixture(boolean parallel, int attempts) { + this.parallel = parallel; + this.attempts = attempts; + when(engine.getHeight()).thenReturn(16); + when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenReturn(null); + when(engine.getDimension().getDepositVariants()).thenReturn(new KList<>()); + when(context.getRegion().get(anyInt(), anyInt()).getDepositVariants()).thenReturn(new KList<>()); + when(context.getGenerationSessionId()).thenReturn(11L); + chunk = mock(MantleChunk.class); + Mantle mantle = engine.getMantle().getMantle(); + doReturn(chunk).when(mantle).getChunk(0, 0); + when(chunk.use()).thenReturn(chunk); + MultiBurst pool = mock(MultiBurst.class); + BurstExecutor burst = new BurstExecutor(executor, 3); + burst.setMulticore(parallel); + when(engine.burst()).thenReturn(pool); + when(pool.burst(parallel)).thenReturn(burst); + IrisDepositGenerator dimensionDeposit = generator("dimension", stone, granite); + IrisDepositGenerator regionDeposit = generator("region", granite, diorite); + IrisDepositGenerator biomeDeposit = generator("biome", diorite, diamond); + when(engine.getDimension().getDeposits()).thenReturn(new KList<>(dimensionDeposit)); + when(context.getRegion().get(7, 7).getDeposits()).thenReturn(new KList<>(regionDeposit)); + when(context.getBiome().get(7, 7).getDeposits()).thenReturn(new KList<>(biomeDeposit)); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + output.set(x, 4, z, stone); + } + } + } + + private IrisDepositGenerator generator(String name, PlatformBlockState requiredHost, PlatformBlockState replacement) { + IrisDepositGenerator generator = mock(IrisDepositGenerator.class); + when(generator.getSpawnChance()).thenReturn(1D); + when(generator.getPerClumpSpawnChance()).thenReturn(1D); + when(generator.getMinPerChunk()).thenReturn(attempts); + when(generator.getMaxPerChunk()).thenReturn(attempts); + when(generator.getMinHeight()).thenReturn(4); + when(generator.getMaxHeight()).thenReturn(4); + when(generator.getPlacementScope()).thenReturn(IrisDepositPlacementScope.FULL_HEIGHT); + when(generator.getHeightDistribution()).thenReturn(IrisDepositHeightDistribution.UNIFORM); + when(generator.matchesBiome(any(), any())).thenReturn(true); + when(generator.canReplace(requiredHost)).thenReturn(true); + IrisObject clump = new IrisObject(33, 1, 33); + for (int x = -16; x <= 16; x++) { + for (int z = -16; z <= 16; z++) { + clump.getBlocks().put(new IrisBlockVector(x, 0, z), replacement); + } + } + 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")) { + assertTrue(regionReady.await(10, TimeUnit.SECONDS)); + } else if (parallel && attempts == 1 && name.equals("region")) { + assertTrue(biomeReady.await(10, TimeUnit.SECONDS)); + } + RNG rng = invocation.getArgument(1); + samples.add(name + ":" + rng.getSeed()); + completed.add(name); + if (name.equals("biome")) { + biomeReady.countDown(); + } else if (name.equals("region")) { + regionReady.countDown(); + } + return clump; + }); + return generator; + } + + private void generate() { + try (MockedStatic blocks = mockStatic(B.class, CALLS_REAL_METHODS)) { + blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> invocation.getArgument(1)); + new IrisDepositModifier(engine).generateDeposits(output, 0, 0, parallel, context); + } + verify(chunk).release(); + } + + private void assertBiomeWins() { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + assertSame("Deposit collision at " + x + ",4," + z, diamond, output.get(x, 4, z)); + } + } + } + + @Override + public void close() throws Exception { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/fungi/FungusStemBuilderTest.java b/core/src/test/java/art/arcane/iris/engine/object/fungi/FungusStemBuilderTest.java new file mode 100644 index 000000000..5db50cf78 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/fungi/FungusStemBuilderTest.java @@ -0,0 +1,67 @@ +package art.arcane.iris.engine.object.fungi; + +import art.arcane.iris.engine.object.IrisFungus; +import art.arcane.iris.util.common.math.Vector3i; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class FungusStemBuilderTest { + @Test + public void straightStemsKeepOddAndEvenWidthAnchors() { + for (int width = 1; width <= 3; width++) { + IrisFungus fungus = new IrisFungus(); + fungus.setStemWidth(width); + fungus.setStemCurve(0D); + fungus.setStemWaveAmplitude(0D); + Map roles = new HashMap<>(); + + double[] top = FungusStemBuilder.build(roles, fungus, 7, 421L); + + assertArrayEquals(new double[]{0D, 6D, 0D}, top, 0D); + assertEquals(width * width * 7, roles.size()); + int origin = width == 3 ? -1 : 0; + for (int y = 0; y < 7; y++) { + for (int x = origin; x < origin + width; x++) { + for (int z = origin; z < origin + width; z++) { + assertEquals(FungusCellRole.STEM, roles.get(new Vector3i(x, y, z))); + } + } + } + } + } + + @Test + public void curvedStemsKeepEveryLayerAndReturnTheirCapAnchor() { + IrisFungus fungus = new IrisFungus(); + fungus.setStemWidth(3); + fungus.setStemCurve(22D); + fungus.setStemLeanAzimuth(225D); + fungus.setStemWaveAmplitude(2D); + fungus.setStemWavePeriods(1.5D); + Map roles = new HashMap<>(); + + double[] top = FungusStemBuilder.build(roles, fungus, 24, -733L); + + assertEquals(24 * 9, roles.size()); + assertTrue(top[0] < 0D); + assertTrue(top[2] < 0D); + assertEquals(23D, top[1], 0D); + assertEquals(FungusCellRole.STEM, roles.get(new Vector3i( + (int) Math.round(top[0]), 23, (int) Math.round(top[2])))); + for (int y = 0; y < 24; y++) { + int count = 0; + for (Vector3i position : roles.keySet()) { + if (position.getY() == y) { + count++; + } + } + assertEquals(9, count); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/tree/ProceduralTreeGeneratorTest.java b/core/src/test/java/art/arcane/iris/engine/object/tree/ProceduralTreeGeneratorTest.java index 4e4f5fa31..a19ecfe2b 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/tree/ProceduralTreeGeneratorTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/tree/ProceduralTreeGeneratorTest.java @@ -6,9 +6,11 @@ import art.arcane.iris.engine.object.IrisTreeBranches; import art.arcane.iris.engine.object.IrisTreeFunction; import art.arcane.iris.engine.object.IrisTreeLeafMode; import art.arcane.iris.engine.object.IrisTreeProfile; +import art.arcane.iris.spi.PlatformBlockState; import org.junit.Test; import java.util.HashSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -18,6 +20,9 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class ProceduralTreeGeneratorTest { @@ -268,4 +273,23 @@ public class ProceduralTreeGeneratorTest { distances.get(new TreeBlockCanvas.Vec(x, 0, 0))); } } + + @Test + public void disabledPlausibilityMakesDisconnectedLeavesPersistent() { + IrisProceduralTree tree = new IrisProceduralTree(); + tree.setPlausible(false); + TreeBlockCanvas.Vec position = new TreeBlockCanvas.Vec(40, 20, -10); + PlatformBlockState leaf = mock(PlatformBlockState.class); + PlatformBlockState persistent = mock(PlatformBlockState.class); + PlatformBlockState supported = mock(PlatformBlockState.class); + when(leaf.key()).thenReturn("minecraft:oak_leaves[persistent=false,distance=7]"); + when(leaf.withProperty("persistent", "true")).thenReturn(persistent); + when(persistent.withProperty("distance", "1")).thenReturn(supported); + Map resolved = new HashMap<>(); + resolved.put(position, leaf); + + TreePlausibility.apply(resolved, Set.of(new TreeBlockCanvas.Vec(0, 0, 0)), Set.of(position), tree); + + assertSame(supported, resolved.get(position)); + } } diff --git a/core/src/test/java/art/arcane/iris/engine/object/tree/TreeBlockResolverTest.java b/core/src/test/java/art/arcane/iris/engine/object/tree/TreeBlockResolverTest.java new file mode 100644 index 000000000..52fdd22b7 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/tree/TreeBlockResolverTest.java @@ -0,0 +1,70 @@ +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.engine.object.IrisBlockData; +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TreeBlockResolverTest { + @Test + public void sharedPaletteRandomSourceMatchesFreshPerVoxelSources() { + IrisProceduralTree tree = new IrisProceduralTree(); + tree.setSeed(84261L); + tree.setTrunkPalette(palette("oak_log", "spruce_log")); + tree.setLeavesPalette(palette("oak_leaves", "birch_leaves")); + tree.setSecondaryTrunkPalette(palette("birch_log", "jungle_log")); + tree.setSecondaryLeavesPalette(palette("spruce_leaves", "jungle_leaves")); + RNG shared = new RNG(tree.getSeed()); + List freshBlocks = new ArrayList<>(); + List sharedBlocks = new ArrayList<>(); + Set materials = new HashSet<>(); + TreeBlockCanvas.Role[] roles = { + TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Role.LEAF, + TreeBlockCanvas.Role.SECONDARY_TRUNK, TreeBlockCanvas.Role.SECONDARY_LEAF + }; + for (int x = -8; x <= 8; x++) { + for (int y = 0; y < 24; y++) { + TreeBlockCanvas.Vec position = new TreeBlockCanvas.Vec(x, y, x - y); + TreeBlockCanvas.Cell cell = new TreeBlockCanvas.Cell( + roles[Math.floorMod(x + y, roles.length)], TreeBlockCanvas.Axis.NONE, false, -1, null); + PlatformBlockState expected = TreeBlockResolver.resolve(tree, null, cell, position, new RNG(tree.getSeed())); + PlatformBlockState actual = TreeBlockResolver.resolve(tree, null, cell, position, shared); + freshBlocks.add(position + ":" + expected.key()); + sharedBlocks.add(position + ":" + actual.key()); + materials.add(actual.key()); + } + } + + assertEquals(freshBlocks, sharedBlocks); + assertEquals(new RNG(tree.getSeed()).nextLong(), shared.nextLong()); + assertTrue(materials.size() > roles.length); + } + + private static IrisMaterialPalette palette(String first, String second) { + IrisBlockData firstBlock = block(first); + IrisBlockData secondBlock = block(second); + return new IrisMaterialPalette().setPalette(new KList<>(firstBlock, secondBlock)); + } + + private static IrisBlockData block(String material) { + PlatformBlockState state = mock(PlatformBlockState.class); + when(state.key()).thenReturn("minecraft:" + material); + IrisBlockData block = mock(IrisBlockData.class); + when(block.getWeight()).thenReturn(1); + when(block.getBlockData(null)).thenReturn(state); + return block; + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplierTest.java b/core/src/test/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplierTest.java new file mode 100644 index 000000000..6fc05ebae --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplierTest.java @@ -0,0 +1,45 @@ +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.engine.object.IrisTreeDecorator; +import art.arcane.iris.engine.object.IrisTreeDecoratorTarget; +import art.arcane.volmlib.util.collection.KList; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class TreeDecoratorApplierTest { + @Test + public void branchTipsExtendOutwardThroughLeavesWithoutReplacingWood() { + TreeBlockCanvas canvas = new TreeBlockCanvas(); + for (int x = -3; x <= 3; x++) { + canvas.setTrunk(x, 4, 0, TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Axis.X); + } + canvas.setLeaf(-4, 4, 0, TreeBlockCanvas.Role.LEAF); + canvas.setLeaf(4, 4, 0, TreeBlockCanvas.Role.LEAF); + IrisTreeDecorator decorator = new IrisTreeDecorator() + .setTarget(IrisTreeDecoratorTarget.BRANCH_TIP) + .setBlock("minecraft:shroomlight") + .setChance(1D) + .setAxisAware(true); + IrisProceduralTree tree = new IrisProceduralTree(); + tree.setDecorators(new KList<>(decorator)); + List endpoints = List.of(new int[]{-3, 4, 0, 0, 0}, new int[]{3, 4, 0, 0, 0}); + + TreeDecoratorApplier.apply(canvas, tree, 4321L, endpoints); + + assertEquals(TreeBlockCanvas.Role.DECORATOR, canvas.get(-5, 4, 0).role()); + assertEquals("west", canvas.get(-5, 4, 0).facing()); + assertEquals(TreeBlockCanvas.Role.DECORATOR, canvas.get(5, 4, 0).role()); + assertEquals("east", canvas.get(5, 4, 0).facing()); + assertEquals(7, canvas.getTrunk().size()); + assertEquals(2, canvas.getLeaf().size()); + assertEquals(11, canvas.getCells().size()); + + TreeDecoratorApplier.apply(canvas, tree, 4321L, endpoints); + + assertEquals(11, canvas.getCells().size()); + } +}