mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
d
d
This commit is contained in:
@@ -79,6 +79,11 @@ public final class IrisStartupValidation {
|
||||
return isReady(snapshot);
|
||||
}
|
||||
|
||||
public static boolean isRestartRequired() {
|
||||
Snapshot current = snapshot;
|
||||
return current.enforced() && current.datapacks() == ValidationState.RESTART_REQUIRED;
|
||||
}
|
||||
|
||||
public static Optional<String> denialReason() {
|
||||
Snapshot current = snapshot;
|
||||
if (!current.enforced() || isReady(current)) {
|
||||
|
||||
@@ -124,6 +124,7 @@ public class ServerConfigurator {
|
||||
&& pinLoadedDatapackCompilerInputs()
|
||||
&& pinLoadedDatapackRegistryRequirements();
|
||||
if (result.restartRequired()) {
|
||||
requireDatapackRestart();
|
||||
IrisLogging.warn("Iris datapack changes require another server restart before worlds can use them.");
|
||||
}
|
||||
}
|
||||
@@ -1020,6 +1021,24 @@ public class ServerConfigurator {
|
||||
}));
|
||||
}
|
||||
|
||||
public static void restartAtStartupBoundary(String reason) {
|
||||
String restartReason = reason == null || reason.isBlank()
|
||||
? "Iris startup validation requires a restart."
|
||||
: reason.trim();
|
||||
IrisLogging.warn(restartReason + " Restarting server before default worlds are loaded.");
|
||||
try {
|
||||
Bukkit.restart();
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Unable to restart the server at the Iris startup boundary.", failure);
|
||||
}
|
||||
IrisLogging.error("The immediate Iris startup restart returned unexpectedly; stopping the server instead.");
|
||||
try {
|
||||
Bukkit.shutdown();
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Unable to stop the server after the Iris startup restart returned.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
|
||||
KSet<String> keys = new KSet<>();
|
||||
boolean warn = false;
|
||||
|
||||
@@ -24,13 +24,19 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.awt.desktop.QuitResponse;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class GuiHost {
|
||||
private static final AtomicBoolean DESKTOP_QUIT_GUARD_INSTALLED = new AtomicBoolean(false);
|
||||
private static final Set<JFrame> MANAGED_FRAMES = ConcurrentHashMap.newKeySet();
|
||||
private static volatile Provider provider = new Provider() {
|
||||
};
|
||||
private static volatile boolean desktopSuppressed = false;
|
||||
@@ -78,6 +84,13 @@ public final class GuiHost {
|
||||
|
||||
public static void prepareFrame(JFrame frame) {
|
||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
MANAGED_FRAMES.add(frame);
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent event) {
|
||||
MANAGED_FRAMES.remove(frame);
|
||||
}
|
||||
});
|
||||
prepareServerDesktop();
|
||||
}
|
||||
|
||||
@@ -96,15 +109,20 @@ public final class GuiHost {
|
||||
if (!desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
|
||||
return;
|
||||
}
|
||||
desktop.setQuitHandler((event, response) -> cancelDesktopQuit(response));
|
||||
desktop.setQuitHandler((event, response) -> closeDesktopWindowsAndCancelQuit(response));
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
IrisLogging.info("Unable to install the Iris desktop quit guard; use the server stop command instead of macOS Quit");
|
||||
}
|
||||
}
|
||||
|
||||
static void cancelDesktopQuit(QuitResponse response) {
|
||||
static void closeDesktopWindowsAndCancelQuit(QuitResponse response) {
|
||||
response.cancelQuit();
|
||||
EventQueue.invokeLater(() -> {
|
||||
for (JFrame frame : MANAGED_FRAMES) {
|
||||
frame.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
enum NoisePalette {
|
||||
TERRAIN("Terrain", 0D, 1D, new int[]{0x071A2F, 0x155E75, 0x2A9D8F, 0xE9C46A, 0xF4F1DE}),
|
||||
SIGNED("Signed", -1D, 1D, new int[]{0x173B66, 0x4F86C6, 0xE7EDF3, 0xE89555, 0x9D3B35}),
|
||||
GRAYSCALE("Grayscale", 0D, 1D, new int[]{0x050505, 0xFFFFFF});
|
||||
|
||||
static final int INVALID_COLOR = 0xFF2DAA;
|
||||
|
||||
private final String label;
|
||||
private final double minimum;
|
||||
private final double maximum;
|
||||
private final int[] lookup;
|
||||
private final Color[] displayColors;
|
||||
|
||||
NoisePalette(String label, double minimum, double maximum, int[] stops) {
|
||||
this.label = label;
|
||||
this.minimum = minimum;
|
||||
this.maximum = maximum;
|
||||
this.lookup = buildLookup(stops);
|
||||
this.displayColors = buildDisplayColors(lookup);
|
||||
}
|
||||
|
||||
int color(double value) {
|
||||
if (!Double.isFinite(value)) {
|
||||
return INVALID_COLOR;
|
||||
}
|
||||
return colorFinite(value);
|
||||
}
|
||||
|
||||
int colorFinite(double value) {
|
||||
double normalized = (value - minimum) / (maximum - minimum);
|
||||
return colorNormalized(normalized);
|
||||
}
|
||||
|
||||
int colorNormalized(double normalized) {
|
||||
double clipped = Math.max(0D, Math.min(1D, normalized));
|
||||
return lookup[(int) Math.round(clipped * (lookup.length - 1))];
|
||||
}
|
||||
|
||||
Color displayColorNormalized(double normalized) {
|
||||
double clipped = Math.max(0D, Math.min(1D, normalized));
|
||||
return displayColors[(int) Math.round(clipped * (displayColors.length - 1))];
|
||||
}
|
||||
|
||||
String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
double minimum() {
|
||||
return minimum;
|
||||
}
|
||||
|
||||
double maximum() {
|
||||
return maximum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
|
||||
private static int[] buildLookup(int[] stops) {
|
||||
int[] values = new int[256];
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
double position = (index / 255D) * (stops.length - 1);
|
||||
int lowerIndex = Math.min(stops.length - 2, (int) position);
|
||||
double fraction = position - lowerIndex;
|
||||
values[index] = interpolate(stops[lowerIndex], stops[lowerIndex + 1], fraction);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static Color[] buildDisplayColors(int[] lookup) {
|
||||
Color[] colors = new Color[lookup.length];
|
||||
for (int index = 0; index < lookup.length; index++) {
|
||||
colors[index] = new Color(lookup[index]);
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
private static int interpolate(int from, int to, double fraction) {
|
||||
int red = channel(from, 16, to, fraction);
|
||||
int green = channel(from, 8, to, fraction);
|
||||
int blue = channel(from, 0, to, fraction);
|
||||
return (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
private static int channel(int from, int shift, int to, double fraction) {
|
||||
int fromChannel = (from >>> shift) & 0xFF;
|
||||
int toChannel = (to >>> shift) & 0xFF;
|
||||
return (int) Math.round(fromChannel + ((toChannel - fromChannel) * fraction));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
final class NoiseRenderCoordinator implements AutoCloseable {
|
||||
private final int workerCount;
|
||||
private final Listener listener;
|
||||
private final ThreadFactory workerThreadFactory;
|
||||
private final ThreadFactory coordinatorThreadFactory;
|
||||
private final AtomicReference<RenderGeneration> latestRequested = new AtomicReference<>();
|
||||
private final AtomicLong latestRevision = new AtomicLong();
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
NoiseRenderCoordinator(Listener listener) {
|
||||
this(Math.min(4, Math.max(1, Runtime.getRuntime().availableProcessors() / 2)), listener);
|
||||
}
|
||||
|
||||
NoiseRenderCoordinator(int workerCount, Listener listener) {
|
||||
this.workerCount = Math.max(1, workerCount);
|
||||
this.listener = Objects.requireNonNull(listener, "listener");
|
||||
AtomicInteger threadIds = new AtomicInteger();
|
||||
workerThreadFactory = runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris Noise Renderer " + threadIds.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
};
|
||||
coordinatorThreadFactory = runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris Noise Coordinator " + threadIds.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
};
|
||||
}
|
||||
|
||||
void request(Request request) {
|
||||
Objects.requireNonNull(request, "request");
|
||||
if (closed.get()) {
|
||||
return;
|
||||
}
|
||||
latestRevision.accumulateAndGet(request.revision(), Math::max);
|
||||
cancelActiveRender();
|
||||
RenderGeneration generation = createGeneration(request);
|
||||
latestRequested.set(generation);
|
||||
generation.start();
|
||||
}
|
||||
|
||||
void cancel(long revision) {
|
||||
latestRevision.accumulateAndGet(revision, Math::max);
|
||||
cancelActiveRender();
|
||||
}
|
||||
|
||||
boolean isClosed() {
|
||||
return closed.get();
|
||||
}
|
||||
|
||||
static int sampleStepForBudget(int width, int height, long sampleBudget) {
|
||||
if (width < 1 || height < 1 || sampleBudget < 1L) {
|
||||
throw new IllegalArgumentException("Invalid noise sample budget");
|
||||
}
|
||||
long totalSamples = (long) width * height;
|
||||
int sampleStep = Math.max(1, (int) Math.ceil(Math.sqrt(totalSamples / (double) sampleBudget)));
|
||||
while (sampleCount(width, height, sampleStep) > sampleBudget) {
|
||||
sampleStep++;
|
||||
}
|
||||
return sampleStep;
|
||||
}
|
||||
|
||||
static long sampleCount(int width, int height, int sampleStep) {
|
||||
if (width < 1 || height < 1 || sampleStep < 1) {
|
||||
throw new IllegalArgumentException("Invalid noise sample dimensions");
|
||||
}
|
||||
long outputWidth = ((long) width + sampleStep - 1L) / sampleStep;
|
||||
long outputHeight = ((long) height + sampleStep - 1L) / sampleStep;
|
||||
return outputWidth * outputHeight;
|
||||
}
|
||||
|
||||
static int nextRefinementStep(int width, int height, int currentStep, long sampleBudget) {
|
||||
if (currentStep <= 1) {
|
||||
throw new IllegalArgumentException("Noise refinement requires a coarse input");
|
||||
}
|
||||
return Math.min(currentStep, sampleStepForBudget(width, height, sampleBudget));
|
||||
}
|
||||
|
||||
static long timeBoundSampleBudget(long completedSamples, double milliseconds, double targetMilliseconds,
|
||||
long minimumBudget, long maximumBudget) {
|
||||
if (completedSamples < 1L
|
||||
|| !Double.isFinite(milliseconds)
|
||||
|| milliseconds <= 0D
|
||||
|| !Double.isFinite(targetMilliseconds)
|
||||
|| targetMilliseconds <= 0D
|
||||
|| minimumBudget < 1L
|
||||
|| maximumBudget < minimumBudget) {
|
||||
throw new IllegalArgumentException("Invalid timed noise sample budget");
|
||||
}
|
||||
double projectedSamples = Math.ceil((completedSamples / milliseconds) * targetMilliseconds);
|
||||
long timeBoundBudget = (long) Math.min(maximumBudget, projectedSamples);
|
||||
return Math.max(minimumBudget, timeBoundBudget);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
latestRevision.incrementAndGet();
|
||||
cancelActiveRender();
|
||||
}
|
||||
|
||||
private void renderGeneration(RenderGeneration generation) {
|
||||
Request request = generation.request();
|
||||
if (!isCurrent(request)) {
|
||||
generation.close();
|
||||
return;
|
||||
}
|
||||
listener.onRenderStarted(request);
|
||||
try {
|
||||
Result result = render(generation);
|
||||
if (result != null && isCurrent(request)) {
|
||||
listener.onRenderCompleted(result);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
if (isCurrent(request)) {
|
||||
listener.onRenderFailed(request, error);
|
||||
}
|
||||
} finally {
|
||||
latestRequested.compareAndSet(generation, null);
|
||||
generation.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Result render(RenderGeneration generation) throws InterruptedException {
|
||||
Request request = generation.request();
|
||||
long started = System.nanoTime();
|
||||
int outputWidth = Math.max(1, (request.width() + request.sampleStep() - 1) / request.sampleStep());
|
||||
int outputHeight = Math.max(1, (request.height() + request.sampleStep() - 1) / request.sampleStep());
|
||||
BufferedImage image = new BufferedImage(outputWidth, outputHeight, BufferedImage.TYPE_INT_RGB);
|
||||
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||
int bandCount = Math.min(workerCount, outputHeight);
|
||||
BandStats[] bandStats = new BandStats[bandCount];
|
||||
CountDownLatch completion = new CountDownLatch(bandCount);
|
||||
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||
for (int band = 0; band < bandCount; band++) {
|
||||
bandStats[band] = new BandStats();
|
||||
}
|
||||
for (int band = 0; band < bandCount; band++) {
|
||||
int bandIndex = band;
|
||||
Runnable task = () -> {
|
||||
try {
|
||||
renderBand(request, image, pixels, bandIndex, bandCount, bandStats[bandIndex]);
|
||||
} catch (Throwable error) {
|
||||
failure.compareAndSet(null, error);
|
||||
} finally {
|
||||
completion.countDown();
|
||||
}
|
||||
};
|
||||
try {
|
||||
generation.executor().execute(task);
|
||||
} catch (RejectedExecutionException exception) {
|
||||
failure.compareAndSet(null, exception);
|
||||
completion.countDown();
|
||||
}
|
||||
}
|
||||
completion.await();
|
||||
Throwable renderFailure = failure.get();
|
||||
if (renderFailure != null) {
|
||||
if (renderFailure instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (renderFailure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException("Noise rendering failed", renderFailure);
|
||||
}
|
||||
if (!isCurrent(request)) {
|
||||
return null;
|
||||
}
|
||||
RenderStats combined = combine(bandStats);
|
||||
double milliseconds = (System.nanoTime() - started) / 1_000_000D;
|
||||
return new Result(request, image, milliseconds, combined.samples, combined.minimum, combined.maximum,
|
||||
combined.centerValue, combined.underflow, combined.overflow, combined.invalid);
|
||||
}
|
||||
|
||||
private void renderBand(Request request, BufferedImage image, int[] pixels, int bandIndex, int bandCount,
|
||||
BandStats stats) {
|
||||
int outputWidth = image.getWidth();
|
||||
int outputHeight = image.getHeight();
|
||||
int fromY = (outputHeight * bandIndex) / bandCount;
|
||||
int toY = (outputHeight * (bandIndex + 1)) / bandCount;
|
||||
double sampleOffset = request.sampleStep() * 0.5D;
|
||||
double startWorldX = request.viewport().worldX(sampleOffset, request.width());
|
||||
double worldStep = request.viewport().blocksPerPixel() * request.sampleStep();
|
||||
NoisePalette palette = request.palette();
|
||||
double paletteMinimum = palette.minimum();
|
||||
double paletteMaximum = palette.maximum();
|
||||
for (int y = fromY; y < toY; y++) {
|
||||
double screenY = (y * (double) request.sampleStep()) + sampleOffset;
|
||||
double worldZ = request.viewport().worldZ(screenY, request.height());
|
||||
double worldX = startWorldX;
|
||||
int pixelIndex = y * outputWidth;
|
||||
boolean centerRow = y == outputHeight / 2;
|
||||
for (int x = 0; x < outputWidth; x++) {
|
||||
if ((x & 31) == 0 && (!isCurrent(request) || Thread.currentThread().isInterrupted())) {
|
||||
return;
|
||||
}
|
||||
double value = request.sampler().noise(worldX, worldZ);
|
||||
boolean center = centerRow && x == outputWidth / 2;
|
||||
if (Double.isFinite(value)) {
|
||||
pixels[pixelIndex++] = palette.colorFinite(value);
|
||||
stats.acceptFinite(value, center, paletteMinimum, paletteMaximum);
|
||||
} else {
|
||||
pixels[pixelIndex++] = NoisePalette.INVALID_COLOR;
|
||||
stats.acceptInvalid(value, center);
|
||||
}
|
||||
worldX += worldStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCurrent(Request request) {
|
||||
return !closed.get()
|
||||
&& latestRequested.get() != null
|
||||
&& latestRequested.get().request() == request
|
||||
&& request.revision() >= latestRevision.get();
|
||||
}
|
||||
|
||||
private void cancelActiveRender() {
|
||||
RenderGeneration generation = latestRequested.getAndSet(null);
|
||||
if (generation != null) {
|
||||
generation.close();
|
||||
}
|
||||
}
|
||||
|
||||
private RenderGeneration createGeneration(Request request) {
|
||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||
workerCount,
|
||||
workerCount,
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<>(Math.max(1, workerCount)),
|
||||
workerThreadFactory,
|
||||
new ThreadPoolExecutor.AbortPolicy()
|
||||
);
|
||||
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
|
||||
if (preservation != null) {
|
||||
preservation.register(executor);
|
||||
}
|
||||
RenderGeneration generation = new RenderGeneration(request, executor);
|
||||
Thread actualRunner = coordinatorThreadFactory.newThread(() -> renderGeneration(generation));
|
||||
generation.setRunner(actualRunner);
|
||||
if (preservation != null) {
|
||||
preservation.register(actualRunner);
|
||||
}
|
||||
return generation;
|
||||
}
|
||||
|
||||
private static RenderStats combine(BandStats[] bands) {
|
||||
long samples = 0L;
|
||||
long underflow = 0L;
|
||||
long overflow = 0L;
|
||||
long invalid = 0L;
|
||||
double minimum = Double.POSITIVE_INFINITY;
|
||||
double maximum = Double.NEGATIVE_INFINITY;
|
||||
double centerValue = Double.NaN;
|
||||
for (BandStats band : bands) {
|
||||
samples += band.samples;
|
||||
underflow += band.underflow;
|
||||
overflow += band.overflow;
|
||||
invalid += band.invalid;
|
||||
minimum = Math.min(minimum, band.minimum);
|
||||
maximum = Math.max(maximum, band.maximum);
|
||||
if (!Double.isNaN(band.centerValue)) {
|
||||
centerValue = band.centerValue;
|
||||
}
|
||||
}
|
||||
if (minimum == Double.POSITIVE_INFINITY) {
|
||||
minimum = Double.NaN;
|
||||
maximum = Double.NaN;
|
||||
}
|
||||
return new RenderStats(samples, minimum, maximum, centerValue, underflow, overflow, invalid);
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
void onRenderStarted(Request request);
|
||||
|
||||
void onRenderCompleted(Result result);
|
||||
|
||||
void onRenderFailed(Request request, Throwable error);
|
||||
}
|
||||
|
||||
record Request(long revision, NoiseProvider sampler, NoiseViewport viewport, NoisePalette palette,
|
||||
int width, int height, int sampleStep) {
|
||||
Request {
|
||||
Objects.requireNonNull(sampler, "sampler");
|
||||
Objects.requireNonNull(viewport, "viewport");
|
||||
Objects.requireNonNull(palette, "palette");
|
||||
if (revision < 0L || width < 1 || height < 1 || sampleStep < 1) {
|
||||
throw new IllegalArgumentException("Invalid noise render request");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record Result(Request request, BufferedImage image, double milliseconds, long samples, double minimum,
|
||||
double maximum, double centerValue, long underflow, long overflow, long invalid) {
|
||||
}
|
||||
|
||||
private static final class RenderGeneration {
|
||||
private final Request request;
|
||||
private final ThreadPoolExecutor executor;
|
||||
private volatile Thread runner;
|
||||
|
||||
private RenderGeneration(Request request, ThreadPoolExecutor executor) {
|
||||
this.request = Objects.requireNonNull(request, "request");
|
||||
this.executor = Objects.requireNonNull(executor, "executor");
|
||||
}
|
||||
|
||||
private Request request() {
|
||||
return request;
|
||||
}
|
||||
|
||||
private ThreadPoolExecutor executor() {
|
||||
return executor;
|
||||
}
|
||||
|
||||
private void setRunner(Thread runner) {
|
||||
this.runner = Objects.requireNonNull(runner, "runner");
|
||||
}
|
||||
|
||||
private void start() {
|
||||
runner.start();
|
||||
}
|
||||
|
||||
private void close() {
|
||||
Thread activeRunner = runner;
|
||||
if (activeRunner != null) {
|
||||
activeRunner.interrupt();
|
||||
}
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BandStats {
|
||||
private long samples;
|
||||
private long underflow;
|
||||
private long overflow;
|
||||
private long invalid;
|
||||
private double minimum = Double.POSITIVE_INFINITY;
|
||||
private double maximum = Double.NEGATIVE_INFINITY;
|
||||
private double centerValue = Double.NaN;
|
||||
|
||||
private void acceptFinite(double value, boolean center, double paletteMinimum, double paletteMaximum) {
|
||||
samples++;
|
||||
if (center) {
|
||||
centerValue = value;
|
||||
}
|
||||
minimum = Math.min(minimum, value);
|
||||
maximum = Math.max(maximum, value);
|
||||
if (value < paletteMinimum) {
|
||||
underflow++;
|
||||
} else if (value > paletteMaximum) {
|
||||
overflow++;
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptInvalid(double value, boolean center) {
|
||||
samples++;
|
||||
invalid++;
|
||||
if (center) {
|
||||
centerValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record RenderStats(long samples, double minimum, double maximum, double centerValue,
|
||||
long underflow, long overflow, long invalid) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
record NoiseViewport(double centerX, double centerZ, double blocksPerPixel) {
|
||||
static final double MIN_BLOCKS_PER_PIXEL = 0.0001D;
|
||||
static final double MAX_BLOCKS_PER_PIXEL = 1_000_000D;
|
||||
|
||||
NoiseViewport {
|
||||
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
|
||||
throw new IllegalArgumentException("Viewport center must be finite");
|
||||
}
|
||||
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
||||
throw new IllegalArgumentException("Viewport scale must be finite and positive");
|
||||
}
|
||||
}
|
||||
|
||||
double worldX(double screenX, int width) {
|
||||
return centerX + ((screenX - (width / 2D)) * blocksPerPixel);
|
||||
}
|
||||
|
||||
double worldZ(double screenZ, int height) {
|
||||
return centerZ + ((screenZ - (height / 2D)) * blocksPerPixel);
|
||||
}
|
||||
|
||||
NoiseViewport panPixels(double deltaX, double deltaZ) {
|
||||
return new NoiseViewport(
|
||||
centerX - (deltaX * blocksPerPixel),
|
||||
centerZ - (deltaZ * blocksPerPixel),
|
||||
blocksPerPixel
|
||||
);
|
||||
}
|
||||
|
||||
NoiseViewport zoomAt(double screenX, double screenZ, int width, int height, double factor) {
|
||||
if (!Double.isFinite(factor) || factor <= 0D) {
|
||||
throw new IllegalArgumentException("Zoom factor must be finite and positive");
|
||||
}
|
||||
double anchorX = worldX(screenX, width);
|
||||
double anchorZ = worldZ(screenZ, height);
|
||||
double nextScale = Math.max(MIN_BLOCKS_PER_PIXEL,
|
||||
Math.min(MAX_BLOCKS_PER_PIXEL, blocksPerPixel * factor));
|
||||
double nextCenterX = anchorX - ((screenX - (width / 2D)) * nextScale);
|
||||
double nextCenterZ = anchorZ - ((screenZ - (height / 2D)) * nextScale);
|
||||
return new NoiseViewport(nextCenterX, nextCenterZ, nextScale);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,626 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.engine.framework.render.IrisRenderer;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
final class VisionRenderController implements AutoCloseable {
|
||||
static final int TILE_PIXELS = 64;
|
||||
static final double MINIMUM_BLOCKS_PER_PIXEL = 1D;
|
||||
static final double MAXIMUM_BLOCKS_PER_PIXEL = 4_096D;
|
||||
|
||||
private static final int MAXIMUM_WORKERS = 3;
|
||||
private static final int MAXIMUM_VISIBLE_TILES = 32_768;
|
||||
private static final int RENDER_QUEUE_CAPACITY = 12;
|
||||
private static final int PROBE_QUEUE_CAPACITY = 1;
|
||||
private static final long CACHE_BYTES = 64L * 1024L * 1024L;
|
||||
|
||||
private final Runnable listener;
|
||||
private final int renderWorkerCount;
|
||||
private final ThreadPoolExecutor renderExecutor;
|
||||
private final ThreadPoolExecutor probeExecutor;
|
||||
private final WeightedTileCache cache;
|
||||
private final AtomicLong viewSequence;
|
||||
private final AtomicLong probeSequence;
|
||||
private final AtomicBoolean closed;
|
||||
private final AtomicBoolean publicationQueued;
|
||||
private final AtomicBoolean publicationDirty;
|
||||
private final Set<Future<?>> activeRenderTasks;
|
||||
private volatile Frame currentFrame;
|
||||
private volatile WorkState currentWork;
|
||||
private volatile CancellationToken currentToken;
|
||||
|
||||
VisionRenderController(Runnable listener) {
|
||||
this(listener, RuntimeOptions.production());
|
||||
}
|
||||
|
||||
VisionRenderController(Runnable listener, RuntimeOptions options) {
|
||||
this.listener = Objects.requireNonNull(listener, "listener");
|
||||
Objects.requireNonNull(options, "options");
|
||||
AtomicInteger threadSequence = new AtomicInteger();
|
||||
this.renderWorkerCount = options.workers();
|
||||
this.renderExecutor = new ThreadPoolExecutor(
|
||||
options.workers(),
|
||||
options.workers(),
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<>(options.renderQueueCapacity()),
|
||||
daemonFactory("Iris Vision Render", Thread.NORM_PRIORITY, threadSequence),
|
||||
new ThreadPoolExecutor.AbortPolicy()
|
||||
);
|
||||
this.probeExecutor = new ThreadPoolExecutor(
|
||||
1,
|
||||
1,
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<>(PROBE_QUEUE_CAPACITY),
|
||||
daemonFactory("Iris Vision Probe", Thread.MIN_PRIORITY, threadSequence),
|
||||
new ThreadPoolExecutor.DiscardOldestPolicy()
|
||||
);
|
||||
this.cache = new WeightedTileCache(CACHE_BYTES);
|
||||
this.viewSequence = new AtomicLong();
|
||||
this.probeSequence = new AtomicLong();
|
||||
this.closed = new AtomicBoolean();
|
||||
this.publicationQueued = new AtomicBoolean();
|
||||
this.publicationDirty = new AtomicBoolean();
|
||||
this.activeRenderTasks = ConcurrentHashMap.newKeySet();
|
||||
if (options.registerPreservation()) {
|
||||
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
|
||||
if (preservation != null) {
|
||||
preservation.register(renderExecutor);
|
||||
preservation.register(probeExecutor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized Frame request(RenderSpec spec) {
|
||||
Objects.requireNonNull(spec, "spec");
|
||||
if (closed.get()) {
|
||||
throw new IllegalStateException("Vision render controller is closed");
|
||||
}
|
||||
|
||||
CancellationToken previousToken = currentToken;
|
||||
if (previousToken != null) {
|
||||
previousToken.cancel();
|
||||
}
|
||||
renderExecutor.getQueue().clear();
|
||||
cancelActiveRenderTasks();
|
||||
probeSequence.incrementAndGet();
|
||||
probeExecutor.getQueue().clear();
|
||||
|
||||
List<VisibleTile> tiles = visibleTiles(spec);
|
||||
Frame frame = new Frame(viewSequence.incrementAndGet(), spec, tiles);
|
||||
for (VisibleTile tile : tiles) {
|
||||
tile.setImage(cache.get(frame.key(tile)));
|
||||
}
|
||||
CancellationToken token = new CancellationToken();
|
||||
WorkState work = new WorkState(frame, token);
|
||||
currentFrame = frame;
|
||||
currentToken = token;
|
||||
currentWork = work;
|
||||
schedule(work);
|
||||
publish(frame, token);
|
||||
return frame;
|
||||
}
|
||||
|
||||
Frame currentFrame() {
|
||||
return currentFrame;
|
||||
}
|
||||
|
||||
BufferedImage image(Frame frame, VisibleTile tile) {
|
||||
if (frame == null || tile == null) {
|
||||
return null;
|
||||
}
|
||||
return tile.image();
|
||||
}
|
||||
|
||||
Progress progress(Frame frame) {
|
||||
if (frame == null) {
|
||||
return new Progress(0, 0, 0, 0);
|
||||
}
|
||||
int ready = 0;
|
||||
for (VisibleTile tile : frame.tiles()) {
|
||||
if (tile.image() != null) {
|
||||
ready++;
|
||||
}
|
||||
}
|
||||
return new Progress(frame.tiles().size(), ready, renderExecutor.getActiveCount(), renderExecutor.getQueue().size());
|
||||
}
|
||||
|
||||
<T> void submitProbe(Frame frame, Callable<T> probe, Consumer<T> consumer) {
|
||||
Objects.requireNonNull(probe, "probe");
|
||||
Objects.requireNonNull(consumer, "consumer");
|
||||
if (frame == null || closed.get() || currentFrame != frame) {
|
||||
return;
|
||||
}
|
||||
long probeRevision = probeSequence.incrementAndGet();
|
||||
probeExecutor.getQueue().clear();
|
||||
try {
|
||||
probeExecutor.execute(() -> runProbe(frame, probeRevision, probe, consumer));
|
||||
} catch (RejectedExecutionException ignored) {
|
||||
probeExecutor.getQueue().clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
CancellationToken token = currentToken;
|
||||
if (token != null) {
|
||||
token.cancel();
|
||||
}
|
||||
currentWork = null;
|
||||
currentFrame = null;
|
||||
renderExecutor.getQueue().clear();
|
||||
cancelActiveRenderTasks();
|
||||
probeExecutor.getQueue().clear();
|
||||
renderExecutor.shutdownNow();
|
||||
probeExecutor.shutdownNow();
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
static List<VisibleTile> visibleTiles(RenderSpec spec) {
|
||||
Objects.requireNonNull(spec, "spec");
|
||||
double blocksPerPixel = spec.blocksPerPixel();
|
||||
double tileSpan = TILE_PIXELS * blocksPerPixel;
|
||||
double halfWidth = spec.width() * blocksPerPixel * 0.5D;
|
||||
double halfHeight = spec.height() * blocksPerPixel * 0.5D;
|
||||
long minimumX = floorTile(spec.centerX() - halfWidth, tileSpan);
|
||||
long maximumX = floorTile(Math.nextDown(spec.centerX() + halfWidth), tileSpan);
|
||||
long minimumZ = floorTile(spec.centerZ() - halfHeight, tileSpan);
|
||||
long maximumZ = floorTile(Math.nextDown(spec.centerZ() + halfHeight), tileSpan);
|
||||
long tileCount = Math.multiplyExact(maximumX - minimumX + 1L, maximumZ - minimumZ + 1L);
|
||||
if (tileCount > MAXIMUM_VISIBLE_TILES) {
|
||||
throw new IllegalArgumentException("Vision viewport contains too many tiles");
|
||||
}
|
||||
|
||||
ArrayList<VisibleTile> tiles = new ArrayList<>((int) tileCount);
|
||||
double centerTileX = spec.centerX() / tileSpan;
|
||||
double centerTileZ = spec.centerZ() / tileSpan;
|
||||
for (long tileZ = minimumZ; ; tileZ++) {
|
||||
for (long tileX = minimumX; ; tileX++) {
|
||||
int screenX = (int) Math.round(spec.width() * 0.5D + (tileX * tileSpan - spec.centerX()) / blocksPerPixel);
|
||||
int screenY = (int) Math.round(spec.height() * 0.5D + (tileZ * tileSpan - spec.centerZ()) / blocksPerPixel);
|
||||
double deltaX = tileX + 0.5D - centerTileX;
|
||||
double deltaZ = tileZ + 0.5D - centerTileZ;
|
||||
tiles.add(new VisibleTile(tileX, tileZ, screenX, screenY, deltaX * deltaX + deltaZ * deltaZ));
|
||||
if (tileX == maximumX) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tileZ == maximumZ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
tiles.sort(Comparator.comparingDouble(VisibleTile::distanceSquared)
|
||||
.thenComparingLong(VisibleTile::tileZ)
|
||||
.thenComparingLong(VisibleTile::tileX));
|
||||
return List.copyOf(tiles);
|
||||
}
|
||||
|
||||
static long sampleCount(int tileCount) {
|
||||
if (tileCount < 1) {
|
||||
throw new IllegalArgumentException("Vision tile count must be positive");
|
||||
}
|
||||
return Math.multiplyExact((long) tileCount, (long) TILE_PIXELS * TILE_PIXELS);
|
||||
}
|
||||
|
||||
private static long floorTile(double coordinate, double tileSpan) {
|
||||
double tile = Math.floor(coordinate / tileSpan);
|
||||
if (tile < Long.MIN_VALUE || tile > Long.MAX_VALUE) {
|
||||
throw new IllegalArgumentException("Vision viewport exceeds tile coordinate range");
|
||||
}
|
||||
return (long) tile;
|
||||
}
|
||||
|
||||
private static ThreadFactory daemonFactory(String name, int priority, AtomicInteger sequence) {
|
||||
return (Runnable runnable) -> {
|
||||
Thread thread = new Thread(runnable, name + " " + sequence.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(priority);
|
||||
thread.setUncaughtExceptionHandler((Thread failedThread, Throwable error) -> IrisLogging.reportError(error));
|
||||
return thread;
|
||||
};
|
||||
}
|
||||
|
||||
private void schedule(WorkState work) {
|
||||
synchronized (work) {
|
||||
if (!isCurrent(work)) {
|
||||
return;
|
||||
}
|
||||
int admissionLimit = work.frame().spec().type() == RenderType.RIVER ? 1 : renderWorkerCount;
|
||||
while (work.inFlight() < admissionLimit) {
|
||||
VisibleTile tile = work.nextMissing();
|
||||
if (tile == null) {
|
||||
return;
|
||||
}
|
||||
work.incrementInFlight();
|
||||
TrackedRenderTask task = new TrackedRenderTask(() -> render(work, tile), activeRenderTasks);
|
||||
activeRenderTasks.add(task);
|
||||
try {
|
||||
renderExecutor.execute(task);
|
||||
} catch (RejectedExecutionException ignored) {
|
||||
task.cancel(false);
|
||||
work.decrementInFlight();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void render(WorkState work, VisibleTile tile) {
|
||||
Frame frame = work.frame();
|
||||
CancellationToken token = work.token();
|
||||
try {
|
||||
if (!isCurrent(frame, token)) {
|
||||
return;
|
||||
}
|
||||
double tileSpan = TILE_PIXELS * frame.spec().blocksPerPixel();
|
||||
BufferedImage image = frame.spec().renderer().renderStudio(
|
||||
tile.tileX() * tileSpan,
|
||||
tile.tileZ() * tileSpan,
|
||||
tileSpan,
|
||||
TILE_PIXELS,
|
||||
frame.spec().type(),
|
||||
() -> !isCurrent(frame, token)
|
||||
);
|
||||
if (!isCurrent(frame, token)) {
|
||||
return;
|
||||
}
|
||||
cache.put(frame.key(tile), image);
|
||||
tile.setImage(image);
|
||||
publish(frame, token);
|
||||
} catch (CancellationException ignored) {
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.debug("Vision tile render failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
|
||||
} finally {
|
||||
complete(work);
|
||||
}
|
||||
}
|
||||
|
||||
private void complete(WorkState work) {
|
||||
synchronized (work) {
|
||||
work.decrementInFlight();
|
||||
if (!isCurrent(work)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
schedule(work);
|
||||
}
|
||||
|
||||
private <T> void runProbe(Frame frame, long probeRevision, Callable<T> probe, Consumer<T> consumer) {
|
||||
try {
|
||||
if (!isProbeCurrent(frame, probeRevision)) {
|
||||
return;
|
||||
}
|
||||
T result = probe.call();
|
||||
if (!isProbeCurrent(frame, probeRevision)) {
|
||||
return;
|
||||
}
|
||||
EventQueue.invokeLater(() -> {
|
||||
if (isProbeCurrent(frame, probeRevision)) {
|
||||
consumer.accept(result);
|
||||
}
|
||||
});
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.debug("Vision probe failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isProbeCurrent(Frame frame, long probeRevision) {
|
||||
return !closed.get() && currentFrame == frame && probeSequence.get() == probeRevision;
|
||||
}
|
||||
|
||||
private boolean isCurrent(Frame frame, CancellationToken token) {
|
||||
return !closed.get() && !token.cancelled() && currentFrame == frame;
|
||||
}
|
||||
|
||||
private boolean isCurrent(WorkState work) {
|
||||
return currentWork == work && isCurrent(work.frame(), work.token());
|
||||
}
|
||||
|
||||
private void publish(Frame frame, CancellationToken token) {
|
||||
if (!isCurrent(frame, token)) {
|
||||
return;
|
||||
}
|
||||
publicationDirty.set(true);
|
||||
queuePublication();
|
||||
}
|
||||
|
||||
private void queuePublication() {
|
||||
if (!publicationQueued.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
EventQueue.invokeLater(() -> {
|
||||
try {
|
||||
if (publicationDirty.getAndSet(false) && !closed.get()) {
|
||||
listener.run();
|
||||
}
|
||||
} finally {
|
||||
publicationQueued.set(false);
|
||||
if (publicationDirty.get() && !closed.get()) {
|
||||
queuePublication();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void cancelActiveRenderTasks() {
|
||||
for (Future<?> task : activeRenderTasks) {
|
||||
task.cancel(true);
|
||||
}
|
||||
activeRenderTasks.clear();
|
||||
}
|
||||
|
||||
record RenderSpec(
|
||||
IrisRenderer renderer,
|
||||
RenderType type,
|
||||
long contentRevision,
|
||||
double centerX,
|
||||
double centerZ,
|
||||
double blocksPerPixel,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
RenderSpec {
|
||||
Objects.requireNonNull(renderer, "renderer");
|
||||
Objects.requireNonNull(type, "type");
|
||||
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
|
||||
throw new IllegalArgumentException("Vision center must be finite");
|
||||
}
|
||||
if (!Double.isFinite(blocksPerPixel)
|
||||
|| blocksPerPixel < MINIMUM_BLOCKS_PER_PIXEL
|
||||
|| blocksPerPixel > MAXIMUM_BLOCKS_PER_PIXEL) {
|
||||
throw new IllegalArgumentException("Vision scale is outside the supported range");
|
||||
}
|
||||
if (width < 1 || height < 1) {
|
||||
throw new IllegalArgumentException("Vision viewport must be positive");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record Frame(long viewRevision, RenderSpec spec, List<VisibleTile> tiles) {
|
||||
Frame {
|
||||
Objects.requireNonNull(spec, "spec");
|
||||
tiles = List.copyOf(tiles);
|
||||
}
|
||||
|
||||
TileKey key(VisibleTile tile) {
|
||||
return new TileKey(spec.contentRevision(), spec.type(), spec.blocksPerPixel(), tile.tileX(), tile.tileZ());
|
||||
}
|
||||
}
|
||||
|
||||
static final class VisibleTile {
|
||||
private final long tileX;
|
||||
private final long tileZ;
|
||||
private final int screenX;
|
||||
private final int screenY;
|
||||
private final double distanceSquared;
|
||||
private volatile BufferedImage image;
|
||||
|
||||
private VisibleTile(long tileX, long tileZ, int screenX, int screenY, double distanceSquared) {
|
||||
this.tileX = tileX;
|
||||
this.tileZ = tileZ;
|
||||
this.screenX = screenX;
|
||||
this.screenY = screenY;
|
||||
this.distanceSquared = distanceSquared;
|
||||
}
|
||||
|
||||
long tileX() {
|
||||
return tileX;
|
||||
}
|
||||
|
||||
long tileZ() {
|
||||
return tileZ;
|
||||
}
|
||||
|
||||
int screenX() {
|
||||
return screenX;
|
||||
}
|
||||
|
||||
int screenY() {
|
||||
return screenY;
|
||||
}
|
||||
|
||||
double distanceSquared() {
|
||||
return distanceSquared;
|
||||
}
|
||||
|
||||
BufferedImage image() {
|
||||
return image;
|
||||
}
|
||||
|
||||
void setImage(BufferedImage image) {
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
|
||||
record TileKey(long contentRevision, RenderType type, double blocksPerPixel, long tileX, long tileZ) {
|
||||
TileKey {
|
||||
Objects.requireNonNull(type, "type");
|
||||
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
||||
throw new IllegalArgumentException("Vision cache scale must be finite and positive");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record Progress(int total, int ready, int active, int queued) {
|
||||
double completion() {
|
||||
if (total == 0) {
|
||||
return 1D;
|
||||
}
|
||||
return Math.min(1D, ready / (double) total);
|
||||
}
|
||||
}
|
||||
|
||||
record RuntimeOptions(int workers, int renderQueueCapacity, boolean registerPreservation) {
|
||||
RuntimeOptions {
|
||||
if (workers < 1 || renderQueueCapacity < 1) {
|
||||
throw new IllegalArgumentException("Vision render runtime options are invalid");
|
||||
}
|
||||
}
|
||||
|
||||
static RuntimeOptions production() {
|
||||
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
|
||||
int workers = Math.max(1, Math.min(MAXIMUM_WORKERS, processors));
|
||||
return new RuntimeOptions(workers, RENDER_QUEUE_CAPACITY, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CancellationToken {
|
||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
||||
|
||||
void cancel() {
|
||||
cancelled.set(true);
|
||||
}
|
||||
|
||||
boolean cancelled() {
|
||||
return cancelled.get();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TrackedRenderTask extends FutureTask<Void> {
|
||||
private final Set<Future<?>> tasks;
|
||||
|
||||
private TrackedRenderTask(Runnable task, Set<Future<?>> tasks) {
|
||||
super(task, null);
|
||||
this.tasks = tasks;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
tasks.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WorkState {
|
||||
private final Frame frame;
|
||||
private final CancellationToken token;
|
||||
private int index;
|
||||
private int inFlight;
|
||||
|
||||
private WorkState(Frame frame, CancellationToken token) {
|
||||
this.frame = frame;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
Frame frame() {
|
||||
return frame;
|
||||
}
|
||||
|
||||
CancellationToken token() {
|
||||
return token;
|
||||
}
|
||||
|
||||
VisibleTile nextMissing() {
|
||||
while (index < frame.tiles().size()) {
|
||||
VisibleTile tile = frame.tiles().get(index++);
|
||||
if (tile.image() == null) {
|
||||
return tile;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int inFlight() {
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
void incrementInFlight() {
|
||||
inFlight++;
|
||||
}
|
||||
|
||||
void decrementInFlight() {
|
||||
inFlight--;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WeightedTileCache {
|
||||
private final long maximumBytes;
|
||||
private final LinkedHashMap<TileKey, CacheEntry> entries;
|
||||
private long bytes;
|
||||
|
||||
private WeightedTileCache(long maximumBytes) {
|
||||
this.maximumBytes = maximumBytes;
|
||||
this.entries = new LinkedHashMap<>(128, 0.75F, true);
|
||||
}
|
||||
|
||||
synchronized BufferedImage get(TileKey key) {
|
||||
CacheEntry entry = entries.get(key);
|
||||
return entry == null ? null : entry.image();
|
||||
}
|
||||
|
||||
synchronized void put(TileKey key, BufferedImage image) {
|
||||
long imageBytes = (long) image.getWidth() * image.getHeight() * Integer.BYTES;
|
||||
CacheEntry previous = entries.put(key, new CacheEntry(image, imageBytes));
|
||||
if (previous != null) {
|
||||
bytes -= previous.bytes();
|
||||
}
|
||||
bytes += imageBytes;
|
||||
while (bytes > maximumBytes && !entries.isEmpty()) {
|
||||
Iterator<Map.Entry<TileKey, CacheEntry>> iterator = entries.entrySet().iterator();
|
||||
Map.Entry<TileKey, CacheEntry> eldest = iterator.next();
|
||||
bytes -= eldest.getValue().bytes();
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void clear() {
|
||||
entries.clear();
|
||||
bytes = 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private record CacheEntry(BufferedImage image, long bytes) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
record VisionViewport(double centerX, double centerZ, double blocksPerPixel) {
|
||||
VisionViewport {
|
||||
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
|
||||
throw new IllegalArgumentException("Vision viewport center must be finite");
|
||||
}
|
||||
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
||||
throw new IllegalArgumentException("Vision viewport scale must be finite and positive");
|
||||
}
|
||||
}
|
||||
|
||||
double worldX(double screenX, int width) {
|
||||
return centerX + (screenX - width * 0.5D) * blocksPerPixel;
|
||||
}
|
||||
|
||||
double worldZ(double screenZ, int height) {
|
||||
return centerZ + (screenZ - height * 0.5D) * blocksPerPixel;
|
||||
}
|
||||
|
||||
VisionViewport zoomAt(
|
||||
double screenX,
|
||||
double screenZ,
|
||||
int width,
|
||||
int height,
|
||||
double factor,
|
||||
double minimumScale,
|
||||
double maximumScale
|
||||
) {
|
||||
if (!Double.isFinite(factor) || factor <= 0D) {
|
||||
throw new IllegalArgumentException("Vision zoom factor must be finite and positive");
|
||||
}
|
||||
double anchorX = worldX(screenX, width);
|
||||
double anchorZ = worldZ(screenZ, height);
|
||||
double nextScale = Math.max(minimumScale, Math.min(maximumScale, blocksPerPixel * factor));
|
||||
return new VisionViewport(
|
||||
anchorX - (screenX - width * 0.5D) * nextScale,
|
||||
anchorZ - (screenZ - height * 0.5D) * nextScale,
|
||||
nextScale
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_VIEW = TextKey.of("iris.desktop.vision.view", "View:");
|
||||
public static final TextKey VISION_GRID = TextKey.of("iris.desktop.vision.grid", "Grid");
|
||||
public static final TextKey VISION_FOLLOW = TextKey.of("iris.desktop.vision.follow", "Follow");
|
||||
public static final TextKey VISION_LOW_QUALITY_SHORT = TextKey.of("iris.desktop.vision.low_quality_short", "LQ");
|
||||
public static final TextKey VISION_REFRESHING = TextKey.of("iris.desktop.vision.refreshing", "Refreshing");
|
||||
public static final TextKey VISION_FPS = TextKey.of("iris.desktop.vision.fps", "{fps} FPS");
|
||||
public static final TextKey VISION_ZOOM_RESET = TextKey.of("iris.desktop.vision.zoom_reset", "Zoom reset");
|
||||
@@ -19,8 +18,6 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_FOLLOWING = TextKey.of("iris.desktop.vision.following", "Following {player}");
|
||||
public static final TextKey VISION_NO_PLAYER = TextKey.of("iris.desktop.vision.no_player", "No player in world");
|
||||
public static final TextKey VISION_FOLLOW_DISABLED = TextKey.of("iris.desktop.vision.follow_disabled", "Follow disabled");
|
||||
public static final TextKey VISION_LOW_QUALITY = TextKey.of("iris.desktop.vision.low_quality", "Low quality");
|
||||
public static final TextKey VISION_HIGH_QUALITY = TextKey.of("iris.desktop.vision.high_quality", "High quality");
|
||||
public static final TextKey VISION_STATUS_LEFT = TextKey.of("iris.desktop.vision.status_left", "{mode} | {bpp} bpp | {width} x {height} blocks");
|
||||
public static final TextKey VISION_STATUS_RIGHT = TextKey.of("iris.desktop.vision.status_right", "X: {x} Z: {z} | {fps} FPS");
|
||||
public static final TextKey VISION_ENTITY_POSITION = TextKey.of("iris.desktop.vision.entity_position", "Position: {x}, {y}, {z}");
|
||||
@@ -31,8 +28,8 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_BIOME_KEY = TextKey.of("iris.desktop.vision.biome_key", "Key: {key}");
|
||||
public static final TextKey VISION_BIOME_FILE = TextKey.of("iris.desktop.vision.biome_file", "File: {file}");
|
||||
public static final TextKey VISION_VELOCITY = TextKey.of("iris.desktop.vision.velocity", "Velocity: {velocity}");
|
||||
public static final TextKey VISION_TILES = TextKey.of("iris.desktop.vision.tiles", "Tiles: {high} HD / {low} LQ");
|
||||
public static final TextKey VISION_WORKERS = TextKey.of("iris.desktop.vision.workers", "Workers: {high} HD / {low} LQ");
|
||||
public static final TextKey VISION_TILES = TextKey.of("iris.desktop.vision.tiles", "Atlas pages: {ready} / {total} exact");
|
||||
public static final TextKey VISION_WORKERS = TextKey.of("iris.desktop.vision.workers", "Workers: {active} active / {queued} queued");
|
||||
public static final TextKey VISION_CENTER = TextKey.of("iris.desktop.vision.center", "Center: {x}, {z}");
|
||||
public static final TextKey VISION_HELP_TOGGLE = TextKey.of("iris.desktop.vision.help.toggle", "Toggle help");
|
||||
public static final TextKey VISION_HELP_REFRESH = TextKey.of("iris.desktop.vision.help.refresh", "Refresh tiles");
|
||||
@@ -40,7 +37,6 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_HELP_ZOOM = TextKey.of("iris.desktop.vision.help.zoom", "Zoom in/out");
|
||||
public static final TextKey VISION_HELP_RESET_ZOOM = TextKey.of("iris.desktop.vision.help.reset_zoom", "Reset zoom");
|
||||
public static final TextKey VISION_HELP_CYCLE_MODE = TextKey.of("iris.desktop.vision.help.cycle_mode", "Cycle render mode");
|
||||
public static final TextKey VISION_HELP_QUALITY = TextKey.of("iris.desktop.vision.help.quality", "Toggle tile quality");
|
||||
public static final TextKey VISION_HELP_FPS = TextKey.of("iris.desktop.vision.help.fps", "Toggle 30/60 FPS");
|
||||
public static final TextKey VISION_HELP_GRID = TextKey.of("iris.desktop.vision.help.grid", "Toggle grid");
|
||||
public static final TextKey VISION_HELP_BIOME = TextKey.of("iris.desktop.vision.help.biome", "Detailed biome info");
|
||||
@@ -53,6 +49,7 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_MODE_BIOME_SEA = TextKey.of("iris.desktop.vision.mode.biome_sea", "Biome sea");
|
||||
public static final TextKey VISION_MODE_REGION = TextKey.of("iris.desktop.vision.mode.region", "Region");
|
||||
public static final TextKey VISION_MODE_CAVE_LAND = TextKey.of("iris.desktop.vision.mode.cave_land", "Cave land");
|
||||
public static final TextKey VISION_MODE_RIVER = TextKey.of("iris.desktop.vision.mode.river", "River network");
|
||||
public static final TextKey VISION_MODE_HEIGHT = TextKey.of("iris.desktop.vision.mode.height", "Height");
|
||||
public static final TextKey VISION_MODE_OBJECT_LOAD = TextKey.of("iris.desktop.vision.mode.object_load", "Object load");
|
||||
public static final TextKey VISION_MODE_DECORATOR_LOAD = TextKey.of("iris.desktop.vision.mode.decorator_load", "Decorator load");
|
||||
@@ -95,17 +92,17 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey PREGEN_MEMORY = TextKey.of("iris.desktop.pregen.memory", "Memory: {used} ({usage}) Pressure: {pressure}/s");
|
||||
|
||||
private static final List<MessageKey> KEYS = List.of(
|
||||
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_FOLLOW, VISION_LOW_QUALITY_SHORT,
|
||||
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_FOLLOW,
|
||||
VISION_REFRESHING, VISION_FPS, VISION_ZOOM_RESET, VISION_GRID_ENABLED, VISION_GRID_DISABLED,
|
||||
VISION_FOLLOWING, VISION_NO_PLAYER, VISION_FOLLOW_DISABLED, VISION_LOW_QUALITY, VISION_HIGH_QUALITY,
|
||||
VISION_FOLLOWING, VISION_NO_PLAYER, VISION_FOLLOW_DISABLED,
|
||||
VISION_STATUS_LEFT, VISION_STATUS_RIGHT, VISION_ENTITY_POSITION, VISION_ENTITY_HEALTH,
|
||||
VISION_BLOCK_POSITION, VISION_CHUNK_POSITION, VISION_REGION_POSITION, VISION_BIOME_KEY,
|
||||
VISION_BIOME_FILE, VISION_VELOCITY, VISION_TILES, VISION_WORKERS, VISION_CENTER,
|
||||
VISION_HELP_TOGGLE, VISION_HELP_REFRESH, VISION_HELP_FOLLOW, VISION_HELP_ZOOM,
|
||||
VISION_HELP_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_QUALITY, VISION_HELP_FPS,
|
||||
VISION_HELP_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_FPS,
|
||||
VISION_HELP_GRID, VISION_HELP_BIOME, VISION_HELP_TELEPORT, VISION_HELP_EDITOR, VISION_OPENED,
|
||||
VISION_TELEPORTING, VISION_MODE_BIOME, VISION_MODE_BIOME_LAND, VISION_MODE_BIOME_SEA,
|
||||
VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD,
|
||||
VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_RIVER, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD,
|
||||
VISION_MODE_DECORATOR_LOAD, VISION_MODE_CONTINENT, VISION_MODE_LAYER_LOAD, NOISE_TITLE,
|
||||
NOISE_TITLE_GENERATOR, NOISE_SEARCH, NOISE_STATUS, NOISE_CATEGORY_CUSTOM,
|
||||
NOISE_CATEGORY_PACK_GENERATORS, NOISE_CATEGORY_SIMPLEX, NOISE_CATEGORY_PERLIN,
|
||||
|
||||
@@ -239,7 +239,7 @@ public interface INMSBinding {
|
||||
IrisImportedStructureControl importedStructures
|
||||
) throws NoSuchFieldException, IllegalAccessException;
|
||||
|
||||
void completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException;
|
||||
CompletableFuture<Void> completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException;
|
||||
|
||||
void abandonStudioStructureBootstrap(World world);
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import org.bukkit.inventory.ItemStack;
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class NMSBinding1X implements INMSBinding {
|
||||
@@ -117,7 +118,8 @@ public class NMSBinding1X implements INMSBinding {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeStudioStructureBootstrap(World world) {
|
||||
public CompletableFuture<Void> completeStudioStructureBootstrap(World world) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,876 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.river.RiverTopologyComplexity;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
final class PackRiverValidator {
|
||||
private static final Set<String> WATER_MODES = Set.of("SEA_LEVEL", "TERRACED");
|
||||
private static final Set<String> TERMINAL_MODES = Set.of("SUPPRESS", "DRY_CHANNEL", "SINKHOLE_GROTTO");
|
||||
private static final Set<String> ROUTING_POLICIES = Set.of("ALLOW", "AVOID", "BLOCK");
|
||||
private static final Set<String> CAVE_MODES = Set.of(
|
||||
"SEALED",
|
||||
"FLOOD_CLOSED_COMPONENT",
|
||||
"GENERATE_GROTTO",
|
||||
"GROTTO_OR_CLOSED_COMPONENT",
|
||||
"WATERFALL_POOL"
|
||||
);
|
||||
private static final Set<String> CAVE_FALLBACKS = Set.of("SEALED", "GENERATE_GROTTO");
|
||||
private static final Set<String> EXISTING_FLUID_POLICIES = Set.of("REJECT", "ALLOW_SAME", "REPLACE");
|
||||
private static final Set<String> UNSAFE_RIVER_STREAMS = Set.of("HEIGHT", "HEIGHT_OR_FLUID", "SLOPE");
|
||||
private static final Set<String> NOISE_STYLES = noiseStyles();
|
||||
|
||||
private PackRiverValidator() {
|
||||
}
|
||||
|
||||
static Validation validate(File packFolder, File[] dimensionFiles) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
if (packFolder == null || !packFolder.isDirectory() || dimensionFiles == null) {
|
||||
return new Validation(errors, warnings);
|
||||
}
|
||||
|
||||
boolean enabled = false;
|
||||
List<DimensionRiverContext> contexts = new ArrayList<>();
|
||||
List<File> sortedDimensions = new ArrayList<>(List.of(dimensionFiles));
|
||||
sortedDimensions.sort(Comparator.comparing(File::getPath));
|
||||
for (File dimensionFile : sortedDimensions) {
|
||||
JSONObject dimension = PackValidationIo.readJson(dimensionFile);
|
||||
if (dimension == null || !dimension.has("rivers")) {
|
||||
continue;
|
||||
}
|
||||
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
|
||||
String path = "Dimension '" + dimensionKey + "' rivers";
|
||||
JSONObject rivers = requireObject(dimension, "rivers", path, errors);
|
||||
if (rivers == null) {
|
||||
continue;
|
||||
}
|
||||
PackJsonFieldChecks.validateOptionalBoolean(path, rivers, "enabled", errors);
|
||||
if (!booleanValue(rivers, "enabled", false)) {
|
||||
continue;
|
||||
}
|
||||
enabled = true;
|
||||
DimensionRiverContext context = new DimensionRiverContext(
|
||||
dimensionKey,
|
||||
dimension,
|
||||
rivers,
|
||||
referencedKeys(dimension.optJSONArray("regions"))
|
||||
);
|
||||
contexts.add(context);
|
||||
validateNetwork(packFolder, path, context, errors, warnings);
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
validateOverrides(packFolder, new File(packFolder, "regions"), "Region", contexts, errors, warnings);
|
||||
validateOverrides(packFolder, new File(packFolder, "biomes"), "Biome", contexts, errors, warnings);
|
||||
}
|
||||
return new Validation(errors, warnings);
|
||||
}
|
||||
|
||||
private static void validateNetwork(File packFolder, String path, DimensionRiverContext context,
|
||||
List<String> errors, List<String> warnings) {
|
||||
JSONObject rivers = context.rivers();
|
||||
JSONObject topology = nestedObject(rivers, "topology", path, errors);
|
||||
JSONObject terrain = nestedObject(rivers, "terrain", path, errors);
|
||||
JSONObject water = nestedObject(rivers, "water", path, errors);
|
||||
JSONObject biomes = nestedObject(rivers, "biomes", path, errors);
|
||||
JSONObject caves = nestedObject(rivers, "caves", path, errors);
|
||||
|
||||
if (topology != null) {
|
||||
validateTopology(packFolder, path + ".topology", topology, errors, warnings);
|
||||
}
|
||||
if (terrain != null) {
|
||||
validateTerrain(packFolder, path + ".terrain", terrain, errors, warnings);
|
||||
}
|
||||
if (water != null) {
|
||||
validateWater(path + ".water", water, errors);
|
||||
}
|
||||
if (biomes != null) {
|
||||
validateBiomePools(packFolder, path + ".biomes", biomes, false, errors, warnings);
|
||||
}
|
||||
boolean sinkholeTerminal = terrain != null
|
||||
&& "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL"));
|
||||
if (caves != null) {
|
||||
validateCaves(packFolder, path + ".caves", caves, sinkholeTerminal, errors, warnings);
|
||||
}
|
||||
|
||||
if (topology != null && terrain != null) {
|
||||
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
|
||||
int cellSize = integerValue(topology, "cellSize", 512);
|
||||
if (Double.isFinite(meanderStrength) && meanderStrength > cellSize) {
|
||||
warnings.add(path + ".terrain.meanderStrength exceeds topology.cellSize; reaches may require large cache halos.");
|
||||
}
|
||||
validateTopologyComplexity(path, topology, terrain, errors);
|
||||
}
|
||||
if (sinkholeTerminal && caves != null) {
|
||||
validateSinkholeCapability(
|
||||
path + ".terrain.terminalMode",
|
||||
context,
|
||||
caves,
|
||||
path + ".caves",
|
||||
errors
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateTopology(File packFolder, String path, JSONObject topology,
|
||||
List<String> errors, List<String> warnings) {
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "cellSize", 64, 4096, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "tileCells", 1, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "siteJitter", 0D, 0.49D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "maxRouteReaches", 1, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "minimumSourcesPerTile", 0, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "sinkSearchReaches", 0, 7, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "routingBasinCells", 8, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingPlateauHeight", 1D, 64D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingNoiseWeight", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainHeightWeight", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainSlopeWeight", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "oceanAttraction", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalBoolean(path, topology, "requireOcean", errors);
|
||||
validateNoiseChance(packFolder, topology, "source", path, errors);
|
||||
validateNoiseChance(packFolder, topology, "continuation", path, errors);
|
||||
validateStyle(packFolder, topology, "routingStyle", path, errors);
|
||||
|
||||
int tileCells = integerValue(topology, "tileCells", 4);
|
||||
int minimumSourcesPerTile = integerValue(topology, "minimumSourcesPerTile", 0);
|
||||
if (tileCells >= 1 && tileCells <= 64
|
||||
&& minimumSourcesPerTile >= 0
|
||||
&& minimumSourcesPerTile > tileCells * tileCells) {
|
||||
errors.add(path + ".minimumSourcesPerTile must not exceed tileCells squared.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void validateTerrain(File packFolder, String path, JSONObject terrain,
|
||||
List<String> errors, List<String> warnings) {
|
||||
validateStyledRange(packFolder, terrain, "channelWidth", path, 1D, 2048D, errors, warnings);
|
||||
validateStyledRange(packFolder, terrain, "bankWidth", path, 0D, 2048D, errors, warnings);
|
||||
validateStyledRange(packFolder, terrain, "depth", path, 1D, 512D, errors, warnings);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxChannelWidth", 1D, 2048D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxBankWidth", 0D, 2048D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxDepth", 1D, 512D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "orderWidthFactor", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "orderDepthFactor", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "maxIncision", 0, 512, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bankExponent", 0.125D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "meanderStrength", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "meanderSubdivisions", 1, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bedRoughness", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, terrain, "terminalMode", TERMINAL_MODES, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "terminalTaper", 8, 1024, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "dryContinuationChance", 0D, 1D, errors);
|
||||
validateNoiseChance(packFolder, terrain, "incision", path, errors);
|
||||
validateStyle(packFolder, terrain, "meanderStyle", path, errors);
|
||||
validateStyle(packFolder, terrain, "bedRoughnessStyle", path, errors);
|
||||
}
|
||||
|
||||
private static void validateWater(String path, JSONObject water, List<String> errors) {
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, water, "mode", WATER_MODES, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "poolLength", 8, 4096, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "maximumPoolRise", 0, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "dropHeight", 1, 32, errors);
|
||||
|
||||
String mode = stringValue(water, "mode", "SEA_LEVEL");
|
||||
int maximumPoolRise = integerValue(water, "maximumPoolRise", 4);
|
||||
int dropHeight = integerValue(water, "dropHeight", 1);
|
||||
if ("TERRACED".equals(mode) && dropHeight > maximumPoolRise) {
|
||||
errors.add(path + ".dropHeight must not exceed maximumPoolRise in TERRACED mode.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateTopologyComplexity(
|
||||
String path,
|
||||
JSONObject topology,
|
||||
JSONObject terrain,
|
||||
List<String> errors
|
||||
) {
|
||||
int cellSize = integerValue(topology, "cellSize", 512);
|
||||
int tileCells = integerValue(topology, "tileCells", 4);
|
||||
double siteJitter = doubleValue(topology, "siteJitter", 0.35D);
|
||||
int maxRouteReaches = integerValue(topology, "maxRouteReaches", 16);
|
||||
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
|
||||
int meanderSubdivisions = integerValue(terrain, "meanderSubdivisions", 8);
|
||||
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
|
||||
double maximumBankWidth = doubleValue(terrain, "maxBankWidth", 4D);
|
||||
if (cellSize < 64 || cellSize > 4096
|
||||
|| tileCells < 1 || tileCells > 64
|
||||
|| !Double.isFinite(siteJitter) || siteJitter < 0D || siteJitter > 0.49D
|
||||
|| maxRouteReaches < 1 || maxRouteReaches > 256
|
||||
|| !Double.isFinite(meanderStrength) || meanderStrength < 0D || meanderStrength > 1024D
|
||||
|| meanderSubdivisions < 1 || meanderSubdivisions > 64
|
||||
|| !Double.isFinite(maximumChannelWidth) || maximumChannelWidth < 1D || maximumChannelWidth > 2048D
|
||||
|| !Double.isFinite(maximumBankWidth) || maximumBankWidth < 0D || maximumBankWidth > 2048D) {
|
||||
return;
|
||||
}
|
||||
double maximumReachRadius = maximumChannelWidth * 0.5D + maximumBankWidth;
|
||||
RiverTopologyComplexity.Estimate estimate = RiverTopologyComplexity.estimate(
|
||||
cellSize,
|
||||
tileCells,
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
maximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
);
|
||||
for (String violation : estimate.violations()) {
|
||||
errors.add(path + " exceeds the safe derived complexity budget. " + violation);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateCaves(File packFolder, String path, JSONObject caves,
|
||||
boolean forceGeneratedGrotto,
|
||||
List<String> errors, List<String> warnings) {
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, caves, "mode", CAVE_MODES, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "minimumSpacing", 16, 4096, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maximumPerReach", 0, 16, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxBoreDepth", 1, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "throatRadius", 1, 16, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "waterLevelOffset", -64, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "dryHeadroom", 0, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "grottoHorizontalRadius", 2, 128, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "grottoVerticalRadius", 2, 128, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, caves, "grottoWarpStrength", 0D, 32D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodRadius", 4, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodDepth", 4, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodVolume", 64, 1048576, errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, caves, "fallback", CAVE_FALLBACKS, errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, caves, "existingFluidPolicy", EXISTING_FLUID_POLICIES, errors);
|
||||
validateNoiseChance(packFolder, caves, "entry", path, errors);
|
||||
validateStyle(packFolder, caves, "grottoShapeStyle", path, errors);
|
||||
validateStyle(packFolder, caves, "grottoWarpStyle", path, errors);
|
||||
|
||||
String mode = stringValue(caves, "mode", "SEALED");
|
||||
if ("SEALED".equals(mode) && !forceGeneratedGrotto) {
|
||||
return;
|
||||
}
|
||||
|
||||
int maximumPerReach = integerValue(caves, "maximumPerReach", 1);
|
||||
double entryChance = noiseChanceValue(caves, "entry", 0.12D);
|
||||
if (maximumPerReach == 0 || (!forceGeneratedGrotto && entryChance == 0D)) {
|
||||
warnings.add(path + " enables cave hydrology but its entry gate cannot accept any connections.");
|
||||
}
|
||||
|
||||
int maxBoreDepth = integerValue(caves, "maxBoreDepth", 48);
|
||||
int throatRadius = integerValue(caves, "throatRadius", 2);
|
||||
int maxFloodRadius = integerValue(caves, "maxFloodRadius", 48);
|
||||
int maxFloodDepth = integerValue(caves, "maxFloodDepth", 32);
|
||||
if (throatRadius >= maxFloodRadius) {
|
||||
errors.add(path + ".throatRadius must be smaller than maxFloodRadius so the proof boundary can contain the throat.");
|
||||
}
|
||||
if (throatRadius >= maxFloodDepth) {
|
||||
errors.add(path + ".throatRadius must be smaller than maxFloodDepth so the proof boundary can contain the throat.");
|
||||
}
|
||||
if (maxBoreDepth > maxFloodDepth) {
|
||||
warnings.add(path + ".maxBoreDepth exceeds maxFloodDepth; deeper cave targets found by the bore search will be rejected by containment proof.");
|
||||
}
|
||||
|
||||
String fallback = stringValue(caves, "fallback", "SEALED");
|
||||
if (forceGeneratedGrotto || usesGeneratedGrotto(mode, fallback)) {
|
||||
validateGrotto(path, caves, errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateGrotto(String path, JSONObject caves, List<String> errors) {
|
||||
int throatRadius = integerValue(caves, "throatRadius", 2);
|
||||
int dryHeadroom = integerValue(caves, "dryHeadroom", 4);
|
||||
int horizontalRadius = integerValue(caves, "grottoHorizontalRadius", 12);
|
||||
int verticalRadius = integerValue(caves, "grottoVerticalRadius", 7);
|
||||
double warpStrength = doubleValue(caves, "grottoWarpStrength", 2D);
|
||||
int maxFloodRadius = integerValue(caves, "maxFloodRadius", 48);
|
||||
int maxFloodDepth = integerValue(caves, "maxFloodDepth", 32);
|
||||
int maxFloodVolume = integerValue(caves, "maxFloodVolume", 8192);
|
||||
if (throatRadius >= horizontalRadius || throatRadius >= verticalRadius) {
|
||||
addDistinct(errors, path + ".throatRadius must be smaller than both grotto radii so a sealed chamber can surround the inlet.");
|
||||
}
|
||||
if (dryHeadroom >= (verticalRadius * 2) + 1) {
|
||||
addDistinct(errors, path + ".dryHeadroom must fit inside the generated grotto height.");
|
||||
}
|
||||
|
||||
int warpEnvelope = Double.isFinite(warpStrength) ? (int) Math.ceil(warpStrength) : 0;
|
||||
int requiredRadius = horizontalRadius + warpEnvelope + 1;
|
||||
int requiredDepth = verticalRadius + warpEnvelope + 1;
|
||||
if (maxFloodRadius < requiredRadius) {
|
||||
addDistinct(errors, path + ".maxFloodRadius must be at least " + requiredRadius
|
||||
+ " to prove the configured grotto and its sealed shell.");
|
||||
}
|
||||
if (maxFloodDepth < requiredDepth) {
|
||||
addDistinct(errors, path + ".maxFloodDepth must be at least " + requiredDepth
|
||||
+ " to prove the configured grotto and its sealed shell.");
|
||||
}
|
||||
|
||||
long volume = grottoVolume(horizontalRadius, verticalRadius);
|
||||
if (volume > maxFloodVolume) {
|
||||
addDistinct(errors, path + ".maxFloodVolume must be at least " + volume
|
||||
+ " to contain the configured grotto before its throat and shell are considered.");
|
||||
}
|
||||
}
|
||||
|
||||
private static long grottoVolume(int horizontalRadius, int verticalRadius) {
|
||||
long volume = 0L;
|
||||
double horizontalSquared = (double) horizontalRadius * horizontalRadius;
|
||||
double verticalSquared = (double) verticalRadius * verticalRadius;
|
||||
for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) {
|
||||
for (int dy = -verticalRadius; dy <= verticalRadius; dy++) {
|
||||
double remaining = 1D - ((double) dx * dx / horizontalSquared)
|
||||
- ((double) dy * dy / verticalSquared);
|
||||
if (remaining < 0D) {
|
||||
continue;
|
||||
}
|
||||
int maximumZ = (int) Math.floor(horizontalRadius * Math.sqrt(remaining));
|
||||
volume += (maximumZ * 2L) + 1L;
|
||||
}
|
||||
}
|
||||
return volume;
|
||||
}
|
||||
|
||||
private static boolean usesGeneratedGrotto(String mode, String fallback) {
|
||||
return "GENERATE_GROTTO".equals(mode)
|
||||
|| "GROTTO_OR_CLOSED_COMPONENT".equals(mode)
|
||||
|| "WATERFALL_POOL".equals(mode)
|
||||
|| "GENERATE_GROTTO".equals(fallback);
|
||||
}
|
||||
|
||||
private static void validateSinkholeCapability(
|
||||
String terminalPath,
|
||||
DimensionRiverContext context,
|
||||
JSONObject caves,
|
||||
String cavesPath,
|
||||
List<String> errors
|
||||
) {
|
||||
String suffix = " in Dimension '" + context.dimensionKey() + "'.";
|
||||
if (!booleanValue(context.dimension(), "carvingEnabled", true)) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires carvingEnabled to be true" + suffix);
|
||||
}
|
||||
if (!booleanValue(context.dimension(), "useMantle", true)) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires useMantle to be true" + suffix);
|
||||
}
|
||||
if (disabled(context.dimension(), "CARVED")) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires CARVED to remain enabled" + suffix);
|
||||
}
|
||||
if (disabled(context.dimension(), "RIVER_HYDROLOGY")) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires RIVER_HYDROLOGY to remain enabled" + suffix);
|
||||
}
|
||||
if ("SEALED".equals(stringValue(caves, "mode", "SEALED"))) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires a non-SEALED caves.mode" + suffix);
|
||||
}
|
||||
if (integerValue(caves, "maximumPerReach", 1) <= 0) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires caves.maximumPerReach above zero" + suffix);
|
||||
}
|
||||
validateGrotto(cavesPath, caves, errors);
|
||||
}
|
||||
|
||||
private static void validateOverrideSinkhole(
|
||||
File packFolder,
|
||||
String terminalPath,
|
||||
String resourceKey,
|
||||
String resourceType,
|
||||
List<DimensionRiverContext> contexts,
|
||||
List<String> errors,
|
||||
List<String> warnings
|
||||
) {
|
||||
boolean referenced = false;
|
||||
for (DimensionRiverContext context : contexts) {
|
||||
boolean reachable = "Region".equals(resourceType)
|
||||
? context.regionKeys().contains(resourceKey)
|
||||
: referencedSurfaceBiomes(packFolder, context.regionKeys()).contains(resourceKey);
|
||||
if (!reachable) {
|
||||
continue;
|
||||
}
|
||||
referenced = true;
|
||||
JSONObject caves = nestedObject(context.rivers(), "caves", "Dimension '"
|
||||
+ context.dimensionKey() + "' rivers", errors);
|
||||
if (caves != null) {
|
||||
validateSinkholeCapability(
|
||||
terminalPath,
|
||||
context,
|
||||
caves,
|
||||
"Dimension '" + context.dimensionKey() + "' rivers.caves",
|
||||
errors
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!referenced) {
|
||||
addDistinct(warnings, terminalPath
|
||||
+ " selects SINKHOLE_GROTTO but no enabled river dimension reaches this "
|
||||
+ resourceType.toLowerCase() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> referencedSurfaceBiomes(File packFolder, Set<String> regionKeys) {
|
||||
Set<String> biomes = new HashSet<>();
|
||||
File regionsFolder = new File(packFolder, "regions");
|
||||
for (String regionKey : regionKeys) {
|
||||
JSONObject region = PackValidationIo.readJson(new File(regionsFolder, regionKey + ".json"));
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
collectBiomeKeys(region.optJSONArray("landBiomes"), biomes);
|
||||
collectBiomeKeys(region.optJSONArray("seaBiomes"), biomes);
|
||||
collectBiomeKeys(region.optJSONArray("shoreBiomes"), biomes);
|
||||
}
|
||||
Set<String> roots = Set.copyOf(biomes);
|
||||
for (String biomeKey : roots) {
|
||||
collectBiomeChildren(packFolder, biomeKey, 0, biomes);
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private static void collectBiomeChildren(File packFolder, String biomeKey, int depth, Set<String> biomes) {
|
||||
if (depth >= 4) {
|
||||
return;
|
||||
}
|
||||
JSONObject biome = PackValidationIo.readJson(new File(packFolder, "biomes/" + biomeKey + ".json"));
|
||||
if (biome == null) {
|
||||
return;
|
||||
}
|
||||
JSONArray children = biome.optJSONArray("children");
|
||||
if (children == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < children.length(); index++) {
|
||||
String child = children.optString(index, null);
|
||||
if (child == null || child.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
boolean added = biomes.add(child);
|
||||
if (added) {
|
||||
collectBiomeChildren(packFolder, child, depth + 1, biomes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> referencedKeys(JSONArray keys) {
|
||||
Set<String> referenced = new HashSet<>();
|
||||
collectBiomeKeys(keys, referenced);
|
||||
return Set.copyOf(referenced);
|
||||
}
|
||||
|
||||
private static void collectBiomeKeys(JSONArray keys, Set<String> destination) {
|
||||
if (keys == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < keys.length(); index++) {
|
||||
String key = keys.optString(index, null);
|
||||
if (key != null && !key.isBlank()) {
|
||||
destination.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean disabled(JSONObject dimension, String flag) {
|
||||
JSONArray disabled = dimension.optJSONArray("disabledComponents");
|
||||
if (disabled == null) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < disabled.length(); index++) {
|
||||
if (flag.equals(disabled.optString(index, null))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void addDistinct(List<String> destination, String value) {
|
||||
if (!destination.contains(value)) {
|
||||
destination.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateOverrides(File packFolder, File resourceFolder, String resourceType,
|
||||
List<DimensionRiverContext> contexts,
|
||||
List<String> errors, List<String> warnings) {
|
||||
if (!resourceFolder.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
List<File> files = PackValidationIo.listJsonRecursive(resourceFolder);
|
||||
files.sort(Comparator.comparing(File::getPath));
|
||||
for (File file : files) {
|
||||
JSONObject resource = PackValidationIo.readJson(file);
|
||||
if (resource == null || !resource.has("riverOverride")) {
|
||||
continue;
|
||||
}
|
||||
String key = PackValidationIo.deriveKey(resourceFolder, file);
|
||||
String path = resourceType + " '" + key + "' riverOverride";
|
||||
Object rawOverride = resource.opt("riverOverride");
|
||||
if (rawOverride == JSONObject.NULL) {
|
||||
continue;
|
||||
}
|
||||
if (!(rawOverride instanceof JSONObject override)) {
|
||||
errors.add(path + " must be an object or null.");
|
||||
continue;
|
||||
}
|
||||
validateOverride(packFolder, path, key, resourceType, override, contexts, errors, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateOverride(File packFolder, String path, String resourceKey, String resourceType,
|
||||
JSONObject override, List<DimensionRiverContext> contexts,
|
||||
List<String> errors, List<String> warnings) {
|
||||
PackJsonFieldChecks.validateOptionalBoolean(path, override, "allowSources", errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, override, "routingPolicy", ROUTING_POLICIES, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "routingCostMultiplier", 0D, 64D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "widthMultiplier", 0.0001D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "bankWidthMultiplier", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "depthMultiplier", 0.0001D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "maxIncisionMultiplier", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "continuationChanceMultiplier", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "caveEntryMultiplier", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, override, "terminalMode", TERMINAL_MODES, errors);
|
||||
validateBiomePool(packFolder, path, override, "channelBiomes", RiverBiomeRole.CHANNEL, true, errors, warnings);
|
||||
validateBiomePool(packFolder, path, override, "bankBiomes", RiverBiomeRole.BANK, true, errors, warnings);
|
||||
validateBiomePool(packFolder, path, override, "mouthBiomes", RiverBiomeRole.MOUTH, true, errors, warnings);
|
||||
validateBiomePool(packFolder, path, override, "dryBiomes", RiverBiomeRole.DRY, true, errors, warnings);
|
||||
validateBiomePool(packFolder, path, override, "floodedCaveBiomes", RiverBiomeRole.FLOODED_CAVE, true,
|
||||
errors, warnings);
|
||||
if ("SINKHOLE_GROTTO".equals(stringValue(override, "terminalMode", null))) {
|
||||
validateOverrideSinkhole(
|
||||
packFolder,
|
||||
path + ".terminalMode",
|
||||
resourceKey,
|
||||
resourceType,
|
||||
contexts,
|
||||
errors,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateBiomePools(File packFolder, String path, JSONObject biomes, boolean allowNull,
|
||||
List<String> errors, List<String> warnings) {
|
||||
validateStyle(packFolder, biomes, "selectionStyle", path, errors);
|
||||
validateBiomePool(packFolder, path, biomes, "channel", RiverBiomeRole.CHANNEL, allowNull, errors, warnings);
|
||||
validateBiomePool(packFolder, path, biomes, "bank", RiverBiomeRole.BANK, allowNull, errors, warnings);
|
||||
validateBiomePool(packFolder, path, biomes, "mouth", RiverBiomeRole.MOUTH, allowNull, errors, warnings);
|
||||
validateBiomePool(packFolder, path, biomes, "dry", RiverBiomeRole.DRY, allowNull, errors, warnings);
|
||||
validateBiomePool(packFolder, path, biomes, "floodedCave", RiverBiomeRole.FLOODED_CAVE, allowNull,
|
||||
errors, warnings);
|
||||
}
|
||||
|
||||
private static void validateBiomePool(File packFolder, String path, JSONObject owner, String field,
|
||||
RiverBiomeRole role, boolean allowNull,
|
||||
List<String> errors, List<String> warnings) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
Object rawPool = owner.opt(field);
|
||||
if (rawPool == JSONObject.NULL && allowNull) {
|
||||
return;
|
||||
}
|
||||
if (!(rawPool instanceof JSONArray pool)) {
|
||||
errors.add(path + "." + field + " must be an array" + (allowNull ? " or null" : "") + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> seen = new HashSet<>();
|
||||
File biomesFolder = new File(packFolder, "biomes");
|
||||
for (int index = 0; index < pool.length(); index++) {
|
||||
Object rawKey = pool.opt(index);
|
||||
String entryPath = path + "." + field + "[" + index + "]";
|
||||
if (!(rawKey instanceof String key) || key.isBlank()) {
|
||||
errors.add(entryPath + " must name a biome resource.");
|
||||
continue;
|
||||
}
|
||||
if (!seen.add(key)) {
|
||||
warnings.add(entryPath + " duplicates biome '" + key + "' in the same river pool.");
|
||||
continue;
|
||||
}
|
||||
File biomeFile = new File(biomesFolder, key + ".json");
|
||||
if (!biomeFile.isFile()) {
|
||||
errors.add(entryPath + " references missing biome '" + key + "'.");
|
||||
continue;
|
||||
}
|
||||
validateBiomeSuitability(entryPath, key, role, PackValidationIo.readJson(biomeFile), warnings);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateBiomeSuitability(String path, String biomeKey, RiverBiomeRole role,
|
||||
JSONObject biome, List<String> warnings) {
|
||||
if (biome == null || role == RiverBiomeRole.DRY || role == RiverBiomeRole.FLOODED_CAVE) {
|
||||
return;
|
||||
}
|
||||
String derivative = stringValue(biome, "vanillaDerivative", null);
|
||||
if (derivative == null || derivative.isBlank()) {
|
||||
derivative = stringValue(biome, "derivative", "minecraft:the_void");
|
||||
}
|
||||
String normalized = derivative.indexOf(':') >= 0 ? derivative : "minecraft:" + derivative;
|
||||
if (!normalized.startsWith("minecraft:")) {
|
||||
return;
|
||||
}
|
||||
boolean suitable = switch (role) {
|
||||
case CHANNEL, MOUTH -> normalized.contains("ocean") || normalized.endsWith("river");
|
||||
case BANK -> normalized.endsWith("beach") || normalized.endsWith("shore");
|
||||
default -> true;
|
||||
};
|
||||
if (!suitable) {
|
||||
warnings.add(path + " assigns biome '" + biomeKey + "' the inferred river role " + role.label
|
||||
+ " but its vanilla derivative '" + normalized
|
||||
+ "' does not match that role; native structure selection will use Iris's safe role fallback.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateNoiseChance(File packFolder, JSONObject owner, String field, String path,
|
||||
List<String> errors) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
JSONObject chance = requireObject(owner, field, path + "." + field, errors);
|
||||
if (chance == null) {
|
||||
return;
|
||||
}
|
||||
String chancePath = path + "." + field;
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(chancePath, chance, "chance", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(chancePath, chance, "influence", 0D, 1D, errors);
|
||||
validateStyle(packFolder, chance, "style", chancePath, errors);
|
||||
}
|
||||
|
||||
private static void validateStyledRange(File packFolder, JSONObject owner, String field, String path,
|
||||
double minimum, double maximum,
|
||||
List<String> errors, List<String> warnings) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
String rangePath = path + "." + field;
|
||||
JSONObject range = resolveObject(packFolder, owner.opt(field), "snippet/style-range/", rangePath, errors);
|
||||
if (range == null) {
|
||||
return;
|
||||
}
|
||||
boolean hasMinimum = range.has("min") && range.opt("min") != JSONObject.NULL;
|
||||
boolean hasMaximum = range.has("max") && range.opt("max") != JSONObject.NULL;
|
||||
if (!hasMinimum && !hasMaximum) {
|
||||
errors.add(rangePath + " must set min and max explicitly.");
|
||||
} else if (!hasMinimum || !hasMaximum) {
|
||||
warnings.add(rangePath
|
||||
+ " should set both min and max explicitly; the omitted bound uses the shared style-range default.");
|
||||
}
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(rangePath, range, "min", minimum, maximum, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(rangePath, range, "max", minimum, maximum, errors);
|
||||
double minimumValue = doubleValue(range, "min", 16D);
|
||||
double maximumValue = doubleValue(range, "max", 32D);
|
||||
if (Double.isFinite(minimumValue) && Double.isFinite(maximumValue) && minimumValue > maximumValue) {
|
||||
errors.add(rangePath + ".min must not exceed " + rangePath + ".max.");
|
||||
}
|
||||
validateStyle(packFolder, range, "style", rangePath, errors);
|
||||
}
|
||||
|
||||
private static void validateStyle(File packFolder, JSONObject owner, String field, String path,
|
||||
List<String> errors) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
validateStyle(packFolder, owner.opt(field), path + "." + field, errors, new HashSet<>());
|
||||
}
|
||||
|
||||
private static void validateStyle(File packFolder, Object rawStyle, String path,
|
||||
List<String> errors, Set<String> dependencyStack) {
|
||||
String styleMarker = rawStyle instanceof String reference ? "style:" + reference : null;
|
||||
if (styleMarker != null && !dependencyStack.add(styleMarker)) {
|
||||
errors.add(path + " has a cyclic river-noise style snippet dependency.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject style = resolveObject(packFolder, rawStyle, "snippet/style/", path, errors);
|
||||
if (style != null) {
|
||||
validateResolvedStyle(packFolder, style, path, errors, dependencyStack);
|
||||
}
|
||||
} finally {
|
||||
if (styleMarker != null) {
|
||||
dependencyStack.remove(styleMarker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateResolvedStyle(File packFolder, JSONObject style, String path,
|
||||
List<String> errors, Set<String> dependencyStack) {
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, style, "style", NOISE_STYLES, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "cellularFrequency", 0D, Double.MAX_VALUE, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "cellularZoom", 0.00001D, Double.MAX_VALUE, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "zoom", 0.00001D, Double.MAX_VALUE, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "multiplier",
|
||||
-Double.MAX_VALUE, Double.MAX_VALUE, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "exponent", 0.01562D, 64D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, style, "cacheSize", 0, 8192, errors);
|
||||
|
||||
if (style.has("expression") && style.opt("expression") != JSONObject.NULL) {
|
||||
Object rawExpression = style.opt("expression");
|
||||
if (!(rawExpression instanceof String expressionKey) || expressionKey.isBlank()) {
|
||||
errors.add(path + ".expression must name an expression resource.");
|
||||
} else {
|
||||
validateExpression(packFolder, expressionKey, path, errors, dependencyStack);
|
||||
}
|
||||
}
|
||||
if (style.has("fracture") && style.opt("fracture") != JSONObject.NULL) {
|
||||
validateStyle(packFolder, style.opt("fracture"), path + ".fracture", errors, dependencyStack);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateExpression(File packFolder, String expressionKey, String usePath,
|
||||
List<String> errors, Set<String> dependencyStack) {
|
||||
String expressionMarker = "expression:" + expressionKey;
|
||||
if (!dependencyStack.add(expressionMarker)) {
|
||||
errors.add(usePath + " has a cyclic river-noise expression dependency through '" + expressionKey + "'.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
File expressionFile = new File(new File(packFolder, "expressions"), expressionKey + ".json");
|
||||
JSONObject expression = PackValidationIo.readJson(expressionFile);
|
||||
if (expression == null) {
|
||||
return;
|
||||
}
|
||||
scanExpressionEntries(packFolder, expression, "variables", expressionKey, usePath, errors, dependencyStack);
|
||||
scanExpressionEntries(packFolder, expression, "functions", expressionKey, usePath, errors, dependencyStack);
|
||||
} finally {
|
||||
dependencyStack.remove(expressionMarker);
|
||||
}
|
||||
}
|
||||
|
||||
private static void scanExpressionEntries(File packFolder, JSONObject expression, String field,
|
||||
String expressionKey, String usePath,
|
||||
List<String> errors, Set<String> dependencyStack) {
|
||||
JSONArray entries = expression.optJSONArray(field);
|
||||
if (entries == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < entries.length(); index++) {
|
||||
Object rawEntry = entries.opt(index);
|
||||
String snippetFolder = "variables".equals(field)
|
||||
? "snippet/expression-load/"
|
||||
: "snippet/expression-function/";
|
||||
JSONObject entry = resolveExpressionEntry(packFolder, rawEntry, snippetFolder);
|
||||
if (entry == null) {
|
||||
continue;
|
||||
}
|
||||
String stream = stringValue(entry, "engineStreamValue", null);
|
||||
if (stream != null && isUnsafeRiverStream(stream)) {
|
||||
errors.add(usePath + " uses expression '" + expressionKey + "' " + field + "[" + index
|
||||
+ "].engineStreamValue '" + stream
|
||||
+ "', which depends on final river-shaped terrain and would recurse during river generation.");
|
||||
}
|
||||
if (entry.has("styleValue")) {
|
||||
validateStyle(packFolder, entry.opt("styleValue"), usePath + " -> expression '" + expressionKey
|
||||
+ "' " + field + "[" + index + "].styleValue", errors, dependencyStack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject resolveExpressionEntry(File packFolder, Object rawEntry, String snippetFolder) {
|
||||
if (rawEntry instanceof JSONObject entry) {
|
||||
return entry;
|
||||
}
|
||||
if (!(rawEntry instanceof String reference) || !reference.startsWith("snippet/")) {
|
||||
return null;
|
||||
}
|
||||
String resolved = reference.startsWith(snippetFolder)
|
||||
? reference
|
||||
: snippetFolder + reference.substring("snippet/".length());
|
||||
return PackValidationIo.readJson(new File(packFolder, resolved + ".json"));
|
||||
}
|
||||
|
||||
private static boolean isUnsafeRiverStream(String stream) {
|
||||
return UNSAFE_RIVER_STREAMS.contains(stream) || stream.startsWith("RIVER_");
|
||||
}
|
||||
|
||||
private static Set<String> noiseStyles() {
|
||||
Set<String> styles = new HashSet<>();
|
||||
for (NoiseStyle style : NoiseStyle.values()) {
|
||||
styles.add(style.name());
|
||||
}
|
||||
return Set.copyOf(styles);
|
||||
}
|
||||
|
||||
private static JSONObject nestedObject(JSONObject owner, String field, String path, List<String> errors) {
|
||||
if (!owner.has(field)) {
|
||||
return new JSONObject();
|
||||
}
|
||||
return requireObject(owner, field, path + "." + field, errors);
|
||||
}
|
||||
|
||||
private static JSONObject requireObject(JSONObject owner, String field, String path, List<String> errors) {
|
||||
Object raw = owner.opt(field);
|
||||
if (!(raw instanceof JSONObject object)) {
|
||||
errors.add(path + " must be an object.");
|
||||
return null;
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
private static JSONObject resolveObject(File packFolder, Object raw, String snippetFolder,
|
||||
String path, List<String> errors) {
|
||||
if (raw instanceof JSONObject object) {
|
||||
return object;
|
||||
}
|
||||
if (raw instanceof String reference && reference.startsWith("snippet/")) {
|
||||
String resolved = reference.startsWith(snippetFolder)
|
||||
? reference
|
||||
: snippetFolder + reference.substring("snippet/".length());
|
||||
return PackValidationIo.readJson(new File(packFolder, resolved + ".json"));
|
||||
}
|
||||
errors.add(path + " must be an object or snippet reference.");
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean booleanValue(JSONObject object, String field, boolean defaultValue) {
|
||||
Object raw = object.opt(field);
|
||||
return raw instanceof Boolean value ? value : defaultValue;
|
||||
}
|
||||
|
||||
private static int integerValue(JSONObject object, String field, int defaultValue) {
|
||||
Object raw = object.opt(field);
|
||||
if (!(raw instanceof Number number) || !Double.isFinite(number.doubleValue())) {
|
||||
return defaultValue;
|
||||
}
|
||||
return number.intValue();
|
||||
}
|
||||
|
||||
private static double doubleValue(JSONObject object, String field, double defaultValue) {
|
||||
Object raw = object.opt(field);
|
||||
return raw instanceof Number number ? number.doubleValue() : defaultValue;
|
||||
}
|
||||
|
||||
private static String stringValue(JSONObject object, String field, String defaultValue) {
|
||||
Object raw = object.opt(field);
|
||||
return raw instanceof String value ? value : defaultValue;
|
||||
}
|
||||
|
||||
private static double noiseChanceValue(JSONObject owner, String field, double defaultValue) {
|
||||
JSONObject chance = owner.optJSONObject(field);
|
||||
return chance == null ? defaultValue : doubleValue(chance, "chance", defaultValue);
|
||||
}
|
||||
|
||||
record Validation(List<String> errors, List<String> warnings) {
|
||||
Validation {
|
||||
errors = List.copyOf(errors);
|
||||
warnings = List.copyOf(warnings);
|
||||
}
|
||||
}
|
||||
|
||||
private record DimensionRiverContext(
|
||||
String dimensionKey,
|
||||
JSONObject dimension,
|
||||
JSONObject rivers,
|
||||
Set<String> regionKeys
|
||||
) {
|
||||
}
|
||||
|
||||
private enum RiverBiomeRole {
|
||||
CHANNEL("SEA"),
|
||||
BANK("SHORE"),
|
||||
MOUTH("SEA"),
|
||||
DRY("LAND"),
|
||||
FLOODED_CAVE("CAVE");
|
||||
|
||||
private final String label;
|
||||
|
||||
RiverBiomeRole(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,9 @@ public final class PackValidator {
|
||||
}
|
||||
|
||||
PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
||||
PackRiverValidator.Validation riverValidation = PackRiverValidator.validate(packFolder, dimensionFiles);
|
||||
addDistinct(blockingErrors, riverValidation.errors());
|
||||
addDistinct(warnings, riverValidation.warnings());
|
||||
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
|
||||
PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder);
|
||||
addDistinct(blockingErrors, lootIssues.errors());
|
||||
|
||||
@@ -16,7 +16,6 @@ import art.arcane.iris.core.tools.IrisCreator;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.exceptions.IrisException;
|
||||
@@ -55,6 +54,7 @@ import java.util.function.Supplier;
|
||||
public final class StudioOpenCoordinator {
|
||||
private static final long STUDIO_CLOSE_TIMEOUT_SECONDS = 120L;
|
||||
private static final long STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS = 30L;
|
||||
private static final long STUDIO_ENTRY_RELEASE_TIMEOUT_SECONDS = 15L;
|
||||
private static final long STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS = 30L;
|
||||
private static final long STUDIO_ENTRY_CLEANUP_BOUNDARY_SECONDS = 120L;
|
||||
private static volatile StudioOpenCoordinator instance;
|
||||
@@ -103,10 +103,115 @@ public final class StudioOpenCoordinator {
|
||||
return closeWorldCoordinated(provider, worldName, world, true, project);
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> teleportPlayerToProject(
|
||||
IrisProject project,
|
||||
Player player,
|
||||
AtomicBoolean admission,
|
||||
long deadlineNanos
|
||||
) {
|
||||
if (project == null || player == null) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
AtomicBoolean activeAdmission = Objects.requireNonNull(
|
||||
admission,
|
||||
"Studio teleport admission");
|
||||
if (!isStudioTeleportAdmitted(activeAdmission, deadlineNanos)) {
|
||||
return studioTeleportDeadlineFailure("entry loading");
|
||||
}
|
||||
PlatformChunkGenerator provider = project.getActiveProvider();
|
||||
if (provider == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio runtime provider is unavailable."));
|
||||
}
|
||||
World world = BukkitWorldBinding.world(provider.getTarget().getWorld());
|
||||
if (world == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio world is not loaded."));
|
||||
}
|
||||
Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world);
|
||||
if (entryAnchor == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio entry anchor could not be resolved."));
|
||||
}
|
||||
EntryChunkResolution entryResolution = loadEntryChunk(world, entryAnchor);
|
||||
CompletableFuture<Boolean> teleportOperation = beforeStudioTeleportDeadline(
|
||||
entryResolution.chunk(),
|
||||
activeAdmission,
|
||||
deadlineNanos,
|
||||
"entry loading")
|
||||
.thenCompose(ignored -> {
|
||||
if (!isStudioTeleportAdmitted(activeAdmission, deadlineNanos)) {
|
||||
return studioTeleportDeadlineFailure("safe-entry resolution");
|
||||
}
|
||||
return beforeStudioTeleportDeadline(
|
||||
entryResolution.safeEntry(),
|
||||
activeAdmission,
|
||||
deadlineNanos,
|
||||
"safe-entry resolution");
|
||||
})
|
||||
.thenCompose(entry -> {
|
||||
if (entry == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio entry point could not be resolved."));
|
||||
}
|
||||
if (System.nanoTime() >= deadlineNanos
|
||||
|| !activeAdmission.compareAndSet(true, false)) {
|
||||
return studioTeleportDeadlineFailure("native teleport delegation");
|
||||
}
|
||||
CompletableFuture<Boolean> teleport =
|
||||
WorldRuntimeControlService.get().teleport(player, entry);
|
||||
if (teleport == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio native teleport returned no completion future."));
|
||||
}
|
||||
return teleport;
|
||||
});
|
||||
return teleportOperation;
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> beforeStudioTeleportDeadline(
|
||||
CompletableFuture<T> stage,
|
||||
AtomicBoolean admission,
|
||||
long deadlineNanos,
|
||||
String stageName
|
||||
) {
|
||||
if (stage == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio " + stageName + " returned no completion future."));
|
||||
}
|
||||
long remainingNanos = deadlineNanos - System.nanoTime();
|
||||
if (!admission.get() || remainingNanos <= 0L) {
|
||||
return studioTeleportDeadlineFailure(stageName);
|
||||
}
|
||||
CompletableFuture<T> bounded = new CompletableFuture<>();
|
||||
stage.whenComplete((value, failure) -> {
|
||||
if (failure == null) {
|
||||
bounded.complete(value);
|
||||
} else {
|
||||
bounded.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
CompletableFuture.delayedExecutor(remainingNanos, TimeUnit.NANOSECONDS)
|
||||
.execute(() -> bounded.completeExceptionally(new TimeoutException(
|
||||
"Studio teleport deadline expired during " + stageName + ".")));
|
||||
return bounded;
|
||||
}
|
||||
|
||||
private boolean isStudioTeleportAdmitted(AtomicBoolean admission, long deadlineNanos) {
|
||||
return admission.get() && System.nanoTime() < deadlineNanos;
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> studioTeleportDeadlineFailure(String stageName) {
|
||||
return CompletableFuture.failedFuture(new TimeoutException(
|
||||
"Studio teleport deadline expired before " + stageName + "."));
|
||||
}
|
||||
|
||||
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
|
||||
World world = null;
|
||||
PlatformChunkGenerator provider = null;
|
||||
CompletableFuture<Void> entryLoadFuture = null;
|
||||
CompletableFuture<Void> entryUseFuture = null;
|
||||
CompletableFuture<Boolean> nativeTeleportFuture = null;
|
||||
try {
|
||||
long openStart = System.nanoTime();
|
||||
long t = openStart;
|
||||
@@ -158,35 +263,75 @@ public final class StudioOpenCoordinator {
|
||||
}
|
||||
t = logStudioPhase(request, "resolve_entry_anchor", t, openStart);
|
||||
|
||||
updateStage(request, "load_entry_chunk", 0.80D);
|
||||
int entryChunkX = entryAnchor.getBlockX() >> 4;
|
||||
int entryChunkZ = entryAnchor.getBlockZ() >> 4;
|
||||
try {
|
||||
entryLoadFuture = loadEntryChunk(world, entryChunkX, entryChunkZ);
|
||||
entryLoads.register(request.worldName(), entryLoadFuture);
|
||||
entryLoadFuture.get(STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio entry chunk did not load in time at "
|
||||
+ entryChunkX + "," + entryChunkZ + " — chunk system may be stalled.", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Studio entry chunk load was interrupted at "
|
||||
+ entryChunkX + "," + entryChunkZ + ".", e);
|
||||
long entryPrecomputeStartedAt = System.nanoTime();
|
||||
CompletableFuture<Void> preparedEntryChunks = CompletableFuture.completedFuture(null);
|
||||
if (requiresLoadedEntry(request)) {
|
||||
if (!(provider instanceof BukkitChunkGenerator bukkitGenerator)) {
|
||||
throw new IllegalStateException(
|
||||
"Studio runtime provider cannot prepare its entry chunks.");
|
||||
}
|
||||
preparedEntryChunks = bukkitGenerator.prepareStudioEntryChunks(
|
||||
world,
|
||||
entryAnchor.getBlockX() >> 4,
|
||||
entryAnchor.getBlockZ() >> 4
|
||||
);
|
||||
}
|
||||
t = logStudioPhase(request, "load_entry_chunk", t, openStart);
|
||||
|
||||
updateStage(request, "resolve_safe_entry", 0.84D);
|
||||
Location safeEntry;
|
||||
try {
|
||||
safeEntry = WorldRuntimeControlService.get().resolveSafeEntry(world, entryAnchor)
|
||||
.get(5L, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled.");
|
||||
updateStage(request, "prepare_structure_rings", 0.79D);
|
||||
endStudioEntryBootstrap(world, provider);
|
||||
t = logStudioPhase(request, "prepare_structure_rings", t, openStart);
|
||||
|
||||
if (requiresLoadedEntry(request)) {
|
||||
try {
|
||||
preparedEntryChunks.get(
|
||||
STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException(
|
||||
"Studio entry chunk precompute did not finish in time.", e);
|
||||
}
|
||||
t = logOverlappedStudioPhase(
|
||||
request,
|
||||
"prepare_entry_chunks",
|
||||
entryPrecomputeStartedAt,
|
||||
openStart
|
||||
);
|
||||
}
|
||||
if (safeEntry == null) {
|
||||
throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\".");
|
||||
|
||||
Location safeEntry = entryAnchor;
|
||||
if (requiresLoadedEntry(request)) {
|
||||
updateStage(request, "load_entry_chunk", 0.80D);
|
||||
int entryChunkX = entryAnchor.getBlockX() >> 4;
|
||||
int entryChunkZ = entryAnchor.getBlockZ() >> 4;
|
||||
EntryChunkResolution entryResolution = loadEntryChunk(world, entryAnchor);
|
||||
CompletableFuture<Void> useSettlement = new CompletableFuture<>();
|
||||
entryUseFuture = useSettlement;
|
||||
entryLoadFuture = entryResolution.safeEntry().thenCompose(ignored -> useSettlement);
|
||||
entryLoads.register(request.worldName(), entryLoadFuture);
|
||||
try {
|
||||
entryResolution.chunk().get(STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio entry chunk did not load in time at "
|
||||
+ entryChunkX + "," + entryChunkZ + " — chunk system may be stalled.", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Studio entry chunk load was interrupted at "
|
||||
+ entryChunkX + "," + entryChunkZ + ".", e);
|
||||
}
|
||||
t = logStudioPhase(request, "load_entry_chunk", t, openStart);
|
||||
|
||||
updateStage(request, "resolve_safe_entry", 0.84D);
|
||||
try {
|
||||
safeEntry = entryResolution.safeEntry().get(5L, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled.");
|
||||
}
|
||||
if (safeEntry == null) {
|
||||
throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\".");
|
||||
}
|
||||
t = logStudioPhase(request, "resolve_safe_entry", t, openStart);
|
||||
}
|
||||
t = logStudioPhase(request, "resolve_safe_entry", t, openStart);
|
||||
|
||||
if (request.openKind().teleportThroughStandardEntry()
|
||||
&& request.playerName() != null
|
||||
@@ -199,7 +344,12 @@ public final class StudioOpenCoordinator {
|
||||
|
||||
Boolean teleported;
|
||||
try {
|
||||
teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(60L, TimeUnit.SECONDS);
|
||||
nativeTeleportFuture = WorldRuntimeControlService.get().teleport(player, safeEntry);
|
||||
if (nativeTeleportFuture == null) {
|
||||
throw new IllegalStateException(
|
||||
"Studio native teleport returned no completion future.");
|
||||
}
|
||||
teleported = nativeTeleportFuture.get(60L, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio teleport timed out — destination region may still be generating.");
|
||||
}
|
||||
@@ -209,8 +359,6 @@ public final class StudioOpenCoordinator {
|
||||
t = logStudioPhase(request, "teleport_standard_entry", t, openStart);
|
||||
}
|
||||
|
||||
endStudioEntryBootstrap(world, provider);
|
||||
|
||||
updateStage(request, "finalize_open", 1.00D);
|
||||
if (request.project() != null) {
|
||||
request.project().setActiveProvider(provider);
|
||||
@@ -221,11 +369,17 @@ public final class StudioOpenCoordinator {
|
||||
runOpenFinalizer(request.onDone(), world);
|
||||
t = logStudioPhase(request, "finalize_open", t, openStart);
|
||||
|
||||
settleEntryUseAfterOperation(entryUseFuture, nativeTeleportFuture);
|
||||
if (entryLoadFuture != null) {
|
||||
entryLoadFuture.get(STUDIO_ENTRY_RELEASE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
IrisLogging.info("Studio open: " + world.getName() + " ready in "
|
||||
+ elapsedMillis(openStart) + "ms");
|
||||
entryLoads.release(request.worldName(), entryLoadFuture);
|
||||
future.complete(new StudioOpenResult(world, safeEntry));
|
||||
} catch (Throwable e) {
|
||||
settleEntryUseAfterOperation(entryUseFuture, nativeTeleportFuture);
|
||||
abandonStudioEntryBootstrap(world, e);
|
||||
IrisLogging.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
|
||||
if (!request.retainOnFailure()) {
|
||||
@@ -262,11 +416,23 @@ public final class StudioOpenCoordinator {
|
||||
+ request.worldName() + "\".", unwrapFailure(cleanupError));
|
||||
}
|
||||
}
|
||||
} else if (entryLoadFuture != null) {
|
||||
entryLoads.releaseAfterSuccessfulCompletion(
|
||||
request.worldName(),
|
||||
entryLoadFuture,
|
||||
entryLoadFuture);
|
||||
}
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean requiresLoadedEntry(StudioOpenRequest request) {
|
||||
Objects.requireNonNull(request, "Studio open request");
|
||||
return request.openKind().teleportThroughStandardEntry()
|
||||
&& request.playerName() != null
|
||||
&& !request.playerName().isBlank();
|
||||
}
|
||||
|
||||
private void deferFailedOpenCleanupToRestart(
|
||||
PlatformChunkGenerator provider,
|
||||
String worldName,
|
||||
@@ -278,7 +444,12 @@ public final class StudioOpenCoordinator {
|
||||
worldName,
|
||||
new IllegalStateException("Studio cleanup deferred across the queued server restart."));
|
||||
}
|
||||
entryLoads.release(worldName, entryLoadFuture);
|
||||
if (entryLoadFuture != null) {
|
||||
entryLoads.releaseAfterSuccessfulCompletion(
|
||||
worldName,
|
||||
entryLoadFuture,
|
||||
entryLoadFuture);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean transientWorldStorageExists(String worldName) {
|
||||
@@ -305,6 +476,22 @@ public final class StudioOpenCoordinator {
|
||||
return now;
|
||||
}
|
||||
|
||||
private long logOverlappedStudioPhase(
|
||||
StudioOpenRequest request,
|
||||
String phase,
|
||||
long phaseStart,
|
||||
long openStart
|
||||
) {
|
||||
long now = System.nanoTime();
|
||||
IrisLogging.info("[Studio timing] world=%s kind=%s phase=%s duration=%dms cumulative=%dms",
|
||||
request.worldName(),
|
||||
request.openKind().name().toLowerCase(Locale.ROOT),
|
||||
phase,
|
||||
TimeUnit.NANOSECONDS.toMillis(now - phaseStart),
|
||||
TimeUnit.NANOSECONDS.toMillis(now - openStart));
|
||||
return now;
|
||||
}
|
||||
|
||||
private void runOpenFinalizer(Consumer<World> finalizer, World world)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
if (finalizer == null) {
|
||||
@@ -331,14 +518,12 @@ public final class StudioOpenCoordinator {
|
||||
duration);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> loadEntryChunk(World world, int chunkX, int chunkZ) {
|
||||
if (!J.isFolia()) {
|
||||
return loadEntryChunkAsync(world, chunkX, chunkZ);
|
||||
}
|
||||
return scheduleEntryChunkRetention(world, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> loadEntryChunkAsync(World world, int chunkX, int chunkZ) {
|
||||
private EntryChunkResolution loadEntryChunk(World world, Location entryAnchor) {
|
||||
int chunkX = entryAnchor.getBlockX() >> 4;
|
||||
int chunkZ = entryAnchor.getBlockZ() >> 4;
|
||||
CompletableFuture<Chunk> chunkFuture = new CompletableFuture<>();
|
||||
CompletableFuture<Location> safeEntryFuture = new CompletableFuture<>();
|
||||
EntryChunkResolution resolution = new EntryChunkResolution(chunkFuture, safeEntryFuture);
|
||||
CompletableFuture<Chunk> requested;
|
||||
try {
|
||||
requested = WorldRuntimeControlService.get().requestChunkAsync(
|
||||
@@ -348,56 +533,67 @@ public final class StudioOpenCoordinator {
|
||||
true,
|
||||
true);
|
||||
} catch (Throwable throwable) {
|
||||
return CompletableFuture.failedFuture(throwable);
|
||||
failEntryResolution(resolution, throwable);
|
||||
return resolution;
|
||||
}
|
||||
if (requested == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
failEntryResolution(resolution, new IllegalStateException(
|
||||
"Entry-chunk async request did not return a future at " + chunkX + "," + chunkZ + "."));
|
||||
return resolution;
|
||||
}
|
||||
return requested.thenCompose(chunk -> {
|
||||
if (chunk == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Entry-chunk async request returned no chunk at " + chunkX + "," + chunkZ + "."));
|
||||
requested.whenComplete((chunk, failure) -> {
|
||||
if (failure != null) {
|
||||
failEntryResolution(resolution, failure);
|
||||
return;
|
||||
}
|
||||
if (chunk == null) {
|
||||
failEntryResolution(resolution, new IllegalStateException(
|
||||
"Entry-chunk async request returned no chunk at " + chunkX + "," + chunkZ + "."));
|
||||
return;
|
||||
}
|
||||
Runnable resolve = () -> {
|
||||
chunkFuture.complete(chunk);
|
||||
try {
|
||||
Location safeEntry = WorldRuntimeControlService.findTopSafeStudioLocation(world, entryAnchor);
|
||||
safeEntryFuture.complete(safeEntry);
|
||||
} catch (Throwable resolutionFailure) {
|
||||
failEntryResolution(resolution, resolutionFailure);
|
||||
}
|
||||
};
|
||||
try {
|
||||
if (J.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
||||
resolve.run();
|
||||
return;
|
||||
}
|
||||
if (!J.runRegion(world, chunkX, chunkZ, resolve)) {
|
||||
failEntryResolution(resolution, new IllegalStateException(
|
||||
"Failed to resolve the entry chunk on its owning region at "
|
||||
+ chunkX + "," + chunkZ + "."));
|
||||
}
|
||||
} catch (Throwable schedulingFailure) {
|
||||
failEntryResolution(resolution, schedulingFailure);
|
||||
}
|
||||
return scheduleEntryChunkRetention(world, chunkX, chunkZ);
|
||||
});
|
||||
return resolution;
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> scheduleEntryChunkRetention(World world, int chunkX, int chunkZ) {
|
||||
CompletableFuture<Void> loaded = new CompletableFuture<>();
|
||||
try {
|
||||
J.s(() -> retainAndConfirmEntryChunk(world, chunkX, chunkZ)
|
||||
.whenComplete((ignored, throwable) -> complete(loaded, throwable)));
|
||||
} catch (Throwable throwable) {
|
||||
loaded.completeExceptionally(throwable);
|
||||
}
|
||||
return loaded;
|
||||
private void failEntryResolution(EntryChunkResolution resolution, Throwable failure) {
|
||||
resolution.chunk().completeExceptionally(failure);
|
||||
resolution.safeEntry().completeExceptionally(failure);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> retainAndConfirmEntryChunk(World world, int chunkX, int chunkZ) {
|
||||
CompletableFuture<Void> confirmed = new CompletableFuture<>();
|
||||
try {
|
||||
world.addPluginChunkTicket(
|
||||
chunkX,
|
||||
chunkZ,
|
||||
BukkitPlatform.plugin());
|
||||
} catch (Throwable throwable) {
|
||||
confirmed.completeExceptionally(throwable);
|
||||
return confirmed;
|
||||
}
|
||||
if (!J.runRegion(world, chunkX, chunkZ, () -> confirmed.complete(null))) {
|
||||
confirmed.completeExceptionally(new IllegalStateException(
|
||||
"Failed to confirm entry-chunk region at " + chunkX + "," + chunkZ + "."));
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
private void complete(CompletableFuture<Void> target, Throwable throwable) {
|
||||
if (throwable == null) {
|
||||
target.complete(null);
|
||||
private void settleEntryUseAfterOperation(
|
||||
CompletableFuture<Void> entryUseFuture,
|
||||
CompletableFuture<?> operation
|
||||
) {
|
||||
if (entryUseFuture == null) {
|
||||
return;
|
||||
}
|
||||
target.completeExceptionally(throwable);
|
||||
if (operation == null) {
|
||||
entryUseFuture.complete(null);
|
||||
return;
|
||||
}
|
||||
operation.whenComplete((ignored, failure) -> entryUseFuture.complete(null));
|
||||
}
|
||||
|
||||
private void endStudioEntryBootstrap(World world, PlatformChunkGenerator provider) {
|
||||
@@ -405,19 +601,28 @@ public final class StudioOpenCoordinator {
|
||||
throw new IllegalStateException("Studio runtime provider cannot finish its entry bootstrap.");
|
||||
}
|
||||
AtomicBoolean activationClaim = new AtomicBoolean(true);
|
||||
CompletableFuture<Void> activation = J.sfut(() -> {
|
||||
CompletableFuture<CompletableFuture<Void>> scheduledActivation = J.sfut(() -> {
|
||||
if (!activationClaim.compareAndSet(true, false)) {
|
||||
INMS.get().abandonStudioStructureBootstrap(world);
|
||||
return;
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio native structure activation was cancelled before it began."));
|
||||
}
|
||||
try {
|
||||
INMS.get().completeStudioStructureBootstrap(world);
|
||||
bukkitGenerator.endStudioEntryBootstrap();
|
||||
CompletableFuture<Void> nativeActivation =
|
||||
INMS.get().completeStudioStructureBootstrap(world);
|
||||
if (nativeActivation == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio native structure activation returned no completion future."));
|
||||
}
|
||||
return nativeActivation;
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(
|
||||
"Studio native structure state could not be activated after entry bootstrap.", e);
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio native structure state could not be activated after entry bootstrap.", e));
|
||||
}
|
||||
});
|
||||
CompletableFuture<Void> activation = scheduledActivation
|
||||
.thenCompose(nativeActivation -> nativeActivation)
|
||||
.thenCompose(ignored -> J.sfut(bukkitGenerator::endStudioEntryBootstrap));
|
||||
try {
|
||||
activation.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
@@ -910,7 +1115,7 @@ public final class StudioOpenCoordinator {
|
||||
};
|
||||
}
|
||||
|
||||
private Throwable unwrapFailure(Throwable throwable) {
|
||||
private static Throwable unwrapFailure(Throwable throwable) {
|
||||
Throwable cursor = throwable;
|
||||
while (cursor instanceof CompletionException || cursor instanceof ExecutionException) {
|
||||
if (cursor.getCause() == null) {
|
||||
@@ -1053,6 +1258,12 @@ public final class StudioOpenCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private record EntryChunkResolution(
|
||||
CompletableFuture<Chunk> chunk,
|
||||
CompletableFuture<Location> safeEntry
|
||||
) {
|
||||
}
|
||||
|
||||
static final class EntryLoadRegistry {
|
||||
private final ConcurrentHashMap<String, CompletableFuture<?>> entryLoads;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.Levelled;
|
||||
import org.bukkit.block.data.Waterlogged;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.world.TimeSkipEvent;
|
||||
@@ -430,6 +431,62 @@ public final class WorldRuntimeControlService {
|
||||
return null;
|
||||
}
|
||||
|
||||
static Location findTopSafeStudioLocation(World world, Location source) {
|
||||
Location dryLocation = findTopSafeLocation(world, source);
|
||||
if (dryLocation != null) {
|
||||
return dryLocation;
|
||||
}
|
||||
|
||||
int sourceX = source.getBlockX();
|
||||
int sourceZ = source.getBlockZ();
|
||||
int chunkX = sourceX >> 4;
|
||||
int chunkZ = sourceZ >> 4;
|
||||
if (!world.isChunkLoaded(chunkX, chunkZ)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int minimumFloorY = world.getMinHeight();
|
||||
int maximumFloorY = world.getMaxHeight() - 3;
|
||||
if (minimumFloorY > maximumFloorY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int minimumX = chunkX << 4;
|
||||
int minimumZ = chunkZ << 4;
|
||||
int maximumX = minimumX + 15;
|
||||
int maximumZ = minimumZ + 15;
|
||||
for (int radius = 0; radius <= MAX_SAFE_ENTRY_HORIZONTAL_RADIUS; radius++) {
|
||||
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
|
||||
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
|
||||
if (Math.max(Math.abs(offsetX), Math.abs(offsetZ)) != radius) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int x = sourceX + offsetX;
|
||||
int z = sourceZ + offsetZ;
|
||||
if (x < minimumX || x > maximumX || z < minimumZ || z > maximumZ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Location waterLocation = findSafeWaterSurfaceLocationInColumn(
|
||||
world,
|
||||
x,
|
||||
z,
|
||||
minimumFloorY,
|
||||
maximumFloorY,
|
||||
source.getYaw(),
|
||||
source.getPitch()
|
||||
);
|
||||
if (waterLocation != null) {
|
||||
return waterLocation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Location findSafeLocationInColumn(
|
||||
World world,
|
||||
int x,
|
||||
@@ -458,6 +515,48 @@ public final class WorldRuntimeControlService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Location findSafeWaterSurfaceLocationInColumn(
|
||||
World world,
|
||||
int x,
|
||||
int z,
|
||||
int minimumFloorY,
|
||||
int maximumFloorY,
|
||||
float yaw,
|
||||
float pitch
|
||||
) {
|
||||
int surfaceY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
|
||||
if (surfaceY <= minimumFloorY || surfaceY > maximumFloorY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Block surface = world.getBlockAt(x, surfaceY, z);
|
||||
if (!isStableWater(surface)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Block support = world.getBlockAt(x, surfaceY - 1, z);
|
||||
if (!isStableWater(support) && !isSafeFloor(support)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Block feet = world.getBlockAt(x, surfaceY + 1, z);
|
||||
Block head = world.getBlockAt(x, surfaceY + 2, z);
|
||||
if (!isClearEntryBlock(feet) || !isClearEntryBlock(head)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Location(world, x + BLOCK_CENTER, surfaceY + 1D, z + BLOCK_CENTER, yaw, pitch);
|
||||
}
|
||||
|
||||
private static boolean isStableWater(Block block) {
|
||||
if (block.getType() != Material.WATER || !block.isLiquid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BlockData blockData = block.getBlockData();
|
||||
return blockData instanceof Levelled levelled && levelled.getLevel() == 0;
|
||||
}
|
||||
|
||||
private static boolean isSafeFloor(Block block) {
|
||||
Material material = block.getType();
|
||||
if (material == null
|
||||
|
||||
@@ -59,6 +59,7 @@ import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -82,6 +83,7 @@ import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -93,6 +95,7 @@ import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
public class StudioSVC implements IrisService {
|
||||
public static final String WORKSPACE_NAME = "packs";
|
||||
private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L;
|
||||
private static final long STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS = 10L;
|
||||
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
|
||||
private static final AtomicCache<Integer> counter = new AtomicCache<>();
|
||||
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
|
||||
@@ -474,6 +477,28 @@ public class StudioSVC implements IrisService {
|
||||
return activeProject != null && activeProject.isOpen();
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> teleportToActiveProject(Player player) {
|
||||
Player target = Objects.requireNonNull(player, "Studio teleport player");
|
||||
AtomicBoolean admission = new AtomicBoolean(true);
|
||||
long deadlineNanos = System.nanoTime()
|
||||
+ TimeUnit.SECONDS.toNanos(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS);
|
||||
CompletableFuture<Boolean> transition = studioTransitions.submit(() -> {
|
||||
IrisProject project = activeProject;
|
||||
if (project == null || !project.isOpen()) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"No active Studio project is available for teleport."));
|
||||
}
|
||||
return StudioOpenCoordinator.get().teleportPlayerToProject(
|
||||
project,
|
||||
target,
|
||||
admission,
|
||||
deadlineNanos);
|
||||
});
|
||||
transition.orTimeout(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
transition.whenComplete((ignored, failure) -> admission.set(false));
|
||||
return transition;
|
||||
}
|
||||
|
||||
public void open(VolmitSender sender, String dimm) {
|
||||
open(sender, 1337, dimm);
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ public class TreeSVC implements IrisService {
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return worldAccess.getEngine().getDimension().getFluidHeight();
|
||||
return worldFluidHeight(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -290,6 +290,10 @@ public class TreeSVC implements IrisService {
|
||||
}
|
||||
}
|
||||
|
||||
static int worldFluidHeight(Engine engine) {
|
||||
return engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single object placement (which may contain more than one object) for the requirements species, location &
|
||||
* size
|
||||
|
||||
@@ -29,7 +29,16 @@ import art.arcane.iris.engine.object.IrisDecorator;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisRiverOverride;
|
||||
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
|
||||
import art.arcane.iris.engine.object.IrisRiverWaterMode;
|
||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
@@ -61,8 +70,8 @@ import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"})
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime"})
|
||||
public class IrisComplex implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||
@@ -96,14 +105,22 @@ public class IrisComplex implements DataProvider {
|
||||
private ProceduralStream<IrisBiome> shoreBiomeStream;
|
||||
private ProceduralStream<IrisBiome> baseBiomeStream;
|
||||
private ProceduralStream<UUID> baseBiomeIDStream;
|
||||
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
|
||||
private ProceduralStream<IrisBiome> trueBiomeStream;
|
||||
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
|
||||
private ProceduralStream<Double> naturalHeightStream;
|
||||
private ProceduralStream<Double> heightStream;
|
||||
private ProceduralStream<Integer> roundedHeighteightStream;
|
||||
private ProceduralStream<Double> maxHeightStream;
|
||||
private ProceduralStream<Double> overlayStream;
|
||||
private ProceduralStream<Double> heightFluidStream;
|
||||
private ProceduralStream<Double> naturalSlopeStream;
|
||||
private ProceduralStream<Double> slopeStream;
|
||||
private ProceduralStream<IrisRiverSurfaceSample> riverSurfaceStream;
|
||||
private ProceduralStream<Double> riverDistanceStream;
|
||||
private ProceduralStream<Double> riverFlowStream;
|
||||
private ProceduralStream<Double> riverCarveWeightStream;
|
||||
private ProceduralStream<Double> riverWaterSurfaceStream;
|
||||
private ProceduralStream<Integer> topSurfaceStream;
|
||||
private ProceduralStream<IrisDecorator> terrainSurfaceDecoration;
|
||||
private ProceduralStream<IrisDecorator> terrainCeilingDecoration;
|
||||
@@ -118,6 +135,7 @@ public class IrisComplex implements DataProvider {
|
||||
private IrisRegion focusRegion;
|
||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||
private Set<IrisBiome> generatorBiomes;
|
||||
private IrisRiverRuntime riverRuntime;
|
||||
// Copy-on-write: reads happen per column on every burst thread; the synchronizedMap
|
||||
// monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map
|
||||
// per insert is cheap. Identity keying is load-bearing (IrisBiome is mutable/value-hashed).
|
||||
@@ -150,7 +168,7 @@ public class IrisComplex implements DataProvider {
|
||||
//@builder
|
||||
if (focusRegion != null) {
|
||||
prepareInferredBiomes(focusRegion);
|
||||
focusRegion.getAllBiomes(this).forEach(this::registerGenerators);
|
||||
focusRegion.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
} else {
|
||||
engine.getDimension().getRegions().forEach(regionKey -> {
|
||||
IrisRegion region = data.getRegionLoader().load(regionKey);
|
||||
@@ -158,7 +176,7 @@ public class IrisComplex implements DataProvider {
|
||||
return;
|
||||
}
|
||||
prepareInferredBiomes(region);
|
||||
region.getAllBiomes(this).forEach(this::registerGenerators);
|
||||
region.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
});
|
||||
}
|
||||
int interpolatorCount = generators.size();
|
||||
@@ -246,25 +264,85 @@ public class IrisComplex implements DataProvider {
|
||||
bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
||||
.convertAware2D(this::implode)
|
||||
.cache2D("baseBiomeStream", engine, cacheSize);
|
||||
heightStream = ProceduralStream.of((x, z) -> {
|
||||
naturalHeightStream = ProceduralStream.of((x, z) -> {
|
||||
IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z);
|
||||
return getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
|
||||
}, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize);
|
||||
}, Interpolated.DOUBLE).cache2DDouble("naturalHeightStream", engine, cacheSize);
|
||||
naturalSlopeStream = naturalHeightStream.slope(3)
|
||||
.cache2DDouble("naturalSlopeStream", engine, cacheSize);
|
||||
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||
b -> focusBiome))
|
||||
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
|
||||
.convertAware2D((h, x, z) ->
|
||||
fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight))
|
||||
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
|
||||
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
|
||||
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
|
||||
naturalHeightStream,
|
||||
bridgeStream,
|
||||
focusBiome,
|
||||
fluidHeight,
|
||||
engine.getDimension().getRivers().getWater().getMode()
|
||||
).cache2D("naturalOceanStream", engine, cacheSize);
|
||||
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
|
||||
engine.getSeedManager().getBodies(),
|
||||
engine.getDimension().getRivers(),
|
||||
data,
|
||||
(int) Math.round(fluidHeight),
|
||||
IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()),
|
||||
IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()),
|
||||
blockingRiverRoutingPossible(engine),
|
||||
variableMaxIncisionPossible(engine),
|
||||
biomeRiverOverridesPossible(focusBiome, generatorBiomes),
|
||||
(blockX, blockZ) -> naturalHeightBounds(engine, overlayNoise, blockX, blockZ),
|
||||
naturalHeightStream,
|
||||
naturalSlopeStream,
|
||||
naturalOceanStream,
|
||||
naturalTrueBiomeStream,
|
||||
regionStream
|
||||
));
|
||||
riverSurfaceStream = ProceduralStream.of(
|
||||
(x, z) -> riverRuntime.sample(x, z),
|
||||
Interpolated.of(
|
||||
IrisRiverSurfaceSample::terrainHeight,
|
||||
value -> IrisRiverSurfaceSample.none(value, fluidHeight)
|
||||
)
|
||||
)
|
||||
.cache2D("riverSurfaceStream", engine, cacheSize);
|
||||
} else {
|
||||
riverSurfaceStream = naturalHeightStream.convert(
|
||||
value -> IrisRiverSurfaceSample.none(value, fluidHeight)
|
||||
)
|
||||
.cache2D("riverSurfaceStream-disabled", engine, cacheSize);
|
||||
}
|
||||
heightStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::terrainHeight)
|
||||
.cache2DDouble("heightStream", engine, cacheSize);
|
||||
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.round();
|
||||
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
|
||||
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||
b -> focusBiome))
|
||||
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
|
||||
.convertAware2D((h, x, z) ->
|
||||
fixBiomeType(h, baseBiomeStream.get(x, z),
|
||||
regionStream.contextInjecting(engine, (c, xx, zz) -> c.getRegion().get(xx, zz)).get(x, z), x, z, fluidHeight))
|
||||
.cache2D("trueBiomeStream-focus", engine, cacheSize) : riverSurfaceStream
|
||||
.convertAware2D((sample, x, z) -> resolveRiverSurfaceBiome(sample, x, z))
|
||||
.cache2D("trueBiomeStream", engine, cacheSize);
|
||||
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||
.convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize);
|
||||
heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize);
|
||||
riverDistanceStream = riverSurfaceStream.convert(sample -> sample.river().present()
|
||||
? sample.river().distance()
|
||||
: Double.MAX_VALUE)
|
||||
.cache2DDouble("riverDistanceStream", engine, cacheSize);
|
||||
riverFlowStream = riverSurfaceStream.convert(sample -> (double) sample.river().flow())
|
||||
.cache2DDouble("riverFlowStream", engine, cacheSize);
|
||||
riverCarveWeightStream = riverSurfaceStream.convert(sample -> sample.river().carveWeight())
|
||||
.cache2DDouble("riverCarveWeightStream", engine, cacheSize);
|
||||
riverWaterSurfaceStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::waterSurfaceY)
|
||||
.cache2DDouble("riverWaterSurfaceStream", engine, cacheSize);
|
||||
heightFluidStream = ProceduralStream.ofDouble((x, z) -> Math.max(
|
||||
heightStream.get(x, z),
|
||||
riverWaterSurfaceStream.get(x, z)
|
||||
))
|
||||
.cache2DDouble("heightFluidStream", engine, cacheSize);
|
||||
maxHeightStream = ProceduralStream.ofDouble((x, z) -> height);
|
||||
terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize);
|
||||
@@ -290,6 +368,90 @@ public class IrisComplex implements DataProvider {
|
||||
//@done
|
||||
}
|
||||
|
||||
private boolean blockingRiverRoutingPossible(Engine engine) {
|
||||
if (blocksRiverRouting(focusRegion == null ? null : focusRegion.getRiverOverride())
|
||||
|| blocksRiverRouting(focusBiome == null ? null : focusBiome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
||||
for (IrisRegion loadedRegion : regions) {
|
||||
if (loadedRegion != null && blocksRiverRouting(loadedRegion.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
KList<IrisBiome> biomes = engine.getDimension().getReachableBiomes(engine);
|
||||
for (IrisBiome biome : biomes) {
|
||||
if (biome != null && blocksRiverRouting(biome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean blocksRiverRouting(IrisRiverOverride override) {
|
||||
return override != null && override.getRoutingPolicy() == IrisRiverRoutingPolicy.BLOCK;
|
||||
}
|
||||
|
||||
private boolean variableMaxIncisionPossible(Engine engine) {
|
||||
if (changesMaxIncision(focusRegion == null ? null : focusRegion.getRiverOverride())
|
||||
|| changesMaxIncision(focusBiome == null ? null : focusBiome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
||||
for (IrisRegion loadedRegion : regions) {
|
||||
if (loadedRegion != null && changesMaxIncision(loadedRegion.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
KList<IrisBiome> biomes = engine.getDimension().getReachableBiomes(engine);
|
||||
for (IrisBiome biome : biomes) {
|
||||
if (biome != null && changesMaxIncision(biome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean changesMaxIncision(IrisRiverOverride override) {
|
||||
if (override == null || override.getMaxIncisionMultiplier() == null) {
|
||||
return false;
|
||||
}
|
||||
double multiplier = override.getMaxIncisionMultiplier();
|
||||
return Double.isFinite(multiplier) && Double.compare(Math.max(0D, multiplier), 1D) != 0;
|
||||
}
|
||||
|
||||
static boolean biomeRiverOverridesPossible(IrisBiome focusBiome, Iterable<IrisBiome> naturalBiomes) {
|
||||
if (focusBiome != null) {
|
||||
return focusBiome.getRiverOverride() != null;
|
||||
}
|
||||
for (IrisBiome biome : naturalBiomes) {
|
||||
if (biome != null && biome.getRiverOverride() != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ProceduralStream<Boolean> createNaturalOceanStream(
|
||||
ProceduralStream<Double> naturalHeightStream,
|
||||
ProceduralStream<InferredType> bridgeStream,
|
||||
IrisBiome focusBiome,
|
||||
double fluidHeight,
|
||||
IrisRiverWaterMode waterMode
|
||||
) {
|
||||
if (focusBiome != null) {
|
||||
boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
|
||||
return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN);
|
||||
}
|
||||
if (waterMode == IrisRiverWaterMode.SEA_LEVEL) {
|
||||
return bridgeStream.convert(type -> type == InferredType.SEA);
|
||||
}
|
||||
return ProceduralStream.of(
|
||||
(x, z) -> naturalHeightStream.getDouble(x, z) < fluidHeight - 1D,
|
||||
Interpolated.BOOLEAN
|
||||
);
|
||||
}
|
||||
|
||||
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
|
||||
switch (type) {
|
||||
case CAVE:
|
||||
@@ -343,6 +505,63 @@ public class IrisComplex implements DataProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
private IrisBiome resolveRiverSurfaceBiome(IrisRiverSurfaceSample sample, double x, double z) {
|
||||
if (riverRuntime != null) {
|
||||
if (sample.subterranean()) {
|
||||
return fixBiomeType(
|
||||
sample.naturalHeight(),
|
||||
baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight
|
||||
);
|
||||
}
|
||||
IrisBiome riverBiome = riverRuntime.selectSurfaceBiome(sample, x, z);
|
||||
if (riverBiome != null) {
|
||||
return implode(riverBiome, x, z);
|
||||
}
|
||||
InferredType directFallback = directRiverFallback(sample.river());
|
||||
if (directFallback != null) {
|
||||
IrisBiome baseBiome = baseBiomeStream.get(x, z);
|
||||
return implode(baseBiome.withInferredType(directFallback), x, z);
|
||||
}
|
||||
if (sample.river().present()
|
||||
&& sample.river().state() == RiverRouteState.WET
|
||||
&& sample.river().section() == RiverSection.BANK) {
|
||||
return fixBiomeType(
|
||||
sample.terrainHeight(),
|
||||
baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
sample.waterSurfaceY()
|
||||
);
|
||||
}
|
||||
}
|
||||
return fixBiomeType(
|
||||
sample.terrainHeight(),
|
||||
baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight
|
||||
);
|
||||
}
|
||||
|
||||
static InferredType directRiverFallback(RiverSample river) {
|
||||
if (!river.present()) {
|
||||
return null;
|
||||
}
|
||||
if (river.state() == RiverRouteState.DRY) {
|
||||
return InferredType.LAND;
|
||||
}
|
||||
return switch (river.section()) {
|
||||
case CHANNEL, MOUTH -> InferredType.SEA;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
|
||||
IrisBiome resolved = resolveSurfaceBiome(
|
||||
height,
|
||||
@@ -491,6 +710,36 @@ public class IrisComplex implements DataProvider {
|
||||
return h;
|
||||
}
|
||||
|
||||
private NoiseBounds naturalHeightBounds(
|
||||
Engine engine,
|
||||
KList<IrisShapedGeneratorStyle> overlayNoise,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
double minimum = fluidHeight;
|
||||
double maximum = fluidHeight;
|
||||
for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) {
|
||||
NoiseBounds bounds = gridSampleBounds(
|
||||
engine,
|
||||
frozenInterpolators[interpolatorIndex],
|
||||
interpolatorIndex,
|
||||
frozenGenerators[interpolatorIndex],
|
||||
x,
|
||||
z
|
||||
);
|
||||
minimum += Math.min(bounds.min(), bounds.max());
|
||||
maximum += Math.max(bounds.min(), bounds.max());
|
||||
}
|
||||
for (IrisShapedGeneratorStyle style : overlayNoise) {
|
||||
minimum += Math.min(style.getMin(), style.getMax());
|
||||
maximum += Math.max(style.getMin(), style.getMax());
|
||||
}
|
||||
return new NoiseBounds(
|
||||
Math.max(0D, Math.min(engine.getHeight(), minimum)),
|
||||
Math.max(0D, Math.min(engine.getHeight(), maximum))
|
||||
);
|
||||
}
|
||||
|
||||
private double getHeight(Engine engine, IrisBiome b, double x, double z, long seed) {
|
||||
return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0);
|
||||
}
|
||||
@@ -922,6 +1171,8 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
public void close() {
|
||||
|
||||
if (riverRuntime != null) {
|
||||
riverRuntime.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ import art.arcane.iris.engine.mantle.MantlePass;
|
||||
import art.arcane.iris.engine.mantle.components.MantleCarvingComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleRiverHydrologyComponent;
|
||||
import art.arcane.iris.engine.mantle.components.IrisStructureComponent;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.matter.IrisMatterContext;
|
||||
@@ -88,6 +90,7 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
this.mantle = createMantle(engine);
|
||||
components = new KMap<>();
|
||||
registerComponent(new MantleCarvingComponent(this));
|
||||
registerComponent(new MantleRiverHydrologyComponent(this));
|
||||
object = new MantleObjectComponent(this);
|
||||
registerComponent(object);
|
||||
registerComponent(new MantleFloatingObjectComponent(this));
|
||||
@@ -126,9 +129,14 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
.mapToInt(MantleComponent::getRadius)
|
||||
.max()
|
||||
.orElse(0);
|
||||
int cumulative = downstreamBlockRadius + passBlockRadius;
|
||||
built[i] = new MantlePass(pass, Math.ceilDiv(cumulative, 16), downstreamBlockRadius);
|
||||
downstreamBlockRadius = cumulative;
|
||||
int passInputRadius = pass.stream()
|
||||
.filter(MantleComponent::isEnabled)
|
||||
.mapToInt(MantleComponent::getInputRadius)
|
||||
.max()
|
||||
.orElse(0);
|
||||
int invocationRadius = downstreamBlockRadius + passBlockRadius;
|
||||
built[i] = new MantlePass(pass, Math.ceilDiv(invocationRadius, 16), downstreamBlockRadius);
|
||||
downstreamBlockRadius = invocationRadius + passInputRadius;
|
||||
}
|
||||
|
||||
return List.of(built);
|
||||
@@ -157,7 +165,7 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
@Override
|
||||
public void hotload() {
|
||||
disabledFlags.reset();
|
||||
for (var component : registeredComponents.values()) {
|
||||
for (MantleComponent component : registeredComponents.values()) {
|
||||
component.hotload();
|
||||
component.setEnabled(!getDisabledFlags().contains(component.getFlag()));
|
||||
}
|
||||
@@ -171,10 +179,22 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
if (!getDimension().isCarvingEnabled()) {
|
||||
disabled.addIfMissing(ReservedFlag.CARVED);
|
||||
}
|
||||
if (disabled.contains(ReservedFlag.CARVED)
|
||||
|| !isRiverHydrologyEnabled(getDimension())) {
|
||||
disabled.addIfMissing(ReservedFlag.RIVER_HYDROLOGY);
|
||||
}
|
||||
return Set.copyOf(disabled);
|
||||
});
|
||||
}
|
||||
|
||||
static boolean isRiverHydrologyEnabled(IrisDimension dimension) {
|
||||
return MantleRiverHydrologyComponent.isEnabledFor(dimension);
|
||||
}
|
||||
|
||||
static boolean isRiverCaveHydrologyEnabled(IrisDimension dimension) {
|
||||
return MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MantleObjectComponent getObjectComponent() {
|
||||
return object;
|
||||
|
||||
@@ -68,8 +68,8 @@ public class UpperDimensionContext implements DataProvider {
|
||||
engine.getDimension(),
|
||||
engine.getData(),
|
||||
chunkHeight,
|
||||
complex.getHeightStream(),
|
||||
complex.getTrueBiomeStream(),
|
||||
complex.getNaturalHeightStream(),
|
||||
complex.getNaturalTrueBiomeStream(),
|
||||
complex.getRegionStream(),
|
||||
complex.getRockStream(),
|
||||
true
|
||||
@@ -94,7 +94,7 @@ public class UpperDimensionContext implements DataProvider {
|
||||
upperDim.getRegions().forEach(regionKey -> {
|
||||
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
||||
if (region != null) {
|
||||
region.getAllBiomes(dataProvider).forEach(biome -> {
|
||||
region.getNaturalBiomes(dataProvider).forEach(biome -> {
|
||||
allBiomes.add(biome);
|
||||
biome.getGenerators().forEach(link -> {
|
||||
IrisGenerator gen = link.getCachedGenerator(dataProvider);
|
||||
|
||||
@@ -27,6 +27,8 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||
import art.arcane.iris.engine.framework.EngineDecorator;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
@@ -64,6 +66,12 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
seaFloorDecorator = new IrisSeaFloorDecorator(getEngine());
|
||||
}
|
||||
|
||||
static boolean shouldDecorateShoreline(IrisRiverSurfaceSample sample, int height) {
|
||||
return !sample.subterranean()
|
||||
&& height == Math.round(sample.waterSurfaceY())
|
||||
&& (!sample.river().present() || sample.river().state() != RiverRouteState.DRY);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
@Override
|
||||
public void onActuate(int x, int z, Hunk<PlatformBlockState> output, boolean multicore, ChunkContext context) {
|
||||
@@ -86,23 +94,25 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
height = context.getRoundedHeight(i, j);
|
||||
biome = context.getBiome().get(i, j);
|
||||
cave = shouldRay ? context.getCave().get(i, j) : null;
|
||||
IrisRiverSurfaceSample riverSurface = getComplex().getRiverSurfaceStream().get(realX, realZ);
|
||||
int surfaceFluidHeight = (int) Math.round(riverSurface.waterSurfaceY());
|
||||
|
||||
if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))
|
||||
if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j))
|
||||
&& height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, j))) {
|
||||
getSeaSurfaceDecorator().decorate(i, j,
|
||||
realX, Math.round(i + 1), Math.round(x + i - 1),
|
||||
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
|
||||
output, biome, getDimension().getFluidHeight(), getEngine().getHeight());
|
||||
output, biome, surfaceFluidHeight, getEngine().getHeight());
|
||||
getSeaFloorDecorator().decorate(i, j,
|
||||
realX, realZ, output, biome, height + 1,
|
||||
getDimension().getFluidHeight() + 1);
|
||||
surfaceFluidHeight + 1);
|
||||
}
|
||||
|
||||
if (height == getDimension().getFluidHeight()) {
|
||||
if (shouldDecorateShoreline(riverSurface, height)) {
|
||||
getShoreLineDecorator().decorate(i, j,
|
||||
realX, Math.round(x + i + 1), Math.round(x + i - 1),
|
||||
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
|
||||
|
||||
@@ -67,10 +67,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
getEngine().getMetrics().getTerrain().put(p.getMilliseconds());
|
||||
}
|
||||
|
||||
private int fluidOrHeight(int height) {
|
||||
return Math.max(getDimension().getFluidHeight(), height);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is calling 1/16th of a chunk x/z slice. It is a plane from sky to bedrock 1 thick in the x direction.
|
||||
*
|
||||
@@ -92,8 +88,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
IrisData data = getData();
|
||||
IrisComplex complex = getComplex();
|
||||
RNG localRng = rng;
|
||||
int fluidHeight = dimension.getFluidHeight();
|
||||
int clampedFluidHeight = Math.min(chunkHeight, fluidHeight);
|
||||
boolean bedrockEnabled = dimension.isBedrock();
|
||||
boolean hideOres = dimension.isHideOresForHiddenOre();
|
||||
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
||||
@@ -114,7 +108,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
IrisBiome biome = biomeCache.get(xf, zf);
|
||||
IrisRegion region = regionCache.get(xf, zf);
|
||||
int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf));
|
||||
int hf = Math.max(clampedFluidHeight, he);
|
||||
int surfaceFluidHeight = Math.min(
|
||||
chunkHeight,
|
||||
(int) Math.round(complex.getRiverWaterSurfaceStream().get(realX, realZ))
|
||||
);
|
||||
int hf = Math.max(surfaceFluidHeight, he);
|
||||
if (hf < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -40,16 +40,17 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
@Override
|
||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
|
||||
if (height != getDimension().getFluidHeight()) {
|
||||
double localFluidHeight = getComplex().getRiverWaterSurfaceStream().get(realX, realZ);
|
||||
if (height != Math.round(localFluidHeight)) {
|
||||
return;
|
||||
}
|
||||
|
||||
double complexFluidHeight = getComplex().getFluidHeight();
|
||||
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
|
||||
if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight
|
||||
&& Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight
|
||||
&& Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight
|
||||
&& Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) {
|
||||
ProceduralStream<Double> fluidStream = getComplex().getRiverWaterSurfaceStream();
|
||||
if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ))
|
||||
&& Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ))
|
||||
&& Math.round(heightStream.get(realX, realZ1)) >= Math.round(fluidStream.get(realX, realZ1))
|
||||
&& Math.round(heightStream.get(realX, realZ_1)) >= Math.round(fluidStream.get(realX, realZ_1))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
@BlockCoordinates
|
||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||
Hunk<PlatformBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
|
||||
int fluidHeight = getDimension().getFluidHeight();
|
||||
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
|
||||
if (inferredType == InferredType.SHORE && height < fluidHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -244,6 +246,14 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
|
||||
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(
|
||||
getMantle().getMantle(), x, y, z);
|
||||
if (hydrology != null && !hydrology.floodedBiomeKey().isEmpty()) {
|
||||
IrisBiome biome = getData().getBiomeLoader().load(hydrology.floodedBiomeKey());
|
||||
if (biome != null) {
|
||||
return biome;
|
||||
}
|
||||
}
|
||||
MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class);
|
||||
|
||||
if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) {
|
||||
|
||||
+4
-1
@@ -157,7 +157,10 @@ public final class NativeStructurePlacementPlanner {
|
||||
}
|
||||
|
||||
static boolean isSubmerged(Engine engine, int blockX, int blockZ) {
|
||||
return engine.getHeight(blockX, blockZ, true) < engine.getDimension().getFluidHeight();
|
||||
int localFluidHeight = engine.getComplex() == null
|
||||
? engine.getDimension().getFluidHeight()
|
||||
: (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
||||
return engine.getHeight(blockX, blockZ, true) < localFluidHeight;
|
||||
}
|
||||
|
||||
private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) {
|
||||
|
||||
+26
-1
@@ -3,6 +3,8 @@ package art.arcane.iris.engine.framework;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisStructureAnchorMode;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
|
||||
@@ -286,10 +288,12 @@ public final class StructureCaveAnchorResolver {
|
||||
int mantleY,
|
||||
int blockZ
|
||||
) {
|
||||
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||
MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ);
|
||||
return acceptsAnchorFluid(
|
||||
placement.isUnderwater(),
|
||||
cavern,
|
||||
hydrology,
|
||||
mantleY,
|
||||
engine.getDimension().getCaveLavaHeight());
|
||||
}
|
||||
@@ -300,6 +304,19 @@ public final class StructureCaveAnchorResolver {
|
||||
int mantleY,
|
||||
int defaultLavaHeight
|
||||
) {
|
||||
return acceptsAnchorFluid(underwater, cavern, null, mantleY, defaultLavaHeight);
|
||||
}
|
||||
|
||||
static boolean acceptsAnchorFluid(
|
||||
boolean underwater,
|
||||
MatterCavern cavern,
|
||||
RiverCaveHydrology hydrology,
|
||||
int mantleY,
|
||||
int defaultLavaHeight
|
||||
) {
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
return false;
|
||||
}
|
||||
if (cavern == null || !cavern.isCavern()) {
|
||||
return false;
|
||||
}
|
||||
@@ -313,7 +330,15 @@ public final class StructureCaveAnchorResolver {
|
||||
}
|
||||
|
||||
private static MatterCavern cavernAt(Engine engine, int blockX, int mantleY, int blockZ) {
|
||||
return engine.getMantle().getMantle().get(blockX, mantleY, blockZ, MatterCavern.class);
|
||||
MatterCavern baseline = engine.getMantle().getMantle()
|
||||
.get(blockX, mantleY, blockZ, MatterCavern.class);
|
||||
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||
return hydrology == null ? baseline : hydrology.asCavern();
|
||||
}
|
||||
|
||||
private static RiverCaveHydrology hydrologyAt(Engine engine, int blockX, int mantleY, int blockZ) {
|
||||
return RiverCaveHydrologyStorage.getIfPresent(
|
||||
engine.getMantle().getMantle(), blockX, mantleY, blockZ);
|
||||
}
|
||||
|
||||
static int toMantleY(int worldY, int worldMinHeight) {
|
||||
|
||||
@@ -6,12 +6,17 @@ import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class StructurePlacementScope {
|
||||
private static final List<CachedScopeIndex> SCOPE_INDEXES = new ArrayList<>();
|
||||
|
||||
private StructurePlacementScope() {
|
||||
}
|
||||
|
||||
@@ -22,20 +27,93 @@ public final class StructurePlacementScope {
|
||||
int blockZ = (chunkZ << 4) + 8;
|
||||
KList<IrisStructurePlacement> placements = new KList<>();
|
||||
Set<IrisStructurePlacement> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
ScopeIndex index = scopeIndex(activeEngine);
|
||||
if (complex != null) {
|
||||
IrisBiome biome = complex.getTrueBiomeStream().get(blockX, blockZ);
|
||||
IrisBiome caveBiome = complex.getCaveBiomeStream().get(blockX, blockZ);
|
||||
IrisRegion region = complex.getRegionStream().get(blockX, blockZ);
|
||||
addUnique(placements, seen, biome == null ? null : biome.getStructures());
|
||||
addCaveUnique(placements, seen, caveBiome == null ? null : caveBiome.getStructures());
|
||||
addUnique(placements, seen, region == null ? null : region.getStructures());
|
||||
if (index.biome()) {
|
||||
IrisBiome biome = complex.getTrueBiomeStream().get(blockX, blockZ);
|
||||
addUnique(placements, seen, biome == null ? null : biome.getStructures());
|
||||
}
|
||||
if (index.caveBiome()) {
|
||||
IrisBiome caveBiome = complex.getCaveBiomeStream().get(blockX, blockZ);
|
||||
addCaveUnique(placements, seen, caveBiome == null ? null : caveBiome.getStructures());
|
||||
}
|
||||
if (index.region()) {
|
||||
IrisRegion region = complex.getRegionStream().get(blockX, blockZ);
|
||||
addUnique(placements, seen, region == null ? null : region.getStructures());
|
||||
}
|
||||
}
|
||||
if (activeEngine.getDimension() != null) {
|
||||
if (index.dimension()) {
|
||||
addUnique(placements, seen, activeEngine.getDimension().getStructures());
|
||||
}
|
||||
return placements;
|
||||
}
|
||||
|
||||
private static ScopeIndex scopeIndex(Engine engine) {
|
||||
int revision = engine.getCacheID();
|
||||
synchronized (SCOPE_INDEXES) {
|
||||
for (int i = SCOPE_INDEXES.size() - 1; i >= 0; i--) {
|
||||
CachedScopeIndex cached = SCOPE_INDEXES.get(i);
|
||||
Engine indexedEngine = cached.engine().get();
|
||||
if (indexedEngine == null) {
|
||||
SCOPE_INDEXES.remove(i);
|
||||
continue;
|
||||
}
|
||||
if (indexedEngine == engine) {
|
||||
if (cached.revision() == revision) {
|
||||
return cached.index();
|
||||
}
|
||||
SCOPE_INDEXES.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ScopeIndex index = buildScopeIndex(engine);
|
||||
SCOPE_INDEXES.add(new CachedScopeIndex(new WeakReference<>(engine), revision, index));
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
private static ScopeIndex buildScopeIndex(Engine engine) {
|
||||
boolean biome = false;
|
||||
boolean caveBiome = false;
|
||||
KList<IrisBiome> allBiomes = engine.getAllBiomes();
|
||||
if (allBiomes == null) {
|
||||
biome = true;
|
||||
caveBiome = true;
|
||||
} else {
|
||||
for (IrisBiome candidate : allBiomes) {
|
||||
KList<IrisStructurePlacement> structures = candidate.getStructures();
|
||||
if (structures == null || structures.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
biome = true;
|
||||
for (IrisStructurePlacement placement : structures) {
|
||||
if (placement != null && placement.resolvedAnchor().isCave()) {
|
||||
caveBiome = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean region = false;
|
||||
if (engine.getDimension() != null) {
|
||||
KList<IrisRegion> allRegions = engine.getDimension().getAllRegions(engine);
|
||||
if (allRegions == null) {
|
||||
region = true;
|
||||
} else {
|
||||
for (IrisRegion candidate : allRegions) {
|
||||
if (candidate.getStructures() != null && !candidate.getStructures().isEmpty()) {
|
||||
region = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean dimension = engine.getDimension() != null
|
||||
&& engine.getDimension().getStructures() != null
|
||||
&& !engine.getDimension().getStructures().isEmpty();
|
||||
return new ScopeIndex(biome, caveBiome, region, dimension);
|
||||
}
|
||||
|
||||
private static void addUnique(KList<IrisStructurePlacement> destination,
|
||||
Set<IrisStructurePlacement> seen,
|
||||
KList<IrisStructurePlacement> source) {
|
||||
@@ -61,4 +139,10 @@ public final class StructurePlacementScope {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedScopeIndex(WeakReference<Engine> engine, int revision, ScopeIndex index) {
|
||||
}
|
||||
|
||||
private record ScopeIndex(boolean biome, boolean caveBiome, boolean region, boolean dimension) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -112,7 +111,7 @@ public class WorldObjectPlacer implements IObjectPlacer {
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return mantle.getMantle().get(x, y, z, MatterCavern.class) != null;
|
||||
return mantle.isCarved(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,63 +18,712 @@
|
||||
|
||||
package art.arcane.iris.engine.framework.render;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.Arrays;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
public class IrisRenderer {
|
||||
private static final int BLUE = Color.BLUE.getRGB();
|
||||
private static final int YELLOW = Color.YELLOW.getRGB();
|
||||
private static final int GREEN = Color.GREEN.getRGB();
|
||||
public final class IrisRenderer {
|
||||
private static final int BLUE = new Color(45, 91, 156).getRGB();
|
||||
private static final int YELLOW = new Color(211, 164, 67).getRGB();
|
||||
private static final int GREEN = new Color(78, 137, 83).getRGB();
|
||||
private static final int RIVER_CHANNEL = new Color(48, 112, 190).getRGB();
|
||||
private static final int RIVER_MOUTH = new Color(54, 164, 205).getRGB();
|
||||
private static final int RIVER_BANK = new Color(92, 146, 78).getRGB();
|
||||
private static final int DRY_CHANNEL = new Color(171, 128, 68).getRGB();
|
||||
private static final int DRY_BANK = new Color(132, 105, 62).getRGB();
|
||||
private static final int NO_RIVER = new Color(28, 31, 38).getRGB();
|
||||
private static final int DEEP_WATER = new Color(20, 48, 92).getRGB();
|
||||
private static final int SHALLOW_WATER = new Color(50, 112, 154).getRGB();
|
||||
private static final int LOWLAND = new Color(78, 128, 76).getRGB();
|
||||
private static final int HIGHLAND = new Color(151, 139, 92).getRGB();
|
||||
private static final int ROCK = new Color(126, 119, 112).getRGB();
|
||||
private static final int SNOW = new Color(226, 230, 232).getRGB();
|
||||
|
||||
private final Engine renderer;
|
||||
|
||||
public IrisRenderer(Engine renderer) {
|
||||
this.renderer = renderer;
|
||||
this.renderer = Objects.requireNonNull(renderer, "renderer");
|
||||
}
|
||||
|
||||
public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) {
|
||||
return render(sx, sz, size, resolution, currentType, () -> false);
|
||||
}
|
||||
|
||||
public BufferedImage render(
|
||||
double sx,
|
||||
double sz,
|
||||
double size,
|
||||
int resolution,
|
||||
RenderType currentType,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
return render(sx, sz, size, resolution, currentType, cancelled, false);
|
||||
}
|
||||
|
||||
public BufferedImage renderStudio(
|
||||
double sx,
|
||||
double sz,
|
||||
double size,
|
||||
int resolution,
|
||||
RenderType currentType,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
return render(sx, sz, size, resolution, currentType, cancelled, true);
|
||||
}
|
||||
|
||||
private BufferedImage render(
|
||||
double sx,
|
||||
double sz,
|
||||
double size,
|
||||
int resolution,
|
||||
RenderType currentType,
|
||||
BooleanSupplier cancelled,
|
||||
boolean studio
|
||||
) {
|
||||
if (!Double.isFinite(sx) || !Double.isFinite(sz) || !Double.isFinite(size) || size <= 0D) {
|
||||
throw new IllegalArgumentException("Vision render coordinates and size must be finite");
|
||||
}
|
||||
if (resolution < 1) {
|
||||
throw new IllegalArgumentException("Vision render resolution must be positive");
|
||||
}
|
||||
Objects.requireNonNull(currentType, "currentType");
|
||||
Objects.requireNonNull(cancelled, "cancelled");
|
||||
checkCancelled(cancelled);
|
||||
|
||||
BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB);
|
||||
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||
BiFunction<Double, Double, Integer> colorFunction = (d, dx) -> 0;
|
||||
|
||||
switch (currentType) {
|
||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getTrueBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case BIOME_LAND ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getLandBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case BIOME_SEA ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getSeaBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case REGION ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getRegionStream().get(x, z).getColor(renderer.getComplex(), currentType).getRGB();
|
||||
case CAVE_LAND ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getCaveBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case HEIGHT ->
|
||||
colorFunction = (x, z) -> Color.getHSBColor(renderer.getComplex().getHeightStream().get(x, z).floatValue(), 1f, 1f).getRGB();
|
||||
case CONTINENT -> colorFunction = (x, z) -> {
|
||||
IrisBiome b = renderer.getBiome((int) Math.round(x), renderer.getMaxHeight() - 1, (int) Math.round(z));
|
||||
IrisBiomeGeneratorLink g = b.getGenerators().get(0);
|
||||
if (g.getMax() <= 0) return BLUE;
|
||||
if (g.getMin() < 0) return YELLOW;
|
||||
return GREEN;
|
||||
};
|
||||
double step = size / resolution;
|
||||
if (studio && currentType == RenderType.HEIGHT) {
|
||||
renderHeightAtlas(pixels, resolution, sx, sz, step, renderer, cancelled);
|
||||
return image;
|
||||
}
|
||||
PixelShader shader = shader(currentType, step, studio);
|
||||
if (studio && currentType == RenderType.RIVER) {
|
||||
Arrays.fill(pixels, NO_RIVER);
|
||||
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, false);
|
||||
return image;
|
||||
}
|
||||
if (studio && adaptiveStudioType(currentType)) {
|
||||
renderAdaptiveAtlas(pixels, resolution, sx, sz, step, shader, cancelled);
|
||||
if (currentType == RenderType.BIOME) {
|
||||
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, true);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
int groupSize = sampleGroup(step, resolution);
|
||||
|
||||
double x, z;
|
||||
for (int i = 0; i < resolution; i++) {
|
||||
x = IrisInterpolation.lerp(sx, sx + size, (double) i / (double) resolution);
|
||||
for (int j = 0; j < resolution; j++) {
|
||||
z = IrisInterpolation.lerp(sz, sz + size, (double) j / (double) resolution);
|
||||
pixels[j * resolution + i] = colorFunction.apply(x, z);
|
||||
for (int groupZ = 0; groupZ < resolution; groupZ += groupSize) {
|
||||
checkCancelled(cancelled);
|
||||
int maximumZ = Math.min(resolution, groupZ + groupSize);
|
||||
for (int groupX = 0; groupX < resolution; groupX += groupSize) {
|
||||
checkCancelled(cancelled);
|
||||
int maximumX = Math.min(resolution, groupX + groupSize);
|
||||
for (int pixelZ = groupZ; pixelZ < maximumZ; pixelZ++) {
|
||||
double z = sz + step * pixelZ;
|
||||
int row = pixelZ * resolution;
|
||||
for (int pixelX = groupX; pixelX < maximumX; pixelX++) {
|
||||
checkCancelled(cancelled);
|
||||
double x = sx + step * pixelX;
|
||||
pixels[row + pixelX] = shader.color(x, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
public static int riverColor(RiverSection section) {
|
||||
Objects.requireNonNull(section, "section");
|
||||
return switch (section) {
|
||||
case CHANNEL -> RIVER_CHANNEL;
|
||||
case MOUTH -> RIVER_MOUTH;
|
||||
case BANK -> RIVER_BANK;
|
||||
case DRY_CHANNEL -> DRY_CHANNEL;
|
||||
case DRY_BANK -> DRY_BANK;
|
||||
case NONE -> NO_RIVER;
|
||||
};
|
||||
}
|
||||
|
||||
public static int heightColor(double height, double maximumHeight, double fluidHeight) {
|
||||
double boundedMaximum = Math.max(1D, maximumHeight);
|
||||
double boundedHeight = clamp(height, 0D, boundedMaximum);
|
||||
double boundedFluid = clamp(fluidHeight, 0D, boundedMaximum);
|
||||
if (boundedHeight <= boundedFluid && boundedFluid > 0D) {
|
||||
return blend(DEEP_WATER, SHALLOW_WATER, boundedHeight / boundedFluid);
|
||||
}
|
||||
|
||||
double landRange = Math.max(1D, boundedMaximum - boundedFluid);
|
||||
double land = clamp((boundedHeight - boundedFluid) / landRange, 0D, 1D);
|
||||
if (land < 0.45D) {
|
||||
return blend(LOWLAND, HIGHLAND, land / 0.45D);
|
||||
}
|
||||
if (land < 0.78D) {
|
||||
return blend(HIGHLAND, ROCK, (land - 0.45D) / 0.33D);
|
||||
}
|
||||
return blend(ROCK, SNOW, (land - 0.78D) / 0.22D);
|
||||
}
|
||||
|
||||
static int sampleGroup(double step, int resolution) {
|
||||
double absoluteStep = Math.abs(step);
|
||||
if (!Double.isFinite(absoluteStep) || absoluteStep <= 0D) {
|
||||
return 1;
|
||||
}
|
||||
return Math.max(1, Math.min(resolution, (int) Math.floor(16D / absoluteStep)));
|
||||
}
|
||||
|
||||
private PixelShader shader(RenderType currentType, double step, boolean studio) {
|
||||
IrisComplex complex = renderer.getComplex();
|
||||
return switch (currentType) {
|
||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> biomeShader(
|
||||
studio ? complex.getBaseBiomeStream() : complex.getTrueBiomeStream(), currentType);
|
||||
case BIOME_LAND -> biomeShader(complex.getLandBiomeStream(), currentType);
|
||||
case BIOME_SEA -> biomeShader(complex.getSeaBiomeStream(), currentType);
|
||||
case REGION -> regionShader(complex, currentType);
|
||||
case CAVE_LAND -> biomeShader(complex.getCaveBiomeStream(), currentType);
|
||||
case HEIGHT -> heightShader(studio ? complex.getNaturalHeightStream() : complex.getHeightStream());
|
||||
case RIVER -> (double x, double z) -> riverColor(complex, x, z, step);
|
||||
case CONTINENT -> studio
|
||||
? continentShader(complex.getBaseBiomeStream())
|
||||
: this::continentColor;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean adaptiveStudioType(RenderType type) {
|
||||
return switch (type) {
|
||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND,
|
||||
CONTINENT -> true;
|
||||
case HEIGHT, RIVER -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static void renderAdaptiveAtlas(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
PixelShader shader,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
|
||||
int blockPixels = Integer.highestOneBit(maximumPixels);
|
||||
AdaptiveSampler sampler = new AdaptiveSampler(pixels, resolution, startX, startZ, step, shader, cancelled);
|
||||
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
|
||||
int height = Math.min(blockPixels, resolution - pixelZ);
|
||||
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
|
||||
sampler.render(pixelX, pixelZ, Math.min(blockPixels, resolution - pixelX), height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderHeightAtlas(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
Engine engine,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
double fluidHeight = dimension == null ? 0D : dimension.getFluidHeight();
|
||||
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
|
||||
int blockPixels = Integer.highestOneBit(maximumPixels);
|
||||
HeightSampler sampler = new HeightSampler(
|
||||
pixels,
|
||||
resolution,
|
||||
startX,
|
||||
startZ,
|
||||
step,
|
||||
complex.getNaturalHeightStream(),
|
||||
engine.getHeight(),
|
||||
fluidHeight,
|
||||
cancelled
|
||||
);
|
||||
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
|
||||
int height = Math.min(blockPixels, resolution - pixelZ);
|
||||
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
|
||||
sampler.render(pixelX, pixelZ, Math.min(blockPixels, resolution - pixelX), height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderRiverAtlas(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
IrisComplex complex,
|
||||
BooleanSupplier cancelled,
|
||||
boolean composite
|
||||
) {
|
||||
IrisRiverRuntime runtime = complex.getRiverRuntime();
|
||||
if (runtime == null) {
|
||||
return;
|
||||
}
|
||||
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
|
||||
int blockPixels = Integer.highestOneBit(maximumPixels);
|
||||
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
|
||||
int height = Math.min(blockPixels, resolution - pixelZ);
|
||||
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
|
||||
renderRiverBlock(
|
||||
pixels,
|
||||
resolution,
|
||||
startX,
|
||||
startZ,
|
||||
step,
|
||||
runtime,
|
||||
cancelled,
|
||||
composite,
|
||||
pixelX,
|
||||
pixelZ,
|
||||
Math.min(blockPixels, resolution - pixelX),
|
||||
height
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderRiverBlock(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
IrisRiverRuntime runtime,
|
||||
BooleanSupplier cancelled,
|
||||
boolean composite,
|
||||
int pixelX,
|
||||
int pixelZ,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
checkCancelled(cancelled);
|
||||
RiverSample sample = runtime.sampleFootprint(
|
||||
startX + pixelX * step,
|
||||
startZ + pixelZ * step,
|
||||
startX + (pixelX + width) * step,
|
||||
startZ + (pixelZ + height) * step
|
||||
);
|
||||
if (!sample.present()) {
|
||||
return;
|
||||
}
|
||||
if (width == 1 && height == 1) {
|
||||
int index = pixelZ * resolution + pixelX;
|
||||
int color = riverColor(sample.section());
|
||||
pixels[index] = composite ? riverCompositeColor(pixels[index], sample.section(), color) : color;
|
||||
return;
|
||||
}
|
||||
int leftWidth = Math.max(1, width / 2);
|
||||
int rightWidth = width - leftWidth;
|
||||
int topHeight = Math.max(1, height / 2);
|
||||
int bottomHeight = height - topHeight;
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
pixelX, pixelZ, leftWidth, topHeight);
|
||||
if (rightWidth > 0) {
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
pixelX + leftWidth, pixelZ, rightWidth, topHeight);
|
||||
}
|
||||
if (bottomHeight > 0) {
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
|
||||
if (rightWidth > 0) {
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int riverCompositeColor(int base, RiverSection section, int river) {
|
||||
return switch (section) {
|
||||
case CHANNEL, MOUTH -> river;
|
||||
case BANK -> blend(base, river, 0.68D);
|
||||
case DRY_CHANNEL -> blend(base, river, 0.88D);
|
||||
case DRY_BANK -> blend(base, river, 0.58D);
|
||||
case NONE -> base;
|
||||
};
|
||||
}
|
||||
|
||||
private PixelShader biomeShader(ProceduralStream<IrisBiome> stream, RenderType currentType) {
|
||||
IdentityHashMap<IrisBiome, Integer> colors = new IdentityHashMap<>();
|
||||
return (double x, double z) -> {
|
||||
IrisBiome biome = stream.get(x, z);
|
||||
Integer color = colors.get(biome);
|
||||
if (color == null) {
|
||||
color = biome.getColor(renderer, currentType).getRGB();
|
||||
colors.put(biome, color);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
}
|
||||
|
||||
private PixelShader regionShader(IrisComplex complex, RenderType currentType) {
|
||||
ProceduralStream<IrisRegion> stream = complex.getRegionStream();
|
||||
IdentityHashMap<IrisRegion, Integer> colors = new IdentityHashMap<>();
|
||||
return (double x, double z) -> {
|
||||
IrisRegion region = stream.get(x, z);
|
||||
Integer color = colors.get(region);
|
||||
if (color == null) {
|
||||
color = region.getColor(complex, currentType).getRGB();
|
||||
colors.put(region, color);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
}
|
||||
|
||||
private PixelShader heightShader(ProceduralStream<Double> stream) {
|
||||
double maximumHeight = renderer.getHeight();
|
||||
IrisDimension dimension = renderer.getDimension();
|
||||
double fluidHeight = dimension == null ? 0D : dimension.getFluidHeight();
|
||||
return (double x, double z) -> heightColor(stream.getDouble(x, z), maximumHeight, fluidHeight);
|
||||
}
|
||||
|
||||
private int riverColor(IrisComplex complex, double x, double z, double step) {
|
||||
IrisRiverRuntime runtime = complex.getRiverRuntime();
|
||||
if (runtime == null) {
|
||||
return riverColor(RiverSection.NONE);
|
||||
}
|
||||
double endX = x + step;
|
||||
double endZ = z + step;
|
||||
RiverSample sample = runtime.sampleFootprint(
|
||||
StrictMath.min(x, endX),
|
||||
StrictMath.min(z, endZ),
|
||||
StrictMath.max(x, endX),
|
||||
StrictMath.max(z, endZ)
|
||||
);
|
||||
return riverColor(sample.section());
|
||||
}
|
||||
|
||||
private int continentColor(double x, double z) {
|
||||
IrisBiome biome = renderer.getBiome(
|
||||
(int) Math.round(x),
|
||||
renderer.getMaxHeight() - 1,
|
||||
(int) Math.round(z)
|
||||
);
|
||||
return continentColor(biome);
|
||||
}
|
||||
|
||||
private PixelShader continentShader(ProceduralStream<IrisBiome> stream) {
|
||||
IdentityHashMap<IrisBiome, Integer> colors = new IdentityHashMap<>();
|
||||
return (double x, double z) -> {
|
||||
IrisBiome biome = stream.get(x, z);
|
||||
Integer color = colors.get(biome);
|
||||
if (color == null) {
|
||||
color = continentColor(biome);
|
||||
colors.put(biome, color);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
}
|
||||
|
||||
private int continentColor(IrisBiome biome) {
|
||||
if (biome == null) {
|
||||
return GREEN;
|
||||
}
|
||||
List<IrisBiomeGeneratorLink> generators = biome.getGenerators();
|
||||
if (generators.isEmpty()) {
|
||||
return GREEN;
|
||||
}
|
||||
IrisBiomeGeneratorLink generator = generators.get(0);
|
||||
if (generator.getMax() <= 0D) {
|
||||
return BLUE;
|
||||
}
|
||||
if (generator.getMin() < 0D) {
|
||||
return YELLOW;
|
||||
}
|
||||
return GREEN;
|
||||
}
|
||||
|
||||
private static int blend(int first, int second, double progress) {
|
||||
double bounded = clamp(progress, 0D, 1D);
|
||||
int red = (int) Math.round(((first >> 16) & 0xFF) * (1D - bounded) + ((second >> 16) & 0xFF) * bounded);
|
||||
int green = (int) Math.round(((first >> 8) & 0xFF) * (1D - bounded) + ((second >> 8) & 0xFF) * bounded);
|
||||
int blue = (int) Math.round((first & 0xFF) * (1D - bounded) + (second & 0xFF) * bounded);
|
||||
return (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
private static double clamp(double value, double minimum, double maximum) {
|
||||
return Math.max(minimum, Math.min(maximum, value));
|
||||
}
|
||||
|
||||
private static void checkCancelled(BooleanSupplier cancelled) {
|
||||
if (cancelled.getAsBoolean() || Thread.currentThread().isInterrupted()) {
|
||||
throw new CancellationException("Vision render cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface PixelShader {
|
||||
int color(double x, double z);
|
||||
}
|
||||
|
||||
private static final class AdaptiveSampler {
|
||||
private final int[] pixels;
|
||||
private final boolean[] sampled;
|
||||
private final int resolution;
|
||||
private final double startX;
|
||||
private final double startZ;
|
||||
private final double step;
|
||||
private final PixelShader shader;
|
||||
private final BooleanSupplier cancelled;
|
||||
|
||||
private AdaptiveSampler(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
PixelShader shader,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
this.pixels = pixels;
|
||||
this.sampled = new boolean[pixels.length];
|
||||
this.resolution = resolution;
|
||||
this.startX = startX;
|
||||
this.startZ = startZ;
|
||||
this.step = step;
|
||||
this.shader = shader;
|
||||
this.cancelled = cancelled;
|
||||
}
|
||||
|
||||
private void render(int pixelX, int pixelZ, int width, int height) {
|
||||
checkCancelled(cancelled);
|
||||
if (width == 1 && height == 1) {
|
||||
sample(pixelX, pixelZ);
|
||||
return;
|
||||
}
|
||||
int maximumX = pixelX + width - 1;
|
||||
int maximumZ = pixelZ + height - 1;
|
||||
int centerX = pixelX + width / 2;
|
||||
int centerZ = pixelZ + height / 2;
|
||||
int color = sample(pixelX, pixelZ);
|
||||
if (sample(maximumX, pixelZ) == color
|
||||
&& sample(pixelX, maximumZ) == color
|
||||
&& sample(maximumX, maximumZ) == color
|
||||
&& sample(centerX, centerZ) == color) {
|
||||
fill(pixelX, pixelZ, width, height, color);
|
||||
return;
|
||||
}
|
||||
int leftWidth = Math.max(1, width / 2);
|
||||
int rightWidth = width - leftWidth;
|
||||
int topHeight = Math.max(1, height / 2);
|
||||
int bottomHeight = height - topHeight;
|
||||
render(pixelX, pixelZ, leftWidth, topHeight);
|
||||
if (rightWidth > 0) {
|
||||
render(pixelX + leftWidth, pixelZ, rightWidth, topHeight);
|
||||
}
|
||||
if (bottomHeight > 0) {
|
||||
render(pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
|
||||
if (rightWidth > 0) {
|
||||
render(pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int sample(int pixelX, int pixelZ) {
|
||||
int index = pixelZ * resolution + pixelX;
|
||||
if (!sampled[index]) {
|
||||
pixels[index] = shader.color(startX + pixelX * step, startZ + pixelZ * step);
|
||||
sampled[index] = true;
|
||||
}
|
||||
return pixels[index];
|
||||
}
|
||||
|
||||
private void fill(int pixelX, int pixelZ, int width, int height, int color) {
|
||||
for (int row = pixelZ; row < pixelZ + height; row++) {
|
||||
int start = row * resolution + pixelX;
|
||||
Arrays.fill(pixels, start, start + width, color);
|
||||
Arrays.fill(sampled, start, start + width, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HeightSampler {
|
||||
private static final double MAXIMUM_INTERPOLATION_ERROR = 1.5D;
|
||||
|
||||
private final int[] pixels;
|
||||
private final double[] heights;
|
||||
private final boolean[] sampled;
|
||||
private final int resolution;
|
||||
private final double startX;
|
||||
private final double startZ;
|
||||
private final double step;
|
||||
private final ProceduralStream<Double> stream;
|
||||
private final double maximumHeight;
|
||||
private final double fluidHeight;
|
||||
private final BooleanSupplier cancelled;
|
||||
|
||||
private HeightSampler(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
ProceduralStream<Double> stream,
|
||||
double maximumHeight,
|
||||
double fluidHeight,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
this.pixels = pixels;
|
||||
this.heights = new double[pixels.length];
|
||||
this.sampled = new boolean[pixels.length];
|
||||
this.resolution = resolution;
|
||||
this.startX = startX;
|
||||
this.startZ = startZ;
|
||||
this.step = step;
|
||||
this.stream = stream;
|
||||
this.maximumHeight = maximumHeight;
|
||||
this.fluidHeight = fluidHeight;
|
||||
this.cancelled = cancelled;
|
||||
}
|
||||
|
||||
private void render(int pixelX, int pixelZ, int width, int height) {
|
||||
checkCancelled(cancelled);
|
||||
if (width == 1 && height == 1) {
|
||||
pixels[pixelZ * resolution + pixelX] = heightColor(
|
||||
sample(pixelX, pixelZ), maximumHeight, fluidHeight);
|
||||
return;
|
||||
}
|
||||
int maximumX = pixelX + width - 1;
|
||||
int maximumZ = pixelZ + height - 1;
|
||||
int centerX = pixelX + width / 2;
|
||||
int centerZ = pixelZ + height / 2;
|
||||
double topLeft = sample(pixelX, pixelZ);
|
||||
double topRight = sample(maximumX, pixelZ);
|
||||
double bottomLeft = sample(pixelX, maximumZ);
|
||||
double bottomRight = sample(maximumX, maximumZ);
|
||||
if (matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
|
||||
centerX, pixelZ)
|
||||
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
|
||||
pixelX, centerZ)
|
||||
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
|
||||
maximumX, centerZ)
|
||||
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
|
||||
centerX, maximumZ)
|
||||
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
|
||||
centerX, centerZ)) {
|
||||
fillPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight);
|
||||
return;
|
||||
}
|
||||
int leftWidth = Math.max(1, width / 2);
|
||||
int rightWidth = width - leftWidth;
|
||||
int topHeight = Math.max(1, height / 2);
|
||||
int bottomHeight = height - topHeight;
|
||||
render(pixelX, pixelZ, leftWidth, topHeight);
|
||||
if (rightWidth > 0) {
|
||||
render(pixelX + leftWidth, pixelZ, rightWidth, topHeight);
|
||||
}
|
||||
if (bottomHeight > 0) {
|
||||
render(pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
|
||||
if (rightWidth > 0) {
|
||||
render(pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesPlane(
|
||||
int pixelX,
|
||||
int pixelZ,
|
||||
int width,
|
||||
int height,
|
||||
double topLeft,
|
||||
double topRight,
|
||||
double bottomLeft,
|
||||
double bottomRight,
|
||||
int sampleX,
|
||||
int sampleZ
|
||||
) {
|
||||
double predicted = interpolate(
|
||||
pixelX,
|
||||
pixelZ,
|
||||
width,
|
||||
height,
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
sampleX,
|
||||
sampleZ
|
||||
);
|
||||
return Math.abs(sample(sampleX, sampleZ) - predicted) <= MAXIMUM_INTERPOLATION_ERROR;
|
||||
}
|
||||
|
||||
private void fillPlane(
|
||||
int pixelX,
|
||||
int pixelZ,
|
||||
int width,
|
||||
int height,
|
||||
double topLeft,
|
||||
double topRight,
|
||||
double bottomLeft,
|
||||
double bottomRight
|
||||
) {
|
||||
for (int row = pixelZ; row < pixelZ + height; row++) {
|
||||
int offset = row * resolution;
|
||||
for (int column = pixelX; column < pixelX + width; column++) {
|
||||
double value = interpolate(
|
||||
pixelX,
|
||||
pixelZ,
|
||||
width,
|
||||
height,
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
column,
|
||||
row
|
||||
);
|
||||
pixels[offset + column] = heightColor(value, maximumHeight, fluidHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double sample(int pixelX, int pixelZ) {
|
||||
int index = pixelZ * resolution + pixelX;
|
||||
if (!sampled[index]) {
|
||||
heights[index] = stream.getDouble(startX + pixelX * step, startZ + pixelZ * step);
|
||||
sampled[index] = true;
|
||||
}
|
||||
return heights[index];
|
||||
}
|
||||
|
||||
private static double interpolate(
|
||||
int pixelX,
|
||||
int pixelZ,
|
||||
int width,
|
||||
int height,
|
||||
double topLeft,
|
||||
double topRight,
|
||||
double bottomLeft,
|
||||
double bottomRight,
|
||||
int sampleX,
|
||||
int sampleZ
|
||||
) {
|
||||
double x = width <= 1 ? 0D : (sampleX - pixelX) / (double) (width - 1);
|
||||
double z = height <= 1 ? 0D : (sampleZ - pixelZ) / (double) (height - 1);
|
||||
double top = topLeft + (topRight - topLeft) * x;
|
||||
double bottom = bottomLeft + (bottomRight - bottomLeft) * x;
|
||||
return top + (bottom - top) * z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
package art.arcane.iris.engine.framework.render;
|
||||
|
||||
public enum RenderType {
|
||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
|
||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, RIVER, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
@@ -102,7 +104,7 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getEngine().getDimension().getFluidHeight());
|
||||
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z));
|
||||
}
|
||||
|
||||
default int trueHeight(int x, int z) {
|
||||
@@ -110,6 +112,10 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
default boolean isCarved(int x, int h, int z) {
|
||||
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(getMantle(), x, h, z);
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
}
|
||||
return getMantle().get(x, h, z, MatterCavern.class) != null;
|
||||
}
|
||||
|
||||
@@ -125,13 +131,17 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
default boolean isUnderwater(int x, int z) {
|
||||
return getHighest(x, z, true) <= getFluidHeight();
|
||||
return getHighest(x, z, true) < getFluidHeight(x, z);
|
||||
}
|
||||
|
||||
default int getFluidHeight() {
|
||||
return getEngine().getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
default int getFluidHeight(int x, int z) {
|
||||
return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
|
||||
}
|
||||
|
||||
default boolean isDebugSmartBore() {
|
||||
return getEngine().getDimension().isDebugSmartBore();
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ public interface MantleComponent extends Comparable<MantleComponent> {
|
||||
|
||||
int getRadius();
|
||||
|
||||
default int getInputRadius() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
default IrisData getData() {
|
||||
return getEngineMantle().getData();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisGeneratorStyle;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.function.Function3;
|
||||
@@ -181,6 +182,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
if (chunk == null) return;
|
||||
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if ((t instanceof PlatformBlockState || t instanceof MatterCavern)
|
||||
&& hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
if (t instanceof PlatformBlockState) {
|
||||
clearDeferredPlacement(matter, x, y, z);
|
||||
}
|
||||
@@ -206,6 +211,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
}
|
||||
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
return false;
|
||||
}
|
||||
MatterCavern existing = matter.<MatterCavern>slice(MatterCavern.class).get(x & 15, y & 15, z & 15);
|
||||
if (existing != null) {
|
||||
return false;
|
||||
@@ -226,6 +234,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
}
|
||||
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
return false;
|
||||
}
|
||||
if (matter.hasSlice(PlatformBlockState.class)) {
|
||||
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
@@ -248,6 +259,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
+ x + "," + y + "," + z);
|
||||
}
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
if (matter.hasSlice(PlatformBlockState.class)) {
|
||||
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
@@ -271,6 +285,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
if (matter == null) {
|
||||
return;
|
||||
}
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
if (matter.hasSlice(PlatformBlockState.class)) {
|
||||
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
@@ -328,9 +345,22 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
if (matter == null || !matter.hasSlice(type)) {
|
||||
return;
|
||||
}
|
||||
if ((type == PlatformBlockState.class || type == MatterCavern.class)
|
||||
&& hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
matter.getSlice(type).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
|
||||
private static boolean hasProtectedHydrology(Matter matter, int x, int y, int z) {
|
||||
if (!matter.hasSlice(RiverCaveHydrology.class)) {
|
||||
return false;
|
||||
}
|
||||
RiverCaveHydrology hydrology = matter.<RiverCaveHydrology>getSlice(RiverCaveHydrology.class)
|
||||
.get(x & 15, y & 15, z & 15);
|
||||
return hydrology != null && hydrology.protectsPlacement();
|
||||
}
|
||||
|
||||
private static void clearDeferredPlacement(Matter matter, int x, int y, int z) {
|
||||
if (matter.hasSlice(Identifier.class)) {
|
||||
matter.<Identifier>getSlice(Identifier.class).set(x & 15, y & 15, z & 15, null);
|
||||
@@ -425,6 +455,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
RiverCaveHydrology hydrology = getDataIfPresent(x, y, z, RiverCaveHydrology.class);
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
}
|
||||
return getDataIfPresent(x, y, z, MatterCavern.class) != null;
|
||||
}
|
||||
|
||||
@@ -449,15 +483,28 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
}
|
||||
|
||||
Matter matter = chunk.get(section);
|
||||
if (matter == null || !matter.hasSlice(MatterCavern.class)) {
|
||||
if (matter == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MatterSlice<MatterCavern> slice = matter.getSlice(MatterCavern.class);
|
||||
MatterSlice<MatterCavern> cavernSlice = matter.hasSlice(MatterCavern.class)
|
||||
? matter.getSlice(MatterCavern.class)
|
||||
: null;
|
||||
MatterSlice<RiverCaveHydrology> hydrologySlice = matter.hasSlice(RiverCaveHydrology.class)
|
||||
? matter.getSlice(RiverCaveHydrology.class)
|
||||
: null;
|
||||
if (cavernSlice == null && hydrologySlice == null) {
|
||||
continue;
|
||||
}
|
||||
int sectionBaseY = section << 4;
|
||||
int sectionMaxY = Math.min(cappedHeight, sectionBaseY + 16);
|
||||
for (int y = sectionBaseY; y < sectionMaxY; y++) {
|
||||
if (slice.get(localX, y & 15, localZ) != null) {
|
||||
RiverCaveHydrology hydrology = hydrologySlice == null
|
||||
? null
|
||||
: hydrologySlice.get(localX, y & 15, localZ);
|
||||
if (hydrology != null) {
|
||||
carvedColumn[y] = hydrology.carves() ? (byte) 1 : 0;
|
||||
} else if (cavernSlice != null && cavernSlice.get(localX, y & 15, localZ) != null) {
|
||||
carvedColumn[y] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,15 +61,14 @@ public final class CarveOrphanSweep {
|
||||
int[] surfaceHeights,
|
||||
int maxSurfaceBreakDepth,
|
||||
int worldCeilingY,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
int fluidHeight
|
||||
long[] surfaceFluidBoundaries
|
||||
) {
|
||||
if (chunk == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return sweep(surfaceHeights, maxSurfaceBreakDepth, 0, worldCeilingY,
|
||||
new MantleCarveAccess(chunk, surfaceFluidBoundaryStartY, fluidHeight));
|
||||
new MantleCarveAccess(chunk, surfaceFluidBoundaries));
|
||||
}
|
||||
|
||||
public static int sweep(int[] surfaceHeights, int maxSurfaceBreakDepth, int worldFloorY, int worldCeilingY, CarveAccess access) {
|
||||
@@ -229,15 +228,13 @@ public final class CarveOrphanSweep {
|
||||
|
||||
private static final class MantleCarveAccess implements CarveAccess {
|
||||
private final MantleChunk<Matter> chunk;
|
||||
private final int[] surfaceFluidBoundaryStartY;
|
||||
private final int fluidHeight;
|
||||
private final long[] surfaceFluidBoundaries;
|
||||
private MatterSlice<MatterCavern> cachedSlice;
|
||||
private int cachedSectionIndex = -1;
|
||||
|
||||
private MantleCarveAccess(MantleChunk<Matter> chunk, int[] surfaceFluidBoundaryStartY, int fluidHeight) {
|
||||
private MantleCarveAccess(MantleChunk<Matter> chunk, long[] surfaceFluidBoundaries) {
|
||||
this.chunk = chunk;
|
||||
this.surfaceFluidBoundaryStartY = surfaceFluidBoundaryStartY;
|
||||
this.fluidHeight = fluidHeight;
|
||||
this.surfaceFluidBoundaries = surfaceFluidBoundaries;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -261,7 +258,7 @@ public final class CarveOrphanSweep {
|
||||
@Override
|
||||
public boolean isProtected(int localX, int y, int localZ) {
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
||||
return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight);
|
||||
return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
-1
@@ -23,6 +23,7 @@ import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -71,6 +72,12 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
discard();
|
||||
return CommitResult.REJECTED_BOUNDS;
|
||||
}
|
||||
RiverCaveHydrology hydrology = delegate.getData(
|
||||
mutation.x(), mutation.y(), mutation.z(), RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
discard();
|
||||
return CommitResult.REJECTED_HYDROLOGY;
|
||||
}
|
||||
}
|
||||
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
@@ -245,7 +252,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
enum CommitResult {
|
||||
COMMITTED,
|
||||
EMPTY,
|
||||
REJECTED_BOUNDS
|
||||
REJECTED_BOUNDS,
|
||||
REJECTED_HYDROLOGY
|
||||
}
|
||||
|
||||
private interface BufferedMutation {
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisGeneratorStyle;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveGrottoShape;
|
||||
import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveSource;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
|
||||
private static final long SHAPE_SALT = 0x3C6EF372FE94F82BL;
|
||||
private static final long WARP_X_SALT = 0xA54FF53A5F1D36F1L;
|
||||
private static final long WARP_Y_SALT = 0x510E527FADE682D1L;
|
||||
private static final long WARP_Z_SALT = 0x9B05688C2B3E6C1FL;
|
||||
|
||||
private final CNG shape;
|
||||
private final CNG warpX;
|
||||
private final CNG warpY;
|
||||
private final CNG warpZ;
|
||||
private final double warpStrength;
|
||||
|
||||
ConfiguredRiverGrottoShape(
|
||||
long seed,
|
||||
IrisData data,
|
||||
IrisGeneratorStyle shapeStyle,
|
||||
IrisGeneratorStyle warpStyle,
|
||||
double warpStrength
|
||||
) {
|
||||
IrisGeneratorStyle resolvedShape = shapeStyle == null
|
||||
? new IrisGeneratorStyle(NoiseStyle.FLAT)
|
||||
: shapeStyle;
|
||||
IrisGeneratorStyle resolvedWarp = warpStyle == null
|
||||
? new IrisGeneratorStyle(NoiseStyle.FLAT)
|
||||
: warpStyle;
|
||||
shape = resolvedShape.createNoCache(new RNG(seed ^ SHAPE_SALT), data);
|
||||
warpX = resolvedWarp.createNoCache(new RNG(seed ^ WARP_X_SALT), data);
|
||||
warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data);
|
||||
warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data);
|
||||
this.warpStrength = Math.max(0D, warpStrength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
int offsetX,
|
||||
int offsetY,
|
||||
int offsetZ
|
||||
) {
|
||||
double worldX = source.target().x() + offsetX;
|
||||
double worldY = source.target().y() + offsetY;
|
||||
double worldZ = source.target().z() + offsetZ;
|
||||
double warpedX = offsetX + warpX.fitDouble(-warpStrength, warpStrength, worldX, worldY, worldZ);
|
||||
double warpedY = offsetY + warpY.fitDouble(-warpStrength, warpStrength, worldY, worldZ, worldX);
|
||||
double warpedZ = offsetZ + warpZ.fitDouble(-warpStrength, warpStrength, worldZ, worldX, worldY);
|
||||
double horizontalRadius = settings.grottoHorizontalRadius();
|
||||
double verticalRadius = settings.grottoVerticalRadius();
|
||||
double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius))
|
||||
+ (warpedY * warpedY / (verticalRadius * verticalRadius))
|
||||
+ (warpedZ * warpedZ / (horizontalRadius * horizontalRadius));
|
||||
double boundary = shape.fitDouble(-0.2D, 0.2D, worldX, worldY, worldZ);
|
||||
return normalized <= 1D + boundary;
|
||||
}
|
||||
}
|
||||
@@ -197,7 +197,7 @@ public class IrisCaveCarver3D {
|
||||
double thresholdPenalty,
|
||||
IrisRange worldYRange,
|
||||
int[] precomputedSurfaceHeights,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
long[] surfaceFluidBoundaries,
|
||||
IrisRange overrideVerticalRange,
|
||||
CaveFluidSupportPlan fluidSupportPlan
|
||||
) {
|
||||
@@ -318,7 +318,7 @@ public class IrisCaveCarver3D {
|
||||
surfaceBreakThresholdBoost,
|
||||
columnMaxY,
|
||||
fluidMaxY,
|
||||
surfaceFluidBoundaryStartY,
|
||||
surfaceFluidBoundaries,
|
||||
surfaceBreakFloorY,
|
||||
surfaceBreakColumn,
|
||||
columnThreshold,
|
||||
@@ -341,7 +341,7 @@ public class IrisCaveCarver3D {
|
||||
surfaceBreakThresholdBoost,
|
||||
columnMaxY,
|
||||
fluidMaxY,
|
||||
surfaceFluidBoundaryStartY,
|
||||
surfaceFluidBoundaries,
|
||||
surfaceBreakFloorY,
|
||||
surfaceBreakColumn,
|
||||
columnThreshold,
|
||||
@@ -367,7 +367,7 @@ public class IrisCaveCarver3D {
|
||||
surfaceBreakThresholdBoost,
|
||||
columnMaxY,
|
||||
fluidMaxY,
|
||||
surfaceFluidBoundaryStartY,
|
||||
surfaceFluidBoundaries,
|
||||
surfaceBreakFloorY,
|
||||
surfaceBreakColumn,
|
||||
columnThreshold,
|
||||
@@ -391,7 +391,7 @@ public class IrisCaveCarver3D {
|
||||
surfaceBreakThresholdBoost,
|
||||
columnMaxY,
|
||||
fluidMaxY,
|
||||
surfaceFluidBoundaryStartY,
|
||||
surfaceFluidBoundaries,
|
||||
surfaceBreakFloorY,
|
||||
surfaceBreakColumn,
|
||||
columnThreshold,
|
||||
@@ -422,7 +422,7 @@ public class IrisCaveCarver3D {
|
||||
double surfaceBreakThresholdBoost,
|
||||
int[] columnMaxY,
|
||||
int[] fluidMaxY,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
long[] surfaceFluidBoundaries,
|
||||
int[] surfaceBreakFloorY,
|
||||
boolean[] surfaceBreakColumn,
|
||||
double[] columnThreshold,
|
||||
@@ -478,7 +478,7 @@ public class IrisCaveCarver3D {
|
||||
}
|
||||
|
||||
int columnIndex = activeColumnIndices[activeIndex];
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) {
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) {
|
||||
continue;
|
||||
}
|
||||
planeColumnIndices[planeCount] = columnIndex;
|
||||
@@ -553,7 +553,7 @@ public class IrisCaveCarver3D {
|
||||
double surfaceBreakThresholdBoost,
|
||||
int[] columnMaxY,
|
||||
int[] fluidMaxY,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
long[] surfaceFluidBoundaries,
|
||||
int[] surfaceBreakFloorY,
|
||||
boolean[] surfaceBreakColumn,
|
||||
double[] columnThreshold,
|
||||
@@ -615,7 +615,7 @@ public class IrisCaveCarver3D {
|
||||
}
|
||||
|
||||
int columnIndex = activeColumnIndices[activeIndex];
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) {
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) {
|
||||
continue;
|
||||
}
|
||||
planeColumnIndices[planeCount] = columnIndex;
|
||||
@@ -712,7 +712,7 @@ public class IrisCaveCarver3D {
|
||||
double surfaceBreakThresholdBoost,
|
||||
int[] columnMaxY,
|
||||
int[] fluidMaxY,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
long[] surfaceFluidBoundaries,
|
||||
int[] surfaceBreakFloorY,
|
||||
boolean[] surfaceBreakColumn,
|
||||
double[] columnThreshold,
|
||||
@@ -814,7 +814,7 @@ public class IrisCaveCarver3D {
|
||||
}
|
||||
|
||||
int index = tileIndices[columnIndex];
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) {
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
|
||||
continue;
|
||||
}
|
||||
double localThreshold = passThreshold[index];
|
||||
@@ -861,7 +861,7 @@ public class IrisCaveCarver3D {
|
||||
double surfaceBreakThresholdBoost,
|
||||
int[] columnMaxY,
|
||||
int[] fluidMaxY,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
long[] surfaceFluidBoundaries,
|
||||
int[] surfaceBreakFloorY,
|
||||
boolean[] surfaceBreakColumn,
|
||||
double[] columnThreshold,
|
||||
@@ -909,7 +909,7 @@ public class IrisCaveCarver3D {
|
||||
|
||||
int carveMaxY = Math.min(columnTopY, y + sampleStep - 1);
|
||||
for (int yy = y; yy <= carveMaxY; yy++) {
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) {
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
|
||||
continue;
|
||||
}
|
||||
MatterCavern verticalMatter = matterByY[yy - minY];
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 3);
|
||||
|
||||
public IrisStructureComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.JIGSAW, 3);
|
||||
super(engineMantle, ReservedFlag.JIGSAW, 4);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+23
-13
@@ -104,20 +104,19 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
|
||||
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
|
||||
getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
|
||||
int fluidHeight = getDimension().getFluidHeight();
|
||||
int[] surfaceFluidBoundaryStartY = blendScratch.surfaceFluidBoundaryStartY;
|
||||
long[] surfaceFluidBoundaries = blendScratch.surfaceFluidBoundaries;
|
||||
SurfaceFluidBoundaryPlan.fill(
|
||||
chunkSurfaceHeights,
|
||||
blendScratch.fieldSurfaceHeights,
|
||||
blendScratch.fieldHasFluid,
|
||||
blendScratch.fieldFluidHeights,
|
||||
FIELD_SIZE,
|
||||
BLEND_RADIUS,
|
||||
fluidHeight,
|
||||
surfaceFluidBoundaryStartY
|
||||
surfaceFluidBoundaries
|
||||
);
|
||||
CaveFluidSupportPlan fluidSupportPlan = new CaveFluidSupportPlan();
|
||||
for (WeightedProfile weightedProfile : weightedProfiles) {
|
||||
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaryStartY, fluidSupportPlan);
|
||||
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaries, fluidSupportPlan);
|
||||
}
|
||||
|
||||
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
|
||||
@@ -132,8 +131,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
chunkSurfaceHeights,
|
||||
maxSurfaceBreakDepth(weightedProfiles),
|
||||
writer.getMantle().getWorldHeight() - 1,
|
||||
surfaceFluidBoundaryStartY,
|
||||
fluidHeight
|
||||
surfaceFluidBoundaries
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -148,11 +146,11 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
|
||||
@ChunkCoordinates
|
||||
private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz,
|
||||
int[] chunkSurfaceHeights, int[] surfaceFluidBoundaryStartY,
|
||||
int[] chunkSurfaceHeights, long[] surfaceFluidBoundaries,
|
||||
CaveFluidSupportPlan fluidSupportPlan) {
|
||||
IrisCaveCarver3D carver = getCarver(weightedProfile.profile);
|
||||
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY,
|
||||
weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaryStartY, null, fluidSupportPlan);
|
||||
weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaries, null, fluidSupportPlan);
|
||||
}
|
||||
|
||||
private void carveUpperTerrain(UpperDimensionContext upperCtx, List<WeightedProfile> normalProfiles,
|
||||
@@ -475,7 +473,9 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
|
||||
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
|
||||
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
|
||||
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldHasFluid);
|
||||
fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights);
|
||||
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldSurfaceHeights,
|
||||
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
|
||||
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
|
||||
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
|
||||
fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes);
|
||||
@@ -499,11 +499,20 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private void fillFieldFluidPresence(ProceduralStream<PlatformBlockState> stream, int startX, int startZ, boolean[] target) {
|
||||
private void fillFieldFluidPresence(
|
||||
ProceduralStream<PlatformBlockState> stream,
|
||||
int startX,
|
||||
int startZ,
|
||||
double[] surfaceHeights,
|
||||
double[] fluidHeights,
|
||||
boolean[] target
|
||||
) {
|
||||
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
|
||||
int worldX = startX + fieldX;
|
||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||
target[(fieldX * FIELD_SIZE) + fieldZ] = B.isFluid(stream.get(worldX, startZ + fieldZ));
|
||||
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
|
||||
target[fieldIndex] = B.isFluid(stream.get(worldX, startZ + fieldZ))
|
||||
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -697,12 +706,13 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
private final IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = new IdentityHashMap<>();
|
||||
private final List<IrisCaveProfile> profileOrder = new ArrayList<>();
|
||||
private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE];
|
||||
private final double[] fieldFluidHeights = new double[FIELD_SIZE * FIELD_SIZE];
|
||||
private final boolean[] fieldHasFluid = new boolean[FIELD_SIZE * FIELD_SIZE];
|
||||
private final long[] surfaceFluidBoundaries = new long[CHUNK_AREA];
|
||||
private final IrisRegion[] fieldRegions = new IrisRegion[FIELD_SIZE * FIELD_SIZE];
|
||||
private final IrisBiome[] fieldSurfaceBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
|
||||
private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
|
||||
private final int[] chunkSurfaceHeights = new int[CHUNK_AREA];
|
||||
private final int[] surfaceFluidBoundaryStartY = new int[CHUNK_AREA];
|
||||
private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA];
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0);
|
||||
|
||||
public MantleFloatingObjectComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2);
|
||||
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 3);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+23
-2
@@ -48,6 +48,7 @@ import art.arcane.iris.engine.object.IrisProceduralPlacement;
|
||||
import art.arcane.iris.engine.object.IrisProceduralTree;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -83,7 +84,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public MantleObjectComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.OBJECT, 1);
|
||||
super(engineMantle, ReservedFlag.OBJECT, 2);
|
||||
}
|
||||
|
||||
private static String placementMarker(IrisObject object, int id, String context) {
|
||||
@@ -581,11 +582,18 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
minDepthBelowSurface,
|
||||
anchorCache
|
||||
);
|
||||
RiverCaveHydrology hydrology = candidateY < 0
|
||||
? null
|
||||
: writer.getDataIfPresent(candidateX, candidateY, candidateZ, RiverCaveHydrology.class);
|
||||
MatterCavern cavern = candidateY < 0
|
||||
? null
|
||||
: writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class);
|
||||
if (candidateY < 0
|
||||
|| caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey)
|
||||
|| !acceptsCaveAnchorFluid(
|
||||
underwater,
|
||||
writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class),
|
||||
hydrology == null ? cavern : hydrology.asCavern(),
|
||||
hydrology,
|
||||
candidateY,
|
||||
getDimension().getCaveLavaHeight())) {
|
||||
continue;
|
||||
@@ -596,6 +604,19 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
|
||||
static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) {
|
||||
return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight);
|
||||
}
|
||||
|
||||
static boolean acceptsCaveAnchorFluid(
|
||||
boolean underwater,
|
||||
MatterCavern cavern,
|
||||
RiverCaveHydrology hydrology,
|
||||
int y,
|
||||
int lavaHeight
|
||||
) {
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
return false;
|
||||
}
|
||||
if (cavern == null || !cavern.isCavern()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.river.cave.CavePosition;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxelView;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.object.IrisProceduralBlocks;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView {
|
||||
private static final int CLOSED_COLUMN = Integer.MAX_VALUE;
|
||||
private static final int CACHE_MISS = Integer.MIN_VALUE;
|
||||
|
||||
private final Mantle<Matter> mantle;
|
||||
private final int worldHeight;
|
||||
private final Function2<Integer, Integer, Integer> surfaceHeight;
|
||||
private final Function2<Integer, Integer, PlatformBlockState> compatibleFluid;
|
||||
private final Long2IntOpenHashMap openFloorCache;
|
||||
private final Long2IntOpenHashMap surfaceHeightCache;
|
||||
|
||||
MantleRiverCaveVoxelView(
|
||||
Mantle<Matter> mantle,
|
||||
int worldHeight,
|
||||
Function2<Integer, Integer, Integer> surfaceHeight,
|
||||
Function2<Integer, Integer, PlatformBlockState> compatibleFluid
|
||||
) {
|
||||
this.mantle = Objects.requireNonNull(mantle);
|
||||
this.worldHeight = worldHeight;
|
||||
this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
|
||||
this.compatibleFluid = Objects.requireNonNull(compatibleFluid);
|
||||
openFloorCache = new Long2IntOpenHashMap();
|
||||
openFloorCache.defaultReturnValue(CACHE_MISS);
|
||||
surfaceHeightCache = new Long2IntOpenHashMap();
|
||||
surfaceHeightCache.defaultReturnValue(CACHE_MISS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInWorld(CavePosition position) {
|
||||
return position.y() > 0 && position.y() < worldHeight - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxel voxelAt(CavePosition position) {
|
||||
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.carves()) {
|
||||
return hydrology.isWet() ? CaveVoxel.COMPATIBLE_FLUID : CaveVoxel.CAVE_AIR;
|
||||
}
|
||||
MatterCavern cavern = dataIfPresent(position, MatterCavern.class);
|
||||
if (cavern != null) {
|
||||
if (cavern.isLava()) {
|
||||
return CaveVoxel.LAVA;
|
||||
}
|
||||
if (cavern.getLiquid() == 1) {
|
||||
return CaveVoxel.COMPATIBLE_FLUID;
|
||||
}
|
||||
return CaveVoxel.CAVE_AIR;
|
||||
}
|
||||
PlatformBlockState block = dataIfPresent(position, PlatformBlockState.class);
|
||||
if (block == null) {
|
||||
return position.y() > surfaceY(position.x(), position.z())
|
||||
? CaveVoxel.CAVE_AIR
|
||||
: CaveVoxel.SOLID;
|
||||
}
|
||||
if (!block.isFluid()) {
|
||||
return CaveVoxel.SOLID;
|
||||
}
|
||||
if (IrisProceduralBlocks.materialKey(block).endsWith(":lava")) {
|
||||
return CaveVoxel.LAVA;
|
||||
}
|
||||
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
|
||||
return expected != null
|
||||
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))
|
||||
? CaveVoxel.COMPATIBLE_FLUID
|
||||
: CaveVoxel.INCOMPATIBLE_FLUID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpenToSurface(CavePosition position) {
|
||||
if (!isInWorld(position) || voxelAt(position) == CaveVoxel.SOLID) {
|
||||
return false;
|
||||
}
|
||||
if (position.y() > surfaceY(position.x(), position.z())) {
|
||||
return true;
|
||||
}
|
||||
long key = Cache.key(position.x(), position.z());
|
||||
int openFloor = openFloorCache.get(key);
|
||||
if (openFloor == CACHE_MISS) {
|
||||
openFloor = resolveOpenFloor(position.x(), position.z());
|
||||
openFloorCache.put(key, openFloor);
|
||||
}
|
||||
return openFloor != CLOSED_COLUMN && position.y() >= openFloor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RiverCaveAction riverActionAt(CavePosition position) {
|
||||
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
|
||||
return hydrology == null ? null : hydrology.action();
|
||||
}
|
||||
|
||||
private int resolveOpenFloor(int x, int z) {
|
||||
int top = surfaceY(x, z);
|
||||
CavePosition surface = new CavePosition(x, top, z);
|
||||
if (voxelAt(surface) == CaveVoxel.SOLID) {
|
||||
return CLOSED_COLUMN;
|
||||
}
|
||||
int y = top;
|
||||
while (y > 0 && voxelAt(new CavePosition(x, y - 1, z)) != CaveVoxel.SOLID) {
|
||||
y--;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
private int surfaceY(int x, int z) {
|
||||
long key = Cache.key(x, z);
|
||||
int cached = surfaceHeightCache.get(key);
|
||||
if (cached != CACHE_MISS) {
|
||||
return cached;
|
||||
}
|
||||
int resolved = Math.max(1, Math.min(worldHeight - 2, surfaceHeight.apply(x, z)));
|
||||
surfaceHeightCache.put(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private <T> T dataIfPresent(CavePosition position, Class<T> type) {
|
||||
int chunkX = position.x() >> 4;
|
||||
int chunkZ = position.z() >> 4;
|
||||
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5));
|
||||
if (plate == null || plate.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
MantleChunk<Matter> chunk = plate.get(chunkX & 31, chunkZ & 31);
|
||||
int section = position.y() >> 4;
|
||||
if (chunk == null || !chunk.exists(section)) {
|
||||
return null;
|
||||
}
|
||||
Matter matter = chunk.get(section);
|
||||
if (matter == null || !matter.hasSlice(type)) {
|
||||
return null;
|
||||
}
|
||||
return matter.<T>getSlice(type).get(
|
||||
position.x() & 15,
|
||||
position.y() & 15,
|
||||
position.z() & 15
|
||||
);
|
||||
}
|
||||
}
|
||||
+707
@@ -0,0 +1,707 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.mantle.IrisMantleComponent;
|
||||
import art.arcane.iris.engine.mantle.MantleWriter;
|
||||
import art.arcane.iris.engine.object.IrisRiverCaveFallback;
|
||||
import art.arcane.iris.engine.object.IrisRiverCaveMode;
|
||||
import art.arcane.iris.engine.object.IrisRiverCaves;
|
||||
import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRiverNetwork;
|
||||
import art.arcane.iris.engine.river.RiverAnchor;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.cave.CavePosition;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxelPrecondition;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxelView;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveMode;
|
||||
import art.arcane.iris.engine.river.cave.RiverCavePlan;
|
||||
import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings;
|
||||
import art.arcane.iris.engine.river.cave.RiverCavePlanningResult;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveSource;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverTunnelSample;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@ComponentFlag(ReservedFlag.RIVER_HYDROLOGY)
|
||||
public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
||||
private static final long CANDIDATE_SALT = 0x6A09E667F3BCC909L;
|
||||
static final int PRIORITY = 1;
|
||||
private static final int[] FALLBACK_X = {0, 1, -1, 0, 0};
|
||||
private static final int[] FALLBACK_Z = {0, 0, 0, 1, -1};
|
||||
private static final MantleFlag[] PREREQUISITES = {ReservedFlag.CARVED};
|
||||
private static final int[][] NEIGHBORS = {
|
||||
{1, 0, 0}, {-1, 0, 0},
|
||||
{0, 1, 0}, {0, -1, 0},
|
||||
{0, 0, 1}, {0, 0, -1}
|
||||
};
|
||||
private static final Comparator<Map.Entry<CavePosition, RiverCaveAction>> ACTION_ORDER = Comparator
|
||||
.comparingInt((Map.Entry<CavePosition, RiverCaveAction> entry) -> entry.getKey().x())
|
||||
.thenComparingInt(entry -> entry.getKey().y())
|
||||
.thenComparingInt(entry -> entry.getKey().z());
|
||||
|
||||
private final RiverCaveContainmentPlanner planner;
|
||||
|
||||
public MantleRiverHydrologyComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.RIVER_HYDROLOGY, PRIORITY);
|
||||
planner = new RiverCaveContainmentPlanner();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MantleFlag[] getPrerequisiteFlags() {
|
||||
return PREREQUISITES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInputRadius() {
|
||||
if (!getDimension().isCarvingEnabled()
|
||||
|| getDimension().getRivers() == null
|
||||
|| !getDimension().getRivers().isEnabled()) {
|
||||
return 0;
|
||||
}
|
||||
IrisRiverRuntime runtime = getComplex().getRiverRuntime();
|
||||
if (runtime == null) {
|
||||
return 0;
|
||||
}
|
||||
return inputRadius(runtime.caveSettings(), tunnelHalo(runtime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
|
||||
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
|
||||
if (runtime == null || !getDimension().isCarvingEnabled()) {
|
||||
return;
|
||||
}
|
||||
publishTunnels(writer, context, runtime, chunkX, chunkZ);
|
||||
|
||||
IrisRiverCaves caves = runtime.caveSettings();
|
||||
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
MantleRiverCaveVoxelView view = createView(writer, context);
|
||||
int candidateHalo = candidateHalo(caves);
|
||||
int minimumX = (chunkX << 4) - candidateHalo;
|
||||
int minimumZ = (chunkZ << 4) - candidateHalo;
|
||||
int maximumX = ((chunkX + 1) << 4) + candidateHalo;
|
||||
int maximumZ = ((chunkZ + 1) << 4) + candidateHalo;
|
||||
List<RiverAnchor> anchors = runtime.candidateAnchors(
|
||||
minimumX,
|
||||
minimumZ,
|
||||
maximumX,
|
||||
maximumZ,
|
||||
caves.getMinimumSpacing(),
|
||||
CANDIDATE_SALT
|
||||
);
|
||||
if (anchors.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RiverCavePlannerSettings settings = plannerSettings(caves, seed(), getData());
|
||||
List<RiverCaveSource> sources = new ArrayList<>();
|
||||
Map<Long, String> floodedBiomes = new HashMap<>();
|
||||
for (RiverAnchor anchor : anchors) {
|
||||
if (!runtime.acceptsCaveAnchor(anchor)) {
|
||||
continue;
|
||||
}
|
||||
SourceCandidate candidate = sourceFor(runtime, view, caves, anchor);
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
RiverCaveSource source = candidate.source();
|
||||
RiverCavePlan initial = planner.plan(view, source, settings);
|
||||
if (!initial.accepted()
|
||||
&& caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO) {
|
||||
source = fallbackSource(view, caves, settings, candidate, source);
|
||||
}
|
||||
if (source == null) {
|
||||
continue;
|
||||
}
|
||||
sources.add(source);
|
||||
floodedBiomes.put(source.sourceId(), runtime.selectFloodedCaveBiome(anchor));
|
||||
}
|
||||
if (sources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RiverCavePlanningResult result = planner.planAll(view, sources, settings);
|
||||
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
|
||||
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
|
||||
return;
|
||||
}
|
||||
publishLocal(writer, chunkX, chunkZ, result, floodedBiomes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeRadius() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static boolean isEnabledFor(IrisDimension dimension) {
|
||||
IrisRiverNetwork rivers = dimension.getRivers();
|
||||
if (!dimension.isUseMantle()
|
||||
|| !dimension.isCarvingEnabled()
|
||||
|| dimension.getDisabledComponents().contains(ReservedFlag.CARVED)
|
||||
|| dimension.getDisabledComponents().contains(ReservedFlag.RIVER_HYDROLOGY)
|
||||
|| rivers == null
|
||||
|| !rivers.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isCaveConnectionsEnabledFor(IrisDimension dimension) {
|
||||
if (!isEnabledFor(dimension)) {
|
||||
return false;
|
||||
}
|
||||
IrisRiverCaves caves = dimension.getRivers().getCaves();
|
||||
return caves != null
|
||||
&& caves.getMode() != IrisRiverCaveMode.SEALED
|
||||
&& caves.getMaximumPerReach() > 0;
|
||||
}
|
||||
|
||||
static int planningHalo(IrisRiverCaves caves) {
|
||||
return cavePublicationRadius(caves) * 4;
|
||||
}
|
||||
|
||||
static int inputRadius(IrisRiverCaves caves, int tunnelRadius) {
|
||||
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
|
||||
return tunnelRadius;
|
||||
}
|
||||
return Math.max(tunnelRadius, planningHalo(caves));
|
||||
}
|
||||
|
||||
static int candidateHalo(IrisRiverCaves caves) {
|
||||
return cavePublicationRadius(caves) * 3;
|
||||
}
|
||||
|
||||
static int cavePublicationRadius(IrisRiverCaves caves) {
|
||||
int generatedRadius = generatedGrottoPublicationRadius(caves);
|
||||
return switch (caves.getMode()) {
|
||||
case SEALED -> 0;
|
||||
case GENERATE_GROTTO -> generatedRadius;
|
||||
case FLOOD_CLOSED_COMPONENT, GROTTO_OR_CLOSED_COMPONENT, WATERFALL_POOL ->
|
||||
Math.max(closedComponentPublicationRadius(caves), generatedRadius);
|
||||
};
|
||||
}
|
||||
|
||||
static int closedComponentPublicationRadius(IrisRiverCaves caves) {
|
||||
return caves.getMaxFloodRadius() + 1;
|
||||
}
|
||||
|
||||
static int generatedGrottoPublicationRadius(IrisRiverCaves caves) {
|
||||
int targetOffset = caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO
|
||||
? caves.getThroatRadius() + 2
|
||||
: 0;
|
||||
long maximumX = (long) targetOffset + caves.getGrottoHorizontalRadius() + 1L;
|
||||
long maximumZ = caves.getGrottoHorizontalRadius();
|
||||
int grottoRadius = (int) StrictMath.ceil(StrictMath.sqrt(
|
||||
maximumX * maximumX + maximumZ * maximumZ
|
||||
));
|
||||
int throatRadius = targetOffset + caves.getThroatRadius();
|
||||
return Math.max(grottoRadius, throatRadius);
|
||||
}
|
||||
|
||||
static int tunnelHalo(IrisRiverRuntime runtime) {
|
||||
return Math.max(1, (int) StrictMath.ceil(runtime.maximumChannelWidth() * 0.5D) + 1);
|
||||
}
|
||||
|
||||
static int waterHeadY(IrisRiverSurfaceSample sample, IrisRiverCaves caves) {
|
||||
return (int) Math.round(sample.waterSurfaceY()) + caves.getWaterLevelOffset();
|
||||
}
|
||||
|
||||
static boolean owns(int chunkX, int chunkZ, CavePosition position) {
|
||||
return (position.x() >> 4) == chunkX && (position.z() >> 4) == chunkZ;
|
||||
}
|
||||
|
||||
static RiverCaveFluidPolicy fluidPolicy(IrisRiverExistingFluidPolicy policy) {
|
||||
return switch (policy) {
|
||||
case REJECT -> RiverCaveFluidPolicy.REJECT_EXISTING;
|
||||
case ALLOW_SAME -> RiverCaveFluidPolicy.ALLOW_COMPATIBLE;
|
||||
case REPLACE -> RiverCaveFluidPolicy.REPLACE_CONTAINED;
|
||||
};
|
||||
}
|
||||
|
||||
static boolean preconditionsHold(
|
||||
CaveVoxelView view,
|
||||
Map<CavePosition, CaveVoxelPrecondition> preconditions
|
||||
) {
|
||||
for (Map.Entry<CavePosition, CaveVoxelPrecondition> entry : preconditions.entrySet()) {
|
||||
CaveVoxelPrecondition expected = entry.getValue();
|
||||
if (view.voxelAt(entry.getKey()) != expected.voxel()
|
||||
|| view.isOpenToSurface(entry.getKey()) != expected.openToSurface()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static TunnelPlan planTunnels(
|
||||
CaveVoxelView view,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
int halo,
|
||||
FootprintSampler footprintSampler,
|
||||
TunnelSampler tunnelSampler,
|
||||
SurfaceSampler surfaceSampler
|
||||
) {
|
||||
int minimumX = (chunkX << 4) - halo;
|
||||
int minimumZ = (chunkZ << 4) - halo;
|
||||
int maximumX = ((chunkX + 1) << 4) + halo;
|
||||
int maximumZ = ((chunkZ + 1) << 4) + halo;
|
||||
if (!footprintSampler.sample(minimumX, minimumZ, maximumX, maximumZ).present()) {
|
||||
return TunnelPlan.empty();
|
||||
}
|
||||
ArrayList<TunnelColumn> solidColumns = new ArrayList<>();
|
||||
for (int x = minimumX; x < maximumX; x++) {
|
||||
for (int z = minimumZ; z < maximumZ; z++) {
|
||||
IrisRiverTunnelSample sample = tunnelSampler.sample(x, z);
|
||||
TunnelColumn column = createTunnelColumn(view, x, z, sample);
|
||||
if (column != null) {
|
||||
solidColumns.add(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (solidColumns.isEmpty()) {
|
||||
return TunnelPlan.empty();
|
||||
}
|
||||
|
||||
Map<CavePosition, RiverCaveAction> candidateActions = mergeActions(solidColumns);
|
||||
ArrayList<TunnelColumn> containedColumns = new ArrayList<>(solidColumns.size());
|
||||
for (TunnelColumn column : solidColumns) {
|
||||
if (isTunnelColumnContained(view, column, candidateActions.keySet(), surfaceSampler)) {
|
||||
containedColumns.add(column);
|
||||
}
|
||||
}
|
||||
Map<CavePosition, RiverCaveAction> actions = mergeActions(containedColumns);
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
for (CavePosition position : actions.keySet()) {
|
||||
preconditions.put(position, new CaveVoxelPrecondition(
|
||||
view.voxelAt(position),
|
||||
view.isOpenToSurface(position)
|
||||
));
|
||||
}
|
||||
for (CavePosition position : List.copyOf(actions.keySet())) {
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
CavePosition neighbor = offset(position, offset);
|
||||
if (actions.containsKey(neighbor)
|
||||
|| !view.isInWorld(neighbor)
|
||||
|| view.voxelAt(neighbor) != CaveVoxel.SOLID) {
|
||||
continue;
|
||||
}
|
||||
actions.putIfAbsent(neighbor, RiverCaveAction.SEAL_GUARD);
|
||||
preconditions.putIfAbsent(neighbor, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
|
||||
}
|
||||
}
|
||||
return new TunnelPlan(Map.copyOf(actions), Map.copyOf(preconditions));
|
||||
}
|
||||
|
||||
private static TunnelColumn createTunnelColumn(
|
||||
CaveVoxelView view,
|
||||
int x,
|
||||
int z,
|
||||
IrisRiverTunnelSample sample
|
||||
) {
|
||||
if (sample == null) {
|
||||
return null;
|
||||
}
|
||||
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
|
||||
for (int y = sample.bedY() + 1; y <= sample.ceilingY(); y++) {
|
||||
CavePosition position = new CavePosition(x, y, z);
|
||||
RiverCaveAction action = y <= sample.waterHeadY()
|
||||
? RiverCaveAction.WET_SOURCE
|
||||
: RiverCaveAction.DRY_AIR;
|
||||
if (!view.isInWorld(position)
|
||||
|| (view.voxelAt(position) != CaveVoxel.SOLID
|
||||
&& !matchesPublishedAction(view, position, action))) {
|
||||
return null;
|
||||
}
|
||||
actions.put(position, action);
|
||||
}
|
||||
return actions.isEmpty() ? null : new TunnelColumn(actions);
|
||||
}
|
||||
|
||||
private static boolean isTunnelColumnContained(
|
||||
CaveVoxelView view,
|
||||
TunnelColumn column,
|
||||
Set<CavePosition> candidateActions,
|
||||
SurfaceSampler surfaceSampler
|
||||
) {
|
||||
for (CavePosition position : column.actions().keySet()) {
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
CavePosition neighbor = offset(position, offset);
|
||||
if (candidateActions.contains(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (!view.isInWorld(neighbor)) {
|
||||
return false;
|
||||
}
|
||||
if (view.voxelAt(neighbor) == CaveVoxel.SOLID || isSurfaceMouth(neighbor, surfaceSampler)) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isSurfaceMouth(CavePosition position, SurfaceSampler surfaceSampler) {
|
||||
IrisRiverSurfaceSample sample = surfaceSampler.sample(position.x(), position.z());
|
||||
if (!isWetChannelBed(sample) || sample.subterranean()) {
|
||||
return false;
|
||||
}
|
||||
int bedY = (int) Math.round(sample.terrainHeight());
|
||||
int headY = (int) Math.round(sample.waterSurfaceY());
|
||||
return position.y() > bedY && position.y() <= headY;
|
||||
}
|
||||
|
||||
private static Map<CavePosition, RiverCaveAction> mergeActions(List<TunnelColumn> columns) {
|
||||
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
|
||||
for (TunnelColumn column : columns) {
|
||||
actions.putAll(column.actions());
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
private static boolean matchesPublishedAction(
|
||||
CaveVoxelView view,
|
||||
CavePosition position,
|
||||
RiverCaveAction action
|
||||
) {
|
||||
return view instanceof TunnelVoxelView tunnelView
|
||||
&& tunnelView.riverActionAt(position) == action;
|
||||
}
|
||||
|
||||
private static CavePosition offset(CavePosition position, int[] offset) {
|
||||
return new CavePosition(
|
||||
position.x() + offset[0],
|
||||
position.y() + offset[1],
|
||||
position.z() + offset[2]
|
||||
);
|
||||
}
|
||||
|
||||
private MantleRiverCaveVoxelView createView(MantleWriter writer, ChunkContext context) {
|
||||
return new MantleRiverCaveVoxelView(
|
||||
writer.getMantle(),
|
||||
writer.getMantle().getWorldHeight(),
|
||||
(x, z) -> context.getComplex().getRoundedHeighteightStream().get(x, z),
|
||||
(x, z) -> context.getComplex().getFluidStream().get(x, z)
|
||||
);
|
||||
}
|
||||
|
||||
private void publishTunnels(
|
||||
MantleWriter writer,
|
||||
ChunkContext context,
|
||||
IrisRiverRuntime runtime,
|
||||
int chunkX,
|
||||
int chunkZ
|
||||
) {
|
||||
for (int attempt = 0; attempt < 2; attempt++) {
|
||||
MantleRiverCaveVoxelView view = createView(writer, context);
|
||||
TunnelPlan plan = planTunnels(
|
||||
view,
|
||||
chunkX,
|
||||
chunkZ,
|
||||
tunnelHalo(runtime),
|
||||
runtime::sampleFootprint,
|
||||
runtime::sampleTunnel,
|
||||
runtime::sample
|
||||
);
|
||||
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
|
||||
if (preconditionsHold(revalidationView, plan.preconditions())) {
|
||||
publishTunnelLocal(writer, chunkX, chunkZ, plan);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static RiverCavePlannerSettings plannerSettings(IrisRiverCaves caves, long seed, IrisData data) {
|
||||
int horizontalRadius = generatedGrottoPublicationRadius(caves);
|
||||
int maximumDepth = caves.getMaxBoreDepth() + caves.getGrottoVerticalRadius() + 1;
|
||||
int throatLength = caves.getMaxBoreDepth() + horizontalRadius;
|
||||
return new RiverCavePlannerSettings(
|
||||
horizontalRadius,
|
||||
maximumDepth,
|
||||
caves.getMaxFloodVolume(),
|
||||
throatLength,
|
||||
caves.getThroatRadius(),
|
||||
caves.getGrottoHorizontalRadius(),
|
||||
caves.getGrottoVerticalRadius(),
|
||||
caves.getDryHeadroom(),
|
||||
fluidPolicy(caves.getExistingFluidPolicy()),
|
||||
new ConfiguredRiverGrottoShape(
|
||||
seed,
|
||||
data,
|
||||
caves.getGrottoShapeStyle(),
|
||||
caves.getGrottoWarpStyle(),
|
||||
caves.getGrottoWarpStrength()
|
||||
),
|
||||
caves.getMaxFloodRadius(),
|
||||
caves.getMaxFloodDepth()
|
||||
);
|
||||
}
|
||||
|
||||
private SourceCandidate sourceFor(
|
||||
IrisRiverRuntime runtime,
|
||||
CaveVoxelView view,
|
||||
IrisRiverCaves caves,
|
||||
RiverAnchor anchor
|
||||
) {
|
||||
int x = (int) StrictMath.floor(anchor.x());
|
||||
int z = (int) StrictMath.floor(anchor.z());
|
||||
IrisRiverSurfaceSample sample = runtime.sample(x, z);
|
||||
if (!isWetChannelBed(sample)) {
|
||||
return null;
|
||||
}
|
||||
int bedY = (int) Math.round(sample.terrainHeight());
|
||||
int headY = waterHeadY(sample, caves);
|
||||
int entryY = Math.max(bedY, headY);
|
||||
CavePosition entry = new CavePosition(x, entryY, z);
|
||||
if (!view.isInWorld(entry)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CavePosition existingTarget = findExistingTarget(view, caves, x, z, bedY, headY);
|
||||
RiverCaveMode requestedMode = sourceMode(
|
||||
caves.getMode(),
|
||||
runtime.isTerminalCaveAnchor(anchor)
|
||||
);
|
||||
CavePosition target;
|
||||
RiverCaveMode sourceMode;
|
||||
if (requestedMode == RiverCaveMode.GENERATED_GROTTO) {
|
||||
target = findGeneratedTarget(view, caves, entry, headY, 0, 0);
|
||||
sourceMode = RiverCaveMode.GENERATED_GROTTO;
|
||||
} else if (requestedMode == RiverCaveMode.GROTTO_OR_CLOSED_COMPONENT) {
|
||||
target = existingTarget;
|
||||
sourceMode = RiverCaveMode.CLOSED_COMPONENT;
|
||||
if (target == null) {
|
||||
target = findGeneratedTarget(view, caves, entry, headY, 0, 0);
|
||||
sourceMode = RiverCaveMode.GENERATED_GROTTO;
|
||||
}
|
||||
} else {
|
||||
target = existingTarget;
|
||||
sourceMode = requestedMode;
|
||||
}
|
||||
if (target == null && caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO) {
|
||||
target = findGeneratedTarget(view, caves, entry, headY, 0, 0);
|
||||
sourceMode = RiverCaveMode.GENERATED_GROTTO;
|
||||
}
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
RiverCaveSource source = new RiverCaveSource(anchor.stableId(), entry, target, headY, sourceMode);
|
||||
return new SourceCandidate(entry, headY, source);
|
||||
}
|
||||
|
||||
static boolean isWetChannelBed(IrisRiverSurfaceSample sample) {
|
||||
return sample.river().present()
|
||||
&& sample.river().state() == RiverRouteState.WET
|
||||
&& sample.river().section() == RiverSection.CHANNEL
|
||||
&& sample.surfaceFluid();
|
||||
}
|
||||
|
||||
static CavePosition findExistingTarget(
|
||||
CaveVoxelView view,
|
||||
IrisRiverCaves caves,
|
||||
int x,
|
||||
int z,
|
||||
int bedY,
|
||||
int headY
|
||||
) {
|
||||
int maximumY = Math.min(bedY - 1, headY);
|
||||
int minimumY = Math.max(1, bedY - caves.getMaxBoreDepth());
|
||||
for (int y = maximumY; y >= minimumY; y--) {
|
||||
CavePosition position = new CavePosition(x, y, z);
|
||||
CaveVoxel voxel = view.voxelAt(position);
|
||||
if (voxel != CaveVoxel.SOLID) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static CavePosition findGeneratedTarget(
|
||||
CaveVoxelView view,
|
||||
IrisRiverCaves caves,
|
||||
CavePosition entry,
|
||||
int headY,
|
||||
int offsetX,
|
||||
int offsetZ
|
||||
) {
|
||||
int preferredY = headY + caves.getDryHeadroom() - caves.getGrottoVerticalRadius();
|
||||
int maximumY = Math.min(Math.min(entry.y() - 1, headY), preferredY);
|
||||
int minimumY = Math.max(1, entry.y() - caves.getMaxBoreDepth());
|
||||
for (int y = maximumY; y >= minimumY; y--) {
|
||||
CavePosition target = new CavePosition(entry.x() + offsetX, y, entry.z() + offsetZ);
|
||||
if (view.isInWorld(target) && view.voxelAt(target) == CaveVoxel.SOLID) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private RiverCaveSource fallbackSource(
|
||||
CaveVoxelView view,
|
||||
IrisRiverCaves caves,
|
||||
RiverCavePlannerSettings settings,
|
||||
SourceCandidate candidate,
|
||||
RiverCaveSource rejected
|
||||
) {
|
||||
int fallbackDistance = caves.getThroatRadius() + 2;
|
||||
for (int index = 0; index < FALLBACK_X.length; index++) {
|
||||
int offsetX = FALLBACK_X[index] * fallbackDistance;
|
||||
int offsetZ = FALLBACK_Z[index] * fallbackDistance;
|
||||
CavePosition target = findGeneratedTarget(
|
||||
view,
|
||||
caves,
|
||||
candidate.entry(),
|
||||
candidate.waterHeadY(),
|
||||
offsetX,
|
||||
offsetZ
|
||||
);
|
||||
if (target == null || target.equals(rejected.target())) {
|
||||
continue;
|
||||
}
|
||||
RiverCaveSource fallback = new RiverCaveSource(
|
||||
rejected.sourceId(),
|
||||
candidate.entry(),
|
||||
target,
|
||||
candidate.waterHeadY(),
|
||||
RiverCaveMode.GENERATED_GROTTO
|
||||
);
|
||||
if (planner.plan(view, fallback, settings).accepted()) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static RiverCaveMode sourceMode(IrisRiverCaveMode mode, boolean forcedTerminal) {
|
||||
if (forcedTerminal) {
|
||||
return RiverCaveMode.GENERATED_GROTTO;
|
||||
}
|
||||
return switch (mode) {
|
||||
case FLOOD_CLOSED_COMPONENT -> RiverCaveMode.CLOSED_COMPONENT;
|
||||
case GENERATE_GROTTO -> RiverCaveMode.GENERATED_GROTTO;
|
||||
case GROTTO_OR_CLOSED_COMPONENT -> RiverCaveMode.GROTTO_OR_CLOSED_COMPONENT;
|
||||
case WATERFALL_POOL -> RiverCaveMode.WATERFALL_POOL;
|
||||
case SEALED -> throw new IllegalArgumentException("Sealed river caves do not create sources");
|
||||
};
|
||||
}
|
||||
|
||||
private void publishLocal(
|
||||
MantleWriter writer,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
RiverCavePlanningResult result,
|
||||
Map<Long, String> floodedBiomes
|
||||
) {
|
||||
Map<CavePosition, RiverCaveSource> owners = actionOwners(result);
|
||||
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(result.actions().entrySet());
|
||||
actions.sort(ACTION_ORDER);
|
||||
for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) {
|
||||
CavePosition position = entry.getKey();
|
||||
if (!owns(chunkX, chunkZ, position)) {
|
||||
continue;
|
||||
}
|
||||
RiverCaveSource source = owners.get(position);
|
||||
String biome = source == null ? "" : floodedBiomes.getOrDefault(source.sourceId(), "");
|
||||
if (entry.getValue() == RiverCaveAction.SEAL_GUARD) {
|
||||
biome = "";
|
||||
}
|
||||
writer.setData(
|
||||
position.x(),
|
||||
position.y(),
|
||||
position.z(),
|
||||
new RiverCaveHydrology(entry.getValue(), biome)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void publishTunnelLocal(
|
||||
MantleWriter writer,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
TunnelPlan plan
|
||||
) {
|
||||
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(plan.actions().entrySet());
|
||||
actions.sort(ACTION_ORDER);
|
||||
for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) {
|
||||
CavePosition position = entry.getKey();
|
||||
if (owns(chunkX, chunkZ, position)) {
|
||||
writer.setData(position.x(), position.y(), position.z(), RiverCaveHydrology.of(entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<CavePosition, RiverCaveSource> actionOwners(RiverCavePlanningResult result) {
|
||||
Map<CavePosition, RiverCaveSource> owners = new LinkedHashMap<>();
|
||||
for (RiverCavePlan plan : result.plans()) {
|
||||
if (!plan.accepted()) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition position : plan.actions().keySet()) {
|
||||
owners.put(position, plan.source());
|
||||
}
|
||||
}
|
||||
return owners;
|
||||
}
|
||||
|
||||
private record SourceCandidate(
|
||||
CavePosition entry,
|
||||
int waterHeadY,
|
||||
RiverCaveSource source
|
||||
) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface FootprintSampler {
|
||||
RiverSample sample(double minimumX, double minimumZ, double maximumX, double maximumZ);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface TunnelSampler {
|
||||
IrisRiverTunnelSample sample(int x, int z);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface SurfaceSampler {
|
||||
IrisRiverSurfaceSample sample(int x, int z);
|
||||
}
|
||||
|
||||
interface TunnelVoxelView extends CaveVoxelView {
|
||||
RiverCaveAction riverActionAt(CavePosition position);
|
||||
}
|
||||
|
||||
record TunnelPlan(
|
||||
Map<CavePosition, RiverCaveAction> actions,
|
||||
Map<CavePosition, CaveVoxelPrecondition> preconditions
|
||||
) {
|
||||
static TunnelPlan empty() {
|
||||
return new TunnelPlan(Map.of(), Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
private record TunnelColumn(Map<CavePosition, RiverCaveAction> actions) {
|
||||
}
|
||||
}
|
||||
+48
-24
@@ -32,16 +32,17 @@ final class SurfaceFluidBoundaryPlan {
|
||||
int[] chunkSurfaceHeights,
|
||||
double[] fieldSurfaceHeights,
|
||||
boolean[] fieldHasFluid,
|
||||
double[] fieldFluidHeights,
|
||||
int fieldSize,
|
||||
int padding,
|
||||
int fluidHeight,
|
||||
int[] boundaryStartY
|
||||
long[] boundaries
|
||||
) {
|
||||
if (chunkSurfaceHeights == null || chunkSurfaceHeights.length < CHUNK_AREA
|
||||
|| boundaryStartY == null || boundaryStartY.length < CHUNK_AREA
|
||||
|| boundaries == null || boundaries.length < CHUNK_AREA
|
||||
|| padding < 1 || fieldSize < CHUNK_SIZE + (padding * 2)
|
||||
|| fieldSurfaceHeights == null || fieldSurfaceHeights.length < fieldSize * fieldSize
|
||||
|| fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize) {
|
||||
|| fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize
|
||||
|| fieldFluidHeights == null || fieldFluidHeights.length < fieldSize * fieldSize) {
|
||||
throw new IllegalArgumentException("Surface fluid boundary fields do not cover a padded chunk");
|
||||
}
|
||||
|
||||
@@ -51,44 +52,67 @@ final class SurfaceFluidBoundaryPlan {
|
||||
int fieldZ = localZ + padding;
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
||||
int boundaryY = NO_BOUNDARY;
|
||||
int boundaryEndY = Integer.MIN_VALUE;
|
||||
int surfaceY = chunkSurfaceHeights[columnIndex];
|
||||
int fieldIndex = (fieldX * fieldSize) + fieldZ;
|
||||
int fluidHeight = roundedHeight(fieldFluidHeights[fieldIndex]);
|
||||
if (fieldHasFluid[fieldIndex] && surfaceY < fluidHeight) {
|
||||
boundaryY = surfaceY;
|
||||
boundaryEndY = fluidHeight;
|
||||
}
|
||||
|
||||
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
|
||||
((fieldX - 1) * fieldSize) + fieldZ, fluidHeight);
|
||||
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
|
||||
((fieldX + 1) * fieldSize) + fieldZ, fluidHeight);
|
||||
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
|
||||
(fieldX * fieldSize) + fieldZ - 1, fluidHeight);
|
||||
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
|
||||
(fieldX * fieldSize) + fieldZ + 1, fluidHeight);
|
||||
boundaryStartY[columnIndex] = boundaryY;
|
||||
long boundary = expandBoundary(boundaryY, boundaryEndY, fieldSurfaceHeights,
|
||||
fieldHasFluid, fieldFluidHeights, ((fieldX - 1) * fieldSize) + fieldZ);
|
||||
boundary = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
|
||||
fieldHasFluid, fieldFluidHeights, ((fieldX + 1) * fieldSize) + fieldZ);
|
||||
boundary = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
|
||||
fieldHasFluid, fieldFluidHeights, (fieldX * fieldSize) + fieldZ - 1);
|
||||
boundaries[columnIndex] = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
|
||||
fieldHasFluid, fieldFluidHeights, (fieldX * fieldSize) + fieldZ + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean protects(int[] boundaryStartY, int columnIndex, int y, int fluidHeight) {
|
||||
return boundaryStartY != null
|
||||
&& columnIndex >= 0
|
||||
&& columnIndex < boundaryStartY.length
|
||||
&& y >= boundaryStartY[columnIndex]
|
||||
&& y <= fluidHeight;
|
||||
static boolean protects(long[] boundaries, int columnIndex, int y) {
|
||||
if (boundaries == null || columnIndex < 0 || columnIndex >= boundaries.length) {
|
||||
return false;
|
||||
}
|
||||
long boundary = boundaries[columnIndex];
|
||||
return y >= startY(boundary) && y <= endY(boundary);
|
||||
}
|
||||
|
||||
private static int lowerBoundary(
|
||||
static int startY(long boundary) {
|
||||
return (int) (boundary >> 32);
|
||||
}
|
||||
|
||||
static int endY(long boundary) {
|
||||
return (int) boundary;
|
||||
}
|
||||
|
||||
private static long expandBoundary(
|
||||
int currentBoundaryY,
|
||||
int currentBoundaryEndY,
|
||||
double[] fieldSurfaceHeights,
|
||||
boolean[] fieldHasFluid,
|
||||
int fieldIndex,
|
||||
int fluidHeight
|
||||
double[] fieldFluidHeights,
|
||||
int fieldIndex
|
||||
) {
|
||||
int fluidHeight = roundedHeight(fieldFluidHeights[fieldIndex]);
|
||||
int neighborSurfaceY = (int) Math.round(fieldSurfaceHeights[fieldIndex]);
|
||||
if (!fieldHasFluid[fieldIndex] || neighborSurfaceY >= fluidHeight) {
|
||||
return currentBoundaryY;
|
||||
return boundary(currentBoundaryY, currentBoundaryEndY);
|
||||
}
|
||||
return Math.min(currentBoundaryY, neighborSurfaceY + 1);
|
||||
return boundary(
|
||||
Math.min(currentBoundaryY, neighborSurfaceY + 1),
|
||||
Math.max(currentBoundaryEndY, fluidHeight)
|
||||
);
|
||||
}
|
||||
|
||||
private static int roundedHeight(double height) {
|
||||
return Double.isFinite(height) ? (int) Math.round(height) : Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
static long boundary(int startY, int endY) {
|
||||
return ((long) startY << 32) | (endY & 0xffffffffL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ 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.engine.object.IrisProceduralBlocks;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
@@ -105,49 +107,26 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
|
||||
int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
|
||||
int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight();
|
||||
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> {
|
||||
if (cavern == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (yy >= worldHeightSpan || yy <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int rx = xx & 15;
|
||||
int rz = zz & 15;
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
|
||||
|
||||
if (upperSurfaceHeights != null && yy >= upperSurfaceHeights[columnIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformBlockState current = output.getRaw(rx, yy, rz);
|
||||
boolean explicitCarveIntent = hasExplicitCarveIntent(cavern);
|
||||
|
||||
if (shouldPreserveExistingFluid(cavern, current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
columnMasks[columnIndex].add(yy);
|
||||
|
||||
if (!cavern.getCustomBiome().isEmpty()) {
|
||||
scratch.customCaveBiomePresent = true;
|
||||
}
|
||||
|
||||
if (current.isAir() && !explicitCarveIntent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (explicitCarveIntent) {
|
||||
// Only a fluid cavern consumes the fluid sample, and on the maintenance path that
|
||||
// sample is a full procedural stream evaluation, so never take it per voxel.
|
||||
PlatformBlockState fluid = isFluidIntent(cavern) ? context.getFluid().get(rx, rz) : null;
|
||||
output.setRaw(rx, yy, rz, resolveExplicitCarveState(cavern, fluid, LAVA, AIR));
|
||||
} else if (usesDefaultLava(caveLavaHeight, yy)) {
|
||||
output.setRaw(rx, yy, rz, LAVA);
|
||||
} else {
|
||||
output.setRaw(rx, yy, rz, AIR);
|
||||
CarveResolutionContext resolutionContext = new CarveResolutionContext(
|
||||
output,
|
||||
context,
|
||||
scratch,
|
||||
columnMasks,
|
||||
upperSurfaceHeights,
|
||||
worldHeightSpan,
|
||||
caveLavaHeight
|
||||
);
|
||||
CarveResolver carveResolver = new CarveResolver(resolutionContext);
|
||||
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
|
||||
xx,
|
||||
yy,
|
||||
zz,
|
||||
cavern,
|
||||
dataIfPresent(mantleChunk, xx, yy, zz, RiverCaveHydrology.class)
|
||||
));
|
||||
mantleChunk.iterate(RiverCaveHydrology.class, (xx, yy, zz, hydrology) -> {
|
||||
if (dataIfPresent(mantleChunk, xx, yy, zz, MatterCavern.class) == null) {
|
||||
carveResolver.apply(xx, yy, zz, null, hydrology);
|
||||
}
|
||||
});
|
||||
if (scratch.customCaveBiomePresent) {
|
||||
@@ -161,6 +140,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
|
||||
try {
|
||||
walls.forEach((rx, yy, rz, cavern) -> {
|
||||
RiverCaveHydrology hydrology = dataIfPresent(
|
||||
mantleChunk, rx, yy, rz, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
return;
|
||||
}
|
||||
int worldX = rx + chunkBlockX;
|
||||
int worldZ = rz + chunkBlockZ;
|
||||
String customBiome = cavern.getCustomBiome();
|
||||
@@ -179,14 +163,39 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
});
|
||||
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
processColumnFromMask(output, mantleChunk, mantle, columnMasks[columnIndex], columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
|
||||
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
||||
int localZ = columnIndex & 15;
|
||||
processColumnFromMask(
|
||||
output,
|
||||
mantleChunk,
|
||||
mantle,
|
||||
columnMasks[columnIndex],
|
||||
columnIndex,
|
||||
x,
|
||||
z,
|
||||
resolverState,
|
||||
caveBiomeCache,
|
||||
customBiomeCache,
|
||||
context.getFluid().get(localX, localZ)
|
||||
);
|
||||
}
|
||||
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
if (boundaryMasks[columnIndex].isEmpty() || !columnMasks[columnIndex].isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], walls, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
|
||||
processBoundaryColumnFromMask(
|
||||
output,
|
||||
mantleChunk,
|
||||
boundaryMasks[columnIndex],
|
||||
walls,
|
||||
columnIndex,
|
||||
x,
|
||||
z,
|
||||
resolverState,
|
||||
caveBiomeCache,
|
||||
customBiomeCache
|
||||
);
|
||||
}
|
||||
|
||||
// Surface-break carving must not leave an ore cap suspended across the opening.
|
||||
@@ -252,6 +261,141 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
return cavern.getLiquid() == 3 ? air : null;
|
||||
}
|
||||
|
||||
static MatterCavern composeCavern(MatterCavern baseline, RiverCaveHydrology hydrology) {
|
||||
return hydrology == null ? baseline : hydrology.asCavern();
|
||||
}
|
||||
|
||||
static PlatformBlockState resolveHydrologyState(
|
||||
RiverCaveHydrology hydrology,
|
||||
PlatformBlockState current,
|
||||
PlatformBlockState fluid,
|
||||
PlatformBlockState air
|
||||
) {
|
||||
if (hydrology == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (hydrology.action()) {
|
||||
case WET_SOURCE -> fluid;
|
||||
case FALLING_WATER -> fallingFluidState(fluid);
|
||||
case DRY_AIR -> air;
|
||||
case SEAL_GUARD -> normalizeWaterlogging(current, null);
|
||||
};
|
||||
}
|
||||
|
||||
static PlatformBlockState normalizeWaterlogging(PlatformBlockState state, PlatformBlockState resultingFluid) {
|
||||
if (state == null || B.isFluid(state) || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
|
||||
return state;
|
||||
}
|
||||
String target = resultingFluid != null && resultingFluid.isWater() ? "true" : "false";
|
||||
if (target.equals(IrisProceduralBlocks.propertyValue(state, "waterlogged"))) {
|
||||
return state;
|
||||
}
|
||||
return state.withProperty("waterlogged", target);
|
||||
}
|
||||
|
||||
static PlatformBlockState normalizeHydrologyWaterlogging(
|
||||
PlatformBlockState state,
|
||||
MatterCavern baseline,
|
||||
RiverCaveHydrology hydrology,
|
||||
PlatformBlockState columnFluid
|
||||
) {
|
||||
if (hydrology == null) {
|
||||
return state;
|
||||
}
|
||||
MatterCavern composed = composeCavern(baseline, hydrology);
|
||||
PlatformBlockState resultingFluid = isFluidIntent(composed) ? columnFluid : null;
|
||||
return normalizeWaterlogging(state, resultingFluid);
|
||||
}
|
||||
|
||||
private static PlatformBlockState fallingFluidState(PlatformBlockState fluid) {
|
||||
if (fluid == null || !IrisProceduralBlocks.hasProperty(fluid, "level")) {
|
||||
return fluid;
|
||||
}
|
||||
if ("8".equals(IrisProceduralBlocks.propertyValue(fluid, "level"))) {
|
||||
return fluid;
|
||||
}
|
||||
return fluid.withProperty("level", "8");
|
||||
}
|
||||
|
||||
private final class CarveResolver {
|
||||
private final CarveResolutionContext context;
|
||||
|
||||
private CarveResolver(CarveResolutionContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private void apply(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
MatterCavern baseline,
|
||||
RiverCaveHydrology hydrology
|
||||
) {
|
||||
if (y >= context.worldHeightSpan() || y <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int localX = x & 15;
|
||||
int localZ = z & 15;
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
||||
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformBlockState current = context.output().getRaw(localX, y, localZ);
|
||||
if (hydrology != null && hydrology.action() == RiverCaveAction.SEAL_GUARD) {
|
||||
PlatformBlockState normalized = resolveHydrologyState(hydrology, current, null, AIR);
|
||||
if (normalized != current) {
|
||||
context.output().setRaw(localX, y, localZ, normalized);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MatterCavern cavern = composeCavern(baseline, hydrology);
|
||||
if (cavern == null || shouldPreserveExistingFluid(cavern, current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.columnMasks()[columnIndex].add(y);
|
||||
if (!cavern.getCustomBiome().isEmpty()) {
|
||||
context.scratch().customCaveBiomePresent = true;
|
||||
}
|
||||
|
||||
boolean explicitCarveIntent = hasExplicitCarveIntent(cavern);
|
||||
if (current.isAir() && !explicitCarveIntent) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformBlockState fluid = isFluidIntent(cavern)
|
||||
? context.chunkContext().getFluid().get(localX, localZ)
|
||||
: null;
|
||||
if (hydrology != null) {
|
||||
context.output().setRaw(localX, y, localZ,
|
||||
resolveHydrologyState(hydrology, current, fluid, AIR));
|
||||
return;
|
||||
}
|
||||
if (explicitCarveIntent) {
|
||||
context.output().setRaw(localX, y, localZ,
|
||||
resolveExplicitCarveState(cavern, fluid, LAVA, AIR));
|
||||
} else if (usesDefaultLava(context.caveLavaHeight(), y)) {
|
||||
context.output().setRaw(localX, y, localZ, LAVA);
|
||||
} else {
|
||||
context.output().setRaw(localX, y, localZ, AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record CarveResolutionContext(
|
||||
Hunk<PlatformBlockState> output,
|
||||
ChunkContext chunkContext,
|
||||
IrisCarveScratch scratch,
|
||||
CarveColumnMask[] columnMasks,
|
||||
int[] upperSurfaceHeights,
|
||||
int worldHeightSpan,
|
||||
int caveLavaHeight
|
||||
) {
|
||||
}
|
||||
|
||||
private void addInternalWallsFromMasks(CarveWallBuffer walls, CarveColumnMask[] columnMasks) {
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
CarveColumnMask columnMask = columnMasks[columnIndex];
|
||||
@@ -291,18 +435,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
int rz = columnIndex & 15;
|
||||
int yy = columnMask.nextSetBit(0);
|
||||
while (yy >= 0) {
|
||||
MatterCavern cavern = mc.get(rx, yy, rz, MatterCavern.class);
|
||||
MatterCavern cavern = composedCavernAt(mc, rx, yy, rz);
|
||||
if (cavern != null) {
|
||||
if (rz < 15 && mc.get(rx, yy, rz + 1, MatterCavern.class) == null) {
|
||||
if (rz < 15 && composedCavernAt(mc, rx, yy, rz + 1) == null) {
|
||||
walls.put(rx, yy, rz + 1, cavern);
|
||||
}
|
||||
if (rx < 15 && mc.get(rx + 1, yy, rz, MatterCavern.class) == null) {
|
||||
if (rx < 15 && composedCavernAt(mc, rx + 1, yy, rz) == null) {
|
||||
walls.put(rx + 1, yy, rz, cavern);
|
||||
}
|
||||
if (rz > 0 && mc.get(rx, yy, rz - 1, MatterCavern.class) == null) {
|
||||
if (rz > 0 && composedCavernAt(mc, rx, yy, rz - 1) == null) {
|
||||
walls.put(rx, yy, rz - 1, cavern);
|
||||
}
|
||||
if (rx > 0 && mc.get(rx - 1, yy, rz, MatterCavern.class) == null) {
|
||||
if (rx > 0 && composedCavernAt(mc, rx - 1, yy, rz) == null) {
|
||||
walls.put(rx - 1, yy, rz, cavern);
|
||||
}
|
||||
}
|
||||
@@ -370,11 +514,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
int neighborX,
|
||||
int neighborZ
|
||||
) {
|
||||
if (mc.get(localX, yy, localZ, MatterCavern.class) != null) {
|
||||
if (composedCavernAt(mc, localX, yy, localZ) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MatterCavern neighbor = neighborChunk.get(neighborX, yy, neighborZ, MatterCavern.class);
|
||||
MatterCavern neighbor = composedCavernAt(neighborChunk, neighborX, yy, neighborZ);
|
||||
if (neighbor == null) {
|
||||
return;
|
||||
}
|
||||
@@ -392,6 +536,24 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
return plate.get(chunkX & 31, chunkZ & 31);
|
||||
}
|
||||
|
||||
private MatterCavern composedCavernAt(MantleChunk<Matter> mantleChunk, int x, int y, int z) {
|
||||
MatterCavern baseline = dataIfPresent(mantleChunk, x, y, z, MatterCavern.class);
|
||||
RiverCaveHydrology hydrology = dataIfPresent(mantleChunk, x, y, z, RiverCaveHydrology.class);
|
||||
return composeCavern(baseline, hydrology);
|
||||
}
|
||||
|
||||
private static <T> T dataIfPresent(MantleChunk<Matter> mantleChunk, int x, int y, int z, Class<T> type) {
|
||||
int section = y >> 4;
|
||||
if (y < 0 || !mantleChunk.exists(section)) {
|
||||
return null;
|
||||
}
|
||||
Matter matter = mantleChunk.get(section);
|
||||
if (matter == null || !matter.hasSlice(type)) {
|
||||
return null;
|
||||
}
|
||||
return matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
|
||||
}
|
||||
|
||||
private void processColumnFromMask(
|
||||
Hunk<PlatformBlockState> output,
|
||||
MantleChunk<Matter> mc,
|
||||
@@ -402,7 +564,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
int chunkZ,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache
|
||||
Map<String, IrisBiome> customBiomeCache,
|
||||
PlatformBlockState columnFluid
|
||||
) {
|
||||
if (columnMask == null || columnMask.isEmpty()) {
|
||||
return;
|
||||
@@ -429,7 +592,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
zone.ceiling = buf;
|
||||
} else {
|
||||
if (zone.isValid(getEngine())) {
|
||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache);
|
||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
|
||||
caveBiomeCache, customBiomeCache, columnFluid);
|
||||
}
|
||||
zone = new CaveZone();
|
||||
zone.setFloor(y);
|
||||
@@ -441,12 +605,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
}
|
||||
|
||||
if (zone.isValid(getEngine())) {
|
||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache);
|
||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
|
||||
caveBiomeCache, customBiomeCache, columnFluid);
|
||||
}
|
||||
}
|
||||
|
||||
private void processBoundaryColumnFromMask(
|
||||
Hunk<PlatformBlockState> output,
|
||||
MantleChunk<Matter> mantleChunk,
|
||||
CarveColumnMask boundaryMask,
|
||||
CarveWallBuffer walls,
|
||||
int columnIndex,
|
||||
@@ -473,18 +639,21 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (y == zoneCeiling + 1) {
|
||||
zoneCeiling = y;
|
||||
} else {
|
||||
paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
|
||||
paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
|
||||
resolverState, caveBiomeCache, customBiomeCache);
|
||||
zoneFloor = y;
|
||||
zoneCeiling = y;
|
||||
}
|
||||
y = boundaryMask.nextSetBit(y + 1);
|
||||
}
|
||||
|
||||
paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
|
||||
paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
|
||||
resolverState, caveBiomeCache, customBiomeCache);
|
||||
}
|
||||
|
||||
private void paintBoundaryZone(
|
||||
Hunk<PlatformBlockState> output,
|
||||
MantleChunk<Matter> mantleChunk,
|
||||
CarveWallBuffer walls,
|
||||
int rx,
|
||||
int rz,
|
||||
@@ -517,6 +686,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (floorY < 0) {
|
||||
break;
|
||||
}
|
||||
RiverCaveHydrology hydrology = dataIfPresent(
|
||||
mantleChunk, rx, floorY, rz, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState existing = output.getRaw(rx, floorY, rz);
|
||||
PlatformBlockState layer = floorLayers.get(i);
|
||||
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
|
||||
@@ -539,6 +713,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (ceilingY >= worldMaxY) {
|
||||
break;
|
||||
}
|
||||
RiverCaveHydrology hydrology = dataIfPresent(
|
||||
mantleChunk, rx, ceilingY, rz, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState existing = output.getRaw(rx, ceilingY, rz);
|
||||
if (!B.isSolid(existing)) {
|
||||
continue;
|
||||
@@ -565,7 +744,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
return (h & 15L) == 0L;
|
||||
}
|
||||
|
||||
private void processZone(Hunk<PlatformBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
private void processZone(Hunk<PlatformBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle,
|
||||
CaveZone zone, int rx, int rz, int xx, int zz,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache,
|
||||
PlatformBlockState columnFluid) {
|
||||
int maxY = output.getHeight();
|
||||
|
||||
if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) {
|
||||
@@ -590,6 +774,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
|
||||
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
|
||||
if (floorBiome == null && ceilingBiome == null) {
|
||||
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -600,6 +785,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
break;
|
||||
}
|
||||
int y = zone.floor - i - 1;
|
||||
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, y, rz, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState block = floorBlocks.get(i);
|
||||
PlatformBlockState existing = output.getRaw(rx, y, rz);
|
||||
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, y, rz, block)) {
|
||||
@@ -620,6 +809,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (cy >= maxY) {
|
||||
break;
|
||||
}
|
||||
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, cy, rz, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState block = ceilingBlocks.get(i);
|
||||
PlatformBlockState existing = output.getRaw(rx, cy, rz);
|
||||
if (!B.isSolid(existing)) {
|
||||
@@ -646,10 +839,43 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (ceilingDecorators.length > 0 && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
|
||||
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
|
||||
}
|
||||
|
||||
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
|
||||
}
|
||||
|
||||
private void normalizeCaveZoneWaterlogging(
|
||||
Hunk<PlatformBlockState> output,
|
||||
MantleChunk<Matter> mantleChunk,
|
||||
CaveZone zone,
|
||||
int localX,
|
||||
int localZ,
|
||||
PlatformBlockState columnFluid
|
||||
) {
|
||||
int minimumY = Math.max(0, zone.floor - 1);
|
||||
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
|
||||
for (int y = minimumY; y <= maximumY; y++) {
|
||||
RiverCaveHydrology hydrology = dataIfPresent(
|
||||
mantleChunk, localX, y, localZ, RiverCaveHydrology.class);
|
||||
if (hydrology == null) {
|
||||
continue;
|
||||
}
|
||||
MatterCavern baseline = dataIfPresent(
|
||||
mantleChunk, localX, y, localZ, MatterCavern.class);
|
||||
PlatformBlockState current = output.getRaw(localX, y, localZ);
|
||||
PlatformBlockState normalized = normalizeHydrologyWaterlogging(
|
||||
current,
|
||||
baseline,
|
||||
hydrology,
|
||||
columnFluid
|
||||
);
|
||||
if (normalized != current) {
|
||||
output.setRaw(localX, y, localZ, normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IrisBiome resolveCaveBoundaryBiome(MantleChunk<Matter> mantleChunk, int x, int y, int z, int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
MatterCavern cavern = mantleChunk.get(x, y, z, MatterCavern.class);
|
||||
MatterCavern cavern = composedCavernAt(mantleChunk, x, y, z);
|
||||
return resolveCaveBoundaryBiome(
|
||||
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache);
|
||||
}
|
||||
|
||||
@@ -57,10 +57,9 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
|
||||
IrisDimension dimension = getDimension();
|
||||
boolean walls = dimension.isPostProcessingWalls();
|
||||
boolean slabs = dimension.isPostProcessingSlabs();
|
||||
int fluidHeight = dimension.getFluidHeight();
|
||||
for (int i = 0; i < width; i++) {
|
||||
for (int j = 0; j < depth; j++) {
|
||||
post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs, fluidHeight);
|
||||
post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +89,7 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
|
||||
return heights;
|
||||
}
|
||||
|
||||
private void post(int currentPostX, int currentPostZ, Hunk<PlatformBlockState> currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs, int fluidHeight) {
|
||||
private void post(int currentPostX, int currentPostZ, Hunk<PlatformBlockState> currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs) {
|
||||
// x/z are world coordinates, the hunk is indexed relative to this chunk origin.
|
||||
int originX = x - currentPostX;
|
||||
int originZ = z - currentPostZ;
|
||||
@@ -100,6 +99,7 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
|
||||
int hb = heights[center + planeWidth];
|
||||
int hc = heights[center - 1];
|
||||
int hd = heights[center - planeWidth];
|
||||
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
|
||||
|
||||
// Floating Nibs
|
||||
int g = 0;
|
||||
|
||||
@@ -50,6 +50,17 @@ public interface IObjectPlacer {
|
||||
|
||||
int getFluidHeight();
|
||||
|
||||
default int getFluidHeight(int x, int z) {
|
||||
Engine engine = getEngine();
|
||||
if (engine == null || engine.getComplex() == null) {
|
||||
return getFluidHeight();
|
||||
}
|
||||
int coordinateShift = getFluidHeight() - engine.getDimension().getFluidHeight();
|
||||
return coordinateShift + (int) Math.round(
|
||||
engine.getComplex().getRiverWaterSurfaceStream().get(x, z)
|
||||
);
|
||||
}
|
||||
|
||||
boolean isDebugSmartBore();
|
||||
|
||||
void setTile(int xx, int yy, int zz, TileData tile);
|
||||
|
||||
@@ -130,6 +130,8 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
private int lockLayersMax = 7;
|
||||
@Desc("Profile-driven 3D cave configuration")
|
||||
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
||||
@Desc("Biome-level river routing, shape, cave-entry, and biome-pool overrides. Omit to inherit region and dimension settings.")
|
||||
private IrisRiverOverride riverOverride = null;
|
||||
@MinNumber(1)
|
||||
@MaxNumber(512)
|
||||
@Desc("The rarity of this biome (integer)")
|
||||
|
||||
@@ -36,7 +36,7 @@ final class IrisBiomeColorRenderer {
|
||||
|
||||
static Color getColor(IrisBiome biome, Engine engine, RenderType type) {
|
||||
switch (type) {
|
||||
case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND -> {
|
||||
case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND, RIVER -> {
|
||||
return biome.getCacheColor().aquire(() -> {
|
||||
if (biome.getColor() == null) {
|
||||
RandomColor randomColor = new RandomColor(biome.getName().hashCode());
|
||||
|
||||
@@ -164,6 +164,8 @@ public class IrisDimension extends IrisRegistrant {
|
||||
private KList<IrisDimensionCarvingEntry> carving = new KList<>();
|
||||
@Desc("Profile-driven 3D cave configuration")
|
||||
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
||||
@Desc("Connected surface rivers and contained river cave-water generation.")
|
||||
private IrisRiverNetwork rivers = new IrisRiverNetwork();
|
||||
@Desc("Refuse to place surface objects and trees over carved surface openings.")
|
||||
private boolean requireObjectSurfaceSupport = true;
|
||||
@MinNumber(0)
|
||||
@@ -489,6 +491,11 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
|
||||
Deque<String> pending = new ArrayDeque<>();
|
||||
IrisRiverNetwork riverNetwork = getRivers();
|
||||
boolean riversEnabled = riverNetwork != null && riverNetwork.isEnabled();
|
||||
if (riversEnabled && riverNetwork.getBiomes() != null) {
|
||||
addReachableBiomeKeys(pending, riverNetwork.getBiomes().getAllBiomeIds());
|
||||
}
|
||||
KList<String> regionKeys = getRegions();
|
||||
if (regionKeys != null) {
|
||||
for (String regionKey : regionKeys) {
|
||||
@@ -496,7 +503,8 @@ public class IrisDimension extends IrisRegistrant {
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
addReachableBiomeKeys(pending, region.getAllBiomeIds());
|
||||
addReachableBiomeKeys(pending,
|
||||
riversEnabled ? region.getAllBiomeIds() : region.getNaturalBiomeIds());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,6 +539,9 @@ public class IrisDimension extends IrisRegistrant {
|
||||
biomes.put(loadKey, biome);
|
||||
addReachableBiomeKeys(pending, biome.getChildren());
|
||||
addReachableBiomeKey(pending, biome.getCarvingBiome());
|
||||
if (riversEnabled && biome.getRiverOverride() != null) {
|
||||
addReachableBiomeKeys(pending, biome.getRiverOverride().getAllBiomeIds());
|
||||
}
|
||||
|
||||
KList<IrisFloatingChildBiomes> floatingChildren = biome.getFloatingChildBiomes();
|
||||
if (floatingChildren == null) {
|
||||
|
||||
@@ -28,6 +28,9 @@ public enum IrisEngineStreamType {
|
||||
@Desc("Represents the given slope at the x, z coordinates")
|
||||
SLOPE((f) -> f.getComplex().getSlopeStream()),
|
||||
|
||||
@Desc("Represents terrain height before river incision and river biome replacement.")
|
||||
NATURAL_HEIGHT((f) -> f.getComplex().getNaturalHeightStream()),
|
||||
|
||||
@Desc("Represents the base generator height at the given position. This includes only the biome generators / interpolation and noise features but does not include carving, caves.")
|
||||
HEIGHT((f) -> f.getComplex().getHeightStream()),
|
||||
|
||||
@@ -41,7 +44,19 @@ public enum IrisEngineStreamType {
|
||||
REGION_STYLE((f) -> f.getComplex().getRegionStyleStream()),
|
||||
|
||||
@Desc("Represents the identity of regions. Each region has a unique number (very large numbers)")
|
||||
REGION_IDENTITY((f) -> f.getComplex().getRegionIdentityStream());
|
||||
REGION_IDENTITY((f) -> f.getComplex().getRegionIdentityStream()),
|
||||
|
||||
@Desc("Represents block distance from the nearest active river centerline.")
|
||||
RIVER_DISTANCE((f) -> f.getComplex().getRiverDistanceStream()),
|
||||
|
||||
@Desc("Represents the merged upstream flow carried by the active river reach.")
|
||||
RIVER_FLOW((f) -> f.getComplex().getRiverFlowStream()),
|
||||
|
||||
@Desc("Represents the normalized river terrain-incision weight.")
|
||||
RIVER_CARVE_WEIGHT((f) -> f.getComplex().getRiverCarveWeightStream()),
|
||||
|
||||
@Desc("Represents the solved river water-surface height.")
|
||||
RIVER_WATER_SURFACE((f) -> f.getComplex().getRiverWaterSurfaceStream());
|
||||
|
||||
private final Function<Engine, ProceduralStream<Double>> getter;
|
||||
|
||||
|
||||
@@ -332,7 +332,8 @@ final class IrisObjectPlacementRunner {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) {
|
||||
if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater()
|
||||
&& y + rty + ty >= placer.getFluidHeight(x, z)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ public class IrisRegion extends IrisRegistrant implements IRare {
|
||||
private double caveBiomeZoom = 1;
|
||||
@Desc("Profile-driven 3D cave configuration")
|
||||
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
||||
@Desc("Region-level river routing, shape, cave-entry, and biome-pool overrides. Omit to inherit dimension settings.")
|
||||
private IrisRiverOverride riverOverride = null;
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@Required
|
||||
@ArrayType(min = 1, type = String.class)
|
||||
@@ -277,18 +279,33 @@ public class IrisRegion extends IrisRegistrant implements IRare {
|
||||
}
|
||||
|
||||
public KSet<String> getAllBiomeIds() {
|
||||
KSet<String> names = getNaturalBiomeIds();
|
||||
if (riverOverride != null) {
|
||||
names.addAll(riverOverride.getAllBiomeIds());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public KSet<String> getNaturalBiomeIds() {
|
||||
KSet<String> names = new KSet<>();
|
||||
names.addAll(landBiomes);
|
||||
names.addAll(caveBiomes);
|
||||
names.addAll(seaBiomes);
|
||||
names.addAll(shoreBiomes);
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
public KList<IrisBiome> getAllBiomes(DataProvider g) {
|
||||
return resolveBiomes(g, getAllBiomeIds());
|
||||
}
|
||||
|
||||
public KList<IrisBiome> getNaturalBiomes(DataProvider g) {
|
||||
return resolveBiomes(g, getNaturalBiomeIds());
|
||||
}
|
||||
|
||||
private KList<IrisBiome> resolveBiomes(DataProvider g, KSet<String> biomeIds) {
|
||||
KMap<String, IrisBiome> b = new KMap<>();
|
||||
KSet<String> names = getAllBiomeIds();
|
||||
KSet<String> names = biomeIds.copy();
|
||||
|
||||
while (!names.isEmpty()) {
|
||||
for (String i : new KList<>(names)) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Biome pools used by dimension-level river sections and contained river caves.")
|
||||
@Data
|
||||
public class IrisRiverBiomes {
|
||||
@Desc("Noise used to select a biome inside the active river-section pool.")
|
||||
private IrisGeneratorStyle selectionStyle = new IrisGeneratorStyle(NoiseStyle.CELLULAR_IRIS_DOUBLE)
|
||||
.zoomed(512D);
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Biome pool for wet river channels.")
|
||||
private KList<String> channel = new KList<>();
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Biome pool for river banks outside the wet channel.")
|
||||
private KList<String> bank = new KList<>();
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Biome pool for river reaches meeting natural sea.")
|
||||
private KList<String> mouth = new KList<>();
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Biome pool for dry river channels and terminal tapers.")
|
||||
private KList<String> dry = new KList<>();
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Cave biome pool for accepted contained river cave bodies.")
|
||||
private KList<String> floodedCave = new KList<>();
|
||||
|
||||
public KSet<String> getAllBiomeIds() {
|
||||
KSet<String> biomeIds = new KSet<>();
|
||||
addAll(biomeIds, channel);
|
||||
addAll(biomeIds, bank);
|
||||
addAll(biomeIds, mouth);
|
||||
addAll(biomeIds, dry);
|
||||
addAll(biomeIds, floodedCave);
|
||||
return biomeIds;
|
||||
}
|
||||
|
||||
private static void addAll(KSet<String> destination, KList<String> source) {
|
||||
if (source != null) {
|
||||
destination.addAll(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Selects the fallback when a requested river cave connection cannot be proven safe.")
|
||||
public enum IrisRiverCaveFallback {
|
||||
@Desc("Keep the rejected connection sealed.")
|
||||
SEALED,
|
||||
|
||||
@Desc("Try a bounded generated grotto instead of the rejected existing cave.")
|
||||
GENERATE_GROTTO
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Selects the contained cave-water behavior available to river entry events.")
|
||||
public enum IrisRiverCaveMode {
|
||||
@Desc("Keep the surface reservoir sealed from caves.")
|
||||
SEALED,
|
||||
|
||||
@Desc("Flood only an existing cave component whose complete fluid-reachable boundary is proven closed.")
|
||||
FLOOD_CLOSED_COMPONENT,
|
||||
|
||||
@Desc("Generate a bounded grotto with a guaranteed solid shell.")
|
||||
GENERATE_GROTTO,
|
||||
|
||||
@Desc("Use a proven closed cave component when available, otherwise generate a bounded grotto.")
|
||||
GROTTO_OR_CLOSED_COMPONENT,
|
||||
|
||||
@Desc("Generate a controlled falling column into a proven contained pool.")
|
||||
WATERFALL_POOL
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls bounded and transactionally validated river-to-cave connections.")
|
||||
@Data
|
||||
public class IrisRiverCaves {
|
||||
@Desc("The contained cave-water behavior available to river entry events.")
|
||||
private IrisRiverCaveMode mode = IrisRiverCaveMode.SEALED;
|
||||
|
||||
@Desc("Selects cave-entry stations at stable river-reach anchors.")
|
||||
private IrisRiverNoiseChance entry = new IrisRiverNoiseChance()
|
||||
.setChance(0.12D)
|
||||
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(1024D))
|
||||
.setInfluence(0.4D);
|
||||
|
||||
@MinNumber(16)
|
||||
@MaxNumber(4096)
|
||||
@Desc("The minimum distance in blocks between cave-entry candidates.")
|
||||
private int minimumSpacing = 128;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("The maximum entry-noise-eligible cave anchors accepted on one ordinary reach. A forced sinkhole terminal uses its reach exclusively and requires this value above zero.")
|
||||
private int maximumPerReach = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(256)
|
||||
@Desc("The maximum vertical distance searched while boring from river bed to cave.")
|
||||
private int maxBoreDepth = 48;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(16)
|
||||
@Desc("The radius in blocks of a generated river-to-cave throat.")
|
||||
private int throatRadius = 2;
|
||||
|
||||
@MinNumber(-64)
|
||||
@MaxNumber(64)
|
||||
@Desc("The offset applied to river water height when filling an accepted cave body.")
|
||||
private int waterLevelOffset = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("The minimum dry headroom retained above water in generated grottos.")
|
||||
private int dryHeadroom = 4;
|
||||
|
||||
@MinNumber(2)
|
||||
@MaxNumber(128)
|
||||
@Desc("The horizontal radius in blocks of a generated sealed grotto.")
|
||||
private int grottoHorizontalRadius = 12;
|
||||
|
||||
@MinNumber(2)
|
||||
@MaxNumber(128)
|
||||
@Desc("The vertical radius in blocks of a generated sealed grotto.")
|
||||
private int grottoVerticalRadius = 7;
|
||||
|
||||
@Desc("Noise shaping the boundary of generated sealed grottos.")
|
||||
private IrisGeneratorStyle grottoShapeStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(24D);
|
||||
|
||||
@Desc("Noise warping the coordinate field used for generated sealed grottos.")
|
||||
private IrisGeneratorStyle grottoWarpStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(48D);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(32)
|
||||
@Desc("The maximum coordinate warp applied to generated sealed grottos in blocks.")
|
||||
private double grottoWarpStrength = 2D;
|
||||
|
||||
@MinNumber(4)
|
||||
@MaxNumber(256)
|
||||
@Desc("The horizontal proof radius for an existing closed cave component.")
|
||||
private int maxFloodRadius = 48;
|
||||
|
||||
@MinNumber(4)
|
||||
@MaxNumber(256)
|
||||
@Desc("The vertical proof depth for an existing closed cave component.")
|
||||
private int maxFloodDepth = 32;
|
||||
|
||||
@MinNumber(64)
|
||||
@MaxNumber(1048576)
|
||||
@Desc("The greatest cave-component volume that may be fully proven and flooded.")
|
||||
private int maxFloodVolume = 8192;
|
||||
|
||||
@Desc("The behavior used when a requested cave connection cannot be proven safe.")
|
||||
private IrisRiverCaveFallback fallback = IrisRiverCaveFallback.SEALED;
|
||||
|
||||
@Desc("The policy for fluid already present in a candidate contained cave body.")
|
||||
private IrisRiverExistingFluidPolicy existingFluidPolicy = IrisRiverExistingFluidPolicy.REJECT;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Controls how river cave hydrology treats fluid already present in a candidate cave body.")
|
||||
public enum IrisRiverExistingFluidPolicy {
|
||||
@Desc("Reject a candidate containing any existing fluid.")
|
||||
REJECT,
|
||||
|
||||
@Desc("Accept only fluid compatible with the river fluid palette.")
|
||||
ALLOW_SAME,
|
||||
|
||||
@Desc("Replace contained existing fluid with the river fluid palette.")
|
||||
REPLACE
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Dimension-owned configuration for connected surface rivers and contained river cave water.")
|
||||
@Data
|
||||
public class IrisRiverNetwork {
|
||||
@Desc("Enable the river network for this dimension.")
|
||||
private boolean enabled = false;
|
||||
|
||||
@Desc("Dimension-owned connected graph and source-selection settings.")
|
||||
private IrisRiverTopology topology = new IrisRiverTopology();
|
||||
|
||||
@Desc("Channel geometry, terrain incision, meanders, and terminal behavior.")
|
||||
private IrisRiverTerrain terrain = new IrisRiverTerrain();
|
||||
|
||||
@Desc("River water-surface settings.")
|
||||
private IrisRiverWater water = new IrisRiverWater();
|
||||
|
||||
@Desc("Dimension-level biome pools for river sections and contained river caves.")
|
||||
private IrisRiverBiomes biomes = new IrisRiverBiomes();
|
||||
|
||||
@Desc("Bounded river-to-cave connection settings.")
|
||||
private IrisRiverCaves caves = new IrisRiverCaves();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("A deterministic graph-event chance modulated by configurable noise.")
|
||||
@Data
|
||||
public class IrisRiverNoiseChance {
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The base probability before noise modulation.")
|
||||
private double chance = 1D;
|
||||
|
||||
@Desc("The noise sampled once at the stable graph-event anchor.")
|
||||
private IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.FLAT);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The maximum centered noise contribution added to the base probability.")
|
||||
private double influence = 0D;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Nullable river settings overridden by a region or natural biome without changing graph identity.")
|
||||
@Data
|
||||
public class IrisRiverOverride {
|
||||
@Desc("Whether new river sources may begin in this area. Existing trunks are unaffected.")
|
||||
private Boolean allowSources = null;
|
||||
|
||||
@Desc("How downstream routing treats this area.")
|
||||
private IrisRiverRoutingPolicy routingPolicy = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("Multiplier applied to downstream routing cost.")
|
||||
private Double routingCostMultiplier = null;
|
||||
|
||||
@MinNumber(0.0001)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to wet channel width.")
|
||||
private Double widthMultiplier = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to river bank width.")
|
||||
private Double bankWidthMultiplier = null;
|
||||
|
||||
@MinNumber(0.0001)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to river-bed depth.")
|
||||
private Double depthMultiplier = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to maximum terrain incision.")
|
||||
private Double maxIncisionMultiplier = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to reach continuation probability.")
|
||||
private Double continuationChanceMultiplier = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to cave-entry probability.")
|
||||
private Double caveEntryMultiplier = null;
|
||||
|
||||
@Desc("Optional terminal behavior override for failed routes in this area.")
|
||||
private IrisRiverTerminalMode terminalMode = null;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Optional replacement biome pool for wet river channels. Empty explicitly disables this pool.")
|
||||
private KList<String> channelBiomes = null;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Optional replacement biome pool for river banks. Empty explicitly disables this pool.")
|
||||
private KList<String> bankBiomes = null;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Optional replacement biome pool for river mouths. Empty explicitly disables this pool.")
|
||||
private KList<String> mouthBiomes = null;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Optional replacement biome pool for dry river channels. Empty explicitly disables this pool.")
|
||||
private KList<String> dryBiomes = null;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Optional replacement cave biome pool for accepted contained river cave bodies. Empty explicitly disables this pool.")
|
||||
private KList<String> floodedCaveBiomes = null;
|
||||
|
||||
public KSet<String> getAllBiomeIds() {
|
||||
KSet<String> biomeIds = new KSet<>();
|
||||
addAll(biomeIds, channelBiomes);
|
||||
addAll(biomeIds, bankBiomes);
|
||||
addAll(biomeIds, mouthBiomes);
|
||||
addAll(biomeIds, dryBiomes);
|
||||
addAll(biomeIds, floodedCaveBiomes);
|
||||
return biomeIds;
|
||||
}
|
||||
|
||||
private static void addAll(KSet<String> destination, KList<String> source) {
|
||||
if (source != null) {
|
||||
destination.addAll(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Controls how river routing treats a region or biome.")
|
||||
public enum IrisRiverRoutingPolicy {
|
||||
@Desc("Allow normal river routing through this area.")
|
||||
ALLOW,
|
||||
|
||||
@Desc("Increase the routing cost while still permitting established river trunks.")
|
||||
AVOID,
|
||||
|
||||
@Desc("Forbid river reaches from crossing this area.")
|
||||
BLOCK
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Selects how a river route ends when it cannot continue to an outlet.")
|
||||
public enum IrisRiverTerminalMode {
|
||||
@Desc("Suppress the failed route instead of generating it.")
|
||||
SUPPRESS,
|
||||
|
||||
@Desc("Continue as a dry channel that tapers back into natural terrain.")
|
||||
DRY_CHANNEL,
|
||||
|
||||
@Desc("End in a contained underground grotto when cave hydrology accepts the connection.")
|
||||
SINKHOLE_GROTTO
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls river channel geometry, banks, incision, meanders, and terminal tapering.")
|
||||
@Data
|
||||
public class IrisRiverTerrain {
|
||||
@Desc("The wet channel width in blocks before stream-order scaling.")
|
||||
private IrisStyledRange channelWidth = range(8D, 20D, NoiseStyle.IRIS, 1024D);
|
||||
|
||||
@Desc("The bank width outside the wet channel in blocks.")
|
||||
private IrisStyledRange bankWidth = range(5D, 18D, NoiseStyle.IRIS, 1024D);
|
||||
|
||||
@Desc("The wet-bed depth below the local water surface, or dry-channel depth below natural terrain, in blocks.")
|
||||
private IrisStyledRange depth = range(2D, 7D, NoiseStyle.IRIS, 768D);
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(2048)
|
||||
@Desc("The final wet-channel width cap after region, biome, and stream-order scaling.")
|
||||
private double maxChannelWidth = 10D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(2048)
|
||||
@Desc("The final bank-width cap on each side after region and biome scaling.")
|
||||
private double maxBankWidth = 4D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(512)
|
||||
@Desc("The final river-depth cap after region, biome, and stream-order scaling.")
|
||||
private double maxDepth = 10D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("Additional channel-width fraction applied for each merged upstream flow order.")
|
||||
private double orderWidthFactor = 0.35D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("Additional river-bed depth fraction applied for each merged upstream flow order.")
|
||||
private double orderDepthFactor = 0.2D;
|
||||
|
||||
@Desc("Selects whether a complete graph reach may incise terrain. A rejected reach follows terminal behavior.")
|
||||
private IrisRiverNoiseChance incision = new IrisRiverNoiseChance();
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(512)
|
||||
@Desc("The greatest permitted vertical incision below natural terrain.")
|
||||
private int maxIncision = 48;
|
||||
|
||||
@MinNumber(0.125)
|
||||
@MaxNumber(16)
|
||||
@Desc("The exponent shaping the channel-to-bank cross-section transition.")
|
||||
private double bankExponent = 2D;
|
||||
|
||||
@Desc("Modulates perpendicular spline displacement while preserving graph endpoints.")
|
||||
private IrisGeneratorStyle meanderStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(512D);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1024)
|
||||
@Desc("The maximum perpendicular meander displacement in blocks.")
|
||||
private double meanderStrength = 72D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("The number of straight segments used to flatten each meandering graph reach.")
|
||||
private int meanderSubdivisions = 8;
|
||||
|
||||
@Desc("Modulates small river-bed height variation after the connected channel shape is solved.")
|
||||
private IrisGeneratorStyle bedRoughnessStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(96D);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("The maximum river-bed roughness in blocks.")
|
||||
private double bedRoughness = 0.75D;
|
||||
|
||||
@Desc("The behavior used when a graph route cannot continue as a wet channel.")
|
||||
private IrisRiverTerminalMode terminalMode = IrisRiverTerminalMode.DRY_CHANNEL;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(1024)
|
||||
@Desc("The distance in blocks over which a terminal channel returns to natural terrain.")
|
||||
private int terminalTaper = 64;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The probability that a failed wet route continues as a tapered dry channel.")
|
||||
private double dryContinuationChance = 1D;
|
||||
|
||||
private static IrisStyledRange range(double min, double max, NoiseStyle style, double zoom) {
|
||||
return new IrisStyledRange(min, max, new IrisGeneratorStyle(style).zoomed(zoom));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Dimension-owned settings for the deterministic connected river graph.")
|
||||
@Data
|
||||
public class IrisRiverTopology {
|
||||
@MinNumber(64)
|
||||
@MaxNumber(4096)
|
||||
@Desc("The routing-cell width in blocks. This controls graph identity and cannot be overridden by regions or biomes.")
|
||||
private int cellSize = 512;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("The number of routing cells grouped into one immutable river cache tile.")
|
||||
private int tileCells = 4;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.49)
|
||||
@Desc("The fraction of a routing cell used to jitter its graph node away from the center.")
|
||||
private double siteJitter = 0.35D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(256)
|
||||
@Desc("The maximum number of directed graph reaches followed by one source route.")
|
||||
private int maxRouteReaches = 16;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("The minimum number of noise-weighted source nodes selected in each routing tile while source chance is above zero.")
|
||||
private int minimumSourcesPerTile = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(7)
|
||||
@Desc("The number of alternate downstream reaches inspected before declaring a sink.")
|
||||
private int sinkSearchReaches = 4;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(256)
|
||||
@Desc("The spacing of deterministic drainage-basin sinks in routing cells. Larger values produce longer trunks and wider tributary trees.")
|
||||
private int routingBasinCells = 64;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("The horizontal basin-distance span in routing cells per one block of terraced water rise.")
|
||||
private double routingPlateauHeight = 8D;
|
||||
|
||||
@Desc("Selects complete river source routes at stable graph nodes.")
|
||||
private IrisRiverNoiseChance source = new IrisRiverNoiseChance()
|
||||
.setChance(0.05D)
|
||||
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(8192D))
|
||||
.setInfluence(0.035D);
|
||||
|
||||
@Desc("Selects complete continuation reaches. A rejected reach terminates or suppresses its route rather than creating a gap.")
|
||||
private IrisRiverNoiseChance continuation = new IrisRiverNoiseChance()
|
||||
.setChance(0.99D)
|
||||
.setStyle(new IrisGeneratorStyle(NoiseStyle.VASCULAR).zoomed(4096D))
|
||||
.setInfluence(0.01D);
|
||||
|
||||
@Desc("Adds deterministic cost variation while choosing downstream graph neighbors.")
|
||||
private IrisGeneratorStyle routingStyle = new IrisGeneratorStyle(NoiseStyle.VASCULAR).zoomed(8192D);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1024)
|
||||
@Desc("The maximum routing-cost contribution from routingStyle.")
|
||||
private double routingNoiseWeight = 24D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("The contribution of natural terrain height to downstream routing cost.")
|
||||
private double terrainHeightWeight = 0.7D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("The contribution of natural terrain slope to downstream routing cost.")
|
||||
private double terrainSlopeWeight = 0.35D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("The routing preference toward natural sea outlets.")
|
||||
private double oceanAttraction = 1D;
|
||||
|
||||
@Desc("Require every wet source route to reach natural sea or a proven sea-reaching trunk.")
|
||||
private boolean requireOcean = false;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls the river water-surface solver.")
|
||||
@Data
|
||||
public class IrisRiverWater {
|
||||
@Desc("The strategy used to determine river water-surface height.")
|
||||
private IrisRiverWaterMode mode = IrisRiverWaterMode.SEA_LEVEL;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(4096)
|
||||
@Desc("The target length of each flat terraced pool in blocks.")
|
||||
private int poolLength = 96;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("The greatest river water height permitted above the dimension fluid height.")
|
||||
private int maximumPoolRise = 4;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(32)
|
||||
@Desc("The vertical height of controlled drops between terraced pools.")
|
||||
private int dropHeight = 1;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Selects how a river determines its surface fluid height.")
|
||||
public enum IrisRiverWaterMode {
|
||||
@Desc("Use the dimension fluid height for every wet river reach.")
|
||||
SEA_LEVEL,
|
||||
|
||||
@Desc("Use flat pools connected by controlled vertical drops.")
|
||||
TERRACED
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import art.arcane.iris.core.IrisWorlds;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.runtime.ObjectStudioActivation;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
@@ -61,6 +62,7 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
@@ -83,12 +85,17 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -102,6 +109,11 @@ import java.util.function.Supplier;
|
||||
@Data
|
||||
public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChunkGenerator, Listener {
|
||||
private static final int LOAD_LOCKS = Runtime.getRuntime().availableProcessors() * 4;
|
||||
private static final int STUDIO_ENTRY_PRECOMPUTE_RADIUS = 2;
|
||||
private static final int STUDIO_ENTRY_PRECOMPUTE_THREADS = Math.max(
|
||||
1,
|
||||
Math.min(6, Runtime.getRuntime().availableProcessors() / 2));
|
||||
private static final AtomicInteger STUDIO_ENTRY_THREAD_SEQUENCE = new AtomicInteger();
|
||||
private static final long HOTLOAD_LOOP_DELAY_MS = 250L;
|
||||
private static final long HOTLOAD_MAINTENANCE_DELAY_MS = 4000L;
|
||||
private final GenerationStageGate loadLock;
|
||||
@@ -115,6 +127,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
private final AtomicBoolean setup;
|
||||
private final boolean studio;
|
||||
private final AtomicBoolean studioEntryBootstrapActive;
|
||||
private final ConcurrentHashMap<Long, PreparedStudioChunk> preparedStudioEntryChunks;
|
||||
private final AtomicInteger a = new AtomicInteger(0);
|
||||
private volatile long lastChunkGenTime = 0L;
|
||||
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
|
||||
@@ -143,6 +156,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
this.hotloadChecker = new ChronoLatch(1000, false);
|
||||
this.studio = studio;
|
||||
this.studioEntryBootstrapActive = new AtomicBoolean(studio);
|
||||
this.preparedStudioEntryChunks = new ConcurrentHashMap<>();
|
||||
this.dataLocation = dataLocation;
|
||||
this.dimensionKey = dimensionKey;
|
||||
this.folder = new ReactiveFolder(
|
||||
@@ -475,6 +489,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
if (currentEngine != null && !currentEngine.isClosed()) {
|
||||
currentEngine.close();
|
||||
}
|
||||
preparedStudioEntryChunks.clear();
|
||||
folder.clear();
|
||||
populators.clear();
|
||||
});
|
||||
@@ -525,6 +540,124 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
studioEntryBootstrapActive.set(false);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> prepareStudioEntryChunks(
|
||||
World bukkitWorld,
|
||||
int centerChunkX,
|
||||
int centerChunkZ
|
||||
) {
|
||||
if (!studio || closing) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
Engine activeEngine = getEngine(bukkitWorld);
|
||||
computeStudioGenerator();
|
||||
if (studioGenerator != null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
long generationSessionId = activeEngine.getGenerationSessionId();
|
||||
ConcurrentHashMap<Long, PreparedStudioChunk> prepared = new ConcurrentHashMap<>();
|
||||
ExecutorService executor = createStudioEntryExecutor();
|
||||
int diameter = STUDIO_ENTRY_PRECOMPUTE_RADIUS * 2 + 1;
|
||||
ArrayList<CompletableFuture<Void>> tasks = new ArrayList<>(diameter * diameter);
|
||||
for (int offsetX = -STUDIO_ENTRY_PRECOMPUTE_RADIUS;
|
||||
offsetX <= STUDIO_ENTRY_PRECOMPUTE_RADIUS;
|
||||
offsetX++) {
|
||||
for (int offsetZ = -STUDIO_ENTRY_PRECOMPUTE_RADIUS;
|
||||
offsetZ <= STUDIO_ENTRY_PRECOMPUTE_RADIUS;
|
||||
offsetZ++) {
|
||||
int chunkX = centerChunkX + offsetX;
|
||||
int chunkZ = centerChunkZ + offsetZ;
|
||||
CompletableFuture<Void> task = CompletableFuture.runAsync(
|
||||
() -> prepareStudioEntryChunk(
|
||||
bukkitWorld,
|
||||
activeEngine,
|
||||
generationSessionId,
|
||||
chunkX,
|
||||
chunkZ,
|
||||
prepared),
|
||||
executor);
|
||||
tasks.add(task);
|
||||
}
|
||||
}
|
||||
|
||||
CompletableFuture<Void> completion = CompletableFuture.allOf(
|
||||
tasks.toArray(new CompletableFuture<?>[0]));
|
||||
CompletableFuture<Void> publication = completion.thenRun(() -> {
|
||||
if (closing
|
||||
|| engine != activeEngine
|
||||
|| activeEngine.getGenerationSessionId() != generationSessionId) {
|
||||
throw new IllegalStateException(
|
||||
"Studio entry precompute finished for a replaced engine runtime.");
|
||||
}
|
||||
preparedStudioEntryChunks.clear();
|
||||
preparedStudioEntryChunks.putAll(prepared);
|
||||
});
|
||||
return publication.whenComplete((ignored, failure) -> executor.shutdownNow());
|
||||
}
|
||||
|
||||
private ExecutorService createStudioEntryExecutor() {
|
||||
return Executors.newFixedThreadPool(STUDIO_ENTRY_PRECOMPUTE_THREADS, runnable -> {
|
||||
Thread thread = new Thread(
|
||||
runnable,
|
||||
"Iris Studio Entry-" + STUDIO_ENTRY_THREAD_SEQUENCE.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.NORM_PRIORITY);
|
||||
return thread;
|
||||
});
|
||||
}
|
||||
|
||||
private void prepareStudioEntryChunk(
|
||||
World bukkitWorld,
|
||||
Engine activeEngine,
|
||||
long generationSessionId,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
ConcurrentHashMap<Long, PreparedStudioChunk> prepared
|
||||
) {
|
||||
try (GenerationStagePermit ignored = acquireGenerationStage(
|
||||
"studio_entry_chunk_precompute")) {
|
||||
TerrainChunk terrainChunk = TerrainChunk.create(bukkitWorld);
|
||||
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(terrainChunk.getChunkData());
|
||||
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(terrainChunk);
|
||||
ChunkContext context = createStudioEntryContext(
|
||||
activeEngine,
|
||||
generationSessionId,
|
||||
chunkX,
|
||||
chunkZ);
|
||||
activeEngine.generateMatter(chunkX, chunkZ, true, context);
|
||||
try {
|
||||
activeEngine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
|
||||
} catch (WrongEngineBroException exception) {
|
||||
throw new CompletionException(exception);
|
||||
}
|
||||
prepared.put(
|
||||
chunkKey(chunkX, chunkZ),
|
||||
new PreparedStudioChunk(activeEngine, generationSessionId, blocks)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private ChunkContext createStudioEntryContext(
|
||||
Engine activeEngine,
|
||||
long generationSessionId,
|
||||
int chunkX,
|
||||
int chunkZ
|
||||
) {
|
||||
boolean cacheContext = !activeEngine.getPlatformHooks()
|
||||
.shouldDisableChunkContextCache(activeEngine);
|
||||
ChunkContext.PrefillPlan prefillPlan = cacheContext
|
||||
? ChunkContext.PrefillPlan.NO_CAVE
|
||||
: ChunkContext.PrefillPlan.NONE;
|
||||
return new ChunkContext(
|
||||
chunkX << 4,
|
||||
chunkZ << 4,
|
||||
activeEngine.getComplex(),
|
||||
generationSessionId,
|
||||
cacheContext,
|
||||
prefillPlan,
|
||||
activeEngine.getMetrics());
|
||||
}
|
||||
|
||||
public boolean isStudioEntryBootstrapActive() {
|
||||
return studioEntryBootstrapActive.get();
|
||||
}
|
||||
@@ -783,12 +916,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
if (studioGenerator != null) {
|
||||
studioGenerator.generateChunk(engine, tc, x, z);
|
||||
} else {
|
||||
PreparedStudioChunk prepared = preparedStudioEntryChunks.remove(chunkKey(x, z));
|
||||
if (prepared != null
|
||||
&& prepared.engine() == engine
|
||||
&& prepared.generationSessionId() == engine.getGenerationSessionId()) {
|
||||
prepared.blocks().applyTo(d);
|
||||
IrisLogging.debug("Applied prepared Studio entry chunk " + x + " " + z);
|
||||
return;
|
||||
}
|
||||
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
|
||||
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(tc);
|
||||
boolean useMulticore = studio && !J.isFolia();
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
engine.generate(x << 4, z << 4, blocks, biomes, useMulticore);
|
||||
engine.generate(x << 4, z << 4, blocks, biomes, false);
|
||||
blocks.apply();
|
||||
}
|
||||
}
|
||||
@@ -829,6 +969,17 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
return isMaintenanceActive();
|
||||
}
|
||||
|
||||
private static long chunkKey(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) ^ (chunkZ & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private record PreparedStudioChunk(
|
||||
Engine engine,
|
||||
long generationSessionId,
|
||||
ChunkDataHunkHolder blocks
|
||||
) {
|
||||
}
|
||||
|
||||
private boolean isMaintenanceActive() {
|
||||
World realWorld = BukkitWorldBinding.world(this.world);
|
||||
return realWorld != null && IrisToolbelt.isWorldMaintenanceActive(realWorld);
|
||||
@@ -910,7 +1061,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
StudioMode desired = studio
|
||||
? java.util.Optional.ofNullable(getEngine().getDimension().getStudioMode()).orElse(StudioMode.NORMAL)
|
||||
: StudioMode.NORMAL;
|
||||
if (studio && art.arcane.iris.core.runtime.ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
|
||||
if (studio && ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
|
||||
desired = StudioMode.OBJECT_BUFFET;
|
||||
}
|
||||
if (!desired.equals(lastMode)) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record RiverAnchor(
|
||||
RiverEdgeId reachId,
|
||||
int index,
|
||||
long stableId,
|
||||
double samplingSpacing,
|
||||
long samplingSalt,
|
||||
double x,
|
||||
double z,
|
||||
double alongReach,
|
||||
RiverRouteState state,
|
||||
int flow,
|
||||
int order
|
||||
) {
|
||||
public RiverAnchor {
|
||||
Objects.requireNonNull(reachId);
|
||||
Objects.requireNonNull(state);
|
||||
if (index < 0 || !Double.isFinite(samplingSpacing) || samplingSpacing <= 0D
|
||||
|| !Double.isFinite(x) || !Double.isFinite(z)
|
||||
|| !Double.isFinite(alongReach) || alongReach < 0.0 || alongReach > 1.0) {
|
||||
throw new IllegalArgumentException("River anchor index and coordinates must be valid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record RiverEdgeId(RiverNodeId first, RiverNodeId second) implements Comparable<RiverEdgeId> {
|
||||
public RiverEdgeId {
|
||||
Objects.requireNonNull(first);
|
||||
Objects.requireNonNull(second);
|
||||
if (first.compareTo(second) >= 0) {
|
||||
throw new IllegalArgumentException("River edge endpoints must be distinct and canonical");
|
||||
}
|
||||
}
|
||||
|
||||
public static RiverEdgeId of(RiverNodeId first, RiverNodeId second) {
|
||||
Objects.requireNonNull(first);
|
||||
Objects.requireNonNull(second);
|
||||
if (first.equals(second)) {
|
||||
throw new IllegalArgumentException("River edge endpoints must be distinct");
|
||||
}
|
||||
return first.compareTo(second) < 0
|
||||
? new RiverEdgeId(first, second)
|
||||
: new RiverEdgeId(second, first);
|
||||
}
|
||||
|
||||
public long stableId() {
|
||||
return RiverNetwork.mix(first.stableId() ^ Long.rotateLeft(second.stableId(), 29));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(RiverEdgeId other) {
|
||||
int firstComparison = first.compareTo(other.first);
|
||||
return firstComparison != 0 ? firstComparison : second.compareTo(other.second);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record RiverMeanderContext(
|
||||
RiverEdgeId reachId,
|
||||
double normalizedPosition,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
public RiverMeanderContext {
|
||||
Objects.requireNonNull(reachId);
|
||||
if (!Double.isFinite(normalizedPosition) || normalizedPosition < 0.0 || normalizedPosition > 1.0
|
||||
|| !Double.isFinite(x) || !Double.isFinite(z)) {
|
||||
throw new IllegalArgumentException("River meander context must be finite and normalized");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,918 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class RiverNetwork {
|
||||
private static final long NODE_X_SALT = 0x6A09E667F3BCC909L;
|
||||
private static final long NODE_Z_SALT = 0xBB67AE8584CAA73BL;
|
||||
private static final long NODE_RANK_SALT = 0x3C6EF372FE94F82BL;
|
||||
private static final long BASIN_X_SALT = 0xCBBB9D5DC1059ED8L;
|
||||
private static final long BASIN_Z_SALT = 0x629A292A367CD507L;
|
||||
private static final long DIAGONAL_SALT = 0xA54FF53A5F1D36F1L;
|
||||
private static final long SOURCE_SALT = 0x510E527FADE682D1L;
|
||||
private static final long SOURCE_FLOOR_SALT = 0xD6E8FEB86659FD93L;
|
||||
private static final long REACH_SALT = 0x9B05688C2B3E6C1FL;
|
||||
private static final long DRY_SALT = 0x1F83D9ABFB41BD6BL;
|
||||
private static final long MEANDER_SALT = 0x5BE0CD19137E2179L;
|
||||
|
||||
private final RiverNetworkOptions options;
|
||||
|
||||
public RiverNetwork(RiverNetworkOptions options) {
|
||||
this.options = Objects.requireNonNull(options);
|
||||
}
|
||||
|
||||
public RiverNetworkOptions options() {
|
||||
return options;
|
||||
}
|
||||
|
||||
public RiverNode nodeAtCell(long cellX, long cellZ, RiverTerrainSampler terrain) {
|
||||
return createNode(new RiverNodeId(cellX, cellZ), Objects.requireNonNull(terrain));
|
||||
}
|
||||
|
||||
public RiverNode nodeAtWorld(int blockX, int blockZ, RiverTerrainSampler terrain) {
|
||||
long cellX = Math.floorDiv(blockX, options.cellSize());
|
||||
long cellZ = Math.floorDiv(blockZ, options.cellSize());
|
||||
return nodeAtCell(cellX, cellZ, terrain);
|
||||
}
|
||||
|
||||
public int tileXForBlock(int blockX) {
|
||||
return Math.floorDiv(blockX, options.cellSize() * options.tileCells());
|
||||
}
|
||||
|
||||
public int tileZForBlock(int blockZ) {
|
||||
return Math.floorDiv(blockZ, options.cellSize() * options.tileCells());
|
||||
}
|
||||
|
||||
public RiverTile buildTileForBlock(int blockX, int blockZ, RiverTerrainSampler terrain) {
|
||||
return buildTile(tileXForBlock(blockX), tileZForBlock(blockZ), terrain);
|
||||
}
|
||||
|
||||
public RiverSample sample(int blockX, int blockZ, RiverTerrainSampler terrain) {
|
||||
return buildTileForBlock(blockX, blockZ, terrain).sample(blockX, blockZ);
|
||||
}
|
||||
|
||||
public List<RiverNodeId> neighbors(RiverNodeId id) {
|
||||
Objects.requireNonNull(id);
|
||||
ArrayList<RiverNodeId> neighbors = new ArrayList<>(8);
|
||||
addUnique(neighbors, new RiverNodeId(id.cellX() - 1L, id.cellZ()));
|
||||
addUnique(neighbors, new RiverNodeId(id.cellX() + 1L, id.cellZ()));
|
||||
addUnique(neighbors, new RiverNodeId(id.cellX(), id.cellZ() - 1L));
|
||||
addUnique(neighbors, new RiverNodeId(id.cellX(), id.cellZ() + 1L));
|
||||
for (long squareX = id.cellX() - 1L; squareX <= id.cellX(); squareX++) {
|
||||
for (long squareZ = id.cellZ() - 1L; squareZ <= id.cellZ(); squareZ++) {
|
||||
RiverNodeId diagonal = diagonalNeighbor(id, squareX, squareZ);
|
||||
if (diagonal != null) {
|
||||
addUnique(neighbors, diagonal);
|
||||
}
|
||||
}
|
||||
}
|
||||
neighbors.sort(Comparator.naturalOrder());
|
||||
return List.copyOf(neighbors);
|
||||
}
|
||||
|
||||
public RiverNode downstream(RiverNodeId id, RiverTerrainSampler terrain) {
|
||||
Objects.requireNonNull(id);
|
||||
Objects.requireNonNull(terrain);
|
||||
NodeResolver resolver = new NodeResolver(terrain);
|
||||
RiverNode node = resolver.resolve(id);
|
||||
List<RiverNode> candidates = resolver.downstreamCandidates(node);
|
||||
for (RiverNode candidate : candidates) {
|
||||
RiverRoutingContext context = resolver.routingContext(node, candidate);
|
||||
if (!resolver.reachFeasible(context)) {
|
||||
continue;
|
||||
}
|
||||
return resolver.continuationPermitted(context) ? candidate : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<RiverNode> downstreamCandidates(RiverNodeId id, RiverTerrainSampler terrain) {
|
||||
Objects.requireNonNull(id);
|
||||
Objects.requireNonNull(terrain);
|
||||
NodeResolver resolver = new NodeResolver(terrain);
|
||||
return resolver.downstreamCandidates(resolver.resolve(id));
|
||||
}
|
||||
|
||||
public RiverRoute trace(RiverNodeId source, RiverTerrainSampler terrain) {
|
||||
Objects.requireNonNull(source);
|
||||
Objects.requireNonNull(terrain);
|
||||
return trace(source, new NodeResolver(terrain));
|
||||
}
|
||||
|
||||
public RiverTile buildTile(int tileX, int tileZ, RiverTerrainSampler terrain) {
|
||||
Objects.requireNonNull(terrain);
|
||||
long tileWorldSize = (long) options.cellSize() * options.tileCells();
|
||||
long minimumX = (long) tileX * tileWorldSize;
|
||||
long minimumZ = (long) tileZ * tileWorldSize;
|
||||
long maximumX = minimumX + tileWorldSize;
|
||||
long maximumZ = minimumZ + tileWorldSize;
|
||||
requireWorldBounds(minimumX, minimumZ, maximumX, maximumZ);
|
||||
|
||||
int geometryPadding = geometryPaddingCells();
|
||||
long targetMinimumCellX = (long) tileX * options.tileCells() - geometryPadding;
|
||||
long targetMinimumCellZ = (long) tileZ * options.tileCells() - geometryPadding;
|
||||
long targetMaximumCellX = (long) (tileX + 1) * options.tileCells() - 1L + geometryPadding;
|
||||
long targetMaximumCellZ = (long) (tileZ + 1) * options.tileCells() - 1L + geometryPadding;
|
||||
long sourceMinimumCellX = targetMinimumCellX - options.maxRouteReaches();
|
||||
long sourceMinimumCellZ = targetMinimumCellZ - options.maxRouteReaches();
|
||||
long sourceMaximumCellX = targetMaximumCellX + options.maxRouteReaches();
|
||||
long sourceMaximumCellZ = targetMaximumCellZ + options.maxRouteReaches();
|
||||
|
||||
NodeResolver resolver = new NodeResolver(terrain);
|
||||
int sourceWidth = Math.toIntExact(sourceMaximumCellX - sourceMinimumCellX + 1L);
|
||||
int sourceDepth = Math.toIntExact(sourceMaximumCellZ - sourceMinimumCellZ + 1L);
|
||||
int sourceCount = Math.multiplyExact(sourceWidth, sourceDepth);
|
||||
ArrayList<RiverRoute> routes = new ArrayList<>(sourceCount);
|
||||
for (long cellX = sourceMinimumCellX; cellX <= sourceMaximumCellX; cellX++) {
|
||||
for (long cellZ = sourceMinimumCellZ; cellZ <= sourceMaximumCellZ; cellZ++) {
|
||||
routes.add(trace(new RiverNodeId(cellX, cellZ), resolver));
|
||||
}
|
||||
}
|
||||
LinkedHashMap<RiverEdgeId, ReachAccumulator> accumulators = new LinkedHashMap<>();
|
||||
for (RiverRoute route : routes) {
|
||||
accumulate(route, resolver, accumulators);
|
||||
}
|
||||
|
||||
ArrayList<RiverReach> reaches = new ArrayList<>(accumulators.size());
|
||||
for (ReachAccumulator accumulator : accumulators.values()) {
|
||||
if (!potentiallyIntersects(
|
||||
accumulator.from,
|
||||
accumulator.to,
|
||||
minimumX,
|
||||
minimumZ,
|
||||
maximumX,
|
||||
maximumZ
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
RiverReach reach = accumulator.build();
|
||||
if (intersects(reach, minimumX, minimumZ, maximumX, maximumZ)) {
|
||||
reaches.add(reach);
|
||||
}
|
||||
}
|
||||
reaches.sort(Comparator.comparing(RiverReach::id));
|
||||
return new RiverTile(
|
||||
tileX,
|
||||
tileZ,
|
||||
(int) minimumX,
|
||||
(int) minimumZ,
|
||||
(int) maximumX,
|
||||
(int) maximumZ,
|
||||
reaches
|
||||
);
|
||||
}
|
||||
|
||||
public static long mix(long value) {
|
||||
long mixed = value;
|
||||
mixed ^= mixed >>> 30;
|
||||
mixed *= 0xBF58476D1CE4E5B9L;
|
||||
mixed ^= mixed >>> 27;
|
||||
mixed *= 0x94D049BB133111EBL;
|
||||
mixed ^= mixed >>> 31;
|
||||
return mixed;
|
||||
}
|
||||
|
||||
private RiverNode createNode(RiverNodeId id, RiverTerrainSampler terrain) {
|
||||
NodePosition position = nodePosition(id);
|
||||
double x = position.x();
|
||||
double z = position.z();
|
||||
int blockX = position.blockX();
|
||||
int blockZ = position.blockZ();
|
||||
RiverTerrainNodeSample terrainSample = terrain.sampleNode(blockX, blockZ);
|
||||
double naturalHeight = finiteOrZero(terrainSample.naturalHeight());
|
||||
boolean ocean = terrainSample.ocean();
|
||||
boolean riverAllowed = terrainSample.riverAllowed();
|
||||
double routingNoise = centered(hash(id, NODE_RANK_SALT));
|
||||
double routingScore = naturalHeight * options.terrainHeightWeight()
|
||||
+ routingNoise * options.routingNoiseWeight()
|
||||
+ finiteOrZero(terrainSample.routingCost());
|
||||
double drainageDistance = drainageDistance(id);
|
||||
double hydraulicHeight = ocean
|
||||
? options.hydraulicBaseHeight()
|
||||
: options.hydraulicBaseHeight()
|
||||
+ StrictMath.floor(drainageDistance / options.routingPlateauHeight());
|
||||
double rank = ocean ? -Double.MAX_VALUE : drainageDistance;
|
||||
return new RiverNode(
|
||||
id,
|
||||
x,
|
||||
z,
|
||||
naturalHeight,
|
||||
hydraulicHeight,
|
||||
rank,
|
||||
finiteOrZero(routingScore),
|
||||
ocean,
|
||||
riverAllowed
|
||||
);
|
||||
}
|
||||
|
||||
private double drainageDistance(RiverNodeId id) {
|
||||
int basinCells = options.routingBasinCells();
|
||||
long basinX = Math.floorDiv(id.cellX(), basinCells);
|
||||
long basinZ = Math.floorDiv(id.cellZ(), basinCells);
|
||||
double nodeX = id.cellX() + 0.5D;
|
||||
double nodeZ = id.cellZ() + 0.5D;
|
||||
double jitterRadius = basinCells * 0.45D;
|
||||
double nearestDistance = Double.MAX_VALUE;
|
||||
for (long candidateX = basinX - 1L; candidateX <= basinX + 1L; candidateX++) {
|
||||
for (long candidateZ = basinZ - 1L; candidateZ <= basinZ + 1L; candidateZ++) {
|
||||
double siteX = (candidateX + 0.5D) * basinCells
|
||||
+ centered(hash(candidateX, candidateZ, BASIN_X_SALT)) * jitterRadius;
|
||||
double siteZ = (candidateZ + 0.5D) * basinCells
|
||||
+ centered(hash(candidateX, candidateZ, BASIN_Z_SALT)) * jitterRadius;
|
||||
nearestDistance = StrictMath.min(
|
||||
nearestDistance,
|
||||
StrictMath.hypot(nodeX - siteX, nodeZ - siteZ)
|
||||
);
|
||||
}
|
||||
}
|
||||
return nearestDistance;
|
||||
}
|
||||
|
||||
private NodePosition nodePosition(RiverNodeId id) {
|
||||
double centerX = ((double) id.cellX() + 0.5D) * options.cellSize();
|
||||
double centerZ = ((double) id.cellZ() + 0.5D) * options.cellSize();
|
||||
double jitterRadius = options.siteJitter() * options.cellSize() * 0.5D;
|
||||
double x = centerX + centered(hash(id, NODE_X_SALT)) * jitterRadius;
|
||||
double z = centerZ + centered(hash(id, NODE_Z_SALT)) * jitterRadius;
|
||||
return new NodePosition(
|
||||
x,
|
||||
z,
|
||||
clampToInt(StrictMath.round(x)),
|
||||
clampToInt(StrictMath.round(z))
|
||||
);
|
||||
}
|
||||
|
||||
private List<RiverNode> computeDownstreamCandidates(RiverNode node, NodeResolver resolver) {
|
||||
if (node.ocean() || !node.riverAllowed()) {
|
||||
return List.of();
|
||||
}
|
||||
ArrayList<RankedCandidate> ranked = new ArrayList<>(8);
|
||||
for (RiverNodeId neighborId : neighbors(node.id())) {
|
||||
RiverNode neighbor = resolver.resolve(neighborId);
|
||||
if (!neighbor.riverAllowed()) {
|
||||
continue;
|
||||
}
|
||||
if (compareRank(neighbor, node) >= 0) {
|
||||
continue;
|
||||
}
|
||||
RiverRoutingContext context = resolver.routingContext(node, neighbor);
|
||||
double routingCost = finiteNonNegative(resolver.terrain.reachRoutingCost(context));
|
||||
double oceanAttraction = neighbor.ocean() ? options.oceanAttraction() : 0.0;
|
||||
double flowAlignmentCost = flowAlignmentCost(node, neighbor, resolver);
|
||||
ranked.add(new RankedCandidate(
|
||||
neighbor,
|
||||
neighbor.routingScore() + routingCost + flowAlignmentCost - oceanAttraction
|
||||
));
|
||||
}
|
||||
ranked.sort((first, second) -> {
|
||||
int costComparison = Double.compare(first.cost(), second.cost());
|
||||
return costComparison != 0 ? costComparison : compareRank(first.node(), second.node());
|
||||
});
|
||||
ArrayList<RiverNode> candidates = new ArrayList<>(ranked.size());
|
||||
for (RankedCandidate candidate : ranked) {
|
||||
candidates.add(candidate.node());
|
||||
}
|
||||
return List.copyOf(candidates);
|
||||
}
|
||||
|
||||
private RiverRoute trace(RiverNodeId sourceId, NodeResolver resolver) {
|
||||
if (!resolver.sourcePermitted(sourceId)) {
|
||||
return new RiverRoute(sourceId, RiverRouteState.SUPPRESSED, List.of(), false, false);
|
||||
}
|
||||
RiverNode source = resolver.resolve(sourceId);
|
||||
|
||||
ArrayList<RiverEdgeId> edges = new ArrayList<>(options.maxRouteReaches());
|
||||
RiverNode current = source;
|
||||
boolean reachedOcean = false;
|
||||
for (int reachIndex = 0; reachIndex < options.maxRouteReaches(); reachIndex++) {
|
||||
RiverNode next = null;
|
||||
int examined = 0;
|
||||
for (RiverNode candidate : resolver.downstreamCandidates(current)) {
|
||||
if (examined >= options.downstreamCandidateLimit()) {
|
||||
break;
|
||||
}
|
||||
examined++;
|
||||
RiverRoutingContext context = resolver.routingContext(current, candidate);
|
||||
if (resolver.reachFeasible(context)) {
|
||||
next = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (next == null) {
|
||||
break;
|
||||
}
|
||||
RiverEdgeId edgeId = RiverEdgeId.of(current.id(), next.id());
|
||||
if (!resolver.continuationPermitted(resolver.routingContext(current, next))) {
|
||||
break;
|
||||
}
|
||||
edges.add(edgeId);
|
||||
current = next;
|
||||
if (current.ocean()) {
|
||||
reachedOcean = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (reachedOcean) {
|
||||
return new RiverRoute(sourceId, RiverRouteState.WET, edges, true, false);
|
||||
}
|
||||
if (!edges.isEmpty()) {
|
||||
RiverTerminalPolicy terminalPolicy = resolver.terminalPolicy(current);
|
||||
if (terminalPolicy == RiverTerminalPolicy.WET
|
||||
|| (terminalPolicy == RiverTerminalPolicy.INHERIT && !options.requireOcean())) {
|
||||
return new RiverRoute(sourceId, RiverRouteState.WET, edges, false, true);
|
||||
}
|
||||
if ((terminalPolicy == RiverTerminalPolicy.DRY
|
||||
|| terminalPolicy == RiverTerminalPolicy.INHERIT)
|
||||
&& resolver.dryPermitted(sourceId)) {
|
||||
return new RiverRoute(sourceId, RiverRouteState.DRY, edges, false, true);
|
||||
}
|
||||
}
|
||||
return new RiverRoute(sourceId, RiverRouteState.SUPPRESSED, List.of(), false, false);
|
||||
}
|
||||
|
||||
private void accumulate(
|
||||
RiverRoute route,
|
||||
NodeResolver resolver,
|
||||
Map<RiverEdgeId, ReachAccumulator> accumulators
|
||||
) {
|
||||
if (route.state() == RiverRouteState.SUPPRESSED) {
|
||||
return;
|
||||
}
|
||||
for (int edgeIndex = 0; edgeIndex < route.edges().size(); edgeIndex++) {
|
||||
RiverEdgeId edgeId = route.edges().get(edgeIndex);
|
||||
ReachAccumulator accumulator = accumulators.get(edgeId);
|
||||
if (accumulator == null) {
|
||||
RiverNode first = resolver.resolve(edgeId.first());
|
||||
RiverNode second = resolver.resolve(edgeId.second());
|
||||
RiverNode from = compareRank(first, second) > 0 ? first : second;
|
||||
RiverNode to = from == first ? second : first;
|
||||
accumulator = new ReachAccumulator(
|
||||
edgeId,
|
||||
from,
|
||||
to,
|
||||
resolver.routingContext(from, to),
|
||||
resolver.terrain
|
||||
);
|
||||
accumulators.put(edgeId, accumulator);
|
||||
}
|
||||
boolean terminal = route.terminal() && edgeIndex == route.edges().size() - 1;
|
||||
accumulator.add(route.state(), terminal);
|
||||
}
|
||||
}
|
||||
|
||||
private RiverNodeId diagonalNeighbor(RiverNodeId id, long squareX, long squareZ) {
|
||||
boolean ascending = (hash(squareX, squareZ, DIAGONAL_SALT) & 1L) == 0L;
|
||||
RiverNodeId first = ascending
|
||||
? new RiverNodeId(squareX, squareZ)
|
||||
: new RiverNodeId(squareX, squareZ + 1L);
|
||||
RiverNodeId second = ascending
|
||||
? new RiverNodeId(squareX + 1L, squareZ + 1L)
|
||||
: new RiverNodeId(squareX + 1L, squareZ);
|
||||
if (id.equals(first)) {
|
||||
return second;
|
||||
}
|
||||
return id.equals(second) ? first : null;
|
||||
}
|
||||
|
||||
private RiverPolyline createPolyline(
|
||||
RiverEdgeId id,
|
||||
RiverNode from,
|
||||
RiverNode to,
|
||||
NodeResolver resolver
|
||||
) {
|
||||
int pointCount = options.meanderSubdivisions() + 1;
|
||||
double[] x = new double[pointCount];
|
||||
double[] z = new double[pointCount];
|
||||
double deltaX = to.x() - from.x();
|
||||
double deltaZ = to.z() - from.z();
|
||||
double length = StrictMath.hypot(deltaX, deltaZ);
|
||||
double normalX = length == 0.0 ? 0.0 : -deltaZ / length;
|
||||
double normalZ = length == 0.0 ? 0.0 : deltaX / length;
|
||||
double maximumOffset = StrictMath.min(options.meanderStrength(), length * 0.35);
|
||||
double directionX = length == 0D ? 1D : deltaX / length;
|
||||
double directionZ = length == 0D ? 0D : deltaZ / length;
|
||||
FlowTangent fromTangent = flowTangent(from, directionX, directionZ, resolver);
|
||||
FlowTangent toTangent = flowTangent(to, directionX, directionZ, resolver);
|
||||
for (int point = 0; point < pointCount; point++) {
|
||||
double t = (double) point / (pointCount - 1);
|
||||
double tSquared = t * t;
|
||||
double tCubed = tSquared * t;
|
||||
double fromWeight = 2D * tCubed - 3D * tSquared + 1D;
|
||||
double fromTangentWeight = tCubed - 2D * tSquared + t;
|
||||
double toWeight = -2D * tCubed + 3D * tSquared;
|
||||
double toTangentWeight = tCubed - tSquared;
|
||||
double curvedX = fromWeight * from.x()
|
||||
+ fromTangentWeight * fromTangent.x() * maximumOffset
|
||||
+ toWeight * to.x()
|
||||
+ toTangentWeight * toTangent.x() * maximumOffset;
|
||||
double curvedZ = fromWeight * from.z()
|
||||
+ fromTangentWeight * fromTangent.z() * maximumOffset
|
||||
+ toWeight * to.z()
|
||||
+ toTangentWeight * toTangent.z() * maximumOffset;
|
||||
double straightX = from.x() + deltaX * t;
|
||||
double straightZ = from.z() + deltaZ * t;
|
||||
RiverMeanderContext context = new RiverMeanderContext(id, t, straightX, straightZ);
|
||||
double configuredNoise = resolver.terrain.meanderNoise(context);
|
||||
double noise = Double.isFinite(configuredNoise)
|
||||
? StrictMath.max(-1.0, StrictMath.min(1.0, configuredNoise))
|
||||
: smoothEdgeNoise(id, t * 3.0);
|
||||
double envelope = 16D * tSquared * (1D - t) * (1D - t);
|
||||
double offset = maximumOffset * 0.5D * envelope * noise;
|
||||
x[point] = curvedX + normalX * offset;
|
||||
z[point] = curvedZ + normalZ * offset;
|
||||
}
|
||||
x[0] = from.x();
|
||||
z[0] = from.z();
|
||||
x[pointCount - 1] = to.x();
|
||||
z[pointCount - 1] = to.z();
|
||||
return new RiverPolyline(x, z);
|
||||
}
|
||||
|
||||
private FlowTangent flowTangent(
|
||||
RiverNode node,
|
||||
double fallbackX,
|
||||
double fallbackZ,
|
||||
NodeResolver resolver
|
||||
) {
|
||||
FlowTangent preferred = resolver.flowTangent(node);
|
||||
double tangentX = preferred.x();
|
||||
double tangentZ = preferred.z();
|
||||
if (tangentX == 0D && tangentZ == 0D) {
|
||||
return new FlowTangent(fallbackX, fallbackZ);
|
||||
}
|
||||
double alignment = tangentX * fallbackX + tangentZ * fallbackZ;
|
||||
if (alignment < 0D) {
|
||||
tangentX = -tangentX;
|
||||
tangentZ = -tangentZ;
|
||||
alignment = -alignment;
|
||||
}
|
||||
if (alignment < 0.15D) {
|
||||
return new FlowTangent(fallbackX, fallbackZ);
|
||||
}
|
||||
return new FlowTangent(tangentX, tangentZ);
|
||||
}
|
||||
|
||||
private FlowTangent resolveFlowTangent(RiverNode node, RiverTerrainSampler terrain) {
|
||||
double spacing = StrictMath.max(1D, options.cellSize() * 0.5D);
|
||||
double left = terrain.flowNoise(node.x() - spacing, node.z());
|
||||
double right = terrain.flowNoise(node.x() + spacing, node.z());
|
||||
double top = terrain.flowNoise(node.x(), node.z() - spacing);
|
||||
double bottom = terrain.flowNoise(node.x(), node.z() + spacing);
|
||||
if (!Double.isFinite(left) || !Double.isFinite(right)
|
||||
|| !Double.isFinite(top) || !Double.isFinite(bottom)) {
|
||||
return new FlowTangent(0D, 0D);
|
||||
}
|
||||
double tangentX = -(bottom - top);
|
||||
double tangentZ = right - left;
|
||||
double tangentLength = StrictMath.hypot(tangentX, tangentZ);
|
||||
if (tangentLength <= 0.0000001D) {
|
||||
return new FlowTangent(0D, 0D);
|
||||
}
|
||||
return new FlowTangent(tangentX / tangentLength, tangentZ / tangentLength);
|
||||
}
|
||||
|
||||
private double flowAlignmentCost(RiverNode from, RiverNode to, NodeResolver resolver) {
|
||||
if (options.flowAlignmentWeight() <= 0D) {
|
||||
return 0D;
|
||||
}
|
||||
FlowTangent tangent = resolver.flowTangent(from);
|
||||
if (tangent.x() == 0D && tangent.z() == 0D) {
|
||||
return 0D;
|
||||
}
|
||||
double deltaX = to.x() - from.x();
|
||||
double deltaZ = to.z() - from.z();
|
||||
double length = StrictMath.hypot(deltaX, deltaZ);
|
||||
if (length <= 0.0000001D) {
|
||||
return options.flowAlignmentWeight();
|
||||
}
|
||||
double alignment = StrictMath.abs(
|
||||
tangent.x() * deltaX / length + tangent.z() * deltaZ / length
|
||||
);
|
||||
return options.flowAlignmentWeight() * (1D - StrictMath.min(1D, alignment));
|
||||
}
|
||||
|
||||
private double smoothEdgeNoise(RiverEdgeId id, double position) {
|
||||
int lower = (int) StrictMath.floor(position);
|
||||
int upper = lower + 1;
|
||||
double fraction = position - lower;
|
||||
double fade = fraction * fraction * (3.0 - 2.0 * fraction);
|
||||
double a = centered(mix(options.seed() ^ id.stableId() ^ MEANDER_SALT ^ lower * 0x9E3779B97F4A7C15L));
|
||||
double b = centered(mix(options.seed() ^ id.stableId() ^ MEANDER_SALT ^ upper * 0x9E3779B97F4A7C15L));
|
||||
return a + (b - a) * fade;
|
||||
}
|
||||
|
||||
private boolean intersects(
|
||||
RiverReach reach,
|
||||
long minimumX,
|
||||
long minimumZ,
|
||||
long maximumX,
|
||||
long maximumZ
|
||||
) {
|
||||
double radius = reach.width() * 0.5 + reach.bankWidth();
|
||||
RiverPolyline polyline = reach.polyline();
|
||||
for (int point = 0; point < polyline.size() - 1; point++) {
|
||||
double segmentMinimumX = StrictMath.min(polyline.x(point), polyline.x(point + 1)) - radius;
|
||||
double segmentMaximumX = StrictMath.max(polyline.x(point), polyline.x(point + 1)) + radius;
|
||||
double segmentMinimumZ = StrictMath.min(polyline.z(point), polyline.z(point + 1)) - radius;
|
||||
double segmentMaximumZ = StrictMath.max(polyline.z(point), polyline.z(point + 1)) + radius;
|
||||
if (segmentMaximumX >= minimumX && segmentMinimumX < maximumX
|
||||
&& segmentMaximumZ >= minimumZ && segmentMinimumZ < maximumZ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean potentiallyIntersects(
|
||||
RiverNode from,
|
||||
RiverNode to,
|
||||
long minimumX,
|
||||
long minimumZ,
|
||||
long maximumX,
|
||||
long maximumZ
|
||||
) {
|
||||
double length = StrictMath.hypot(to.x() - from.x(), to.z() - from.z());
|
||||
double maximumMeander = StrictMath.min(options.meanderStrength(), length * 0.35D);
|
||||
double padding = options.maximumReachRadius() + maximumMeander;
|
||||
double reachMinimumX = StrictMath.min(from.x(), to.x()) - padding;
|
||||
double reachMaximumX = StrictMath.max(from.x(), to.x()) + padding;
|
||||
double reachMinimumZ = StrictMath.min(from.z(), to.z()) - padding;
|
||||
double reachMaximumZ = StrictMath.max(from.z(), to.z()) + padding;
|
||||
return reachMaximumX >= minimumX && reachMinimumX < maximumX
|
||||
&& reachMaximumZ >= minimumZ && reachMinimumZ < maximumZ;
|
||||
}
|
||||
|
||||
private int geometryPaddingCells() {
|
||||
double maximumEdgeAxisDelta = options.cellSize() * (1D + options.siteJitter());
|
||||
double maximumEdgeLength = StrictMath.sqrt(2D) * maximumEdgeAxisDelta;
|
||||
double maximumMeander = StrictMath.min(options.meanderStrength(), maximumEdgeLength * 0.35D);
|
||||
double displacement = options.maximumReachRadius() + maximumMeander;
|
||||
return 1 + (int) StrictMath.ceil(displacement / options.cellSize());
|
||||
}
|
||||
|
||||
private int compareRank(RiverNode first, RiverNode second) {
|
||||
if (first.ocean() != second.ocean()) {
|
||||
return first.ocean() ? -1 : 1;
|
||||
}
|
||||
int rankComparison = Double.compare(first.rank(), second.rank());
|
||||
if (rankComparison != 0) {
|
||||
return rankComparison;
|
||||
}
|
||||
int hydraulicComparison = Double.compare(first.hydraulicHeight(), second.hydraulicHeight());
|
||||
return hydraulicComparison != 0 ? hydraulicComparison : first.id().compareTo(second.id());
|
||||
}
|
||||
|
||||
private long hash(RiverNodeId id, long salt) {
|
||||
return mix(options.seed() ^ id.stableId() ^ salt);
|
||||
}
|
||||
|
||||
private long hash(RiverEdgeId id, long salt) {
|
||||
return mix(options.seed() ^ id.stableId() ^ salt);
|
||||
}
|
||||
|
||||
private long hash(long x, long z, long salt) {
|
||||
return mix(options.seed() ^ salt ^ mix(x * 0x9E3779B97F4A7C15L) ^ Long.rotateLeft(mix(z), 27));
|
||||
}
|
||||
|
||||
private static void addUnique(List<RiverNodeId> values, RiverNodeId candidate) {
|
||||
if (!values.contains(candidate)) {
|
||||
values.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean gate(long hash, double chance) {
|
||||
if (chance <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
if (chance >= 1.0) {
|
||||
return true;
|
||||
}
|
||||
return unit(hash) < chance;
|
||||
}
|
||||
|
||||
private static double centered(long hash) {
|
||||
return unit(hash) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
private static double unit(long hash) {
|
||||
return (hash >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
private static int clampToInt(long value) {
|
||||
return (int) StrictMath.max(Integer.MIN_VALUE, StrictMath.min(Integer.MAX_VALUE, value));
|
||||
}
|
||||
|
||||
private static double finiteOrZero(double value) {
|
||||
return Double.isFinite(value) ? value : 0.0;
|
||||
}
|
||||
|
||||
private static double finiteNonNegative(double value) {
|
||||
return Double.isFinite(value) && value > 0.0 ? value : 0.0;
|
||||
}
|
||||
|
||||
private static double effectiveChance(double baseChance, double multiplier) {
|
||||
if (!Double.isFinite(multiplier) || multiplier <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
return StrictMath.min(1.0, baseChance * multiplier);
|
||||
}
|
||||
|
||||
private static void requireWorldBounds(long minimumX, long minimumZ, long maximumX, long maximumZ) {
|
||||
if (minimumX < Integer.MIN_VALUE || minimumZ < Integer.MIN_VALUE
|
||||
|| maximumX > Integer.MAX_VALUE || maximumZ > Integer.MAX_VALUE) {
|
||||
throw new IllegalArgumentException("River tile exceeds integer world coordinates");
|
||||
}
|
||||
}
|
||||
|
||||
private final class NodeResolver {
|
||||
private final RiverTerrainSampler terrain;
|
||||
private final Map<RiverNodeId, RiverNode> nodes;
|
||||
private final Map<RiverNodeId, List<RiverNode>> downstreamCandidates;
|
||||
private final Map<RiverNodeId, Boolean> sourceGates;
|
||||
private final Map<SourceTileId, List<RiverNodeId>> minimumSources;
|
||||
private final Map<RiverEdgeId, Boolean> reachFeasibilities;
|
||||
private final Map<RiverEdgeId, Boolean> continuationGates;
|
||||
private final Map<RiverNodeId, Boolean> dryGates;
|
||||
private final Map<RiverNodeId, RiverTerminalPolicy> terminalPolicies;
|
||||
private final Map<RiverEdgeId, RiverRoutingContext> routingContexts;
|
||||
private final Map<RiverNodeId, FlowTangent> flowTangents;
|
||||
|
||||
private NodeResolver(RiverTerrainSampler terrain) {
|
||||
this.terrain = terrain;
|
||||
nodes = new HashMap<>();
|
||||
downstreamCandidates = new HashMap<>();
|
||||
sourceGates = new HashMap<>();
|
||||
minimumSources = new HashMap<>();
|
||||
reachFeasibilities = new HashMap<>();
|
||||
continuationGates = new HashMap<>();
|
||||
dryGates = new HashMap<>();
|
||||
terminalPolicies = new HashMap<>();
|
||||
routingContexts = new HashMap<>();
|
||||
flowTangents = new HashMap<>();
|
||||
}
|
||||
|
||||
private RiverNode resolve(RiverNodeId id) {
|
||||
return nodes.computeIfAbsent(id, key -> createNode(key, terrain));
|
||||
}
|
||||
|
||||
private List<RiverNode> downstreamCandidates(RiverNode node) {
|
||||
return downstreamCandidates.computeIfAbsent(
|
||||
node.id(),
|
||||
ignored -> computeDownstreamCandidates(node, this));
|
||||
}
|
||||
|
||||
private boolean sourcePermitted(RiverNodeId sourceId) {
|
||||
return sourceGates.computeIfAbsent(sourceId, this::computeSourcePermitted);
|
||||
}
|
||||
|
||||
private boolean computeSourcePermitted(RiverNodeId sourceId) {
|
||||
if (options.sourceChance() <= 0D) {
|
||||
return false;
|
||||
}
|
||||
boolean minimumSelected = minimumSources(sourceId).contains(sourceId);
|
||||
if (minimumSelected) {
|
||||
return true;
|
||||
}
|
||||
long sourceHash = hash(sourceId, SOURCE_SALT);
|
||||
double maximumMultiplier = terrain.maximumSourceChanceMultiplier();
|
||||
if (!minimumSelected
|
||||
&& Double.isFinite(maximumMultiplier)
|
||||
&& !gate(sourceHash, effectiveChance(options.sourceChance(), maximumMultiplier))) {
|
||||
return false;
|
||||
}
|
||||
NodePosition position = nodePosition(sourceId);
|
||||
RiverTerrainSourceSample sourceSample = terrain.sampleSource(position.blockX(), position.blockZ());
|
||||
double chance = effectiveChance(
|
||||
options.sourceChance(),
|
||||
sourceSample.chanceMultiplier()
|
||||
);
|
||||
boolean selected = gate(sourceHash, chance);
|
||||
boolean permitted = selected
|
||||
&& sourceSample.riverAllowed()
|
||||
&& !sourceSample.ocean();
|
||||
return permitted;
|
||||
}
|
||||
|
||||
private List<RiverNodeId> minimumSources(RiverNodeId sourceId) {
|
||||
if (options.minimumSourcesPerTile() <= 0 || options.sourceChance() <= 0D) {
|
||||
return List.of();
|
||||
}
|
||||
SourceTileId tileId = new SourceTileId(
|
||||
Math.floorDiv(sourceId.cellX(), options.tileCells()),
|
||||
Math.floorDiv(sourceId.cellZ(), options.tileCells())
|
||||
);
|
||||
return minimumSources.computeIfAbsent(tileId, this::computeMinimumSources);
|
||||
}
|
||||
|
||||
private List<RiverNodeId> computeMinimumSources(SourceTileId tileId) {
|
||||
long minimumCellX = tileId.tileX() * options.tileCells();
|
||||
long minimumCellZ = tileId.tileZ() * options.tileCells();
|
||||
int candidateCount = options.tileCells() * options.tileCells();
|
||||
int targetCount = Math.min(options.minimumSourcesPerTile(), candidateCount);
|
||||
if (targetCount == candidateCount) {
|
||||
ArrayList<RiverNodeId> selected = new ArrayList<>(candidateCount);
|
||||
for (long cellX = minimumCellX; cellX < minimumCellX + options.tileCells(); cellX++) {
|
||||
for (long cellZ = minimumCellZ; cellZ < minimumCellZ + options.tileCells(); cellZ++) {
|
||||
RiverNodeId candidateId = new RiverNodeId(cellX, cellZ);
|
||||
if (sourceFloorEligible(candidateId)) {
|
||||
selected.add(candidateId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(selected);
|
||||
}
|
||||
ArrayList<WeightedSource> candidates = new ArrayList<>(candidateCount);
|
||||
for (long cellX = minimumCellX; cellX < minimumCellX + options.tileCells(); cellX++) {
|
||||
for (long cellZ = minimumCellZ; cellZ < minimumCellZ + options.tileCells(); cellZ++) {
|
||||
RiverNodeId candidateId = new RiverNodeId(cellX, cellZ);
|
||||
candidates.add(new WeightedSource(
|
||||
candidateId,
|
||||
-drainageDistance(candidateId)
|
||||
+ unit(hash(candidateId, SOURCE_FLOOR_SALT)) * 0.25D
|
||||
));
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(WeightedSource::priority)
|
||||
.thenComparing(WeightedSource::id));
|
||||
ArrayList<RiverNodeId> selected = new ArrayList<>(targetCount);
|
||||
for (WeightedSource candidate : candidates) {
|
||||
if (!sourceFloorEligible(candidate.id())) {
|
||||
continue;
|
||||
}
|
||||
selected.add(candidate.id());
|
||||
if (selected.size() >= targetCount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return List.copyOf(selected);
|
||||
}
|
||||
|
||||
private boolean sourceFloorEligible(RiverNodeId candidateId) {
|
||||
NodePosition position = nodePosition(candidateId);
|
||||
RiverTerrainSourceSample sourceSample = terrain.sampleSource(position.blockX(), position.blockZ());
|
||||
return sourceSample.riverAllowed()
|
||||
&& !sourceSample.ocean()
|
||||
&& Double.isFinite(sourceSample.chanceMultiplier())
|
||||
&& sourceSample.chanceMultiplier() > 0D;
|
||||
}
|
||||
|
||||
private boolean reachFeasible(RiverRoutingContext context) {
|
||||
return reachFeasibilities.computeIfAbsent(
|
||||
context.edgeId(),
|
||||
ignored -> terrain.allowsReach(context));
|
||||
}
|
||||
|
||||
private boolean continuationPermitted(RiverRoutingContext context) {
|
||||
return continuationGates.computeIfAbsent(context.edgeId(), ignored -> {
|
||||
double chance = effectiveChance(
|
||||
options.reachChance(),
|
||||
terrain.reachChanceMultiplier(context.midpointX(), context.midpointZ())
|
||||
);
|
||||
return gate(hash(context.edgeId(), REACH_SALT), chance);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean dryPermitted(RiverNodeId sourceId) {
|
||||
return dryGates.computeIfAbsent(
|
||||
sourceId,
|
||||
ignored -> gate(hash(sourceId, DRY_SALT), options.dryChannelChance()));
|
||||
}
|
||||
|
||||
private RiverTerminalPolicy terminalPolicy(RiverNode terminal) {
|
||||
return terminalPolicies.computeIfAbsent(terminal.id(), ignored -> {
|
||||
int terminalX = clampToInt(StrictMath.round(terminal.x()));
|
||||
int terminalZ = clampToInt(StrictMath.round(terminal.z()));
|
||||
RiverTerminalPolicy sampled = terrain.terminalPolicy(terminalX, terminalZ);
|
||||
return sampled == null ? RiverTerminalPolicy.INHERIT : sampled;
|
||||
});
|
||||
}
|
||||
|
||||
private RiverRoutingContext routingContext(RiverNode from, RiverNode to) {
|
||||
RiverEdgeId edgeId = RiverEdgeId.of(from.id(), to.id());
|
||||
return routingContexts.computeIfAbsent(edgeId, ignored -> RiverRoutingContext.lazy(
|
||||
edgeId,
|
||||
from,
|
||||
to,
|
||||
() -> createPolyline(edgeId, from, to, this)));
|
||||
}
|
||||
|
||||
private FlowTangent flowTangent(RiverNode node) {
|
||||
return flowTangents.computeIfAbsent(
|
||||
node.id(),
|
||||
ignored -> resolveFlowTangent(node, terrain));
|
||||
}
|
||||
}
|
||||
|
||||
private record RankedCandidate(RiverNode node, double cost) {
|
||||
}
|
||||
|
||||
private record NodePosition(double x, double z, int blockX, int blockZ) {
|
||||
}
|
||||
|
||||
private record FlowTangent(double x, double z) {
|
||||
}
|
||||
|
||||
private record SourceTileId(long tileX, long tileZ) {
|
||||
}
|
||||
|
||||
private record WeightedSource(RiverNodeId id, double priority) {
|
||||
}
|
||||
|
||||
private final class ReachAccumulator {
|
||||
private final RiverEdgeId id;
|
||||
private final RiverNode from;
|
||||
private final RiverNode to;
|
||||
private final RiverRoutingContext context;
|
||||
private final RiverTerrainSampler terrain;
|
||||
private int wetFlow;
|
||||
private int dryFlow;
|
||||
private int terminalWetFlow;
|
||||
private int terminalDryFlow;
|
||||
|
||||
private ReachAccumulator(
|
||||
RiverEdgeId id,
|
||||
RiverNode from,
|
||||
RiverNode to,
|
||||
RiverRoutingContext context,
|
||||
RiverTerrainSampler terrain
|
||||
) {
|
||||
this.id = id;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.context = context;
|
||||
this.terrain = terrain;
|
||||
}
|
||||
|
||||
private void add(RiverRouteState state, boolean terminal) {
|
||||
if (state == RiverRouteState.WET) {
|
||||
wetFlow++;
|
||||
if (terminal) {
|
||||
terminalWetFlow++;
|
||||
}
|
||||
} else if (state == RiverRouteState.DRY) {
|
||||
dryFlow++;
|
||||
if (terminal) {
|
||||
terminalDryFlow++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RiverReach build() {
|
||||
int flow = wetFlow + dryFlow;
|
||||
int order = 1 + (31 - Integer.numberOfLeadingZeros(flow));
|
||||
double baseWidth = positiveOrFallback(
|
||||
terrain.channelWidth(context, options.channelWidth()),
|
||||
options.channelWidth()
|
||||
);
|
||||
double bankWidth = nonNegativeOrFallback(
|
||||
terrain.bankWidth(context, options.bankWidth()),
|
||||
options.bankWidth()
|
||||
);
|
||||
double baseDepth = positiveOrFallback(
|
||||
terrain.depth(context, options.depth()),
|
||||
options.depth()
|
||||
);
|
||||
double width = StrictMath.min(
|
||||
options.maxChannelWidth(),
|
||||
baseWidth * (1.0 + options.orderWidthFactor() * (order - 1))
|
||||
);
|
||||
bankWidth = StrictMath.min(options.maxBankWidth(), bankWidth);
|
||||
double depth = StrictMath.min(
|
||||
options.maxDepth(),
|
||||
baseDepth * (1.0 + options.orderDepthFactor() * (order - 1))
|
||||
);
|
||||
RiverRouteState state = wetFlow > 0 ? RiverRouteState.WET : RiverRouteState.DRY;
|
||||
return new RiverReach(
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
state,
|
||||
flow,
|
||||
order,
|
||||
width,
|
||||
bankWidth,
|
||||
depth,
|
||||
state == RiverRouteState.WET && to.ocean(),
|
||||
state == RiverRouteState.WET
|
||||
? terminalWetFlow == wetFlow
|
||||
: terminalDryFlow == dryFlow,
|
||||
context.polyline()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static double positiveOrFallback(double value, double fallback) {
|
||||
return Double.isFinite(value) && value > 0.0 ? value : fallback;
|
||||
}
|
||||
|
||||
private static double nonNegativeOrFallback(double value, double fallback) {
|
||||
return Double.isFinite(value) && value >= 0.0 ? value : fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
public record RiverNetworkOptions(
|
||||
long seed,
|
||||
int cellSize,
|
||||
int tileCells,
|
||||
double siteJitter,
|
||||
int maxRouteReaches,
|
||||
int minimumSourcesPerTile,
|
||||
int downstreamCandidateLimit,
|
||||
int routingBasinCells,
|
||||
double routingPlateauHeight,
|
||||
double hydraulicBaseHeight,
|
||||
boolean requireOcean,
|
||||
double sourceChance,
|
||||
double reachChance,
|
||||
double dryChannelChance,
|
||||
double terrainHeightWeight,
|
||||
double routingNoiseWeight,
|
||||
double flowAlignmentWeight,
|
||||
double oceanAttraction,
|
||||
double channelWidth,
|
||||
double bankWidth,
|
||||
double depth,
|
||||
double maxChannelWidth,
|
||||
double maxBankWidth,
|
||||
double maxDepth,
|
||||
double orderWidthFactor,
|
||||
double orderDepthFactor,
|
||||
double maximumReachRadius,
|
||||
double meanderStrength,
|
||||
int meanderSubdivisions
|
||||
) {
|
||||
public RiverNetworkOptions {
|
||||
requireRange(cellSize, 8, 4096, "cellSize");
|
||||
requireRange(tileCells, 1, 64, "tileCells");
|
||||
requireRange(maxRouteReaches, 1, 256, "maxRouteReaches");
|
||||
requireRange(minimumSourcesPerTile, 0, tileCells * tileCells, "minimumSourcesPerTile");
|
||||
requireRange(downstreamCandidateLimit, 1, 8, "downstreamCandidateLimit");
|
||||
requireRange(routingBasinCells, 8, 256, "routingBasinCells");
|
||||
requirePositive(routingPlateauHeight, "routingPlateauHeight");
|
||||
requireFinite(hydraulicBaseHeight, "hydraulicBaseHeight");
|
||||
requireRange(meanderSubdivisions, 1, 64, "meanderSubdivisions");
|
||||
requireProbability(siteJitter, "siteJitter");
|
||||
requireProbability(sourceChance, "sourceChance");
|
||||
requireProbability(reachChance, "reachChance");
|
||||
requireProbability(dryChannelChance, "dryChannelChance");
|
||||
requireFiniteNonNegative(terrainHeightWeight, "terrainHeightWeight");
|
||||
requireFiniteNonNegative(routingNoiseWeight, "routingNoiseWeight");
|
||||
requireFiniteNonNegative(flowAlignmentWeight, "flowAlignmentWeight");
|
||||
requireFiniteNonNegative(oceanAttraction, "oceanAttraction");
|
||||
requirePositive(channelWidth, "channelWidth");
|
||||
requireFiniteNonNegative(bankWidth, "bankWidth");
|
||||
requirePositive(depth, "depth");
|
||||
requirePositive(maxChannelWidth, "maxChannelWidth");
|
||||
requireFiniteNonNegative(maxBankWidth, "maxBankWidth");
|
||||
requirePositive(maxDepth, "maxDepth");
|
||||
requireFiniteNonNegative(orderWidthFactor, "orderWidthFactor");
|
||||
requireFiniteNonNegative(orderDepthFactor, "orderDepthFactor");
|
||||
requireFiniteNonNegative(maximumReachRadius, "maximumReachRadius");
|
||||
requireFiniteNonNegative(meanderStrength, "meanderStrength");
|
||||
RiverTopologyComplexity.requireSafe(
|
||||
cellSize,
|
||||
tileCells,
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
maximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
);
|
||||
}
|
||||
|
||||
public static Builder builder(long seed) {
|
||||
return new Builder(seed);
|
||||
}
|
||||
|
||||
private static void requireRange(int value, int minimum, int maximum, String name) {
|
||||
if (value < minimum || value > maximum) {
|
||||
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireProbability(double value, String name) {
|
||||
if (!Double.isFinite(value) || value < 0.0 || value > 1.0) {
|
||||
throw new IllegalArgumentException(name + " must be finite and between 0 and 1");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireFiniteNonNegative(double value, String name) {
|
||||
if (!Double.isFinite(value) || value < 0.0) {
|
||||
throw new IllegalArgumentException(name + " must be finite and non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requirePositive(double value, String name) {
|
||||
if (!Double.isFinite(value) || value <= 0.0) {
|
||||
throw new IllegalArgumentException(name + " must be finite and positive");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireFinite(double value, String name) {
|
||||
if (!Double.isFinite(value)) {
|
||||
throw new IllegalArgumentException(name + " must be finite");
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final long seed;
|
||||
private int cellSize;
|
||||
private int tileCells;
|
||||
private double siteJitter;
|
||||
private int maxRouteReaches;
|
||||
private int minimumSourcesPerTile;
|
||||
private int downstreamCandidateLimit;
|
||||
private int routingBasinCells;
|
||||
private double routingPlateauHeight;
|
||||
private double hydraulicBaseHeight;
|
||||
private boolean requireOcean;
|
||||
private double sourceChance;
|
||||
private double reachChance;
|
||||
private double dryChannelChance;
|
||||
private double terrainHeightWeight;
|
||||
private double routingNoiseWeight;
|
||||
private double flowAlignmentWeight;
|
||||
private double oceanAttraction;
|
||||
private double channelWidth;
|
||||
private double bankWidth;
|
||||
private double depth;
|
||||
private double maxChannelWidth;
|
||||
private double maxBankWidth;
|
||||
private double maxDepth;
|
||||
private double orderWidthFactor;
|
||||
private double orderDepthFactor;
|
||||
private double maximumReachRadius;
|
||||
private double meanderStrength;
|
||||
private int meanderSubdivisions;
|
||||
|
||||
private Builder(long seed) {
|
||||
this.seed = seed;
|
||||
cellSize = 512;
|
||||
tileCells = 4;
|
||||
siteJitter = 0.35;
|
||||
maxRouteReaches = 16;
|
||||
minimumSourcesPerTile = 0;
|
||||
downstreamCandidateLimit = 4;
|
||||
routingBasinCells = 64;
|
||||
routingPlateauHeight = 8.0;
|
||||
hydraulicBaseHeight = 64D;
|
||||
requireOcean = false;
|
||||
sourceChance = 0.12;
|
||||
reachChance = 0.98;
|
||||
dryChannelChance = 0.35;
|
||||
terrainHeightWeight = 1.0;
|
||||
routingNoiseWeight = 24.0;
|
||||
flowAlignmentWeight = 0D;
|
||||
oceanAttraction = 64.0;
|
||||
channelWidth = 10.0;
|
||||
bankWidth = 8.0;
|
||||
depth = 4.0;
|
||||
maxChannelWidth = 10D;
|
||||
maxBankWidth = 8D;
|
||||
maxDepth = 10D;
|
||||
orderWidthFactor = 0.35;
|
||||
orderDepthFactor = 0.2;
|
||||
maximumReachRadius = Double.NaN;
|
||||
meanderStrength = 40.0;
|
||||
meanderSubdivisions = 8;
|
||||
}
|
||||
|
||||
public Builder cellSize(int value) {
|
||||
cellSize = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder tileCells(int value) {
|
||||
tileCells = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder siteJitter(double value) {
|
||||
siteJitter = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maxRouteReaches(int value) {
|
||||
maxRouteReaches = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder minimumSourcesPerTile(int value) {
|
||||
minimumSourcesPerTile = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder downstreamCandidateLimit(int value) {
|
||||
downstreamCandidateLimit = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder routingBasinCells(int value) {
|
||||
routingBasinCells = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder routingPlateauHeight(double value) {
|
||||
routingPlateauHeight = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder hydraulicBaseHeight(double value) {
|
||||
hydraulicBaseHeight = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder requireOcean(boolean value) {
|
||||
requireOcean = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder sourceChance(double value) {
|
||||
sourceChance = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder reachChance(double value) {
|
||||
reachChance = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder dryChannelChance(double value) {
|
||||
dryChannelChance = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder terrainHeightWeight(double value) {
|
||||
terrainHeightWeight = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder routingNoiseWeight(double value) {
|
||||
routingNoiseWeight = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder flowAlignmentWeight(double value) {
|
||||
flowAlignmentWeight = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder oceanAttraction(double value) {
|
||||
oceanAttraction = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder channelWidth(double value) {
|
||||
channelWidth = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder bankWidth(double value) {
|
||||
bankWidth = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder depth(double value) {
|
||||
depth = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maxChannelWidth(double value) {
|
||||
maxChannelWidth = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maxBankWidth(double value) {
|
||||
maxBankWidth = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maxDepth(double value) {
|
||||
maxDepth = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder orderWidthFactor(double value) {
|
||||
orderWidthFactor = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder orderDepthFactor(double value) {
|
||||
orderDepthFactor = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maximumReachRadius(double value) {
|
||||
maximumReachRadius = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder meanderStrength(double value) {
|
||||
meanderStrength = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder meanderSubdivisions(int value) {
|
||||
meanderSubdivisions = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RiverNetworkOptions build() {
|
||||
double resolvedMaximumReachRadius = Double.isFinite(maximumReachRadius)
|
||||
? maximumReachRadius
|
||||
: defaultMaximumReachRadius();
|
||||
return new RiverNetworkOptions(
|
||||
seed,
|
||||
cellSize,
|
||||
tileCells,
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
minimumSourcesPerTile,
|
||||
downstreamCandidateLimit,
|
||||
routingBasinCells,
|
||||
routingPlateauHeight,
|
||||
hydraulicBaseHeight,
|
||||
requireOcean,
|
||||
sourceChance,
|
||||
reachChance,
|
||||
dryChannelChance,
|
||||
terrainHeightWeight,
|
||||
routingNoiseWeight,
|
||||
flowAlignmentWeight,
|
||||
oceanAttraction,
|
||||
channelWidth,
|
||||
bankWidth,
|
||||
depth,
|
||||
maxChannelWidth,
|
||||
maxBankWidth,
|
||||
maxDepth,
|
||||
orderWidthFactor,
|
||||
orderDepthFactor,
|
||||
resolvedMaximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
);
|
||||
}
|
||||
|
||||
private double defaultMaximumReachRadius() {
|
||||
return maxChannelWidth * 0.5D + maxBankWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record RiverNode(
|
||||
RiverNodeId id,
|
||||
double x,
|
||||
double z,
|
||||
double naturalHeight,
|
||||
double hydraulicHeight,
|
||||
double rank,
|
||||
double routingScore,
|
||||
boolean ocean,
|
||||
boolean riverAllowed
|
||||
) {
|
||||
public RiverNode {
|
||||
Objects.requireNonNull(id);
|
||||
if (!Double.isFinite(x) || !Double.isFinite(z) || !Double.isFinite(naturalHeight)
|
||||
|| !Double.isFinite(hydraulicHeight)
|
||||
|| !Double.isFinite(rank) || !Double.isFinite(routingScore)) {
|
||||
throw new IllegalArgumentException("River node coordinates, height, and rank must be finite");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
public record RiverNodeId(long cellX, long cellZ) implements Comparable<RiverNodeId> {
|
||||
public long stableId() {
|
||||
return RiverNetwork.mix(cellX * 0x9E3779B97F4A7C15L ^ Long.rotateLeft(cellZ * 0xC2B2AE3D27D4EB4FL, 31));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(RiverNodeId other) {
|
||||
int xComparison = Long.compare(cellX, other.cellX);
|
||||
return xComparison != 0 ? xComparison : Long.compare(cellZ, other.cellZ);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
public final class RiverPolyline {
|
||||
private final double[] x;
|
||||
private final double[] z;
|
||||
private final double[] cumulativeLength;
|
||||
private final double length;
|
||||
|
||||
public RiverPolyline(double[] x, double[] z) {
|
||||
if (x.length != z.length || x.length < 2) {
|
||||
throw new IllegalArgumentException("River polyline requires matching coordinate arrays and at least two points");
|
||||
}
|
||||
this.x = x.clone();
|
||||
this.z = z.clone();
|
||||
cumulativeLength = new double[x.length];
|
||||
double measuredLength = 0.0;
|
||||
for (int i = 0; i < x.length; i++) {
|
||||
if (!Double.isFinite(x[i]) || !Double.isFinite(z[i])) {
|
||||
throw new IllegalArgumentException("River polyline coordinates must be finite");
|
||||
}
|
||||
if (i > 0) {
|
||||
measuredLength += StrictMath.hypot(x[i] - x[i - 1], z[i] - z[i - 1]);
|
||||
cumulativeLength[i] = measuredLength;
|
||||
}
|
||||
}
|
||||
length = measuredLength;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return x.length;
|
||||
}
|
||||
|
||||
public double x(int index) {
|
||||
return x[index];
|
||||
}
|
||||
|
||||
public double z(int index) {
|
||||
return z[index];
|
||||
}
|
||||
|
||||
public double cumulativeLength(int index) {
|
||||
return cumulativeLength[index];
|
||||
}
|
||||
|
||||
public double length() {
|
||||
return length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record RiverReach(
|
||||
RiverEdgeId id,
|
||||
RiverNode from,
|
||||
RiverNode to,
|
||||
RiverRouteState state,
|
||||
int flow,
|
||||
int order,
|
||||
double width,
|
||||
double bankWidth,
|
||||
double depth,
|
||||
boolean mouth,
|
||||
boolean terminal,
|
||||
RiverPolyline polyline
|
||||
) {
|
||||
public RiverReach {
|
||||
Objects.requireNonNull(id);
|
||||
Objects.requireNonNull(from);
|
||||
Objects.requireNonNull(to);
|
||||
Objects.requireNonNull(state);
|
||||
Objects.requireNonNull(polyline);
|
||||
if (state == RiverRouteState.SUPPRESSED) {
|
||||
throw new IllegalArgumentException("Suppressed routes cannot produce reaches");
|
||||
}
|
||||
if (flow < 1 || order < 1) {
|
||||
throw new IllegalArgumentException("River reach flow and order must be positive");
|
||||
}
|
||||
if (!Double.isFinite(width) || width <= 0.0 || !Double.isFinite(bankWidth) || bankWidth < 0.0
|
||||
|| !Double.isFinite(depth) || depth <= 0.0) {
|
||||
throw new IllegalArgumentException("River reach dimensions must be finite and valid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record RiverRoute(
|
||||
RiverNodeId source,
|
||||
RiverRouteState state,
|
||||
List<RiverEdgeId> edges,
|
||||
boolean oceanConnected,
|
||||
boolean terminal
|
||||
) {
|
||||
public RiverRoute {
|
||||
Objects.requireNonNull(source);
|
||||
Objects.requireNonNull(state);
|
||||
edges = List.copyOf(edges);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
public enum RiverRouteState {
|
||||
WET,
|
||||
DRY,
|
||||
SUPPRESSED
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class RiverRoutingContext {
|
||||
private final RiverEdgeId edgeId;
|
||||
private final RiverNode from;
|
||||
private final RiverNode to;
|
||||
private final Supplier<RiverPolyline> polylineSupplier;
|
||||
private volatile RiverPolyline polyline;
|
||||
|
||||
public RiverRoutingContext(RiverEdgeId edgeId, RiverNode from, RiverNode to, RiverPolyline polyline) {
|
||||
this(edgeId, from, to, () -> polyline, Objects.requireNonNull(polyline));
|
||||
}
|
||||
|
||||
static RiverRoutingContext lazy(
|
||||
RiverEdgeId edgeId,
|
||||
RiverNode from,
|
||||
RiverNode to,
|
||||
Supplier<RiverPolyline> polylineSupplier
|
||||
) {
|
||||
return new RiverRoutingContext(edgeId, from, to, polylineSupplier, null);
|
||||
}
|
||||
|
||||
private RiverRoutingContext(
|
||||
RiverEdgeId edgeId,
|
||||
RiverNode from,
|
||||
RiverNode to,
|
||||
Supplier<RiverPolyline> polylineSupplier,
|
||||
RiverPolyline polyline
|
||||
) {
|
||||
this.edgeId = Objects.requireNonNull(edgeId);
|
||||
this.from = Objects.requireNonNull(from);
|
||||
this.to = Objects.requireNonNull(to);
|
||||
this.polylineSupplier = Objects.requireNonNull(polylineSupplier);
|
||||
this.polyline = polyline;
|
||||
}
|
||||
|
||||
public RiverEdgeId edgeId() {
|
||||
return edgeId;
|
||||
}
|
||||
|
||||
public RiverNode from() {
|
||||
return from;
|
||||
}
|
||||
|
||||
public RiverNode to() {
|
||||
return to;
|
||||
}
|
||||
|
||||
public RiverPolyline polyline() {
|
||||
RiverPolyline resolved = polyline;
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (polyline == null) {
|
||||
polyline = Objects.requireNonNull(polylineSupplier.get());
|
||||
}
|
||||
return polyline;
|
||||
}
|
||||
}
|
||||
|
||||
public int midpointX() {
|
||||
return (int) StrictMath.max(
|
||||
Integer.MIN_VALUE,
|
||||
StrictMath.min(Integer.MAX_VALUE, StrictMath.round((from.x() + to.x()) * 0.5))
|
||||
);
|
||||
}
|
||||
|
||||
public int midpointZ() {
|
||||
return (int) StrictMath.max(
|
||||
Integer.MIN_VALUE,
|
||||
StrictMath.min(Integer.MAX_VALUE, StrictMath.round((from.z() + to.z()) * 0.5))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object candidate) {
|
||||
if (this == candidate) {
|
||||
return true;
|
||||
}
|
||||
if (!(candidate instanceof RiverRoutingContext context)) {
|
||||
return false;
|
||||
}
|
||||
return edgeId.equals(context.edgeId)
|
||||
&& from.equals(context.from)
|
||||
&& to.equals(context.to)
|
||||
&& polyline().equals(context.polyline());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(edgeId, from, to, polyline());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RiverRoutingContext[edgeId=" + edgeId
|
||||
+ ", from=" + from
|
||||
+ ", to=" + to
|
||||
+ ", polyline=" + polyline() + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
public record RiverSample(
|
||||
boolean present,
|
||||
RiverRouteState state,
|
||||
RiverSection section,
|
||||
double distance,
|
||||
double alongReach,
|
||||
double carveWeight,
|
||||
int flow,
|
||||
int order,
|
||||
double width,
|
||||
double bankWidth,
|
||||
double depth,
|
||||
boolean terminal,
|
||||
RiverEdgeId reachId
|
||||
) {
|
||||
private static final RiverSample NONE = new RiverSample(
|
||||
false,
|
||||
RiverRouteState.SUPPRESSED,
|
||||
RiverSection.NONE,
|
||||
Double.POSITIVE_INFINITY,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
false,
|
||||
null
|
||||
);
|
||||
|
||||
public static RiverSample none() {
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
public enum RiverSection {
|
||||
NONE,
|
||||
CHANNEL,
|
||||
MOUTH,
|
||||
BANK,
|
||||
DRY_CHANNEL,
|
||||
DRY_BANK
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
public enum RiverTerminalPolicy {
|
||||
INHERIT,
|
||||
WET,
|
||||
DRY,
|
||||
SUPPRESS
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
public record RiverTerrainNodeSample(
|
||||
double naturalHeight,
|
||||
boolean ocean,
|
||||
boolean riverAllowed,
|
||||
double routingCost
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
public interface RiverTerrainSampler {
|
||||
double naturalHeight(int blockX, int blockZ);
|
||||
|
||||
boolean isOcean(int blockX, int blockZ);
|
||||
|
||||
default RiverTerrainNodeSample sampleNode(int blockX, int blockZ) {
|
||||
return new RiverTerrainNodeSample(
|
||||
naturalHeight(blockX, blockZ),
|
||||
isOcean(blockX, blockZ),
|
||||
allowsRiver(blockX, blockZ),
|
||||
routingCost(blockX, blockZ)
|
||||
);
|
||||
}
|
||||
|
||||
default RiverTerrainSourceSample sampleSource(int blockX, int blockZ) {
|
||||
return new RiverTerrainSourceSample(
|
||||
sourceChanceMultiplier(blockX, blockZ),
|
||||
allowsRiver(blockX, blockZ),
|
||||
isOcean(blockX, blockZ)
|
||||
);
|
||||
}
|
||||
|
||||
default double routingCost(int blockX, int blockZ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
default double sourceChanceMultiplier(int blockX, int blockZ) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
default double maximumSourceChanceMultiplier() {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
default double reachChanceMultiplier(int blockX, int blockZ) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
default boolean allowsRiver(int blockX, int blockZ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
default boolean allowsReach(RiverRoutingContext context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
default double reachRoutingCost(RiverRoutingContext context) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
default double meanderNoise(RiverMeanderContext context) {
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
default double flowNoise(double x, double z) {
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
default double channelWidth(RiverRoutingContext context, double fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
default double bankWidth(RiverRoutingContext context, double fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
default double depth(RiverRoutingContext context, double fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
default RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
||||
return RiverTerminalPolicy.INHERIT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
public record RiverTerrainSourceSample(
|
||||
double chanceMultiplier,
|
||||
boolean riverAllowed,
|
||||
boolean ocean
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class RiverTile {
|
||||
private static final int BUCKET_SIZE = 64;
|
||||
|
||||
private final int tileX;
|
||||
private final int tileZ;
|
||||
private final int minimumX;
|
||||
private final int minimumZ;
|
||||
private final int maximumX;
|
||||
private final int maximumZ;
|
||||
private final List<RiverReach> reaches;
|
||||
private final Map<RiverEdgeId, RiverReach> reachesById;
|
||||
private final Map<Long, List<RiverReach>> spatialIndex;
|
||||
|
||||
public RiverTile(
|
||||
int tileX,
|
||||
int tileZ,
|
||||
int minimumX,
|
||||
int minimumZ,
|
||||
int maximumX,
|
||||
int maximumZ,
|
||||
List<RiverReach> reaches
|
||||
) {
|
||||
if (minimumX >= maximumX || minimumZ >= maximumZ) {
|
||||
throw new IllegalArgumentException("River tile bounds must have positive area");
|
||||
}
|
||||
this.tileX = tileX;
|
||||
this.tileZ = tileZ;
|
||||
this.minimumX = minimumX;
|
||||
this.minimumZ = minimumZ;
|
||||
this.maximumX = maximumX;
|
||||
this.maximumZ = maximumZ;
|
||||
this.reaches = List.copyOf(reaches);
|
||||
reachesById = indexById(this.reaches);
|
||||
spatialIndex = createSpatialIndex(this.reaches);
|
||||
}
|
||||
|
||||
public int tileX() {
|
||||
return tileX;
|
||||
}
|
||||
|
||||
public int tileZ() {
|
||||
return tileZ;
|
||||
}
|
||||
|
||||
public int minimumX() {
|
||||
return minimumX;
|
||||
}
|
||||
|
||||
public int minimumZ() {
|
||||
return minimumZ;
|
||||
}
|
||||
|
||||
public int maximumX() {
|
||||
return maximumX;
|
||||
}
|
||||
|
||||
public int maximumZ() {
|
||||
return maximumZ;
|
||||
}
|
||||
|
||||
public List<RiverReach> reaches() {
|
||||
return reaches;
|
||||
}
|
||||
|
||||
public RiverReach reach(RiverEdgeId id) {
|
||||
return reachesById.get(Objects.requireNonNull(id));
|
||||
}
|
||||
|
||||
public List<RiverAnchor> candidateAnchors(double spacing, long salt) {
|
||||
return candidateAnchors(minimumX, minimumZ, maximumX, maximumZ, spacing, salt);
|
||||
}
|
||||
|
||||
public List<RiverAnchor> candidateAnchors(
|
||||
double queryMinimumX,
|
||||
double queryMinimumZ,
|
||||
double queryMaximumX,
|
||||
double queryMaximumZ,
|
||||
double spacing,
|
||||
long salt
|
||||
) {
|
||||
if (!Double.isFinite(spacing) || spacing <= 0.0) {
|
||||
throw new IllegalArgumentException("River anchor spacing must be finite and positive");
|
||||
}
|
||||
if (!Double.isFinite(queryMinimumX) || !Double.isFinite(queryMinimumZ)
|
||||
|| !Double.isFinite(queryMaximumX) || !Double.isFinite(queryMaximumZ)
|
||||
|| queryMinimumX >= queryMaximumX || queryMinimumZ >= queryMaximumZ) {
|
||||
throw new IllegalArgumentException("River anchor query bounds must be finite and have positive area");
|
||||
}
|
||||
ArrayList<RiverAnchor> anchors = new ArrayList<>();
|
||||
for (RiverReach reach : indexedReaches(queryMinimumX, queryMinimumZ, queryMaximumX, queryMaximumZ)) {
|
||||
addAnchors(
|
||||
reach,
|
||||
spacing,
|
||||
salt,
|
||||
queryMinimumX,
|
||||
queryMinimumZ,
|
||||
queryMaximumX,
|
||||
queryMaximumZ,
|
||||
anchors
|
||||
);
|
||||
}
|
||||
return List.copyOf(anchors);
|
||||
}
|
||||
|
||||
public int sampleCandidateCount(double x, double z) {
|
||||
return indexedReaches(x, z).size();
|
||||
}
|
||||
|
||||
public RiverSample sample(double x, double z) {
|
||||
RiverReach nearestReach = null;
|
||||
double nearestDistanceSquared = Double.POSITIVE_INFINITY;
|
||||
double nearestAlongReach = 0.0;
|
||||
for (RiverReach reach : indexedReaches(x, z)) {
|
||||
ClosestPoint closest = closestPoint(reach.polyline(), x, z);
|
||||
double outerRadius = reach.width() * 0.5 + reach.bankWidth();
|
||||
if (closest.distanceSquared() > outerRadius * outerRadius) {
|
||||
continue;
|
||||
}
|
||||
if (closest.distanceSquared() < nearestDistanceSquared
|
||||
|| (closest.distanceSquared() == nearestDistanceSquared
|
||||
&& nearestReach != null
|
||||
&& reach.id().compareTo(nearestReach.id()) < 0)) {
|
||||
nearestReach = reach;
|
||||
nearestDistanceSquared = closest.distanceSquared();
|
||||
nearestAlongReach = closest.alongReach();
|
||||
}
|
||||
}
|
||||
if (nearestReach == null) {
|
||||
return RiverSample.none();
|
||||
}
|
||||
|
||||
return createSample(nearestReach, nearestDistanceSquared, nearestAlongReach);
|
||||
}
|
||||
|
||||
public RiverSample sampleFootprint(
|
||||
double queryMinimumX,
|
||||
double queryMinimumZ,
|
||||
double queryMaximumX,
|
||||
double queryMaximumZ
|
||||
) {
|
||||
if (!Double.isFinite(queryMinimumX) || !Double.isFinite(queryMinimumZ)
|
||||
|| !Double.isFinite(queryMaximumX) || !Double.isFinite(queryMaximumZ)
|
||||
|| queryMinimumX > queryMaximumX || queryMinimumZ > queryMaximumZ) {
|
||||
throw new IllegalArgumentException("River footprint bounds must be finite and ordered");
|
||||
}
|
||||
RiverReach nearestReach = null;
|
||||
double nearestDistanceSquared = Double.POSITIVE_INFINITY;
|
||||
double nearestAlongReach = 0.0;
|
||||
for (RiverReach reach : indexedReachesInclusive(
|
||||
queryMinimumX,
|
||||
queryMinimumZ,
|
||||
queryMaximumX,
|
||||
queryMaximumZ
|
||||
)) {
|
||||
ClosestPoint closest = closestPoint(
|
||||
reach.polyline(),
|
||||
queryMinimumX,
|
||||
queryMinimumZ,
|
||||
queryMaximumX,
|
||||
queryMaximumZ
|
||||
);
|
||||
double outerRadius = reach.width() * 0.5 + reach.bankWidth();
|
||||
if (closest.distanceSquared() > outerRadius * outerRadius) {
|
||||
continue;
|
||||
}
|
||||
if (closest.distanceSquared() < nearestDistanceSquared
|
||||
|| (closest.distanceSquared() == nearestDistanceSquared
|
||||
&& nearestReach != null
|
||||
&& reach.id().compareTo(nearestReach.id()) < 0)) {
|
||||
nearestReach = reach;
|
||||
nearestDistanceSquared = closest.distanceSquared();
|
||||
nearestAlongReach = closest.alongReach();
|
||||
}
|
||||
}
|
||||
if (nearestReach == null) {
|
||||
return RiverSample.none();
|
||||
}
|
||||
|
||||
return createSample(nearestReach, nearestDistanceSquared, nearestAlongReach);
|
||||
}
|
||||
|
||||
private static RiverSample createSample(
|
||||
RiverReach nearestReach,
|
||||
double nearestDistanceSquared,
|
||||
double nearestAlongReach
|
||||
) {
|
||||
|
||||
double distance = StrictMath.sqrt(nearestDistanceSquared);
|
||||
double channelRadius = nearestReach.width() * 0.5;
|
||||
RiverSection section = section(nearestReach, distance, channelRadius);
|
||||
double carveWeight = carveWeight(distance, channelRadius, nearestReach.bankWidth());
|
||||
return new RiverSample(
|
||||
true,
|
||||
nearestReach.state(),
|
||||
section,
|
||||
distance,
|
||||
nearestAlongReach,
|
||||
carveWeight,
|
||||
nearestReach.flow(),
|
||||
nearestReach.order(),
|
||||
nearestReach.width(),
|
||||
nearestReach.bankWidth(),
|
||||
nearestReach.depth(),
|
||||
nearestReach.terminal(),
|
||||
nearestReach.id()
|
||||
);
|
||||
}
|
||||
|
||||
private static RiverSection section(RiverReach reach, double distance, double channelRadius) {
|
||||
if (distance <= channelRadius) {
|
||||
if (reach.state() == RiverRouteState.DRY) {
|
||||
return RiverSection.DRY_CHANNEL;
|
||||
}
|
||||
return reach.mouth() ? RiverSection.MOUTH : RiverSection.CHANNEL;
|
||||
}
|
||||
return reach.state() == RiverRouteState.DRY ? RiverSection.DRY_BANK : RiverSection.BANK;
|
||||
}
|
||||
|
||||
private void addAnchors(
|
||||
RiverReach reach,
|
||||
double spacing,
|
||||
long salt,
|
||||
double queryMinimumX,
|
||||
double queryMinimumZ,
|
||||
double queryMaximumX,
|
||||
double queryMaximumZ,
|
||||
List<RiverAnchor> anchors
|
||||
) {
|
||||
double length = reach.polyline().length();
|
||||
double firstDistance = unit(RiverNetwork.mix(reach.id().stableId() ^ salt)) * spacing;
|
||||
int index = 0;
|
||||
for (double distance = firstDistance; distance < length; distance += spacing) {
|
||||
Position position = positionAt(reach.polyline(), distance);
|
||||
if (position.x() >= minimumX && position.x() < maximumX
|
||||
&& position.z() >= minimumZ && position.z() < maximumZ
|
||||
&& position.x() >= queryMinimumX && position.x() < queryMaximumX
|
||||
&& position.z() >= queryMinimumZ && position.z() < queryMaximumZ) {
|
||||
long stableId = RiverNetwork.mix(
|
||||
reach.id().stableId() ^ salt ^ (long) index * 0x9E3779B97F4A7C15L
|
||||
);
|
||||
anchors.add(new RiverAnchor(
|
||||
reach.id(),
|
||||
index,
|
||||
stableId,
|
||||
spacing,
|
||||
salt,
|
||||
position.x(),
|
||||
position.z(),
|
||||
position.alongReach(),
|
||||
reach.state(),
|
||||
reach.flow(),
|
||||
reach.order()
|
||||
));
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
private static Position positionAt(RiverPolyline polyline, double targetDistance) {
|
||||
double traversed = 0.0;
|
||||
for (int point = 0; point < polyline.size() - 1; point++) {
|
||||
double startX = polyline.x(point);
|
||||
double startZ = polyline.z(point);
|
||||
double deltaX = polyline.x(point + 1) - startX;
|
||||
double deltaZ = polyline.z(point + 1) - startZ;
|
||||
double segmentLength = StrictMath.hypot(deltaX, deltaZ);
|
||||
if (targetDistance <= traversed + segmentLength || point == polyline.size() - 2) {
|
||||
double t = segmentLength == 0.0 ? 0.0 : (targetDistance - traversed) / segmentLength;
|
||||
t = StrictMath.max(0.0, StrictMath.min(1.0, t));
|
||||
double alongReach = polyline.length() == 0.0 ? 0.0 : targetDistance / polyline.length();
|
||||
return new Position(startX + deltaX * t, startZ + deltaZ * t, alongReach);
|
||||
}
|
||||
traversed += segmentLength;
|
||||
}
|
||||
return new Position(
|
||||
polyline.x(polyline.size() - 1),
|
||||
polyline.z(polyline.size() - 1),
|
||||
1.0
|
||||
);
|
||||
}
|
||||
|
||||
private static double unit(long hash) {
|
||||
return (hash >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
private static double carveWeight(double distance, double channelRadius, double bankWidth) {
|
||||
if (distance <= channelRadius || bankWidth == 0.0) {
|
||||
return 1.0;
|
||||
}
|
||||
double t = StrictMath.min(1.0, (distance - channelRadius) / bankWidth);
|
||||
double smooth = t * t * (3.0 - 2.0 * t);
|
||||
return 1.0 - smooth;
|
||||
}
|
||||
|
||||
private static ClosestPoint closestPoint(RiverPolyline polyline, double x, double z) {
|
||||
double nearest = Double.POSITIVE_INFINITY;
|
||||
double nearestAlong = 0.0;
|
||||
for (int point = 0; point < polyline.size() - 1; point++) {
|
||||
SegmentPoint segmentPoint = segmentPoint(
|
||||
polyline.x(point),
|
||||
polyline.z(point),
|
||||
polyline.x(point + 1),
|
||||
polyline.z(point + 1),
|
||||
x,
|
||||
z
|
||||
);
|
||||
if (segmentPoint.distanceSquared() < nearest) {
|
||||
nearest = segmentPoint.distanceSquared();
|
||||
double segmentLength = polyline.cumulativeLength(point + 1) - polyline.cumulativeLength(point);
|
||||
double alongLength = polyline.cumulativeLength(point) + segmentLength * segmentPoint.t();
|
||||
nearestAlong = polyline.length() == 0.0 ? 0.0 : alongLength / polyline.length();
|
||||
}
|
||||
}
|
||||
return new ClosestPoint(nearest, nearestAlong);
|
||||
}
|
||||
|
||||
private static ClosestPoint closestPoint(
|
||||
RiverPolyline polyline,
|
||||
double minimumX,
|
||||
double minimumZ,
|
||||
double maximumX,
|
||||
double maximumZ
|
||||
) {
|
||||
double nearest = Double.POSITIVE_INFINITY;
|
||||
double nearestAlong = 0.0;
|
||||
for (int point = 0; point < polyline.size() - 1; point++) {
|
||||
SegmentPoint segmentPoint = segmentRectanglePoint(
|
||||
polyline.x(point),
|
||||
polyline.z(point),
|
||||
polyline.x(point + 1),
|
||||
polyline.z(point + 1),
|
||||
minimumX,
|
||||
minimumZ,
|
||||
maximumX,
|
||||
maximumZ
|
||||
);
|
||||
if (segmentPoint.distanceSquared() < nearest) {
|
||||
nearest = segmentPoint.distanceSquared();
|
||||
double segmentLength = polyline.cumulativeLength(point + 1) - polyline.cumulativeLength(point);
|
||||
double alongLength = polyline.cumulativeLength(point) + segmentLength * segmentPoint.t();
|
||||
nearestAlong = polyline.length() == 0.0 ? 0.0 : alongLength / polyline.length();
|
||||
}
|
||||
}
|
||||
return new ClosestPoint(nearest, nearestAlong);
|
||||
}
|
||||
|
||||
private static SegmentPoint segmentPoint(
|
||||
double startX,
|
||||
double startZ,
|
||||
double endX,
|
||||
double endZ,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
double deltaX = endX - startX;
|
||||
double deltaZ = endZ - startZ;
|
||||
double lengthSquared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
if (lengthSquared == 0.0) {
|
||||
return new SegmentPoint(squared(x - startX) + squared(z - startZ), 0.0);
|
||||
}
|
||||
double projection = ((x - startX) * deltaX + (z - startZ) * deltaZ) / lengthSquared;
|
||||
double t = StrictMath.max(0.0, StrictMath.min(1.0, projection));
|
||||
double nearestX = startX + deltaX * t;
|
||||
double nearestZ = startZ + deltaZ * t;
|
||||
return new SegmentPoint(squared(x - nearestX) + squared(z - nearestZ), t);
|
||||
}
|
||||
|
||||
private static SegmentPoint segmentRectanglePoint(
|
||||
double startX,
|
||||
double startZ,
|
||||
double endX,
|
||||
double endZ,
|
||||
double minimumX,
|
||||
double minimumZ,
|
||||
double maximumX,
|
||||
double maximumZ
|
||||
) {
|
||||
double intersectionPosition = segmentRectangleIntersectionPosition(
|
||||
startX,
|
||||
startZ,
|
||||
endX,
|
||||
endZ,
|
||||
minimumX,
|
||||
minimumZ,
|
||||
maximumX,
|
||||
maximumZ
|
||||
);
|
||||
if (!Double.isNaN(intersectionPosition)) {
|
||||
return new SegmentPoint(0.0, intersectionPosition);
|
||||
}
|
||||
|
||||
double nearestDistanceSquared = pointRectangleDistanceSquared(
|
||||
startX,
|
||||
startZ,
|
||||
minimumX,
|
||||
minimumZ,
|
||||
maximumX,
|
||||
maximumZ
|
||||
);
|
||||
double nearestPosition = 0.0;
|
||||
double endDistanceSquared = pointRectangleDistanceSquared(
|
||||
endX,
|
||||
endZ,
|
||||
minimumX,
|
||||
minimumZ,
|
||||
maximumX,
|
||||
maximumZ
|
||||
);
|
||||
if (endDistanceSquared < nearestDistanceSquared) {
|
||||
nearestDistanceSquared = endDistanceSquared;
|
||||
nearestPosition = 1.0;
|
||||
}
|
||||
|
||||
SegmentPoint corner = segmentPoint(startX, startZ, endX, endZ, minimumX, minimumZ);
|
||||
if (corner.distanceSquared() < nearestDistanceSquared) {
|
||||
nearestDistanceSquared = corner.distanceSquared();
|
||||
nearestPosition = corner.t();
|
||||
}
|
||||
corner = segmentPoint(startX, startZ, endX, endZ, minimumX, maximumZ);
|
||||
if (corner.distanceSquared() < nearestDistanceSquared) {
|
||||
nearestDistanceSquared = corner.distanceSquared();
|
||||
nearestPosition = corner.t();
|
||||
}
|
||||
corner = segmentPoint(startX, startZ, endX, endZ, maximumX, minimumZ);
|
||||
if (corner.distanceSquared() < nearestDistanceSquared) {
|
||||
nearestDistanceSquared = corner.distanceSquared();
|
||||
nearestPosition = corner.t();
|
||||
}
|
||||
corner = segmentPoint(startX, startZ, endX, endZ, maximumX, maximumZ);
|
||||
if (corner.distanceSquared() < nearestDistanceSquared) {
|
||||
nearestDistanceSquared = corner.distanceSquared();
|
||||
nearestPosition = corner.t();
|
||||
}
|
||||
return new SegmentPoint(nearestDistanceSquared, nearestPosition);
|
||||
}
|
||||
|
||||
private static double segmentRectangleIntersectionPosition(
|
||||
double startX,
|
||||
double startZ,
|
||||
double endX,
|
||||
double endZ,
|
||||
double minimumX,
|
||||
double minimumZ,
|
||||
double maximumX,
|
||||
double maximumZ
|
||||
) {
|
||||
double minimumPosition = 0.0;
|
||||
double maximumPosition = 1.0;
|
||||
double deltaX = endX - startX;
|
||||
if (deltaX == 0.0) {
|
||||
if (startX < minimumX || startX > maximumX) {
|
||||
return Double.NaN;
|
||||
}
|
||||
} else {
|
||||
double first = (minimumX - startX) / deltaX;
|
||||
double second = (maximumX - startX) / deltaX;
|
||||
minimumPosition = StrictMath.max(minimumPosition, StrictMath.min(first, second));
|
||||
maximumPosition = StrictMath.min(maximumPosition, StrictMath.max(first, second));
|
||||
if (minimumPosition > maximumPosition) {
|
||||
return Double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
double deltaZ = endZ - startZ;
|
||||
if (deltaZ == 0.0) {
|
||||
if (startZ < minimumZ || startZ > maximumZ) {
|
||||
return Double.NaN;
|
||||
}
|
||||
} else {
|
||||
double first = (minimumZ - startZ) / deltaZ;
|
||||
double second = (maximumZ - startZ) / deltaZ;
|
||||
minimumPosition = StrictMath.max(minimumPosition, StrictMath.min(first, second));
|
||||
maximumPosition = StrictMath.min(maximumPosition, StrictMath.max(first, second));
|
||||
if (minimumPosition > maximumPosition) {
|
||||
return Double.NaN;
|
||||
}
|
||||
}
|
||||
return minimumPosition;
|
||||
}
|
||||
|
||||
private static double pointRectangleDistanceSquared(
|
||||
double x,
|
||||
double z,
|
||||
double minimumX,
|
||||
double minimumZ,
|
||||
double maximumX,
|
||||
double maximumZ
|
||||
) {
|
||||
double deltaX = x < minimumX ? minimumX - x : StrictMath.max(0.0, x - maximumX);
|
||||
double deltaZ = z < minimumZ ? minimumZ - z : StrictMath.max(0.0, z - maximumZ);
|
||||
return squared(deltaX) + squared(deltaZ);
|
||||
}
|
||||
|
||||
private static double squared(double value) {
|
||||
return value * value;
|
||||
}
|
||||
|
||||
private static Map<Long, List<RiverReach>> createSpatialIndex(List<RiverReach> reaches) {
|
||||
HashMap<Long, Set<RiverReach>> mutable = new HashMap<>();
|
||||
for (RiverReach reach : reaches) {
|
||||
double radius = reach.width() * 0.5 + reach.bankWidth();
|
||||
RiverPolyline polyline = reach.polyline();
|
||||
for (int point = 0; point < polyline.size() - 1; point++) {
|
||||
int minimumBucketX = bucket(StrictMath.min(polyline.x(point), polyline.x(point + 1)) - radius);
|
||||
int maximumBucketX = bucket(StrictMath.max(polyline.x(point), polyline.x(point + 1)) + radius);
|
||||
int minimumBucketZ = bucket(StrictMath.min(polyline.z(point), polyline.z(point + 1)) - radius);
|
||||
int maximumBucketZ = bucket(StrictMath.max(polyline.z(point), polyline.z(point + 1)) + radius);
|
||||
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
|
||||
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
|
||||
mutable.computeIfAbsent(bucketKey(bucketX, bucketZ), ignored -> new LinkedHashSet<>()).add(reach);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
HashMap<Long, List<RiverReach>> immutable = new HashMap<>(mutable.size());
|
||||
for (Map.Entry<Long, Set<RiverReach>> entry : mutable.entrySet()) {
|
||||
immutable.put(entry.getKey(), List.copyOf(entry.getValue()));
|
||||
}
|
||||
return Map.copyOf(immutable);
|
||||
}
|
||||
|
||||
private static Map<RiverEdgeId, RiverReach> indexById(List<RiverReach> reaches) {
|
||||
HashMap<RiverEdgeId, RiverReach> indexed = new HashMap<>(reaches.size());
|
||||
for (RiverReach reach : reaches) {
|
||||
RiverReach previous = indexed.put(reach.id(), reach);
|
||||
if (previous != null) {
|
||||
throw new IllegalArgumentException("River tile cannot contain duplicate reach IDs");
|
||||
}
|
||||
}
|
||||
return Map.copyOf(indexed);
|
||||
}
|
||||
|
||||
private List<RiverReach> indexedReaches(double x, double z) {
|
||||
List<RiverReach> indexed = spatialIndex.get(bucketKey(bucket(x), bucket(z)));
|
||||
return indexed == null ? List.of() : indexed;
|
||||
}
|
||||
|
||||
private List<RiverReach> indexedReaches(
|
||||
double queryMinimumX,
|
||||
double queryMinimumZ,
|
||||
double queryMaximumX,
|
||||
double queryMaximumZ
|
||||
) {
|
||||
LinkedHashSet<RiverReach> indexed = new LinkedHashSet<>();
|
||||
int minimumBucketX = bucket(queryMinimumX);
|
||||
int maximumBucketX = bucket(StrictMath.nextDown(queryMaximumX));
|
||||
int minimumBucketZ = bucket(queryMinimumZ);
|
||||
int maximumBucketZ = bucket(StrictMath.nextDown(queryMaximumZ));
|
||||
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
|
||||
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
|
||||
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(bucketX, bucketZ));
|
||||
if (bucketReaches != null) {
|
||||
indexed.addAll(bucketReaches);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(indexed);
|
||||
}
|
||||
|
||||
private List<RiverReach> indexedReachesInclusive(
|
||||
double queryMinimumX,
|
||||
double queryMinimumZ,
|
||||
double queryMaximumX,
|
||||
double queryMaximumZ
|
||||
) {
|
||||
int minimumBucketX = bucket(queryMinimumX);
|
||||
int maximumBucketX = bucket(queryMaximumX);
|
||||
int minimumBucketZ = bucket(queryMinimumZ);
|
||||
int maximumBucketZ = bucket(queryMaximumZ);
|
||||
if (spatialIndex.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
|
||||
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(minimumBucketX, minimumBucketZ));
|
||||
return bucketReaches == null ? List.of() : bucketReaches;
|
||||
}
|
||||
long bucketWidth = (long) maximumBucketX - minimumBucketX + 1L;
|
||||
long bucketDepth = (long) maximumBucketZ - minimumBucketZ + 1L;
|
||||
if (bucketWidth > spatialIndex.size() / bucketDepth
|
||||
|| bucketWidth * bucketDepth >= spatialIndex.size()) {
|
||||
return reaches;
|
||||
}
|
||||
LinkedHashSet<RiverReach> indexed = new LinkedHashSet<>();
|
||||
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
|
||||
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
|
||||
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(bucketX, bucketZ));
|
||||
if (bucketReaches != null) {
|
||||
indexed.addAll(bucketReaches);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(indexed);
|
||||
}
|
||||
|
||||
private static int bucket(double coordinate) {
|
||||
return (int) StrictMath.floor(coordinate / BUCKET_SIZE);
|
||||
}
|
||||
|
||||
private static long bucketKey(int bucketX, int bucketZ) {
|
||||
return ((long) bucketX << 32) ^ (bucketZ & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private record Position(double x, double z, double alongReach) {
|
||||
}
|
||||
|
||||
private record ClosestPoint(double distanceSquared, double alongReach) {
|
||||
}
|
||||
|
||||
private record SegmentPoint(double distanceSquared, double t) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
public final class RiverTileCache implements AutoCloseable {
|
||||
private final Object lock;
|
||||
private final int maxCompletedEntries;
|
||||
private final Map<TileKey, Entry> entries;
|
||||
private final LinkedHashMap<TileKey, Entry> completedEntries;
|
||||
private TileBuilder builder;
|
||||
private boolean closed;
|
||||
|
||||
public RiverTileCache(int maxCompletedEntries, TileBuilder builder) {
|
||||
if (maxCompletedEntries < 1) {
|
||||
throw new IllegalArgumentException("River tile cache capacity must be positive");
|
||||
}
|
||||
this.maxCompletedEntries = maxCompletedEntries;
|
||||
this.builder = Objects.requireNonNull(builder);
|
||||
lock = new Object();
|
||||
entries = new HashMap<>(maxCompletedEntries);
|
||||
completedEntries = new LinkedHashMap<>(maxCompletedEntries, 0.75f, true);
|
||||
}
|
||||
|
||||
public RiverTile get(int tileX, int tileZ) {
|
||||
TileKey key = new TileKey(tileX, tileZ);
|
||||
Entry entry;
|
||||
TileBuilder activeBuilder;
|
||||
boolean build;
|
||||
synchronized (lock) {
|
||||
requireOpen();
|
||||
entry = entries.get(key);
|
||||
if (entry == null) {
|
||||
entry = new Entry();
|
||||
entries.put(key, entry);
|
||||
activeBuilder = builder;
|
||||
build = true;
|
||||
} else {
|
||||
if (entry.completed) {
|
||||
completedEntries.get(key);
|
||||
}
|
||||
activeBuilder = null;
|
||||
build = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (build) {
|
||||
build(key, entry, activeBuilder);
|
||||
}
|
||||
return await(entry.future, key);
|
||||
}
|
||||
|
||||
public int completedSize() {
|
||||
synchronized (lock) {
|
||||
return completedEntries.size();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
synchronized (lock) {
|
||||
return closed;
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
List<CompletableFuture<RiverTile>> invalidated;
|
||||
synchronized (lock) {
|
||||
requireOpen();
|
||||
invalidated = clearLocked();
|
||||
}
|
||||
invalidate(invalidated, "River tile cache was cleared");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
List<CompletableFuture<RiverTile>> invalidated;
|
||||
synchronized (lock) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
builder = null;
|
||||
invalidated = clearLocked();
|
||||
}
|
||||
invalidate(invalidated, "River tile cache was closed");
|
||||
}
|
||||
|
||||
private void build(TileKey key, Entry entry, TileBuilder activeBuilder) {
|
||||
try {
|
||||
RiverTile tile = Objects.requireNonNull(
|
||||
activeBuilder.build(key.tileX(), key.tileZ()),
|
||||
"River tile builder returned null"
|
||||
);
|
||||
if (tile.tileX() != key.tileX() || tile.tileZ() != key.tileZ()) {
|
||||
throw new IllegalStateException(
|
||||
"River tile builder returned " + tile.tileX() + "," + tile.tileZ()
|
||||
+ " for " + key.tileX() + "," + key.tileZ()
|
||||
);
|
||||
}
|
||||
publishCompleted(key, entry, tile);
|
||||
} catch (Throwable failure) {
|
||||
removeFailed(key, entry);
|
||||
entry.future.completeExceptionally(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void publishCompleted(TileKey key, Entry entry, RiverTile tile) {
|
||||
synchronized (lock) {
|
||||
if (closed || entries.get(key) != entry) {
|
||||
entry.future.completeExceptionally(new IllegalStateException(
|
||||
closed ? "River tile cache was closed" : "River tile cache entry was cleared"
|
||||
));
|
||||
return;
|
||||
}
|
||||
entry.completed = true;
|
||||
completedEntries.put(key, entry);
|
||||
while (completedEntries.size() > maxCompletedEntries) {
|
||||
Map.Entry<TileKey, Entry> eldest = completedEntries.entrySet().iterator().next();
|
||||
completedEntries.remove(eldest.getKey());
|
||||
entries.remove(eldest.getKey(), eldest.getValue());
|
||||
}
|
||||
entry.future.complete(tile);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeFailed(TileKey key, Entry entry) {
|
||||
synchronized (lock) {
|
||||
entries.remove(key, entry);
|
||||
completedEntries.remove(key, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private List<CompletableFuture<RiverTile>> clearLocked() {
|
||||
ArrayList<CompletableFuture<RiverTile>> invalidated = new ArrayList<>(entries.size());
|
||||
for (Entry entry : entries.values()) {
|
||||
if (!entry.future.isDone()) {
|
||||
invalidated.add(entry.future);
|
||||
}
|
||||
}
|
||||
entries.clear();
|
||||
completedEntries.clear();
|
||||
return invalidated;
|
||||
}
|
||||
|
||||
private void requireOpen() {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("River tile cache is closed");
|
||||
}
|
||||
}
|
||||
|
||||
private static RiverTile await(CompletableFuture<RiverTile> future, TileKey key) {
|
||||
try {
|
||||
return future.join();
|
||||
} catch (CompletionException failure) {
|
||||
Throwable cause = failure.getCause();
|
||||
if (cause instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
if (cause instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (cause instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"Failed to build river tile " + key.tileX() + "," + key.tileZ(),
|
||||
cause
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void invalidate(List<CompletableFuture<RiverTile>> futures, String message) {
|
||||
for (CompletableFuture<RiverTile> future : futures) {
|
||||
future.completeExceptionally(new IllegalStateException(message));
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TileBuilder {
|
||||
RiverTile build(int tileX, int tileZ) throws Exception;
|
||||
}
|
||||
|
||||
private record TileKey(int tileX, int tileZ) {
|
||||
}
|
||||
|
||||
private static final class Entry {
|
||||
private final CompletableFuture<RiverTile> future;
|
||||
private boolean completed;
|
||||
|
||||
private Entry() {
|
||||
future = new CompletableFuture<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class RiverTopologyComplexity {
|
||||
public static final long MAXIMUM_SOURCE_WINDOW_CELLS = 65_536L;
|
||||
public static final long MAXIMUM_ROUTE_SCAN_STEPS = 65_536L;
|
||||
public static final long MAXIMUM_BUCKET_WRITES_PER_REACH = 1_048_576L;
|
||||
private static final int SPATIAL_BUCKET_SIZE = 64;
|
||||
|
||||
private RiverTopologyComplexity() {
|
||||
}
|
||||
|
||||
public static Estimate estimate(
|
||||
int cellSize,
|
||||
int tileCells,
|
||||
double siteJitter,
|
||||
int maxRouteReaches,
|
||||
double maximumReachRadius,
|
||||
double meanderStrength,
|
||||
int meanderSubdivisions
|
||||
) {
|
||||
double maximumEdgeAxisDelta = cellSize * (1D + siteJitter);
|
||||
double maximumEdgeLength = StrictMath.sqrt(2D) * maximumEdgeAxisDelta;
|
||||
double maximumMeander = StrictMath.min(meanderStrength, maximumEdgeLength * 0.35D);
|
||||
long geometryPaddingCells = 1L + ceilToLong(
|
||||
(maximumReachRadius + maximumMeander) / cellSize
|
||||
);
|
||||
long targetWindowAxis = saturatedAdd(tileCells, saturatedMultiply(2L, geometryPaddingCells));
|
||||
long sourceWindowAxis = saturatedAdd(
|
||||
targetWindowAxis,
|
||||
saturatedMultiply(2L, maxRouteReaches)
|
||||
);
|
||||
long sourceWindowCells = saturatedMultiply(sourceWindowAxis, sourceWindowAxis);
|
||||
long maximumRouteScanSteps = saturatedMultiply(sourceWindowCells, maxRouteReaches);
|
||||
double maximumSegmentSpan = maximumEdgeAxisDelta
|
||||
+ maximumMeander * 2D
|
||||
+ maximumReachRadius * 2D;
|
||||
long maximumSegmentBucketAxis = saturatedAdd(
|
||||
ceilToLong(maximumSegmentSpan / SPATIAL_BUCKET_SIZE),
|
||||
1L
|
||||
);
|
||||
long maximumSegmentBucketCount = saturatedMultiply(
|
||||
maximumSegmentBucketAxis,
|
||||
maximumSegmentBucketAxis
|
||||
);
|
||||
long maximumBucketWritesPerReach = saturatedMultiply(
|
||||
maximumSegmentBucketCount,
|
||||
meanderSubdivisions
|
||||
);
|
||||
return new Estimate(
|
||||
geometryPaddingCells,
|
||||
sourceWindowAxis,
|
||||
sourceWindowCells,
|
||||
maximumRouteScanSteps,
|
||||
maximumSegmentBucketAxis,
|
||||
maximumBucketWritesPerReach
|
||||
);
|
||||
}
|
||||
|
||||
public static void requireSafe(
|
||||
int cellSize,
|
||||
int tileCells,
|
||||
double siteJitter,
|
||||
int maxRouteReaches,
|
||||
double maximumReachRadius,
|
||||
double meanderStrength,
|
||||
int meanderSubdivisions
|
||||
) {
|
||||
Estimate estimate = estimate(
|
||||
cellSize,
|
||||
tileCells,
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
maximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
);
|
||||
List<String> violations = estimate.violations();
|
||||
if (!violations.isEmpty()) {
|
||||
throw new IllegalArgumentException(String.join(" ", violations));
|
||||
}
|
||||
}
|
||||
|
||||
private static long ceilToLong(double value) {
|
||||
if (!Double.isFinite(value) || value >= Long.MAX_VALUE) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
if (value <= 0D) {
|
||||
return 0L;
|
||||
}
|
||||
return (long) StrictMath.ceil(value);
|
||||
}
|
||||
|
||||
private static long saturatedAdd(long first, long second) {
|
||||
if (first > Long.MAX_VALUE - second) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
return first + second;
|
||||
}
|
||||
|
||||
private static long saturatedMultiply(long first, long second) {
|
||||
if (first == 0L || second == 0L) {
|
||||
return 0L;
|
||||
}
|
||||
if (first > Long.MAX_VALUE / second) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
return first * second;
|
||||
}
|
||||
|
||||
public record Estimate(
|
||||
long geometryPaddingCells,
|
||||
long sourceWindowAxis,
|
||||
long sourceWindowCells,
|
||||
long maximumRouteScanSteps,
|
||||
long maximumSegmentBucketAxis,
|
||||
long maximumBucketWritesPerReach
|
||||
) {
|
||||
public boolean safe() {
|
||||
return violations().isEmpty();
|
||||
}
|
||||
|
||||
public List<String> violations() {
|
||||
ArrayList<String> violations = new ArrayList<>(3);
|
||||
if (sourceWindowCells > MAXIMUM_SOURCE_WINDOW_CELLS) {
|
||||
violations.add("River topology source window requires " + sourceWindowCells
|
||||
+ " cells (" + sourceWindowAxis + " per axis), above the safe limit of "
|
||||
+ MAXIMUM_SOURCE_WINDOW_CELLS
|
||||
+ "; increase cellSize or reduce tileCells, maxRouteReaches, channel width, or bank width.");
|
||||
}
|
||||
if (maximumRouteScanSteps > MAXIMUM_ROUTE_SCAN_STEPS) {
|
||||
violations.add("River topology route scan permits " + maximumRouteScanSteps
|
||||
+ " source-to-reach steps, above the safe limit of " + MAXIMUM_ROUTE_SCAN_STEPS
|
||||
+ "; reduce maxRouteReaches, tileCells, channel width, or bank width.");
|
||||
}
|
||||
if (maximumBucketWritesPerReach > MAXIMUM_BUCKET_WRITES_PER_REACH) {
|
||||
violations.add("River topology spatial index may require " + maximumBucketWritesPerReach
|
||||
+ " bucket writes for one reach (" + maximumSegmentBucketAxis
|
||||
+ " buckets per segment axis), above the safe limit of "
|
||||
+ MAXIMUM_BUCKET_WRITES_PER_REACH
|
||||
+ "; reduce channel width, bank width, orderWidthFactor, meanderStrength, or meanderSubdivisions.");
|
||||
}
|
||||
return List.copyOf(violations);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
|
||||
public record CavePosition(int x, int y, int z) {
|
||||
public CavePosition offset(int dx, int dy, int dz) {
|
||||
return new CavePosition(x + dx, y + dy, z + dz);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
|
||||
public enum CaveVoxel {
|
||||
SOLID,
|
||||
CAVE_AIR,
|
||||
COMPATIBLE_FLUID,
|
||||
LAVA,
|
||||
INCOMPATIBLE_FLUID
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record CaveVoxelPrecondition(CaveVoxel voxel, boolean openToSurface) {
|
||||
public CaveVoxelPrecondition {
|
||||
Objects.requireNonNull(voxel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
|
||||
public interface CaveVoxelView {
|
||||
boolean isInWorld(CavePosition position);
|
||||
|
||||
CaveVoxel voxelAt(CavePosition position);
|
||||
|
||||
boolean isOpenToSurface(CavePosition position);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
|
||||
public enum RiverCaveAction {
|
||||
WET_SOURCE,
|
||||
FALLING_WATER,
|
||||
DRY_AIR,
|
||||
SEAL_GUARD
|
||||
}
|
||||
+941
@@ -0,0 +1,941 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
|
||||
public final class RiverCaveContainmentPlanner {
|
||||
private static final List<CavePosition> DIRECTIONS = List.of(
|
||||
new CavePosition(1, 0, 0),
|
||||
new CavePosition(-1, 0, 0),
|
||||
new CavePosition(0, 1, 0),
|
||||
new CavePosition(0, -1, 0),
|
||||
new CavePosition(0, 0, 1),
|
||||
new CavePosition(0, 0, -1)
|
||||
);
|
||||
private static final Comparator<RiverCaveSource> SOURCE_PRIORITY = Comparator
|
||||
.comparingInt(RiverCaveSource::waterHeadY)
|
||||
.reversed()
|
||||
.thenComparingLong(RiverCaveSource::sourceId)
|
||||
.thenComparingInt(source -> source.entry().x())
|
||||
.thenComparingInt(source -> source.entry().y())
|
||||
.thenComparingInt(source -> source.entry().z())
|
||||
.thenComparingInt(source -> source.target().x())
|
||||
.thenComparingInt(source -> source.target().y())
|
||||
.thenComparingInt(source -> source.target().z())
|
||||
.thenComparing(RiverCaveSource::mode);
|
||||
|
||||
public RiverCavePlan plan(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings
|
||||
) {
|
||||
Objects.requireNonNull(view);
|
||||
Objects.requireNonNull(source);
|
||||
Objects.requireNonNull(settings);
|
||||
|
||||
RiverCaveRejection sourceRejection = validateSource(source);
|
||||
if (sourceRejection != RiverCaveRejection.NONE) {
|
||||
return rejected(source, sourceRejection);
|
||||
}
|
||||
|
||||
PathResult throat = buildThroat(view, source, settings);
|
||||
if (throat.rejection() != RiverCaveRejection.NONE) {
|
||||
return rejected(source, throat.rejection());
|
||||
}
|
||||
|
||||
return switch (source.mode()) {
|
||||
case CLOSED_COMPONENT -> planClosedComponent(view, source, settings, throat.positions());
|
||||
case GENERATED_GROTTO -> planGeneratedGrotto(view, source, settings, throat.positions());
|
||||
case GROTTO_OR_CLOSED_COMPONENT -> planGrottoOrClosedComponent(
|
||||
view,
|
||||
source,
|
||||
settings,
|
||||
throat.positions()
|
||||
);
|
||||
case WATERFALL_POOL -> planWaterfallPool(
|
||||
view,
|
||||
source,
|
||||
settings,
|
||||
throat.positions()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
public RiverCavePlanningResult planAll(
|
||||
CaveVoxelView view,
|
||||
Collection<RiverCaveSource> sources,
|
||||
RiverCavePlannerSettings settings
|
||||
) {
|
||||
Objects.requireNonNull(view);
|
||||
Objects.requireNonNull(sources);
|
||||
Objects.requireNonNull(settings);
|
||||
|
||||
List<RiverCaveSource> orderedSources = new ArrayList<>(sources);
|
||||
orderedSources.sort(SOURCE_PRIORITY);
|
||||
List<RiverCavePlan> plans = new ArrayList<>(orderedSources.size());
|
||||
Map<CavePosition, RiverCaveAction> combinedActions = new LinkedHashMap<>();
|
||||
Map<CavePosition, RiverCaveSource> claimedBy = new HashMap<>();
|
||||
Map<CavePosition, CaveVoxelPrecondition> combinedPreconditions = new LinkedHashMap<>();
|
||||
|
||||
for (RiverCaveSource source : orderedSources) {
|
||||
RiverCavePlan candidate = plan(view, source, settings);
|
||||
if (!candidate.accepted()) {
|
||||
plans.add(candidate);
|
||||
continue;
|
||||
}
|
||||
OptionalLong winnerSourceId = findWinningSourceId(
|
||||
candidate.actions().keySet(),
|
||||
claimedBy
|
||||
);
|
||||
if (winnerSourceId.isPresent()) {
|
||||
plans.add(rejectedOverlap(source, winnerSourceId.getAsLong()));
|
||||
} else {
|
||||
plans.add(candidate);
|
||||
combinedActions.putAll(candidate.actions());
|
||||
combinedPreconditions.putAll(candidate.baselinePreconditions());
|
||||
}
|
||||
for (CavePosition position : candidate.actions().keySet()) {
|
||||
claimedBy.putIfAbsent(position, source);
|
||||
}
|
||||
}
|
||||
|
||||
return new RiverCavePlanningResult(plans, combinedActions, combinedPreconditions);
|
||||
}
|
||||
|
||||
private RiverCavePlan planGrottoOrClosedComponent(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
CaveVoxel targetVoxel = voxelAt(view, source.target());
|
||||
if (isFluidReachable(targetVoxel, settings)) {
|
||||
return planClosedComponent(view, source, settings, throat);
|
||||
}
|
||||
if (targetVoxel == CaveVoxel.LAVA) {
|
||||
return rejected(source, RiverCaveRejection.LAVA_CONTACT);
|
||||
}
|
||||
if (targetVoxel == CaveVoxel.INCOMPATIBLE_FLUID) {
|
||||
return rejected(source, RiverCaveRejection.INCOMPATIBLE_FLUID);
|
||||
}
|
||||
return planGeneratedGrotto(view, source, settings, throat);
|
||||
}
|
||||
|
||||
private RiverCavePlan planWaterfallPool(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
if (!view.isOpenToSurface(source.target())) {
|
||||
return planGrottoOrClosedComponent(view, source, settings, throat);
|
||||
}
|
||||
RiverCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat);
|
||||
if (dryThroatRejection != RiverCaveRejection.NONE) {
|
||||
return rejected(source, dryThroatRejection);
|
||||
}
|
||||
RiverCaveRejection shaftRejection = validateWaterfallShaft(view, source, settings, throat);
|
||||
if (shaftRejection != RiverCaveRejection.NONE) {
|
||||
return rejected(source, shaftRejection);
|
||||
}
|
||||
CaveVoxel targetVoxel = voxelAt(view, source.target());
|
||||
if (!isFluidReachable(targetVoxel, settings)) {
|
||||
return rejected(source, rejectionForTarget(targetVoxel, settings));
|
||||
}
|
||||
|
||||
Map<CavePosition, RiverCaveAction> actions = new HashMap<>();
|
||||
addThroatActions(actions, throat, source);
|
||||
addSealGuards(view, source, actions);
|
||||
return accepted(view, source, actions);
|
||||
}
|
||||
|
||||
private RiverCavePlan planClosedComponent(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
RiverCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat);
|
||||
if (dryThroatRejection != RiverCaveRejection.NONE) {
|
||||
return rejected(source, dryThroatRejection);
|
||||
}
|
||||
RiverCaveRejection waterfallRejection = validateWaterfallShaft(view, source, settings, throat);
|
||||
if (waterfallRejection != RiverCaveRejection.NONE) {
|
||||
return rejected(source, waterfallRejection);
|
||||
}
|
||||
|
||||
CaveVoxel targetVoxel = voxelAt(view, source.target());
|
||||
if (!isFluidReachable(targetVoxel, settings)) {
|
||||
return rejected(source, rejectionForTarget(targetVoxel, settings));
|
||||
}
|
||||
|
||||
ComponentResult component = resolveClosedComponent(view, source, settings, throat);
|
||||
if (component.rejection() != RiverCaveRejection.NONE) {
|
||||
return rejected(source, component.rejection());
|
||||
}
|
||||
|
||||
Map<CavePosition, RiverCaveAction> actions = new HashMap<>();
|
||||
addThroatActions(actions, throat, source);
|
||||
for (CavePosition position : component.positions()) {
|
||||
actions.put(position, RiverCaveAction.WET_SOURCE);
|
||||
}
|
||||
addSealGuards(view, source, actions);
|
||||
return accepted(view, source, actions);
|
||||
}
|
||||
|
||||
private RiverCaveRejection validateDryThroatContacts(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
Set<CavePosition> throatPositions = Set.copyOf(throat);
|
||||
for (CavePosition position : throat) {
|
||||
if (position.y() <= source.waterHeadY()) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition direction : DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (!view.isInWorld(neighbor)) {
|
||||
return RiverCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
CaveVoxel voxel = voxelAt(view, neighbor);
|
||||
if (voxel == CaveVoxel.LAVA) {
|
||||
return RiverCaveRejection.LAVA_CONTACT;
|
||||
}
|
||||
if (voxel == CaveVoxel.COMPATIBLE_FLUID) {
|
||||
return RiverCaveRejection.EXISTING_FLUID;
|
||||
}
|
||||
if (voxel == CaveVoxel.INCOMPATIBLE_FLUID) {
|
||||
return RiverCaveRejection.INCOMPATIBLE_FLUID;
|
||||
}
|
||||
}
|
||||
}
|
||||
return RiverCaveRejection.NONE;
|
||||
}
|
||||
|
||||
private RiverCaveRejection validateWaterfallShaft(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
if (source.mode() != RiverCaveMode.WATERFALL_POOL) {
|
||||
return RiverCaveRejection.NONE;
|
||||
}
|
||||
|
||||
Set<CavePosition> throatPositions = Set.copyOf(throat);
|
||||
for (CavePosition position : throat) {
|
||||
if (position.y() <= source.waterHeadY()) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition direction : DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (!view.isInWorld(neighbor)) {
|
||||
return RiverCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
RiverCaveRejection boundsRejection = validateBounds(source, settings, neighbor);
|
||||
if (boundsRejection != RiverCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
CaveVoxel voxel = voxelAt(view, neighbor);
|
||||
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
|
||||
if (hazard != RiverCaveRejection.NONE) {
|
||||
return hazard;
|
||||
}
|
||||
if (voxel != CaveVoxel.SOLID) {
|
||||
return RiverCaveRejection.WATERFALL_SHAFT_OPEN;
|
||||
}
|
||||
}
|
||||
}
|
||||
return RiverCaveRejection.NONE;
|
||||
}
|
||||
|
||||
private RiverCavePlan planGeneratedGrotto(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
GrottoResult grotto = buildGrotto(source, settings);
|
||||
if (grotto.rejection() != RiverCaveRejection.NONE) {
|
||||
return rejected(source, grotto.rejection());
|
||||
}
|
||||
Set<CavePosition> chamber = grotto.positions();
|
||||
|
||||
Set<CavePosition> carve = new HashSet<>(chamber.size() + throat.size());
|
||||
carve.addAll(chamber);
|
||||
carve.addAll(throat);
|
||||
RiverCaveRejection carveRejection = validateGeneratedCarve(view, source, settings, carve);
|
||||
if (carveRejection != RiverCaveRejection.NONE) {
|
||||
return rejected(source, carveRejection);
|
||||
}
|
||||
|
||||
BoundaryResult boundary = validateGeneratedBoundary(view, source, settings, carve);
|
||||
if (boundary.rejection() != RiverCaveRejection.NONE) {
|
||||
return rejected(source, boundary.rejection());
|
||||
}
|
||||
|
||||
Map<CavePosition, RiverCaveAction> actions = new HashMap<>();
|
||||
addChamberActions(actions, chamber, source.waterHeadY());
|
||||
addThroatActions(actions, throat, source);
|
||||
for (CavePosition position : boundary.sealGuards()) {
|
||||
actions.put(position, RiverCaveAction.SEAL_GUARD);
|
||||
}
|
||||
return accepted(view, source, actions);
|
||||
}
|
||||
|
||||
private ComponentResult resolveClosedComponent(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
Queue<CavePosition> queue = new ArrayDeque<>();
|
||||
Set<CavePosition> queued = new HashSet<>();
|
||||
Set<CavePosition> resolved = new HashSet<>();
|
||||
|
||||
queue.add(source.target());
|
||||
queued.add(source.target());
|
||||
RiverCaveRejection seedRejection = addThroatContacts(view, source, settings, throat, queue, queued);
|
||||
if (seedRejection != RiverCaveRejection.NONE) {
|
||||
return ComponentResult.rejected(seedRejection);
|
||||
}
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
CavePosition position = queue.remove();
|
||||
RiverCaveRejection positionRejection = validateReachablePosition(view, source, settings, position);
|
||||
if (positionRejection != RiverCaveRejection.NONE) {
|
||||
return ComponentResult.rejected(positionRejection);
|
||||
}
|
||||
if (!resolved.add(position)) {
|
||||
continue;
|
||||
}
|
||||
if (resolved.size() > settings.maxFloodVolume()) {
|
||||
return ComponentResult.rejected(RiverCaveRejection.VOLUME_LIMIT);
|
||||
}
|
||||
|
||||
for (CavePosition direction : DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
RiverCaveRejection neighborRejection = inspectReachableNeighbor(
|
||||
view,
|
||||
source,
|
||||
settings,
|
||||
neighbor,
|
||||
queue,
|
||||
queued
|
||||
);
|
||||
if (neighborRejection != RiverCaveRejection.NONE) {
|
||||
return ComponentResult.rejected(neighborRejection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ComponentResult.accepted(resolved);
|
||||
}
|
||||
|
||||
private RiverCaveRejection addThroatContacts(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
List<CavePosition> throat,
|
||||
Queue<CavePosition> queue,
|
||||
Set<CavePosition> queued
|
||||
) {
|
||||
for (CavePosition position : throat) {
|
||||
if (position.y() > source.waterHeadY()) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition direction : DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
RiverCaveRejection rejection = inspectReachableNeighbor(
|
||||
view,
|
||||
source,
|
||||
settings,
|
||||
neighbor,
|
||||
queue,
|
||||
queued
|
||||
);
|
||||
if (rejection != RiverCaveRejection.NONE) {
|
||||
return rejection;
|
||||
}
|
||||
}
|
||||
}
|
||||
return RiverCaveRejection.NONE;
|
||||
}
|
||||
|
||||
private RiverCaveRejection inspectReachableNeighbor(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
CavePosition position,
|
||||
Queue<CavePosition> queue,
|
||||
Set<CavePosition> queued
|
||||
) {
|
||||
if (position.y() > source.waterHeadY()) {
|
||||
return inspectAboveHeadNeighbor(view, source, position);
|
||||
}
|
||||
if (!view.isInWorld(position)) {
|
||||
return RiverCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
|
||||
CaveVoxel voxel = voxelAt(view, position);
|
||||
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
|
||||
if (hazard != RiverCaveRejection.NONE) {
|
||||
return hazard;
|
||||
}
|
||||
if (!isFluidReachable(voxel, settings)) {
|
||||
return RiverCaveRejection.NONE;
|
||||
}
|
||||
|
||||
RiverCaveRejection boundsRejection = validateBounds(source, settings, position);
|
||||
if (boundsRejection != RiverCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
if (queued.add(position)) {
|
||||
queue.add(position);
|
||||
}
|
||||
return RiverCaveRejection.NONE;
|
||||
}
|
||||
|
||||
private RiverCaveRejection inspectAboveHeadNeighbor(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
CavePosition position
|
||||
) {
|
||||
if (isInletOpening(source, position) || !view.isInWorld(position)) {
|
||||
return RiverCaveRejection.NONE;
|
||||
}
|
||||
CaveVoxel voxel = voxelAt(view, position);
|
||||
return switch (voxel) {
|
||||
case LAVA -> RiverCaveRejection.LAVA_CONTACT;
|
||||
case COMPATIBLE_FLUID -> RiverCaveRejection.EXISTING_FLUID;
|
||||
case INCOMPATIBLE_FLUID -> RiverCaveRejection.INCOMPATIBLE_FLUID;
|
||||
default -> RiverCaveRejection.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private RiverCaveRejection validateReachablePosition(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
CavePosition position
|
||||
) {
|
||||
if (!view.isInWorld(position)) {
|
||||
return RiverCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
RiverCaveRejection boundsRejection = validateBounds(source, settings, position);
|
||||
if (boundsRejection != RiverCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
if (view.isOpenToSurface(position)) {
|
||||
return RiverCaveRejection.OPEN_SURFACE;
|
||||
}
|
||||
CaveVoxel voxel = voxelAt(view, position);
|
||||
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
|
||||
if (hazard != RiverCaveRejection.NONE) {
|
||||
return hazard;
|
||||
}
|
||||
return isFluidReachable(voxel, settings)
|
||||
? RiverCaveRejection.NONE
|
||||
: RiverCaveRejection.NO_CAVE_TARGET;
|
||||
}
|
||||
|
||||
private PathResult buildThroat(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings
|
||||
) {
|
||||
CavePosition entry = source.entry();
|
||||
CavePosition target = source.target();
|
||||
int deltaX = target.x() - entry.x();
|
||||
int deltaY = target.y() - entry.y();
|
||||
int deltaZ = target.z() - entry.z();
|
||||
int movesX = Math.abs(deltaX);
|
||||
int movesY = Math.abs(deltaY);
|
||||
int movesZ = Math.abs(deltaZ);
|
||||
int length = movesX + movesY + movesZ;
|
||||
if (length > settings.maxThroatLength()) {
|
||||
return PathResult.rejected(RiverCaveRejection.THROAT_LIMIT);
|
||||
}
|
||||
|
||||
int stepX = Integer.signum(deltaX);
|
||||
int stepY = Integer.signum(deltaY);
|
||||
int stepZ = Integer.signum(deltaZ);
|
||||
int usedX = 0;
|
||||
int usedY = 0;
|
||||
int usedZ = 0;
|
||||
CavePosition current = entry;
|
||||
List<CavePosition> positions = new ArrayList<>(length + 1);
|
||||
|
||||
while (true) {
|
||||
RiverCaveRejection positionRejection = validateThroatPosition(view, source, settings, current);
|
||||
if (positionRejection != RiverCaveRejection.NONE) {
|
||||
return PathResult.rejected(positionRejection);
|
||||
}
|
||||
positions.add(current);
|
||||
if (current.equals(target)) {
|
||||
return expandThroat(view, source, settings, positions);
|
||||
}
|
||||
|
||||
int axis = selectNextAxis(source.sourceId(), movesX, movesY, movesZ, usedX, usedY, usedZ);
|
||||
if (axis == 0) {
|
||||
current = current.offset(stepX, 0, 0);
|
||||
usedX++;
|
||||
} else if (axis == 1) {
|
||||
current = current.offset(0, stepY, 0);
|
||||
usedY++;
|
||||
} else {
|
||||
current = current.offset(0, 0, stepZ);
|
||||
usedZ++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PathResult expandThroat(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
List<CavePosition> centerline
|
||||
) {
|
||||
int radius = settings.throatRadius();
|
||||
int extent = radius - 1;
|
||||
int radiusSquared = radius * radius;
|
||||
Set<CavePosition> expanded = new LinkedHashSet<>();
|
||||
for (CavePosition center : centerline) {
|
||||
for (int dx = -extent; dx <= extent; dx++) {
|
||||
for (int dy = -extent; dy <= extent; dy++) {
|
||||
for (int dz = -extent; dz <= extent; dz++) {
|
||||
if ((dx * dx) + (dy * dy) + (dz * dz) >= radiusSquared) {
|
||||
continue;
|
||||
}
|
||||
CavePosition position = center.offset(dx, dy, dz);
|
||||
if (position.y() > source.entry().y()) {
|
||||
continue;
|
||||
}
|
||||
RiverCaveRejection rejection = validateThroatPosition(view, source, settings, position);
|
||||
if (rejection != RiverCaveRejection.NONE) {
|
||||
return PathResult.rejected(rejection);
|
||||
}
|
||||
expanded.add(position);
|
||||
if (expanded.size() > settings.maxFloodVolume()) {
|
||||
return PathResult.rejected(RiverCaveRejection.VOLUME_LIMIT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return PathResult.accepted(List.copyOf(expanded));
|
||||
}
|
||||
|
||||
private int selectNextAxis(
|
||||
long sourceId,
|
||||
int movesX,
|
||||
int movesY,
|
||||
int movesZ,
|
||||
int usedX,
|
||||
int usedY,
|
||||
int usedZ
|
||||
) {
|
||||
double scoreX = nextAxisScore(movesX, usedX);
|
||||
double scoreY = nextAxisScore(movesY, usedY);
|
||||
double scoreZ = nextAxisScore(movesZ, usedZ);
|
||||
double minimum = Math.min(scoreX, Math.min(scoreY, scoreZ));
|
||||
int tieOffset = Math.floorMod(sourceId, 3);
|
||||
for (int offset = 0; offset < 3; offset++) {
|
||||
int axis = (tieOffset + offset) % 3;
|
||||
double score = axis == 0 ? scoreX : axis == 1 ? scoreY : scoreZ;
|
||||
if (score == minimum) {
|
||||
return axis;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("No remaining throat axis");
|
||||
}
|
||||
|
||||
private double nextAxisScore(int moves, int used) {
|
||||
if (used >= moves) {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
return ((2D * used) + 1D) / moves;
|
||||
}
|
||||
|
||||
private RiverCaveRejection validateThroatPosition(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
CavePosition position
|
||||
) {
|
||||
if (!view.isInWorld(position)) {
|
||||
return RiverCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
RiverCaveRejection boundsRejection = validateBounds(source, settings, position);
|
||||
if (boundsRejection != RiverCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
return rejectionForHazard(voxelAt(view, position), settings);
|
||||
}
|
||||
|
||||
private GrottoResult buildGrotto(RiverCaveSource source, RiverCavePlannerSettings settings) {
|
||||
int horizontalRadius = settings.grottoHorizontalRadius();
|
||||
int verticalRadius = settings.grottoVerticalRadius();
|
||||
Set<CavePosition> candidates = new HashSet<>();
|
||||
|
||||
for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) {
|
||||
for (int dy = -verticalRadius; dy <= verticalRadius; dy++) {
|
||||
for (int dz = -horizontalRadius; dz <= horizontalRadius; dz++) {
|
||||
if (settings.grottoShape().contains(source, settings, dx, dy, dz)) {
|
||||
candidates.add(source.target().offset(dx, dy, dz));
|
||||
if (candidates.size() > settings.maxFloodVolume()) {
|
||||
return GrottoResult.rejected(RiverCaveRejection.VOLUME_LIMIT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.add(source.target());
|
||||
for (int offset = 1; offset <= settings.dryHeadroom(); offset++) {
|
||||
CavePosition headroom = new CavePosition(
|
||||
source.target().x(), source.waterHeadY() + offset, source.target().z());
|
||||
if (Math.abs(headroom.y() - source.target().y()) > verticalRadius) {
|
||||
return GrottoResult.rejected(RiverCaveRejection.DRY_HEADROOM_LIMIT);
|
||||
}
|
||||
candidates.add(headroom);
|
||||
if (candidates.size() > settings.maxFloodVolume()) {
|
||||
return GrottoResult.rejected(RiverCaveRejection.VOLUME_LIMIT);
|
||||
}
|
||||
}
|
||||
return GrottoResult.accepted(connectedGrotto(source.target(), candidates));
|
||||
}
|
||||
|
||||
private Set<CavePosition> connectedGrotto(CavePosition target, Set<CavePosition> candidates) {
|
||||
Queue<CavePosition> queue = new ArrayDeque<>();
|
||||
Set<CavePosition> connected = new HashSet<>();
|
||||
queue.add(target);
|
||||
connected.add(target);
|
||||
while (!queue.isEmpty()) {
|
||||
CavePosition position = queue.remove();
|
||||
for (CavePosition direction : DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (candidates.contains(neighbor) && connected.add(neighbor)) {
|
||||
queue.add(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
private RiverCaveRejection validateGeneratedCarve(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
Set<CavePosition> carve
|
||||
) {
|
||||
for (CavePosition position : carve) {
|
||||
if (!view.isInWorld(position)) {
|
||||
return RiverCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
RiverCaveRejection boundsRejection = validateBounds(source, settings, position);
|
||||
if (boundsRejection != RiverCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
CaveVoxel voxel = voxelAt(view, position);
|
||||
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
|
||||
if (hazard != RiverCaveRejection.NONE) {
|
||||
return hazard;
|
||||
}
|
||||
if (voxel != CaveVoxel.SOLID) {
|
||||
return RiverCaveRejection.GROTTO_INTERSECTION;
|
||||
}
|
||||
}
|
||||
return RiverCaveRejection.NONE;
|
||||
}
|
||||
|
||||
private BoundaryResult validateGeneratedBoundary(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
Set<CavePosition> carve
|
||||
) {
|
||||
Set<CavePosition> guards = new HashSet<>();
|
||||
for (CavePosition position : carve) {
|
||||
for (CavePosition direction : DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (carve.contains(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (isInletOpening(source, neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (!view.isInWorld(neighbor)) {
|
||||
return BoundaryResult.rejected(RiverCaveRejection.WORLD_BOUNDARY);
|
||||
}
|
||||
RiverCaveRejection boundsRejection = validateBounds(source, settings, neighbor);
|
||||
if (boundsRejection != RiverCaveRejection.NONE) {
|
||||
return BoundaryResult.rejected(boundsRejection);
|
||||
}
|
||||
CaveVoxel voxel = voxelAt(view, neighbor);
|
||||
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
|
||||
if (hazard != RiverCaveRejection.NONE) {
|
||||
return BoundaryResult.rejected(hazard);
|
||||
}
|
||||
if (voxel != CaveVoxel.SOLID) {
|
||||
return BoundaryResult.rejected(RiverCaveRejection.GROTTO_SHELL_OPEN);
|
||||
}
|
||||
guards.add(neighbor);
|
||||
}
|
||||
}
|
||||
return BoundaryResult.accepted(guards);
|
||||
}
|
||||
|
||||
private void addChamberActions(
|
||||
Map<CavePosition, RiverCaveAction> actions,
|
||||
Collection<CavePosition> positions,
|
||||
int waterHeadY
|
||||
) {
|
||||
for (CavePosition position : positions) {
|
||||
RiverCaveAction action = position.y() <= waterHeadY
|
||||
? RiverCaveAction.WET_SOURCE
|
||||
: RiverCaveAction.DRY_AIR;
|
||||
actions.put(position, action);
|
||||
}
|
||||
}
|
||||
|
||||
private void addThroatActions(
|
||||
Map<CavePosition, RiverCaveAction> actions,
|
||||
Collection<CavePosition> throat,
|
||||
RiverCaveSource source
|
||||
) {
|
||||
for (CavePosition position : throat) {
|
||||
RiverCaveAction action;
|
||||
if (position.y() <= source.waterHeadY()) {
|
||||
action = RiverCaveAction.WET_SOURCE;
|
||||
} else if (source.mode() == RiverCaveMode.WATERFALL_POOL) {
|
||||
action = RiverCaveAction.FALLING_WATER;
|
||||
} else {
|
||||
action = RiverCaveAction.DRY_AIR;
|
||||
}
|
||||
actions.put(position, action);
|
||||
}
|
||||
}
|
||||
|
||||
private void addSealGuards(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
Map<CavePosition, RiverCaveAction> actions
|
||||
) {
|
||||
Set<CavePosition> guards = new HashSet<>();
|
||||
for (CavePosition position : List.copyOf(actions.keySet())) {
|
||||
for (CavePosition direction : DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (actions.containsKey(neighbor)
|
||||
|| isInletOpening(source, neighbor)
|
||||
|| !view.isInWorld(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (voxelAt(view, neighbor) == CaveVoxel.SOLID) {
|
||||
guards.add(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (CavePosition guard : guards) {
|
||||
actions.put(guard, RiverCaveAction.SEAL_GUARD);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInletOpening(RiverCaveSource source, CavePosition position) {
|
||||
return position.equals(source.entry().offset(0, 1, 0));
|
||||
}
|
||||
|
||||
private RiverCaveRejection validateSource(RiverCaveSource source) {
|
||||
if (source.entry().y() < source.waterHeadY()) {
|
||||
return RiverCaveRejection.INVALID_SOURCE;
|
||||
}
|
||||
if (source.target().y() > source.waterHeadY()) {
|
||||
return RiverCaveRejection.INVALID_SOURCE;
|
||||
}
|
||||
if (source.target().y() > source.entry().y()) {
|
||||
return RiverCaveRejection.INVALID_SOURCE;
|
||||
}
|
||||
return RiverCaveRejection.NONE;
|
||||
}
|
||||
|
||||
private RiverCaveRejection validateBounds(
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
CavePosition position
|
||||
) {
|
||||
boolean closedComponent = source.mode() == RiverCaveMode.CLOSED_COMPONENT
|
||||
|| source.mode() == RiverCaveMode.WATERFALL_POOL;
|
||||
int horizontalRadius = closedComponent
|
||||
? settings.maxClosedComponentHorizontalRadius()
|
||||
: settings.maxHorizontalRadius();
|
||||
int maximumDepth = closedComponent
|
||||
? settings.maxClosedComponentDepth()
|
||||
: settings.maxDepth();
|
||||
long deltaX = (long) position.x() - source.entry().x();
|
||||
long deltaZ = (long) position.z() - source.entry().z();
|
||||
long radiusSquared = (long) horizontalRadius * horizontalRadius;
|
||||
if ((deltaX * deltaX) + (deltaZ * deltaZ) > radiusSquared) {
|
||||
return RiverCaveRejection.RADIUS_LIMIT;
|
||||
}
|
||||
long depth = (long) source.entry().y() - position.y();
|
||||
int maximumGeneratedY = source.waterHeadY() + settings.dryHeadroom() + 1;
|
||||
boolean allowedGeneratedHeadroom = !closedComponent
|
||||
&& position.y() <= maximumGeneratedY;
|
||||
if ((depth < 0L && !allowedGeneratedHeadroom) || depth > maximumDepth) {
|
||||
return RiverCaveRejection.DEPTH_LIMIT;
|
||||
}
|
||||
return RiverCaveRejection.NONE;
|
||||
}
|
||||
|
||||
private RiverCaveRejection rejectionForTarget(
|
||||
CaveVoxel voxel,
|
||||
RiverCavePlannerSettings settings
|
||||
) {
|
||||
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
|
||||
return hazard == RiverCaveRejection.NONE ? RiverCaveRejection.NO_CAVE_TARGET : hazard;
|
||||
}
|
||||
|
||||
private RiverCaveRejection rejectionForHazard(
|
||||
CaveVoxel voxel,
|
||||
RiverCavePlannerSettings settings
|
||||
) {
|
||||
return switch (voxel) {
|
||||
case LAVA -> RiverCaveRejection.LAVA_CONTACT;
|
||||
case INCOMPATIBLE_FLUID -> settings.existingFluidPolicy() == RiverCaveFluidPolicy.REPLACE_CONTAINED
|
||||
? RiverCaveRejection.NONE
|
||||
: RiverCaveRejection.INCOMPATIBLE_FLUID;
|
||||
case COMPATIBLE_FLUID -> settings.existingFluidPolicy() == RiverCaveFluidPolicy.REJECT_EXISTING
|
||||
? RiverCaveRejection.EXISTING_FLUID
|
||||
: RiverCaveRejection.NONE;
|
||||
default -> RiverCaveRejection.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isFluidReachable(CaveVoxel voxel, RiverCavePlannerSettings settings) {
|
||||
return voxel == CaveVoxel.CAVE_AIR
|
||||
|| (voxel == CaveVoxel.COMPATIBLE_FLUID
|
||||
&& settings.existingFluidPolicy() != RiverCaveFluidPolicy.REJECT_EXISTING)
|
||||
|| (voxel == CaveVoxel.INCOMPATIBLE_FLUID
|
||||
&& settings.existingFluidPolicy() == RiverCaveFluidPolicy.REPLACE_CONTAINED);
|
||||
}
|
||||
|
||||
private CaveVoxel voxelAt(CaveVoxelView view, CavePosition position) {
|
||||
return Objects.requireNonNull(view.voxelAt(position));
|
||||
}
|
||||
|
||||
private OptionalLong findWinningSourceId(
|
||||
Set<CavePosition> positions,
|
||||
Map<CavePosition, RiverCaveSource> claimedBy
|
||||
) {
|
||||
RiverCaveSource winner = null;
|
||||
for (CavePosition position : positions) {
|
||||
RiverCaveSource contender = claimedBy.get(position);
|
||||
if (contender == null) {
|
||||
continue;
|
||||
}
|
||||
if (winner == null || SOURCE_PRIORITY.compare(contender, winner) < 0) {
|
||||
winner = contender;
|
||||
}
|
||||
}
|
||||
return winner == null ? OptionalLong.empty() : OptionalLong.of(winner.sourceId());
|
||||
}
|
||||
|
||||
private RiverCavePlan accepted(
|
||||
CaveVoxelView view,
|
||||
RiverCaveSource source,
|
||||
Map<CavePosition, RiverCaveAction> actions
|
||||
) {
|
||||
Map<CavePosition, CaveVoxelPrecondition> preconditions = new HashMap<>(actions.size());
|
||||
for (CavePosition position : actions.keySet()) {
|
||||
preconditions.put(
|
||||
position,
|
||||
new CaveVoxelPrecondition(voxelAt(view, position), view.isOpenToSurface(position))
|
||||
);
|
||||
}
|
||||
return new RiverCavePlan(
|
||||
source,
|
||||
RiverCaveRejection.NONE,
|
||||
actions,
|
||||
preconditions,
|
||||
OptionalLong.empty()
|
||||
);
|
||||
}
|
||||
|
||||
private RiverCavePlan rejected(RiverCaveSource source, RiverCaveRejection rejection) {
|
||||
return new RiverCavePlan(
|
||||
source,
|
||||
rejection,
|
||||
Map.of(),
|
||||
Map.of(),
|
||||
OptionalLong.empty()
|
||||
);
|
||||
}
|
||||
|
||||
private RiverCavePlan rejectedOverlap(RiverCaveSource source, long winnerSourceId) {
|
||||
return new RiverCavePlan(
|
||||
source,
|
||||
RiverCaveRejection.OVERLAPPING_SOURCE,
|
||||
Map.of(),
|
||||
Map.of(),
|
||||
OptionalLong.of(winnerSourceId)
|
||||
);
|
||||
}
|
||||
|
||||
private record PathResult(List<CavePosition> positions, RiverCaveRejection rejection) {
|
||||
private static PathResult accepted(List<CavePosition> positions) {
|
||||
return new PathResult(List.copyOf(positions), RiverCaveRejection.NONE);
|
||||
}
|
||||
|
||||
private static PathResult rejected(RiverCaveRejection rejection) {
|
||||
return new PathResult(List.of(), rejection);
|
||||
}
|
||||
}
|
||||
|
||||
private record ComponentResult(Set<CavePosition> positions, RiverCaveRejection rejection) {
|
||||
private static ComponentResult accepted(Set<CavePosition> positions) {
|
||||
return new ComponentResult(Set.copyOf(positions), RiverCaveRejection.NONE);
|
||||
}
|
||||
|
||||
private static ComponentResult rejected(RiverCaveRejection rejection) {
|
||||
return new ComponentResult(Set.of(), rejection);
|
||||
}
|
||||
}
|
||||
|
||||
private record BoundaryResult(Set<CavePosition> sealGuards, RiverCaveRejection rejection) {
|
||||
private static BoundaryResult accepted(Set<CavePosition> sealGuards) {
|
||||
return new BoundaryResult(Set.copyOf(sealGuards), RiverCaveRejection.NONE);
|
||||
}
|
||||
|
||||
private static BoundaryResult rejected(RiverCaveRejection rejection) {
|
||||
return new BoundaryResult(Set.of(), rejection);
|
||||
}
|
||||
}
|
||||
|
||||
private record GrottoResult(Set<CavePosition> positions, RiverCaveRejection rejection) {
|
||||
private static GrottoResult accepted(Set<CavePosition> positions) {
|
||||
return new GrottoResult(Set.copyOf(positions), RiverCaveRejection.NONE);
|
||||
}
|
||||
|
||||
private static GrottoResult rejected(RiverCaveRejection rejection) {
|
||||
return new GrottoResult(Set.of(), rejection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
|
||||
public enum RiverCaveFluidPolicy {
|
||||
REJECT_EXISTING,
|
||||
ALLOW_COMPATIBLE,
|
||||
REPLACE_CONTAINED
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RiverCaveGrottoShape {
|
||||
RiverCaveGrottoShape ELLIPSOID = (source, settings, dx, dy, dz) -> {
|
||||
double horizontalRadius = settings.grottoHorizontalRadius();
|
||||
double verticalRadius = settings.grottoVerticalRadius();
|
||||
double normalized = ((double) dx * dx / (horizontalRadius * horizontalRadius))
|
||||
+ ((double) dy * dy / (verticalRadius * verticalRadius))
|
||||
+ ((double) dz * dz / (horizontalRadius * horizontalRadius));
|
||||
return normalized <= 1D;
|
||||
};
|
||||
|
||||
boolean contains(
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
int offsetX,
|
||||
int offsetY,
|
||||
int offsetZ
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user