diff --git a/core/build.gradle b/core/build.gradle index 47b946d91..0ea5d3099 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -112,6 +112,11 @@ dependencies { // Script Engine slim(libs.kotlin.stdlib) slim(libs.kotlin.coroutines) + + testImplementation('junit:junit:4.13.2') + testImplementation('org.mockito:mockito-core:5.16.1') + testImplementation(libs.spigot) + testRuntimeOnly(libs.spigot) } java { diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index 307b9522e..a37322bdf 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -152,11 +152,21 @@ public class IrisSettings { public boolean useVirtualThreads = false; public boolean useTicketQueue = true; public int maxConcurrency = 256; + public int chunkLoadTimeoutSeconds = 15; + public int timeoutWarnIntervalMs = 500; public boolean startupNoisemapPrebake = true; public boolean enablePregenPerformanceProfile = true; public int pregenProfileNoiseCacheSize = 4_096; public boolean pregenProfileEnableFastCache = true; public boolean pregenProfileLogJvmHints = true; + + public int getChunkLoadTimeoutSeconds() { + return Math.max(5, Math.min(chunkLoadTimeoutSeconds, 120)); + } + + public int getTimeoutWarnIntervalMs() { + return Math.max(timeoutWarnIntervalMs, 250); + } } @Data diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java index a01c2dcc2..4a045396d 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -45,13 +45,23 @@ import java.util.concurrent.atomic.AtomicLong; public class AsyncPregenMethod implements PregeneratorMethod { private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); private static final int FOLIA_MAX_CONCURRENCY = 32; - private static final long CHUNK_LOAD_TIMEOUT_SECONDS = 15L; + private static final int NON_FOLIA_MAX_CONCURRENCY = 96; + private static final int NON_FOLIA_CONCURRENCY_FACTOR = 2; + private static final int ADAPTIVE_TIMEOUT_STEP = 3; private final World world; private final Executor executor; private final Semaphore semaphore; private final int threads; + private final int timeoutSeconds; + private final int timeoutWarnIntervalMs; private final boolean urgent; private final Map lastUse; + private final AtomicInteger adaptiveInFlightLimit; + private final int adaptiveMinInFlightLimit; + private final AtomicInteger timeoutStreak = new AtomicInteger(); + private final AtomicLong lastTimeoutLogAt = new AtomicLong(0L); + private final AtomicInteger suppressedTimeoutLogs = new AtomicInteger(); + private final AtomicLong lastAdaptiveLogAt = new AtomicLong(0L); private final AtomicInteger inFlight = new AtomicInteger(); private final AtomicLong submitted = new AtomicLong(); private final AtomicLong completed = new AtomicLong(); @@ -71,14 +81,21 @@ public class AsyncPregenMethod implements PregeneratorMethod { boolean useTicketQueue = IrisSettings.get().getPregen().isUseTicketQueue(); this.executor = useTicketQueue ? new TicketExecutor() : new ServiceExecutor(); } - int configuredThreads = IrisSettings.get().getPregen().getMaxConcurrency(); + IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); + int configuredThreads = pregen.getMaxConcurrency(); if (J.isFolia()) { configuredThreads = Math.min(configuredThreads, FOLIA_MAX_CONCURRENCY); + } else { + configuredThreads = Math.min(configuredThreads, resolveNonFoliaConcurrencyCap()); } this.threads = Math.max(1, configuredThreads); this.semaphore = new Semaphore(this.threads, true); + this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds(); + this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs(); this.urgent = IrisSettings.get().getPregen().useHighPriority; this.lastUse = new ConcurrentHashMap<>(); + this.adaptiveInFlightLimit = new AtomicInteger(this.threads); + this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4))); } private void unloadAndSaveAllChunks() { @@ -121,7 +138,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { } if (root instanceof java.util.concurrent.TimeoutException) { - Iris.warn("Timed out async pregen chunk load at " + x + "," + z + " after " + CHUNK_LOAD_TIMEOUT_SECONDS + "s. " + metricsSnapshot()); + onTimeout(x, z); } else { Iris.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot()); } @@ -130,10 +147,93 @@ public class AsyncPregenMethod implements PregeneratorMethod { return null; } + private void onTimeout(int x, int z) { + int streak = timeoutStreak.incrementAndGet(); + if (streak % ADAPTIVE_TIMEOUT_STEP == 0) { + lowerAdaptiveInFlightLimit(); + } + + long now = M.ms(); + long last = lastTimeoutLogAt.get(); + if (now - last < timeoutWarnIntervalMs || !lastTimeoutLogAt.compareAndSet(last, now)) { + suppressedTimeoutLogs.incrementAndGet(); + return; + } + + int suppressed = suppressedTimeoutLogs.getAndSet(0); + String suppressedText = suppressed <= 0 ? "" : " suppressed=" + suppressed; + Iris.warn("Timed out async pregen chunk load at " + x + "," + z + + " after " + timeoutSeconds + "s." + + " adaptiveLimit=" + adaptiveInFlightLimit.get() + + suppressedText + " " + metricsSnapshot()); + } + + private void onSuccess() { + int streak = timeoutStreak.get(); + if (streak > 0) { + timeoutStreak.compareAndSet(streak, streak - 1); + return; + } + + if ((completed.get() & 31L) == 0L) { + raiseAdaptiveInFlightLimit(); + } + } + + private void lowerAdaptiveInFlightLimit() { + while (true) { + int current = adaptiveInFlightLimit.get(); + if (current <= adaptiveMinInFlightLimit) { + return; + } + + int next = Math.max(adaptiveMinInFlightLimit, current - 1); + if (adaptiveInFlightLimit.compareAndSet(current, next)) { + logAdaptiveLimit("decrease", next); + return; + } + } + } + + private void raiseAdaptiveInFlightLimit() { + while (true) { + int current = adaptiveInFlightLimit.get(); + if (current >= threads) { + return; + } + + int next = Math.min(threads, current + 1); + if (adaptiveInFlightLimit.compareAndSet(current, next)) { + logAdaptiveLimit("increase", next); + return; + } + } + } + + private void logAdaptiveLimit(String mode, int value) { + long now = M.ms(); + long last = lastAdaptiveLogAt.get(); + if (now - last < 5000L) { + return; + } + + if (lastAdaptiveLogAt.compareAndSet(last, now)) { + Iris.info("Async pregen adaptive limit " + mode + " -> " + value + " " + metricsSnapshot()); + } + } + + private int resolveNonFoliaConcurrencyCap() { + int worldGenThreads = Math.max(1, IrisSettings.get().getConcurrency().getWorldGenThreads()); + int recommended = worldGenThreads * NON_FOLIA_CONCURRENCY_FACTOR; + int bounded = Math.max(8, Math.min(NON_FOLIA_MAX_CONCURRENCY, recommended)); + return bounded; + } + private String metricsSnapshot() { long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get()); return "world=" + world.getName() + " permits=" + semaphore.availablePermits() + "/" + threads + + " adaptiveLimit=" + adaptiveInFlightLimit.get() + " inFlight=" + inFlight.get() + " submitted=" + submitted.get() + " completed=" + completed.get() @@ -149,6 +249,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { private void markFinished(boolean success) { if (success) { completed.incrementAndGet(); + onSuccess(); } else { failed.incrementAndGet(); } @@ -177,8 +278,9 @@ public class AsyncPregenMethod implements PregeneratorMethod { Iris.info("Async pregen init: world=" + world.getName() + ", mode=" + (J.isFolia() ? "folia" : "paper") + ", threads=" + threads + + ", adaptiveLimit=" + adaptiveInFlightLimit.get() + ", urgent=" + urgent - + ", timeout=" + CHUNK_LOAD_TIMEOUT_SECONDS + "s"); + + ", timeout=" + timeoutSeconds + "s"); unloadAndSaveAllChunks(); increaseWorkerThreads(); } @@ -216,6 +318,14 @@ public class AsyncPregenMethod implements PregeneratorMethod { listener.onChunkGenerating(x, z); try { long waitStart = M.ms(); + while (inFlight.get() >= adaptiveInFlightLimit.get()) { + long waited = Math.max(0L, M.ms() - waitStart); + logPermitWaitIfNeeded(x, z, waited); + if (!J.sleep(5)) { + return; + } + } + while (!semaphore.tryAcquire(5, TimeUnit.SECONDS)) { logPermitWaitIfNeeded(x, z, Math.max(0L, M.ms() - waitStart)); } @@ -288,7 +398,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { @Override public void generate(int x, int z, PregenListener listener) { if (!J.runRegion(world, x, z, () -> PaperLib.getChunkAtAsync(world, x, z, true, urgent) - .orTimeout(CHUNK_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .orTimeout(timeoutSeconds, TimeUnit.SECONDS) .whenComplete((chunk, throwable) -> { boolean success = false; try { @@ -328,15 +438,16 @@ public class AsyncPregenMethod implements PregeneratorMethod { boolean success = false; try { Chunk i = PaperLib.getChunkAtAsync(world, x, z, true, urgent) - .orTimeout(CHUNK_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .orTimeout(timeoutSeconds, TimeUnit.SECONDS) .exceptionally(e -> onChunkFutureFailure(x, z, e)) .get(); - listener.onChunkGenerated(x, z); - listener.onChunkCleaned(x, z); if (i == null) { return; } + + listener.onChunkGenerated(x, z); + listener.onChunkCleaned(x, z); lastUse.put(i, M.ms()); success = true; } catch (InterruptedException ignored) { @@ -361,16 +472,18 @@ public class AsyncPregenMethod implements PregeneratorMethod { @Override public void generate(int x, int z, PregenListener listener) { PaperLib.getChunkAtAsync(world, x, z, true, urgent) - .orTimeout(CHUNK_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .orTimeout(timeoutSeconds, TimeUnit.SECONDS) .exceptionally(e -> onChunkFutureFailure(x, z, e)) .thenAccept(i -> { boolean success = false; try { + if (i == null) { + return; + } + listener.onChunkGenerated(x, z); listener.onChunkCleaned(x, z); - if (i != null) { - lastUse.put(i, M.ms()); - } + lastUse.put(i, M.ms()); success = true; } finally { markFinished(success); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index e118be386..3c6e51301 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -236,12 +236,17 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat @BlockCoordinates default IrisBiome getCaveBiome(int x, int y, int z) { + return getCaveBiome(x, y, z, null); + } + + @BlockCoordinates + default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) { IrisBiome surfaceBiome = getSurfaceBiome(x, z); int worldY = y + getWorld().minHeight(); - IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY); + IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state); if (rootCarvingEntry != null) { - IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(this, rootCarvingEntry, x, z); - IrisBiome resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(this, resolvedCarvingEntry); + IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(this, rootCarvingEntry, x, z, state); + IrisBiome resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(this, resolvedCarvingEntry, state); if (resolvedCarvingBiome != null) { return resolvedCarvingBiome; } @@ -321,70 +326,85 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat var chunk = mantle.getChunk(c).use(); try { + Runnable tileTask = () -> { + chunk.iterate(TileWrapper.class, (x, y, z, v) -> { + Block block = c.getBlock(x & 15, y + getWorld().minHeight(), z & 15); + if (!TileData.setTileState(block, v.getData())) { + NamespacedKey blockTypeKey = KeyedType.getKey(block.getType()); + NamespacedKey tileTypeKey = KeyedType.getKey(v.getData().getMaterial()); + String blockType = blockTypeKey == null ? block.getType().name() : blockTypeKey.toString(); + String tileType = tileTypeKey == null ? v.getData().getMaterial().name() : tileTypeKey.toString(); + Iris.warn("Failed to set tile entity data at [%d %d %d | %s] for tile %s!", block.getX(), block.getY(), block.getZ(), blockType, tileType); + } + }); + }; + + Runnable customTask = () -> { + chunk.iterate(Identifier.class, (x, y, z, v) -> { + Iris.service(ExternalDataSVC.class).processUpdate(this, c.getBlock(x & 15, y + getWorld().minHeight(), z & 15), v); + }); + }; + + Runnable updateTask = () -> { + PrecisionStopwatch p = PrecisionStopwatch.start(); + int[][] grid = new int[16][16]; + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + grid[x][z] = Integer.MIN_VALUE; + } + } + + RNG rng = new RNG(Cache.key(c.getX(), c.getZ())); + chunk.iterate(MatterCavern.class, (x, yf, z, v) -> { + int y = yf + getWorld().minHeight(); + x &= 15; + z &= 15; + Block block = c.getBlock(x, y, z); + if (!B.isFluid(block.getBlockData())) { + return; + } + boolean u = B.isAir(block.getRelative(BlockFace.DOWN).getBlockData()) + || B.isAir(block.getRelative(BlockFace.WEST).getBlockData()) + || B.isAir(block.getRelative(BlockFace.EAST).getBlockData()) + || B.isAir(block.getRelative(BlockFace.SOUTH).getBlockData()) + || B.isAir(block.getRelative(BlockFace.NORTH).getBlockData()); + + if (u) grid[x][z] = Math.max(grid[x][z], y); + }); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + if (grid[x][z] == Integer.MIN_VALUE) { + continue; + } + update(x, grid[x][z], z, c, chunk, rng); + } + } + + chunk.iterate(MatterUpdate.class, (x, yf, z, v) -> { + int y = yf + getWorld().minHeight(); + if (v != null && v.isUpdate()) { + update(x, y, z, c, chunk, rng); + } + }); + chunk.deleteSlices(MatterUpdate.class); + getMetrics().getUpdates().put(p.getMilliseconds()); + }; + + if (shouldRunChunkUpdateInline(c)) { + chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> { + chunk.raiseFlagUnchecked(MantleFlag.TILE, tileTask); + chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, customTask); + chunk.raiseFlagUnchecked(MantleFlag.UPDATE, updateTask); + }); + return; + } + Semaphore semaphore = new Semaphore(1024); chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> { - chunk.raiseFlagUnchecked(MantleFlag.TILE, run(semaphore, c, () -> { - chunk.iterate(TileWrapper.class, (x, y, z, v) -> { - Block block = c.getBlock(x & 15, y + getWorld().minHeight(), z & 15); - if (!TileData.setTileState(block, v.getData())) { - NamespacedKey blockTypeKey = KeyedType.getKey(block.getType()); - NamespacedKey tileTypeKey = KeyedType.getKey(v.getData().getMaterial()); - String blockType = blockTypeKey == null ? block.getType().name() : blockTypeKey.toString(); - String tileType = tileTypeKey == null ? v.getData().getMaterial().name() : tileTypeKey.toString(); - Iris.warn("Failed to set tile entity data at [%d %d %d | %s] for tile %s!", block.getX(), block.getY(), block.getZ(), blockType, tileType); - } - }); - }, 0)); - chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, run(semaphore, c, () -> { - chunk.iterate(Identifier.class, (x, y, z, v) -> { - Iris.service(ExternalDataSVC.class).processUpdate(this, c.getBlock(x & 15, y + getWorld().minHeight(), z & 15), v); - }); - }, 0)); - - chunk.raiseFlagUnchecked(MantleFlag.UPDATE, run(semaphore, c, () -> { - PrecisionStopwatch p = PrecisionStopwatch.start(); - int[][] grid = new int[16][16]; - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - grid[x][z] = Integer.MIN_VALUE; - } - } - - RNG rng = new RNG(Cache.key(c.getX(), c.getZ())); - chunk.iterate(MatterCavern.class, (x, yf, z, v) -> { - int y = yf + getWorld().minHeight(); - x &= 15; - z &= 15; - Block block = c.getBlock(x, y, z); - if (!B.isFluid(block.getBlockData())) { - return; - } - boolean u = B.isAir(block.getRelative(BlockFace.DOWN).getBlockData()) - || B.isAir(block.getRelative(BlockFace.WEST).getBlockData()) - || B.isAir(block.getRelative(BlockFace.EAST).getBlockData()) - || B.isAir(block.getRelative(BlockFace.SOUTH).getBlockData()) - || B.isAir(block.getRelative(BlockFace.NORTH).getBlockData()); - - if (u) grid[x][z] = Math.max(grid[x][z], y); - }); - - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - if (grid[x][z] == Integer.MIN_VALUE) - continue; - update(x, grid[x][z], z, c, chunk, rng); - } - } - - chunk.iterate(MatterUpdate.class, (x, yf, z, v) -> { - int y = yf + getWorld().minHeight(); - if (v != null && v.isUpdate()) { - update(x, y, z, c, chunk, rng); - } - }); - chunk.deleteSlices(MatterUpdate.class); - getMetrics().getUpdates().put(p.getMilliseconds()); - }, RNG.r.i(1, 20))); //Why is there a random delay here? + chunk.raiseFlagUnchecked(MantleFlag.TILE, run(semaphore, c, tileTask, 0)); + chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, run(semaphore, c, customTask, 0)); + chunk.raiseFlagUnchecked(MantleFlag.UPDATE, run(semaphore, c, updateTask, RNG.r.i(1, 20))); }); try { @@ -398,6 +418,18 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat } } + private static boolean shouldRunChunkUpdateInline(Chunk chunk) { + if (chunk == null) { + return false; + } + + if (!J.isFolia()) { + return J.isPrimaryThread(); + } + + return J.isOwnedByCurrentRegion(chunk.getWorld(), chunk.getX(), chunk.getZ()); + } + private static Runnable run(Semaphore semaphore, Chunk contextChunk, Runnable runnable, int delay) { return () -> { try { @@ -407,13 +439,23 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat } int effectiveDelay = J.isFolia() ? 0 : delay; - J.runRegion(contextChunk.getWorld(), contextChunk.getX(), contextChunk.getZ(), () -> { + boolean scheduled = J.runRegion(contextChunk.getWorld(), contextChunk.getX(), contextChunk.getZ(), () -> { try { runnable.run(); } finally { semaphore.release(); } }, effectiveDelay); + + if (!scheduled) { + try { + if (J.isPrimaryThread()) { + runnable.run(); + } + } finally { + semaphore.release(); + } + } }; } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java index 73edc706a..294edfff7 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java @@ -37,6 +37,8 @@ public class EngineMetrics { private final AtomicRollingSequence cave; private final AtomicRollingSequence ravine; private final AtomicRollingSequence deposit; + private final AtomicRollingSequence carveResolve; + private final AtomicRollingSequence carveApply; public EngineMetrics(int mem) { this.total = new AtomicRollingSequence(mem); @@ -52,6 +54,8 @@ public class EngineMetrics { this.cave = new AtomicRollingSequence(mem); this.ravine = new AtomicRollingSequence(mem); this.deposit = new AtomicRollingSequence(mem); + this.carveResolve = new AtomicRollingSequence(mem); + this.carveApply = new AtomicRollingSequence(mem); } public KMap pull() { @@ -69,6 +73,8 @@ public class EngineMetrics { v.put("cave", cave.getAverage()); v.put("ravine", ravine.getAverage()); v.put("deposit", deposit.getAverage()); + v.put("carve.resolve", carveResolve.getAverage()); + v.put("carve.apply", carveApply.getAverage()); return v; } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index 78dd0d116..1c3466b69 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -28,6 +28,7 @@ import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.util.Arrays; @@ -35,6 +36,7 @@ public class IrisCaveCarver3D { private static final byte LIQUID_AIR = 0; private static final byte LIQUID_LAVA = 2; private static final byte LIQUID_FORCED_AIR = 3; + private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(Scratch::new); private final Engine engine; private final IrisData data; @@ -49,6 +51,11 @@ public class IrisCaveCarver3D { private final MatterCavern carveAir; private final MatterCavern carveLava; private final MatterCavern carveForcedAir; + private final double baseWeight; + private final double detailWeight; + private final double warpStrength; + private final boolean hasWarp; + private final boolean hasModules; public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) { this.engine = engine; @@ -65,8 +72,12 @@ public class IrisCaveCarver3D { this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data); this.surfaceBreakDensity = profile.getSurfaceBreakStyle().create(baseRng.nextParallelRNG(341_219), data); this.thresholdRng = baseRng.nextParallelRNG(489_112); + this.baseWeight = profile.getBaseWeight(); + this.detailWeight = profile.getDetailWeight(); + this.warpStrength = profile.getWarpStrength(); + this.hasWarp = this.warpStrength > 0D; - double weight = Math.abs(profile.getBaseWeight()) + Math.abs(profile.getDetailWeight()); + double weight = Math.abs(baseWeight) + Math.abs(detailWeight); int index = 0; for (IrisCaveFieldModule module : profile.getModules()) { CNG moduleDensity = module.getStyle().create(baseRng.nextParallelRNG(1_000_003L + (index * 65_537L)), data); @@ -77,12 +88,16 @@ public class IrisCaveCarver3D { } normalization = weight <= 0 ? 1 : weight; + hasModules = !modules.isEmpty(); } public int carve(MantleWriter writer, int chunkX, int chunkZ) { - double[] fullWeights = new double[256]; - Arrays.fill(fullWeights, 1D); - return carve(writer, chunkX, chunkZ, fullWeights, 0D, 0D, null); + Scratch scratch = SCRATCH.get(); + if (!scratch.fullWeightsInitialized) { + Arrays.fill(scratch.fullWeights, 1D); + scratch.fullWeightsInitialized = true; + } + return carve(writer, chunkX, chunkZ, scratch.fullWeights, 0D, 0D, null); } public int carve( @@ -105,91 +120,71 @@ public class IrisCaveCarver3D { double thresholdPenalty, IrisRange worldYRange ) { - if (columnWeights == null || columnWeights.length < 256) { - double[] fullWeights = new double[256]; - Arrays.fill(fullWeights, 1D); - columnWeights = fullWeights; - } - - double resolvedMinWeight = Math.max(0D, Math.min(1D, minWeight)); - double resolvedThresholdPenalty = Math.max(0D, thresholdPenalty); - int worldHeight = writer.getMantle().getWorldHeight(); - int minY = Math.max(0, (int) Math.floor(profile.getVerticalRange().getMin())); - int maxY = Math.min(worldHeight - 1, (int) Math.ceil(profile.getVerticalRange().getMax())); - if (worldYRange != null) { - int worldMinHeight = engine.getWorld().minHeight(); - int rangeMinY = (int) Math.floor(worldYRange.getMin() - worldMinHeight); - int rangeMaxY = (int) Math.ceil(worldYRange.getMax() - worldMinHeight); - minY = Math.max(minY, rangeMinY); - maxY = Math.min(maxY, rangeMaxY); - } - int sampleStep = Math.max(1, profile.getSampleStep()); - int surfaceClearance = Math.max(0, profile.getSurfaceClearance()); - int surfaceBreakDepth = Math.max(0, profile.getSurfaceBreakDepth()); - double surfaceBreakNoiseThreshold = profile.getSurfaceBreakNoiseThreshold(); - double surfaceBreakThresholdBoost = Math.max(0, profile.getSurfaceBreakThresholdBoost()); - int waterMinDepthBelowSurface = Math.max(0, profile.getWaterMinDepthBelowSurface()); - boolean waterRequiresFloor = profile.isWaterRequiresFloor(); - boolean allowSurfaceBreak = profile.isAllowSurfaceBreak(); - if (maxY < minY) { - return 0; - } - - int x0 = chunkX << 4; - int z0 = chunkZ << 4; - int[] columnSurface = new int[256]; - int[] columnMaxY = new int[256]; - int[] surfaceBreakFloorY = new int[256]; - boolean[] surfaceBreakColumn = new boolean[256]; - double[] columnThreshold = new double[256]; - - for (int lx = 0; lx < 16; lx++) { - int x = x0 + lx; - for (int lz = 0; lz < 16; lz++) { - int z = z0 + lz; - int index = (lx << 4) | lz; - int columnSurfaceY = engine.getHeight(x, z); - int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurfaceY - surfaceClearance)); - boolean breakColumn = allowSurfaceBreak - && signed(surfaceBreakDensity.noise(x, z)) >= surfaceBreakNoiseThreshold; - int columnTopY = breakColumn - ? Math.min(maxY, Math.max(minY, columnSurfaceY)) - : clearanceTopY; - - columnSurface[index] = columnSurfaceY; - columnMaxY[index] = columnTopY; - surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth); - surfaceBreakColumn[index] = breakColumn; - columnThreshold[index] = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias(); + PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start(); + try { + Scratch scratch = SCRATCH.get(); + if (columnWeights == null || columnWeights.length < 256) { + if (!scratch.fullWeightsInitialized) { + Arrays.fill(scratch.fullWeights, 1D); + scratch.fullWeightsInitialized = true; + } + columnWeights = scratch.fullWeights; } - } - int carved = carvePass( - writer, - x0, - z0, - minY, - maxY, - sampleStep, - surfaceBreakThresholdBoost, - waterMinDepthBelowSurface, - waterRequiresFloor, - columnSurface, - columnMaxY, - surfaceBreakFloorY, - surfaceBreakColumn, - columnThreshold, - columnWeights, - resolvedMinWeight, - resolvedThresholdPenalty, - 0D, - false - ); + double resolvedMinWeight = Math.max(0D, Math.min(1D, minWeight)); + double resolvedThresholdPenalty = Math.max(0D, thresholdPenalty); + int worldHeight = writer.getMantle().getWorldHeight(); + int minY = Math.max(0, (int) Math.floor(profile.getVerticalRange().getMin())); + int maxY = Math.min(worldHeight - 1, (int) Math.ceil(profile.getVerticalRange().getMax())); + if (worldYRange != null) { + int worldMinHeight = engine.getWorld().minHeight(); + int rangeMinY = (int) Math.floor(worldYRange.getMin() - worldMinHeight); + int rangeMaxY = (int) Math.ceil(worldYRange.getMax() - worldMinHeight); + minY = Math.max(minY, rangeMinY); + maxY = Math.min(maxY, rangeMaxY); + } + int sampleStep = Math.max(1, profile.getSampleStep()); + int surfaceClearance = Math.max(0, profile.getSurfaceClearance()); + int surfaceBreakDepth = Math.max(0, profile.getSurfaceBreakDepth()); + double surfaceBreakNoiseThreshold = profile.getSurfaceBreakNoiseThreshold(); + double surfaceBreakThresholdBoost = Math.max(0, profile.getSurfaceBreakThresholdBoost()); + int waterMinDepthBelowSurface = Math.max(0, profile.getWaterMinDepthBelowSurface()); + boolean waterRequiresFloor = profile.isWaterRequiresFloor(); + boolean allowSurfaceBreak = profile.isAllowSurfaceBreak(); + if (maxY < minY) { + return 0; + } - int minCarveCells = Math.max(0, profile.getMinCarveCells()); - double recoveryThresholdBoost = Math.max(0, profile.getRecoveryThresholdBoost()); - if (carved < minCarveCells && recoveryThresholdBoost > 0D) { - carved += carvePass( + int x0 = chunkX << 4; + int z0 = chunkZ << 4; + int[] columnSurface = scratch.columnSurface; + int[] columnMaxY = scratch.columnMaxY; + int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY; + boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn; + double[] columnThreshold = scratch.columnThreshold; + + for (int lx = 0; lx < 16; lx++) { + int x = x0 + lx; + for (int lz = 0; lz < 16; lz++) { + int z = z0 + lz; + int index = (lx << 4) | lz; + int columnSurfaceY = engine.getHeight(x, z); + int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurfaceY - surfaceClearance)); + boolean breakColumn = allowSurfaceBreak + && signed(surfaceBreakDensity.noise(x, z)) >= surfaceBreakNoiseThreshold; + int columnTopY = breakColumn + ? Math.min(maxY, Math.max(minY, columnSurfaceY)) + : clearanceTopY; + + columnSurface[index] = columnSurfaceY; + columnMaxY[index] = columnTopY; + surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth); + surfaceBreakColumn[index] = breakColumn; + columnThreshold[index] = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias(); + } + } + + int carved = carvePass( writer, x0, z0, @@ -207,12 +202,40 @@ public class IrisCaveCarver3D { columnWeights, resolvedMinWeight, resolvedThresholdPenalty, - recoveryThresholdBoost, - true + 0D, + false ); - } - return carved; + int minCarveCells = Math.max(0, profile.getMinCarveCells()); + double recoveryThresholdBoost = Math.max(0, profile.getRecoveryThresholdBoost()); + if (carved < minCarveCells && recoveryThresholdBoost > 0D) { + carved += carvePass( + writer, + x0, + z0, + minY, + maxY, + sampleStep, + surfaceBreakThresholdBoost, + waterMinDepthBelowSurface, + waterRequiresFloor, + columnSurface, + columnMaxY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + columnWeights, + resolvedMinWeight, + resolvedThresholdPenalty, + recoveryThresholdBoost, + true + ); + } + + return carved; + } finally { + engine.getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds()); + } } private int carvePass( @@ -305,12 +328,16 @@ public class IrisCaveCarver3D { } private double sampleDensity(int x, int y, int z) { + if (!hasWarp && !hasModules) { + double density = signed(baseDensity.noise(x, y, z)) * baseWeight; + density += signed(detailDensity.noise(x, y, z)) * detailWeight; + return density / normalization; + } + double warpedX = x; double warpedY = y; double warpedZ = z; - double warpStrength = profile.getWarpStrength(); - - if (warpStrength > 0) { + if (hasWarp) { double warpA = signed(warpDensity.noise(x, y, z)); double warpB = signed(warpDensity.noise(x + 31.37D, y - 17.21D, z + 23.91D)); double offsetX = warpA * warpStrength; @@ -321,20 +348,22 @@ public class IrisCaveCarver3D { warpedZ += offsetZ; } - double density = signed(baseDensity.noise(warpedX, warpedY, warpedZ)) * profile.getBaseWeight(); - density += signed(detailDensity.noise(warpedX, warpedY, warpedZ)) * profile.getDetailWeight(); + double density = signed(baseDensity.noise(warpedX, warpedY, warpedZ)) * baseWeight; + density += signed(detailDensity.noise(warpedX, warpedY, warpedZ)) * detailWeight; - for (ModuleState module : modules) { - if (y < module.minY || y > module.maxY) { - continue; + if (hasModules) { + for (ModuleState module : modules) { + if (y < module.minY || y > module.maxY) { + continue; + } + + double moduleDensity = signed(module.density.noise(warpedX, warpedY, warpedZ)) - module.threshold; + if (module.invert) { + moduleDensity = -moduleDensity; + } + + density += moduleDensity * module.weight; } - - double moduleDensity = signed(module.density.noise(warpedX, warpedY, warpedZ)) - module.threshold; - if (module.invert) { - moduleDensity = -moduleDensity; - } - - density += moduleDensity * module.weight; } return density / normalization; @@ -397,4 +426,14 @@ public class IrisCaveCarver3D { this.invert = module.isInvert(); } } + + private static final class Scratch { + private final int[] columnSurface = new int[256]; + private final int[] columnMaxY = new int[256]; + private final int[] surfaceBreakFloorY = new int[256]; + private final boolean[] surfaceBreakColumn = new boolean[256]; + private final double[] columnThreshold = new double[256]; + private final double[] fullWeights = new double[256]; + private boolean fullWeightsInitialized; + } } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index b78e79788..3c058a731 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -31,9 +31,9 @@ import art.arcane.iris.engine.object.IrisRange; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.IdentityHashMap; import java.util.List; @@ -47,16 +47,37 @@ public class MantleCarvingComponent extends IrisMantleComponent { private static final int FIELD_SIZE = CHUNK_SIZE + (BLEND_RADIUS * 2); private static final double MIN_WEIGHT = 0.08D; private static final double THRESHOLD_PENALTY = 0.24D; + private static final int KERNEL_WIDTH = (BLEND_RADIUS * 2) + 1; + private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH; + private static final int[] KERNEL_DX = new int[KERNEL_SIZE]; + private static final int[] KERNEL_DZ = new int[KERNEL_SIZE]; + private static final double[] KERNEL_WEIGHT = new double[KERNEL_SIZE]; private final Map profileCarvers = new IdentityHashMap<>(); + static { + int kernelIndex = 0; + for (int offsetX = -BLEND_RADIUS; offsetX <= BLEND_RADIUS; offsetX++) { + for (int offsetZ = -BLEND_RADIUS; offsetZ <= BLEND_RADIUS; offsetZ++) { + KERNEL_DX[kernelIndex] = offsetX; + KERNEL_DZ[kernelIndex] = offsetZ; + int edgeDistance = Math.max(Math.abs(offsetX), Math.abs(offsetZ)); + KERNEL_WEIGHT[kernelIndex] = (BLEND_RADIUS + 1D) - edgeDistance; + kernelIndex++; + } + } + } + public MantleCarvingComponent(EngineMantle engineMantle) { super(engineMantle, ReservedFlag.CARVED, 0); } @Override public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { - List weightedProfiles = resolveWeightedProfiles(x, z); + IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); + List weightedProfiles = resolveWeightedProfiles(x, z, resolverState); + getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); for (WeightedProfile weightedProfile : weightedProfiles) { carveProfile(weightedProfile, writer, x, z); } @@ -68,17 +89,51 @@ public class MantleCarvingComponent extends IrisMantleComponent { carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange); } - private List resolveWeightedProfiles(int chunkX, int chunkZ) { - IrisCaveProfile[] profileField = buildProfileField(chunkX, chunkZ); + private List resolveWeightedProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState) { + IrisCaveProfile[] profileField = buildProfileField(chunkX, chunkZ, resolverState); Map profileWeights = new IdentityHashMap<>(); + IrisCaveProfile[] columnProfiles = new IrisCaveProfile[KERNEL_SIZE]; + double[] columnProfileWeights = new double[KERNEL_SIZE]; for (int localX = 0; localX < CHUNK_SIZE; localX++) { for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int profileCount = 0; int columnIndex = (localX << 4) | localZ; - Map columnInfluence = sampleColumnInfluence(profileField, localX, localZ); - for (Map.Entry entry : columnInfluence.entrySet()) { - double[] weights = profileWeights.computeIfAbsent(entry.getKey(), key -> new double[CHUNK_AREA]); - weights[columnIndex] = entry.getValue(); + int centerX = localX + BLEND_RADIUS; + int centerZ = localZ + BLEND_RADIUS; + double totalKernelWeight = 0D; + + for (int kernelIndex = 0; kernelIndex < KERNEL_SIZE; kernelIndex++) { + int sampleX = centerX + KERNEL_DX[kernelIndex]; + int sampleZ = centerZ + KERNEL_DZ[kernelIndex]; + IrisCaveProfile profile = profileField[(sampleX * FIELD_SIZE) + sampleZ]; + if (!isProfileEnabled(profile)) { + continue; + } + + double kernelWeight = KERNEL_WEIGHT[kernelIndex]; + int existingIndex = findProfileIndex(columnProfiles, profileCount, profile); + if (existingIndex >= 0) { + columnProfileWeights[existingIndex] += kernelWeight; + } else { + columnProfiles[profileCount] = profile; + columnProfileWeights[profileCount] = kernelWeight; + profileCount++; + } + totalKernelWeight += kernelWeight; + } + + if (totalKernelWeight <= 0D || profileCount == 0) { + continue; + } + + for (int profileIndex = 0; profileIndex < profileCount; profileIndex++) { + IrisCaveProfile profile = columnProfiles[profileIndex]; + double normalizedWeight = columnProfileWeights[profileIndex] / totalKernelWeight; + double[] weights = profileWeights.computeIfAbsent(profile, key -> new double[CHUNK_AREA]); + weights[columnIndex] = normalizedWeight; + columnProfiles[profileIndex] = null; + columnProfileWeights[profileIndex] = 0D; } } } @@ -106,11 +161,11 @@ public class MantleCarvingComponent extends IrisMantleComponent { } weightedProfiles.sort(Comparator.comparingDouble(WeightedProfile::averageWeight)); - weightedProfiles.addAll(0, resolveDimensionCarvingProfiles(chunkX, chunkZ)); + weightedProfiles.addAll(0, resolveDimensionCarvingProfiles(chunkX, chunkZ, resolverState)); return weightedProfiles; } - private List resolveDimensionCarvingProfiles(int chunkX, int chunkZ) { + private List resolveDimensionCarvingProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState) { List weightedProfiles = new ArrayList<>(); List entries = getDimension().getCarving(); if (entries == null || entries.isEmpty()) { @@ -122,7 +177,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } - IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), entry); + IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), entry, resolverState); if (rootBiome == null) { continue; } @@ -134,8 +189,8 @@ public class MantleCarvingComponent extends IrisMantleComponent { int worldX = (chunkX << 4) + localX; int worldZ = (chunkZ << 4) + localZ; int columnIndex = (localX << 4) | localZ; - IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ); - IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry); + IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState); + IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState); if (resolvedBiome == null) { continue; } @@ -160,40 +215,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { return weightedProfiles; } - private Map sampleColumnInfluence(IrisCaveProfile[] profileField, int localX, int localZ) { - Map profileBlend = new IdentityHashMap<>(); - int centerX = localX + BLEND_RADIUS; - int centerZ = localZ + BLEND_RADIUS; - double totalKernelWeight = 0D; - - for (int offsetX = -BLEND_RADIUS; offsetX <= BLEND_RADIUS; offsetX++) { - for (int offsetZ = -BLEND_RADIUS; offsetZ <= BLEND_RADIUS; offsetZ++) { - int sampleX = centerX + offsetX; - int sampleZ = centerZ + offsetZ; - IrisCaveProfile profile = profileField[(sampleX * FIELD_SIZE) + sampleZ]; - if (!isProfileEnabled(profile)) { - continue; - } - - double kernelWeight = haloWeight(offsetX, offsetZ); - profileBlend.merge(profile, kernelWeight, Double::sum); - totalKernelWeight += kernelWeight; - } - } - - if (totalKernelWeight <= 0D || profileBlend.isEmpty()) { - return Collections.emptyMap(); - } - - Map normalized = new IdentityHashMap<>(); - for (Map.Entry entry : profileBlend.entrySet()) { - normalized.put(entry.getKey(), entry.getValue() / totalKernelWeight); - } - - return normalized; - } - - private IrisCaveProfile[] buildProfileField(int chunkX, int chunkZ) { + private IrisCaveProfile[] buildProfileField(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState) { IrisCaveProfile[] profileField = new IrisCaveProfile[FIELD_SIZE * FIELD_SIZE]; int startX = (chunkX << 4) - BLEND_RADIUS; int startZ = (chunkZ << 4) - BLEND_RADIUS; @@ -202,19 +224,24 @@ public class MantleCarvingComponent extends IrisMantleComponent { int worldX = startX + fieldX; for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { int worldZ = startZ + fieldZ; - profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ); + profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ, resolverState); } } return profileField; } - private double haloWeight(int offsetX, int offsetZ) { - int edgeDistance = Math.max(Math.abs(offsetX), Math.abs(offsetZ)); - return (BLEND_RADIUS + 1D) - edgeDistance; + private int findProfileIndex(IrisCaveProfile[] profiles, int size, IrisCaveProfile profile) { + for (int index = 0; index < size; index++) { + if (profiles[index] == profile) { + return index; + } + } + + return -1; } - private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ) { + private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState) { IrisCaveProfile resolved = null; IrisCaveProfile dimensionProfile = getDimension().getCaveProfile(); if (isProfileEnabled(dimensionProfile)) { @@ -239,7 +266,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { int surfaceY = getEngineMantle().getEngine().getHeight(worldX, worldZ, true); int sampleY = Math.max(1, surfaceY - 56); - IrisBiome caveBiome = getEngineMantle().getEngine().getCaveBiome(worldX, sampleY, worldZ); + IrisBiome caveBiome = getEngineMantle().getEngine().getCaveBiome(worldX, sampleY, worldZ, resolverState); if (caveBiome != null) { IrisCaveProfile caveProfile = caveBiome.getCaveProfile(); if (isProfileEnabled(caveProfile)) { diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java index b580cfb82..a96737ed7 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -19,17 +19,18 @@ package art.arcane.iris.engine.modifier; import art.arcane.iris.engine.actuator.IrisDecorantActuator; -import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedModifier; -import art.arcane.iris.engine.object.*; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.engine.object.InferredType; +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.engine.object.IrisDimensionCarvingResolver; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.common.data.B; import art.arcane.volmlib.util.documentation.ChunkCoordinates; -import art.arcane.volmlib.util.function.Consumer4; import art.arcane.iris.util.project.hunk.Hunk; +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.math.M; @@ -42,6 +43,8 @@ import lombok.Data; import org.bukkit.Material; import org.bukkit.block.data.BlockData; +import java.util.Arrays; + public class IrisCarveModifier extends EngineAssignedModifier { private final RNG rng; private final BlockData AIR = Material.CAVE_AIR.createBlockData(); @@ -60,124 +63,135 @@ public class IrisCarveModifier extends EngineAssignedModifier { PrecisionStopwatch p = PrecisionStopwatch.start(); Mantle mantle = getEngine().getMantle().getMantle(); MantleChunk mc = mantle.getChunk(x, z).use(); - KMap> positions = new KMap<>(); - KMap walls = new KMap<>(); - Consumer4 iterator = (xx, yy, zz, c) -> { - if (c == null) { - return; - } + IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + int[][] columnHeights = new int[256][]; + int[] columnHeightSizes = new int[256]; + PackedWallBuffer walls = new PackedWallBuffer(512); + try { + PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); + mc.iterate(MatterCavern.class, (xx, yy, zz, c) -> { + if (c == null) { + return; + } - if (yy >= getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight() || yy <= 0) { // Yes, skip bedrock - return; - } + if (yy >= getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight() || yy <= 0) { + return; + } - int rx = xx & 15; - int rz = zz & 15; + int rx = xx & 15; + int rz = zz & 15; + int columnIndex = (rx << 4) | rz; + BlockData current = output.get(rx, yy, rz); - BlockData current = output.get(rx, yy, rz); + if (B.isFluid(current)) { + return; + } - if (B.isFluid(current)) { - return; - } + appendColumnHeight(columnHeights, columnHeightSizes, columnIndex, yy); - positions.computeIfAbsent(Cache.key(rx, rz), (k) -> new KList<>()).qadd(yy); + if (rz < 15 && mc.get(xx, yy, zz + 1, MatterCavern.class) == null) { + walls.put(rx, yy, rz + 1, c); + } - //todo: Fix chunk decoration not working on chunk's border + if (rx < 15 && mc.get(xx + 1, yy, zz, MatterCavern.class) == null) { + walls.put(rx + 1, yy, rz, c); + } - if (rz < 15 && mc.get(xx, yy, zz + 1, MatterCavern.class) == null) { - walls.put(new IrisPosition(rx, yy, rz + 1), c); - } + if (rz > 0 && mc.get(xx, yy, zz - 1, MatterCavern.class) == null) { + walls.put(rx, yy, rz - 1, c); + } - if (rx < 15 && mc.get(xx + 1, yy, zz, MatterCavern.class) == null) { - walls.put(new IrisPosition(rx + 1, yy, rz), c); - } + if (rx > 0 && mc.get(xx - 1, yy, zz, MatterCavern.class) == null) { + walls.put(rx - 1, yy, rz, c); + } - if (rz > 0 && mc.get(xx, yy, zz - 1, MatterCavern.class) == null) { - walls.put(new IrisPosition(rx, yy, rz - 1), c); - } + if (current.getMaterial().isAir()) { + return; + } - if (rx > 0 && mc.get(xx - 1, yy, zz, MatterCavern.class) == null) { - walls.put(new IrisPosition(rx - 1, yy, rz), c); - } - - if (current.getMaterial().isAir()) { - return; - } - - if (c.isWater()) { - output.set(rx, yy, rz, context.getFluid().get(rx, rz)); - } else if (c.isLava()) { - output.set(rx, yy, rz, LAVA); - } else if (c.getLiquid() == 3) { - output.set(rx, yy, rz, AIR); - } else { - if (getEngine().getDimension().getCaveLavaHeight() > yy) { + if (c.isWater()) { + output.set(rx, yy, rz, context.getFluid().get(rx, rz)); + } else if (c.isLava()) { + output.set(rx, yy, rz, LAVA); + } else if (c.getLiquid() == 3) { + output.set(rx, yy, rz, AIR); + } else if (getEngine().getDimension().getCaveLavaHeight() > yy) { output.set(rx, yy, rz, LAVA); } else { output.set(rx, yy, rz, AIR); } - } - }; + }); + getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); - mc.iterate(MatterCavern.class, iterator); + PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start(); + try { + walls.forEach((rx, yy, rz, cavern) -> { + int worldX = rx + (x << 4); + int worldZ = rz + (z << 4); + IrisBiome biome = cavern.getCustomBiome().isEmpty() + ? getEngine().getCaveBiome(worldX, yy, worldZ, resolverState) + : getEngine().getData().getBiomeLoader().load(cavern.getCustomBiome()); - walls.forEach((i, v) -> { - IrisBiome biome = v.getCustomBiome().isEmpty() - ? getEngine().getCaveBiome(i.getX() + (x << 4), i.getY(), i.getZ() + (z << 4)) - : getEngine().getData().getBiomeLoader().load(v.getCustomBiome()); + if (biome != null) { + biome.setInferredType(InferredType.CAVE); + BlockData data = biome.getWall().get(rng, worldX, yy, worldZ, getData()); - if (biome != null) { - biome.setInferredType(InferredType.CAVE); - BlockData d = biome.getWall().get(rng, i.getX() + (x << 4), i.getY(), i.getZ() + (z << 4), getData()); + if (data != null && B.isSolid(output.get(rx, yy, rz)) && yy <= context.getHeight().get(rx, rz)) { + output.set(rx, yy, rz, data); + } + } + }); - if (d != null && B.isSolid(output.get(i.getX(), i.getY(), i.getZ())) && i.getY() <= context.getHeight().get(i.getX(), i.getZ())) { - output.set(i.getX(), i.getY(), i.getZ(), d); + for (int columnIndex = 0; columnIndex < 256; columnIndex++) { + int size = columnHeightSizes[columnIndex]; + if (size <= 0) { + continue; + } + + int[] heights = columnHeights[columnIndex]; + Arrays.sort(heights, 0, size); + int rx = columnIndex >> 4; + int rz = columnIndex & 15; + CaveZone zone = new CaveZone(); + zone.setFloor(heights[0]); + int buf = heights[0] - 1; + + for (int heightIndex = 0; heightIndex < size; heightIndex++) { + int y = heights[heightIndex]; + if (y < 0 || y > getEngine().getHeight()) { + continue; + } + + if (y == buf + 1) { + buf = y; + zone.ceiling = buf; + } else if (zone.isValid(getEngine())) { + processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4), resolverState); + zone = new CaveZone(); + zone.setFloor(y); + buf = y; + } else { + zone = new CaveZone(); + zone.setFloor(y); + buf = y; + } + } + + if (zone.isValid(getEngine())) { + processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4), resolverState); + } } + } finally { + getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds()); } - }); - - positions.forEach((k, v) -> { - if (v.isEmpty()) { - return; - } - - int rx = Cache.keyX(k); - int rz = Cache.keyZ(k); - v.sort(Integer::compare); - CaveZone zone = new CaveZone(); - zone.setFloor(v.get(0)); - int buf = v.get(0) - 1; - - for (Integer i : v) { - if (i < 0 || i > getEngine().getHeight()) { - continue; - } - - if (i == buf + 1) { - buf = i; - zone.ceiling = buf; - } else if (zone.isValid(getEngine())) { - processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4)); - zone = new CaveZone(); - zone.setFloor(i); - buf = i; - } - } - - if (zone.isValid(getEngine())) { - processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4)); - } - }); - - getEngine().getMetrics().getDeposit().put(p.getMilliseconds()); - mc.release(); + } finally { + getEngine().getMetrics().getCave().put(p.getMilliseconds()); + mc.release(); + } } - private void processZone(Hunk output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz) { - boolean decFloor = B.isSolid(output.getClosest(rx, zone.floor - 1, rz)); - boolean decCeiling = B.isSolid(output.getClosest(rx, zone.ceiling + 1, rz)); + private void processZone(Hunk output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState) { int center = (zone.floor + zone.ceiling) / 2; - int thickness = zone.airThickness(); String customBiome = ""; if (B.isDecorant(output.getClosest(rx, zone.ceiling + 1, rz))) { @@ -207,7 +221,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { } IrisBiome biome = customBiome.isEmpty() - ? getEngine().getCaveBiome(xx, center, zz) + ? getEngine().getCaveBiome(xx, center, zz, resolverState) : getEngine().getData().getBiomeLoader().load(customBiome); if (biome == null) { @@ -272,6 +286,151 @@ public class IrisCarveModifier extends EngineAssignedModifier { } } + private void appendColumnHeight(int[][] heights, int[] sizes, int columnIndex, int y) { + int[] column = heights[columnIndex]; + int size = sizes[columnIndex]; + if (column == null) { + column = new int[8]; + heights[columnIndex] = column; + } else if (size >= column.length) { + int nextSize = column.length << 1; + column = Arrays.copyOf(column, nextSize); + heights[columnIndex] = column; + } + + column[size] = y; + sizes[columnIndex] = size + 1; + } + + private static final class PackedWallBuffer { + private static final int EMPTY_KEY = -1; + private static final double LOAD_FACTOR = 0.75D; + + private int[] keys; + private MatterCavern[] values; + private int mask; + private int resizeAt; + private int size; + + private PackedWallBuffer(int expectedSize) { + int capacity = 1; + int minimumCapacity = Math.max(8, expectedSize); + while (capacity < minimumCapacity) { + capacity <<= 1; + } + + this.keys = new int[capacity]; + Arrays.fill(this.keys, EMPTY_KEY); + this.values = new MatterCavern[capacity]; + this.mask = capacity - 1; + this.resizeAt = Math.max(1, (int) (capacity * LOAD_FACTOR)); + } + + private void put(int x, int y, int z, MatterCavern value) { + int key = pack(x, y, z); + int index = mix(key) & mask; + + while (true) { + int existingKey = keys[index]; + if (existingKey == EMPTY_KEY) { + keys[index] = key; + values[index] = value; + size++; + if (size >= resizeAt) { + resize(); + } + return; + } + + if (existingKey == key) { + values[index] = value; + return; + } + + index = (index + 1) & mask; + } + } + + private void forEach(PackedWallConsumer consumer) { + for (int index = 0; index < keys.length; index++) { + int key = keys[index]; + if (key == EMPTY_KEY) { + continue; + } + + MatterCavern cavern = values[index]; + if (cavern == null) { + continue; + } + + consumer.accept(unpackX(key), unpackY(key), unpackZ(key), cavern); + } + } + + private void resize() { + int[] oldKeys = keys; + MatterCavern[] oldValues = values; + int nextCapacity = oldKeys.length << 1; + keys = new int[nextCapacity]; + Arrays.fill(keys, EMPTY_KEY); + values = new MatterCavern[nextCapacity]; + mask = nextCapacity - 1; + resizeAt = Math.max(1, (int) (nextCapacity * LOAD_FACTOR)); + size = 0; + + for (int index = 0; index < oldKeys.length; index++) { + int key = oldKeys[index]; + if (key == EMPTY_KEY) { + continue; + } + + MatterCavern value = oldValues[index]; + if (value == null) { + continue; + } + + reinsert(key, value); + } + } + + private void reinsert(int key, MatterCavern value) { + int index = mix(key) & mask; + while (keys[index] != EMPTY_KEY) { + index = (index + 1) & mask; + } + + keys[index] = key; + values[index] = value; + size++; + } + + private int pack(int x, int y, int z) { + return (y << 8) | ((x & 15) << 4) | (z & 15); + } + + private int unpackX(int key) { + return (key >> 4) & 15; + } + + private int unpackY(int key) { + return key >> 8; + } + + private int unpackZ(int key) { + return key & 15; + } + + private int mix(int value) { + int mixed = value * 0x9E3779B9; + return mixed ^ (mixed >>> 16); + } + } + + @FunctionalInterface + private interface PackedWallConsumer { + void accept(int x, int y, int z, MatterCavern cavern); + } + @Data public static class CaveZone { private int ceiling = -1; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java index 70ffc2a1f..eb1679119 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java @@ -4,6 +4,9 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.project.noise.CNG; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -16,30 +19,46 @@ public final class IrisDimensionCarvingResolver { } public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY) { + return resolveRootEntry(engine, worldY, new State()); + } + + public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) { + State resolvedState = state == null ? new State() : state; + if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) { + return resolvedState.rootEntriesByWorldY.get(worldY); + } + IrisDimension dimension = engine.getDimension(); List entries = dimension.getCarving(); if (entries == null || entries.isEmpty()) { + resolvedState.rootEntriesByWorldY.put(worldY, null); return null; } IrisDimensionCarvingEntry resolved = null; for (IrisDimensionCarvingEntry entry : entries) { - if (!isRootCandidate(engine, entry, worldY)) { + if (!isRootCandidate(engine, entry, worldY, resolvedState)) { continue; } resolved = entry; } + resolvedState.rootEntriesByWorldY.put(worldY, resolved); return resolved; } public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) { + return resolveFromRoot(engine, rootEntry, worldX, worldZ, new State()); + } + + public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) { + State resolvedState = state == null ? new State() : state; if (rootEntry == null) { return null; } - IrisBiome rootBiome = resolveEntryBiome(engine, rootEntry); + IrisBiome rootBiome = resolveEntryBiome(engine, rootEntry, resolvedState); if (rootBiome == null) { return null; } @@ -49,11 +68,11 @@ public final class IrisDimensionCarvingResolver { return rootEntry; } - Map entryIndex = engine.getDimension().getCarvingEntryIndex(); + Map entryIndex = resolveEntryIndex(engine, resolvedState); IrisDimensionCarvingEntry current = rootEntry; int depth = remainingDepth; while (depth > 0) { - IrisDimensionCarvingEntry selected = selectChild(engine, current, worldX, worldZ, entryIndex); + IrisDimensionCarvingEntry selected = selectChild(engine, current, worldX, worldZ, entryIndex, resolvedState); if (selected == null || selected == current) { break; } @@ -70,14 +89,28 @@ public final class IrisDimensionCarvingResolver { } public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry) { + return resolveEntryBiome(engine, entry, null); + } + + public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry, State state) { if (entry == null) { return null; } - return entry.getRealBiome(engine.getData()); + if (state == null) { + return entry.getRealBiome(engine.getData()); + } + + if (state.biomeCache.containsKey(entry)) { + return state.biomeCache.get(entry); + } + + IrisBiome biome = entry.getRealBiome(engine.getData()); + state.biomeCache.put(entry, biome); + return biome; } - private static boolean isRootCandidate(Engine engine, IrisDimensionCarvingEntry entry, int worldY) { + private static boolean isRootCandidate(Engine engine, IrisDimensionCarvingEntry entry, int worldY, State state) { if (entry == null || !entry.isEnabled()) { return false; } @@ -87,7 +120,7 @@ public final class IrisDimensionCarvingResolver { return false; } - return resolveEntryBiome(engine, entry) != null; + return resolveEntryBiome(engine, entry, state) != null; } private static IrisDimensionCarvingEntry selectChild( @@ -95,45 +128,33 @@ public final class IrisDimensionCarvingResolver { IrisDimensionCarvingEntry parent, int worldX, int worldZ, - Map entryIndex + Map entryIndex, + State state ) { KList children = parent.getChildren(); if (children == null || children.isEmpty()) { return parent; } - IrisBiome parentBiome = resolveEntryBiome(engine, parent); + IrisBiome parentBiome = resolveEntryBiome(engine, parent, state); if (parentBiome == null) { return parent; } - KList options = new KList<>(); - for (String childId : children) { - if (childId == null || childId.isBlank()) { - continue; - } - - IrisDimensionCarvingEntry child = entryIndex.get(childId.trim()); - if (child == null || !child.isEnabled()) { - continue; - } - - IrisBiome childBiome = resolveEntryBiome(engine, child); - if (childBiome == null) { - continue; - } - - options.add(new CarvingChoice(child, rarity(childBiome))); + ParentSelectionPlan selectionPlan = state.selectionPlans.get(parent); + if (selectionPlan == null) { + selectionPlan = buildSelectionPlan(engine, parent, parentBiome, entryIndex, state); + state.selectionPlans.put(parent, selectionPlan); } - options.add(new CarvingChoice(parent, rarity(parentBiome))); - if (options.size() <= 1) { + if (selectionPlan.parentOnly) { return parent; } - long seed = engine.getSeedManager().getCarve() ^ CHILD_SEED_SALT; + long seed = resolveChildSeed(engine, state); CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData()); - CarvingChoice selected = childGenerator.fitRarity(options, worldX, worldZ); + int selectedIndex = childGenerator.fit(0, selectionPlan.maxIndex, worldX, worldZ); + CarvingChoice selected = selectionPlan.get(selectedIndex); if (selected == null || selected.entry == null) { return parent; } @@ -141,6 +162,74 @@ public final class IrisDimensionCarvingResolver { return selected.entry; } + private static ParentSelectionPlan buildSelectionPlan( + Engine engine, + IrisDimensionCarvingEntry parent, + IrisBiome parentBiome, + Map entryIndex, + State state + ) { + List options = new ArrayList<>(); + KList children = parent.getChildren(); + if (children != null) { + for (String childId : children) { + if (childId == null || childId.isBlank()) { + continue; + } + + IrisDimensionCarvingEntry child = entryIndex.get(childId.trim()); + if (child == null || !child.isEnabled()) { + continue; + } + + IrisBiome childBiome = resolveEntryBiome(engine, child, state); + if (childBiome == null) { + continue; + } + + options.add(new CarvingChoice(child, rarity(childBiome))); + } + } + + options.add(new CarvingChoice(parent, rarity(parentBiome))); + if (options.size() <= 1) { + return ParentSelectionPlan.parentOnly(); + } + + CarvingChoice[] mappedChoices = buildRarityMappedChoices(options); + if (mappedChoices.length == 0) { + return ParentSelectionPlan.parentOnly(); + } + + return new ParentSelectionPlan(mappedChoices); + } + + private static CarvingChoice[] buildRarityMappedChoices(List choices) { + int max = 1; + for (CarvingChoice choice : choices) { + if (choice.rarity > max) { + max = choice.rarity; + } + } + + max++; + List mapped = new ArrayList<>(); + boolean flip = false; + for (CarvingChoice choice : choices) { + int count = max - choice.rarity; + for (int index = 0; index < count; index++) { + flip = !flip; + if (flip) { + mapped.add(choice); + } else { + mapped.add(0, choice); + } + } + } + + return mapped.toArray(new CarvingChoice[0]); + } + private static int rarity(IrisBiome biome) { if (biome == null) { return 1; @@ -158,6 +247,68 @@ public final class IrisDimensionCarvingResolver { return Math.min(depth, MAX_CHILD_DEPTH); } + private static Map resolveEntryIndex(Engine engine, State state) { + if (state.entryIndex == null) { + state.entryIndex = engine.getDimension().getCarvingEntryIndex(); + } + + return state.entryIndex; + } + + private static long resolveChildSeed(Engine engine, State state) { + if (state.childSeed == null) { + state.childSeed = engine.getSeedManager().getCarve() ^ CHILD_SEED_SALT; + } + + return state.childSeed; + } + + public static final class State { + private final Map rootEntriesByWorldY = new HashMap<>(); + private final Map selectionPlans = new IdentityHashMap<>(); + private final Map biomeCache = new IdentityHashMap<>(); + private Map entryIndex; + private Long childSeed; + } + + private static final class ParentSelectionPlan { + private final CarvingChoice[] mappedChoices; + private final int maxIndex; + private final boolean parentOnly; + + private ParentSelectionPlan(CarvingChoice[] mappedChoices) { + this.mappedChoices = mappedChoices; + this.maxIndex = mappedChoices.length - 1; + this.parentOnly = false; + } + + private ParentSelectionPlan() { + this.mappedChoices = null; + this.maxIndex = -1; + this.parentOnly = true; + } + + private static ParentSelectionPlan parentOnly() { + return new ParentSelectionPlan(); + } + + private CarvingChoice get(int index) { + if (mappedChoices == null || mappedChoices.length == 0) { + return null; + } + + if (index < 0) { + return mappedChoices[0]; + } + + if (index >= mappedChoices.length) { + return mappedChoices[mappedChoices.length - 1]; + } + + return mappedChoices[index]; + } + } + private static final class CarvingChoice implements IRare { private final IrisDimensionCarvingEntry entry; private final int rarity; diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java new file mode 100644 index 000000000..b993be395 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java @@ -0,0 +1,319 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.SeedManager; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.block.data.BlockData; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import static org.junit.Assert.assertSame; +import static org.mockito.Answers.CALLS_REAL_METHODS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class IrisDimensionCarvingResolverParityTest { + private static final int MAX_CHILD_DEPTH = 32; + private static final long CHILD_SEED_SALT = 0x9E3779B97F4A7C15L; + + @BeforeClass + public static void setupBukkit() { + if (Bukkit.getServer() != null) { + return; + } + + Server server = mock(Server.class); + BlockData emptyBlockData = mock(BlockData.class); + doReturn(Logger.getLogger("IrisTest")).when(server).getLogger(); + doReturn("IrisTestServer").when(server).getName(); + doReturn("1.0").when(server).getVersion(); + doReturn("1.0").when(server).getBukkitVersion(); + doReturn(emptyBlockData).when(server).createBlockData(any(Material.class)); + doReturn(emptyBlockData).when(server).createBlockData(anyString()); + Bukkit.setServer(server); + } + + @Test + public void resolverStatefulOverloadsMatchLegacyResolverAcrossSampleGrid() { + Fixture fixture = createFixture(); + IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); + + for (int worldY = -64; worldY <= 320; worldY += 11) { + IrisDimensionCarvingEntry legacyRoot = legacyResolveRootEntry(fixture.engine, worldY); + IrisDimensionCarvingEntry statefulRoot = IrisDimensionCarvingResolver.resolveRootEntry(fixture.engine, worldY, state); + assertSame("root mismatch at worldY=" + worldY, legacyRoot, statefulRoot); + + for (int worldX = -192; worldX <= 192; worldX += 31) { + for (int worldZ = -192; worldZ <= 192; worldZ += 37) { + IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ); + IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state); + assertSame("entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved); + } + } + } + } + + @Test + public void caveBiomeStateOverloadMatchesDefaultOverloadAcrossSampleGrid() { + Fixture fixture = createFixture(); + IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); + + for (int y = 1; y <= 300; y += 17) { + for (int x = -160; x <= 160; x += 23) { + for (int z = -160; z <= 160; z += 29) { + IrisBiome defaultBiome = fixture.engine.getCaveBiome(x, y, z); + IrisBiome stateBiome = fixture.engine.getCaveBiome(x, y, z, state); + assertSame("cave biome mismatch at x=" + x + " y=" + y + " z=" + z, defaultBiome, stateBiome); + } + } + } + } + + private Fixture createFixture() { + IrisBiome rootLowBiome = mock(IrisBiome.class); + IrisBiome rootHighBiome = mock(IrisBiome.class); + IrisBiome childABiome = mock(IrisBiome.class); + IrisBiome childBBiome = mock(IrisBiome.class); + IrisBiome childCBiome = mock(IrisBiome.class); + IrisBiome fallbackBiome = mock(IrisBiome.class); + IrisBiome surfaceBiome = mock(IrisBiome.class); + + doReturn(6).when(rootLowBiome).getRarity(); + doReturn(4).when(rootHighBiome).getRarity(); + doReturn(2).when(childABiome).getRarity(); + doReturn(5).when(childBBiome).getRarity(); + doReturn(1).when(childCBiome).getRarity(); + doReturn(0).when(fallbackBiome).getCaveMinDepthBelowSurface(); + + @SuppressWarnings("unchecked") + ResourceLoader biomeLoader = mock(ResourceLoader.class); + doReturn(rootLowBiome).when(biomeLoader).load("root-low"); + doReturn(rootHighBiome).when(biomeLoader).load("root-high"); + doReturn(childABiome).when(biomeLoader).load("child-a"); + doReturn(childBBiome).when(biomeLoader).load("child-b"); + doReturn(childCBiome).when(biomeLoader).load("child-c"); + + IrisData data = mock(IrisData.class); + doReturn(biomeLoader).when(data).getBiomeLoader(); + + IrisDimensionCarvingEntry rootLow = buildEntry("root-low", "root-low", new IrisRange(-64, 120), 4, List.of("child-a", "child-b")); + IrisDimensionCarvingEntry rootHigh = buildEntry("root-high", "root-high", new IrisRange(121, 320), 3, List.of("child-b", "child-c")); + IrisDimensionCarvingEntry childA = buildEntry("child-a", "child-a", new IrisRange(-2048, -1024), 3, List.of("child-b")); + IrisDimensionCarvingEntry childB = buildEntry("child-b", "child-b", new IrisRange(-2048, -1024), 2, List.of("child-c", "child-a")); + IrisDimensionCarvingEntry childC = buildEntry("child-c", "child-c", new IrisRange(-2048, -1024), 1, List.of()); + + KList carvingEntries = new KList<>(); + carvingEntries.add(rootLow); + carvingEntries.add(rootHigh); + carvingEntries.add(childA); + carvingEntries.add(childB); + carvingEntries.add(childC); + + Map index = new HashMap<>(); + index.put(rootLow.getId(), rootLow); + index.put(rootHigh.getId(), rootHigh); + index.put(childA.getId(), childA); + index.put(childB.getId(), childB); + index.put(childC.getId(), childC); + + IrisDimension dimension = mock(IrisDimension.class); + doReturn(carvingEntries).when(dimension).getCarving(); + doReturn(index).when(dimension).getCarvingEntryIndex(); + + Engine engine = mock(Engine.class, CALLS_REAL_METHODS); + doReturn(dimension).when(engine).getDimension(); + doReturn(data).when(engine).getData(); + doReturn(new SeedManager(913_531_771L)).when(engine).getSeedManager(); + doReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()).when(engine).getWorld(); + doReturn(surfaceBiome).when(engine).getSurfaceBiome(anyInt(), anyInt()); + doReturn(fallbackBiome).when(engine).getCaveBiome(anyInt(), anyInt()); + + return new Fixture(engine); + } + + private IrisDimensionCarvingEntry buildEntry(String id, String biome, IrisRange worldRange, int depth, List children) { + IrisDimensionCarvingEntry entry = new IrisDimensionCarvingEntry(); + entry.setId(id); + entry.setEnabled(true); + entry.setBiome(biome); + entry.setWorldYRange(worldRange); + entry.setChildRecursionDepth(depth); + entry.setChildren(new KList<>(children)); + return entry; + } + + private IrisDimensionCarvingEntry legacyResolveRootEntry(Engine engine, int worldY) { + IrisDimension dimension = engine.getDimension(); + List entries = dimension.getCarving(); + if (entries == null || entries.isEmpty()) { + return null; + } + + IrisDimensionCarvingEntry resolved = null; + for (IrisDimensionCarvingEntry entry : entries) { + if (!legacyIsRootCandidate(engine, entry, worldY)) { + continue; + } + + resolved = entry; + } + + return resolved; + } + + private IrisDimensionCarvingEntry legacyResolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) { + if (rootEntry == null) { + return null; + } + + IrisBiome rootBiome = legacyResolveEntryBiome(engine, rootEntry); + if (rootBiome == null) { + return null; + } + + int remainingDepth = clampDepth(rootEntry.getChildRecursionDepth()); + if (remainingDepth <= 0) { + return rootEntry; + } + + Map entryIndex = engine.getDimension().getCarvingEntryIndex(); + IrisDimensionCarvingEntry current = rootEntry; + int depth = remainingDepth; + while (depth > 0) { + IrisDimensionCarvingEntry selected = legacySelectChild(engine, current, worldX, worldZ, entryIndex); + if (selected == null || selected == current) { + break; + } + + depth--; + int childDepthLimit = clampDepth(selected.getChildRecursionDepth()); + if (childDepthLimit < depth) { + depth = childDepthLimit; + } + current = selected; + } + + return current; + } + + private IrisBiome legacyResolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry) { + if (entry == null) { + return null; + } + + return entry.getRealBiome(engine.getData()); + } + + private boolean legacyIsRootCandidate(Engine engine, IrisDimensionCarvingEntry entry, int worldY) { + if (entry == null || !entry.isEnabled()) { + return false; + } + + IrisRange worldYRange = entry.getWorldYRange(); + if (worldYRange != null && !worldYRange.contains(worldY)) { + return false; + } + + return legacyResolveEntryBiome(engine, entry) != null; + } + + private IrisDimensionCarvingEntry legacySelectChild( + Engine engine, + IrisDimensionCarvingEntry parent, + int worldX, + int worldZ, + Map entryIndex + ) { + KList children = parent.getChildren(); + if (children == null || children.isEmpty()) { + return parent; + } + + IrisBiome parentBiome = legacyResolveEntryBiome(engine, parent); + if (parentBiome == null) { + return parent; + } + + KList options = new KList<>(); + for (String childId : children) { + if (childId == null || childId.isBlank()) { + continue; + } + + IrisDimensionCarvingEntry child = entryIndex.get(childId.trim()); + if (child == null || !child.isEnabled()) { + continue; + } + + IrisBiome childBiome = legacyResolveEntryBiome(engine, child); + if (childBiome == null) { + continue; + } + + options.add(new LegacyCarvingChoice(child, rarity(childBiome))); + } + + options.add(new LegacyCarvingChoice(parent, rarity(parentBiome))); + if (options.size() <= 1) { + return parent; + } + + long seed = engine.getSeedManager().getCarve() ^ CHILD_SEED_SALT; + CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData()); + LegacyCarvingChoice selected = childGenerator.fitRarity(options, worldX, worldZ); + if (selected == null || selected.entry == null) { + return parent; + } + + return selected.entry; + } + + private int rarity(IrisBiome biome) { + if (biome == null) { + return 1; + } + + int rarity = biome.getRarity(); + return Math.max(rarity, 1); + } + + private int clampDepth(int depth) { + if (depth <= 0) { + return 0; + } + + return Math.min(depth, MAX_CHILD_DEPTH); + } + + private record Fixture(Engine engine) { + } + + private static final class LegacyCarvingChoice implements IRare { + private final IrisDimensionCarvingEntry entry; + private final int rarity; + + private LegacyCarvingChoice(IrisDimensionCarvingEntry entry, int rarity) { + this.entry = entry; + this.rarity = rarity; + } + + @Override + public int getRarity() { + return rarity; + } + } +}