diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java index f124c8e89..1bac17826 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java @@ -58,7 +58,7 @@ public class IrisOreGenerator { } public PlatformBlockState generate(int x, int y, int z, RNG rng, IrisData data) { - if (!range.contains(y)) { + if (threshold <= 0 || !range.contains(y)) { return null; } 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 26ea6a96e..e7f2822fa 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 @@ -88,16 +88,18 @@ public final class CoralGenerator { private static void buildFan(CoralCanvas canvas, IrisCoral coral, int height, long seed) { int half = Math.max(1, coral.getFanWidth()); - for (int y = 0; y < height; y++) { + int tippedWidth = -1; + for (int y = height - 1; y >= 0; y--) { double t = y / (double) Math.max(1, height - 1); double profile = Math.sin(Math.PI * t); int w = (int) Math.round(half * profile); for (int dx = -w; dx <= w; dx++) { canvas.set(dx, y, 0, CoralCanvas.Role.STRUCTURE); + if (Math.abs(dx) > tippedWidth) { + placeTip(canvas, coral, dx, y, 0, seed + dx); + } } - } - for (int dx = -half; dx <= half; dx++) { - placeTip(canvas, coral, dx, height - 1, 0, seed + dx); + tippedWidth = Math.max(tippedWidth, w); } } 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 d869aac20..f0e23f988 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 @@ -23,6 +23,7 @@ import art.arcane.iris.engine.object.IrisMaterialPalette; import art.arcane.iris.engine.object.IrisProceduralBlocks; import art.arcane.iris.engine.object.IrisProceduralTree; import art.arcane.iris.engine.object.IrisTreeDecorator; +import art.arcane.iris.engine.object.IrisTreeSecondaryLeaf; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.common.data.B; import art.arcane.volmlib.util.math.RNG; @@ -106,16 +107,16 @@ public final class TreeBlockResolver { } private static String pickWeighted(IrisProceduralTree tree, RNG rng) { - int total = 0; - for (art.arcane.iris.engine.object.IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) { + long total = 0; + for (IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) { total += Math.max(0, s.getWeight()); } if (total <= 0) { return null; } double r = rng.nextDouble() * total; - double cumulative = 0; - for (art.arcane.iris.engine.object.IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) { + long cumulative = 0; + for (IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) { cumulative += Math.max(0, s.getWeight()); if (r < cumulative) { return s.getBlock(); diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisOreGeneratorTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisOreGeneratorTest.java new file mode 100644 index 000000000..748f09560 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisOreGeneratorTest.java @@ -0,0 +1,66 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +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.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public class IrisOreGeneratorTest { + @Test + public void zeroThresholdDisablesEvenZeroNoiseSamples() { + Fixture fixture = new Fixture(0.0); + fixture.generator.setThreshold(0.0); + + assertNull(fixture.generator.generate(2, 40, 3, fixture.rng, null)); + verifyNoInteractions(fixture.noise, fixture.style); + } + + @Test + public void positiveThresholdStillIncludesExactCutoffAndVerticalBounds() { + Fixture fixture = new Fixture(0.5); + fixture.generator.setThreshold(0.5); + + assertSame(fixture.ore, fixture.generator.generate(2, 30, 3, fixture.rng, null)); + assertSame(fixture.ore, fixture.generator.generate(2, 80, 3, fixture.rng, null)); + assertNull(fixture.generator.generate(2, 29, 3, fixture.rng, null)); + assertNull(fixture.generator.generate(2, 81, 3, fixture.rng, null)); + fixture.generator.setThreshold(0.49); + assertNull(fixture.generator.generate(2, 40, 3, fixture.rng, null)); + } + + @Test + public void fullThresholdIncludesMaximumNoise() { + Fixture fixture = new Fixture(1.0); + fixture.generator.setThreshold(1.0); + + assertSame(fixture.ore, fixture.generator.generate(2, 40, 3, fixture.rng, null)); + } + + private static final class Fixture { + private final RNG rng = new RNG(34L); + private final PlatformBlockState ore = mock(PlatformBlockState.class); + private final CNG noise = mock(CNG.class); + private final IrisGeneratorStyle style = mock(IrisGeneratorStyle.class); + private final IrisOreGenerator generator = new IrisOreGenerator(); + + private Fixture(double sample) { + IrisMaterialPalette palette = mock(IrisMaterialPalette.class); + when(palette.getPalette()).thenReturn(new KList<>(mock(IrisBlockData.class))); + when(palette.get(any(), anyDouble(), anyDouble(), anyDouble(), isNull())).thenReturn(ore); + when(noise.noise(anyDouble(), anyDouble(), anyDouble())).thenReturn(sample); + when(style.create(any(), isNull())).thenReturn(noise); + generator.setPalette(palette); + generator.setChanceStyle(style); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/coral/CoralGeneratorTest.java b/core/src/test/java/art/arcane/iris/engine/object/coral/CoralGeneratorTest.java new file mode 100644 index 000000000..69325e61a --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/coral/CoralGeneratorTest.java @@ -0,0 +1,72 @@ +package art.arcane.iris.engine.object.coral; + +import art.arcane.iris.engine.object.IrisBlockData; +import art.arcane.iris.engine.object.IrisCoral; +import art.arcane.iris.engine.object.IrisCoralForm; +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +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 CoralGeneratorTest { + @Test + public void fanTipsFollowTheTopOfEachOccupiedColumn() { + PlatformBlockState structure = mock(PlatformBlockState.class); + PlatformBlockState tip = mock(PlatformBlockState.class); + for (int height : new int[]{2, 7, 14}) { + for (int width : new int[]{1, 3, 11}) { + IrisCoral coral = new IrisCoral().setForm(IrisCoralForm.FAN) + .setHeightMin(height).setHeightMax(height).setFanWidth(width) + .setWaterlogged(false).setBlockPalette(palette(structure)); + IrisObject bare = CoralGenerator.generate(coral, 0, new RNG(0L), null); + Map tops = new HashMap<>(); + Set barePositions = new HashSet<>(); + for (IrisBlockVector position : bare.getBlocks().keys()) { + barePositions.add(position); + tops.merge(position.getBlockX(), position.getBlockY(), Math::max); + } + coral.setTipPalette(palette(tip)); + for (double chance : new double[]{0.0, 0.35, 1.0}) { + coral.setTipChance(chance); + for (long seed = 0; seed < 16; seed++) { + IrisObject decorated = CoralGenerator.generate(coral, 0, new RNG(seed), null); + Set positions = new HashSet<>(); + int tipCount = 0; + for (IrisBlockVector position : decorated.getBlocks().keys()) { + positions.add(position); + if (decorated.getBlocks().get(position) == tip) { + assertTrue("Fan tip must occupy an existing column", tops.containsKey(position.getBlockX())); + assertEquals(tops.get(position.getBlockX()).intValue(), position.getBlockY()); + tipCount++; + } + } + assertEquals(barePositions, positions); + if (chance == 1.0) { + assertEquals(tops.size(), tipCount); + } + } + } + } + } + } + + private static IrisMaterialPalette palette(PlatformBlockState state) { + IrisBlockData block = mock(IrisBlockData.class); + when(block.getWeight()).thenReturn(1); + when(block.getBlockData(null)).thenReturn(state); + return new IrisMaterialPalette().setPalette(new KList<>(block)); + } +} 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 index 52fdd22b7..5fa5550a2 100644 --- 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 @@ -3,10 +3,13 @@ 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.engine.object.IrisTreeSecondaryLeaf; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; import org.junit.Test; +import org.mockito.MockedStatic; import java.util.ArrayList; import java.util.HashSet; @@ -16,9 +19,42 @@ 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.mockStatic; import static org.mockito.Mockito.when; public class TreeBlockResolverTest { + @Test + public void largeSecondaryWeightsKeepTheirRelativeDistribution() { + IrisProceduralTree tree = new IrisProceduralTree(); + tree.setSeed(91874L); + tree.setLeavesPalette(palette("oak_leaves", "birch_leaves")); + PlatformBlockState blossom = mock(PlatformBlockState.class); + PlatformBlockState light = mock(PlatformBlockState.class); + List normal = new ArrayList<>(); + List large = new ArrayList<>(); + TreeBlockCanvas.Cell cell = new TreeBlockCanvas.Cell( + TreeBlockCanvas.Role.SECONDARY_LEAF, TreeBlockCanvas.Axis.NONE, false, -1, null); + try (MockedStatic blocks = mockStatic(B.class)) { + blocks.when(() -> B.getStateOrNull("minecraft:cherry_leaves", false)).thenReturn(blossom); + blocks.when(() -> B.getStateOrNull("minecraft:shroomlight", false)).thenReturn(light); + for (int weight : new int[]{1, Integer.MAX_VALUE}) { + tree.setWeightedSecondaryLeaves(new KList<>( + new IrisTreeSecondaryLeaf("minecraft:cherry_leaves", weight), + new IrisTreeSecondaryLeaf("minecraft:shroomlight", weight))); + List resolved = weight == 1 ? normal : large; + for (int x = -16; x <= 16; x++) { + for (int y = 0; y < 16; y++) { + resolved.add(TreeBlockResolver.resolve(tree, null, cell, + new TreeBlockCanvas.Vec(x, y, x + y), new RNG(tree.getSeed()))); + } + } + } + } + assertTrue(normal.contains(blossom)); + assertTrue(normal.contains(light)); + assertEquals(normal, large); + } + @Test public void sharedPaletteRandomSourceMatchesFreshPerVoxelSources() { IrisProceduralTree tree = new IrisProceduralTree();