mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-06-18 14:50:57 +00:00
perfpass
This commit is contained in:
@@ -112,6 +112,11 @@ dependencies {
|
|||||||
// Script Engine
|
// Script Engine
|
||||||
slim(libs.kotlin.stdlib)
|
slim(libs.kotlin.stdlib)
|
||||||
slim(libs.kotlin.coroutines)
|
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 {
|
java {
|
||||||
|
|||||||
@@ -152,11 +152,21 @@ public class IrisSettings {
|
|||||||
public boolean useVirtualThreads = false;
|
public boolean useVirtualThreads = false;
|
||||||
public boolean useTicketQueue = true;
|
public boolean useTicketQueue = true;
|
||||||
public int maxConcurrency = 256;
|
public int maxConcurrency = 256;
|
||||||
|
public int chunkLoadTimeoutSeconds = 15;
|
||||||
|
public int timeoutWarnIntervalMs = 500;
|
||||||
public boolean startupNoisemapPrebake = true;
|
public boolean startupNoisemapPrebake = true;
|
||||||
public boolean enablePregenPerformanceProfile = true;
|
public boolean enablePregenPerformanceProfile = true;
|
||||||
public int pregenProfileNoiseCacheSize = 4_096;
|
public int pregenProfileNoiseCacheSize = 4_096;
|
||||||
public boolean pregenProfileEnableFastCache = true;
|
public boolean pregenProfileEnableFastCache = true;
|
||||||
public boolean pregenProfileLogJvmHints = 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
|
@Data
|
||||||
|
|||||||
+124
-11
@@ -45,13 +45,23 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||||||
public class AsyncPregenMethod implements PregeneratorMethod {
|
public class AsyncPregenMethod implements PregeneratorMethod {
|
||||||
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
|
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
|
||||||
private static final int FOLIA_MAX_CONCURRENCY = 32;
|
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 World world;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final Semaphore semaphore;
|
private final Semaphore semaphore;
|
||||||
private final int threads;
|
private final int threads;
|
||||||
|
private final int timeoutSeconds;
|
||||||
|
private final int timeoutWarnIntervalMs;
|
||||||
private final boolean urgent;
|
private final boolean urgent;
|
||||||
private final Map<Chunk, Long> lastUse;
|
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 AtomicInteger inFlight = new AtomicInteger();
|
||||||
private final AtomicLong submitted = new AtomicLong();
|
private final AtomicLong submitted = new AtomicLong();
|
||||||
private final AtomicLong completed = new AtomicLong();
|
private final AtomicLong completed = new AtomicLong();
|
||||||
@@ -71,14 +81,21 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
boolean useTicketQueue = IrisSettings.get().getPregen().isUseTicketQueue();
|
boolean useTicketQueue = IrisSettings.get().getPregen().isUseTicketQueue();
|
||||||
this.executor = useTicketQueue ? new TicketExecutor() : new ServiceExecutor();
|
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()) {
|
if (J.isFolia()) {
|
||||||
configuredThreads = Math.min(configuredThreads, FOLIA_MAX_CONCURRENCY);
|
configuredThreads = Math.min(configuredThreads, FOLIA_MAX_CONCURRENCY);
|
||||||
|
} else {
|
||||||
|
configuredThreads = Math.min(configuredThreads, resolveNonFoliaConcurrencyCap());
|
||||||
}
|
}
|
||||||
this.threads = Math.max(1, configuredThreads);
|
this.threads = Math.max(1, configuredThreads);
|
||||||
this.semaphore = new Semaphore(this.threads, true);
|
this.semaphore = new Semaphore(this.threads, true);
|
||||||
|
this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds();
|
||||||
|
this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
|
||||||
this.urgent = IrisSettings.get().getPregen().useHighPriority;
|
this.urgent = IrisSettings.get().getPregen().useHighPriority;
|
||||||
this.lastUse = new ConcurrentHashMap<>();
|
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() {
|
private void unloadAndSaveAllChunks() {
|
||||||
@@ -121,7 +138,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (root instanceof java.util.concurrent.TimeoutException) {
|
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 {
|
} else {
|
||||||
Iris.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot());
|
Iris.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot());
|
||||||
}
|
}
|
||||||
@@ -130,10 +147,93 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
return null;
|
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() {
|
private String metricsSnapshot() {
|
||||||
long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get());
|
long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get());
|
||||||
return "world=" + world.getName()
|
return "world=" + world.getName()
|
||||||
+ " permits=" + semaphore.availablePermits() + "/" + threads
|
+ " permits=" + semaphore.availablePermits() + "/" + threads
|
||||||
|
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
|
||||||
+ " inFlight=" + inFlight.get()
|
+ " inFlight=" + inFlight.get()
|
||||||
+ " submitted=" + submitted.get()
|
+ " submitted=" + submitted.get()
|
||||||
+ " completed=" + completed.get()
|
+ " completed=" + completed.get()
|
||||||
@@ -149,6 +249,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
private void markFinished(boolean success) {
|
private void markFinished(boolean success) {
|
||||||
if (success) {
|
if (success) {
|
||||||
completed.incrementAndGet();
|
completed.incrementAndGet();
|
||||||
|
onSuccess();
|
||||||
} else {
|
} else {
|
||||||
failed.incrementAndGet();
|
failed.incrementAndGet();
|
||||||
}
|
}
|
||||||
@@ -177,8 +278,9 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
Iris.info("Async pregen init: world=" + world.getName()
|
Iris.info("Async pregen init: world=" + world.getName()
|
||||||
+ ", mode=" + (J.isFolia() ? "folia" : "paper")
|
+ ", mode=" + (J.isFolia() ? "folia" : "paper")
|
||||||
+ ", threads=" + threads
|
+ ", threads=" + threads
|
||||||
|
+ ", adaptiveLimit=" + adaptiveInFlightLimit.get()
|
||||||
+ ", urgent=" + urgent
|
+ ", urgent=" + urgent
|
||||||
+ ", timeout=" + CHUNK_LOAD_TIMEOUT_SECONDS + "s");
|
+ ", timeout=" + timeoutSeconds + "s");
|
||||||
unloadAndSaveAllChunks();
|
unloadAndSaveAllChunks();
|
||||||
increaseWorkerThreads();
|
increaseWorkerThreads();
|
||||||
}
|
}
|
||||||
@@ -216,6 +318,14 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
listener.onChunkGenerating(x, z);
|
listener.onChunkGenerating(x, z);
|
||||||
try {
|
try {
|
||||||
long waitStart = M.ms();
|
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)) {
|
while (!semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
|
||||||
logPermitWaitIfNeeded(x, z, Math.max(0L, M.ms() - waitStart));
|
logPermitWaitIfNeeded(x, z, Math.max(0L, M.ms() - waitStart));
|
||||||
}
|
}
|
||||||
@@ -288,7 +398,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
@Override
|
@Override
|
||||||
public void generate(int x, int z, PregenListener listener) {
|
public void generate(int x, int z, PregenListener listener) {
|
||||||
if (!J.runRegion(world, x, z, () -> PaperLib.getChunkAtAsync(world, x, z, true, urgent)
|
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) -> {
|
.whenComplete((chunk, throwable) -> {
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
@@ -328,15 +438,16 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
Chunk i = PaperLib.getChunkAtAsync(world, x, z, true, urgent)
|
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))
|
.exceptionally(e -> onChunkFutureFailure(x, z, e))
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
listener.onChunkGenerated(x, z);
|
|
||||||
listener.onChunkCleaned(x, z);
|
|
||||||
if (i == null) {
|
if (i == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listener.onChunkGenerated(x, z);
|
||||||
|
listener.onChunkCleaned(x, z);
|
||||||
lastUse.put(i, M.ms());
|
lastUse.put(i, M.ms());
|
||||||
success = true;
|
success = true;
|
||||||
} catch (InterruptedException ignored) {
|
} catch (InterruptedException ignored) {
|
||||||
@@ -361,16 +472,18 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
@Override
|
@Override
|
||||||
public void generate(int x, int z, PregenListener listener) {
|
public void generate(int x, int z, PregenListener listener) {
|
||||||
PaperLib.getChunkAtAsync(world, x, z, true, urgent)
|
PaperLib.getChunkAtAsync(world, x, z, true, urgent)
|
||||||
.orTimeout(CHUNK_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||||
.exceptionally(e -> onChunkFutureFailure(x, z, e))
|
.exceptionally(e -> onChunkFutureFailure(x, z, e))
|
||||||
.thenAccept(i -> {
|
.thenAccept(i -> {
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
|
if (i == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
listener.onChunkGenerated(x, z);
|
listener.onChunkGenerated(x, z);
|
||||||
listener.onChunkCleaned(x, z);
|
listener.onChunkCleaned(x, z);
|
||||||
if (i != null) {
|
|
||||||
lastUse.put(i, M.ms());
|
lastUse.put(i, M.ms());
|
||||||
}
|
|
||||||
success = true;
|
success = true;
|
||||||
} finally {
|
} finally {
|
||||||
markFinished(success);
|
markFinished(success);
|
||||||
|
|||||||
@@ -236,12 +236,17 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default IrisBiome getCaveBiome(int x, int y, int z) {
|
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);
|
IrisBiome surfaceBiome = getSurfaceBiome(x, z);
|
||||||
int worldY = y + getWorld().minHeight();
|
int worldY = y + getWorld().minHeight();
|
||||||
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY);
|
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state);
|
||||||
if (rootCarvingEntry != null) {
|
if (rootCarvingEntry != null) {
|
||||||
IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(this, rootCarvingEntry, x, z);
|
IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(this, rootCarvingEntry, x, z, state);
|
||||||
IrisBiome resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(this, resolvedCarvingEntry);
|
IrisBiome resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(this, resolvedCarvingEntry, state);
|
||||||
if (resolvedCarvingBiome != null) {
|
if (resolvedCarvingBiome != null) {
|
||||||
return resolvedCarvingBiome;
|
return resolvedCarvingBiome;
|
||||||
}
|
}
|
||||||
@@ -321,9 +326,7 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
|||||||
|
|
||||||
var chunk = mantle.getChunk(c).use();
|
var chunk = mantle.getChunk(c).use();
|
||||||
try {
|
try {
|
||||||
Semaphore semaphore = new Semaphore(1024);
|
Runnable tileTask = () -> {
|
||||||
chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> {
|
|
||||||
chunk.raiseFlagUnchecked(MantleFlag.TILE, run(semaphore, c, () -> {
|
|
||||||
chunk.iterate(TileWrapper.class, (x, y, z, v) -> {
|
chunk.iterate(TileWrapper.class, (x, y, z, v) -> {
|
||||||
Block block = c.getBlock(x & 15, y + getWorld().minHeight(), z & 15);
|
Block block = c.getBlock(x & 15, y + getWorld().minHeight(), z & 15);
|
||||||
if (!TileData.setTileState(block, v.getData())) {
|
if (!TileData.setTileState(block, v.getData())) {
|
||||||
@@ -334,14 +337,15 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
|||||||
Iris.warn("Failed to set tile entity data at [%d %d %d | %s] for tile %s!", block.getX(), block.getY(), block.getZ(), blockType, tileType);
|
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, () -> {
|
|
||||||
|
Runnable customTask = () -> {
|
||||||
chunk.iterate(Identifier.class, (x, y, z, v) -> {
|
chunk.iterate(Identifier.class, (x, y, z, v) -> {
|
||||||
Iris.service(ExternalDataSVC.class).processUpdate(this, c.getBlock(x & 15, y + getWorld().minHeight(), z & 15), 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, () -> {
|
Runnable updateTask = () -> {
|
||||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||||
int[][] grid = new int[16][16];
|
int[][] grid = new int[16][16];
|
||||||
for (int x = 0; x < 16; x++) {
|
for (int x = 0; x < 16; x++) {
|
||||||
@@ -370,8 +374,9 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
|||||||
|
|
||||||
for (int x = 0; x < 16; x++) {
|
for (int x = 0; x < 16; x++) {
|
||||||
for (int z = 0; z < 16; z++) {
|
for (int z = 0; z < 16; z++) {
|
||||||
if (grid[x][z] == Integer.MIN_VALUE)
|
if (grid[x][z] == Integer.MIN_VALUE) {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
update(x, grid[x][z], z, c, chunk, rng);
|
update(x, grid[x][z], z, c, chunk, rng);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,7 +389,22 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
|||||||
});
|
});
|
||||||
chunk.deleteSlices(MatterUpdate.class);
|
chunk.deleteSlices(MatterUpdate.class);
|
||||||
getMetrics().getUpdates().put(p.getMilliseconds());
|
getMetrics().getUpdates().put(p.getMilliseconds());
|
||||||
}, RNG.r.i(1, 20))); //Why is there a random delay here?
|
};
|
||||||
|
|
||||||
|
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, 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 {
|
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) {
|
private static Runnable run(Semaphore semaphore, Chunk contextChunk, Runnable runnable, int delay) {
|
||||||
return () -> {
|
return () -> {
|
||||||
try {
|
try {
|
||||||
@@ -407,13 +439,23 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
|||||||
}
|
}
|
||||||
|
|
||||||
int effectiveDelay = J.isFolia() ? 0 : delay;
|
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 {
|
try {
|
||||||
runnable.run();
|
runnable.run();
|
||||||
} finally {
|
} finally {
|
||||||
semaphore.release();
|
semaphore.release();
|
||||||
}
|
}
|
||||||
}, effectiveDelay);
|
}, 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 cave;
|
||||||
private final AtomicRollingSequence ravine;
|
private final AtomicRollingSequence ravine;
|
||||||
private final AtomicRollingSequence deposit;
|
private final AtomicRollingSequence deposit;
|
||||||
|
private final AtomicRollingSequence carveResolve;
|
||||||
|
private final AtomicRollingSequence carveApply;
|
||||||
|
|
||||||
public EngineMetrics(int mem) {
|
public EngineMetrics(int mem) {
|
||||||
this.total = new AtomicRollingSequence(mem);
|
this.total = new AtomicRollingSequence(mem);
|
||||||
@@ -52,6 +54,8 @@ public class EngineMetrics {
|
|||||||
this.cave = new AtomicRollingSequence(mem);
|
this.cave = new AtomicRollingSequence(mem);
|
||||||
this.ravine = new AtomicRollingSequence(mem);
|
this.ravine = new AtomicRollingSequence(mem);
|
||||||
this.deposit = new AtomicRollingSequence(mem);
|
this.deposit = new AtomicRollingSequence(mem);
|
||||||
|
this.carveResolve = new AtomicRollingSequence(mem);
|
||||||
|
this.carveApply = new AtomicRollingSequence(mem);
|
||||||
}
|
}
|
||||||
|
|
||||||
public KMap<String, Double> pull() {
|
public KMap<String, Double> pull() {
|
||||||
@@ -69,6 +73,8 @@ public class EngineMetrics {
|
|||||||
v.put("cave", cave.getAverage());
|
v.put("cave", cave.getAverage());
|
||||||
v.put("ravine", ravine.getAverage());
|
v.put("ravine", ravine.getAverage());
|
||||||
v.put("deposit", deposit.getAverage());
|
v.put("deposit", deposit.getAverage());
|
||||||
|
v.put("carve.resolve", carveResolve.getAverage());
|
||||||
|
v.put("carve.apply", carveApply.getAverage());
|
||||||
|
|
||||||
return v;
|
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.collection.KList;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||||
|
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ public class IrisCaveCarver3D {
|
|||||||
private static final byte LIQUID_AIR = 0;
|
private static final byte LIQUID_AIR = 0;
|
||||||
private static final byte LIQUID_LAVA = 2;
|
private static final byte LIQUID_LAVA = 2;
|
||||||
private static final byte LIQUID_FORCED_AIR = 3;
|
private static final byte LIQUID_FORCED_AIR = 3;
|
||||||
|
private static final ThreadLocal<Scratch> SCRATCH = ThreadLocal.withInitial(Scratch::new);
|
||||||
|
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private final IrisData data;
|
private final IrisData data;
|
||||||
@@ -49,6 +51,11 @@ public class IrisCaveCarver3D {
|
|||||||
private final MatterCavern carveAir;
|
private final MatterCavern carveAir;
|
||||||
private final MatterCavern carveLava;
|
private final MatterCavern carveLava;
|
||||||
private final MatterCavern carveForcedAir;
|
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) {
|
public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
@@ -65,8 +72,12 @@ public class IrisCaveCarver3D {
|
|||||||
this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data);
|
this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data);
|
||||||
this.surfaceBreakDensity = profile.getSurfaceBreakStyle().create(baseRng.nextParallelRNG(341_219), data);
|
this.surfaceBreakDensity = profile.getSurfaceBreakStyle().create(baseRng.nextParallelRNG(341_219), data);
|
||||||
this.thresholdRng = baseRng.nextParallelRNG(489_112);
|
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;
|
int index = 0;
|
||||||
for (IrisCaveFieldModule module : profile.getModules()) {
|
for (IrisCaveFieldModule module : profile.getModules()) {
|
||||||
CNG moduleDensity = module.getStyle().create(baseRng.nextParallelRNG(1_000_003L + (index * 65_537L)), data);
|
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;
|
normalization = weight <= 0 ? 1 : weight;
|
||||||
|
hasModules = !modules.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int carve(MantleWriter writer, int chunkX, int chunkZ) {
|
public int carve(MantleWriter writer, int chunkX, int chunkZ) {
|
||||||
double[] fullWeights = new double[256];
|
Scratch scratch = SCRATCH.get();
|
||||||
Arrays.fill(fullWeights, 1D);
|
if (!scratch.fullWeightsInitialized) {
|
||||||
return carve(writer, chunkX, chunkZ, fullWeights, 0D, 0D, null);
|
Arrays.fill(scratch.fullWeights, 1D);
|
||||||
|
scratch.fullWeightsInitialized = true;
|
||||||
|
}
|
||||||
|
return carve(writer, chunkX, chunkZ, scratch.fullWeights, 0D, 0D, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int carve(
|
public int carve(
|
||||||
@@ -105,10 +120,15 @@ public class IrisCaveCarver3D {
|
|||||||
double thresholdPenalty,
|
double thresholdPenalty,
|
||||||
IrisRange worldYRange
|
IrisRange worldYRange
|
||||||
) {
|
) {
|
||||||
|
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
|
||||||
|
try {
|
||||||
|
Scratch scratch = SCRATCH.get();
|
||||||
if (columnWeights == null || columnWeights.length < 256) {
|
if (columnWeights == null || columnWeights.length < 256) {
|
||||||
double[] fullWeights = new double[256];
|
if (!scratch.fullWeightsInitialized) {
|
||||||
Arrays.fill(fullWeights, 1D);
|
Arrays.fill(scratch.fullWeights, 1D);
|
||||||
columnWeights = fullWeights;
|
scratch.fullWeightsInitialized = true;
|
||||||
|
}
|
||||||
|
columnWeights = scratch.fullWeights;
|
||||||
}
|
}
|
||||||
|
|
||||||
double resolvedMinWeight = Math.max(0D, Math.min(1D, minWeight));
|
double resolvedMinWeight = Math.max(0D, Math.min(1D, minWeight));
|
||||||
@@ -137,11 +157,11 @@ public class IrisCaveCarver3D {
|
|||||||
|
|
||||||
int x0 = chunkX << 4;
|
int x0 = chunkX << 4;
|
||||||
int z0 = chunkZ << 4;
|
int z0 = chunkZ << 4;
|
||||||
int[] columnSurface = new int[256];
|
int[] columnSurface = scratch.columnSurface;
|
||||||
int[] columnMaxY = new int[256];
|
int[] columnMaxY = scratch.columnMaxY;
|
||||||
int[] surfaceBreakFloorY = new int[256];
|
int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY;
|
||||||
boolean[] surfaceBreakColumn = new boolean[256];
|
boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn;
|
||||||
double[] columnThreshold = new double[256];
|
double[] columnThreshold = scratch.columnThreshold;
|
||||||
|
|
||||||
for (int lx = 0; lx < 16; lx++) {
|
for (int lx = 0; lx < 16; lx++) {
|
||||||
int x = x0 + lx;
|
int x = x0 + lx;
|
||||||
@@ -213,6 +233,9 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return carved;
|
return carved;
|
||||||
|
} finally {
|
||||||
|
engine.getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int carvePass(
|
private int carvePass(
|
||||||
@@ -305,12 +328,16 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double sampleDensity(int x, int y, int z) {
|
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 warpedX = x;
|
||||||
double warpedY = y;
|
double warpedY = y;
|
||||||
double warpedZ = z;
|
double warpedZ = z;
|
||||||
double warpStrength = profile.getWarpStrength();
|
if (hasWarp) {
|
||||||
|
|
||||||
if (warpStrength > 0) {
|
|
||||||
double warpA = signed(warpDensity.noise(x, y, z));
|
double warpA = signed(warpDensity.noise(x, y, z));
|
||||||
double warpB = signed(warpDensity.noise(x + 31.37D, y - 17.21D, z + 23.91D));
|
double warpB = signed(warpDensity.noise(x + 31.37D, y - 17.21D, z + 23.91D));
|
||||||
double offsetX = warpA * warpStrength;
|
double offsetX = warpA * warpStrength;
|
||||||
@@ -321,9 +348,10 @@ public class IrisCaveCarver3D {
|
|||||||
warpedZ += offsetZ;
|
warpedZ += offsetZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
double density = signed(baseDensity.noise(warpedX, warpedY, warpedZ)) * profile.getBaseWeight();
|
double density = signed(baseDensity.noise(warpedX, warpedY, warpedZ)) * baseWeight;
|
||||||
density += signed(detailDensity.noise(warpedX, warpedY, warpedZ)) * profile.getDetailWeight();
|
density += signed(detailDensity.noise(warpedX, warpedY, warpedZ)) * detailWeight;
|
||||||
|
|
||||||
|
if (hasModules) {
|
||||||
for (ModuleState module : modules) {
|
for (ModuleState module : modules) {
|
||||||
if (y < module.minY || y > module.maxY) {
|
if (y < module.minY || y > module.maxY) {
|
||||||
continue;
|
continue;
|
||||||
@@ -336,6 +364,7 @@ public class IrisCaveCarver3D {
|
|||||||
|
|
||||||
density += moduleDensity * module.weight;
|
density += moduleDensity * module.weight;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return density / normalization;
|
return density / normalization;
|
||||||
}
|
}
|
||||||
@@ -397,4 +426,14 @@ public class IrisCaveCarver3D {
|
|||||||
this.invert = module.isInvert();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+80
-53
@@ -31,9 +31,9 @@ import art.arcane.iris.engine.object.IrisRange;
|
|||||||
import art.arcane.iris.util.project.context.ChunkContext;
|
import art.arcane.iris.util.project.context.ChunkContext;
|
||||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||||
|
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
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 int FIELD_SIZE = CHUNK_SIZE + (BLEND_RADIUS * 2);
|
||||||
private static final double MIN_WEIGHT = 0.08D;
|
private static final double MIN_WEIGHT = 0.08D;
|
||||||
private static final double THRESHOLD_PENALTY = 0.24D;
|
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<>();
|
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) {
|
public MantleCarvingComponent(EngineMantle engineMantle) {
|
||||||
super(engineMantle, ReservedFlag.CARVED, 0);
|
super(engineMantle, ReservedFlag.CARVED, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
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) {
|
for (WeightedProfile weightedProfile : weightedProfiles) {
|
||||||
carveProfile(weightedProfile, writer, x, z);
|
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);
|
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ) {
|
private List<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState) {
|
||||||
IrisCaveProfile[] profileField = buildProfileField(chunkX, chunkZ);
|
IrisCaveProfile[] profileField = buildProfileField(chunkX, chunkZ, resolverState);
|
||||||
Map<IrisCaveProfile, double[]> profileWeights = new IdentityHashMap<>();
|
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 localX = 0; localX < CHUNK_SIZE; localX++) {
|
||||||
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
|
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
|
||||||
|
int profileCount = 0;
|
||||||
int columnIndex = (localX << 4) | localZ;
|
int columnIndex = (localX << 4) | localZ;
|
||||||
Map<IrisCaveProfile, Double> columnInfluence = sampleColumnInfluence(profileField, localX, localZ);
|
int centerX = localX + BLEND_RADIUS;
|
||||||
for (Map.Entry<IrisCaveProfile, Double> entry : columnInfluence.entrySet()) {
|
int centerZ = localZ + BLEND_RADIUS;
|
||||||
double[] weights = profileWeights.computeIfAbsent(entry.getKey(), key -> new double[CHUNK_AREA]);
|
double totalKernelWeight = 0D;
|
||||||
weights[columnIndex] = entry.getValue();
|
|
||||||
|
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.sort(Comparator.comparingDouble(WeightedProfile::averageWeight));
|
||||||
weightedProfiles.addAll(0, resolveDimensionCarvingProfiles(chunkX, chunkZ));
|
weightedProfiles.addAll(0, resolveDimensionCarvingProfiles(chunkX, chunkZ, resolverState));
|
||||||
return weightedProfiles;
|
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<WeightedProfile> weightedProfiles = new ArrayList<>();
|
||||||
List<IrisDimensionCarvingEntry> entries = getDimension().getCarving();
|
List<IrisDimensionCarvingEntry> entries = getDimension().getCarving();
|
||||||
if (entries == null || entries.isEmpty()) {
|
if (entries == null || entries.isEmpty()) {
|
||||||
@@ -122,7 +177,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), entry);
|
IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), entry, resolverState);
|
||||||
if (rootBiome == null) {
|
if (rootBiome == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -134,8 +189,8 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
int worldX = (chunkX << 4) + localX;
|
int worldX = (chunkX << 4) + localX;
|
||||||
int worldZ = (chunkZ << 4) + localZ;
|
int worldZ = (chunkZ << 4) + localZ;
|
||||||
int columnIndex = (localX << 4) | localZ;
|
int columnIndex = (localX << 4) | localZ;
|
||||||
IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ);
|
IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState);
|
||||||
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry);
|
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState);
|
||||||
if (resolvedBiome == null) {
|
if (resolvedBiome == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -160,40 +215,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
return weightedProfiles;
|
return weightedProfiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<IrisCaveProfile, Double> sampleColumnInfluence(IrisCaveProfile[] profileField, int localX, int localZ) {
|
private IrisCaveProfile[] buildProfileField(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState) {
|
||||||
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) {
|
|
||||||
IrisCaveProfile[] profileField = new IrisCaveProfile[FIELD_SIZE * FIELD_SIZE];
|
IrisCaveProfile[] profileField = new IrisCaveProfile[FIELD_SIZE * FIELD_SIZE];
|
||||||
int startX = (chunkX << 4) - BLEND_RADIUS;
|
int startX = (chunkX << 4) - BLEND_RADIUS;
|
||||||
int startZ = (chunkZ << 4) - BLEND_RADIUS;
|
int startZ = (chunkZ << 4) - BLEND_RADIUS;
|
||||||
@@ -202,19 +224,24 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
int worldX = startX + fieldX;
|
int worldX = startX + fieldX;
|
||||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||||
int worldZ = startZ + fieldZ;
|
int worldZ = startZ + fieldZ;
|
||||||
profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ);
|
profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ, resolverState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return profileField;
|
return profileField;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double haloWeight(int offsetX, int offsetZ) {
|
private int findProfileIndex(IrisCaveProfile[] profiles, int size, IrisCaveProfile profile) {
|
||||||
int edgeDistance = Math.max(Math.abs(offsetX), Math.abs(offsetZ));
|
for (int index = 0; index < size; index++) {
|
||||||
return (BLEND_RADIUS + 1D) - edgeDistance;
|
if (profiles[index] == profile) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ) {
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState) {
|
||||||
IrisCaveProfile resolved = null;
|
IrisCaveProfile resolved = null;
|
||||||
IrisCaveProfile dimensionProfile = getDimension().getCaveProfile();
|
IrisCaveProfile dimensionProfile = getDimension().getCaveProfile();
|
||||||
if (isProfileEnabled(dimensionProfile)) {
|
if (isProfileEnabled(dimensionProfile)) {
|
||||||
@@ -239,7 +266,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
|
|
||||||
int surfaceY = getEngineMantle().getEngine().getHeight(worldX, worldZ, true);
|
int surfaceY = getEngineMantle().getEngine().getHeight(worldX, worldZ, true);
|
||||||
int sampleY = Math.max(1, surfaceY - 56);
|
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) {
|
if (caveBiome != null) {
|
||||||
IrisCaveProfile caveProfile = caveBiome.getCaveProfile();
|
IrisCaveProfile caveProfile = caveBiome.getCaveProfile();
|
||||||
if (isProfileEnabled(caveProfile)) {
|
if (isProfileEnabled(caveProfile)) {
|
||||||
|
|||||||
@@ -19,17 +19,18 @@
|
|||||||
package art.arcane.iris.engine.modifier;
|
package art.arcane.iris.engine.modifier;
|
||||||
|
|
||||||
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineAssignedModifier;
|
import art.arcane.iris.engine.framework.EngineAssignedModifier;
|
||||||
import art.arcane.iris.engine.object.*;
|
import art.arcane.iris.engine.object.InferredType;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
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.project.context.ChunkContext;
|
||||||
import art.arcane.iris.util.common.data.B;
|
import art.arcane.iris.util.common.data.B;
|
||||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
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.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.Mantle;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
import art.arcane.volmlib.util.math.M;
|
import art.arcane.volmlib.util.math.M;
|
||||||
@@ -42,6 +43,8 @@ import lombok.Data;
|
|||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
||||||
private final RNG rng;
|
private final RNG rng;
|
||||||
private final BlockData AIR = Material.CAVE_AIR.createBlockData();
|
private final BlockData AIR = Material.CAVE_AIR.createBlockData();
|
||||||
@@ -60,44 +63,46 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
|||||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||||
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
|
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
|
||||||
MantleChunk<Matter> mc = mantle.getChunk(x, z).use();
|
MantleChunk<Matter> mc = mantle.getChunk(x, z).use();
|
||||||
KMap<Long, KList<Integer>> positions = new KMap<>();
|
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
||||||
KMap<IrisPosition, MatterCavern> walls = new KMap<>();
|
int[][] columnHeights = new int[256][];
|
||||||
Consumer4<Integer, Integer, Integer, MatterCavern> iterator = (xx, yy, zz, c) -> {
|
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) {
|
if (c == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yy >= getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight() || yy <= 0) { // Yes, skip bedrock
|
if (yy >= getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight() || yy <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int rx = xx & 15;
|
int rx = xx & 15;
|
||||||
int rz = zz & 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)) {
|
if (B.isFluid(current)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
positions.computeIfAbsent(Cache.key(rx, rz), (k) -> new KList<>()).qadd(yy);
|
appendColumnHeight(columnHeights, columnHeightSizes, columnIndex, yy);
|
||||||
|
|
||||||
//todo: Fix chunk decoration not working on chunk's border
|
|
||||||
|
|
||||||
if (rz < 15 && mc.get(xx, yy, zz + 1, MatterCavern.class) == null) {
|
if (rz < 15 && mc.get(xx, yy, zz + 1, MatterCavern.class) == null) {
|
||||||
walls.put(new IrisPosition(rx, yy, rz + 1), c);
|
walls.put(rx, yy, rz + 1, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rx < 15 && mc.get(xx + 1, yy, zz, MatterCavern.class) == null) {
|
if (rx < 15 && mc.get(xx + 1, yy, zz, MatterCavern.class) == null) {
|
||||||
walls.put(new IrisPosition(rx + 1, yy, rz), c);
|
walls.put(rx + 1, yy, rz, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rz > 0 && mc.get(xx, yy, zz - 1, MatterCavern.class) == null) {
|
if (rz > 0 && mc.get(xx, yy, zz - 1, MatterCavern.class) == null) {
|
||||||
walls.put(new IrisPosition(rx, yy, rz - 1), c);
|
walls.put(rx, yy, rz - 1, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rx > 0 && mc.get(xx - 1, yy, zz, MatterCavern.class) == null) {
|
if (rx > 0 && mc.get(xx - 1, yy, zz, MatterCavern.class) == null) {
|
||||||
walls.put(new IrisPosition(rx - 1, yy, rz), c);
|
walls.put(rx - 1, yy, rz, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.getMaterial().isAir()) {
|
if (current.getMaterial().isAir()) {
|
||||||
@@ -110,74 +115,83 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
|||||||
output.set(rx, yy, rz, LAVA);
|
output.set(rx, yy, rz, LAVA);
|
||||||
} else if (c.getLiquid() == 3) {
|
} else if (c.getLiquid() == 3) {
|
||||||
output.set(rx, yy, rz, AIR);
|
output.set(rx, yy, rz, AIR);
|
||||||
} else {
|
} else if (getEngine().getDimension().getCaveLavaHeight() > yy) {
|
||||||
if (getEngine().getDimension().getCaveLavaHeight() > yy) {
|
|
||||||
output.set(rx, yy, rz, LAVA);
|
output.set(rx, yy, rz, LAVA);
|
||||||
} else {
|
} else {
|
||||||
output.set(rx, yy, rz, AIR);
|
output.set(rx, yy, rz, AIR);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
};
|
getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
|
||||||
|
|
||||||
mc.iterate(MatterCavern.class, iterator);
|
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
|
||||||
|
try {
|
||||||
walls.forEach((i, v) -> {
|
walls.forEach((rx, yy, rz, cavern) -> {
|
||||||
IrisBiome biome = v.getCustomBiome().isEmpty()
|
int worldX = rx + (x << 4);
|
||||||
? getEngine().getCaveBiome(i.getX() + (x << 4), i.getY(), i.getZ() + (z << 4))
|
int worldZ = rz + (z << 4);
|
||||||
: getEngine().getData().getBiomeLoader().load(v.getCustomBiome());
|
IrisBiome biome = cavern.getCustomBiome().isEmpty()
|
||||||
|
? getEngine().getCaveBiome(worldX, yy, worldZ, resolverState)
|
||||||
|
: getEngine().getData().getBiomeLoader().load(cavern.getCustomBiome());
|
||||||
|
|
||||||
if (biome != null) {
|
if (biome != null) {
|
||||||
biome.setInferredType(InferredType.CAVE);
|
biome.setInferredType(InferredType.CAVE);
|
||||||
BlockData d = biome.getWall().get(rng, i.getX() + (x << 4), i.getY(), i.getZ() + (z << 4), getData());
|
BlockData data = biome.getWall().get(rng, worldX, yy, worldZ, getData());
|
||||||
|
|
||||||
if (d != null && B.isSolid(output.get(i.getX(), i.getY(), i.getZ())) && i.getY() <= context.getHeight().get(i.getX(), i.getZ())) {
|
if (data != null && B.isSolid(output.get(rx, yy, rz)) && yy <= context.getHeight().get(rx, rz)) {
|
||||||
output.set(i.getX(), i.getY(), i.getZ(), d);
|
output.set(rx, yy, rz, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
positions.forEach((k, v) -> {
|
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||||
if (v.isEmpty()) {
|
int size = columnHeightSizes[columnIndex];
|
||||||
return;
|
if (size <= 0) {
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i == buf + 1) {
|
int[] heights = columnHeights[columnIndex];
|
||||||
buf = i;
|
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;
|
zone.ceiling = buf;
|
||||||
} else if (zone.isValid(getEngine())) {
|
} else if (zone.isValid(getEngine())) {
|
||||||
processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4));
|
processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4), resolverState);
|
||||||
zone = new CaveZone();
|
zone = new CaveZone();
|
||||||
zone.setFloor(i);
|
zone.setFloor(y);
|
||||||
buf = i;
|
buf = y;
|
||||||
|
} else {
|
||||||
|
zone = new CaveZone();
|
||||||
|
zone.setFloor(y);
|
||||||
|
buf = y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zone.isValid(getEngine())) {
|
if (zone.isValid(getEngine())) {
|
||||||
processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4));
|
processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4), resolverState);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
} finally {
|
||||||
getEngine().getMetrics().getDeposit().put(p.getMilliseconds());
|
getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
getEngine().getMetrics().getCave().put(p.getMilliseconds());
|
||||||
mc.release();
|
mc.release();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void processZone(Hunk<BlockData> output, MantleChunk<Matter> mc, Mantle<Matter> mantle, CaveZone zone, int rx, int rz, int xx, int zz) {
|
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) {
|
||||||
boolean decFloor = B.isSolid(output.getClosest(rx, zone.floor - 1, rz));
|
|
||||||
boolean decCeiling = B.isSolid(output.getClosest(rx, zone.ceiling + 1, rz));
|
|
||||||
int center = (zone.floor + zone.ceiling) / 2;
|
int center = (zone.floor + zone.ceiling) / 2;
|
||||||
int thickness = zone.airThickness();
|
|
||||||
String customBiome = "";
|
String customBiome = "";
|
||||||
|
|
||||||
if (B.isDecorant(output.getClosest(rx, zone.ceiling + 1, rz))) {
|
if (B.isDecorant(output.getClosest(rx, zone.ceiling + 1, rz))) {
|
||||||
@@ -207,7 +221,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IrisBiome biome = customBiome.isEmpty()
|
IrisBiome biome = customBiome.isEmpty()
|
||||||
? getEngine().getCaveBiome(xx, center, zz)
|
? getEngine().getCaveBiome(xx, center, zz, resolverState)
|
||||||
: getEngine().getData().getBiomeLoader().load(customBiome);
|
: getEngine().getData().getBiomeLoader().load(customBiome);
|
||||||
|
|
||||||
if (biome == null) {
|
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
|
@Data
|
||||||
public static class CaveZone {
|
public static class CaveZone {
|
||||||
private int ceiling = -1;
|
private int ceiling = -1;
|
||||||
|
|||||||
+168
-17
@@ -4,6 +4,9 @@ import art.arcane.iris.engine.framework.Engine;
|
|||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.iris.util.project.noise.CNG;
|
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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -16,30 +19,46 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY) {
|
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();
|
IrisDimension dimension = engine.getDimension();
|
||||||
List<IrisDimensionCarvingEntry> entries = dimension.getCarving();
|
List<IrisDimensionCarvingEntry> entries = dimension.getCarving();
|
||||||
if (entries == null || entries.isEmpty()) {
|
if (entries == null || entries.isEmpty()) {
|
||||||
|
resolvedState.rootEntriesByWorldY.put(worldY, null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisDimensionCarvingEntry resolved = null;
|
IrisDimensionCarvingEntry resolved = null;
|
||||||
for (IrisDimensionCarvingEntry entry : entries) {
|
for (IrisDimensionCarvingEntry entry : entries) {
|
||||||
if (!isRootCandidate(engine, entry, worldY)) {
|
if (!isRootCandidate(engine, entry, worldY, resolvedState)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved = entry;
|
resolved = entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolvedState.rootEntriesByWorldY.put(worldY, resolved);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) {
|
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) {
|
if (rootEntry == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisBiome rootBiome = resolveEntryBiome(engine, rootEntry);
|
IrisBiome rootBiome = resolveEntryBiome(engine, rootEntry, resolvedState);
|
||||||
if (rootBiome == null) {
|
if (rootBiome == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -49,11 +68,11 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
return rootEntry;
|
return rootEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, IrisDimensionCarvingEntry> entryIndex = engine.getDimension().getCarvingEntryIndex();
|
Map<String, IrisDimensionCarvingEntry> entryIndex = resolveEntryIndex(engine, resolvedState);
|
||||||
IrisDimensionCarvingEntry current = rootEntry;
|
IrisDimensionCarvingEntry current = rootEntry;
|
||||||
int depth = remainingDepth;
|
int depth = remainingDepth;
|
||||||
while (depth > 0) {
|
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) {
|
if (selected == null || selected == current) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -70,14 +89,28 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry) {
|
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) {
|
if (entry == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state == null) {
|
||||||
return entry.getRealBiome(engine.getData());
|
return entry.getRealBiome(engine.getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isRootCandidate(Engine engine, IrisDimensionCarvingEntry entry, int worldY) {
|
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, State state) {
|
||||||
if (entry == null || !entry.isEnabled()) {
|
if (entry == null || !entry.isEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -87,7 +120,7 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveEntryBiome(engine, entry) != null;
|
return resolveEntryBiome(engine, entry, state) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IrisDimensionCarvingEntry selectChild(
|
private static IrisDimensionCarvingEntry selectChild(
|
||||||
@@ -95,19 +128,50 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
IrisDimensionCarvingEntry parent,
|
IrisDimensionCarvingEntry parent,
|
||||||
int worldX,
|
int worldX,
|
||||||
int worldZ,
|
int worldZ,
|
||||||
Map<String, IrisDimensionCarvingEntry> entryIndex
|
Map<String, IrisDimensionCarvingEntry> entryIndex,
|
||||||
|
State state
|
||||||
) {
|
) {
|
||||||
KList<String> children = parent.getChildren();
|
KList<String> children = parent.getChildren();
|
||||||
if (children == null || children.isEmpty()) {
|
if (children == null || children.isEmpty()) {
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisBiome parentBiome = resolveEntryBiome(engine, parent);
|
IrisBiome parentBiome = resolveEntryBiome(engine, parent, state);
|
||||||
if (parentBiome == null) {
|
if (parentBiome == null) {
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
KList<CarvingChoice> options = new KList<>();
|
ParentSelectionPlan selectionPlan = state.selectionPlans.get(parent);
|
||||||
|
if (selectionPlan == null) {
|
||||||
|
selectionPlan = buildSelectionPlan(engine, parent, parentBiome, entryIndex, state);
|
||||||
|
state.selectionPlans.put(parent, selectionPlan);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionPlan.parentOnly) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
long seed = resolveChildSeed(engine, state);
|
||||||
|
CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData());
|
||||||
|
int selectedIndex = childGenerator.fit(0, selectionPlan.maxIndex, worldX, worldZ);
|
||||||
|
CarvingChoice selected = selectionPlan.get(selectedIndex);
|
||||||
|
if (selected == null || selected.entry == null) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
for (String childId : children) {
|
||||||
if (childId == null || childId.isBlank()) {
|
if (childId == null || childId.isBlank()) {
|
||||||
continue;
|
continue;
|
||||||
@@ -118,27 +182,52 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisBiome childBiome = resolveEntryBiome(engine, child);
|
IrisBiome childBiome = resolveEntryBiome(engine, child, state);
|
||||||
if (childBiome == null) {
|
if (childBiome == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.add(new CarvingChoice(child, rarity(childBiome)));
|
options.add(new CarvingChoice(child, rarity(childBiome)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
options.add(new CarvingChoice(parent, rarity(parentBiome)));
|
options.add(new CarvingChoice(parent, rarity(parentBiome)));
|
||||||
if (options.size() <= 1) {
|
if (options.size() <= 1) {
|
||||||
return parent;
|
return ParentSelectionPlan.parentOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
long seed = engine.getSeedManager().getCarve() ^ CHILD_SEED_SALT;
|
CarvingChoice[] mappedChoices = buildRarityMappedChoices(options);
|
||||||
CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData());
|
if (mappedChoices.length == 0) {
|
||||||
CarvingChoice selected = childGenerator.fitRarity(options, worldX, worldZ);
|
return ParentSelectionPlan.parentOnly();
|
||||||
if (selected == null || selected.entry == null) {
|
|
||||||
return parent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return selected.entry;
|
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) {
|
private static int rarity(IrisBiome biome) {
|
||||||
@@ -158,6 +247,68 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
return Math.min(depth, MAX_CHILD_DEPTH);
|
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 static final class CarvingChoice implements IRare {
|
||||||
private final IrisDimensionCarvingEntry entry;
|
private final IrisDimensionCarvingEntry entry;
|
||||||
private final int rarity;
|
private final int rarity;
|
||||||
|
|||||||
+319
@@ -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