d
This commit is contained in:
Brian Neumann-Fopiano
2026-08-23 13:56:23 -04:00
parent dce7af1955
commit fe5651f854
216 changed files with 21919 additions and 1865 deletions
@@ -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()) {
@@ -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) {
@@ -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
@@ -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 {
@@ -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];
@@ -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
@@ -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];
}
}
@@ -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
@@ -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;
}
@@ -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
);
}
}
@@ -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) {
}
}
@@ -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
}
@@ -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