mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-04-03 06:16:19 +00:00
perfpass
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user