From b9b969f1f9044456045531f281acb4915bd3bdc6 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 6 Sep 2026 03:31:54 -0400 Subject: [PATCH] Simplified procedural feature generation - Fungi build stem roles directly without intermediate maps or coordinate arrays - Tree palettes reuse one random source per object - Persistent leaves skip distance traversal when plausibility is disabled - Tree support scans avoid copied coordinate sets - Coral arm samples reuse their endpoint array - Sea decorators share scaled stack limits and respect output bounds - Unresolved sea floor palette entries preserve existing water - Tests cover stem geometry persistent leaves and sea stack limits --- .../iris/engine/decorator/DecoratorCore.java | 2 +- .../decorator/IrisSeaFloorDecorator.java | 37 +++--- .../decorator/IrisSeaSurfaceDecorator.java | 5 +- .../engine/object/coral/CoralGenerator.java | 4 +- .../engine/object/fungi/FungusGenerator.java | 6 +- .../object/fungi/FungusStemBuilder.java | 44 ++----- .../object/tree/ProceduralTreeGenerator.java | 3 +- .../engine/object/tree/TreeBlockResolver.java | 11 +- .../engine/object/tree/TreePlausibility.java | 4 +- .../iris/engine/object/tree/TreeSupport.java | 3 +- .../engine/object/tree/TreeTrunkBuilder.java | 2 +- .../decorator/IrisSeaDecoratorBoundsTest.java | 112 ++++++++++++++++++ .../object/fungi/FungusStemBuilderTest.java | 67 +++++++++++ .../tree/ProceduralTreeGeneratorTest.java | 24 ++++ 14 files changed, 245 insertions(+), 79 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/engine/decorator/IrisSeaDecoratorBoundsTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/object/fungi/FungusStemBuilderTest.java 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..0f79a82c3 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 @@ -58,10 +58,7 @@ 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)); - } + int stack = DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height); if (stack == 1) { int targetY = height + 1; 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/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..19312a4c7 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/decorator/IrisSeaDecoratorBoundsTest.java @@ -0,0 +1,112 @@ +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)); + } + + 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 Hunk output; + private final IrisEngineDecorator placement; + + private Fixture(int height, boolean surface) { + Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); + 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/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)); + } }