This commit is contained in:
Brian Neumann-Fopiano
2026-02-22 09:58:33 -05:00
parent 5b1ff6d2f7
commit 651dfa247e
10 changed files with 1238 additions and 367 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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<Chunk, Long> 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);

View File

@@ -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();
}
}
};
}

View File

@@ -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<String, Double> 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;
}

View File

@@ -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> 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;
}
}

View File

@@ -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<IrisCaveProfile, IrisCaveCarver3D> 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<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z);
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
List<WeightedProfile> 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<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ) {
IrisCaveProfile[] profileField = buildProfileField(chunkX, chunkZ);
private List<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState) {
IrisCaveProfile[] profileField = buildProfileField(chunkX, chunkZ, resolverState);
Map<IrisCaveProfile, double[]> 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<IrisCaveProfile, Double> columnInfluence = sampleColumnInfluence(profileField, localX, localZ);
for (Map.Entry<IrisCaveProfile, Double> 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<WeightedProfile> resolveDimensionCarvingProfiles(int chunkX, int chunkZ) {
private List<WeightedProfile> resolveDimensionCarvingProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState) {
List<WeightedProfile> weightedProfiles = new ArrayList<>();
List<IrisDimensionCarvingEntry> 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<IrisCaveProfile, Double> sampleColumnInfluence(IrisCaveProfile[] profileField, int localX, int localZ) {
Map<IrisCaveProfile, Double> 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<IrisCaveProfile, Double> normalized = new IdentityHashMap<>();
for (Map.Entry<IrisCaveProfile, Double> 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)) {

View File

@@ -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<BlockData> {
private final RNG rng;
private final BlockData AIR = Material.CAVE_AIR.createBlockData();
@@ -60,124 +63,135 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
PrecisionStopwatch p = PrecisionStopwatch.start();
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
MantleChunk<Matter> mc = mantle.getChunk(x, z).use();
KMap<Long, KList<Integer>> positions = new KMap<>();
KMap<IrisPosition, MatterCavern> walls = new KMap<>();
Consumer4<Integer, Integer, Integer, MatterCavern> 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<BlockData> output, MantleChunk<Matter> mc, Mantle<Matter> 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<BlockData> output, MantleChunk<Matter> mc, Mantle<Matter> 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<BlockData> {
}
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<BlockData> {
}
}
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;

View File

@@ -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<IrisDimensionCarvingEntry> 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<String, IrisDimensionCarvingEntry> entryIndex = engine.getDimension().getCarvingEntryIndex();
Map<String, IrisDimensionCarvingEntry> 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<String, IrisDimensionCarvingEntry> entryIndex
Map<String, IrisDimensionCarvingEntry> entryIndex,
State state
) {
KList<String> 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<CarvingChoice> 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<String, IrisDimensionCarvingEntry> entryIndex,
State state
) {
List<CarvingChoice> options = new ArrayList<>();
KList<String> 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<CarvingChoice> choices) {
int max = 1;
for (CarvingChoice choice : choices) {
if (choice.rarity > max) {
max = choice.rarity;
}
}
max++;
List<CarvingChoice> 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<String, IrisDimensionCarvingEntry> 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<Integer, IrisDimensionCarvingEntry> rootEntriesByWorldY = new HashMap<>();
private final Map<IrisDimensionCarvingEntry, ParentSelectionPlan> selectionPlans = new IdentityHashMap<>();
private final Map<IrisDimensionCarvingEntry, IrisBiome> biomeCache = new IdentityHashMap<>();
private Map<String, IrisDimensionCarvingEntry> 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;

View File

@@ -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<IrisBiome> 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<IrisDimensionCarvingEntry> carvingEntries = new KList<>();
carvingEntries.add(rootLow);
carvingEntries.add(rootHigh);
carvingEntries.add(childA);
carvingEntries.add(childB);
carvingEntries.add(childC);
Map<String, IrisDimensionCarvingEntry> 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<String> 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<IrisDimensionCarvingEntry> 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<String, IrisDimensionCarvingEntry> 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<String, IrisDimensionCarvingEntry> entryIndex
) {
KList<String> children = parent.getChildren();
if (children == null || children.isEmpty()) {
return parent;
}
IrisBiome parentBiome = legacyResolveEntryBiome(engine, parent);
if (parentBiome == null) {
return parent;
}
KList<LegacyCarvingChoice> 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;
}
}
}