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 bb73646a6..80ff26a4f 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 @@ -53,6 +53,8 @@ import lombok.experimental.Accessors; @Description("Creates ore & other block deposits underground") @Data public class IrisDepositGenerator { + private static final int FOOTPRINT_MAXIMUM_SIZE = 64; + private static final ThreadLocal FOOTPRINT_CELLS = ThreadLocal.withInitial(FootprintCells::new); private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L; private static final long FNV_PRIME = 0x100000001b3L; @@ -267,6 +269,42 @@ public class IrisDepositGenerator { return new IrisObject(1, 1, 1); } + KSet cells = new KSet<>(); + vanillaEllipsoidCells(rng, size, (x, y, z) -> cells.add(new BlockPosition(x, y, z))); + return objectFromCells(cells, rng, rdata); + } + + /** + * Advances {@code rng} exactly as {@link #getClump(Engine, RNG, IrisData)} would and reports the clump's width + * and lowest block offset without building it, so a caller can reject the clump first and replay the build + * 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) { + return null; + } + 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; + } + for (int cell = 0; cell < cells.count; cell++) { + rng.i(0, paletteSize); + } + return new ClumpFootprint(cells.extentX * 2 + 1, cells.minY, false); + } + + private static void vanillaEllipsoidCells(RNG rng, int size, CellSink sink) { float angle = rng.nextFloat() * (float) Math.PI; float reach = size / 8F; double startX = Math.sin(angle) * reach; @@ -309,7 +347,6 @@ public class IrisDepositGenerator { } } - KSet cells = new KSet<>(); for (int i = 0; i < size; i++) { double radius = nodes[i * 4 + 3]; if (radius < 0D) { @@ -338,14 +375,12 @@ public class IrisDepositGenerator { for (int z = minZ; z <= maxZ; z++) { double nz = (z + 0.5D - centerZ) / radius; if (nx * nx + ny * ny + nz * nz < 1D) { - cells.add(new BlockPosition(x, y, z)); + sink.add(x, y, z); } } } } } - - return objectFromCells(cells, rng, rdata); } IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) { @@ -564,4 +599,52 @@ public class IrisDepositGenerator { record ClumpCacheKey(long depositSeed, int minSize, int maxSize) { } + + @FunctionalInterface + private interface CellSink { + void add(int x, int y, int z); + } + + public record ClumpFootprint(int width, int minY, boolean empty) { + static final ClumpFootprint EMPTY = new ClumpFootprint(1, 0, true); + } + + private static final class FootprintCells implements CellSink { + private static final int OFFSET = 32; + 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 int touchedCount; + private int count; + private int extentX; + private int minY; + + private void reset() { + for (int index = 0; index < touchedCount; index++) { + occupied[touched[index]] = 0L; + } + touchedCount = 0; + count = 0; + extentX = 0; + minY = Integer.MAX_VALUE; + } + + @Override + public void add(int x, int y, int z) { + int index = ((x + OFFSET) * AXIS + (y + OFFSET)) * AXIS + (z + OFFSET); + int word = index >>> 6; + long bit = 1L << (index & 63); + long current = occupied[word]; + if ((current & bit) != 0L) { + return; + } + if (current == 0L) { + touched[touchedCount++] = word; + } + occupied[word] = current | bit; + count++; + extentX = Math.max(extentX, Math.abs(x)); + minY = Math.min(minY, y); + } + } } 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 b3afa2a50..afd4ee15b 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,7 @@ 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.decoration.IrisDepositBiomeScope; import art.arcane.iris.generation.decoration.IrisDepositGenerator; import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution; import art.arcane.iris.generation.decoration.IrisDepositPlacementScope; @@ -96,7 +97,7 @@ public class IrisDepositModifier extends EngineAssignedModifier data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) { - DepositPlan plan = plan(k, rng); + 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), chunk, data, cx, cz, he, context); } } - private DepositPlan plan(IrisDepositGenerator generator, RNG rng) { + private DepositPlan plan(IrisDepositGenerator generator, RNG rng, ChunkContext context) { if (generator.getSpawnChance() < rng.d()) { return new DepositPlan(generator, rng.getSeed(), false, 0); } boolean ore = generator.isOre(getData()); int attempts = rng.i(generator.getMinPerChunk(), generator.getMaxPerChunk() + 1); - return new DepositPlan(generator, rng.getSeed(), ore, attempts); + return new DepositPlan(generator, rng.getSeed(), ore, + attempts == 0 || surfaceBiomeMayMatch(generator, context) ? attempts : 0); + } + + /** + * A surface-scoped biome filter reads the surface biome of the clump's own column, so when no column of the + * chunk passes it every clump is rejected at that check. Each clump draws from its own seeded RNG, so skipping + * them all up front changes nothing else. + */ + private static boolean surfaceBiomeMayMatch(IrisDepositGenerator generator, ChunkContext context) { + if (generator.getBiomeScope() != IrisDepositBiomeScope.SURFACE + || (generator.getIncludedBiomes() == null || generator.getIncludedBiomes().isEmpty()) + && (generator.getExcludedBiomes() == null || generator.getExcludedBiomes().isEmpty())) { + return true; + } + IrisBiome checked = null; + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + IrisBiome surface = context.getBiome().get(x, z); + if (surface == checked) { + continue; + } + if (generator.matchesBiome(surface, null)) { + return true; + } + checked = surface; + } + } + return false; } private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) { @@ -190,15 +219,18 @@ public class IrisDepositModifier extends EngineAssignedModifier clumps = new ArrayList<>(limit - first); + int terrainLimit = Integer.MIN_VALUE; for (int l = first; l < limit; l++) { - RNG clumpRng = new RNG(clumpSeed(plan.seed(), l)); + long clumpSeed = clumpSeed(plan.seed(), l); + RNG clumpRng = new RNG(clumpSeed); if (k.getPerClumpSpawnChance() < clumpRng.d()) { continue; } - IrisObject clump = k.getClump(getEngine(), clumpRng, getData()); + IrisDepositGenerator.ClumpFootprint footprint = k.sampleClumpFootprint(clumpRng, getData()); + IrisObject clump = footprint == null ? k.getClump(getEngine(), clumpRng, getData()) : null; - int dim = clump.getW(); + int dim = footprint == null ? clump.getW() : footprint.width(); int min = dim / 2; int max = (int) (16D - dim / 2D); @@ -262,6 +294,20 @@ public class IrisDepositModifier extends EngineAssignedModifier terrainLimit) { + continue; + } + } + RNG replay = new RNG(clumpSeed); + replay.d(); + clump = k.getClump(getEngine(), replay, getData()); + } + clumps.add(new PreparedClump(clump, x, y, z, clumpRng)); } return new PreparedDeposit(k, oreDeposit, clumps); @@ -350,6 +396,20 @@ public class IrisDepositModifier extends EngineAssignedModifier.Cursor cursor = clump.getBlocks().cursor(); + while (cursor.next()) { + minimumY = Math.min(minimumY, cursor.key().getBlockY()); + } + assertEquals(minimumY, footprint.minY()); + } + assertEquals(built.nextLong(), sampled.nextLong()); + } + } + generator.setMinSize(65).setMaxSize(65); + assertNull(generator.sampleClumpFootprint(new RNG(1L), data)); + generator.setShape(IrisDepositShape.VANILLA_SCATTERED).setMinSize(4).setMaxSize(4); + assertNull(generator.sampleClumpFootprint(new RNG(1L), data)); + } + @Test public void vanillaScatteredSizeIsAnUpperCandidateBound() { IrisData data = mock(IrisData.class); 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 40fe303d1..45c47dede 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 @@ -5,12 +5,14 @@ import art.arcane.iris.testsupport.KeyedBlockState; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.generation.runtime.Engine; 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.IrisDepositShape; import art.arcane.iris.generation.decoration.IrisDepositVariant; import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.iris.structure.object.IrisObject; import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; @@ -25,6 +27,7 @@ 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.matter.MatterCavern; +import art.arcane.volmlib.util.math.RNG; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; @@ -99,13 +102,26 @@ public class IrisDepositModifierParityTest { } } + @Test + public void unmatchableSurfaceBiomeFiltersSkipClumpsWithoutChangingOutput() { + long[] expected = {-4609692219192155345L, 784L, 2118L, 53L, 67L, 11L}; + AtomicInteger clumps = new AtomicInteger(); + assertArrayEquals(expected, generateConfigured(101L, false, true, false, new AtomicInteger(), clumps)); + assertEquals(0, clumps.get()); + } + private long[] generateConfigured(long seed, boolean multicore) { return generateConfigured(seed, multicore, true, false, new AtomicInteger()); } - @SuppressWarnings("unchecked") private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack, AtomicInteger caveBiomeLookups) { + return generateConfigured(seed, multicore, caveVariant, enumerablePack, caveBiomeLookups, null); + } + + @SuppressWarnings("unchecked") + private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack, + AtomicInteger caveBiomeLookups, AtomicInteger unmatchableClumps) { Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS); IrisDimension dimension = new IrisDimension(); @@ -136,6 +152,16 @@ public class IrisDepositModifierParityTest { gold.setExcludedBiomes(new KList<>("allowed")); gold.setDiscardChanceOnAirExposure(0.3D); surface.getDeposits().add(gold); + if (unmatchableClumps != null) { + IrisDepositGenerator unmatchable = new CountingPaletteGenerator(state("emerald_ore"), unmatchableClumps) + .setShape(IrisDepositShape.VANILLA_ELLIPSOID).setMinSize(9).setMaxSize(9) + .setMinPerChunk(90).setMaxPerChunk(90).setMinHeight(-4).setMaxHeight(44) + .setHeightDistribution(IrisDepositHeightDistribution.UNIFORM).setSurfaceClearance(0) + .setReplaceableBlocks(new KList<>("stone")) + .setBiomeScope(IrisDepositBiomeScope.SURFACE) + .setIncludedBiomes(new KList<>("elsewhere")); + surface.getDeposits().add(unmatchable); + } when(engine.getHeight()).thenReturn(48); when(engine.getMinHeight()).thenReturn(-64); when(engine.getSeedManager().getDeposit()).thenReturn(seed); @@ -404,7 +430,7 @@ public class IrisDepositModifierParityTest { } } - private static final class PaletteGenerator extends IrisDepositGenerator { + private static class PaletteGenerator extends IrisDepositGenerator { private final KList blocks; private PaletteGenerator(NativeBlockState block) { @@ -416,4 +442,25 @@ public class IrisDepositModifierParityTest { return blocks; } } + + private static final class CountingPaletteGenerator extends PaletteGenerator { + private final AtomicInteger clumps; + + private CountingPaletteGenerator(NativeBlockState block, AtomicInteger clumps) { + super(block); + this.clumps = clumps; + } + + @Override + public IrisObject getClump(Engine engine, RNG rng, IrisData data) { + clumps.incrementAndGet(); + return super.getClump(engine, rng, data); + } + + @Override + public ClumpFootprint sampleClumpFootprint(RNG rng, IrisData data) { + clumps.incrementAndGet(); + return super.sampleClumpFootprint(rng, data); + } + } }