Terrain-first hydrology

- Replace the river package with the hydrology planner, footprint compiler, cave containment and runtime
- Add the hydrology pack schema, validator, probes and tests
- Wire hydrology into IrisComplex, actuators, mantle, Vision and commands
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-01 18:28:00 -04:00
1 parent bdaa84975a
commit 48afa0d93f
352 files changed
+57955 -19113

No files matched your search

@@ -23,10 +23,11 @@ import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.IrisRenderer;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.engine.hydrology.HydrologyCandidateKind;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.localization.MessageArgument;
@@ -125,6 +126,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
private VisionRenderController.Frame renderFrame;
private JComboBox<RenderType> modeSelector;
private JToggleButton gridToggle;
private JToggleButton entitiesToggle;
private JToggleButton followToggle;
private RenderType currentType;
private List<GuiMarker> players;
@@ -142,6 +144,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
private double blocksPerPixel;
private int paintCadenceFps;
private boolean grid;
private boolean entitiesVisible;
private boolean follow;
private boolean help;
private boolean debug;
@@ -160,6 +163,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
this.notifications = new LinkedHashMap<>();
this.players = List.of();
this.entities = List.of();
this.entitiesVisible = false;
this.currentType = RenderType.BIOME;
this.blocksPerPixel = DEFAULT_BLOCKS_PER_PIXEL;
this.contentRevision = 1L;
@@ -265,6 +269,12 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
vision.gridToggle = createToggle(IrisLanguage.plain(DesktopUiMessages.VISION_GRID), vision.grid);
vision.gridToggle.addActionListener(event -> vision.toggleGrid());
trailing.add(vision.gridToggle);
vision.entitiesToggle = createToggle(
IrisLanguage.plain(DesktopUiMessages.VISION_ENTITIES),
vision.entitiesVisible
);
vision.entitiesToggle.addActionListener(event -> vision.toggleEntities());
trailing.add(vision.entitiesToggle);
vision.followToggle = createToggle(IrisLanguage.plain(DesktopUiMessages.VISION_FOLLOW), vision.follow);
vision.followToggle.addActionListener(event -> vision.toggleFollow());
trailing.add(vision.followToggle);
@@ -411,11 +421,13 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
}
private void renderMarkers(Graphics2D canvas) {
for (GuiMarker marker : entities) {
int screenX = (int) Math.round(worldToScreenX(marker.worldX()));
int screenY = (int) Math.round(worldToScreenZ(marker.worldZ()));
canvas.setColor(MOB_COLOR);
canvas.fillRect(screenX - 2, screenY - 2, 5, 5);
if (entitiesVisible) {
for (GuiMarker marker : entities) {
int screenX = (int) Math.round(worldToScreenX(marker.worldX()));
int screenY = (int) Math.round(worldToScreenZ(marker.worldZ()));
canvas.setColor(MOB_COLOR);
canvas.fillRect(screenX - 2, screenY - 2, 5, 5);
}
}
for (GuiMarker marker : players) {
@@ -430,7 +442,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
int labelWidth = canvas.getFontMetrics().stringWidth(marker.label());
canvas.drawString(marker.label(), screenX - labelWidth / 2, screenY - 14);
}
if (detailedHover) {
if (entitiesVisible && detailedHover) {
renderNearestEntity(canvas);
}
}
@@ -483,21 +495,40 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
}
private void renderRiverLegend(Graphics2D canvas) {
RiverSection[] sections = RiverSection.values();
HydrologyFeatureType[] types = HydrologyFeatureType.values();
HydrologyCandidateKind[] candidateKinds = HydrologyCandidateKind.values();
int lineHeight = 18;
int width = 148;
int height = sections.length * lineHeight + CARD_PADDING * 2;
int width = 188;
int height = (types.length + candidateKinds.length + 1) * lineHeight + CARD_PADDING * 2;
int x = getWidth() - width - CARD_PADDING;
int y = getHeight() - STATUS_HEIGHT - PROGRESS_HEIGHT - height - CARD_PADDING;
drawCardBackground(canvas, x, y, width, height);
canvas.setFont(BODY_FONT);
for (int index = 0; index < sections.length; index++) {
RiverSection section = sections[index];
int rowY = y + CARD_PADDING + index * lineHeight;
canvas.setColor(new Color(IrisRenderer.riverColor(section)));
int headwaterY = y + CARD_PADDING;
canvas.setColor(new Color(IrisRenderer.headwaterColor()));
canvas.fillRoundRect(x + CARD_PADDING, headwaterY + 2, 12, 12, 4, 4);
canvas.setColor(new Color(IrisRenderer.headwaterDirectionColor()));
canvas.drawLine(x + CARD_PADDING + 3, headwaterY + 8, x + CARD_PADDING + 9, headwaterY + 8);
canvas.drawLine(x + CARD_PADDING + 9, headwaterY + 8, x + CARD_PADDING + 6, headwaterY + 5);
canvas.drawLine(x + CARD_PADDING + 9, headwaterY + 8, x + CARD_PADDING + 6, headwaterY + 11);
canvas.setColor(TEXT_SECONDARY);
canvas.drawString("headwater / source flow", x + CARD_PADDING + 20, headwaterY + 13);
for (int index = 0; index < types.length; index++) {
HydrologyFeatureType type = types[index];
int rowY = y + CARD_PADDING + (index + 1) * lineHeight;
canvas.setColor(new Color(IrisRenderer.hydrologyFeatureColor(type)));
canvas.fillRoundRect(x + CARD_PADDING, rowY + 2, 12, 12, 4, 4);
canvas.setColor(TEXT_SECONDARY);
String label = section.name().toLowerCase(Locale.ROOT).replace('_', ' ');
String label = type.name().toLowerCase(Locale.ROOT).replace('_', ' ');
canvas.drawString(label, x + CARD_PADDING + 20, rowY + 13);
}
for (int index = 0; index < candidateKinds.length; index++) {
HydrologyCandidateKind kind = candidateKinds[index];
int rowY = y + CARD_PADDING + (types.length + index + 1) * lineHeight;
canvas.setColor(new Color(IrisRenderer.hydrologyDiagnosticColor(kind)));
canvas.fillRoundRect(x + CARD_PADDING, rowY + 2, 12, 12, 4, 4);
canvas.setColor(TEXT_SECONDARY);
String label = "projected " + kind.name().toLowerCase(Locale.ROOT).replace('_', ' ');
canvas.drawString(label, x + CARD_PADDING + 20, rowY + 13);
}
}
@@ -783,12 +814,16 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
try {
List<GuiMarker> nextPlayers = overlay.players();
players = nextPlayers == null ? List.of() : List.copyOf(nextPlayers);
overlay.requestEntities(next -> EventQueue.invokeLater(() -> {
if (!closed) {
entities = next == null ? List.of() : List.copyOf(next);
repaint();
}
}));
if (entitiesVisible) {
overlay.requestEntities(next -> EventQueue.invokeLater(() -> {
if (!closed && entitiesVisible) {
entities = next == null ? List.of() : List.copyOf(next);
repaint();
}
}));
} else if (!entities.isEmpty()) {
entities = List.of();
}
if (follow && !players.isEmpty()) {
GuiMarker player = players.get(0);
if (Math.abs(centerX - player.worldX()) > 0.5D || Math.abs(centerZ - player.worldZ()) > 0.5D) {
@@ -852,6 +887,22 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
notifyUser(IrisLanguage.plain(grid ? DesktopUiMessages.VISION_GRID_ENABLED : DesktopUiMessages.VISION_GRID_DISABLED));
}
private void toggleEntities() {
entitiesVisible = !entitiesVisible;
if (entitiesVisible) {
refreshMarkers();
} else {
entities = List.of();
repaint();
}
syncControls();
notifyUser(IrisLanguage.plain(
entitiesVisible
? DesktopUiMessages.VISION_ENTITIES_ENABLED
: DesktopUiMessages.VISION_ENTITIES_DISABLED
));
}
private void toggleFollow() {
follow = !follow;
if (follow && players.isEmpty()) {
@@ -881,6 +932,9 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
if (gridToggle != null) {
gridToggle.setSelected(grid);
}
if (entitiesToggle != null) {
entitiesToggle.setSelected(entitiesVisible);
}
if (followToggle != null) {
followToggle.setSelected(follow);
}
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.WorldSlotKey;
import art.arcane.iris.engine.IrisEngineMantle;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
@@ -253,7 +254,9 @@ public final class BukkitWorldConfiguration {
if (Files.isDirectory(worldRoot.resolve("iris"), LinkOption.NOFOLLOW_LINKS)) {
return true;
}
for (String directory : new String[]{"region", "entities", "poi", "mantle"}) {
for (String directory : new String[]{
"region", "entities", "poi", IrisEngineMantle.STORAGE_FOLDER_NAME
}) {
if (hasEntries(worldRoot.resolve(directory))) {
return true;
}
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.WorldSlotKey;
import art.arcane.iris.engine.IrisEngineMantle;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
@@ -37,7 +38,9 @@ import java.util.stream.Stream;
public final class HuskWorldQuarantine {
private static final String IRIS_NAMESPACE = "iris";
private static final Pattern MANAGED_KEY = Pattern.compile("[a-z0-9_-]+");
private static final String[] CHUNK_DATA_DIRECTORIES = {"region", "entities", "poi", "mantle"};
private static final String[] CHUNK_DATA_DIRECTORIES = {
"region", "entities", "poi", IrisEngineMantle.STORAGE_FOLDER_NAME
};
private static final Set<String> OS_METADATA_FILES = Set.of(".DS_Store", "Thumbs.db", "desktop.ini");
private static final String APPLE_DOUBLE_PREFIX = "._";
private static final DateTimeFormatter STAMP = DateTimeFormatter
@@ -9,12 +9,15 @@ public final class DesktopUiMessages {
public static final TextKey VISION_TITLE = TextKey.of("iris.desktop.vision.title", "Iris Vision");
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_ENTITIES = TextKey.of("iris.desktop.vision.entities", "Entities");
public static final TextKey VISION_FOLLOW = TextKey.of("iris.desktop.vision.follow", "Follow");
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");
public static final TextKey VISION_GRID_ENABLED = TextKey.of("iris.desktop.vision.grid_enabled", "Grid enabled");
public static final TextKey VISION_GRID_DISABLED = TextKey.of("iris.desktop.vision.grid_disabled", "Grid disabled");
public static final TextKey VISION_ENTITIES_ENABLED = TextKey.of("iris.desktop.vision.entities_enabled", "Entities enabled");
public static final TextKey VISION_ENTITIES_DISABLED = TextKey.of("iris.desktop.vision.entities_disabled", "Entities disabled");
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");
@@ -158,8 +161,9 @@ public final class DesktopUiMessages {
public static final TextKey IMAGEMAP_PREVIEW_STATUS = TextKey.of("iris.desktop.imagemap.preview_status", "X {x} Z {z} | {value} | {scale} blocks/pixel");
private static final List<MessageKey> KEYS = List.of(
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_FOLLOW,
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_ENTITIES, VISION_FOLLOW,
VISION_REFRESHING, VISION_FPS, VISION_ZOOM_RESET, VISION_GRID_ENABLED, VISION_GRID_DISABLED,
VISION_ENTITIES_ENABLED, VISION_ENTITIES_DISABLED,
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,
@@ -388,7 +388,7 @@ public final class DefaultPackBootstrapProvisioner {
try {
validCache = validateArchive(archivePath, spec);
} catch (IOException exception) {
feedback.accept("Cached Iris " + spec.key() + " beta archive is invalid; downloading a replacement.");
feedback.accept("Cached Iris " + spec.key() + " release archive is invalid; downloading a replacement.");
}
}
String cachedSource = metadata.getProperty("source", "");
@@ -439,7 +439,7 @@ public final class DefaultPackBootstrapProvisioner {
copyLimited(input, output, options.maxArchiveBytes(), spec);
}
if (!validateArchive(temporary, spec)) {
throw new IOException("Downloaded Iris " + spec.key() + " beta archive is invalid");
throw new IOException("Downloaded Iris " + spec.key() + " release archive is invalid");
}
move(temporary, archivePath, true);
} finally {
@@ -452,11 +452,11 @@ public final class DefaultPackBootstrapProvisioner {
updated.setProperty("fetchedAt", Long.toString(options.clock().millis()));
updated.setProperty("sha256", sha256(archivePath));
storePropertiesAtomic(metadataPath, updated);
feedback.accept("Downloaded the Iris " + spec.key() + " beta archive.");
feedback.accept("Downloaded the Iris " + spec.key() + " release archive.");
return new Archive(archivePath, updated.getProperty("sha256"));
}
close(response.body());
IOException statusFailure = new IOException("Iris " + spec.key() + " beta download returned HTTP " + status);
IOException statusFailure = new IOException("Iris " + spec.key() + " release download returned HTTP " + status);
if (!retryableStatus(status)) {
networkFailure = statusFailure;
break;
@@ -464,7 +464,7 @@ public final class DefaultPackBootstrapProvisioner {
networkFailure = statusFailure;
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new IOException("Iris " + spec.key() + " beta download was interrupted", exception);
throw new IOException("Iris " + spec.key() + " release download was interrupted", exception);
} catch (IOException exception) {
networkFailure = exception;
}
@@ -474,7 +474,7 @@ public final class DefaultPackBootstrapProvisioner {
}
if (cacheMatchesSource) {
feedback.accept("Iris " + spec.key() + " beta download failed; using the validated cached archive.");
feedback.accept("Iris " + spec.key() + " release download failed; using the validated cached archive.");
return new Archive(archivePath, sha256(archivePath));
}
if (isManagedPackOutputUsable(marker, cacheRoot.getParent().getParent(), spec)) {
@@ -482,8 +482,8 @@ public final class DefaultPackBootstrapProvisioner {
return new Archive(null, sourceSha);
}
throw networkFailure == null
? new IOException("Iris " + spec.key() + " beta archive is unavailable and no valid cache exists")
: new IOException("Iris " + spec.key() + " beta archive is unavailable and no valid cache exists", networkFailure);
? new IOException("Iris " + spec.key() + " release archive is unavailable and no valid cache exists")
: new IOException("Iris " + spec.key() + " release archive is unavailable and no valid cache exists", networkFailure);
}
private static boolean isManagedPackOutputUsable(Properties marker, Path dataDirectory, PackSpec spec) {
@@ -514,11 +514,11 @@ public final class DefaultPackBootstrapProvisioner {
while ((entry = zip.getNextEntry()) != null) {
entries++;
if (entries > MAX_ARCHIVE_ENTRIES) {
throw new IOException("Iris " + spec.key() + " beta archive contains too many entries");
throw new IOException("Iris " + spec.key() + " release archive contains too many entries");
}
String name = normalizedZipEntry(entry.getName());
if (!paths.add(name)) {
throw new IOException("Iris " + spec.key() + " beta archive contains duplicate path " + name);
throw new IOException("Iris " + spec.key() + " release archive contains duplicate path " + name);
}
String requiredDimension = "dimensions/" + spec.requiredDimension() + ".json";
if (name.equals(requiredDimension) || name.endsWith("/" + requiredDimension)) {
@@ -530,7 +530,7 @@ public final class DefaultPackBootstrapProvisioner {
while ((read = zip.read(buffer)) >= 0) {
expanded += read;
if (expanded > MAX_EXPANDED_BYTES) {
throw new IOException("Iris " + spec.key() + " beta archive expands beyond the safety limit");
throw new IOException("Iris " + spec.key() + " release archive expands beyond the safety limit");
}
}
}
@@ -542,7 +542,7 @@ public final class DefaultPackBootstrapProvisioner {
private static Path extractArchive(Path archive, Path extractionRoot, PackSpec spec) throws IOException {
if (archive == null) {
throw new IOException("Cached Iris " + spec.key() + " beta archive is unavailable for required pack rebuild");
throw new IOException("Cached Iris " + spec.key() + " release archive is unavailable for required pack rebuild");
}
long expanded = 0L;
int entries = 0;
@@ -551,12 +551,12 @@ public final class DefaultPackBootstrapProvisioner {
while ((entry = zip.getNextEntry()) != null) {
entries++;
if (entries > MAX_ARCHIVE_ENTRIES) {
throw new IOException("Iris " + spec.key() + " beta archive contains too many entries");
throw new IOException("Iris " + spec.key() + " release archive contains too many entries");
}
String name = normalizedZipEntry(entry.getName());
Path output = extractionRoot.resolve(name).normalize();
if (!output.startsWith(extractionRoot)) {
throw new IOException("Unsafe path in Iris " + spec.key() + " beta archive: " + entry.getName());
throw new IOException("Unsafe path in Iris " + spec.key() + " release archive: " + entry.getName());
}
if (entry.isDirectory()) {
Files.createDirectories(output);
@@ -568,7 +568,7 @@ public final class DefaultPackBootstrapProvisioner {
while ((read = zip.read(buffer)) >= 0) {
expanded += read;
if (expanded > MAX_EXPANDED_BYTES) {
throw new IOException("Iris " + spec.key() + " beta archive expands beyond the safety limit");
throw new IOException("Iris " + spec.key() + " release archive expands beyond the safety limit");
}
file.write(buffer, 0, read);
}
@@ -589,7 +589,7 @@ public final class DefaultPackBootstrapProvisioner {
}
}
if (candidates.size() != 1) {
throw new IOException("Iris " + spec.key() + " beta archive has an invalid root layout");
throw new IOException("Iris " + spec.key() + " release archive has an invalid root layout");
}
return candidates.getFirst();
}
@@ -613,7 +613,7 @@ public final class DefaultPackBootstrapProvisioner {
private static void validatePackRoot(Path path, PackSpec spec) throws IOException {
if (!isPackRoot(path, spec)) {
throw new IOException("Iris " + spec.key() + " beta pack is missing dimensions/"
throw new IOException("Iris " + spec.key() + " release pack is missing dimensions/"
+ spec.requiredDimension() + ".json at " + path);
}
}
@@ -822,7 +822,7 @@ public final class DefaultPackBootstrapProvisioner {
while ((read = input.read(buffer)) >= 0) {
total += read;
if (total > limit) {
throw new IOException("Iris " + spec.key() + " beta archive exceeds the download size limit");
throw new IOException("Iris " + spec.key() + " release archive exceeds the download size limit");
}
output.write(buffer, 0, read);
}
@@ -63,10 +63,10 @@ import java.util.zip.ZipInputStream;
public final class PackDownloader {
private static final String DEFAULT_OVERWORLD_PACK = "overworld";
private static final String DEFAULT_OVERWORLD_URL =
"https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip";
"https://github.com/IrisDimensions/overworld/releases/download/4002/overworld.zip";
private static final String UNDERWORLD_PACK = "underworld";
private static final String UNDERWORLD_URL =
"https://github.com/IrisDimensions/underworld/releases/download/beta/underworld.zip";
"https://github.com/IrisDimensions/underworld/releases/download/1005/underworld.zip";
private static final List<String> BUILT_IN_PACK_KEYS = List.of(DEFAULT_OVERWORLD_PACK, UNDERWORLD_PACK);
private static final Map<String, String> BUILT_IN_PACK_URLS = Map.of(
DEFAULT_OVERWORLD_PACK, DEFAULT_OVERWORLD_URL,
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -84,9 +84,10 @@ public final class PackValidator {
packFolder, dimensionFiles, validateLiveRegistries);
addDistinct(blockingErrors, imageMaps.errors());
addDistinct(warnings, imageMaps.warnings());
PackRiverValidator.Validation riverValidation = PackRiverValidator.validate(packFolder, dimensionFiles);
addDistinct(blockingErrors, riverValidation.errors());
addDistinct(warnings, riverValidation.warnings());
PackHydrologyValidator.Validation hydrologyValidation =
PackHydrologyValidator.validate(packFolder, dimensionFiles);
addDistinct(blockingErrors, hydrologyValidation.errors());
addDistinct(warnings, hydrologyValidation.warnings());
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder);
addDistinct(blockingErrors, lootIssues.errors());
@@ -481,8 +481,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
listener.onChunkFailed(x, z);
} else {
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
cleanupMantleChunksCoveredBy(x, z, listener);
onChunkCompleted(x, z, chunk);
success = true;
}
@@ -692,11 +691,11 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
}
private void cleanupMantleChunk(int x, int z) {
private void cleanupMantleChunksCoveredBy(int x, int z, PregenListener listener) {
Engine engine = resolveMetricsEngine();
if (engine != null) {
try {
engine.getMantle().forceCleanupChunk(x, z);
engine.getMantle().cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned);
} catch (Throwable ignored) {
}
}
@@ -92,6 +92,7 @@ import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
@Accessors(fluent = true, chain = true)
public class IrisCreator {
private static final long WORLD_CREATE_TIMEOUT_SECONDS = 120L;
private static final long INITIAL_SPAWN_TIMEOUT_SECONDS = 600L;
private static final long WORLD_ENTRY_TELEPORT_TIMEOUT_SECONDS = 60L;
private static final long ROLLBACK_PHASE_TIMEOUT_SECONDS = 120L;
@@ -619,7 +620,7 @@ public class IrisCreator {
generator.getInitialSpawnReady(),
"Initial spawn preparation future");
try {
initialSpawnReady.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
initialSpawnReady.get(INITIAL_SPAWN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (TimeoutException failure) {
throw new TimeoutException("Initial spawn preparation timed out for world \""
+ worldName + "\".");
@@ -22,7 +22,14 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyNaturalSample;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntimeContext;
import art.arcane.iris.engine.image.IrisImageMapRuntime;
import art.arcane.iris.engine.mantle.components.MantleHydrologyCaveVoxelView;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
@@ -31,20 +38,11 @@ import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.object.IrisRiverDeepPools;
import art.arcane.iris.engine.object.IrisRiverNetwork;
import art.arcane.iris.engine.object.IrisRiverOverride;
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
import art.arcane.iris.engine.object.IrisRiverWater;
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
import art.arcane.iris.engine.object.IrisHydrology;
import art.arcane.iris.engine.object.IrisRiverHydrology;
import art.arcane.iris.engine.object.IrisRiverProfile;
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.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
@@ -67,36 +65,48 @@ import lombok.ToString;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
@Data
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime", "imageMapRuntime"})
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime", "imageMapRuntime"})
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime"})
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime"})
public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
private static final int HEIGHT_BOUNDS_GRID = 4;
private static final Comparator<IrisInterpolator> INTERPOLATOR_ORDER = Comparator
.comparing((IrisInterpolator interpolator) -> interpolator.getFunction().name())
.thenComparingDouble(IrisInterpolator::getHorizontalScale);
private static final Comparator<IrisGenerator> GENERATOR_ORDER = Comparator.comparing(
IrisGenerator::getLoadKey,
Comparator.nullsFirst(Comparator.naturalOrder())
);
private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
InferredType.LAND,
InferredType.CAVE,
InferredType.SEA,
InferredType.SHORE
};
@Getter(AccessLevel.NONE)
private final transient ThreadLocal<GridBoundsCache> gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new);
/**
* Immutable snapshot of {@link #generators} taken once at the end of construction, in the exact
* iteration order the map produces. The per-column height paths walk these arrays instead of
* allocating map/set iterators, and the frozen order keeps the floating point accumulation order
* identical to the map iteration it replaces. Mutating {@link #generators} after construction is
* not reflected here.
*/
@Getter(AccessLevel.NONE)
private final transient IrisInterpolator[] frozenInterpolators;
@Getter(AccessLevel.NONE)
private final transient IrisGenerator[][] frozenGenerators;
@Getter(AccessLevel.NONE)
private final transient Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> inferredBiomeStreams;
private RNG rng;
private double fluidHeight;
private IrisData data;
@@ -121,9 +131,7 @@ public class IrisComplex implements DataProvider {
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;
@@ -138,13 +146,12 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
private ProceduralStream<PlatformBlockState> rockStream;
private ProceduralStream<PlatformBlockState> fluidStream;
private ProceduralStream<PlatformBlockState> riverFluidStream;
private ProceduralStream<PlatformBlockState> riverDeepPoolFluidStream;
private Map<String, ProceduralStream<PlatformBlockState>> hydrologyFluidStreams;
private IrisBiome focusBiome;
private IrisRegion focusRegion;
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
private Set<IrisBiome> generatorBiomes;
private IrisRiverRuntime riverRuntime;
private IrisHydrologyRuntime hydrologyRuntime;
private transient IrisImageMapRuntime imageMapRuntime;
// 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
@@ -170,6 +177,7 @@ public class IrisComplex implements DataProvider {
focusBiome = engine.getFocus();
focusRegion = engine.getFocusRegion();
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
KList<IrisRegion> preparedRegions = new KList<>();
if (focusBiome != null) {
focusBiome = focusBiome.withInferredType(InferredType.LAND);
@@ -178,7 +186,7 @@ public class IrisComplex implements DataProvider {
//@builder
if (focusRegion != null) {
prepareInferredBiomes(focusRegion);
prepareInferredBiomes(focusRegion, preparedRegions);
focusRegion.getNaturalBiomes(this).forEach(this::registerGenerators);
} else {
engine.getDimension().getRegions().forEach(regionKey -> {
@@ -186,23 +194,26 @@ public class IrisComplex implements DataProvider {
if (region == null) {
return;
}
prepareInferredBiomes(region);
prepareInferredBiomes(region, preparedRegions);
region.getNaturalBiomes(this).forEach(this::registerGenerators);
});
for (IrisRegion region : imageMapRuntime.getMappedRegions()) {
prepareInferredBiomes(region);
prepareInferredBiomes(region, preparedRegions);
region.getNaturalBiomes(this).forEach(this::registerGenerators);
}
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
}
int interpolatorCount = generators.size();
inferredBiomeStreams = compileInferredBiomeStreams(
preparedRegions,
(region, inferredType) -> compileInferredBiomeStream(engine, region, inferredType, emptyBiome)
);
GeneratorGroup[] generatorGroups = freezeGeneratorGroups(generators);
int interpolatorCount = generatorGroups.length;
frozenInterpolators = new IrisInterpolator[interpolatorCount];
frozenGenerators = new IrisGenerator[interpolatorCount][];
int frozenIndex = 0;
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
frozenInterpolators[frozenIndex] = entry.getKey();
frozenGenerators[frozenIndex] = entry.getValue().toArray(new IrisGenerator[0]);
frozenIndex++;
for (int frozenIndex = 0; frozenIndex < generatorGroups.length; frozenIndex++) {
frozenInterpolators[frozenIndex] = generatorGroups[frozenIndex].interpolator();
frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators();
}
generatorBounds = buildGeneratorBounds(engine);
KList<IrisShapedGeneratorStyle> overlayNoise = engine.getDimension().getOverlayNoise();
@@ -221,29 +232,7 @@ public class IrisComplex implements DataProvider {
.select(engine.getDimension().getRockPalette().getBlockData(data));
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
.select(engine.getDimension().getFluidPalette().getBlockData(data));
IrisRiverNetwork configuredRivers = engine.getDimension().getRivers();
IrisRiverWater configuredRiverWater = configuredRivers == null ? null : configuredRivers.getWater();
riverFluidStream = configuredRivers != null && configuredRivers.isEnabled()
? configuredFluidStream(
Objects.requireNonNull(configuredRiverWater).getFluidPalette(),
rng.nextParallelRNG(79),
"River water"
)
: fluidStream;
IrisRiverCaves configuredRiverCaves = configuredRivers == null ? null : configuredRivers.getCaves();
IrisRiverDeepPools configuredDeepPools = configuredRiverCaves == null
? null
: configuredRiverCaves.getDeepPools();
riverDeepPoolFluidStream = configuredRivers != null
&& configuredRivers.isEnabled()
&& configuredDeepPools != null
&& configuredDeepPools.isEnabled()
? configuredFluidStream(
configuredDeepPools.getFluidPalette(),
rng.nextParallelRNG(80),
"River deep-pool"
)
: riverFluidStream;
hydrologyFluidStreams = createHydrologyFluidStreams(engine.getDimension().getHydrology());
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
.zoom(engine.getDimension().getRegionZoom());
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
@@ -262,41 +251,22 @@ public class IrisComplex implements DataProvider {
regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i),
String.valueOf(i * 38445).hashCode() * 3245556666L));
caveBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(r.getCaveBiomeZoom())
.selectRarity(loadInferredBiomes(r.getCaveBiomes(), InferredType.CAVE))
.onNull(emptyBiome)
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize);
.convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
.convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize);
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
landBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getLandBiomeStyle().create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getLandZoom())
.zoom(r.getLandBiomeZoom())
.selectRarity(loadInferredBiomes(r.getLandBiomes(), InferredType.LAND))
).convertAware2D(ProceduralStream::get)
.convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
.convertAware2D(ProceduralStream::get)
.cache2D("landBiomeStream", engine, cacheSize);
inferredStreams.put(InferredType.LAND, landBiomeStream);
seaBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getSeaBiomeStyle().create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getSeaZoom())
.zoom(r.getSeaBiomeZoom())
.selectRarity(loadInferredBiomes(r.getSeaBiomes(), InferredType.SEA))
).convertAware2D(ProceduralStream::get)
.convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
.convertAware2D(ProceduralStream::get)
.cache2D("seaBiomeStream", engine, cacheSize);
inferredStreams.put(InferredType.SEA, seaBiomeStream);
shoreBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(r.getShoreBiomeZoom())
.selectRarity(loadInferredBiomes(r.getShoreBiomes(), InferredType.SHORE))
).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize);
.convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
.convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize);
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) :
@@ -315,13 +285,10 @@ public class IrisComplex implements DataProvider {
return mapped == null ? biome : mapped;
})
.cache2D("imageMappedBaseBiomeStream", engine, cacheSize);
naturalHeightStream = ProceduralStream.of((x, z) -> {
IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z);
double proceduralHeight = getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
}, Interpolated.DOUBLE).cache2DDouble("naturalHeightStream", engine, cacheSize);
naturalSlopeStream = naturalHeightStream.slope(3)
.cache2DDouble("naturalSlopeStream", engine, cacheSize);
naturalHeightStream = ProceduralStream.of(
(x, z) -> sampleNaturalTerrainHeight(engine, x, z),
Interpolated.DOUBLE
).cache2DDouble("naturalHeightStream", engine, cacheSize);
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
@@ -332,46 +299,20 @@ public class IrisComplex implements DataProvider {
: mapped;
})
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
bridgeStream,
focusBiome
).cache2D("naturalOceanStream", engine, cacheSize);
int riverFluidHeight = engine.getDimension().getRivers().getWater().getFluidHeight()
- engine.getDimension().getMinHeight();
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
if (hydrologyActive(configuredHydrology)) {
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
engine.getSeedManager().getBodies(),
engine.getDimension().getRivers(),
engine.getHeight(),
engine.getDimension(),
data,
riverFluidHeight,
(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
(x, z, naturalHeight) -> sampleHydrologyNatural(engine, x, z, naturalHeight),
(x, z) -> naturalHeightStream.getDouble(x, z),
this::sampleNaturalOcean,
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint)
));
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)
heightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z))
.cache2DDouble("heightStream", engine, cacheSize);
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.round();
@@ -379,20 +320,18 @@ public class IrisComplex implements DataProvider {
.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) : riverSurfaceStream
.convertAware2D((sample, x, z) -> resolveRiverSurfaceBiome(sample, x, z))
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, 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);
riverDistanceStream = riverSurfaceStream.convert(sample -> sample.river().present()
? sample.river().distance()
: Double.MAX_VALUE)
riverDistanceStream = ProceduralStream.ofDouble(this::resolveHydrologyDistance)
.cache2DDouble("riverDistanceStream", engine, cacheSize);
riverFlowStream = riverSurfaceStream.convert(sample -> (double) sample.river().flow())
riverFlowStream = ProceduralStream.ofDouble(this::resolveHydrologyFlow)
.cache2DDouble("riverFlowStream", engine, cacheSize);
riverCarveWeightStream = riverSurfaceStream.convert(sample -> sample.river().carveWeight())
riverCarveWeightStream = ProceduralStream.ofDouble(this::resolveHydrologyCarveWeight)
.cache2DDouble("riverCarveWeightStream", engine, cacheSize);
riverWaterSurfaceStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::waterSurfaceY)
riverWaterSurfaceStream = ProceduralStream.ofDouble(this::resolveHydrologyFluidSurface)
.cache2DDouble("riverWaterSurfaceStream", engine, cacheSize);
heightFluidStream = ProceduralStream.ofDouble((x, z) -> Math.max(
heightStream.get(x, z),
@@ -424,79 +363,204 @@ 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(
static InferredType resolveNaturalInferredType(
ProceduralStream<InferredType> bridgeStream,
IrisBiome focusBiome
IrisBiome focusBiome,
double x,
double z
) {
if (focusBiome != null) {
boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN);
return focusBiome.getInferredType();
}
return bridgeStream.convert(type -> type == InferredType.SEA);
return bridgeStream.get(x, z);
}
private IrisHydrologyNaturalSample sampleHydrologyNatural(
Engine engine,
int x,
int z,
double naturalHeight
) {
InferredType inferredType = resolveNaturalInferredType(bridgeStream, focusBiome, x, z);
IrisRegion region = regionStream.get(x, z);
IrisBiome biome = focusBiome == null
? sampleNaturalBiome(inferredType, region, naturalHeight, x, z)
: focusBiome;
return new IrisHydrologyNaturalSample(
naturalHeight,
inferredType == InferredType.SEA,
biome,
region
);
}
private boolean sampleNaturalOcean(int x, int z) {
return resolveNaturalInferredType(bridgeStream, focusBiome, x, z) == InferredType.SEA;
}
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
double proceduralHeight = getHeight(engine, x, z, engine.getSeedManager().getHeight());
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
}
static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) {
double deltaX = easternHeight - naturalHeight;
double deltaZ = southernHeight - naturalHeight;
return Math.sqrt(deltaX * deltaX + deltaZ * deltaZ);
}
private IrisBiome sampleNaturalBiome(
InferredType inferredType,
IrisRegion region,
double naturalHeight,
double x,
double z
) {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
if (mapped != null) {
return mapped;
}
IrisBiome baseBiome = implode(sampleInferredBiome(region, inferredType, x, z), x, z);
IrisBiome resolved = resolveNaturalSurfaceBiome(naturalHeight, baseBiome, region, x, z);
return resolved == baseBiome ? baseBiome : implode(resolved, x, z);
}
private IrisBiome resolveNaturalSurfaceBiome(
double height,
IrisBiome biome,
IrisRegion region,
double x,
double z
) {
if (biome == null || region == null) {
return biome;
}
double shoreHeight = region.getShoreHeight(x, z);
if (height >= fluidHeight - 1 && height <= fluidHeight + shoreHeight && !biome.isShore()) {
return sampleInferredBiome(region, InferredType.SHORE, x, z);
}
if (height > fluidHeight + shoreHeight && !biome.isLand()) {
return sampleInferredBiome(region, InferredType.LAND, x, z);
}
if (height < fluidHeight && !biome.isAquatic()) {
return sampleInferredBiome(region, InferredType.SEA, x, z);
}
if (height == fluidHeight && !biome.isShore()) {
return sampleInferredBiome(region, InferredType.SHORE, x, z);
}
return biome;
}
private IrisBiome sampleInferredBiome(
IrisRegion region,
InferredType inferredType,
double x,
double z
) {
return createInferredBiomeStream(region, inferredType).get(x, z);
}
private ProceduralStream<IrisBiome> createInferredBiomeStream(
IrisRegion region,
InferredType inferredType
) {
return preparedInferredBiomeStream(inferredBiomeStreams, region, inferredType);
}
private ProceduralStream<IrisBiome> compileInferredBiomeStream(
Engine engine,
IrisRegion region,
InferredType inferredType,
IrisBiome emptyBiome
) {
return switch (inferredType) {
case CAVE -> engine.getDimension().getCaveBiomeStyle()
.create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(region.getCaveBiomeZoom())
.selectRarity(loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE))
.onNull(emptyBiome);
case LAND -> engine.getDimension().getLandBiomeStyle()
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getLandZoom())
.zoom(region.getLandBiomeZoom())
.selectRarity(loadInferredBiomes(region.getLandBiomes(), InferredType.LAND));
case SEA -> engine.getDimension().getSeaBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getSeaZoom())
.zoom(region.getSeaBiomeZoom())
.selectRarity(loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA));
case SHORE -> engine.getDimension().getShoreBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(region.getShoreBiomeZoom())
.selectRarity(loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE));
};
}
private static boolean hydrologyActive(IrisHydrology hydrology) {
if (hydrology == null) {
return false;
}
IrisRiverHydrology rivers = hydrology.getRivers();
if (rivers != null && rivers.isEnabled()) {
return true;
}
for (IrisDeepFluidConfig deepFluid : hydrology.getDeepFluids()) {
if (deepFluid != null
&& deepFluid.getDensity() > 0D
&& (deepFluid.isContainedPools() || deepFluid.isShortChannels())) {
return true;
}
}
return false;
}
private Map<String, ProceduralStream<PlatformBlockState>> createHydrologyFluidStreams(IrisHydrology hydrology) {
if (hydrology == null) {
return Map.of();
}
LinkedHashMap<String, ProceduralStream<PlatformBlockState>> streams = new LinkedHashMap<>();
IrisRiverHydrology rivers = Objects.requireNonNull(hydrology.getRivers(), "hydrology.rivers");
int streamIndex = 0;
for (IrisRiverProfile profile : rivers.getProfiles()) {
if (profile == null) {
throw new IllegalArgumentException("hydrology.rivers.profiles cannot contain null entries");
}
String profileKey = requireHydrologyKey(profile.getId(), "hydrology.rivers.profiles[].id");
ProceduralStream<PlatformBlockState> stream = configuredFluidStream(
profile.getFluidPalette(),
rng.nextParallelRNG(7900 + streamIndex++),
"hydrology river profile " + profileKey
);
if (streams.putIfAbsent(profileKey, stream) != null) {
throw new IllegalArgumentException("Duplicate hydrology fluid profile: " + profileKey);
}
}
for (IrisDeepFluidConfig deepFluid : hydrology.getDeepFluids()) {
if (deepFluid == null) {
throw new IllegalArgumentException("hydrology.deepFluids cannot contain null entries");
}
String profileKey = requireHydrologyKey(deepFluid.getId(), "hydrology.deepFluids[].id");
ProceduralStream<PlatformBlockState> stream = configuredFluidStream(
deepFluid.getFluidPalette(),
rng.nextParallelRNG(8900 + streamIndex++),
"hydrology deep-fluid profile " + profileKey
);
if (streams.putIfAbsent(profileKey, stream) != null) {
throw new IllegalArgumentException("Duplicate hydrology fluid profile: " + profileKey);
}
}
return Collections.unmodifiableMap(streams);
}
private static String requireHydrologyKey(String key, String path) {
if (key == null || key.isBlank()) {
throw new IllegalArgumentException(path + " must not be blank");
}
return key.trim();
}
private ProceduralStream<PlatformBlockState> configuredFluidStream(
@@ -519,21 +583,81 @@ public class IrisComplex implements DataProvider {
return palette.getLayerGenerator(fluidRng, data).stream().select(blocks);
}
public PlatformBlockState resolveRiverCaveFluid(RiverCaveFluidKind fluidKind, double x, double z) {
return switch (Objects.requireNonNull(fluidKind)) {
case RIVER -> riverFluidStream.get(x, z);
case DEEP_POOL -> riverDeepPoolFluidStream.get(x, z);
};
public PlatformBlockState resolveHydrologyFluid(String profileKey, double x, double z) {
String key = Objects.requireNonNull(profileKey, "profileKey").trim();
ProceduralStream<PlatformBlockState> stream = hydrologyFluidStreams.get(key);
if (stream == null) {
throw new IllegalArgumentException("Unknown hydrology fluid profile: " + key);
}
return stream.get(x, z);
}
public PlatformBlockState resolveSurfaceFluid(double x, double z) {
IrisRiverSurfaceSample sample = riverSurfaceStream.get(x, z);
if (sample.river().present() && sample.surfaceFluid()) {
return riverFluidStream.get(x, z);
HydrologyColumnLayer layer = surfaceFluidLayer(x, z);
if (layer != null) {
return resolveHydrologyFluid(layer.profileKey(), x, z);
}
return fluidStream.get(x, z);
}
private double resolveHydrologyTerrainHeight(double x, double z) {
HydrologyColumnSample sample = hydrologySample(x, z);
return sample == null ? naturalHeightStream.get(x, z) : sample.terrainHeight();
}
private double resolveHydrologyDistance(double x, double z) {
HydrologyColumnLayer layer = surfaceLayer(x, z);
if (layer == null) {
return Double.MAX_VALUE;
}
if (layer.channel()) {
return 0D;
}
return layer.shore() ? 1D : 2D;
}
private double resolveHydrologyFlow(double x, double z) {
HydrologyColumnLayer layer = surfaceFluidLayer(x, z);
if (layer == null) {
return 0D;
}
return layer.feature().flowDeltaX() == 0 && layer.feature().flowDeltaZ() == 0 ? 0D : 1D;
}
private double resolveHydrologyCarveWeight(double x, double z) {
HydrologyColumnLayer layer = surfaceLayer(x, z);
if (layer == null) {
return 0D;
}
if (layer.channel()) {
return 1D;
}
return layer.shore() ? 0.5D : 0.25D;
}
private double resolveHydrologyFluidSurface(double x, double z) {
HydrologyColumnLayer layer = surfaceFluidLayer(x, z);
return layer == null ? fluidHeight : layer.fluidHeadY();
}
private HydrologyColumnSample hydrologySample(double x, double z) {
return hydrologyRuntime == null ? null : hydrologyRuntime.sample(x, z).orElse(null);
}
private HydrologyColumnLayer surfaceLayer(double x, double z) {
HydrologyColumnSample sample = hydrologySample(x, z);
return sample == null ? null : sample.primarySurfaceLayer().orElse(null);
}
private HydrologyColumnLayer surfaceFluidLayer(double x, double z) {
HydrologyColumnSample sample = hydrologySample(x, z);
return sample == null ? null : sample.primarySurfaceFluidLayer().orElse(null);
}
public boolean hasHydrologySurfaceFluid(int x, int z) {
return surfaceFluidLayer(x, z) != null;
}
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
switch (type) {
case CAVE:
@@ -555,7 +679,7 @@ public class IrisComplex implements DataProvider {
if (engine == null) {
throw new IllegalArgumentException("Engine is required to sample procedural terrain height");
}
return getHeight(engine, null, worldX, worldZ, engine.getSeedManager().getHeight());
return getHeight(engine, worldX, worldZ, engine.getSeedManager().getHeight());
}
private IrisRegion findRegion(IrisBiome focus, Engine engine) {
@@ -594,46 +718,29 @@ public class IrisComplex implements DataProvider {
return null;
}
private IrisBiome resolveRiverSurfaceBiome(IrisRiverSurfaceSample sample, double x, double z) {
private IrisBiome resolveHydrologySurfaceBiome(double terrainHeight, double x, double z) {
HydrologyColumnSample sample = hydrologySample(x, z);
HydrologyColumnLayer layer = sample == null ? null : sample.primarySurfaceLayer().orElse(null);
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
if (mapped != null && (riverRuntime == null || !sample.river().present())) {
if (mapped != null && layer == null) {
return mapped;
}
if (riverRuntime != null) {
if (sample.subterranean()) {
return fixBiomeType(
sample.naturalHeight(),
baseBiomeStream.get(x, z),
regionStream.get(x, z),
x,
z,
fluidHeight
);
if (layer != null) {
String biomeKey = hydrologySurfaceBiomeKey(sample);
IrisBiome biome = data.getBiomeLoader().load(biomeKey);
if (biome == null) {
throw new IllegalStateException("Hydrology biome does not exist: " + biomeKey);
}
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()
);
if (layer.biomeKey() == null) {
return biome;
}
InferredType inferredType = layer.shore()
? InferredType.SHORE
: layer.connectedFluid() ? InferredType.SEA : InferredType.LAND;
return implode(biome.withInferredType(inferredType), x, z);
}
return fixBiomeType(
sample.terrainHeight(),
terrainHeight,
baseBiomeStream.get(x, z),
regionStream.get(x, z),
x,
@@ -642,17 +749,14 @@ public class IrisComplex implements DataProvider {
);
}
static InferredType directRiverFallback(RiverSample river) {
if (!river.present()) {
return null;
static String hydrologySurfaceBiomeKey(HydrologyColumnSample sample) {
Objects.requireNonNull(sample, "sample");
HydrologyColumnLayer layer = sample.primarySurfaceLayer().orElse(null);
if (layer == null) {
return sample.parentBiomeKey();
}
if (river.state() == RiverRouteState.DRY) {
return InferredType.LAND;
}
return switch (river.section()) {
case CHANNEL, MOUTH -> InferredType.SEA;
default -> null;
};
String biomeKey = layer.biomeKey();
return biomeKey == null ? sample.parentBiomeKey() : biomeKey;
}
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
@@ -713,13 +817,7 @@ public class IrisComplex implements DataProvider {
double hi = sampledBounds.max();
double lo = sampledBounds.min();
double d = 0;
for (IrisGenerator i : generators) {
d += M.lerp(lo, hi, i.getHeight(x, z, seed + 239945));
}
return d / generators.length;
return averageGeneratorHeights(generators, lo, hi, x, z, seed + 239945);
}
private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) {
@@ -742,6 +840,22 @@ public class IrisComplex implements DataProvider {
return new NoiseBounds(boundsLow(b00), boundsHigh(b00));
}
if (fz == 0D) {
long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz);
return new NoiseBounds(
biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b00), boundsLow(b10), fx, fz),
biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b00), boundsHigh(b10), fx, fz)
);
}
if (fx == 0D) {
long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid);
return new NoiseBounds(
biLerp(boundsLow(b00), boundsLow(b00), boundsLow(b01), boundsLow(b01), fx, fz),
biLerp(boundsHigh(b00), boundsHigh(b00), boundsHigh(b01), boundsHigh(b01), fx, fz)
);
}
long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz);
long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid);
long b11 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz + grid);
@@ -835,15 +949,54 @@ public class IrisComplex implements DataProvider {
);
}
private double getHeight(Engine engine, IrisBiome b, double x, double z, long seed) {
private double getHeight(Engine engine, double x, double z, long seed) {
return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0);
}
private void prepareInferredBiomes(IrisRegion region) {
private void prepareInferredBiomes(IrisRegion region, KList<IrisRegion> preparedRegions) {
loadInferredBiomes(region.getLandBiomes(), InferredType.LAND);
loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE);
loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA);
loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE);
preparedRegions.add(region);
}
static Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compileInferredBiomeStreams(
Iterable<IrisRegion> regions,
BiFunction<IrisRegion, InferredType, ProceduralStream<IrisBiome>> compiler
) {
IdentityHashMap<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compiled = new IdentityHashMap<>();
for (IrisRegion region : regions) {
if (compiled.containsKey(region)) {
continue;
}
EnumMap<InferredType, ProceduralStream<IrisBiome>> regionStreams = new EnumMap<>(InferredType.class);
for (InferredType inferredType : INFERRED_BIOME_PREPARATION_ORDER) {
regionStreams.put(inferredType, Objects.requireNonNull(compiler.apply(region, inferredType)));
}
compiled.put(region, Collections.unmodifiableMap(regionStreams));
}
return Collections.unmodifiableMap(compiled);
}
static ProceduralStream<IrisBiome> preparedInferredBiomeStream(
Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> streams,
IrisRegion region,
InferredType inferredType
) {
Map<InferredType, ProceduralStream<IrisBiome>> regionStreams = streams.get(region);
if (regionStreams == null) {
String regionKey = region == null || region.getLoadKey() == null || region.getLoadKey().isBlank()
? "<unkeyed>"
: region.getLoadKey();
throw new IllegalStateException("Inferred-biome streams were not prepared for region '"
+ regionKey + "'.");
}
ProceduralStream<IrisBiome> stream = regionStreams.get(inferredType);
if (stream == null) {
throw new IllegalStateException("Inferred-biome stream was not prepared for type " + inferredType + ".");
}
return stream;
}
private KList<IrisBiome> loadInferredBiomes(KList<String> keys, InferredType type) {
@@ -863,6 +1016,37 @@ public class IrisComplex implements DataProvider {
generators.computeIfAbsent(cachedGenerator.getInterpolator(), (k) -> new HashSet<>()).add(cachedGenerator);
}
static GeneratorGroup[] freezeGeneratorGroups(Map<IrisInterpolator, Set<IrisGenerator>> generators) {
GeneratorGroup[] groups = new GeneratorGroup[generators.size()];
int groupIndex = 0;
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
IrisGenerator[] groupGenerators = entry.getValue().toArray(new IrisGenerator[0]);
Arrays.sort(groupGenerators, GENERATOR_ORDER);
groups[groupIndex] = new GeneratorGroup(entry.getKey(), groupGenerators);
groupIndex++;
}
Arrays.sort(groups, Comparator.comparing(GeneratorGroup::interpolator, INTERPOLATOR_ORDER));
return groups;
}
static double averageGeneratorHeights(
IrisGenerator[] generators,
double low,
double high,
double x,
double z,
long seed
) {
if (generators.length == 0) {
return 0D;
}
double height = 0D;
for (IrisGenerator generator : generators) {
height += M.lerp(low, high, generator.getHeight(x, z, seed));
}
return height / generators.length;
}
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> buildGeneratorBounds(Engine engine) {
Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> bounds = new HashMap<>();
KList<IrisBiome> allBiomes = new KList<>(generatorBiomes);
@@ -1265,9 +1449,12 @@ public class IrisComplex implements DataProvider {
}
}
record GeneratorGroup(IrisInterpolator interpolator, IrisGenerator[] generators) {
}
public void close() {
if (riverRuntime != null) {
riverRuntime.close();
if (hydrologyRuntime != null) {
hydrologyRuntime.close();
}
}
}
@@ -30,7 +30,7 @@ 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.MantleHydrologyComponent;
import art.arcane.iris.engine.mantle.components.IrisStructureComponent;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
@@ -75,6 +75,7 @@ import java.util.function.Supplier;
@EqualsAndHashCode(exclude = "engine")
@ToString(exclude = "engine")
public class IrisEngineMantle implements EngineMantle {
public static final String STORAGE_FOLDER_NAME = "mantle-hydrology";
private static final boolean BUKKIT_PRESENT = IrisMatterSupport.isBukkitPresent();
private final Engine engine;
private final Mantle<Matter> mantle;
@@ -90,7 +91,7 @@ public class IrisEngineMantle implements EngineMantle {
this.mantle = createMantle(engine);
components = new KMap<>();
registerComponent(new MantleCarvingComponent(this));
registerComponent(new MantleRiverHydrologyComponent(this));
registerComponent(new MantleHydrologyComponent(this));
object = new MantleObjectComponent(this);
registerComponent(object);
registerComponent(new MantleFloatingObjectComponent(this));
@@ -126,7 +127,7 @@ public class IrisEngineMantle implements EngineMantle {
List<MantleComponent> pass = passes.get(i);
int passBlockRadius = pass.stream()
.filter(MantleComponent::isEnabled)
.mapToInt(MantleComponent::getRadius)
.mapToInt(MantleComponent::getOutputRadius)
.max()
.orElse(0);
int passInputRadius = pass.stream()
@@ -180,19 +181,15 @@ public class IrisEngineMantle implements EngineMantle {
disabled.addIfMissing(ReservedFlag.CARVED);
}
if (disabled.contains(ReservedFlag.CARVED)
|| !isRiverHydrologyEnabled(getDimension())) {
|| !isHydrologyEnabled(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);
static boolean isHydrologyEnabled(IrisDimension dimension) {
return MantleHydrologyComponent.isEnabledFor(dimension);
}
@Override
@@ -202,7 +199,7 @@ public class IrisEngineMantle implements EngineMantle {
private static Mantle<Matter> createMantle(Engine engine) {
IrisMatterSupport.ensureRegistered();
File dataFolder = new File(engine.getWorld().worldFolder(), "mantle");
File dataFolder = new File(engine.getWorld().worldFolder(), STORAGE_FOLDER_NAME);
int worldHeight = engine.getTarget().getHeight();
MantleDataAdapter<Matter> adapter = createDataAdapter(engine::getData);
MantleHooks hooks = createHooks(EnginePanic.scoped("world " + engine.getWorld().name()));
@@ -14,7 +14,6 @@ import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.interpolation.NoiseBounds;
import art.arcane.iris.spi.PlatformBlockState;
@@ -112,13 +111,14 @@ public class UpperDimensionContext implements DataProvider {
registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators);
}
IrisComplex.GeneratorGroup[] generatorGroups = IrisComplex.freezeGeneratorGroups(generators);
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
for (IrisComplex.GeneratorGroup group : generatorGroups) {
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
for (IrisBiome biome : allBiomes) {
double min = 0D;
double max = 0D;
for (IrisGenerator gen : entry.getValue()) {
for (IrisGenerator gen : group.generators()) {
String key = gen.getLoadKey();
if (key == null || key.isBlank()) {
continue;
@@ -128,7 +128,7 @@ public class UpperDimensionContext implements DataProvider {
}
interpolatorBounds.put(biome, new NoiseBounds(min, max));
}
generatorBounds.put(entry.getKey(), interpolatorBounds);
generatorBounds.put(group.interpolator(), interpolatorBounds);
}
ProceduralStream<Double> regionStyleStream = upperDim.getRegionStyle()
@@ -204,10 +204,10 @@ public class UpperDimensionContext implements DataProvider {
return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z);
}
double interpolatedHeight = 0;
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
IrisInterpolator interpolator = entry.getKey();
Set<IrisGenerator> gens = entry.getValue();
if (gens.isEmpty()) {
for (IrisComplex.GeneratorGroup group : generatorGroups) {
IrisInterpolator interpolator = group.interpolator();
IrisGenerator[] groupGenerators = group.generators();
if (groupGenerators.length == 0) {
continue;
}
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
@@ -223,7 +223,7 @@ public class UpperDimensionContext implements DataProvider {
}
double bMin = 0D;
double bMax = 0D;
for (IrisGenerator gen : gens) {
for (IrisGenerator gen : groupGenerators) {
String key = gen.getLoadKey();
if (key == null || key.isBlank()) {
continue;
@@ -239,11 +239,14 @@ public class UpperDimensionContext implements DataProvider {
});
double hi = sampledBounds.max();
double lo = sampledBounds.min();
double d = 0;
for (IrisGenerator gen : gens) {
d += M.lerp(lo, hi, gen.getHeight(x, z, heightSeed + 239945));
}
interpolatedHeight += d / gens.size();
interpolatedHeight += IrisComplex.averageGeneratorHeights(
groupGenerators,
lo,
hi,
x,
z,
heightSeed + 239945
);
}
double proceduralHeight = Math.max(
Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight),
@@ -0,0 +1,21 @@
package art.arcane.iris.engine.actuator;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
final class HydrologyFluidLayerSelector {
private HydrologyFluidLayerSelector() {
}
static PlatformBlockState select(
KList<PlatformBlockState> seaLayers,
int depth,
PlatformBlockState fluid,
boolean hydrologyOwned
) {
if (!hydrologyOwned && seaLayers != null && seaLayers.hasIndex(depth)) {
return seaLayers.get(depth);
}
return fluid;
}
}
@@ -26,9 +26,10 @@ import art.arcane.iris.engine.decorator.IrisSurfaceDecorator;
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.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
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;
@@ -52,7 +53,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
@Getter
private final EngineDecorator seaFloorDecorator;
@Getter
private final EngineDecorator shoreLineDecorator;
private final IrisShoreLineDecorator shoreLineDecorator;
private final boolean shouldRay;
public IrisDecorantActuator(Engine engine) {
@@ -66,10 +67,82 @@ 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);
static ShorelineDecorationMode shorelineDecorationMode(
HydrologyColumnSample sample,
int height,
int fluidHeight
) {
HydrologyColumnLayer surfaceLayer = sample == null
? null
: sample.primarySurfaceLayer().orElse(null);
if (surfaceLayer != null) {
return surfaceLayer.shore()
? ShorelineDecorationMode.ACCEPTED
: ShorelineDecorationMode.NONE;
}
return height == fluidHeight
? ShorelineDecorationMode.LEGACY
: ShorelineDecorationMode.NONE;
}
static boolean hasConnectedSurfaceWater(
Hunk<PlatformBlockState> output,
int x,
int z,
int height,
int fluidHeight
) {
return height < fluidHeight
&& height + 1 < output.getHeight()
&& IrisSurfaceDecorator.hasConnectedWater(output, x, height + 1, z);
}
static boolean hasConnectedWaterColumn(
Hunk<PlatformBlockState> output,
int x,
int z,
int lowerY,
int upperY
) {
int boundedLowerY = Math.max(0, lowerY);
int boundedUpperY = Math.min(output.getHeight() - 1, upperY);
if (boundedLowerY > boundedUpperY) {
return false;
}
for (int y = boundedLowerY; y <= boundedUpperY; y++) {
if (!IrisSurfaceDecorator.hasConnectedWater(output, x, y, z)) {
return false;
}
}
return true;
}
static void restoreUnsupportedAquaticPlacement(
Hunk<PlatformBlockState> output,
int x,
int y,
int z,
PlatformBlockState original
) {
PlatformBlockState placed = output.get(x, y, z);
if (placed != original && IrisSurfaceDecorator.isAquaticPlacement(placed)) {
output.set(x, y, z, original);
}
}
static boolean isFallingWaterfallThroat(HydrologyColumnSample sample) {
if (sample == null) {
return false;
}
for (HydrologyColumnLayer layer : sample.layers()) {
if ((layer.feature().type() == HydrologyFeatureType.WATERFALL
|| layer.feature().type() == HydrologyFeatureType.SINKHOLE)
&& layer.channel()
&& layer.fallingFluid()) {
return true;
}
}
return false;
}
@BlockCoordinates
@@ -80,7 +153,6 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
}
PrecisionStopwatch p = PrecisionStopwatch.start();
for (int i = 0; i < output.getWidth(); i++) {
int height;
int realX = Math.round(x + i);
@@ -94,25 +166,57 @@ 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());
HydrologyColumnSample hydrology = getComplex().getHydrologyRuntime() == null
? null
: getComplex().getHydrologyRuntime().sample(realX, realZ).orElse(null);
if (isFallingWaterfallThroat(hydrology)) {
continue;
}
int surfaceFluidHeight = (int) Math.round(
getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) {
continue;
}
if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j))
&& height + 1 < output.getHeight() && B.isFluid(output.get(i, height + 1, j))) {
if (PREDICATE_SOLID.test(output.get(i, height, j))
&& hasConnectedSurfaceWater(output, i, j, height, surfaceFluidHeight)) {
int seaSurfaceY = surfaceFluidHeight + 1;
PlatformBlockState seaSurfaceOriginal = seaSurfaceY < output.getHeight()
? output.get(i, seaSurfaceY, j)
: null;
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, surfaceFluidHeight, getEngine().getHeight());
getSeaFloorDecorator().decorate(i, j,
realX, realZ, output, biome, height + 1,
surfaceFluidHeight + 1);
if (seaSurfaceY < output.getHeight()) {
restoreUnsupportedAquaticPlacement(
output, i, seaSurfaceY, j, seaSurfaceOriginal);
}
if (hasConnectedWaterColumn(output, i, j, height + 1, surfaceFluidHeight)) {
getSeaFloorDecorator().decorate(i, j,
realX, realZ, output, biome, height + 1,
surfaceFluidHeight + 1);
}
}
if (shouldDecorateShoreline(riverSurface, height)) {
ShorelineDecorationMode shorelineMode = shorelineDecorationMode(
hydrology,
height,
surfaceFluidHeight
);
if (shorelineMode == ShorelineDecorationMode.ACCEPTED) {
getShoreLineDecorator().decorateAcceptedShore(
i,
j,
realX,
realZ,
output,
biome,
height,
getEngine().getHeight()
);
} else if (shorelineMode == ShorelineDecorationMode.LEGACY) {
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),
@@ -150,4 +254,10 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
private boolean shouldRayDecorate() {
return false; // TODO CAVES
}
enum ShorelineDecorationMode {
NONE,
LEGACY,
ACCEPTED
}
}
@@ -23,6 +23,8 @@ import art.arcane.iris.engine.framework.EngineAssignedActuator;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisOreGenerator;
@@ -117,7 +119,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
}
int topY = Math.min(hf, chunkHeight - 1);
PlatformBlockState fluid = complex.resolveSurfaceFluid(realX, realZ);
HydrologyColumnSample hydrology = complex.getHydrologyRuntime() == null
? null
: complex.getHydrologyRuntime().sample(realX, realZ).orElse(null);
HydrologyColumnLayer hydrologyFluid = hydrology == null
? null
: hydrology.primarySurfaceFluidLayer().orElse(null);
PlatformBlockState fluid = hydrologyFluid == null
? complex.resolveSurfaceFluid(realX, realZ)
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
PlatformBlockState rock = rockCache.get(xf, zf);
PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
@@ -159,15 +169,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
if (i > he && i <= hf) {
int fdepth = hf - i;
if (fblocks == null) {
if (hydrologyFluid == null && fblocks == null) {
fblocks = biome.generateSeaLayers(realX, realZ, localRng, hf - he, data);
}
if (fblocks.hasIndex(fdepth)) {
h.setRaw(xf, i, zf, fblocks.get(fdepth));
} else {
h.setRaw(xf, i, zf, fluid);
}
h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select(
fblocks,
fdepth,
fluid,
hydrologyFluid != null
));
continue;
}
@@ -54,6 +54,32 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
return;
}
place(x, z, realX, realZ, data, biome, height, max);
}
public void decorateAcceptedShore(
int x,
int z,
int realX,
int realZ,
Hunk<PlatformBlockState> data,
IrisBiome biome,
int height,
int max
) {
place(x, z, realX, realZ, data, biome, height, max);
}
private void place(
int x,
int z,
int realX,
int realZ,
Hunk<PlatformBlockState> data,
IrisBiome biome,
int height,
int max
) {
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
@@ -71,6 +97,8 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
return;
}
IrisSurfaceDecorator.AquaticPlacementSnapshot aquaticSnapshot = IrisSurfaceDecorator.captureAquaticPlacement(
decorator, getData(), data, x, z, height, max);
if (!decorator.isStacking()) {
int targetY = height + 1;
if (targetY >= data.getHeight()
@@ -80,6 +108,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
data.set(x, targetY, z, block);
aquaticSnapshot.restoreIfUnsupported(data, x, z);
}
return;
}
@@ -100,6 +129,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
data.set(x, targetY, z, block);
aquaticSnapshot.restoreIfUnsupported(data, x, z);
}
return;
}
@@ -123,5 +153,6 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
}
data.set(x, targetY, z, block);
}
aquaticSnapshot.restoreIfUnsupported(data, x, z);
}
}
@@ -18,11 +18,13 @@
package art.arcane.iris.engine.decorator;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG;
@@ -64,6 +66,9 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
}
boolean underwater = isUnderwater(inferredType, height, fluidHeight);
if (underwater && !hasFluidAbove(data, x, height, z)) {
return;
}
boolean caveSkipFluid = skipsFluid(inferredType);
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
@@ -72,6 +77,8 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
return;
}
AquaticPlacementSnapshot aquaticSnapshot = captureAquaticPlacement(
decorator, getData(), data, x, z, height, max);
if (decorator.isStacking()) {
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
opts.reset();
@@ -79,11 +86,13 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
opts.fluidHeight = fluidHeight;
opts.caveSkipFluid = caveSkipFluid;
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts);
aquaticSnapshot.restoreIfUnsupported(data, x, z);
return;
}
DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
aquaticSnapshot.restoreIfUnsupported(data, x, z);
}
static boolean isUnderwater(InferredType inferredType, int height, int fluidHeight) {
@@ -93,4 +102,148 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
static boolean skipsFluid(InferredType inferredType) {
return inferredType == InferredType.CAVE;
}
static boolean hasFluidAbove(Hunk<PlatformBlockState> data, int x, int height, int z) {
return height + 1 < data.getHeight() && data.get(x, height + 1, z).isFluid();
}
public static boolean isAquaticPlacement(PlatformBlockState state) {
if (state == null) {
return false;
}
if (state.isWaterLogged()) {
return true;
}
String material = IrisProceduralBlocks.materialKey(state);
if (material.equals("minecraft:seagrass")
|| material.equals("minecraft:tall_seagrass")
|| material.equals("minecraft:kelp")
|| material.equals("minecraft:kelp_plant")
|| material.equals("minecraft:sea_pickle")) {
return true;
}
return material.startsWith("minecraft:")
&& material.contains("_coral")
&& !material.contains(":dead_");
}
public static boolean hasConnectedWater(Hunk<PlatformBlockState> data, int x, int y, int z) {
if (y < 0 || y >= data.getHeight() || !isWater(data.get(x, y, z))) {
return false;
}
return isWaterNeighbor(data, x - 1, y, z)
|| isWaterNeighbor(data, x + 1, y, z)
|| isWaterNeighbor(data, x, y, z - 1)
|| isWaterNeighbor(data, x, y, z + 1);
}
static AquaticPlacementSnapshot captureAquaticPlacement(
IrisDecorator decorator,
IrisData irisData,
Hunk<PlatformBlockState> data,
int x,
int z,
int height,
int max
) {
if (!mayPlaceAquatic(decorator, irisData)) {
return AquaticPlacementSnapshot.EMPTY;
}
int highestOffset = decorator.isStacking()
? maximumStackHeight(decorator, max)
: 2;
int lowerY = Math.max(0, height);
int upperY = Math.min(data.getHeight() - 1, height + highestOffset);
if (lowerY > upperY) {
return AquaticPlacementSnapshot.EMPTY;
}
PlatformBlockState[] originals = new PlatformBlockState[upperY - lowerY + 1];
boolean[] connectedWater = new boolean[originals.length];
for (int i = 0; i < originals.length; i++) {
int y = lowerY + i;
originals[i] = data.get(x, y, z);
connectedWater[i] = hasConnectedWater(data, x, y, z);
}
return new AquaticPlacementSnapshot(lowerY, originals, connectedWater);
}
private static boolean mayPlaceAquatic(IrisDecorator decorator, IrisData irisData) {
PlatformBlockState[] palette = decorator.getBlockDataArray(irisData);
if (palette != null) {
for (PlatformBlockState state : palette) {
if (isAquaticPlacement(state)) {
return true;
}
}
}
PlatformBlockState[] topPalette = decorator.getBlockDataTopsArray(irisData);
if (topPalette != null) {
for (PlatformBlockState state : topPalette) {
if (isAquaticPlacement(state)) {
return true;
}
}
}
return decorator.getForceBlock() != null
&& isAquaticPlacement(decorator.getForceBlock().getBlockData(irisData));
}
private static int maximumStackHeight(IrisDecorator decorator, int max) {
if (decorator.isScaleStack()) {
return Math.max(1, decorator.getAbsoluteMaxStack());
}
return Math.max(1, Math.min(Math.max(1, max), decorator.getStackMax()));
}
private static boolean isWaterNeighbor(Hunk<PlatformBlockState> data, int x, int y, int z) {
if (x < 0 || x >= data.getWidth() || z < 0 || z >= data.getDepth()) {
return false;
}
return isWater(data.get(x, y, z));
}
private static boolean isWater(PlatformBlockState state) {
return state != null && state.isWater();
}
static final class AquaticPlacementSnapshot {
private static final AquaticPlacementSnapshot EMPTY = new AquaticPlacementSnapshot(
0, new PlatformBlockState[0], new boolean[0]);
private final int lowerY;
private final PlatformBlockState[] originals;
private final boolean[] connectedWater;
private AquaticPlacementSnapshot(
int lowerY,
PlatformBlockState[] originals,
boolean[] connectedWater
) {
this.lowerY = lowerY;
this.originals = originals;
this.connectedWater = connectedWater;
}
void restoreIfUnsupported(Hunk<PlatformBlockState> data, int x, int z) {
if (!containsUnsupportedAquatic(data, x, z)) {
return;
}
for (int i = 0; i < originals.length; i++) {
data.set(x, lowerY + i, z, originals[i]);
}
}
private boolean containsUnsupportedAquatic(Hunk<PlatformBlockState> data, int x, int z) {
for (int i = 0; i < originals.length; i++) {
PlatformBlockState placed = data.get(x, lowerY + i, z);
if (placed != originals[i] && isAquaticPlacement(placed) && !connectedWater[i]) {
return true;
}
}
return false;
}
}
}
@@ -42,8 +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.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveStorage;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
@@ -246,7 +246,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(
getMantle().getMantle(), x, y, z);
if (hydrology != null && !hydrology.floodedBiomeKey().isEmpty()) {
IrisBiome biome = getData().getBiomeLoader().load(hydrology.floodedBiomeKey());
@@ -455,6 +455,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
default PlacedObject getObjectPlacement(int x, int y, int z, MantleChunk<Matter> chunk) {
String objectAt = chunk.get(x & 15, y, z & 15, String.class);
return resolveObjectPlacementMarker(x, z, objectAt);
}
default PlacedObject resolveObjectPlacementMarker(int x, int z, @Nullable String objectAt) {
if (objectAt == null || objectAt.isEmpty()) {
return null;
}
@@ -562,7 +566,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return;
}
if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) {
getMantle().cleanupChunk(x, z);
getMantle().cleanupChunksCoveredBy(
x,
z,
false,
EngineMantle.ChunkCleanupCallback.NONE
);
}
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.volmlib.util.collection.KList;
public interface EnginePlatformHooks {
@@ -70,4 +71,8 @@ public interface EnginePlatformHooks {
default boolean shouldBypassMantleStages(Engine engine) {
return false;
}
default boolean shouldGenerateMantleComponent(Engine engine, MantleComponent component) {
return true;
}
}
@@ -328,7 +328,11 @@ public final class HintedLocator<T> implements Locator<T> {
}
if (!surfaceRegions.isEmpty()) {
return SearchPlan.of(surfaceCoarse(complex, surfaceRegions, landHosts, seaHosts, shoreHosts), BIOME_STRIDE_CHUNKS, null);
return SearchPlan.of(
surfaceCoarse(complex, surfaceRegions, landHosts, seaHosts, shoreHosts),
BIOME_STRIDE_CHUNKS,
null
);
}
if (!caveRegions.isEmpty()) {
@@ -341,6 +345,11 @@ public final class HintedLocator<T> implements Locator<T> {
return SearchPlan.of(coarse, BIOME_STRIDE_CHUNKS, caveExact);
}
for (IrisBiome reachableBiome : engine.getDimension().getReachableBiomes(engine)) {
if (reachableBiome != null && biomeKey.equals(reachableBiome.getLoadKey())) {
return SearchPlan.unpruned();
}
}
return SearchPlan.impossible();
}
@@ -576,6 +585,36 @@ public final class HintedLocator<T> implements Locator<T> {
return false;
}
private KList<IrisBiome> closure(String rootKey) {
KList<IrisBiome> biomes = new KList<>();
if (rootKey == null) {
return biomes;
}
KSet<String> visited = new KSet<>();
ArrayDeque<String> queue = new ArrayDeque<>();
queue.add(rootKey);
while (!queue.isEmpty()) {
String key = queue.poll();
if (!visited.add(key)) {
continue;
}
IrisBiome biome = load(key);
if (biome == null) {
continue;
}
biomes.add(biome);
for (String child : biome.getChildren()) {
if (child != null && !visited.contains(child)) {
queue.add(child);
}
}
}
return biomes;
}
private boolean closurePlacesObject(String rootKey, String objectKey) {
if (rootKey == null || objectKey == null) {
return false;
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.framework;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.nms.container.BlockPos;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisRegion;
@@ -53,10 +54,22 @@ public interface Locator<T> {
}
static Locator<IrisBiome> surfaceBiome(String loadKey) {
Locator<IrisBiome> exact = (e, c) -> e.getSurfaceBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
Locator<IrisBiome> exact = (e, c) -> chunkContainsSurfaceBiome(e, c, loadKey);
return new HintedLocator<>(exact, (engine) -> HintedLocator.biomePlan(engine, loadKey));
}
static boolean chunkContainsSurfaceBiome(Engine engine, Position2 chunk, String loadKey) {
int minimumX = chunk.getX() << 4;
int minimumZ = chunk.getZ() << 4;
IrisBiome center = engine.getSurfaceBiome(minimumX + 8, minimumZ + 8);
if (center != null && loadKey.equals(center.getLoadKey())) {
return true;
}
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
return hydrology != null
&& hydrology.hasAcceptedSurfaceBiomeInChunk(loadKey, chunk.getX(), chunk.getZ());
}
static Locator<art.arcane.iris.engine.object.IrisStructure> structure(String key) {
return (e, c) -> IrisStructureLocator.startsInChunk(e, key, c.getX(), c.getZ());
}
@@ -3,8 +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.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveStorage;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterCavern;
@@ -288,7 +288,7 @@ public final class StructureCaveAnchorResolver {
int mantleY,
int blockZ
) {
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
HydrologyCaveCell hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ);
return acceptsAnchorFluid(
placement.isUnderwater(),
@@ -310,7 +310,7 @@ public final class StructureCaveAnchorResolver {
static boolean acceptsAnchorFluid(
boolean underwater,
MatterCavern cavern,
RiverCaveHydrology hydrology,
HydrologyCaveCell hydrology,
int mantleY,
int defaultLavaHeight
) {
@@ -332,12 +332,12 @@ public final class StructureCaveAnchorResolver {
private static MatterCavern cavernAt(Engine engine, int blockX, int mantleY, int blockZ) {
MatterCavern baseline = engine.getMantle().getMantle()
.get(blockX, mantleY, blockZ, MatterCavern.class);
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
HydrologyCaveCell 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(
private static HydrologyCaveCell hydrologyAt(Engine engine, int blockX, int mantleY, int blockZ) {
return HydrologyCaveStorage.getIfPresent(
engine.getMantle().getMantle(), blockX, mantleY, blockZ);
}
@@ -20,21 +20,29 @@ 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.hydrology.HydrologyCandidateKind;
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticCandidate;
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticRenderSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.HydrologyRenderSample;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
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.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.function.BooleanSupplier;
@@ -43,11 +51,22 @@ 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 HEADWATER = new Color(99, 184, 230).getRGB();
private static final int HEADWATER_DIRECTION = new Color(13, 38, 55).getRGB();
private static final int RIVER_CHANNEL = new Color(48, 112, 190).getRGB();
private static final int RIVER_RIFFLE = new Color(96, 166, 214).getRGB();
private static final int RIVER_CASCADE = new Color(135, 204, 232).getRGB();
private static final int SUBTERRANEAN_CHANNEL = new Color(102, 86, 196).getRGB();
private static final int RIDGE_TUNNEL = new Color(120, 99, 174).getRGB();
private static final int COASTAL_GROTTO = new Color(51, 174, 174).getRGB();
private static final int INLAND_GROTTO = new Color(116, 76, 150).getRGB();
private static final int RIVER_SINKHOLE = new Color(176, 146, 232).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 RIVER_WATERFALL = new Color(190, 236, 255).getRGB();
private static final int CONFIRMED_DEEP_LAVA = new Color(194, 50, 22).getRGB();
private static final int PROJECTED_SOURCE = new Color(225, 75, 178).getRGB();
private static final int PROJECTED_OUTLET = new Color(235, 145, 54).getRGB();
private static final int PROJECTED_DEEP_FLUID = new Color(175, 70, 118).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();
@@ -150,15 +169,53 @@ public final class IrisRenderer {
return image;
}
public static int riverColor(RiverSection section) {
Objects.requireNonNull(section, "section");
return switch (section) {
case CHANNEL -> RIVER_CHANNEL;
public static int waterfallColor() {
return RIVER_WATERFALL;
}
public static int surfaceWaterColor() {
return RIVER_CHANNEL;
}
public static int subterraneanColor() {
return SUBTERRANEAN_CHANNEL;
}
public static int confirmedDeepLavaColor() {
return CONFIRMED_DEEP_LAVA;
}
public static int hydrologyFeatureColor(HydrologyFeatureType type) {
Objects.requireNonNull(type, "type");
return switch (type) {
case SURFACE_POOL -> RIVER_CHANNEL;
case RIFFLE -> RIVER_RIFFLE;
case CASCADE -> RIVER_CASCADE;
case WATERFALL -> RIVER_WATERFALL;
case SINKHOLE -> RIVER_SINKHOLE;
case RIDGE_BORE -> RIDGE_TUNNEL;
case UNDERGROUND_POOL, UNDERGROUND_DROP -> SUBTERRANEAN_CHANNEL;
case COASTAL_GROTTO -> COASTAL_GROTTO;
case INLAND_GROTTO -> INLAND_GROTTO;
case MOUTH -> RIVER_MOUTH;
case BANK -> RIVER_BANK;
case DRY_CHANNEL -> DRY_CHANNEL;
case DRY_BANK -> DRY_BANK;
case NONE -> NO_RIVER;
case DEEP_POOL, DEEP_CHANNEL -> CONFIRMED_DEEP_LAVA;
};
}
public static int headwaterColor() {
return HEADWATER;
}
public static int headwaterDirectionColor() {
return HEADWATER_DIRECTION;
}
public static int hydrologyDiagnosticColor(HydrologyCandidateKind kind) {
Objects.requireNonNull(kind, "kind");
return switch (kind) {
case SOURCE -> PROJECTED_SOURCE;
case OUTLET -> PROJECTED_OUTLET;
case DEEP_FLUID -> PROJECTED_DEEP_FLUID;
};
}
@@ -277,10 +334,11 @@ public final class IrisRenderer {
BooleanSupplier cancelled,
boolean composite
) {
IrisRiverRuntime runtime = complex.getRiverRuntime();
IrisHydrologyRuntime runtime = complex.getHydrologyRuntime();
if (runtime == null) {
return;
}
Map<Long, HydrologyFeatureRef> headwaters = new LinkedHashMap<>();
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) {
@@ -295,6 +353,8 @@ public final class IrisRenderer {
runtime,
cancelled,
composite,
!composite,
headwaters,
pixelX,
pixelZ,
Math.min(blockPixels, resolution - pixelX),
@@ -302,6 +362,7 @@ public final class IrisRenderer {
);
}
}
renderHeadwaterDirections(pixels, resolution, startX, startZ, step, headwaters);
}
private static void renderRiverBlock(
@@ -310,58 +371,163 @@ public final class IrisRenderer {
double startX,
double startZ,
double step,
IrisRiverRuntime runtime,
IrisHydrologyRuntime runtime,
BooleanSupplier cancelled,
boolean composite,
boolean diagnostics,
Map<Long, HydrologyFeatureRef> headwaters,
int pixelX,
int pixelZ,
int width,
int height
) {
checkCancelled(cancelled);
RiverSample sample = runtime.sampleFootprint(
HydrologyRenderSample sample = runtime.sampleRenderFootprint(
startX + pixelX * step,
startZ + pixelZ * step,
startX + (pixelX + width) * step,
startZ + (pixelZ + height) * step
);
if (!sample.present()) {
HydrologyDiagnosticRenderSample diagnosticSample = diagnostics
? runtime.sampleDiagnosticFootprint(
startX + pixelX * step,
startZ + pixelZ * step,
startX + (pixelX + width) * step,
startZ + (pixelZ + height) * step
)
: null;
collectHeadwaters(sample, headwaters);
if (!sample.present() && (diagnosticSample == null || !diagnosticSample.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;
int color = sample.present()
? hydrologyColor(sample)
: hydrologyDiagnosticColor(diagnosticSample);
pixels[index] = composite
? blend(pixels[index], color, 0.72D)
: 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,
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, diagnostics,
headwaters,
pixelX, pixelZ, leftWidth, topHeight);
if (rightWidth > 0) {
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, diagnostics,
headwaters,
pixelX + leftWidth, pixelZ, rightWidth, topHeight);
}
if (bottomHeight > 0) {
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, diagnostics,
headwaters,
pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
if (rightWidth > 0) {
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, diagnostics,
headwaters,
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 static void collectHeadwaters(
HydrologyRenderSample sample,
Map<Long, HydrologyFeatureRef> headwaters
) {
for (HydrologyFeatureRef feature : sample.features()) {
if (!feature.source()) {
continue;
}
HydrologyFeatureRef existing = headwaters.putIfAbsent(feature.id(), feature);
if (existing != null && !existing.equals(feature)) {
throw new IllegalStateException("Hydrology headwater feature id collision in renderer footprint.");
}
}
}
private static void renderHeadwaterDirections(
int[] pixels,
int resolution,
double startX,
double startZ,
double step,
Map<Long, HydrologyFeatureRef> headwaters
) {
if (resolution < 5 || headwaters.isEmpty()) {
return;
}
ArrayList<HydrologyFeatureRef> ordered = new ArrayList<>(headwaters.values());
ordered.sort(Comparator.comparingLong(HydrologyFeatureRef::id));
for (HydrologyFeatureRef feature : ordered) {
int pixelX = worldToPixel(feature.x(), startX, step, resolution);
int pixelZ = worldToPixel(feature.z(), startZ, step, resolution);
drawHeadwaterDirection(
pixels,
resolution,
pixelX,
pixelZ,
feature.flowDeltaX(),
feature.flowDeltaZ()
);
}
}
private static int worldToPixel(int coordinate, double start, double step, int resolution) {
int pixel = (int) StrictMath.floor((coordinate + 0.5D - start) / step);
int markerRadius = 2;
return Math.max(markerRadius, Math.min(resolution - markerRadius - 1, pixel));
}
private static void drawHeadwaterDirection(
int[] pixels,
int resolution,
int centerX,
int centerZ,
int flowX,
int flowZ
) {
int markerRadius = Math.min(2, (resolution - 1) / 2);
for (int deltaZ = -markerRadius; deltaZ <= markerRadius; deltaZ++) {
for (int deltaX = -markerRadius; deltaX <= markerRadius; deltaX++) {
setPixel(pixels, resolution, centerX + deltaX, centerZ + deltaZ, HEADWATER);
}
}
if (flowX == 0 && flowZ == 0) {
setPixel(pixels, resolution, centerX, centerZ, HEADWATER_DIRECTION);
return;
}
setPixel(pixels, resolution, centerX - flowX * 2, centerZ - flowZ * 2, HEADWATER_DIRECTION);
setPixel(pixels, resolution, centerX - flowX, centerZ - flowZ, HEADWATER_DIRECTION);
setPixel(pixels, resolution, centerX, centerZ, HEADWATER_DIRECTION);
setPixel(pixels, resolution, centerX + flowX, centerZ + flowZ, HEADWATER_DIRECTION);
int tipX = centerX + flowX * 2;
int tipZ = centerZ + flowZ * 2;
setPixel(pixels, resolution, tipX, tipZ, HEADWATER_DIRECTION);
setPixel(
pixels,
resolution,
tipX - flowX - flowZ,
tipZ - flowZ + flowX,
HEADWATER_DIRECTION
);
setPixel(
pixels,
resolution,
tipX - flowX + flowZ,
tipZ - flowZ - flowX,
HEADWATER_DIRECTION
);
}
private static void setPixel(int[] pixels, int resolution, int x, int z, int color) {
if (x < 0 || x >= resolution || z < 0 || z >= resolution) {
return;
}
pixels[z * resolution + x] = color;
}
private PixelShader biomeShader(ProceduralStream<IrisBiome> stream, RenderType currentType) {
@@ -399,19 +565,47 @@ public final class IrisRenderer {
}
private int riverColor(IrisComplex complex, double x, double z, double step) {
IrisRiverRuntime runtime = complex.getRiverRuntime();
IrisHydrologyRuntime runtime = complex.getHydrologyRuntime();
if (runtime == null) {
return riverColor(RiverSection.NONE);
return NO_RIVER;
}
double endX = x + step;
double endZ = z + step;
RiverSample sample = runtime.sampleFootprint(
HydrologyRenderSample sample = runtime.sampleRenderFootprint(
StrictMath.min(x, endX),
StrictMath.min(z, endZ),
StrictMath.max(x, endX),
StrictMath.max(z, endZ)
);
return riverColor(sample.section());
if (sample.present()) {
return hydrologyColor(sample);
}
return hydrologyDiagnosticColor(runtime.sampleDiagnosticFootprint(
StrictMath.min(x, endX),
StrictMath.min(z, endZ),
StrictMath.max(x, endX),
StrictMath.max(z, endZ)
));
}
static int hydrologyColor(HydrologyRenderSample sample) {
Objects.requireNonNull(sample, "sample");
HydrologyFeatureRef feature = sample.primaryFeature().orElse(null);
if (feature == null) {
return NO_RIVER;
}
if (feature.source()) {
return HEADWATER;
}
return hydrologyFeatureColor(feature.type());
}
static int hydrologyDiagnosticColor(HydrologyDiagnosticRenderSample sample) {
Objects.requireNonNull(sample, "sample");
HydrologyDiagnosticCandidate candidate = sample.candidates().isEmpty()
? null
: sample.candidates().getFirst();
return candidate == null ? NO_RIVER : hydrologyDiagnosticColor(candidate.kind());
}
private int continentColor(double x, double z) {
@@ -0,0 +1,36 @@
package art.arcane.iris.engine.hydrology;
import java.util.List;
import java.util.Objects;
public record DrainageEdge(
long id,
long upstreamNodeId,
long downstreamNodeId,
long outletId,
double cost,
int contributingSurfaceSources,
int contributingUndergroundSources,
List<HydrologyPoint> centerline
) {
public DrainageEdge {
if (upstreamNodeId == downstreamNodeId) {
throw new IllegalArgumentException("A drainage edge cannot point to the same node.");
}
if (!Double.isFinite(cost) || cost <= 0D) {
throw new IllegalArgumentException("Drainage edge cost must be finite and positive.");
}
if (contributingSurfaceSources < 0 || contributingUndergroundSources < 0) {
throw new IllegalArgumentException("Contributing source counts cannot be negative.");
}
Objects.requireNonNull(centerline, "centerline");
centerline = List.copyOf(centerline);
if (centerline.size() < 2) {
throw new IllegalArgumentException("A drainage edge requires at least two centerline points.");
}
}
public int totalContributingSources() {
return contributingSurfaceSources + contributingUndergroundSources;
}
}
@@ -0,0 +1,26 @@
package art.arcane.iris.engine.hydrology;
import java.util.Objects;
public record DrainageNode(
long id,
int x,
int z,
HydrologyTerrainSample terrain,
double potential,
long outletId
) {
public DrainageNode {
Objects.requireNonNull(terrain, "terrain");
if (!Double.isFinite(potential) || potential < 0D) {
throw new IllegalArgumentException("Drainage potential must be finite and non-negative.");
}
if (terrain.ocean()) {
throw new IllegalArgumentException("Drainage nodes must remain landward of the first ocean column.");
}
}
public HydrologyPoint naturalPoint() {
return new HydrologyPoint(x, terrain.naturalHeight(), z);
}
}
@@ -0,0 +1,66 @@
package art.arcane.iris.engine.hydrology;
import java.util.List;
import java.util.Objects;
public record HydraulicSegment(
long id,
long courseId,
HydrologyFeatureType type,
int upstreamHeadY,
int downstreamHeadY,
int width,
int depth,
boolean fallingFluid,
boolean receivingPool,
List<HydrologyPoint> centerline
) {
public HydraulicSegment {
Objects.requireNonNull(type, "type");
Objects.requireNonNull(centerline, "centerline");
centerline = List.copyOf(centerline);
if (centerline.isEmpty()) {
throw new IllegalArgumentException("A hydraulic segment requires a centerline.");
}
if (upstreamHeadY < downstreamHeadY) {
throw new IllegalArgumentException("Hydraulic heads cannot rise downstream.");
}
if (width <= 0 || depth <= 0) {
throw new IllegalArgumentException("Hydraulic segment width and depth must be positive.");
}
int drop = upstreamHeadY - downstreamHeadY;
if (fallingFluid && drop == 0) {
throw new IllegalArgumentException("Falling fluid requires a positive head transition.");
}
if (drop > 0 && !type.isDrop()) {
throw new IllegalArgumentException("A positive head transition requires a drop segment type.");
}
if (drop == 0 && type.isDrop()) {
throw new IllegalArgumentException("A drop segment type requires a positive head transition.");
}
if (drop > 0 && !fallingFluid) {
if (centerline.getFirst().y() != upstreamHeadY || centerline.getLast().y() != downstreamHeadY) {
throw new IllegalArgumentException("A graded head transition must span its declared heads.");
}
int previousHead = upstreamHeadY;
for (HydrologyPoint point : centerline) {
if (point.y() > previousHead || point.y() < downstreamHeadY) {
throw new IllegalArgumentException("A graded head transition must descend monotonically.");
}
previousHead = point.y();
}
}
}
public int drop() {
return upstreamHeadY - downstreamHeadY;
}
public HydrologyPoint start() {
return centerline.getFirst();
}
public HydrologyPoint end() {
return centerline.getLast();
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.engine.hydrology;
public enum HydrologyCandidateKind {
SOURCE,
OUTLET,
DEEP_FLUID
}
@@ -0,0 +1,25 @@
package art.arcane.iris.engine.hydrology;
public enum HydrologyCandidateRejection {
NO_LEGAL_OUTLET,
NO_DRAINAGE_PATH,
ROUTE_LIMIT,
COURSE_TOO_SHORT,
SOURCE_QUOTA,
SOURCE_SPACING,
OUTLET_LIMIT,
RIDGE_CONTAINMENT,
OUTLET_LEVEL,
CAVE_CONTAINMENT,
VOLUME_LIMIT,
SURFACE_HEAD_RANGE,
SURFACE_HEAD_DISTRIBUTION,
SURFACE_RIDGE_TUNNEL_LIMIT,
SURFACE_EXPOSURE,
SURFACE_DROP_UNSUPPORTED,
SURFACE_CORRIDOR_UNSUPPORTED,
SURFACE_TRANSITION_UNSUPPORTED,
SURFACE_SEGMENT_UNSUPPORTED,
SURFACE_SINKHOLE_CLEARANCE,
SURFACE_SHAPE_UNSUPPORTED
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,13 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
@FunctionalInterface
public interface HydrologyCaveVoxelViewFactory {
CaveVoxelView create(PlannedSurface plannedSurface);
@FunctionalInterface
interface PlannedSurface {
int resolve(int x, int z, int naturalHeight);
}
}
@@ -0,0 +1,89 @@
package art.arcane.iris.engine.hydrology;
import java.util.Objects;
public record HydrologyColumnLayer(
HydrologyFeatureRef feature,
int bedY,
int fluidHeadY,
int ceilingY,
boolean channel,
boolean shore,
boolean grading,
boolean connectedFluid,
boolean fallingFluid,
boolean receivingPool,
boolean terrainOwned,
boolean fluidOwned,
boolean oceanApron,
String profileKey,
String surfaceBiomeKey,
String mouthBiomeKey,
String shoreBiomeKey,
String dryBiomeKey,
String floodedCaveBiomeKey
) {
public HydrologyColumnLayer {
Objects.requireNonNull(feature, "feature");
profileKey = requireKey(profileKey, "profileKey");
surfaceBiomeKey = requireKey(surfaceBiomeKey, "surfaceBiomeKey");
mouthBiomeKey = requireKey(mouthBiomeKey, "mouthBiomeKey");
shoreBiomeKey = requireKey(shoreBiomeKey, "shoreBiomeKey");
dryBiomeKey = requireKey(dryBiomeKey, "dryBiomeKey");
floodedCaveBiomeKey = requireKey(floodedCaveBiomeKey, "floodedCaveBiomeKey");
if (channel && bedY > fluidHeadY) {
throw new IllegalArgumentException("A channel bed cannot be above its fluid head.");
}
if (ceilingY < fluidHeadY) {
throw new IllegalArgumentException("A hydrology carve ceiling cannot be below its fluid head.");
}
if (!feature.type().isUnderground() && !feature.type().isDeepFluid() && ceilingY != fluidHeadY) {
throw new IllegalArgumentException("Surface hydrology ceilings must equal the fluid head.");
}
if (fluidOwned && !channel) {
throw new IllegalArgumentException("Owned fluid requires a channel footprint.");
}
if (fallingFluid && !connectedFluid) {
throw new IllegalArgumentException("Falling fluid must be connected.");
}
if (receivingPool && !connectedFluid) {
throw new IllegalArgumentException("A receiving pool must be connected.");
}
if (oceanApron && (terrainOwned || fluidOwned || grading || shore)) {
throw new IllegalArgumentException("An ocean apron cannot own river writes or grading.");
}
}
public String biomeKey() {
if (grading && !channel && !shore) {
return null;
}
if (shore) {
return shoreBiomeKey;
}
if (feature.type() == HydrologyFeatureType.MOUTH || feature.type() == HydrologyFeatureType.COASTAL_GROTTO) {
return mouthBiomeKey;
}
if (feature.type().isUnderground() || feature.type().isDeepFluid()) {
return floodedCaveBiomeKey;
}
return surfaceBiomeKey;
}
public boolean publishesSurfaceFluid() {
return feature.type().isSurface()
&& !oceanApron
&& channel
&& fluidOwned
&& (terrainOwned || fallingFluid)
&& (feature.type().isDrop() || fallingFluid || receivingPool)
&& fluidHeadY > bedY;
}
private static String requireKey(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank.");
}
return value.trim();
}
}
@@ -0,0 +1,202 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public record HydrologyColumnSample(
int x,
int z,
int naturalHeight,
int seaLevel,
boolean ocean,
String parentBiomeKey,
List<HydrologyColumnLayer> layers
) {
private static final Comparator<HydrologyColumnLayer> LAYER_ORDER = Comparator
.comparingInt((HydrologyColumnLayer layer) -> layer.feature().type().renderPriority())
.thenComparing(Comparator.comparingInt(HydrologyColumnLayer::fluidHeadY).reversed())
.thenComparingLong((HydrologyColumnLayer layer) -> layer.feature().id());
private static final Comparator<HydrologyColumnLayer> SURFACE_LAYER_ORDER = Comparator
.comparingInt(HydrologyColumnSample::surfaceRolePriority)
.thenComparingInt((HydrologyColumnLayer layer) -> layer.feature().type().renderPriority())
.thenComparing(Comparator.comparingInt(HydrologyColumnLayer::fluidHeadY).reversed())
.thenComparingInt(HydrologyColumnLayer::bedY)
.thenComparingLong((HydrologyColumnLayer layer) -> layer.feature().id());
public HydrologyColumnSample {
if (parentBiomeKey == null || parentBiomeKey.isBlank()) {
throw new IllegalArgumentException("parentBiomeKey must not be blank.");
}
parentBiomeKey = parentBiomeKey.trim();
Objects.requireNonNull(layers, "layers");
if (layers.size() < 2) {
layers = List.copyOf(layers);
} else {
ArrayList<HydrologyColumnLayer> ordered = new ArrayList<>(layers);
ordered.sort(LAYER_ORDER);
layers = List.copyOf(ordered);
}
if (ocean) {
for (HydrologyColumnLayer layer : layers) {
if (layer.terrainOwned() || layer.fluidOwned() || layer.grading() || layer.shore()) {
throw new IllegalArgumentException("Ocean columns cannot contain river-owned writes or grading.");
}
if (layer.fluidHeadY() > seaLevel) {
throw new IllegalArgumentException("Ocean column fluid cannot be elevated above sea level.");
}
}
}
if (naturalHeight <= seaLevel) {
for (HydrologyColumnLayer layer : layers) {
if (layer.feature().type().isSurface()
&& !layer.oceanApron()
&& (layer.terrainOwned() || layer.fluidOwned() || layer.grading() || layer.shore())) {
throw new IllegalArgumentException(
"Naturally submerged columns cannot contain owned surface hydrology writes."
);
}
}
}
for (HydrologyColumnLayer layer : layers) {
if (layer.feature().type().isSurface()
&& !layer.oceanApron()
&& layer.channel()
&& layer.fluidOwned()
&& layer.fluidHeadY() >= naturalHeight) {
throw new IllegalArgumentException("Owned surface fluid must remain below natural terrain.");
}
}
}
public boolean present() {
return !layers.isEmpty();
}
public boolean hasFeature(HydrologyFeatureType type) {
Objects.requireNonNull(type, "type");
for (HydrologyColumnLayer layer : layers) {
if (layer.feature().type() == type) {
return true;
}
}
return false;
}
public boolean hasConnectedFluid() {
for (HydrologyColumnLayer layer : layers) {
if (layer.connectedFluid()) {
return true;
}
}
return false;
}
public Optional<HydrologyColumnLayer> primaryLayer() {
return layers.isEmpty() ? Optional.empty() : Optional.of(layers.getFirst());
}
public Optional<HydrologyColumnLayer> primarySurfaceLayer() {
return selectSurfaceLayer(false);
}
public Optional<HydrologyColumnLayer> primarySurfaceFluidLayer() {
return selectSurfaceLayer(true);
}
public Optional<SurfacePublicationCell> surfacePublicationCellAt(int y) {
if (y <= terrainHeight()) {
return Optional.empty();
}
SurfacePublicationCell selected = null;
for (HydrologyColumnLayer layer : layers) {
if (!layer.publishesSurfaceFluid() || y <= layer.bedY() || y > layer.fluidHeadY()) {
continue;
}
HydrologyCaveAction action = layer.fallingFluid() && y < layer.fluidHeadY()
? HydrologyCaveAction.FALLING_FLUID
: HydrologyCaveAction.WET_SOURCE;
SurfacePublicationCell candidate = new SurfacePublicationCell(layer, action);
if (selected == null || surfaceActionPriority(action) < surfaceActionPriority(selected.action())) {
selected = candidate;
}
}
return Optional.ofNullable(selected);
}
public int terrainHeight() {
Optional<HydrologyColumnLayer> layer = primarySurfaceLayer();
if (layer.isEmpty()) {
return naturalHeight;
}
HydrologyColumnLayer primary = layer.get();
if (primary.channel()) {
return primary.bedY();
}
int terrainHeight = primary.bedY();
for (HydrologyColumnLayer candidate : layers) {
if (candidate.feature().type().isSurface()
&& candidate.terrainOwned()
&& !candidate.channel()) {
terrainHeight = Math.max(terrainHeight, candidate.bedY());
}
}
return terrainHeight;
}
public HydrologyRenderSample renderSample() {
ArrayList<HydrologyFeatureRef> features = new ArrayList<>(layers.size());
for (HydrologyColumnLayer layer : layers) {
features.add(layer.feature());
}
return new HydrologyRenderSample(x, z, features);
}
private Optional<HydrologyColumnLayer> selectSurfaceLayer(boolean fluidOnly) {
HydrologyColumnLayer selected = null;
for (HydrologyColumnLayer layer : layers) {
if (layer.oceanApron() || !layer.feature().type().isSurface()) {
continue;
}
if (fluidOnly) {
if (!layer.channel() || !layer.connectedFluid() || !layer.fluidOwned()) {
continue;
}
} else if (!layer.terrainOwned()) {
continue;
}
if (selected == null || SURFACE_LAYER_ORDER.compare(layer, selected) < 0) {
selected = layer;
}
}
return Optional.ofNullable(selected);
}
private static int surfaceRolePriority(HydrologyColumnLayer layer) {
if (layer.channel()) {
return 0;
}
return layer.shore() ? 1 : 2;
}
private static int surfaceActionPriority(HydrologyCaveAction action) {
return action == HydrologyCaveAction.WET_SOURCE ? 0 : 1;
}
public record SurfacePublicationCell(
HydrologyColumnLayer layer,
HydrologyCaveAction action
) {
public SurfacePublicationCell {
Objects.requireNonNull(layer, "layer");
if (action != HydrologyCaveAction.WET_SOURCE
&& action != HydrologyCaveAction.FALLING_FLUID) {
throw new IllegalArgumentException("Surface publication cells must contain fluid.");
}
}
}
}
@@ -0,0 +1,137 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveConflictPolicy;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
final class HydrologyCrossTileCaveAdmission {
private static final Comparator<Claim> CLAIM_PRIORITY = Comparator
.comparing((Claim claim) -> claim.plan().source(), HydrologyCaveConflictPolicy.sourcePriority())
.thenComparing(Claim::profileKey);
private static final Comparator<RankedClaim> BLOCKER_PRIORITY = Comparator
.comparingInt(RankedClaim::ownerRank)
.thenComparing(RankedClaim::claim, CLAIM_PRIORITY)
.thenComparing(RankedClaim::ownerKey);
private HydrologyCrossTileCaveAdmission() {
}
static Result admit(
int currentOwnerRank,
Collection<Claim> currentClaims,
Collection<RankedClaim> blockers
) {
if (currentOwnerRank < 0) {
throw new IllegalArgumentException("currentOwnerRank cannot be negative");
}
ArrayList<Claim> orderedClaims = new ArrayList<>(Objects.requireNonNull(currentClaims, "currentClaims"));
orderedClaims.sort(CLAIM_PRIORITY);
ArrayList<RankedClaim> orderedBlockers = new ArrayList<>(Objects.requireNonNull(blockers, "blockers"));
orderedBlockers.sort(BLOCKER_PRIORITY);
for (RankedClaim blocker : orderedBlockers) {
if (blocker.ownerRank() >= currentOwnerRank) {
throw new IllegalArgumentException("Cross-tile blockers must have a lower owner color rank");
}
}
ArrayList<Rejection> rejections = new ArrayList<>();
for (Claim claim : orderedClaims) {
RankedClaim winner = null;
for (RankedClaim blocker : orderedBlockers) {
if (claim.courseId() == blocker.claim().courseId()) {
throw new IllegalArgumentException("A cave course cannot belong to multiple color-ranked owners");
}
if (!conflicts(claim, blocker.claim())) {
continue;
}
if (winner == null || BLOCKER_PRIORITY.compare(blocker, winner) < 0) {
winner = blocker;
}
}
if (winner != null) {
rejections.add(new Rejection(claim, winner));
}
}
return new Result(rejections);
}
private static boolean conflicts(Claim left, Claim right) {
return HydrologyCaveConflictPolicy.hasIncompatibleOverlap(
left.profileKey(),
left.plan().actions(),
right.profileKey(),
right.plan().actions()
);
}
record Claim(String profileKey, HydrologyCavePlan plan) {
Claim {
if (profileKey == null || profileKey.isBlank()) {
throw new IllegalArgumentException("profileKey must not be blank");
}
profileKey = profileKey.trim();
Objects.requireNonNull(plan, "plan");
if (!plan.accepted()) {
throw new IllegalArgumentException("Cross-tile admission requires accepted cave plans");
}
}
long courseId() {
return plan.source().sourceId();
}
}
record RankedClaim(HydrologyTileKey ownerKey, int ownerRank, Claim claim) {
RankedClaim {
Objects.requireNonNull(ownerKey, "ownerKey");
if (ownerRank < 0) {
throw new IllegalArgumentException("ownerRank cannot be negative");
}
Objects.requireNonNull(claim, "claim");
}
}
record Result(List<Rejection> rejections) {
Result {
rejections = List.copyOf(Objects.requireNonNull(rejections, "rejections"));
}
Map<Long, Long> loserCourseIdsToWinnerSourceIds() {
TreeMap<Long, Long> ordered = new TreeMap<>();
for (Rejection rejection : rejections) {
ordered.put(rejection.loser().courseId(), rejection.winnerSourceId());
}
return Collections.unmodifiableMap(new LinkedHashMap<>(ordered));
}
Rejection rejection(Claim claim) {
Objects.requireNonNull(claim, "claim");
for (Rejection rejection : rejections) {
if (rejection.loser().equals(claim)) {
return rejection;
}
}
return null;
}
}
record Rejection(Claim loser, RankedClaim winner) {
Rejection {
Objects.requireNonNull(loser, "loser");
Objects.requireNonNull(winner, "winner");
}
long winnerSourceId() {
return winner.claim().plan().source().sourceId();
}
}
}
@@ -0,0 +1,163 @@
package art.arcane.iris.engine.hydrology;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
final class HydrologyCrossTileSurfaceAdmission {
private HydrologyCrossTileSurfaceAdmission() {
}
static Result admit(
List<Claim> currentClaims,
List<RankedClaim> blockers,
int minimumSeparation
) {
ArrayList<Rejection> rejections = new ArrayList<>();
for (Claim current : currentClaims) {
for (RankedClaim ranked : blockers) {
Claim blocker = ranked.claim();
if (!conflicts(current, blocker, minimumSeparation)) {
continue;
}
rejections.add(new Rejection(current, blocker.courseId()));
break;
}
}
return new Result(List.copyOf(rejections));
}
private static boolean conflicts(Claim first, Claim second, int minimumSeparation) {
if (first.outletId() == second.outletId()) {
return true;
}
long minimumSeparationSquared = (long) minimumSeparation * minimumSeparation;
if (first.centerline().getFirst().distanceSquared2D(second.centerline().getFirst())
< minimumSeparationSquared) {
return true;
}
if (first.reachesOutlet() && second.reachesOutlet()
&& first.terminal().distanceSquared2D(second.terminal())
< minimumSeparationSquared) {
return true;
}
double clearance = Math.max(4D, (first.maximumWidth() + second.maximumWidth()) / 2D + 4D);
return centerlinesWithin(first.centerline(), second.centerline(), clearance);
}
private static boolean centerlinesWithin(
List<HydrologyPoint> first,
List<HydrologyPoint> second,
double clearance
) {
double clearanceSquared = clearance * clearance;
for (int firstIndex = 1; firstIndex < first.size(); firstIndex++) {
HydrologyPoint firstStart = first.get(firstIndex - 1);
HydrologyPoint firstEnd = first.get(firstIndex);
for (int secondIndex = 1; secondIndex < second.size(); secondIndex++) {
HydrologyPoint secondStart = second.get(secondIndex - 1);
HydrologyPoint secondEnd = second.get(secondIndex);
if (segmentDistanceSquared(firstStart, firstEnd, secondStart, secondEnd) <= clearanceSquared) {
return true;
}
}
}
return false;
}
private static double segmentDistanceSquared(
HydrologyPoint firstStart,
HydrologyPoint firstEnd,
HydrologyPoint secondStart,
HydrologyPoint secondEnd
) {
if (segmentsIntersect(firstStart, firstEnd, secondStart, secondEnd)) {
return 0D;
}
return Math.min(
Math.min(pointSegmentDistanceSquared(firstStart, secondStart, secondEnd),
pointSegmentDistanceSquared(firstEnd, secondStart, secondEnd)),
Math.min(pointSegmentDistanceSquared(secondStart, firstStart, firstEnd),
pointSegmentDistanceSquared(secondEnd, firstStart, firstEnd))
);
}
private static boolean segmentsIntersect(
HydrologyPoint firstStart,
HydrologyPoint firstEnd,
HydrologyPoint secondStart,
HydrologyPoint secondEnd
) {
long firstA = orientation(firstStart, firstEnd, secondStart);
long firstB = orientation(firstStart, firstEnd, secondEnd);
long secondA = orientation(secondStart, secondEnd, firstStart);
long secondB = orientation(secondStart, secondEnd, firstEnd);
return Long.signum(firstA) != Long.signum(firstB)
&& Long.signum(secondA) != Long.signum(secondB);
}
private static long orientation(HydrologyPoint start, HydrologyPoint end, HydrologyPoint point) {
long firstX = end.x() - (long) start.x();
long firstZ = end.z() - (long) start.z();
long secondX = point.x() - (long) start.x();
long secondZ = point.z() - (long) start.z();
return firstX * secondZ - firstZ * secondX;
}
private static double pointSegmentDistanceSquared(
HydrologyPoint point,
HydrologyPoint start,
HydrologyPoint end
) {
double deltaX = end.x() - start.x();
double deltaZ = end.z() - start.z();
double lengthSquared = deltaX * deltaX + deltaZ * deltaZ;
if (lengthSquared == 0D) {
return point.distanceSquared2D(start);
}
double progress = ((point.x() - start.x()) * deltaX + (point.z() - start.z()) * deltaZ)
/ lengthSquared;
progress = Math.max(0D, Math.min(1D, progress));
double closestX = start.x() + deltaX * progress;
double closestZ = start.z() + deltaZ * progress;
double pointX = point.x() - closestX;
double pointZ = point.z() - closestZ;
return pointX * pointX + pointZ * pointZ;
}
record Claim(
long courseId,
long outletId,
HydrologyPoint terminal,
boolean reachesOutlet,
int maximumWidth,
List<HydrologyPoint> centerline
) {
Claim {
Objects.requireNonNull(terminal, "terminal");
centerline = List.copyOf(Objects.requireNonNull(centerline, "centerline"));
if (maximumWidth < 1 || centerline.size() < 2) {
throw new IllegalArgumentException("Surface claims require a positive width and centerline.");
}
}
}
record RankedClaim(HydrologyTileKey ownerKey, int ownerRank, Claim claim) {
RankedClaim {
Objects.requireNonNull(ownerKey, "ownerKey");
Objects.requireNonNull(claim, "claim");
}
}
record Rejection(Claim loser, long winnerCourseId) {
Rejection {
Objects.requireNonNull(loser, "loser");
}
}
record Result(List<Rejection> rejections) {
Result {
rejections = List.copyOf(Objects.requireNonNull(rejections, "rejections"));
}
}
}
@@ -0,0 +1,18 @@
package art.arcane.iris.engine.hydrology;
import java.util.Objects;
public record HydrologyDiagnosticCandidate(
long id,
HydrologyCandidateKind kind,
HydrologyFeatureType projectedType,
HydrologyPoint point,
HydrologyCandidateRejection rejection
) {
public HydrologyDiagnosticCandidate {
Objects.requireNonNull(kind, "kind");
Objects.requireNonNull(projectedType, "projectedType");
Objects.requireNonNull(point, "point");
Objects.requireNonNull(rejection, "rejection");
}
}
@@ -0,0 +1,28 @@
package art.arcane.iris.engine.hydrology;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
public record HydrologyDiagnosticRenderSample(
int x,
int z,
List<HydrologyDiagnosticCandidate> candidates
) {
private static final Comparator<HydrologyDiagnosticCandidate> CANDIDATE_ORDER = Comparator
.comparing(HydrologyDiagnosticCandidate::kind)
.thenComparing(HydrologyDiagnosticCandidate::rejection)
.thenComparingLong(HydrologyDiagnosticCandidate::id);
public HydrologyDiagnosticRenderSample {
Objects.requireNonNull(candidates, "candidates");
ArrayList<HydrologyDiagnosticCandidate> ordered = new ArrayList<>(candidates);
ordered.sort(CANDIDATE_ORDER);
candidates = List.copyOf(ordered);
}
public boolean present() {
return !candidates.isEmpty();
}
}
@@ -0,0 +1,96 @@
package art.arcane.iris.engine.hydrology;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public record HydrologyFeatureQuery(Set<HydrologyFeatureType> types, String profileKey) {
private static final Set<HydrologyFeatureType> SURFACE = Set.of(
HydrologyFeatureType.SURFACE_POOL,
HydrologyFeatureType.RIFFLE,
HydrologyFeatureType.CASCADE,
HydrologyFeatureType.WATERFALL,
HydrologyFeatureType.MOUTH
);
private static final Set<HydrologyFeatureType> UNDERGROUND = Set.of(
HydrologyFeatureType.RIDGE_BORE,
HydrologyFeatureType.UNDERGROUND_POOL,
HydrologyFeatureType.UNDERGROUND_DROP,
HydrologyFeatureType.SINKHOLE
);
private static final Set<HydrologyFeatureType> GROTTOS = Set.of(
HydrologyFeatureType.COASTAL_GROTTO,
HydrologyFeatureType.INLAND_GROTTO
);
private static final Set<HydrologyFeatureType> DEEP = Set.of(
HydrologyFeatureType.DEEP_POOL,
HydrologyFeatureType.DEEP_CHANNEL
);
private static final Map<String, Set<HydrologyFeatureType>> BUILT_INS = builtIns();
public HydrologyFeatureQuery {
types = Set.copyOf(types);
if (types.isEmpty()) {
throw new IllegalArgumentException("Hydrology feature query requires at least one feature type.");
}
profileKey = profileKey == null || profileKey.isBlank() ? null : profileKey.trim();
}
public static HydrologyFeatureQuery parse(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Hydrology feature type must not be blank.");
}
String normalized = value.trim();
if (normalized.regionMatches(true, 0, "type=", 0, 5)) {
normalized = normalized.substring(5).trim();
}
String key = normalizeKeyword(normalized);
Set<HydrologyFeatureType> builtInTypes = BUILT_INS.get(key);
return builtInTypes == null
? new HydrologyFeatureQuery(DEEP, normalized)
: new HydrologyFeatureQuery(builtInTypes, null);
}
public static List<String> suggestions(Collection<String> deepFluidIds) {
LinkedHashSet<String> suggestions = new LinkedHashSet<>(BUILT_INS.keySet());
TreeSet<String> orderedIds = new TreeSet<>();
if (deepFluidIds != null) {
for (String id : deepFluidIds) {
if (id != null && !id.isBlank() && !isReservedKeyword(id)) {
orderedIds.add(id.trim());
}
}
}
suggestions.addAll(orderedIds);
return List.copyOf(suggestions);
}
public static boolean isReservedKeyword(String value) {
return value != null && !value.isBlank() && BUILT_INS.containsKey(normalizeKeyword(value.trim()));
}
private static String normalizeKeyword(String value) {
return value.toLowerCase(Locale.ROOT).replace('-', '_');
}
private static Map<String, Set<HydrologyFeatureType>> builtIns() {
LinkedHashMap<String, Set<HydrologyFeatureType>> builtIns = new LinkedHashMap<>();
builtIns.put("surface", SURFACE);
builtIns.put("waterfall", Set.of(HydrologyFeatureType.WATERFALL));
builtIns.put("sinkhole", Set.of(HydrologyFeatureType.SINKHOLE));
builtIns.put("underground", UNDERGROUND);
builtIns.put("grotto", GROTTOS);
builtIns.put("coastal_grotto", Set.of(HydrologyFeatureType.COASTAL_GROTTO));
builtIns.put("inland_grotto", Set.of(HydrologyFeatureType.INLAND_GROTTO));
builtIns.put("mouth", Set.of(HydrologyFeatureType.MOUTH));
builtIns.put("ridge_tunnel", Set.of(HydrologyFeatureType.RIDGE_BORE));
builtIns.put("deep", DEEP);
return Collections.unmodifiableMap(builtIns);
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.engine.hydrology;
import java.util.Objects;
public record HydrologyFeatureRef(
long id,
HydrologyFeatureType type,
long courseId,
long segmentId,
int x,
int y,
int z,
int flowDeltaX,
int flowDeltaZ,
boolean source
) {
public HydrologyFeatureRef {
Objects.requireNonNull(type, "type");
if (flowDeltaX < -1 || flowDeltaX > 1 || flowDeltaZ < -1 || flowDeltaZ > 1) {
throw new IllegalArgumentException("Flow direction components must be between -1 and 1.");
}
}
public HydrologyPoint point() {
return new HydrologyPoint(x, y, z);
}
}
@@ -0,0 +1,62 @@
package art.arcane.iris.engine.hydrology;
public enum HydrologyFeatureType {
SURFACE_POOL,
RIFFLE,
CASCADE,
WATERFALL,
RIDGE_BORE,
UNDERGROUND_POOL,
UNDERGROUND_DROP,
SINKHOLE,
COASTAL_GROTTO,
INLAND_GROTTO,
MOUTH,
DEEP_POOL,
DEEP_CHANNEL;
public boolean isSurface() {
return switch (this) {
case SURFACE_POOL, RIFFLE, CASCADE, WATERFALL, MOUTH -> true;
default -> false;
};
}
public boolean isUnderground() {
return switch (this) {
case RIDGE_BORE, UNDERGROUND_POOL, UNDERGROUND_DROP, SINKHOLE, COASTAL_GROTTO, INLAND_GROTTO -> true;
default -> false;
};
}
public boolean isGrotto() {
return this == COASTAL_GROTTO || this == INLAND_GROTTO;
}
public boolean isDeepFluid() {
return this == DEEP_POOL || this == DEEP_CHANNEL;
}
public boolean isDrop() {
return this == RIFFLE || this == CASCADE || this == WATERFALL
|| this == UNDERGROUND_DROP || this == SINKHOLE;
}
int renderPriority() {
return switch (this) {
case WATERFALL -> 0;
case SINKHOLE -> 1;
case CASCADE -> 2;
case RIFFLE -> 3;
case COASTAL_GROTTO -> 4;
case INLAND_GROTTO -> 5;
case MOUTH -> 6;
case RIDGE_BORE -> 7;
case UNDERGROUND_DROP -> 8;
case UNDERGROUND_POOL -> 9;
case DEEP_CHANNEL -> 10;
case DEEP_POOL -> 11;
case SURFACE_POOL -> 12;
};
}
}
@@ -0,0 +1,115 @@
package art.arcane.iris.engine.hydrology;
import java.util.Objects;
@FunctionalInterface
public interface HydrologyGeometrySampler {
int sample(Request request);
default int sample(
Field field,
String profileKey,
int x,
int z,
int minimum,
int maximum
) {
return sample(field, profileKey, x, z, 0L, minimum, maximum);
}
default int sample(
Field field,
String profileKey,
int x,
int z,
long stableId,
int minimum,
int maximum
) {
Request request = new Request(field, profileKey, x, z, stableId, minimum, maximum);
int value = sample(request);
if (value < minimum || value > maximum) {
throw new IllegalStateException(
"Hydrology geometry sampler returned " + value + " outside " + minimum + ".." + maximum
+ " for " + field + " at " + x + "," + z + "."
);
}
return value;
}
static HydrologyGeometrySampler deterministic(
long worldSeed,
HydrologyTerrainSampler terrainSampler
) {
Objects.requireNonNull(terrainSampler, "terrainSampler");
return request -> {
if (request.field() == Field.UNDERGROUND_FLUID_LEVEL) {
HydrologyTerrainSample terrain = Objects.requireNonNull(
terrainSampler.sample(request.x(), request.z()),
"Hydrology terrain sampler returned null at " + request.x() + "," + request.z()
);
return Math.max(request.minimum(), Math.min(request.maximum(), terrain.caveFluidY()));
}
if (request.field() == Field.SURFACE_WIDTH
|| request.field() == Field.SURFACE_DEPTH
|| request.field() == Field.SURFACE_INSET
|| request.field() == Field.UNDERGROUND_WIDTH
|| request.field() == Field.UNDERGROUND_DEPTH) {
return request.minimum();
}
long stable = switch (request.field()) {
case SURFACE_BLEND_WIDTH -> HydrologyHash.mix(
request.stableId(),
request.x(),
request.z(),
0x424c454e44L
);
case TARGET_POOL_LENGTH -> HydrologyHash.mix(
worldSeed,
request.stableId(),
0x504f4f4cL
);
case UNDERGROUND_HEADROOM -> HydrologyHash.mix(
request.stableId(),
request.x(),
request.z(),
0x48454144L
);
case DEEP_FLUID_HEIGHT -> HydrologyHash.mix(request.stableId(), 0x484549474854L);
default -> throw new IllegalStateException("Missing deterministic geometry rule for " + request.field() + ".");
};
return HydrologyHash.between(stable, request.minimum(), request.maximum());
};
}
enum Field {
SURFACE_WIDTH,
SURFACE_DEPTH,
SURFACE_INSET,
SURFACE_BLEND_WIDTH,
TARGET_POOL_LENGTH,
UNDERGROUND_FLUID_LEVEL,
UNDERGROUND_WIDTH,
UNDERGROUND_DEPTH,
UNDERGROUND_HEADROOM,
DEEP_FLUID_HEIGHT
}
record Request(
Field field,
String profileKey,
int x,
int z,
long stableId,
int minimum,
int maximum
) {
public Request {
field = Objects.requireNonNull(field, "field");
profileKey = profileKey == null ? "" : profileKey;
if (minimum > maximum) {
throw new IllegalArgumentException("Hydrology geometry sample bounds are reversed.");
}
}
}
}
@@ -0,0 +1,43 @@
package art.arcane.iris.engine.hydrology;
final class HydrologyHash {
private HydrologyHash() {
}
static long mix(long seed, long... values) {
long mixed = avalanche(seed ^ 0x9e3779b97f4a7c15L);
for (long value : values) {
mixed = avalanche(mixed ^ avalanche(value + 0x9e3779b97f4a7c15L));
}
return mixed;
}
static long text(String value) {
long hash = 0xcbf29ce484222325L;
for (int index = 0; index < value.length(); index++) {
hash ^= value.charAt(index);
hash *= 0x100000001b3L;
}
return avalanche(hash);
}
static double unit(long value) {
return (avalanche(value) >>> 11) * 0x1.0p-53;
}
static int between(long value, int minimum, int maximum) {
if (minimum > maximum) {
throw new IllegalArgumentException("minimum cannot exceed maximum.");
}
long span = (long) maximum - minimum + 1L;
return minimum + (int) Long.remainderUnsigned(avalanche(value), span);
}
private static long avalanche(long value) {
value ^= value >>> 30;
value *= 0xbf58476d1ce4e5b9L;
value ^= value >>> 27;
value *= 0x94d049bb133111ebL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,5 @@
package art.arcane.iris.engine.hydrology;
public interface HydrologyNaturalTerrainSampler extends HydrologyRoutingTerrainSampler {
HydrologyTerrainSample sampleBasis(int blockX, int blockZ);
}
@@ -0,0 +1,48 @@
package art.arcane.iris.engine.hydrology;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public final class HydrologyObservedPlannedSurface implements HydrologyCaveVoxelViewFactory.PlannedSurface {
private final HydrologyCaveVoxelViewFactory.PlannedSurface delegate;
private final Long2ObjectOpenHashMap<ArrayList<Observation>> observations;
public HydrologyObservedPlannedSurface(HydrologyCaveVoxelViewFactory.PlannedSurface delegate) {
this.delegate = Objects.requireNonNull(delegate);
this.observations = new Long2ObjectOpenHashMap<>();
}
@Override
public int resolve(int x, int z, int naturalHeight) {
int resolvedHeight = delegate.resolve(x, z, naturalHeight);
long key = RiverFootprint.pack(x, z);
ArrayList<Observation> column = observations.get(key);
if (column == null) {
column = new ArrayList<>(1);
observations.put(key, column);
}
for (Observation observation : column) {
if (observation.naturalHeight() == naturalHeight) {
return resolvedHeight;
}
}
column.add(new Observation(x, z, naturalHeight, resolvedHeight));
return resolvedHeight;
}
public List<Observation> observationsAt(int x, int z) {
ArrayList<Observation> column = observations.get(RiverFootprint.pack(x, z));
return column == null ? List.of() : List.copyOf(column);
}
public record Observation(
int x,
int z,
int naturalHeight,
int resolvedHeight
) {
}
}
@@ -0,0 +1,47 @@
package art.arcane.iris.engine.hydrology;
import java.util.List;
import java.util.Objects;
final class HydrologyOceanBoundaryRefiner {
private HydrologyOceanBoundaryRefiner() {
}
static Result refine(
List<HydrologyPoint> crossing,
HydrologyTerrainSampler terrainSampler,
HydrologyRoutingTerrainSampler routingSampler
) {
Objects.requireNonNull(crossing, "crossing");
Objects.requireNonNull(terrainSampler, "terrainSampler");
Objects.requireNonNull(routingSampler, "routingSampler");
if (crossing.size() < 2) {
return null;
}
HydrologyPoint previous = crossing.getFirst();
for (int index = 1; index < crossing.size(); index++) {
HydrologyPoint point = crossing.get(index);
HydrologyRoutingTerrainSampler.NaturalClassification classification =
routingSampler.classifyNatural(point.x(), point.z());
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.UNAVAILABLE) {
return null;
}
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN) {
HydrologyTerrainSample landwardTerrain = terrainSampler.sample(previous.x(), previous.z());
if (landwardTerrain == null || landwardTerrain.ocean()) {
return null;
}
return new Result(previous, point, landwardTerrain);
}
previous = point;
}
return null;
}
record Result(
HydrologyPoint landwardPoint,
HydrologyPoint oceanPoint,
HydrologyTerrainSample landwardTerrain
) {
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,522 @@
package art.arcane.iris.engine.hydrology;
import java.util.List;
public record HydrologyPlannerSettings(
int seaLevel,
Routing routing,
Surface surface,
Hydraulics hydraulics,
Underground underground,
Outlets outlets,
Geometry geometry,
List<DeepFluid> deepFluids
) {
private static final long PLAN_FORMAT_REVISION = 5L;
private static final int MAXIMUM_CROSS_TILE_COLOR_PERIOD = 4;
public HydrologyPlannerSettings {
if (seaLevel < -2048 || seaLevel > 2048) {
throw new IllegalArgumentException("seaLevel is outside the supported planning range.");
}
if (routing == null || surface == null || hydraulics == null || underground == null
|| outlets == null || geometry == null) {
throw new IllegalArgumentException("Hydrology planner settings cannot contain null sections.");
}
deepFluids = deepFluids == null ? List.of() : List.copyOf(deepFluids);
int publicationRadius = publicationRadius(routing, surface, underground, outlets, geometry, deepFluids);
if (crossTileColorPeriod(publicationRadius, routing.tileSize()) > MAXIMUM_CROSS_TILE_COLOR_PERIOD) {
throw new IllegalArgumentException("Hydrology publication envelope exceeds the bounded cross-tile admission period.");
}
}
public static HydrologyPlannerSettings defaults() {
Source surfaceSources = new Source(true, 0.5D, 88, 0, 1, 384);
Source undergroundSources = new Source(true, 0.25D, Integer.MIN_VALUE, 0, 1, 512);
return new HydrologyPlannerSettings(
63,
new Routing(2048, 64, 8, 8192, 8192, new Branching(384, 192), 1.5D, 24D, 2D, 0.2D),
new Surface(true, surfaceSources, 4, 8, 1, 4, 3, 7, 6, 2, 10, 24, true, 192, 10),
new Hydraulics(80, 180, 1, 7, 24, 8),
new Underground(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true),
new Outlets(
true,
new Grotto(true, 18, 8, 8, 32768),
new Grotto(true, 18, 8, 8, 32768),
false,
12,
64,
8,
12
),
Geometry.defaults(),
List.of()
);
}
public long fingerprint() {
long hash = HydrologyHash.mix(
0x485944524f4c4f47L,
PLAN_FORMAT_REVISION,
routing.hashCode(),
surface.hashCode(),
hydraulics.hashCode(),
underground.hashCode(),
outlets.hashCode(),
geometry.hashCode()
);
for (DeepFluid deepFluid : deepFluids) {
hash = HydrologyHash.mix(hash, deepFluid.hashCode(), HydrologyHash.text(deepFluid.id()));
}
return hash;
}
public int publicationRadius() {
return publicationRadius(routing, surface, underground, outlets, geometry, deepFluids);
}
int crossTileColorPeriod() {
return crossTileColorPeriod(publicationRadius(), routing.tileSize());
}
private static int publicationRadius(
Routing routing,
Surface surface,
Underground underground,
Outlets outlets,
Geometry geometry,
List<DeepFluid> deepFluids
) {
int alignedHalo = Math.floorDiv(
Math.min(
routing.maximumRouteLength(),
Math.multiplyExact(routing.sampleSpacing(), 2)
),
routing.sampleSpacing()
) * routing.sampleSpacing();
int radius = 0;
int routeDisplacement = Math.multiplyExact(routing.sampleSpacing(), 3);
if (surface.enabled() && surface.sources().enabled()) {
int surfaceRadius = (int) StrictMath.ceil(
surface.maximumWidth() / 2D + surface.shoreWidth() + surface.maximumTerrainBlendWidth()
);
surfaceRadius = Math.max(
surfaceRadius,
(int) StrictMath.ceil(geometry.drops().basinWidth(
geometry.drops().flowWidth(surface.maximumWidth())
) / 2D + surface.shoreWidth() + surface.maximumTerrainBlendWidth())
);
surfaceRadius = Math.max(surfaceRadius, outlets.coastalGrotto().horizontalRadius());
surfaceRadius = Math.max(surfaceRadius, outlets.inlandGrotto().horizontalRadius());
radius = Math.max(radius, Math.addExact(alignedHalo, Math.addExact(surfaceRadius, routeDisplacement)));
}
if (underground.enabled() && underground.sources().enabled()) {
int undergroundRadius = (int) StrictMath.ceil(underground.maximumWidth() / 2D);
undergroundRadius = Math.max(
undergroundRadius,
(int) StrictMath.ceil(geometry.drops().basinWidth(
geometry.drops().flowWidth(underground.maximumWidth())
) / 2D)
);
undergroundRadius = Math.max(undergroundRadius, outlets.coastalGrotto().horizontalRadius());
undergroundRadius = Math.max(undergroundRadius, outlets.inlandGrotto().horizontalRadius());
radius = Math.max(radius, Math.addExact(alignedHalo, Math.addExact(undergroundRadius, routeDisplacement)));
}
for (DeepFluid deepFluid : deepFluids) {
if (!deepFluid.enabled() || deepFluid.maximumPerTile() == 0) {
continue;
}
int poolReach = deepFluid.containedPools() ? deepFluid.maximumHorizontalRadius() : 0;
int channelReach = deepFluid.shortChannels()
? Math.addExact(deepFluid.maximumChannelLength(), (int) StrictMath.ceil(deepFluid.channelWidth() / 2D))
: 0;
radius = Math.max(radius, Math.max(poolReach, channelReach));
}
return radius;
}
private static int crossTileColorPeriod(int publicationRadius, int tileSize) {
long actionReach = Math.addExact((long) publicationRadius, 1L);
return Math.toIntExact(Math.addExact(Math.floorDiv(Math.multiplyExact(2L, actionReach), tileSize), 2L));
}
public record Routing(
int tileSize,
int sampleSpacing,
int refinementSpacing,
int maximumRouteNodes,
int maximumRouteLength,
Branching branching,
double valleyPreference,
double uphillPenalty,
double slopePenalty,
double confluenceAttraction
) {
public Routing {
if (tileSize < 32 || sampleSpacing < 4 || refinementSpacing < 1 || branching == null
|| tileSize % sampleSpacing != 0 || sampleSpacing % refinementSpacing != 0) {
throw new IllegalArgumentException("Routing sizes must form an exact bounded tile lattice.");
}
int latticeWidth = tileSize / sampleSpacing + 1;
int halo = Math.min(maximumRouteLength, Math.multiplyExact(sampleSpacing, 2));
int alignedHalo = Math.floorDiv(halo, sampleSpacing) * sampleSpacing;
int expandedWidth = latticeWidth + alignedHalo * 2 / sampleSpacing;
long latticeNodes = (long) expandedWidth * expandedWidth;
if (maximumRouteNodes < latticeNodes || maximumRouteNodes > 1_000_000 || maximumRouteLength < 1) {
throw new IllegalArgumentException("maximumRouteNodes must contain the complete lattice and remain bounded.");
}
if (branching.minimumSurfaceCourseLength() > maximumRouteLength
|| branching.minimumUndergroundCourseLength() > maximumRouteLength) {
throw new IllegalArgumentException("Minimum course lengths cannot exceed maximum route length.");
}
requireFiniteNonNegative(valleyPreference, "valleyPreference");
requireFiniteNonNegative(uphillPenalty, "uphillPenalty");
requireFiniteNonNegative(slopePenalty, "slopePenalty");
requireFiniteNonNegative(confluenceAttraction, "confluenceAttraction");
}
}
public record Branching(
int minimumSurfaceCourseLength,
int minimumUndergroundCourseLength
) {
public Branching {
if (minimumSurfaceCourseLength < 0 || minimumSurfaceCourseLength > 32_768
|| minimumUndergroundCourseLength < 0 || minimumUndergroundCourseLength > 32_768) {
throw new IllegalArgumentException("Hydrology branching lengths are invalid.");
}
}
public int minimumCourseLength(boolean surface) {
return surface ? minimumSurfaceCourseLength : minimumUndergroundCourseLength;
}
}
public record Source(
boolean enabled,
double density,
int minimumElevation,
int minimumPerTile,
int maximumPerTile,
int minimumSpacing
) {
public Source {
if (!Double.isFinite(density) || density < 0D || density > 64D) {
throw new IllegalArgumentException("Source density must be between 0 and 64 expected sources per tile.");
}
if (minimumPerTile < 0 || maximumPerTile < minimumPerTile || maximumPerTile > 4096) {
throw new IllegalArgumentException("Source tile quotas are invalid.");
}
if (minimumSpacing < 0) {
throw new IllegalArgumentException("Source spacing cannot be negative.");
}
}
}
public record Surface(
boolean enabled,
Source sources,
int minimumWidth,
int maximumWidth,
int minimumDepth,
int maximumDepth,
int minimumSurfaceInset,
int maximumSurfaceInset,
int maximumIncision,
double shoreWidth,
int minimumTerrainBlendWidth,
int maximumTerrainBlendWidth,
boolean ridgeTunnelsEnabled,
int maximumRidgeTunnelLength,
int ridgeTunnelHeadroom
) {
public Surface {
if (sources == null) {
throw new IllegalArgumentException("Surface source settings are required.");
}
requireRange(minimumWidth, maximumWidth, 1, 256, "surface width");
requireRange(minimumDepth, maximumDepth, 1, 64, "surface depth");
requireRange(minimumSurfaceInset, maximumSurfaceInset, 1, 64, "surface inset");
if (maximumIncision < 0 || !Double.isFinite(shoreWidth) || shoreWidth < 0D || shoreWidth > 32D) {
throw new IllegalArgumentException("Surface incision and shore width are invalid.");
}
requireRange(minimumTerrainBlendWidth, maximumTerrainBlendWidth, 0, 128, "terrain blend width");
if (maximumRidgeTunnelLength < 0 || ridgeTunnelHeadroom < 0) {
throw new IllegalArgumentException("Ridge tunnel bounds cannot be negative.");
}
}
}
public record Hydraulics(
int minimumTargetPoolLength,
int maximumTargetPoolLength,
int riffleDrop,
int maximumGradualDrop,
int maximumGradualLength,
int waterfallMinimumDrop
) {
public Hydraulics {
requireRange(minimumTargetPoolLength, maximumTargetPoolLength, 1, 4096, "target pool length");
if (riffleDrop < 0 || maximumGradualDrop < riffleDrop
|| maximumGradualLength < Math.max(1, maximumGradualDrop)
|| waterfallMinimumDrop != maximumGradualDrop + 1
|| minimumTargetPoolLength < maximumGradualLength) {
throw new IllegalArgumentException("Hydraulic transition thresholds are inconsistent.");
}
}
}
public record Underground(
boolean enabled,
Source sources,
int minimumFluidY,
int maximumFluidY,
int minimumWidth,
int maximumWidth,
int minimumDepth,
int maximumDepth,
int minimumHeadroom,
int maximumHeadroom,
boolean connectToExistingCaves
) {
public Underground {
if (sources == null || minimumFluidY > maximumFluidY) {
throw new IllegalArgumentException("Underground source and height settings are invalid.");
}
requireRange(minimumWidth, maximumWidth, 1, 256, "underground width");
requireRange(minimumDepth, maximumDepth, 1, 64, "underground depth");
requireRange(minimumHeadroom, maximumHeadroom, 1, 128, "underground headroom");
}
}
public record Outlets(
boolean oceanEnabled,
Grotto coastalGrotto,
Grotto inlandGrotto,
boolean surfaceSinkholesEnabled,
int coastalCliffMinimumHeight,
int mouthLevelingDistance,
int maximumOceanApron,
int maximumPerTile
) {
public Outlets {
if (coastalGrotto == null || inlandGrotto == null) {
throw new IllegalArgumentException("Coastal and inland grotto settings are required.");
}
if (surfaceSinkholesEnabled && !inlandGrotto.enabled()) {
throw new IllegalArgumentException("Surface sinkholes require inland grotto outlets.");
}
if (coastalCliffMinimumHeight < 0 || maximumOceanApron < 0 || maximumOceanApron > 64
|| mouthLevelingDistance < 0 || maximumPerTile < 1 || maximumPerTile > 256) {
throw new IllegalArgumentException("Outlet bounds are invalid.");
}
}
}
public record Grotto(
boolean enabled,
int horizontalRadius,
int verticalRadius,
int headroom,
int maximumVolume
) {
public Grotto {
if (horizontalRadius < 1 || horizontalRadius > 128 || verticalRadius < 1 || verticalRadius > 128
|| headroom < 1 || headroom > 128 || maximumVolume < 1 || maximumVolume > 1_048_576) {
throw new IllegalArgumentException("Grotto bounds are invalid.");
}
}
}
public record Geometry(
Meanders meanders,
ChannelShape surface,
ChannelShape underground,
ChannelShape grottos,
Drops drops
) {
public Geometry {
if (meanders == null || surface == null || underground == null || grottos == null || drops == null) {
throw new IllegalArgumentException("Hydrology geometry settings cannot contain null sections.");
}
}
public static Geometry defaults() {
ChannelShape channel = new ChannelShape(2.4D, 0.28D, 0.24D, 11);
return new Geometry(
new Meanders(64, 12, 0.34D, 0.42D, 0.48D, 1, 82D),
channel,
channel,
channel,
new Drops(2, 1.4D, 2, 0.45D, 2, 1.8D, 8)
);
}
}
public record Meanders(
int primaryWavelength,
int detailWavelength,
double primaryStrength,
double detailStrength,
double maximumOffsetRatio,
int smoothingPasses,
double maximumTurnDegrees
) {
public Meanders {
if (primaryWavelength < 8 || primaryWavelength > 512
|| detailWavelength < 4 || detailWavelength > 128
|| detailWavelength > primaryWavelength
|| smoothingPasses < 0 || smoothingPasses > 4
|| !Double.isFinite(primaryStrength) || primaryStrength < 0D || primaryStrength > 2D
|| !Double.isFinite(detailStrength) || detailStrength < 0D || detailStrength > 2D
|| !Double.isFinite(maximumOffsetRatio) || maximumOffsetRatio < 0D || maximumOffsetRatio > 1D
|| !Double.isFinite(maximumTurnDegrees)
|| maximumTurnDegrees < 10D || maximumTurnDegrees > 150D) {
throw new IllegalArgumentException("Hydrology meander geometry is invalid.");
}
}
}
public record ChannelShape(
double bedRoundness,
double bedRoughness,
double wallRoughness,
int roughnessWavelength
) {
public ChannelShape {
if (!Double.isFinite(bedRoundness) || bedRoundness < 1D || bedRoundness > 6D
|| !Double.isFinite(bedRoughness) || bedRoughness < 0D || bedRoughness > 1D
|| !Double.isFinite(wallRoughness) || wallRoughness < 0D || wallRoughness > 1D
|| roughnessWavelength < 3 || roughnessWavelength > 128) {
throw new IllegalArgumentException("Hydrology channel shape is invalid.");
}
}
}
public record Drops(
int cascadeRunPerBlock,
double cascadeExponent,
int maximumCascadeStep,
double flowWidthRatio,
int maximumFlowDepth,
double basinWidthRatio,
int maximumBasinDepth
) {
public Drops {
if (cascadeRunPerBlock < 1 || cascadeRunPerBlock > 16
|| !Double.isFinite(cascadeExponent) || cascadeExponent < 0.25D || cascadeExponent > 6D
|| maximumCascadeStep < 1 || maximumCascadeStep > 4
|| !Double.isFinite(flowWidthRatio) || flowWidthRatio < 0.25D || flowWidthRatio > 1D
|| maximumFlowDepth < 1 || maximumFlowDepth > 16
|| !Double.isFinite(basinWidthRatio) || basinWidthRatio < 1D || basinWidthRatio > 4D
|| maximumBasinDepth < maximumFlowDepth || maximumBasinDepth > 32) {
throw new IllegalArgumentException("Hydrology drop geometry is invalid.");
}
}
public int flowWidth(int channelWidth) {
return Math.max(1, Math.min(channelWidth, (int) StrictMath.ceil(channelWidth * flowWidthRatio)));
}
public int stepLimit(HydrologyFeatureType type) {
return type.isSurface() ? 1 : maximumCascadeStep;
}
public int flowDepth(int channelDepth) {
return Math.max(1, Math.min(channelDepth, maximumFlowDepth));
}
public int basinWidth(int flowWidth) {
return Math.max(flowWidth, (int) StrictMath.ceil(flowWidth * basinWidthRatio));
}
public int basinDepth(int flowDepth, int drop) {
int erodedDepth = Math.addExact(flowDepth, Math.max(1, (int) StrictMath.ceil(StrictMath.sqrt(drop))));
return Math.max(flowDepth, Math.min(maximumBasinDepth, erodedDepth));
}
}
public record DeepFluid(
String id,
boolean enabled,
double density,
int spacing,
int minimumY,
int maximumY,
int minimumHorizontalRadius,
int maximumHorizontalRadius,
int minimumVerticalRadius,
int maximumVerticalRadius,
int minimumChannelLength,
int maximumChannelLength,
int channelWidth,
int channelDepth,
int headroom,
int maximumVolume,
int maximumPerTile,
boolean containedPools,
boolean shortChannels
) {
public DeepFluid {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("Deep-fluid id must not be blank.");
}
id = id.trim();
if (!Double.isFinite(density) || density < 0D || density > 64D || spacing < 16 || minimumY > maximumY) {
throw new IllegalArgumentException("Deep-fluid density or height range is invalid.");
}
requireRange(minimumHorizontalRadius, maximumHorizontalRadius, 1, 128, "deep-fluid horizontal radius");
requireRange(minimumVerticalRadius, maximumVerticalRadius, 1, 128, "deep-fluid vertical radius");
requireRange(minimumChannelLength, maximumChannelLength, 0, 32_768, "deep-fluid channel length");
if (channelWidth < 1 || channelWidth > 256 || channelDepth < 1 || channelDepth > 64 || headroom < 1
|| maximumVolume < 64 || maximumVolume > 1_048_576
|| maximumPerTile < 0 || maximumPerTile > 1024) {
throw new IllegalArgumentException("Deep-fluid channel bounds are invalid.");
}
long poolEnvelope = ellipsoidVolume(
maximumHorizontalRadius,
Math.max(channelDepth, maximumVerticalRadius),
headroom
);
if (enabled && containedPools && poolEnvelope > maximumVolume) {
throw new IllegalArgumentException("Deep-fluid maximumVolume cannot contain its configured pool ellipsoid.");
}
if (enabled && !containedPools && !shortChannels) {
throw new IllegalArgumentException("An enabled deep fluid requires a pool or channel.");
}
if (enabled && shortChannels && minimumChannelLength < 1) {
throw new IllegalArgumentException("Enabled deep-fluid channels require a positive minimum length.");
}
}
}
private static void requireRange(int minimum, int maximum, int lower, int upper, String name) {
if (minimum < lower || maximum < minimum || maximum > upper) {
throw new IllegalArgumentException(name + " range is invalid.");
}
}
private static void requireFiniteNonNegative(double value, String name) {
if (!Double.isFinite(value) || value < 0D) {
throw new IllegalArgumentException(name + " must be finite and non-negative.");
}
}
static long ellipsoidVolume(int radius, int lowerExtent, int upperExtent) {
long volume = 0L;
for (int deltaZ = -radius; deltaZ <= radius; deltaZ++) {
for (int deltaX = -radius; deltaX <= radius; deltaX++) {
double distance = StrictMath.hypot(deltaX, deltaZ);
if (distance > radius + 0.25D) {
continue;
}
double normalized = Math.min(1D, distance / Math.max(1D, radius));
double scale = StrictMath.sqrt(Math.max(0D, 1D - normalized * normalized));
int lower = (int) StrictMath.ceil(lowerExtent * scale);
int upper = (int) StrictMath.floor(upperExtent * scale);
volume += lower + upper + 1L;
}
}
return volume;
}
}
@@ -0,0 +1,13 @@
package art.arcane.iris.engine.hydrology;
public record HydrologyPoint(int x, int y, int z) {
public long packedColumn() {
return RiverFootprint.pack(x, z);
}
public long distanceSquared2D(HydrologyPoint other) {
long deltaX = (long) x - other.x;
long deltaZ = (long) z - other.z;
return deltaX * deltaX + deltaZ * deltaZ;
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.engine.hydrology;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public record HydrologyRenderSample(int x, int z, List<HydrologyFeatureRef> features) {
private static final Comparator<HydrologyFeatureRef> FEATURE_ORDER = Comparator
.comparingInt((HydrologyFeatureRef feature) -> feature.type().renderPriority())
.thenComparingLong(HydrologyFeatureRef::id);
public HydrologyRenderSample {
Objects.requireNonNull(features, "features");
ArrayList<HydrologyFeatureRef> ordered = new ArrayList<>(features);
ordered.sort(FEATURE_ORDER);
Map<Long, HydrologyFeatureRef> unique = new LinkedHashMap<>();
for (HydrologyFeatureRef feature : ordered) {
unique.putIfAbsent(feature.id(), feature);
}
features = List.copyOf(unique.values());
}
public boolean present() {
return !features.isEmpty();
}
public boolean hasFeature(HydrologyFeatureType type) {
Objects.requireNonNull(type, "type");
for (HydrologyFeatureRef feature : features) {
if (feature.type() == type) {
return true;
}
}
return false;
}
public Optional<HydrologyFeatureRef> primaryFeature() {
return features.isEmpty() ? Optional.empty() : Optional.of(features.getFirst());
}
}
@@ -0,0 +1,21 @@
package art.arcane.iris.engine.hydrology;
public interface HydrologyRoutingTerrainSampler {
HydrologyTerrainSample[] sampleGrid(GridRequest request);
NaturalClassification classifyNatural(int blockX, int blockZ);
enum NaturalClassification {
LAND,
OCEAN,
UNAVAILABLE
}
record GridRequest(int minimumX, int minimumZ, int width, int spacing) {
public GridRequest {
if (width < 1 || spacing < 1) {
throw new IllegalArgumentException("Hydrology routing grid dimensions must be positive");
}
}
}
}
@@ -0,0 +1,107 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.Objects;
final class HydrologyTerrainCaveVoxelView implements CaveVoxelView {
private final HydrologyTerrainSampler sampler;
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> samples;
private final int seaLevel;
private final int minimumY;
private final int maximumY;
HydrologyTerrainCaveVoxelView(HydrologyTerrainSampler sampler, int seaLevel, int worldHeight) {
this(sampler, seaLevel, 0, worldHeight);
}
HydrologyTerrainCaveVoxelView(
HydrologyTerrainSampler sampler,
int seaLevel,
int minimumY,
int maximumY
) {
this(sampler, seaLevel, minimumY, maximumY, new Long2ObjectOpenHashMap<>());
}
private HydrologyTerrainCaveVoxelView(
HydrologyTerrainSampler sampler,
int seaLevel,
int minimumY,
int maximumY,
Long2ObjectOpenHashMap<HydrologyTerrainSample> samples
) {
this.sampler = Objects.requireNonNull(sampler);
this.samples = Objects.requireNonNull(samples);
this.seaLevel = seaLevel;
if (maximumY - minimumY < 3) {
throw new IllegalArgumentException("The cave view requires at least three vertical blocks");
}
this.minimumY = minimumY;
this.maximumY = maximumY;
}
HydrologyTerrainCaveVoxelView withSampleCache(
Long2ObjectOpenHashMap<HydrologyTerrainSample> sampleCache
) {
return new HydrologyTerrainCaveVoxelView(
sampler,
seaLevel,
minimumY,
maximumY,
sampleCache
);
}
@Override
public boolean isInWorld(CavePosition position) {
return position.y() > minimumY && position.y() < maximumY - 1;
}
@Override
public CaveVoxel voxelAt(CavePosition position) {
HydrologyTerrainSample terrain = sample(position.x(), position.z());
if (position.y() <= terrain.naturalHeight()) {
return CaveVoxel.SOLID;
}
if (terrain.ocean() && position.y() <= seaLevel) {
return CaveVoxel.COMPATIBLE_FLUID;
}
return CaveVoxel.CAVE_AIR;
}
@Override
public boolean isOpenToSurface(CavePosition position) {
return isAboveTerrainSurface(position);
}
@Override
public boolean isAboveTerrainSurface(CavePosition position) {
HydrologyTerrainSample terrain = sample(position.x(), position.z());
return position.y() > terrain.naturalHeight();
}
@Override
public boolean hasAboveTerrainSurface(int x, int z, int requestedMinimumY, int requestedMaximumY) {
int firstY = Math.max(requestedMinimumY, minimumY + 1);
int lastY = Math.min(requestedMaximumY, maximumY - 2);
return firstY <= lastY && lastY > sample(x, z).naturalHeight();
}
private HydrologyTerrainSample sample(int x, int z) {
long key = (long) x << 32 ^ z & 0xffffffffL;
HydrologyTerrainSample cached = samples.get(key);
if (cached != null) {
return cached;
}
HydrologyTerrainSample sampled = Objects.requireNonNull(
sampler.sample(x, z),
"Hydrology terrain sampler returned null at " + x + "," + z
);
samples.put(key, sampled);
return sampled;
}
}
@@ -0,0 +1,192 @@
package art.arcane.iris.engine.hydrology;
import java.util.List;
public record HydrologyTerrainSample(
int naturalHeight,
double slope,
boolean ocean,
boolean caveAvailable,
int caveFloorY,
int caveFluidY,
boolean transitAllowed,
boolean outletAllowed,
boolean surfaceSourceAllowed,
boolean surfaceSourceRequired,
boolean undergroundSourceAllowed,
boolean undergroundSourceRequired,
double routingCost,
double surfaceSourceWeight,
double undergroundSourceWeight,
double widthMultiplier,
double depthMultiplier,
double incisionMultiplier,
double routingMultiplier,
String parentBiomeKey,
String surfaceBiomeKey,
String mouthBiomeKey,
String shoreBiomeKey,
String dryBiomeKey,
String floodedCaveBiomeKey,
List<String> preferredProfileKeys
) {
public HydrologyTerrainSample {
requireFiniteNonNegative(slope, "slope");
requireFiniteNonNegative(routingCost, "routingCost");
requireFiniteNonNegative(surfaceSourceWeight, "surfaceSourceWeight");
requireFiniteNonNegative(undergroundSourceWeight, "undergroundSourceWeight");
requireFinitePositive(widthMultiplier, "widthMultiplier");
requireFinitePositive(depthMultiplier, "depthMultiplier");
requireFiniteNonNegative(incisionMultiplier, "incisionMultiplier");
requireFiniteNonNegative(routingMultiplier, "routingMultiplier");
if (caveAvailable && (caveFluidY <= caveFloorY || caveFluidY >= naturalHeight)) {
throw new IllegalArgumentException("Available cave fluid must be above its floor and below natural terrain.");
}
parentBiomeKey = normalizeKey(parentBiomeKey, "parent");
surfaceBiomeKey = normalizeKey(surfaceBiomeKey, parentBiomeKey);
mouthBiomeKey = normalizeKey(mouthBiomeKey, surfaceBiomeKey);
shoreBiomeKey = normalizeKey(shoreBiomeKey, parentBiomeKey);
dryBiomeKey = normalizeKey(dryBiomeKey, parentBiomeKey);
floodedCaveBiomeKey = normalizeKey(floodedCaveBiomeKey, surfaceBiomeKey);
preferredProfileKeys = normalizeProfiles(preferredProfileKeys);
}
public static HydrologyTerrainSample openLand(int naturalHeight, double slope, String parentBiomeKey) {
return new HydrologyTerrainSample(
naturalHeight,
slope,
false,
false,
naturalHeight - 32,
naturalHeight - 30,
true,
true,
true,
false,
false,
false,
0D,
1D,
1D,
1D,
1D,
1D,
1D,
parentBiomeKey,
parentBiomeKey,
parentBiomeKey,
parentBiomeKey,
parentBiomeKey,
parentBiomeKey,
List.of("default")
);
}
public static HydrologyTerrainSample ocean(int naturalHeight, String parentBiomeKey) {
return new HydrologyTerrainSample(
naturalHeight,
0D,
true,
false,
naturalHeight - 32,
naturalHeight - 30,
false,
false,
false,
false,
false,
false,
0D,
0D,
0D,
1D,
1D,
1D,
1D,
parentBiomeKey,
parentBiomeKey,
parentBiomeKey,
parentBiomeKey,
parentBiomeKey,
parentBiomeKey,
List.of("default")
);
}
public HydrologyTerrainSample withSlope(double replacementSlope) {
return new HydrologyTerrainSample(
naturalHeight,
replacementSlope,
ocean,
caveAvailable,
caveFloorY,
caveFluidY,
transitAllowed,
outletAllowed,
surfaceSourceAllowed,
surfaceSourceRequired,
undergroundSourceAllowed,
undergroundSourceRequired,
routingCost,
surfaceSourceWeight,
undergroundSourceWeight,
widthMultiplier,
depthMultiplier,
incisionMultiplier,
routingMultiplier,
parentBiomeKey,
surfaceBiomeKey,
mouthBiomeKey,
shoreBiomeKey,
dryBiomeKey,
floodedCaveBiomeKey,
preferredProfileKeys
);
}
private static List<String> normalizeProfiles(List<String> values) {
if (values == null || values.isEmpty()) {
return List.of("default");
}
if (profilesNormalized(values)) {
return List.copyOf(values);
}
List<String> normalized = values.stream()
.filter((String value) -> value != null && !value.isBlank())
.map(String::trim)
.distinct()
.sorted()
.toList();
return normalized.isEmpty() ? List.of("default") : normalized;
}
private static boolean profilesNormalized(List<String> values) {
String previous = null;
for (String value : values) {
if (value == null
|| value.isBlank()
|| !value.equals(value.trim())
|| previous != null && previous.compareTo(value) >= 0) {
return false;
}
previous = value;
}
return true;
}
private static String normalizeKey(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.trim();
}
private static void requireFiniteNonNegative(double value, String name) {
if (!Double.isFinite(value) || value < 0D) {
throw new IllegalArgumentException(name + " must be finite and non-negative.");
}
}
private static void requireFinitePositive(double value, String name) {
if (!Double.isFinite(value) || value <= 0D) {
throw new IllegalArgumentException(name + " must be finite and positive.");
}
}
}
@@ -0,0 +1,6 @@
package art.arcane.iris.engine.hydrology;
@FunctionalInterface
public interface HydrologyTerrainSampler {
HydrologyTerrainSample sample(int blockX, int blockZ);
}
@@ -0,0 +1,618 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public final class HydrologyTile {
private final HydrologyTileKey key;
private final long worldSeed;
private final long settingsFingerprint;
private final int tileSize;
private final List<DrainageNode> nodes;
private final List<DrainageEdge> edges;
private final List<RiverOutlet> outlets;
private final List<RiverCourse> courses;
private final List<HydrologyCavePlan> cavePlans;
private final List<HydrologyDiagnosticCandidate> diagnosticCandidates;
private final RiverFootprint footprint;
private final Map<Long, DrainageNode> nodesById;
private final Map<Long, RiverOutlet> outletsById;
private final Map<Long, HydrologyCavePlan> cavePlansByCourseId;
private final List<HydrologyFeatureRef> features;
public HydrologyTile(
HydrologyTileKey key,
long worldSeed,
long settingsFingerprint,
int tileSize,
List<DrainageNode> nodes,
List<DrainageEdge> edges,
List<RiverOutlet> outlets,
List<RiverCourse> courses,
List<HydrologyCavePlan> cavePlans,
List<HydrologyDiagnosticCandidate> diagnosticCandidates,
RiverFootprint footprint
) {
this.key = Objects.requireNonNull(key, "key");
if (tileSize <= 0) {
throw new IllegalArgumentException("tileSize must be positive.");
}
this.worldSeed = worldSeed;
this.settingsFingerprint = settingsFingerprint;
this.tileSize = tileSize;
this.nodes = sortedCopy(nodes, Comparator.comparingLong(DrainageNode::id));
this.edges = sortedCopy(edges, Comparator.comparingLong(DrainageEdge::id));
this.outlets = sortedCopy(outlets, Comparator.comparingLong(RiverOutlet::id));
this.courses = sortedCopy(courses, Comparator.comparingLong(RiverCourse::id));
this.cavePlans = sortedCopy(
cavePlans,
Comparator.comparingLong((HydrologyCavePlan plan) -> plan.source().sourceId())
);
this.diagnosticCandidates = sortedCopy(
diagnosticCandidates,
Comparator.comparingLong(HydrologyDiagnosticCandidate::id)
);
this.footprint = Objects.requireNonNull(footprint, "footprint");
this.nodesById = indexNodes(this.nodes);
this.outletsById = indexOutlets(this.outlets);
this.cavePlansByCourseId = indexCavePlans(this.cavePlans);
validateGraph();
validateCourses();
this.features = collectFeatures(footprint);
}
public HydrologyTileKey key() {
return key;
}
public long worldSeed() {
return worldSeed;
}
public long settingsFingerprint() {
return settingsFingerprint;
}
public int tileSize() {
return tileSize;
}
public List<DrainageNode> nodes() {
return nodes;
}
public List<DrainageEdge> edges() {
return edges;
}
public List<RiverOutlet> outlets() {
return outlets;
}
public List<RiverCourse> courses() {
return courses;
}
public List<HydrologyCavePlan> cavePlans() {
return cavePlans;
}
public List<HydrologyDiagnosticCandidate> diagnosticCandidates() {
return diagnosticCandidates;
}
public RiverFootprint footprint() {
return footprint;
}
public Optional<DrainageNode> node(long id) {
return Optional.ofNullable(nodesById.get(id));
}
public Optional<RiverOutlet> outlet(long id) {
return Optional.ofNullable(outletsById.get(id));
}
public Optional<HydrologyCavePlan> cavePlan(long courseId) {
return Optional.ofNullable(cavePlansByCourseId.get(courseId));
}
public Optional<HydrologyColumnSample> columnAt(int x, int z) {
return footprint.sample(x, z);
}
public HydrologyRenderSample renderAt(int x, int z) {
return footprint.renderSample(x, z);
}
public HydrologyDiagnosticRenderSample diagnosticRenderAt(int x, int z, int maximumDistance) {
if (maximumDistance < 0) {
throw new IllegalArgumentException("maximumDistance cannot be negative.");
}
long maximumDistanceSquared = (long) maximumDistance * maximumDistance;
ArrayList<HydrologyDiagnosticCandidate> selected = new ArrayList<>();
for (HydrologyDiagnosticCandidate candidate : diagnosticCandidates) {
long distance = candidate.point().distanceSquared2D(new HydrologyPoint(x, candidate.point().y(), z));
if (distance <= maximumDistanceSquared) {
selected.add(candidate);
}
}
return new HydrologyDiagnosticRenderSample(x, z, selected);
}
public List<HydrologyFeatureRef> features() {
return features;
}
public Optional<HydrologyFeatureRef> nearestFeature(
HydrologyFeatureType type,
int x,
int z,
int maximumDistance
) {
return nearestFeature(Set.of(Objects.requireNonNull(type, "type")), null, x, z, maximumDistance);
}
public Optional<HydrologyFeatureRef> nearestFeature(
Set<HydrologyFeatureType> types,
String profileKey,
int x,
int z,
int maximumDistance
) {
Objects.requireNonNull(types, "types");
if (maximumDistance < 0) {
throw new IllegalArgumentException("maximumDistance cannot be negative.");
}
long maximumDistanceSquared = (long) maximumDistance * maximumDistance;
HydrologyFeatureRef nearest = null;
long nearestDistance = Long.MAX_VALUE;
for (RiverCourse course : courses) {
if (profileKey != null && !profileKey.isBlank() && !profileKey.equals(course.profileKey())) {
continue;
}
for (HydraulicSegment segment : course.segments()) {
if (!types.contains(segment.type())) {
continue;
}
for (HydrologyPoint point : segment.centerline()) {
long deltaX = (long) point.x() - x;
long deltaZ = (long) point.z() - z;
long distance = deltaX * deltaX + deltaZ * deltaZ;
if (distance > maximumDistanceSquared || distance > nearestDistance) {
continue;
}
HydrologyFeatureRef feature = publishedCenterlineFeature(segment, point);
if (feature == null) {
continue;
}
if (distance < nearestDistance || nearest == null || feature.id() < nearest.id()) {
nearest = feature;
nearestDistance = distance;
}
}
}
}
return Optional.ofNullable(nearest);
}
public boolean acyclic() {
for (DrainageEdge edge : edges) {
DrainageNode upstream = nodesById.get(edge.upstreamNodeId());
DrainageNode downstream = nodesById.get(edge.downstreamNodeId());
if (upstream == null || downstream == null || downstream.potential() >= upstream.potential()) {
return false;
}
}
return true;
}
private void validateGraph() {
if (!acyclic()) {
throw new IllegalArgumentException("Drainage edges must strictly lower potential.");
}
for (DrainageNode node : nodes) {
if (!outletsById.containsKey(node.outletId())) {
throw new IllegalArgumentException("Every drainage node must resolve to an accepted outlet.");
}
}
}
private void validateCourses() {
LinkedHashMap<Long, RiverCourse> coursesById = new LinkedHashMap<>();
for (RiverCourse course : courses) {
if (coursesById.put(course.id(), course) != null) {
throw new IllegalArgumentException("Duplicate river course id: " + course.id());
}
if (!course.hydraulicallyNonRising()) {
throw new IllegalArgumentException("Course heads must be non-rising downstream.");
}
if (course.sourceNodeId().isPresent() && !nodesById.containsKey(course.sourceNodeId().getAsLong())) {
throw new IllegalArgumentException("Course source is not part of the accepted drainage graph.");
}
if (course.outletId().isPresent() && !outletsById.containsKey(course.outletId().getAsLong())) {
throw new IllegalArgumentException("Course outlet is not accepted by this tile.");
}
if (isCaveBearing(course) != cavePlansByCourseId.containsKey(course.id())) {
throw new IllegalArgumentException("Every cave-bearing course must have one accepted containment plan.");
}
}
validateFootprintCourses(coursesById);
}
private void validateFootprintCourses(Map<Long, RiverCourse> coursesById) {
LinkedHashMap<Long, Map<CavePosition, HydrologyCaveAction>> expectedActions = new LinkedHashMap<>();
for (HydrologyColumnSample sample : footprint.columns().values()) {
ArrayList<Long> caveCourseIds = new ArrayList<>();
for (HydrologyColumnLayer layer : sample.layers()) {
long courseId = layer.feature().courseId();
if (!coursesById.containsKey(courseId)) {
throw new IllegalArgumentException("Footprint references an unaccepted river course: " + courseId);
}
if (layer.oceanApron() || !layer.channel() || !layer.terrainOwned()
|| !isCaveLayer(layer)) {
continue;
}
Map<CavePosition, HydrologyCaveAction> courseActions = expectedActions.computeIfAbsent(
courseId,
(Long ignored) -> new HashMap<>()
);
if (!caveCourseIds.contains(courseId)) {
caveCourseIds.add(courseId);
}
for (int y = layer.bedY() + 1; y <= layer.ceilingY(); y++) {
CavePosition position = new CavePosition(sample.x(), y, sample.z());
HydrologyCaveAction action = actionAt(layer, y);
HydrologyCaveAction existing = courseActions.get(position);
if (existing == null || actionPriority(action) < actionPriority(existing)) {
courseActions.put(position, action);
}
}
}
composeSurfaceActions(sample, caveCourseIds, coursesById, expectedActions);
}
HashSet<Long> validatedCourseIds = new HashSet<>();
for (RiverCourse course : courses) {
if (!isCaveBearing(course)) {
continue;
}
if (!validatedCourseIds.add(course.id())) {
continue;
}
List<RiverCourse> sharedCourses = sharedOutletCaveCourses(course);
HashMap<CavePosition, HydrologyCaveAction> expected = new HashMap<>();
HashMap<CavePosition, HydrologyCaveAction> actual = new HashMap<>();
for (RiverCourse sharedCourse : sharedCourses) {
validatedCourseIds.add(sharedCourse.id());
mergeCaveActions(
expected,
expectedActions.getOrDefault(sharedCourse.id(), Map.of())
);
HydrologyCavePlan plan = cavePlansByCourseId.get(sharedCourse.id());
plan.forEachAction((CavePosition position, HydrologyCaveAction action) -> {
if (action != HydrologyCaveAction.SEAL_GUARD) {
mergeCaveAction(actual, position, action);
}
});
}
if (!actual.equals(expected)) {
throw caveFootprintMismatch(course.id(), expected, actual);
}
}
}
private List<RiverCourse> sharedOutletCaveCourses(RiverCourse course) {
if (course.outletId().isEmpty()) {
return List.of(course);
}
long outletId = course.outletId().getAsLong();
ArrayList<RiverCourse> shared = new ArrayList<>();
for (RiverCourse candidate : courses) {
if (isCaveBearing(candidate)
&& candidate.outletId().isPresent()
&& candidate.outletId().getAsLong() == outletId
&& candidate.profileKey().equals(course.profileKey())) {
shared.add(candidate);
}
}
return List.copyOf(shared);
}
private void mergeCaveActions(
Map<CavePosition, HydrologyCaveAction> target,
Map<CavePosition, HydrologyCaveAction> source
) {
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : source.entrySet()) {
mergeCaveAction(target, entry.getKey(), entry.getValue());
}
}
private void mergeCaveAction(
Map<CavePosition, HydrologyCaveAction> actions,
CavePosition position,
HydrologyCaveAction action
) {
HydrologyCaveAction existing = actions.get(position);
if (existing == null || actionPriority(action) < actionPriority(existing)) {
actions.put(position, action);
}
}
private IllegalArgumentException caveFootprintMismatch(
long courseId,
Map<CavePosition, HydrologyCaveAction> expected,
Map<CavePosition, HydrologyCaveAction> actual
) {
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : expected.entrySet()) {
HydrologyCaveAction actualAction = actual.get(entry.getKey());
if (actualAction != entry.getValue()) {
return new IllegalArgumentException("Cave footprint differs from its accepted containment plan: "
+ courseId + " at " + entry.getKey() + ", footprint=" + entry.getValue()
+ ", plan=" + actualAction + ".");
}
}
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : actual.entrySet()) {
if (!expected.containsKey(entry.getKey())) {
return new IllegalArgumentException("Cave footprint differs from its accepted containment plan: "
+ courseId + " has extra plan action " + entry.getValue() + " at " + entry.getKey() + ".");
}
}
return new IllegalArgumentException("Cave footprint differs from its accepted containment plan: "
+ courseId + ".");
}
private void composeSurfaceActions(
HydrologyColumnSample sample,
List<Long> caveCourseIds,
Map<Long, RiverCourse> coursesById,
Map<Long, Map<CavePosition, HydrologyCaveAction>> expectedActions
) {
if (caveCourseIds.isEmpty()) {
return;
}
int minimumY = Integer.MAX_VALUE;
int maximumY = Integer.MIN_VALUE;
for (HydrologyColumnLayer layer : sample.layers()) {
if (!layer.publishesSurfaceFluid()) {
continue;
}
minimumY = Math.min(minimumY, layer.bedY() + 1);
maximumY = Math.max(maximumY, layer.fluidHeadY());
}
for (int y = minimumY; y <= maximumY; y++) {
HydrologyColumnSample.SurfacePublicationCell surfaceCell = sample
.surfacePublicationCellAt(y)
.orElse(null);
if (surfaceCell == null) {
continue;
}
CavePosition position = new CavePosition(sample.x(), y, sample.z());
for (long courseId : caveCourseIds) {
Map<CavePosition, HydrologyCaveAction> courseActions = expectedActions.get(courseId);
if (!courseActions.containsKey(position)) {
continue;
}
RiverCourse course = coursesById.get(courseId);
if (!course.profileKey().equals(surfaceCell.layer().profileKey())) {
throw new IllegalArgumentException("Surface and cave hydrology profiles conflict at "
+ position + ".");
}
courseActions.put(position, surfaceCell.action());
}
}
}
private boolean isCaveBearing(RiverCourse course) {
for (HydraulicSegment segment : course.segments()) {
if (segment.type().isUnderground() || segment.type().isDeepFluid()) {
return true;
}
}
return false;
}
private boolean isCaveLayer(HydrologyColumnLayer layer) {
return layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid();
}
private HydrologyCaveAction actionAt(HydrologyColumnLayer layer, int y) {
if (y > layer.fluidHeadY()) {
return HydrologyCaveAction.DRY_AIR;
}
if (layer.fallingFluid() && y < layer.fluidHeadY()) {
return HydrologyCaveAction.FALLING_FLUID;
}
return HydrologyCaveAction.WET_SOURCE;
}
private int actionPriority(HydrologyCaveAction action) {
return switch (action) {
case WET_SOURCE -> 0;
case FALLING_FLUID -> 1;
case DRY_AIR -> 2;
case SEAL_GUARD -> 3;
};
}
private static Map<Long, DrainageNode> indexNodes(List<DrainageNode> values) {
LinkedHashMap<Long, DrainageNode> indexed = new LinkedHashMap<>();
for (DrainageNode value : values) {
if (indexed.put(value.id(), value) != null) {
throw new IllegalArgumentException("Duplicate drainage node id: " + value.id());
}
}
return Collections.unmodifiableMap(indexed);
}
private static Map<Long, RiverOutlet> indexOutlets(List<RiverOutlet> values) {
LinkedHashMap<Long, RiverOutlet> indexed = new LinkedHashMap<>();
for (RiverOutlet value : values) {
if (indexed.put(value.id(), value) != null) {
throw new IllegalArgumentException("Duplicate river outlet id: " + value.id());
}
}
return Collections.unmodifiableMap(indexed);
}
private static Map<Long, HydrologyCavePlan> indexCavePlans(List<HydrologyCavePlan> values) {
LinkedHashMap<Long, HydrologyCavePlan> indexed = new LinkedHashMap<>();
for (HydrologyCavePlan value : values) {
if (!value.accepted()) {
throw new IllegalArgumentException("Hydrology tiles cannot contain rejected cave plans.");
}
long courseId = value.source().sourceId();
if (indexed.put(courseId, value) != null) {
throw new IllegalArgumentException("Duplicate cave plan for course: " + courseId);
}
}
return Collections.unmodifiableMap(indexed);
}
private HydrologyFeatureRef publishedCenterlineFeature(
HydraulicSegment segment,
HydrologyPoint point
) {
HydrologyColumnSample column = footprint.columns().get(RiverFootprint.pack(point.x(), point.z()));
if (column == null) {
return null;
}
for (HydrologyColumnLayer layer : column.layers()) {
HydrologyFeatureRef feature = layer.feature();
if (layer.channel()
&& feature.segmentId() == segment.id()
&& feature.type() == segment.type()) {
if (feature.x() == point.x() && feature.z() == point.z()) {
return feature;
}
return new HydrologyFeatureRef(
feature.id(),
feature.type(),
feature.courseId(),
feature.segmentId(),
point.x(),
layer.fluidHeadY(),
point.z(),
feature.flowDeltaX(),
feature.flowDeltaZ(),
feature.source()
);
}
}
return null;
}
static List<HydrologyFeatureRef> collectFeatures(RiverFootprint footprint) {
HashMap<Long, HydrologyColumnLayer> representatives = new HashMap<>();
for (HydrologyColumnSample column : footprint.columns().values()) {
for (HydrologyColumnLayer layer : column.layers()) {
long segmentId = layer.feature().segmentId();
HydrologyColumnLayer existing = representatives.get(segmentId);
if (existing == null || featureOrder(layer, existing) < 0) {
representatives.put(segmentId, layer);
}
}
}
ArrayList<HydrologyFeatureRef> ordered = new ArrayList<>(representatives.size());
for (HydrologyColumnLayer layer : representatives.values()) {
ordered.add(layer.feature());
}
ordered.sort(Comparator.comparingInt((HydrologyFeatureRef feature) -> feature.type().renderPriority())
.thenComparingLong(HydrologyFeatureRef::id));
return List.copyOf(ordered);
}
private static int featureOrder(HydrologyColumnLayer first, HydrologyColumnLayer second) {
int role = Integer.compare(featureRole(first), featureRole(second));
if (role != 0) {
return role;
}
HydrologyFeatureRef firstFeature = first.feature();
HydrologyFeatureRef secondFeature = second.feature();
int y = Integer.compare(firstFeature.y(), secondFeature.y());
if (y != 0) {
return y;
}
int x = Integer.compare(firstFeature.x(), secondFeature.x());
return x != 0 ? x : Integer.compare(firstFeature.z(), secondFeature.z());
}
private static int featureRole(HydrologyColumnLayer layer) {
if (layer.feature().source()) {
return 0;
}
if (layer.fallingFluid()) {
return 1;
}
if (layer.receivingPool()) {
return 2;
}
if (layer.channel() && !layer.oceanApron()) {
return 3;
}
if (layer.channel()) {
return 4;
}
return 5;
}
private static <T> List<T> sortedCopy(List<T> values, Comparator<T> comparator) {
Objects.requireNonNull(values, "values");
ArrayList<T> ordered = new ArrayList<>(values);
ordered.sort(comparator);
return List.copyOf(ordered);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof HydrologyTile tile)) {
return false;
}
return worldSeed == tile.worldSeed
&& settingsFingerprint == tile.settingsFingerprint
&& tileSize == tile.tileSize
&& key.equals(tile.key)
&& nodes.equals(tile.nodes)
&& edges.equals(tile.edges)
&& outlets.equals(tile.outlets)
&& courses.equals(tile.courses)
&& cavePlans.equals(tile.cavePlans)
&& diagnosticCandidates.equals(tile.diagnosticCandidates)
&& footprint.equals(tile.footprint);
}
@Override
public int hashCode() {
return Objects.hash(
key,
worldSeed,
settingsFingerprint,
tileSize,
nodes,
edges,
outlets,
courses,
cavePlans,
diagnosticCandidates,
footprint
);
}
@Override
public String toString() {
return "HydrologyTile[key=" + key + ", courses=" + courses.size() + ", candidates="
+ diagnosticCandidates.size() + ", columns=" + footprint.size() + "]";
}
}
@@ -0,0 +1,267 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.parallel.MultiBurst;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicLong;
public final class HydrologyTileCache implements AutoCloseable {
private static final int DEFAULT_MAXIMUM_ENTRIES = 64;
private static final int MAXIMUM_COMPOSED_CHUNKS = 256;
private static final int CHUNK_SIZE = 16;
private static final int CHUNK_COLUMN_COUNT = CHUNK_SIZE * CHUNK_SIZE;
private final HydrologyPlanner planner;
private final int maximumEntries;
private final Cache<HydrologyTileKey, HydrologyTile> tiles;
private final Cache<Long, ChunkColumns> composedChunks;
private final AtomicLong cacheEpoch;
private final ThreadLocal<LocalChunkColumns> localChunkColumns;
public HydrologyTileCache(HydrologyPlanner planner) {
this(planner, DEFAULT_MAXIMUM_ENTRIES);
}
public HydrologyTileCache(HydrologyPlanner planner, int maximumEntries) {
this.planner = Objects.requireNonNull(planner, "planner");
if (maximumEntries < 1) {
throw new IllegalArgumentException("maximumEntries must be positive.");
}
this.maximumEntries = maximumEntries;
this.tiles = Caffeine.newBuilder()
.maximumSize(maximumEntries)
.build();
this.composedChunks = Caffeine.newBuilder()
.maximumSize(MAXIMUM_COMPOSED_CHUNKS)
.build();
this.cacheEpoch = new AtomicLong();
this.localChunkColumns = new ThreadLocal<>();
}
public HydrologyTile get(HydrologyTileKey key) {
Objects.requireNonNull(key, "key");
return tiles.get(key, planner::plan);
}
public Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
return chunkColumns(blockX, blockZ).columnAt(blockX, blockZ);
}
public void prepareChunkColumns(int blockX, int blockZ) {
chunkColumns(blockX, blockZ);
}
public HydrologyRenderSample renderAt(int blockX, int blockZ) {
return columnAt(blockX, blockZ)
.map(HydrologyColumnSample::renderSample)
.orElseGet(() -> new HydrologyRenderSample(blockX, blockZ, List.of()));
}
public void invalidate(HydrologyTileKey key) {
Objects.requireNonNull(key, "key");
clear();
}
public void clear() {
cacheEpoch.incrementAndGet();
planner.clearOwnerDrafts();
tiles.invalidateAll();
composedChunks.invalidateAll();
localChunkColumns.remove();
}
public int size() {
tiles.cleanUp();
return Math.toIntExact(tiles.estimatedSize());
}
public int maximumEntries() {
return maximumEntries;
}
@Override
public void close() {
clear();
}
private ChunkColumns chunkColumns(int blockX, int blockZ) {
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
long epoch = cacheEpoch.get();
LocalChunkColumns local = localChunkColumns.get();
if (local != null && local.epoch() == epoch
&& local.columns().chunkX() == chunkX
&& local.columns().chunkZ() == chunkZ) {
return local.columns();
}
long packedChunk = RiverFootprint.pack(chunkX, chunkZ);
ChunkColumns columns = composedChunks.get(
packedChunk,
ignored -> composeChunkColumns(chunkX, chunkZ)
);
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
return columns;
}
private ChunkColumns composeChunkColumns(int chunkX, int chunkZ) {
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumBlockZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
int tileSize = planner.settings().routing().tileSize();
int publicationRadius = planner.settings().publicationRadius();
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) minimumBlockX + CHUNK_SIZE - 1L + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) minimumBlockZ - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) minimumBlockZ + CHUNK_SIZE - 1L + publicationRadius, tileSize);
ArrayList<HydrologyTileKey> relevantKeys = new ArrayList<>(
Math.multiplyExact(maximumTileX - minimumTileX + 1, maximumTileZ - minimumTileZ + 1)
);
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
relevantKeys.add(new HydrologyTileKey(tileX, tileZ));
}
}
List<HydrologyTile> relevantTiles = loadTiles(relevantKeys);
HydrologyColumnSample[] columns = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
int blockX = minimumBlockX + localX;
int blockZ = minimumBlockZ + localZ;
columns[localZ * CHUNK_SIZE + localX] = composeColumn(
blockX,
blockZ,
relevantKeys,
relevantTiles
)
.orElse(null);
}
}
return new ChunkColumns(chunkX, chunkZ, columns);
}
private List<HydrologyTile> loadTiles(List<HydrologyTileKey> keys) {
if (keys.size() < 2
|| !IrisPlatforms.isBound()) {
ArrayList<HydrologyTile> loaded = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
loaded.add(get(key));
}
return List.copyOf(loaded);
}
ArrayList<CompletableFuture<HydrologyTile>> futures = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
futures.add(CompletableFuture.supplyAsync(() -> get(key), MultiBurst.burst));
}
ArrayList<HydrologyTile> loaded = new ArrayList<>(keys.size());
for (CompletableFuture<HydrologyTile> future : futures) {
loaded.add(awaitTile(future));
}
return List.copyOf(loaded);
}
private static HydrologyTile awaitTile(CompletableFuture<HydrologyTile> future) {
try {
return future.join();
} catch (CompletionException failure) {
Throwable cause = failure.getCause();
if (cause instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (cause instanceof Error error) {
throw error;
}
throw new IllegalStateException("Hydrology tile planning failed.", cause);
}
}
private Optional<HydrologyColumnSample> composeColumn(
int blockX,
int blockZ,
List<HydrologyTileKey> relevantKeys,
List<HydrologyTile> relevantTiles
) {
ArrayList<HydrologyColumnLayer> layers = new ArrayList<>();
HydrologyColumnSample template = null;
int tileSize = planner.settings().routing().tileSize();
int publicationRadius = planner.settings().publicationRadius();
int minimumTileX = tileCoordinate((long) blockX - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) blockX + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) blockZ - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) blockZ + publicationRadius, tileSize);
for (int tileIndex = 0; tileIndex < relevantTiles.size(); tileIndex++) {
HydrologyTileKey key = relevantKeys.get(tileIndex);
if (key.tileX() < minimumTileX || key.tileX() > maximumTileX
|| key.tileZ() < minimumTileZ || key.tileZ() > maximumTileZ) {
continue;
}
HydrologyTile tile = relevantTiles.get(tileIndex);
Optional<HydrologyColumnSample> candidate = tile.columnAt(blockX, blockZ);
if (candidate.isEmpty()) {
continue;
}
HydrologyColumnSample sample = candidate.get();
if (template == null) {
template = sample;
} else {
requireMatchingTerrain(template, sample);
}
layers.addAll(sample.layers());
}
if (template == null) {
return Optional.empty();
}
LinkedHashMap<Long, HydrologyColumnLayer> unique = new LinkedHashMap<>();
for (HydrologyColumnLayer layer : layers) {
HydrologyColumnLayer existing = unique.putIfAbsent(layer.feature().id(), layer);
if (existing != null && !existing.equals(layer)) {
throw new IllegalStateException("Hydrology feature id collision at " + blockX + "," + blockZ + ".");
}
}
return Optional.of(new HydrologyColumnSample(
blockX,
blockZ,
template.naturalHeight(),
template.seaLevel(),
template.ocean(),
template.parentBiomeKey(),
new ArrayList<>(unique.values())
));
}
private static int tileCoordinate(long blockCoordinate, int tileSize) {
return Math.toIntExact(Math.floorDiv(blockCoordinate, tileSize));
}
private static void requireMatchingTerrain(
HydrologyColumnSample expected,
HydrologyColumnSample actual
) {
if (expected.naturalHeight() != actual.naturalHeight()
|| expected.seaLevel() != actual.seaLevel()
|| expected.ocean() != actual.ocean()
|| !expected.parentBiomeKey().equals(actual.parentBiomeKey())) {
throw new IllegalStateException(
"Hydrology plans disagree on terrain metadata at " + expected.x() + "," + expected.z() + "."
);
}
}
private record ChunkColumns(int chunkX, int chunkZ, HydrologyColumnSample[] columns) {
private Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
int localX = Math.floorMod(blockX, CHUNK_SIZE);
int localZ = Math.floorMod(blockZ, CHUNK_SIZE);
return Optional.ofNullable(columns[localZ * CHUNK_SIZE + localX]);
}
}
private record LocalChunkColumns(long epoch, ChunkColumns columns) {
}
}
@@ -0,0 +1,31 @@
package art.arcane.iris.engine.hydrology;
public record HydrologyTileKey(int tileX, int tileZ) implements Comparable<HydrologyTileKey> {
public static HydrologyTileKey fromBlock(int blockX, int blockZ, int tileSize) {
if (tileSize <= 0) {
throw new IllegalArgumentException("Tile size must be positive.");
}
return new HydrologyTileKey(Math.floorDiv(blockX, tileSize), Math.floorDiv(blockZ, tileSize));
}
public int minimumBlockX(int tileSize) {
return Math.multiplyExact(tileX, tileSize);
}
public int minimumBlockZ(int tileSize) {
return Math.multiplyExact(tileZ, tileSize);
}
public boolean contains(int blockX, int blockZ, int tileSize) {
int minimumX = minimumBlockX(tileSize);
int minimumZ = minimumBlockZ(tileSize);
return blockX >= minimumX && blockX < minimumX + tileSize
&& blockZ >= minimumZ && blockZ < minimumZ + tileSize;
}
@Override
public int compareTo(HydrologyTileKey other) {
int xComparison = Integer.compare(tileX, other.tileX);
return xComparison != 0 ? xComparison : Integer.compare(tileZ, other.tileZ);
}
}
@@ -0,0 +1,107 @@
package art.arcane.iris.engine.hydrology;
import java.util.List;
import java.util.Objects;
import java.util.OptionalLong;
public record RiverCourse(
long id,
RiverCourseType type,
OptionalLong sourceNodeId,
OptionalLong outletId,
String profileKey,
int discharge,
List<DrainageEdge> drainageEdges,
List<HydraulicSegment> segments
) {
public RiverCourse {
Objects.requireNonNull(type, "type");
Objects.requireNonNull(sourceNodeId, "sourceNodeId");
Objects.requireNonNull(outletId, "outletId");
Objects.requireNonNull(drainageEdges, "drainageEdges");
Objects.requireNonNull(segments, "segments");
if (profileKey == null || profileKey.isBlank()) {
throw new IllegalArgumentException("profileKey must not be blank.");
}
profileKey = profileKey.trim();
if (discharge <= 0) {
throw new IllegalArgumentException("Course discharge must be positive.");
}
drainageEdges = List.copyOf(drainageEdges);
segments = List.copyOf(segments);
if (segments.isEmpty()) {
throw new IllegalArgumentException("A river course requires at least one hydraulic segment.");
}
for (HydraulicSegment segment : segments) {
if (segment.courseId() != id) {
throw new IllegalArgumentException("Every segment must belong to its containing course.");
}
}
validateSinkholeContinuation(type, segments);
if (type == RiverCourseType.DEEP_FLUID && (sourceNodeId.isPresent() || outletId.isPresent())) {
throw new IllegalArgumentException("Independent deep-fluid courses cannot reference drainage nodes or river outlets.");
}
if (type != RiverCourseType.DEEP_FLUID && (sourceNodeId.isEmpty() || outletId.isEmpty())) {
throw new IllegalArgumentException("Surface and underground courses require a source and outlet.");
}
}
public boolean hydraulicallyNonRising() {
int previousHead = Integer.MAX_VALUE;
for (HydraulicSegment segment : segments) {
if (segment.upstreamHeadY() > previousHead || segment.downstreamHeadY() > segment.upstreamHeadY()) {
return false;
}
previousHead = segment.downstreamHeadY();
}
return true;
}
public boolean surfaceSinkholeContinuation() {
return type == RiverCourseType.SURFACE
&& segments.size() >= 2
&& segments.get(segments.size() - 2).type() == HydrologyFeatureType.SINKHOLE
&& segments.getLast().type() == HydrologyFeatureType.INLAND_GROTTO;
}
private static void validateSinkholeContinuation(
RiverCourseType courseType,
List<HydraulicSegment> segments
) {
int sinkholeCount = 0;
for (HydraulicSegment segment : segments) {
if (segment.type() == HydrologyFeatureType.SINKHOLE) {
sinkholeCount++;
}
}
boolean terminalInlandGrotto = courseType == RiverCourseType.SURFACE
&& segments.getLast().type() == HydrologyFeatureType.INLAND_GROTTO;
if (!terminalInlandGrotto) {
if (sinkholeCount > 0) {
throw new IllegalArgumentException("A sinkhole must link a surface course to its terminal inland grotto.");
}
return;
}
if (sinkholeCount != 1 || segments.size() < 3) {
throw new IllegalArgumentException("A surface inland grotto requires one explicit sinkhole link and surface lip.");
}
HydraulicSegment lip = segments.get(segments.size() - 3);
HydraulicSegment sinkhole = segments.get(segments.size() - 2);
HydraulicSegment grotto = segments.getLast();
if (!lip.type().isSurface() && lip.type() != HydrologyFeatureType.RIDGE_BORE
|| sinkhole.type() != HydrologyFeatureType.SINKHOLE
|| sinkhole.drop() <= 0
|| !sinkhole.receivingPool()
|| lip.downstreamHeadY() != sinkhole.upstreamHeadY()
|| sinkhole.downstreamHeadY() != grotto.upstreamHeadY()
|| grotto.upstreamHeadY() != grotto.downstreamHeadY()
|| lip.end().y() != sinkhole.upstreamHeadY()
|| sinkhole.start().y() != sinkhole.upstreamHeadY()
|| sinkhole.end().y() != sinkhole.downstreamHeadY()
|| grotto.start().y() != grotto.upstreamHeadY()
|| lip.end().distanceSquared2D(sinkhole.start()) != 0L
|| sinkhole.end().distanceSquared2D(grotto.start()) != 0L) {
throw new IllegalArgumentException("A surface sinkhole requires a connected lip, graded descent, and receiving grotto.");
}
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.engine.hydrology;
public enum RiverCourseType {
SURFACE,
UNDERGROUND,
DEEP_FLUID
}
@@ -0,0 +1,115 @@
package art.arcane.iris.engine.hydrology;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
public final class RiverFootprint {
private final Map<Long, HydrologyColumnSample> columns;
public RiverFootprint(Map<Long, HydrologyColumnSample> columns) {
if (columns == null) {
throw new IllegalArgumentException("columns must not be null.");
}
TreeMap<Long, HydrologyColumnSample> ordered = new TreeMap<>();
for (Map.Entry<Long, HydrologyColumnSample> entry : columns.entrySet()) {
HydrologyColumnSample sample = entry.getValue();
if (sample == null || entry.getKey() != pack(sample.x(), sample.z())) {
throw new IllegalArgumentException("Footprint keys must match their column coordinates.");
}
ordered.put(entry.getKey(), sample);
}
Long2ObjectLinkedOpenHashMap<HydrologyColumnSample> indexed =
new Long2ObjectLinkedOpenHashMap<>(ordered.size());
for (Map.Entry<Long, HydrologyColumnSample> entry : ordered.entrySet()) {
indexed.put(entry.getKey().longValue(), entry.getValue());
}
columns = Collections.unmodifiableMap(indexed);
this.columns = columns;
}
public static RiverFootprint empty() {
return new RiverFootprint(Map.of());
}
public Optional<HydrologyColumnSample> sample(int x, int z) {
return Optional.ofNullable(columns.get(pack(x, z)));
}
public HydrologyRenderSample renderSample(int x, int z) {
HydrologyColumnSample sample = columns.get(pack(x, z));
return sample == null ? new HydrologyRenderSample(x, z, List.of()) : sample.renderSample();
}
public Map<Long, HydrologyColumnSample> columns() {
return columns;
}
public List<HydrologyColumnSample> columnsIn(int minimumX, int minimumZ, int maximumX, int maximumZ) {
ArrayList<HydrologyColumnSample> selected = new ArrayList<>();
long width = (long) maximumX - minimumX;
long height = (long) maximumZ - minimumZ;
if (width <= 0L || height <= 0L) {
return List.of();
}
long area = width > Long.MAX_VALUE / height ? Long.MAX_VALUE : width * height;
if (area < columns.size()) {
for (long z = minimumZ; z < maximumZ; z++) {
for (long x = minimumX; x < maximumX; x++) {
HydrologyColumnSample sample = columns.get(pack((int) x, (int) z));
if (sample != null) {
selected.add(sample);
}
}
}
return List.copyOf(selected);
}
for (HydrologyColumnSample sample : columns.values()) {
if (sample.x() >= minimumX && sample.x() < maximumX
&& sample.z() >= minimumZ && sample.z() < maximumZ) {
selected.add(sample);
}
}
return List.copyOf(selected);
}
public int size() {
return columns.size();
}
public boolean isEmpty() {
return columns.isEmpty();
}
public static long pack(int x, int z) {
return ((long) x << 32) ^ (z & 0xffffffffL);
}
public static int unpackX(long packed) {
return (int) (packed >> 32);
}
public static int unpackZ(long packed) {
return (int) packed;
}
@Override
public boolean equals(Object other) {
return other instanceof RiverFootprint footprint && columns.equals(footprint.columns);
}
@Override
public int hashCode() {
return columns.hashCode();
}
@Override
public String toString() {
return "RiverFootprint[columns=" + columns.size() + "]";
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.engine.hydrology;
import java.util.Objects;
public record RiverOutlet(
long id,
HydrologyFeatureType type,
long drainageNodeId,
HydrologyPoint landwardPoint,
HydrologyPoint connectionPoint,
int seaLevel,
boolean directOcean
) {
public RiverOutlet {
Objects.requireNonNull(type, "type");
Objects.requireNonNull(landwardPoint, "landwardPoint");
Objects.requireNonNull(connectionPoint, "connectionPoint");
if (type != HydrologyFeatureType.MOUTH
&& type != HydrologyFeatureType.COASTAL_GROTTO
&& type != HydrologyFeatureType.INLAND_GROTTO) {
throw new IllegalArgumentException("Unsupported outlet feature type: " + type);
}
if (directOcean && type == HydrologyFeatureType.INLAND_GROTTO) {
throw new IllegalArgumentException("An inland grotto cannot be a direct ocean outlet.");
}
if (directOcean && connectionPoint.y() != seaLevel) {
throw new IllegalArgumentException("A direct ocean connection must be at sea level.");
}
}
}
@@ -0,0 +1,20 @@
package art.arcane.iris.engine.hydrology.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);
}
@Override
public int hashCode() {
int hash = 0x811C9DC5;
hash = (hash ^ x) * 0x01000193;
hash = (hash ^ y) * 0x01000193;
hash = (hash ^ z) * 0x01000193;
hash ^= hash >>> 16;
hash *= 0x7FEB352D;
hash ^= hash >>> 15;
hash *= 0x846CA68B;
return hash ^ hash >>> 16;
}
}
@@ -0,0 +1,111 @@
package art.arcane.iris.engine.hydrology.cave;
import java.util.Collection;
public final class CavePositionIndex {
private static final int DEFAULT_CAPACITY = 16;
private static final int MAXIMUM_CAPACITY = 1 << 30;
private int[] xCoordinates;
private int[] yCoordinates;
private int[] zCoordinates;
private boolean[] occupied;
private int mask;
private int resizeThreshold;
private int size;
public CavePositionIndex() {
initialize(DEFAULT_CAPACITY);
}
public CavePositionIndex(int expectedSize) {
initialize(capacityFor(expectedSize));
}
public static CavePositionIndex copyOf(Collection<CavePosition> positions) {
CavePositionIndex index = new CavePositionIndex(positions.size());
for (CavePosition position : positions) {
index.add(position.x(), position.y(), position.z());
}
return index;
}
public boolean add(int x, int y, int z) {
int slot = findSlot(x, y, z);
if (occupied[slot]) {
return false;
}
occupied[slot] = true;
xCoordinates[slot] = x;
yCoordinates[slot] = y;
zCoordinates[slot] = z;
size++;
if (size > resizeThreshold) {
resize();
}
return true;
}
public boolean contains(int x, int y, int z) {
return occupied[findSlot(x, y, z)];
}
private void initialize(int capacity) {
xCoordinates = new int[capacity];
yCoordinates = new int[capacity];
zCoordinates = new int[capacity];
occupied = new boolean[capacity];
mask = capacity - 1;
resizeThreshold = capacity - (capacity >>> 2);
size = 0;
}
private int findSlot(int x, int y, int z) {
int slot = hash(x, y, z) & mask;
while (occupied[slot]
&& (xCoordinates[slot] != x || yCoordinates[slot] != y || zCoordinates[slot] != z)) {
slot = slot + 1 & mask;
}
return slot;
}
private void resize() {
if (occupied.length == MAXIMUM_CAPACITY) {
resizeThreshold = Integer.MAX_VALUE;
return;
}
int[] previousXCoordinates = xCoordinates;
int[] previousYCoordinates = yCoordinates;
int[] previousZCoordinates = zCoordinates;
boolean[] previousOccupied = occupied;
initialize(occupied.length << 1);
for (int index = 0; index < previousOccupied.length; index++) {
if (previousOccupied[index]) {
add(previousXCoordinates[index], previousYCoordinates[index], previousZCoordinates[index]);
}
}
}
private static int capacityFor(int expectedSize) {
if (expectedSize < 0) {
throw new IllegalArgumentException("Expected position count must not be negative.");
}
long requiredCapacity = Math.max(DEFAULT_CAPACITY, ((long) expectedSize * 4L + 2L) / 3L);
if (requiredCapacity >= MAXIMUM_CAPACITY) {
return MAXIMUM_CAPACITY;
}
return Integer.highestOneBit((int) requiredCapacity - 1) << 1;
}
private static int hash(int x, int y, int z) {
int hash = 0x811C9DC5;
hash = (hash ^ x) * 0x01000193;
hash = (hash ^ y) * 0x01000193;
hash = (hash ^ z) * 0x01000193;
hash ^= hash >>> 16;
hash *= 0x7FEB352D;
hash ^= hash >>> 15;
hash *= 0x846CA68B;
return hash ^ hash >>> 16;
}
}
@@ -0,0 +1,10 @@
package art.arcane.iris.engine.hydrology.cave;
public enum CaveVoxel {
SOLID,
CAVE_AIR,
COMPATIBLE_FLUID,
LAVA,
INCOMPATIBLE_FLUID,
UNCONDITIONAL
}
@@ -1,4 +1,4 @@
package art.arcane.iris.engine.river.cave;
package art.arcane.iris.engine.hydrology.cave;
import java.util.Objects;
@@ -0,0 +1,26 @@
package art.arcane.iris.engine.hydrology.cave;
public interface CaveVoxelView {
boolean isInWorld(CavePosition position);
CaveVoxel voxelAt(CavePosition position);
boolean isOpenToSurface(CavePosition position);
boolean isAboveTerrainSurface(CavePosition position);
default boolean hasAboveTerrainSurface(int x, int z, int minimumY, int maximumY) {
if (minimumY > maximumY) {
return false;
}
for (int y = minimumY; ; y++) {
CavePosition position = new CavePosition(x, y, z);
if (isInWorld(position) && isAboveTerrainSurface(position)) {
return true;
}
if (y == maximumY) {
return false;
}
}
}
}
@@ -0,0 +1,8 @@
package art.arcane.iris.engine.hydrology.cave;
public enum HydrologyCaveAction {
WET_SOURCE,
FALLING_FLUID,
DRY_AIR,
SEAL_GUARD
}
@@ -0,0 +1,33 @@
package art.arcane.iris.engine.hydrology.cave;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public record HydrologyCaveCandidate(
HydrologyCaveSource source,
String profileKey,
HydrologyCavePlannerSettings settings,
boolean allowDryCaveConnections,
Map<CavePosition, HydrologyCaveAction> actions,
Set<CavePosition> intentionalOpenings
) {
public HydrologyCaveCandidate {
Objects.requireNonNull(source);
if (profileKey == null || profileKey.isBlank()) {
throw new IllegalArgumentException("profileKey must not be blank");
}
profileKey = profileKey.trim();
Objects.requireNonNull(settings);
actions = Collections.unmodifiableMap(new LinkedHashMap<>(Objects.requireNonNull(actions)));
intentionalOpenings = Collections.unmodifiableSet(
new LinkedHashSet<>(Objects.requireNonNull(intentionalOpenings))
);
if (actions.isEmpty()) {
throw new IllegalArgumentException("A cave candidate requires at least one planned action");
}
}
}
@@ -0,0 +1,98 @@
package art.arcane.iris.engine.hydrology.cave;
import art.arcane.volmlib.util.matter.MatterCavern;
import java.util.Objects;
public final class HydrologyCaveCell {
private static final String DEFAULT_FLUID_PROFILE_KEY = "default";
private static final byte LIQUID_FLUID = 1;
private static final byte LIQUID_FORCED_AIR = 3;
private final HydrologyCaveAction action;
private final String fluidProfileKey;
private final String floodedBiomeKey;
private final MatterCavern cavern;
public HydrologyCaveCell(
HydrologyCaveAction action,
String fluidProfileKey,
String floodedBiomeKey
) {
this.action = Objects.requireNonNull(action);
if (fluidProfileKey == null || fluidProfileKey.isBlank()) {
throw new IllegalArgumentException("fluidProfileKey must not be blank");
}
this.fluidProfileKey = fluidProfileKey.trim();
this.floodedBiomeKey = floodedBiomeKey == null ? "" : floodedBiomeKey.trim();
this.cavern = switch (action) {
case WET_SOURCE, FALLING_FLUID -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FLUID);
case DRY_AIR -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FORCED_AIR);
case SEAL_GUARD -> null;
};
}
public static HydrologyCaveCell of(HydrologyCaveAction action) {
return new HydrologyCaveCell(action, DEFAULT_FLUID_PROFILE_KEY, "");
}
public static HydrologyCaveCell of(HydrologyCaveAction action, String fluidProfileKey) {
return new HydrologyCaveCell(action, fluidProfileKey, "");
}
public boolean carves() {
return action != HydrologyCaveAction.SEAL_GUARD;
}
public boolean isWet() {
return action == HydrologyCaveAction.WET_SOURCE || action == HydrologyCaveAction.FALLING_FLUID;
}
public boolean isFalling() {
return action == HydrologyCaveAction.FALLING_FLUID;
}
public boolean protectsPlacement() {
return true;
}
public MatterCavern asCavern() {
return cavern;
}
public HydrologyCaveAction action() {
return action;
}
public String floodedBiomeKey() {
return floodedBiomeKey;
}
public String fluidProfileKey() {
return fluidProfileKey;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof HydrologyCaveCell hydrology)) {
return false;
}
return action == hydrology.action
&& fluidProfileKey.equals(hydrology.fluidProfileKey)
&& floodedBiomeKey.equals(hydrology.floodedBiomeKey);
}
@Override
public int hashCode() {
return (31 * ((31 * action.hashCode()) + fluidProfileKey.hashCode())) + floodedBiomeKey.hashCode();
}
@Override
public String toString() {
return "HydrologyCaveCell[action=" + action + ", fluidProfileKey=" + fluidProfileKey
+ ", floodedBiomeKey=" + floodedBiomeKey + "]";
}
}
@@ -0,0 +1,82 @@
package art.arcane.iris.engine.hydrology.cave;
import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
public final class HydrologyCaveConflictPolicy {
private static final Comparator<HydrologyCaveSource> SOURCE_PRIORITY = Comparator
.comparingInt(HydrologyCaveSource::waterHeadY)
.reversed()
.thenComparingLong(HydrologyCaveSource::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(HydrologyCaveSource::mode);
private HydrologyCaveConflictPolicy() {
}
public static Comparator<HydrologyCaveSource> sourcePriority() {
return SOURCE_PRIORITY;
}
public static int compareSources(HydrologyCaveSource left, HydrologyCaveSource right) {
return SOURCE_PRIORITY.compare(
Objects.requireNonNull(left, "left"),
Objects.requireNonNull(right, "right")
);
}
public static boolean hasIncompatibleOverlap(
String leftProfileKey,
Map<CavePosition, HydrologyCaveAction> leftActions,
String rightProfileKey,
Map<CavePosition, HydrologyCaveAction> rightActions
) {
String normalizedLeftProfileKey = requireProfileKey(leftProfileKey, "leftProfileKey");
String normalizedRightProfileKey = requireProfileKey(rightProfileKey, "rightProfileKey");
Map<CavePosition, HydrologyCaveAction> normalizedLeftActions = Objects.requireNonNull(
leftActions,
"leftActions"
);
Map<CavePosition, HydrologyCaveAction> normalizedRightActions = Objects.requireNonNull(
rightActions,
"rightActions"
);
if (normalizedLeftActions.size() > normalizedRightActions.size()) {
return hasIncompatibleOverlap(
normalizedRightProfileKey,
normalizedRightActions,
normalizedLeftProfileKey,
normalizedLeftActions
);
}
boolean sameProfile = normalizedLeftProfileKey.equals(normalizedRightProfileKey);
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : normalizedLeftActions.entrySet()) {
CavePosition position = Objects.requireNonNull(entry.getKey(), "left action position");
if (!normalizedRightActions.containsKey(position)) {
continue;
}
HydrologyCaveAction leftAction = Objects.requireNonNull(entry.getValue(), "left action");
HydrologyCaveAction rightAction = Objects.requireNonNull(
normalizedRightActions.get(position),
"right action"
);
if (!sameProfile || leftAction != rightAction) {
return true;
}
}
return false;
}
private static String requireProfileKey(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
}
return value.trim();
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.engine.hydrology.cave;
public enum HydrologyCaveFluidPolicy {
REJECT_EXISTING,
ALLOW_COMPATIBLE,
REPLACE_CONTAINED
}
@@ -1,8 +1,8 @@
package art.arcane.iris.engine.river.cave;
package art.arcane.iris.engine.hydrology.cave;
@FunctionalInterface
public interface RiverCaveGrottoShape {
RiverCaveGrottoShape ELLIPSOID = (source, settings, dx, dy, dz) -> {
public interface HydrologyCaveGrottoShape {
HydrologyCaveGrottoShape ELLIPSOID = (source, settings, dx, dy, dz) -> {
double horizontalRadius = settings.grottoHorizontalRadius();
double verticalRadius = settings.grottoVerticalRadius();
double normalized = ((double) dx * dx / (horizontalRadius * horizontalRadius))
@@ -12,8 +12,8 @@ public interface RiverCaveGrottoShape {
};
boolean contains(
RiverCaveSource source,
RiverCavePlannerSettings settings,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
int offsetX,
int offsetY,
int offsetZ
@@ -1,6 +1,6 @@
package art.arcane.iris.engine.river.cave;
package art.arcane.iris.engine.hydrology.cave;
public enum RiverCaveMode {
public enum HydrologyCaveMode {
CLOSED_COMPONENT,
GENERATED_GROTTO,
GROTTO_OR_CLOSED_COMPONENT,
@@ -0,0 +1,692 @@
package art.arcane.iris.engine.hydrology.cave;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.OptionalLong;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
public record HydrologyCavePlan(
HydrologyCaveSource source,
HydrologyCaveRejection rejection,
Map<CavePosition, HydrologyCaveAction> actions,
Map<CavePosition, CaveVoxelPrecondition> baselinePreconditions,
OptionalLong arbitrationWinnerSourceId
) {
private static final int ACTION_MASK = 0x03;
private static final int ACTION_PRESENT = 0x04;
private static final int VOXEL_SHIFT = 3;
private static final int VOXEL_MASK = 0x38;
private static final int OPEN_TO_SURFACE = 0x40;
private static final HydrologyCaveAction[] ACTIONS = HydrologyCaveAction.values();
private static final CaveVoxel[] VOXELS = CaveVoxel.values();
private static final CaveVoxelPrecondition[][] PRECONDITIONS = createPreconditions();
public HydrologyCavePlan {
Objects.requireNonNull(source);
Objects.requireNonNull(rejection);
Map<CavePosition, HydrologyCaveAction> requestedActions = Objects.requireNonNull(actions);
Map<CavePosition, CaveVoxelPrecondition> requestedPreconditions =
Objects.requireNonNull(baselinePreconditions);
Objects.requireNonNull(arbitrationWinnerSourceId);
if (rejection != HydrologyCaveRejection.NONE
&& (!requestedActions.isEmpty() || !requestedPreconditions.isEmpty())) {
throw new IllegalArgumentException("Rejected cave plans cannot contain mutations or preconditions");
}
if (!requestedPreconditions.keySet().containsAll(requestedActions.keySet())) {
throw new IllegalArgumentException("Every cave action requires a baseline precondition");
}
if (rejection != HydrologyCaveRejection.OVERLAPPING_SOURCE && arbitrationWinnerSourceId.isPresent()) {
throw new IllegalArgumentException("Only overlap rejections can name an arbitration winner");
}
if (requestedPreconditions.isEmpty()) {
actions = Map.of();
baselinePreconditions = Map.of();
} else {
LinkedHashMap<CavePosition, Byte> packed = new LinkedHashMap<>(requestedPreconditions.size());
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : requestedActions.entrySet()) {
CaveVoxelPrecondition precondition = requestedPreconditions.get(entry.getKey());
packed.put(entry.getKey(), encode(entry.getValue(), precondition));
}
for (Map.Entry<CavePosition, CaveVoxelPrecondition> entry : requestedPreconditions.entrySet()) {
packed.putIfAbsent(entry.getKey(), encode(null, entry.getValue()));
}
Map<CavePosition, Byte> immutablePacked = Collections.unmodifiableMap(packed);
PositionSpatialIndex spatialIndex = new PositionSpatialIndex(immutablePacked);
actions = new ActionMap(immutablePacked, requestedActions.size(), spatialIndex);
baselinePreconditions = new PreconditionMap(
immutablePacked,
List.copyOf(requestedPreconditions.keySet()),
spatialIndex
);
}
}
public boolean accepted() {
return rejection == HydrologyCaveRejection.NONE;
}
public void forEachAction(BiConsumer<CavePosition, HydrologyCaveAction> consumer) {
actions.forEach(consumer);
}
public void forEachPrecondition(BiConsumer<CavePosition, CaveVoxelPrecondition> consumer) {
baselinePreconditions.forEach(consumer);
}
public boolean allPreconditions(BiPredicate<CavePosition, CaveVoxelPrecondition> predicate) {
Objects.requireNonNull(predicate);
for (CavePosition position : baselinePreconditions.keySet()) {
if (!predicate.test(position, baselinePreconditions.get(position))) {
return false;
}
}
return true;
}
public boolean intersectsActions(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ
) {
if (maximumX <= minimumX || maximumZ <= minimumZ || !(actions instanceof ActionMap actionMap)) {
return false;
}
return actionMap.intersects(minimumX, minimumZ, maximumX, maximumZ);
}
public void forEachActionIn(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ,
BiConsumer<CavePosition, HydrologyCaveAction> consumer
) {
Objects.requireNonNull(consumer);
if (maximumX <= minimumX || maximumZ <= minimumZ || !(actions instanceof ActionMap actionMap)) {
return;
}
actionMap.forEachIn(minimumX, minimumZ, maximumX, maximumZ, consumer);
}
public boolean allPreconditionsIn(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ,
BiPredicate<CavePosition, CaveVoxelPrecondition> predicate
) {
Objects.requireNonNull(predicate);
if (maximumX <= minimumX || maximumZ <= minimumZ
|| !(baselinePreconditions instanceof PreconditionMap preconditionMap)) {
return true;
}
return preconditionMap.allIn(minimumX, minimumZ, maximumX, maximumZ, predicate);
}
private static byte encode(
HydrologyCaveAction action,
CaveVoxelPrecondition precondition
) {
Objects.requireNonNull(precondition);
int packed = precondition.voxel().ordinal() << VOXEL_SHIFT;
if (precondition.openToSurface()) {
packed |= OPEN_TO_SURFACE;
}
if (action != null) {
packed |= ACTION_PRESENT | action.ordinal();
}
return (byte) packed;
}
private static boolean hasAction(byte packed) {
return (packed & ACTION_PRESENT) != 0;
}
private static HydrologyCaveAction decodeAction(byte packed) {
return ACTIONS[packed & ACTION_MASK];
}
private static CaveVoxelPrecondition decodePrecondition(byte packed) {
int unsigned = Byte.toUnsignedInt(packed);
int voxel = unsigned & VOXEL_MASK;
int open = unsigned & OPEN_TO_SURFACE;
return PRECONDITIONS[voxel >>> VOXEL_SHIFT][open == 0 ? 0 : 1];
}
private static CaveVoxelPrecondition[][] createPreconditions() {
if (ACTIONS.length > ACTION_MASK + 1) {
throw new IllegalStateException("Hydrology cave action packing capacity exceeded");
}
if (VOXELS.length > (VOXEL_MASK >>> VOXEL_SHIFT) + 1) {
throw new IllegalStateException("Hydrology cave voxel packing capacity exceeded");
}
CaveVoxelPrecondition[][] preconditions = new CaveVoxelPrecondition[VOXELS.length][2];
for (CaveVoxel voxel : VOXELS) {
preconditions[voxel.ordinal()][0] = new CaveVoxelPrecondition(voxel, false);
preconditions[voxel.ordinal()][1] = new CaveVoxelPrecondition(voxel, true);
}
return preconditions;
}
private static final class ActionMap extends AbstractMap<CavePosition, HydrologyCaveAction> {
private final Map<CavePosition, Byte> packed;
private final int size;
private final PositionSpatialIndex spatialIndex;
private ActionMap(Map<CavePosition, Byte> packed, int size, PositionSpatialIndex spatialIndex) {
this.packed = packed;
this.size = size;
this.spatialIndex = spatialIndex;
}
@Override
public HydrologyCaveAction get(Object key) {
Byte value = packed.get(key);
return value == null || !hasAction(value) ? null : decodeAction(value);
}
@Override
public boolean containsKey(Object key) {
Byte value = packed.get(key);
return value != null && hasAction(value);
}
@Override
public int size() {
return size;
}
@Override
public Set<CavePosition> keySet() {
return new ActionKeySet(packed, size);
}
@Override
public Set<Entry<CavePosition, HydrologyCaveAction>> entrySet() {
return new ActionEntrySet(packed, size);
}
@Override
public Collection<HydrologyCaveAction> values() {
return new ActionValues(packed, size);
}
@Override
public void forEach(BiConsumer<? super CavePosition, ? super HydrologyCaveAction> consumer) {
Objects.requireNonNull(consumer);
for (Map.Entry<CavePosition, Byte> entry : packed.entrySet()) {
if (hasAction(entry.getValue())) {
consumer.accept(entry.getKey(), decodeAction(entry.getValue()));
}
}
}
private boolean intersects(int minimumX, int minimumZ, int maximumX, int maximumZ) {
return spatialIndex.anyIn(minimumX, minimumZ, maximumX, maximumZ,
(CavePosition position) -> hasAction(packed.get(position)));
}
private void forEachIn(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ,
BiConsumer<CavePosition, HydrologyCaveAction> consumer
) {
spatialIndex.forEachIn(minimumX, minimumZ, maximumX, maximumZ, (CavePosition position) -> {
byte value = packed.get(position);
if (hasAction(value)) {
consumer.accept(position, decodeAction(value));
}
});
}
}
private static final class PreconditionMap extends AbstractMap<CavePosition, CaveVoxelPrecondition> {
private final Map<CavePosition, Byte> packed;
private final List<CavePosition> order;
private final PositionSpatialIndex spatialIndex;
private PreconditionMap(
Map<CavePosition, Byte> packed,
List<CavePosition> order,
PositionSpatialIndex spatialIndex
) {
this.packed = packed;
this.order = order;
this.spatialIndex = spatialIndex;
}
@Override
public CaveVoxelPrecondition get(Object key) {
Byte value = packed.get(key);
return value == null ? null : decodePrecondition(value);
}
@Override
public boolean containsKey(Object key) {
return packed.containsKey(key);
}
@Override
public int size() {
return order.size();
}
@Override
public Set<CavePosition> keySet() {
return new PreconditionKeySet(packed, order);
}
@Override
public Set<Entry<CavePosition, CaveVoxelPrecondition>> entrySet() {
return new PreconditionEntrySet(packed, order);
}
@Override
public Collection<CaveVoxelPrecondition> values() {
return new PreconditionValues(packed, order);
}
@Override
public void forEach(BiConsumer<? super CavePosition, ? super CaveVoxelPrecondition> consumer) {
Objects.requireNonNull(consumer);
for (CavePosition position : order) {
consumer.accept(position, decodePrecondition(packed.get(position)));
}
}
private boolean allIn(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ,
BiPredicate<CavePosition, CaveVoxelPrecondition> predicate
) {
return spatialIndex.allIn(minimumX, minimumZ, maximumX, maximumZ,
(CavePosition position) -> predicate.test(position, decodePrecondition(packed.get(position))));
}
}
private static final class PositionSpatialIndex {
private static final int CHUNK_SHIFT = 4;
private final Map<Long, List<CavePosition>> positionsByChunk;
private PositionSpatialIndex(Map<CavePosition, Byte> packed) {
HashMap<Long, ArrayList<CavePosition>> mutable = new HashMap<>();
for (CavePosition position : packed.keySet()) {
long chunkKey = packChunk(position.x() >> CHUNK_SHIFT, position.z() >> CHUNK_SHIFT);
mutable.computeIfAbsent(chunkKey, ignored -> new ArrayList<>()).add(position);
}
HashMap<Long, List<CavePosition>> immutable = HashMap.newHashMap(mutable.size());
for (Map.Entry<Long, ArrayList<CavePosition>> entry : mutable.entrySet()) {
immutable.put(entry.getKey(), List.copyOf(entry.getValue()));
}
positionsByChunk = Map.copyOf(immutable);
}
private boolean anyIn(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ,
java.util.function.Predicate<CavePosition> predicate
) {
int minimumChunkX = minimumX >> CHUNK_SHIFT;
int maximumChunkX = maximumX - 1 >> CHUNK_SHIFT;
int minimumChunkZ = minimumZ >> CHUNK_SHIFT;
int maximumChunkZ = maximumZ - 1 >> CHUNK_SHIFT;
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
List<CavePosition> positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
if (positions == null) {
continue;
}
for (CavePosition position : positions) {
if (inside(position, minimumX, minimumZ, maximumX, maximumZ)
&& predicate.test(position)) {
return true;
}
}
}
}
return false;
}
private boolean allIn(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ,
java.util.function.Predicate<CavePosition> predicate
) {
int minimumChunkX = minimumX >> CHUNK_SHIFT;
int maximumChunkX = maximumX - 1 >> CHUNK_SHIFT;
int minimumChunkZ = minimumZ >> CHUNK_SHIFT;
int maximumChunkZ = maximumZ - 1 >> CHUNK_SHIFT;
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
List<CavePosition> positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
if (positions == null) {
continue;
}
for (CavePosition position : positions) {
if (inside(position, minimumX, minimumZ, maximumX, maximumZ)
&& !predicate.test(position)) {
return false;
}
}
}
}
return true;
}
private void forEachIn(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ,
java.util.function.Consumer<CavePosition> consumer
) {
int minimumChunkX = minimumX >> CHUNK_SHIFT;
int maximumChunkX = maximumX - 1 >> CHUNK_SHIFT;
int minimumChunkZ = minimumZ >> CHUNK_SHIFT;
int maximumChunkZ = maximumZ - 1 >> CHUNK_SHIFT;
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
List<CavePosition> positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
if (positions == null) {
continue;
}
for (CavePosition position : positions) {
if (inside(position, minimumX, minimumZ, maximumX, maximumZ)) {
consumer.accept(position);
}
}
}
}
}
private static boolean inside(
CavePosition position,
int minimumX,
int minimumZ,
int maximumX,
int maximumZ
) {
return position.x() >= minimumX && position.x() < maximumX
&& position.z() >= minimumZ && position.z() < maximumZ;
}
private static long packChunk(int chunkX, int chunkZ) {
return (long) chunkX << 32 ^ chunkZ & 0xffffffffL;
}
}
private static final class ActionKeySet extends AbstractSet<CavePosition> {
private final Map<CavePosition, Byte> packed;
private final int size;
private ActionKeySet(Map<CavePosition, Byte> packed, int size) {
this.packed = packed;
this.size = size;
}
@Override
public Iterator<CavePosition> iterator() {
return new ActionKeyIterator(packed.entrySet().iterator());
}
@Override
public int size() {
return size;
}
@Override
public boolean contains(Object value) {
Byte packedValue = packed.get(value);
return packedValue != null && hasAction(packedValue);
}
}
private static final class ActionEntrySet extends AbstractSet<Entry<CavePosition, HydrologyCaveAction>> {
private final Map<CavePosition, Byte> packed;
private final int size;
private ActionEntrySet(Map<CavePosition, Byte> packed, int size) {
this.packed = packed;
this.size = size;
}
@Override
public Iterator<Entry<CavePosition, HydrologyCaveAction>> iterator() {
return new ActionEntryIterator(packed.entrySet().iterator());
}
@Override
public int size() {
return size;
}
}
private static final class PreconditionEntrySet
extends AbstractSet<Entry<CavePosition, CaveVoxelPrecondition>> {
private final Map<CavePosition, Byte> packed;
private final List<CavePosition> order;
private PreconditionEntrySet(Map<CavePosition, Byte> packed, List<CavePosition> order) {
this.packed = packed;
this.order = order;
}
@Override
public Iterator<Entry<CavePosition, CaveVoxelPrecondition>> iterator() {
return new PreconditionEntryIterator(packed, order.iterator());
}
@Override
public int size() {
return order.size();
}
}
private static final class PreconditionKeySet extends AbstractSet<CavePosition> {
private final Map<CavePosition, Byte> packed;
private final List<CavePosition> order;
private PreconditionKeySet(Map<CavePosition, Byte> packed, List<CavePosition> order) {
this.packed = packed;
this.order = order;
}
@Override
public Iterator<CavePosition> iterator() {
return order.iterator();
}
@Override
public int size() {
return order.size();
}
@Override
public boolean contains(Object value) {
return packed.containsKey(value);
}
}
private static final class ActionValues extends AbstractCollection<HydrologyCaveAction> {
private final Map<CavePosition, Byte> packed;
private final int size;
private ActionValues(Map<CavePosition, Byte> packed, int size) {
this.packed = packed;
this.size = size;
}
@Override
public Iterator<HydrologyCaveAction> iterator() {
return new ActionValueIterator(packed.entrySet().iterator());
}
@Override
public int size() {
return size;
}
}
private static final class PreconditionValues extends AbstractCollection<CaveVoxelPrecondition> {
private final Map<CavePosition, Byte> packed;
private final List<CavePosition> order;
private PreconditionValues(Map<CavePosition, Byte> packed, List<CavePosition> order) {
this.packed = packed;
this.order = order;
}
@Override
public Iterator<CaveVoxelPrecondition> iterator() {
return new PreconditionValueIterator(packed, order.iterator());
}
@Override
public int size() {
return order.size();
}
}
private abstract static class FilteredActionIterator<T> implements Iterator<T> {
private final Iterator<Map.Entry<CavePosition, Byte>> entries;
private Map.Entry<CavePosition, Byte> next;
private FilteredActionIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
this.entries = entries;
}
@Override
public boolean hasNext() {
advance();
return next != null;
}
@Override
public T next() {
advance();
if (next == null) {
throw new NoSuchElementException();
}
Map.Entry<CavePosition, Byte> selected = next;
next = null;
return map(selected);
}
protected abstract T map(Map.Entry<CavePosition, Byte> entry);
private void advance() {
while (next == null && entries.hasNext()) {
Map.Entry<CavePosition, Byte> candidate = entries.next();
if (hasAction(candidate.getValue())) {
next = candidate;
}
}
}
}
private static final class ActionKeyIterator extends FilteredActionIterator<CavePosition> {
private ActionKeyIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
super(entries);
}
@Override
protected CavePosition map(Map.Entry<CavePosition, Byte> entry) {
return entry.getKey();
}
}
private static final class ActionEntryIterator
extends FilteredActionIterator<Entry<CavePosition, HydrologyCaveAction>> {
private ActionEntryIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
super(entries);
}
@Override
protected Entry<CavePosition, HydrologyCaveAction> map(Map.Entry<CavePosition, Byte> entry) {
return Map.entry(entry.getKey(), decodeAction(entry.getValue()));
}
}
private static final class ActionValueIterator extends FilteredActionIterator<HydrologyCaveAction> {
private ActionValueIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
super(entries);
}
@Override
protected HydrologyCaveAction map(Map.Entry<CavePosition, Byte> entry) {
return decodeAction(entry.getValue());
}
}
private static final class PreconditionEntryIterator
implements Iterator<Entry<CavePosition, CaveVoxelPrecondition>> {
private final Map<CavePosition, Byte> packed;
private final Iterator<CavePosition> positions;
private PreconditionEntryIterator(
Map<CavePosition, Byte> packed,
Iterator<CavePosition> positions
) {
this.packed = packed;
this.positions = positions;
}
@Override
public boolean hasNext() {
return positions.hasNext();
}
@Override
public Entry<CavePosition, CaveVoxelPrecondition> next() {
CavePosition position = positions.next();
return Map.entry(position, decodePrecondition(packed.get(position)));
}
}
private static final class PreconditionValueIterator implements Iterator<CaveVoxelPrecondition> {
private final Map<CavePosition, Byte> packed;
private final Iterator<CavePosition> positions;
private PreconditionValueIterator(
Map<CavePosition, Byte> packed,
Iterator<CavePosition> positions
) {
this.packed = packed;
this.positions = positions;
}
@Override
public boolean hasNext() {
return positions.hasNext();
}
@Override
public CaveVoxelPrecondition next() {
return decodePrecondition(packed.get(positions.next()));
}
}
}
@@ -1,8 +1,8 @@
package art.arcane.iris.engine.river.cave;
package art.arcane.iris.engine.hydrology.cave;
import java.util.Objects;
public record RiverCavePlannerSettings(
public record HydrologyCavePlannerSettings(
int maxHorizontalRadius,
int maxDepth,
int maxFloodVolume,
@@ -11,12 +11,14 @@ public record RiverCavePlannerSettings(
int grottoHorizontalRadius,
int grottoVerticalRadius,
int dryHeadroom,
RiverCaveFluidPolicy existingFluidPolicy,
RiverCaveGrottoShape grottoShape,
HydrologyCaveFluidPolicy existingFluidPolicy,
HydrologyCaveGrottoShape grottoShape,
int maxClosedComponentHorizontalRadius,
int maxClosedComponentDepth
) {
public RiverCavePlannerSettings(
public static final int MAXIMUM_PLANNED_MUTATIONS = 262_144;
public HydrologyCavePlannerSettings(
int maxHorizontalRadius,
int maxDepth,
int maxFloodVolume,
@@ -25,8 +27,8 @@ public record RiverCavePlannerSettings(
int grottoHorizontalRadius,
int grottoVerticalRadius,
int dryHeadroom,
RiverCaveFluidPolicy existingFluidPolicy,
RiverCaveGrottoShape grottoShape
HydrologyCaveFluidPolicy existingFluidPolicy,
HydrologyCaveGrottoShape grottoShape
) {
this(
maxHorizontalRadius,
@@ -44,14 +46,14 @@ public record RiverCavePlannerSettings(
);
}
public RiverCavePlannerSettings(
public HydrologyCavePlannerSettings(
int maxHorizontalRadius,
int maxDepth,
int maxFloodVolume,
int maxThroatLength,
int grottoHorizontalRadius,
int grottoVerticalRadius,
RiverCaveFluidPolicy existingFluidPolicy
HydrologyCaveFluidPolicy existingFluidPolicy
) {
this(
maxHorizontalRadius,
@@ -63,11 +65,11 @@ public record RiverCavePlannerSettings(
grottoVerticalRadius,
0,
existingFluidPolicy,
RiverCaveGrottoShape.ELLIPSOID
HydrologyCaveGrottoShape.ELLIPSOID
);
}
public RiverCavePlannerSettings {
public HydrologyCavePlannerSettings {
Objects.requireNonNull(existingFluidPolicy);
Objects.requireNonNull(grottoShape);
if (maxHorizontalRadius < 1) {
@@ -0,0 +1,21 @@
package art.arcane.iris.engine.hydrology.cave;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public record HydrologyCavePlanningResult(
List<HydrologyCavePlan> plans,
Map<CavePosition, HydrologyCaveAction> actions,
Map<CavePosition, CaveVoxelPrecondition> baselinePreconditions
) {
public HydrologyCavePlanningResult {
plans = List.copyOf(Objects.requireNonNull(plans));
actions = Collections.unmodifiableMap(new LinkedHashMap<>(Objects.requireNonNull(actions)));
baselinePreconditions = Collections.unmodifiableMap(
new LinkedHashMap<>(Objects.requireNonNull(baselinePreconditions))
);
}
}
@@ -1,6 +1,6 @@
package art.arcane.iris.engine.river.cave;
package art.arcane.iris.engine.hydrology.cave;
public enum RiverCaveRejection {
public enum HydrologyCaveRejection {
NONE,
INVALID_SOURCE,
NO_CAVE_TARGET,
@@ -1,15 +1,15 @@
package art.arcane.iris.engine.river.cave;
package art.arcane.iris.engine.hydrology.cave;
import java.util.Objects;
public record RiverCaveSource(
public record HydrologyCaveSource(
long sourceId,
CavePosition entry,
CavePosition target,
int waterHeadY,
RiverCaveMode mode
HydrologyCaveMode mode
) {
public RiverCaveSource {
public HydrologyCaveSource {
Objects.requireNonNull(entry);
Objects.requireNonNull(target);
Objects.requireNonNull(mode);
@@ -1,15 +1,15 @@
package art.arcane.iris.engine.river.cave;
package art.arcane.iris.engine.hydrology.cave;
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;
public final class RiverCaveHydrologyStorage {
private RiverCaveHydrologyStorage() {
public final class HydrologyCaveStorage {
private HydrologyCaveStorage() {
}
public static RiverCaveHydrology getIfPresent(Mantle<Matter> mantle, int x, int y, int z) {
public static HydrologyCaveCell getIfPresent(Mantle<Matter> mantle, int x, int y, int z) {
if (y < 0 || y >= mantle.getWorldHeight()) {
return null;
}
@@ -23,7 +23,7 @@ public final class RiverCaveHydrologyStorage {
return getIfPresent(chunk, x, y, z);
}
public static RiverCaveHydrology getIfPresent(MantleChunk<Matter> chunk, int x, int y, int z) {
public static HydrologyCaveCell getIfPresent(MantleChunk<Matter> chunk, int x, int y, int z) {
if (chunk == null || y < 0) {
return null;
}
@@ -32,10 +32,10 @@ public final class RiverCaveHydrologyStorage {
return null;
}
Matter matter = chunk.get(section);
if (matter == null || !matter.hasSlice(RiverCaveHydrology.class)) {
if (matter == null || !matter.hasSlice(HydrologyCaveCell.class)) {
return null;
}
return matter.<RiverCaveHydrology>getSlice(RiverCaveHydrology.class)
return matter.<HydrologyCaveCell>getSlice(HydrologyCaveCell.class)
.get(x & 15, y & 15, z & 15);
}
}
@@ -0,0 +1,58 @@
package art.arcane.iris.engine.hydrology.policy;
import art.arcane.iris.engine.object.IrisRiverPlacementMode;
import art.arcane.iris.engine.object.IrisRiverRoutingMode;
import java.util.List;
import java.util.Objects;
public record EffectiveRiverPolicy(
IrisRiverPlacementMode placement,
IrisRiverRoutingMode routing,
boolean outletAdmission,
List<String> profiles,
List<String> surfaceBiomes,
List<String> mouthBiomes,
List<String> shoreBiomes,
List<String> dryBiomes,
List<String> floodedCaveBiomes,
double widthMultiplier,
double depthMultiplier,
double incisionMultiplier,
double routingMultiplier
) {
public EffectiveRiverPolicy {
placement = Objects.requireNonNull(placement);
routing = Objects.requireNonNull(routing);
profiles = List.copyOf(profiles);
surfaceBiomes = List.copyOf(surfaceBiomes);
mouthBiomes = List.copyOf(mouthBiomes);
shoreBiomes = List.copyOf(shoreBiomes);
dryBiomes = List.copyOf(dryBiomes);
floodedCaveBiomes = List.copyOf(floodedCaveBiomes);
}
public boolean allowsSources() {
return switch (placement) {
case DISABLED, TRANSIT_ONLY -> false;
case NATURAL, PREFERRED_HEADWATER, REQUIRED_HEADWATER -> true;
};
}
public boolean allowsTransit() {
return placement != IrisRiverPlacementMode.DISABLED;
}
public boolean prefersHeadwaters() {
return placement == IrisRiverPlacementMode.PREFERRED_HEADWATER
|| placement == IrisRiverPlacementMode.REQUIRED_HEADWATER;
}
public boolean requiresHeadwaters() {
return placement == IrisRiverPlacementMode.REQUIRED_HEADWATER;
}
public boolean allowsRouting() {
return routing != IrisRiverRoutingMode.BLOCK;
}
}
@@ -0,0 +1,114 @@
package art.arcane.iris.engine.hydrology.policy;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverPlacementMode;
import art.arcane.iris.engine.object.IrisRiverPolicy;
import art.arcane.iris.engine.object.IrisRiverRoutingMode;
import java.util.List;
public final class RiverPolicyResolver {
private RiverPolicyResolver() {
}
public static EffectiveRiverPolicy resolve(IrisDimension dimension, IrisRegion region, IrisBiome biome) {
return resolve(
dimension == null ? null : dimension.getRiverPolicy(),
region == null ? null : region.getRiverPolicy(),
biome == null ? null : biome.getRiverPolicy()
);
}
public static EffectiveRiverPolicy resolve(
IrisRiverPolicy dimensionPolicy,
IrisRiverPolicy regionPolicy,
IrisRiverPolicy biomePolicy
) {
State state = new State();
state.apply(dimensionPolicy);
state.apply(regionPolicy);
state.apply(biomePolicy);
return state.build();
}
private static final class State {
private IrisRiverPlacementMode placement = IrisRiverPlacementMode.NATURAL;
private IrisRiverRoutingMode routing = IrisRiverRoutingMode.ALLOW;
private boolean outletAdmission = true;
private List<String> profiles = List.of();
private List<String> surfaceBiomes = List.of();
private List<String> mouthBiomes = List.of();
private List<String> shoreBiomes = List.of();
private List<String> dryBiomes = List.of();
private List<String> floodedCaveBiomes = List.of();
private double widthMultiplier = 1D;
private double depthMultiplier = 1D;
private double incisionMultiplier = 1D;
private double routingMultiplier = 1D;
private void apply(IrisRiverPolicy policy) {
if (policy == null) {
return;
}
if (policy.getPlacement() != null) {
placement = policy.getPlacement();
}
if (policy.getRouting() != null) {
routing = policy.getRouting();
}
if (policy.getOutletAdmission() != null) {
outletAdmission = policy.getOutletAdmission();
}
if (policy.getProfiles() != null) {
profiles = List.copyOf(policy.getProfiles());
}
if (policy.getSurfaceBiomes() != null) {
surfaceBiomes = List.copyOf(policy.getSurfaceBiomes());
}
if (policy.getMouthBiomes() != null) {
mouthBiomes = List.copyOf(policy.getMouthBiomes());
}
if (policy.getShoreBiomes() != null) {
shoreBiomes = List.copyOf(policy.getShoreBiomes());
}
if (policy.getDryBiomes() != null) {
dryBiomes = List.copyOf(policy.getDryBiomes());
}
if (policy.getFloodedCaveBiomes() != null) {
floodedCaveBiomes = List.copyOf(policy.getFloodedCaveBiomes());
}
if (policy.getWidthMultiplier() != null) {
widthMultiplier = policy.getWidthMultiplier();
}
if (policy.getDepthMultiplier() != null) {
depthMultiplier = policy.getDepthMultiplier();
}
if (policy.getIncisionMultiplier() != null) {
incisionMultiplier = policy.getIncisionMultiplier();
}
if (policy.getRoutingMultiplier() != null) {
routingMultiplier = policy.getRoutingMultiplier();
}
}
private EffectiveRiverPolicy build() {
return new EffectiveRiverPolicy(
placement,
routing,
outletAdmission,
profiles,
surfaceBiomes,
mouthBiomes,
shoreBiomes,
dryBiomes,
floodedCaveBiomes,
widthMultiplier,
depthMultiplier,
incisionMultiplier,
routingMultiplier
);
}
}
}
@@ -0,0 +1,47 @@
package art.arcane.iris.engine.hydrology.runtime;
final class HydrologyFeatureSearchBounds {
private HydrologyFeatureSearchBounds() {
}
static int maximumDistance(
int x,
int z,
int requestedDistance,
int tileSize,
int publicationRadius,
int maximumTiles
) {
if (requestedDistance < 0 || tileSize <= 0 || publicationRadius < 0 || maximumTiles < 1) {
throw new IllegalArgumentException("Hydrology feature search bounds are invalid.");
}
if (tileCount(x, z, 0, tileSize, publicationRadius) > maximumTiles) {
throw new IllegalArgumentException("Hydrology publication radius exceeds the bounded tile limit.");
}
int minimum = 0;
int maximum = requestedDistance;
int accepted = 0;
while (minimum <= maximum) {
int candidate = minimum + (maximum - minimum) / 2;
if (tileCount(x, z, candidate, tileSize, publicationRadius) <= maximumTiles) {
accepted = candidate;
minimum = candidate + 1;
} else {
maximum = candidate - 1;
}
}
return accepted;
}
static long tileCount(int x, int z, int distance, int tileSize, int publicationRadius) {
int minimumTileX = tileCoordinate((long) x - distance - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) x + distance + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) z - distance - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) z + distance + publicationRadius, tileSize);
return (long) (maximumTileX - minimumTileX + 1) * (maximumTileZ - minimumTileZ + 1);
}
private static int tileCoordinate(long blockCoordinate, int tileSize) {
return Math.toIntExact(Math.floorDiv(blockCoordinate, tileSize));
}
}
@@ -0,0 +1,6 @@
package art.arcane.iris.engine.hydrology.runtime;
@FunctionalInterface
public interface IrisHydrologyNaturalHeightProvider {
double sample(int blockX, int blockZ);
}
@@ -0,0 +1,6 @@
package art.arcane.iris.engine.hydrology.runtime;
@FunctionalInterface
public interface IrisHydrologyNaturalOceanClassifier {
boolean isOcean(int blockX, int blockZ);
}
@@ -0,0 +1,12 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
public record IrisHydrologyNaturalSample(
double naturalHeight,
boolean ocean,
IrisBiome biome,
IrisRegion region
) {
}
@@ -0,0 +1,6 @@
package art.arcane.iris.engine.hydrology.runtime;
@FunctionalInterface
public interface IrisHydrologyNaturalSampleProvider {
IrisHydrologyNaturalSample sample(int blockX, int blockZ, double naturalHeight);
}
@@ -0,0 +1,379 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.hydrology.HydrologyNaturalTerrainSampler;
import art.arcane.iris.engine.hydrology.HydrologyRoutingTerrainSampler;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.util.common.parallel.MultiBurst;
import it.unimi.dsi.fastutil.longs.Long2DoubleLinkedOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrainSampler, AutoCloseable {
private static final int MINIMUM_PARALLEL_BASIS_SAMPLES = 64;
private static final int COORDINATE_MIX = 0x9E3779B9;
private final BasisProvider basisProvider;
private final IrisHydrologyNaturalHeightProvider heightProvider;
private final IrisHydrologyNaturalOceanClassifier oceanClassifier;
private final int maximumEntries;
private final SamplingOptions samplingOptions;
private final Object lock;
private final Long2ObjectLinkedOpenHashMap<TerrainBasis> bases;
private final Long2DoubleLinkedOpenHashMap naturalHeights;
private final Long2ObjectLinkedOpenHashMap<NaturalClassification> oceanClassifications;
IrisHydrologyRoutingTerrainSampler(Sources sources, SamplingOptions samplingOptions) {
Objects.requireNonNull(sources, "sources");
this.samplingOptions = Objects.requireNonNull(samplingOptions, "samplingOptions");
this.basisProvider = Objects.requireNonNull(sources.basisProvider(), "basisProvider");
this.heightProvider = Objects.requireNonNull(sources.heightProvider(), "heightProvider");
this.oceanClassifier = Objects.requireNonNull(sources.oceanClassifier(), "oceanClassifier");
int maximumEntries = samplingOptions.maximumEntries();
this.maximumEntries = maximumEntries;
this.lock = new Object();
this.bases = new Long2ObjectLinkedOpenHashMap<>(maximumEntries);
this.naturalHeights = new Long2DoubleLinkedOpenHashMap(maximumEntries);
this.naturalHeights.defaultReturnValue(Double.NaN);
this.oceanClassifications = new Long2ObjectLinkedOpenHashMap<>(maximumEntries);
}
@Override
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
int minimumX = request.minimumX();
int minimumZ = request.minimumZ();
int width = request.width();
int spacing = request.spacing();
int planeWidth = Math.addExact(width, 1);
TerrainBasis[] plane = new TerrainBasis[Math.multiplyExact(planeWidth, planeWidth)];
fillBasisPlane(plane, planeWidth, minimumX, minimumZ, spacing);
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[Math.multiplyExact(width, width)];
double slopeScale = 3D / spacing;
int sampleIndex = 0;
for (int gridZ = 0; gridZ < width; gridZ++) {
int planeIndex = gridZ * planeWidth;
for (int gridX = 0; gridX < width; gridX++) {
TerrainBasis center = plane[planeIndex];
double slope = scaledRoutingSlope(
center.naturalHeight(),
plane[planeIndex + 1].naturalHeight(),
plane[planeIndex + planeWidth].naturalHeight(),
slopeScale
);
samples[sampleIndex++] = center.terrain().withSlope(slope);
planeIndex++;
}
}
return samples;
}
@Override
public NaturalClassification classifyNatural(int blockX, int blockZ) {
long packed = pack(blockX, blockZ);
synchronized (lock) {
TerrainBasis basis = bases.getAndMoveToLast(packed);
if (basis != null) {
return basis.terrain().ocean() ? NaturalClassification.OCEAN : NaturalClassification.LAND;
}
NaturalClassification cached = oceanClassifications.getAndMoveToLast(packed);
if (cached != null) {
return cached;
}
}
NaturalClassification sampled = oceanClassifier.isOcean(blockX, blockZ)
? NaturalClassification.OCEAN
: NaturalClassification.LAND;
synchronized (lock) {
TerrainBasis basis = bases.getAndMoveToLast(packed);
if (basis != null) {
return basis.terrain().ocean() ? NaturalClassification.OCEAN : NaturalClassification.LAND;
}
NaturalClassification existing = oceanClassifications.getAndMoveToLast(packed);
if (existing != null) {
return existing;
}
oceanClassifications.putAndMoveToLast(packed, sampled);
evictOldest(oceanClassifications);
}
return sampled;
}
@Override
public HydrologyTerrainSample sampleBasis(int blockX, int blockZ) {
return basis(blockX, blockZ).terrain();
}
TerrainBasis basis(int blockX, int blockZ) {
long packed = pack(blockX, blockZ);
synchronized (lock) {
TerrainBasis cached = bases.getAndMoveToLast(packed);
if (cached != null) {
return cached;
}
}
double naturalHeight = naturalHeight(packed, blockX, blockZ);
TerrainBasis sampled = basisProvider.sample(blockX, blockZ, naturalHeight);
if (sampled == null) {
throw new NullPointerException(
"Hydrology terrain basis provider returned null at " + blockX + "," + blockZ
);
}
synchronized (lock) {
TerrainBasis existing = bases.getAndMoveToLast(packed);
if (existing != null) {
return existing;
}
bases.putAndMoveToLast(packed, sampled);
oceanClassifications.remove(packed);
evictOldest(bases);
}
return sampled;
}
int basisCacheSize() {
synchronized (lock) {
return bases.size();
}
}
int naturalHeightCacheSize() {
synchronized (lock) {
return naturalHeights.size();
}
}
int oceanClassificationCacheSize() {
synchronized (lock) {
return oceanClassifications.size();
}
}
@Override
public void close() {
synchronized (lock) {
bases.clear();
naturalHeights.clear();
oceanClassifications.clear();
}
}
static double localSlope(double naturalHeight, double easternHeight, double southernHeight) {
double deltaX = easternHeight - naturalHeight;
double deltaZ = southernHeight - naturalHeight;
return Math.sqrt(deltaX * deltaX + deltaZ * deltaZ);
}
double localSlope(int blockX, int blockZ, double naturalHeight) {
double easternHeight = naturalHeight(blockX + 3, blockZ);
double southernHeight = naturalHeight(blockX, blockZ + 3);
return localSlope(naturalHeight, easternHeight, southernHeight);
}
static double routingSlope(
double naturalHeight,
double easternHeight,
double southernHeight,
int spacing
) {
if (spacing < 1) {
throw new IllegalArgumentException("spacing must be positive");
}
double scale = 3D / spacing;
return scaledRoutingSlope(naturalHeight, easternHeight, southernHeight, scale);
}
private static double scaledRoutingSlope(
double naturalHeight,
double easternHeight,
double southernHeight,
double scale
) {
double deltaX = (easternHeight - naturalHeight) * scale;
double deltaZ = (southernHeight - naturalHeight) * scale;
return StrictMath.sqrt(deltaX * deltaX + deltaZ * deltaZ);
}
private void fillBasisPlane(
TerrainBasis[] plane,
int planeWidth,
int minimumX,
int minimumZ,
int spacing
) {
int workerCount = gridWorkerCount(planeWidth, plane.length);
if (workerCount == 1) {
fillBasisRows(plane, planeWidth, minimumX, minimumZ, spacing, 0, planeWidth);
return;
}
int rowsPerWorker = Math.ceilDiv(planeWidth, workerCount);
ArrayList<CompletableFuture<Void>> futures = new ArrayList<>(workerCount);
for (int workerIndex = 0; workerIndex < workerCount; workerIndex++) {
int minimumRow = workerIndex * rowsPerWorker;
if (minimumRow >= planeWidth) {
break;
}
int maximumRow = Math.min(planeWidth, minimumRow + rowsPerWorker);
futures.add(CompletableFuture.runAsync(
() -> fillBasisRows(
plane,
planeWidth,
minimumX,
minimumZ,
spacing,
minimumRow,
maximumRow
),
samplingOptions.executor()
));
}
await(futures);
}
private void fillBasisRows(
TerrainBasis[] plane,
int planeWidth,
int minimumX,
int minimumZ,
int spacing,
int minimumRow,
int maximumRow
) {
for (int gridZ = minimumRow; gridZ < maximumRow; gridZ++) {
int z = Math.toIntExact((long) minimumZ + (long) gridZ * spacing);
int planeIndex = gridZ * planeWidth;
for (int gridX = 0; gridX < planeWidth; gridX++) {
int x = Math.toIntExact((long) minimumX + (long) gridX * spacing);
plane[planeIndex++] = basis(x, z);
}
}
}
private int gridWorkerCount(int planeWidth, int planeSize) {
if (samplingOptions.maximumWorkers() == 1
|| planeSize < MINIMUM_PARALLEL_BASIS_SAMPLES) {
return 1;
}
return Math.min(planeWidth, samplingOptions.maximumWorkers());
}
private static void await(List<CompletableFuture<Void>> futures) {
CompletableFuture<?>[] completions = futures.toArray(new CompletableFuture<?>[futures.size()]);
try {
CompletableFuture.allOf(completions).join();
} catch (CompletionException failure) {
Throwable cause = failure.getCause();
if (cause instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (cause instanceof Error error) {
throw error;
}
throw new IllegalStateException("Hydrology routing terrain sampling failed.", cause);
}
}
private void evictOldest(Long2ObjectLinkedOpenHashMap<?> cache) {
if (cache.size() <= maximumEntries) {
return;
}
cache.removeFirst();
}
private void evictOldest(Long2DoubleLinkedOpenHashMap cache) {
if (cache.size() <= maximumEntries) {
return;
}
cache.removeFirstDouble();
}
private double naturalHeight(int blockX, int blockZ) {
return naturalHeight(pack(blockX, blockZ), blockX, blockZ);
}
private double naturalHeight(long packed, int blockX, int blockZ) {
synchronized (lock) {
double cached = naturalHeights.getAndMoveToLast(packed);
if (!Double.isNaN(cached) || naturalHeights.containsKey(packed)) {
return cached;
}
}
double sampled = heightProvider.sample(blockX, blockZ);
synchronized (lock) {
double existing = naturalHeights.getAndMoveToLast(packed);
if (!Double.isNaN(existing) || naturalHeights.containsKey(packed)) {
return existing;
}
naturalHeights.putAndMoveToLast(packed, sampled);
evictOldest(naturalHeights);
}
return sampled;
}
private static long pack(int blockX, int blockZ) {
int mixedZ = blockZ ^ Integer.rotateLeft(blockX * COORDINATE_MIX, 16);
return ((long) blockX << 32) ^ (mixedZ & 0xffffffffL);
}
@FunctionalInterface
interface BasisProvider {
TerrainBasis sample(int blockX, int blockZ, double naturalHeight);
}
record Sources(
BasisProvider basisProvider,
IrisHydrologyNaturalHeightProvider heightProvider,
IrisHydrologyNaturalOceanClassifier oceanClassifier
) {
Sources {
Objects.requireNonNull(basisProvider, "basisProvider");
Objects.requireNonNull(heightProvider, "heightProvider");
Objects.requireNonNull(oceanClassifier, "oceanClassifier");
}
}
record SamplingOptions(
int maximumEntries,
int maximumWorkers,
Executor executor
) {
SamplingOptions {
if (maximumEntries < 1) {
throw new IllegalArgumentException("maximumEntries must be positive");
}
if (maximumWorkers < 1) {
throw new IllegalArgumentException("maximumWorkers must be positive");
}
Objects.requireNonNull(executor, "executor");
}
static SamplingOptions production(int maximumEntries) {
int configuredWorkers = IrisSettings.getThreadCount(
IrisSettings.get().getConcurrency().getParallelism()
);
return new SamplingOptions(
maximumEntries,
Math.max(1, configuredWorkers),
MultiBurst.burst
);
}
static SamplingOptions serial(int maximumEntries) {
return new SamplingOptions(maximumEntries, 1, Runnable::run);
}
}
record TerrainBasis(double naturalHeight, HydrologyTerrainSample terrain) {
TerrainBasis {
if (!Double.isFinite(naturalHeight)) {
throw new IllegalArgumentException("naturalHeight must be finite");
}
Objects.requireNonNull(terrain, "terrain");
}
}
}
@@ -0,0 +1,913 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticCandidate;
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticRenderSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.HydrologyGeometrySampler;
import art.arcane.iris.engine.hydrology.HydrologyPlanner;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyRenderSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTile;
import art.arcane.iris.engine.hydrology.HydrologyTileCache;
import art.arcane.iris.engine.hydrology.HydrologyTileKey;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import art.arcane.iris.engine.hydrology.policy.EffectiveRiverPolicy;
import art.arcane.iris.engine.hydrology.policy.RiverPolicyResolver;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisCoastalRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisHydrology;
import art.arcane.iris.engine.object.IrisInlandRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisRiverChannelShapeConfig;
import art.arcane.iris.engine.object.IrisRiverDropShapeConfig;
import art.arcane.iris.engine.object.IrisRiverGeometryConfig;
import art.arcane.iris.engine.object.IrisRiverHydraulicsConfig;
import art.arcane.iris.engine.object.IrisRiverHydrology;
import art.arcane.iris.engine.object.IrisRiverInlandOutlet;
import art.arcane.iris.engine.object.IrisRiverMeanderConfig;
import art.arcane.iris.engine.object.IrisRiverProfile;
import art.arcane.iris.engine.object.IrisRiverRoutingConfig;
import art.arcane.iris.engine.object.IrisRiverRoutingMode;
import art.arcane.iris.engine.object.IrisStyledRange;
import art.arcane.iris.engine.object.IrisSurfaceRiverChannelConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverSourceConfig;
import art.arcane.iris.engine.object.IrisUndergroundRiverConfig;
import art.arcane.iris.engine.object.IrisUndergroundRiverSourceConfig;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.math.RNG;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public final class IrisHydrologyRuntime implements AutoCloseable {
private static final int MAXIMUM_CACHE_TILES = 64;
private static final int MAXIMUM_TERRAIN_SAMPLES = 65_536;
private static final int MAXIMUM_FEATURE_SEARCH_TILES = 1_089;
private static final int BIOME_PATCH_SCALE = 32;
private static final long SURFACE_WIDTH_SALT = 0x5355524657494454L;
private static final long SURFACE_DEPTH_SALT = 0x5355524644455054L;
private static final long SURFACE_INSET_SALT = 0x53555246494e5345L;
private static final long SURFACE_BLEND_SALT = 0x53555246424c454eL;
private static final long TARGET_POOL_SALT = 0x544152474554504fL;
private static final long UNDERGROUND_LEVEL_SALT = 0x554e4445524c564cL;
private static final long UNDERGROUND_WIDTH_SALT = 0x554e444552574944L;
private static final long UNDERGROUND_DEPTH_SALT = 0x554e444552444550L;
private static final long UNDERGROUND_HEADROOM_SALT = 0x554e444552484541L;
private static final long DEEP_HEIGHT_SALT = 0x4445455048454947L;
private final IrisHydrologyRuntimeContext context;
private final IrisRiverHydrology rivers;
private final HydrologyPlannerSettings settings;
private final HydrologyTileCache cache;
private final IrisHydrologyRoutingTerrainSampler routingTerrainSampler;
private final Set<String> profileKeys;
private final String defaultProfileKey;
private final Object terrainSampleLock;
private final LinkedHashMap<Long, HydrologyTerrainSample> terrainSamples;
public IrisHydrologyRuntime(IrisHydrologyRuntimeContext context) {
this.context = Objects.requireNonNull(context);
IrisHydrology hydrology = Objects.requireNonNull(context.dimension().getHydrology());
this.rivers = Objects.requireNonNull(hydrology.getRivers());
this.profileKeys = profileKeys(rivers.getProfiles());
this.defaultProfileKey = profileKeys.isEmpty() ? "default" : profileKeys.iterator().next();
this.terrainSampleLock = new Object();
this.terrainSamples = new LinkedHashMap<>(MAXIMUM_TERRAIN_SAMPLES, 0.75F, true);
this.settings = createSettings(context.dimension(), hydrology);
HydrologyGeometrySampler geometrySampler = geometrySampler(context, hydrology);
IrisHydrologyRoutingTerrainSampler.Sources terrainSources = new IrisHydrologyRoutingTerrainSampler.Sources(
(int x, int z, double rawNaturalHeight) -> ProceduralStream.bypass2DCaches(
() -> createTerrainBasis(x, z, rawNaturalHeight)
),
(int x, int z) -> context.naturalHeightProvider().sample(x, z),
(int x, int z) -> ProceduralStream.bypass2DCaches(
() -> context.naturalOceanClassifier().isOcean(x, z)
)
);
this.routingTerrainSampler = new IrisHydrologyRoutingTerrainSampler(
terrainSources,
IrisHydrologyRoutingTerrainSampler.SamplingOptions.production(MAXIMUM_TERRAIN_SAMPLES)
);
HydrologyPlanner planner = new HydrologyPlanner(
context.seed(),
settings,
this::sampleTerrain,
routingTerrainSampler,
geometrySampler,
0,
context.caveViewFactory()
);
this.cache = new HydrologyTileCache(planner, MAXIMUM_CACHE_TILES);
}
public HydrologyPlannerSettings settings() {
return settings;
}
public HydrologyTile tile(HydrologyTileKey key) {
return cache.get(key);
}
public Optional<HydrologyColumnSample> sample(double x, double z) {
int blockX = (int) StrictMath.floor(x);
int blockZ = (int) StrictMath.floor(z);
return cache.columnAt(blockX, blockZ);
}
public void prepareChunkColumns(int blockX, int blockZ) {
cache.prepareChunkColumns(blockX, blockZ);
}
public HydrologyRenderSample renderSample(double x, double z) {
int blockX = (int) StrictMath.floor(x);
int blockZ = (int) StrictMath.floor(z);
return cache.renderAt(blockX, blockZ);
}
public boolean hasAcceptedSurfaceBiomeInChunk(String biomeKey, int chunkX, int chunkZ) {
if (biomeKey == null || biomeKey.isBlank()) {
return false;
}
int minimumX = chunkX << 4;
int minimumZ = chunkZ << 4;
int maximumX = Math.addExact(minimumX, 16);
int maximumZ = Math.addExact(minimumZ, 16);
int tileSize = settings.routing().tileSize();
int publicationRadius = settings.publicationRadius();
int minimumTileX = tileCoordinate((long) minimumX - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) maximumX - 1L + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) minimumZ - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) maximumZ - 1L + publicationRadius, tileSize);
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
HydrologyTile tile = cache.get(new HydrologyTileKey(tileX, tileZ));
for (HydrologyColumnSample column : tile.footprint().columnsIn(
minimumX,
minimumZ,
maximumX,
maximumZ
)) {
HydrologyColumnLayer layer = column.primarySurfaceLayer().orElse(null);
if (layer != null && biomeKey.equals(layer.biomeKey())) {
return true;
}
}
}
}
return false;
}
public List<HydrologyCavePlan> cavePlansIn(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ
) {
if (maximumX <= minimumX || maximumZ <= minimumZ) {
return List.of();
}
int tileSize = settings.routing().tileSize();
int publicationRadius = settings.publicationRadius();
int minimumTileX = tileCoordinate((long) minimumX - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) maximumX - 1L + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) minimumZ - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) maximumZ - 1L + publicationRadius, tileSize);
LinkedHashMap<Long, HydrologyCavePlan> plans = new LinkedHashMap<>();
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
HydrologyTile tile = cache.get(new HydrologyTileKey(tileX, tileZ));
for (HydrologyCavePlan plan : tile.cavePlans()) {
HydrologyCavePlan existing = plans.putIfAbsent(plan.source().sourceId(), plan);
if (existing != null && !existing.equals(plan)) {
throw new IllegalStateException("Hydrology cave plan id collision.");
}
}
}
}
return List.copyOf(plans.values());
}
public HydrologyRenderSample sampleRenderFootprint(
double minimumX,
double minimumZ,
double maximumX,
double maximumZ
) {
int minX = (int) StrictMath.floor(Math.min(minimumX, maximumX));
int minZ = (int) StrictMath.floor(Math.min(minimumZ, maximumZ));
int maxX = (int) StrictMath.ceil(Math.max(minimumX, maximumX));
int maxZ = (int) StrictMath.ceil(Math.max(minimumZ, maximumZ));
if (maxX <= minX) {
maxX = minX + 1;
}
if (maxZ <= minZ) {
maxZ = minZ + 1;
}
LinkedHashMap<Long, HydrologyFeatureRef> features = new LinkedHashMap<>();
int tileSize = settings.routing().tileSize();
int publicationRadius = settings.publicationRadius();
int minimumTileX = tileCoordinate((long) minX - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) maxX - 1L + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) minZ - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) maxZ - 1L + publicationRadius, tileSize);
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
HydrologyTile tile = cache.get(new HydrologyTileKey(tileX, tileZ));
for (HydrologyColumnSample column : tile.footprint().columnsIn(minX, minZ, maxX, maxZ)) {
for (HydrologyFeatureRef feature : column.renderSample().features()) {
HydrologyFeatureRef existing = features.putIfAbsent(feature.id(), feature);
if (existing != null && !existing.equals(feature)) {
throw new IllegalStateException("Hydrology feature id collision in renderer footprint.");
}
}
}
}
}
return new HydrologyRenderSample(minX, minZ, List.copyOf(features.values()));
}
public HydrologyDiagnosticRenderSample sampleDiagnosticFootprint(
double minimumX,
double minimumZ,
double maximumX,
double maximumZ
) {
int minX = (int) StrictMath.floor(Math.min(minimumX, maximumX));
int minZ = (int) StrictMath.floor(Math.min(minimumZ, maximumZ));
int maxX = (int) StrictMath.ceil(Math.max(minimumX, maximumX));
int maxZ = (int) StrictMath.ceil(Math.max(minimumZ, maximumZ));
if (maxX <= minX) {
maxX = minX + 1;
}
if (maxZ <= minZ) {
maxZ = minZ + 1;
}
int tileSize = settings.routing().tileSize();
int publicationRadius = settings.publicationRadius();
int minimumTileX = tileCoordinate((long) minX - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) maxX - 1L + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) minZ - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) maxZ - 1L + publicationRadius, tileSize);
LinkedHashMap<Long, HydrologyDiagnosticCandidate> candidates = new LinkedHashMap<>();
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
HydrologyTile tile = cache.get(new HydrologyTileKey(tileX, tileZ));
for (HydrologyDiagnosticCandidate candidate : tile.diagnosticCandidates()) {
if (candidate.point().x() < minX || candidate.point().x() >= maxX
|| candidate.point().z() < minZ || candidate.point().z() >= maxZ) {
continue;
}
HydrologyDiagnosticCandidate existing = candidates.putIfAbsent(candidate.id(), candidate);
if (existing != null && !existing.equals(candidate)) {
throw new IllegalStateException("Hydrology diagnostic candidate id collision.");
}
}
}
}
return new HydrologyDiagnosticRenderSample(minX, minZ, List.copyOf(candidates.values()));
}
public Optional<HydrologyFeatureRef> nearestFeature(
Set<HydrologyFeatureType> types,
String profileKey,
int x,
int z,
int maximumDistance
) {
Objects.requireNonNull(types);
if (types.isEmpty() || maximumDistance < 0) {
return Optional.empty();
}
int tileSize = settings.routing().tileSize();
int publicationRadius = settings.publicationRadius();
int minimumTileX = tileCoordinate((long) x - maximumDistance - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) x + maximumDistance + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) z - maximumDistance - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) z + maximumDistance + publicationRadius, tileSize);
long tileCount = HydrologyFeatureSearchBounds.tileCount(
x, z, maximumDistance, tileSize, publicationRadius);
if (tileCount > MAXIMUM_FEATURE_SEARCH_TILES) {
throw new IllegalArgumentException("Hydrology feature search exceeds the bounded tile limit.");
}
HydrologyFeatureRef nearest = null;
long nearestDistanceSquared = Long.MAX_VALUE;
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
HydrologyTile tile = cache.get(new HydrologyTileKey(tileX, tileZ));
HydrologyFeatureRef feature = tile.nearestFeature(
types, profileKey, x, z, maximumDistance).orElse(null);
if (feature != null) {
long deltaX = (long) feature.x() - x;
long deltaZ = (long) feature.z() - z;
long distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
if (nearest == null || distanceSquared < nearestDistanceSquared
|| distanceSquared == nearestDistanceSquared && feature.id() < nearest.id()) {
nearest = feature;
nearestDistanceSquared = distanceSquared;
}
}
}
}
return Optional.ofNullable(nearest);
}
public int maximumFeatureSearchDistance(int x, int z, int requestedDistance) {
return HydrologyFeatureSearchBounds.maximumDistance(
x,
z,
requestedDistance,
settings.routing().tileSize(),
settings.publicationRadius(),
MAXIMUM_FEATURE_SEARCH_TILES
);
}
public Set<String> profileKeys() {
return profileKeys;
}
public List<String> featureQueryKeys() {
ArrayList<String> deepFluidIds = new ArrayList<>(settings.deepFluids().size());
for (HydrologyPlannerSettings.DeepFluid deepFluid : settings.deepFluids()) {
deepFluidIds.add(deepFluid.id());
}
return HydrologyFeatureQuery.suggestions(deepFluidIds);
}
@Override
public void close() {
cache.close();
routingTerrainSampler.close();
synchronized (terrainSampleLock) {
terrainSamples.clear();
}
}
private HydrologyTerrainSample sampleTerrain(int x, int z) {
long packed = ((long) x << 32) ^ (z & 0xffffffffL);
synchronized (terrainSampleLock) {
HydrologyTerrainSample cached = terrainSamples.get(packed);
if (cached != null) {
return cached;
}
}
HydrologyTerrainSample sampled = ProceduralStream.bypass2DCaches(() -> createDetailedTerrainSample(x, z));
synchronized (terrainSampleLock) {
HydrologyTerrainSample existing = terrainSamples.get(packed);
if (existing != null) {
return existing;
}
terrainSamples.put(packed, sampled);
if (terrainSamples.size() > MAXIMUM_TERRAIN_SAMPLES) {
Iterator<Long> iterator = terrainSamples.keySet().iterator();
iterator.next();
iterator.remove();
}
}
return sampled;
}
private HydrologyTerrainSample createDetailedTerrainSample(int x, int z) {
IrisHydrologyRoutingTerrainSampler.TerrainBasis basis = routingTerrainSampler.basis(x, z);
double slope = routingTerrainSampler.localSlope(x, z, basis.naturalHeight());
return basis.terrain().withSlope(slope);
}
private IrisHydrologyRoutingTerrainSampler.TerrainBasis createTerrainBasis(
int x,
int z,
double rawNaturalHeight
) {
IrisHydrologyNaturalSample naturalSample = Objects.requireNonNull(
context.naturalSampleProvider().sample(x, z, rawNaturalHeight),
"Hydrology natural sample provider returned null at " + x + "," + z
);
double sampledNaturalHeight = naturalSample.naturalHeight();
double resolvedNaturalHeight = Double.isFinite(sampledNaturalHeight)
? sampledNaturalHeight
: rawNaturalHeight;
if (!Double.isFinite(resolvedNaturalHeight)) {
throw new IllegalStateException("Hydrology natural height was not finite at " + x + "," + z);
}
int naturalHeight = (int) StrictMath.round(resolvedNaturalHeight);
boolean ocean = naturalSample.ocean();
IrisBiome biome = naturalSample.biome();
IrisRegion region = naturalSample.region();
EffectiveRiverPolicy policy = RiverPolicyResolver.resolve(context.dimension(), region, biome);
String parentBiomeKey = requireBiomeKey(biome);
List<String> profiles = validProfiles(policy.profiles());
int configuredFluidY = settings.underground().minimumFluidY()
+ (settings.underground().maximumFluidY() - settings.underground().minimumFluidY()) / 2;
if (!rivers.getUnderground().getFluidLevel().isFlat()) {
int lowestFluidY = Math.max(
settings.underground().minimumFluidY(),
settings.underground().maximumDepth() + 1
);
int highestFluidY = Math.min(
settings.underground().maximumFluidY(),
naturalHeight - settings.underground().minimumHeadroom() - 1
);
if (lowestFluidY <= highestFluidY) {
configuredFluidY = lowestFluidY + (highestFluidY - lowestFluidY) / 2;
}
}
int caveFloorY = configuredFluidY - settings.underground().maximumDepth();
boolean caveAvailable = caveFloorY > 0
&& configuredFluidY + settings.underground().maximumHeadroom() < naturalHeight;
boolean routingAllowed = policy.allowsTransit() && policy.allowsRouting() && !ocean;
boolean sourceAllowed = routingAllowed && policy.allowsSources();
double sourceWeight = switch (policy.placement()) {
case DISABLED, TRANSIT_ONLY -> 0D;
case NATURAL -> 1D;
case PREFERRED_HEADWATER -> 4D;
case REQUIRED_HEADWATER -> 8D;
};
double routingCost = policy.routing() == IrisRiverRoutingMode.AVOID ? 1024D : 0D;
double routingPreference = policy.routing() == IrisRiverRoutingMode.PREFER ? 0.5D : 1D;
double biomePatchNoise = coherentPatchNoise(context.seed(), x, z);
HydrologyTerrainSample terrain = new HydrologyTerrainSample(
naturalHeight,
0D,
ocean,
caveAvailable,
caveFloorY,
configuredFluidY,
routingAllowed,
routingAllowed && policy.outletAdmission(),
sourceAllowed,
sourceAllowed && policy.requiresHeadwaters(),
sourceAllowed && caveAvailable,
sourceAllowed && caveAvailable && policy.requiresHeadwaters(),
routingCost,
sourceWeight,
sourceWeight,
policy.widthMultiplier(),
policy.depthMultiplier(),
policy.incisionMultiplier(),
policy.routingMultiplier() * routingPreference,
parentBiomeKey,
selectKey(policy.surfaceBiomes(), parentBiomeKey, biomePatchNoise, 1),
selectKey(policy.mouthBiomes(), parentBiomeKey, biomePatchNoise, 2),
selectKey(policy.shoreBiomes(), parentBiomeKey, biomePatchNoise, 3),
selectKey(policy.dryBiomes(), parentBiomeKey, biomePatchNoise, 4),
selectKey(policy.floodedCaveBiomes(), parentBiomeKey, biomePatchNoise, 5),
profiles
);
return new IrisHydrologyRoutingTerrainSampler.TerrainBasis(resolvedNaturalHeight, terrain);
}
private List<String> validProfiles(List<String> requested) {
if (requested == null || requested.isEmpty()) {
return List.of(defaultProfileKey);
}
ArrayList<String> valid = new ArrayList<>();
for (String key : requested) {
if (key != null && profileKeys.contains(key) && !valid.contains(key)) {
valid.add(key);
}
}
return valid.isEmpty() ? List.of(defaultProfileKey) : List.copyOf(valid);
}
private String selectKey(List<String> keys, String fallback, double patchNoise, int salt) {
if (keys == null || keys.isEmpty()) {
return fallback;
}
return keys.get(keyIndex(patchNoise, salt, keys.size()));
}
static int coherentKeyIndex(long seed, int x, int z, int salt, int keyCount) {
if (keyCount <= 0) {
throw new IllegalArgumentException("keyCount must be positive");
}
if (keyCount == 1) {
return 0;
}
return keyIndex(coherentPatchNoise(seed, x, z), salt, keyCount);
}
private static int keyIndex(double patchNoise, int salt, int keyCount) {
double shifted = patchNoise + salt * 0.3819660112501051D;
shifted -= StrictMath.floor(shifted);
return Math.min(keyCount - 1, (int) StrictMath.floor(shifted * keyCount));
}
private static double coherentPatchNoise(long seed, int x, int z) {
int cellX = Math.floorDiv(x, BIOME_PATCH_SCALE);
int cellZ = Math.floorDiv(z, BIOME_PATCH_SCALE);
double localX = Math.floorMod(x, BIOME_PATCH_SCALE) / (double) BIOME_PATCH_SCALE;
double localZ = Math.floorMod(z, BIOME_PATCH_SCALE) / (double) BIOME_PATCH_SCALE;
double smoothX = localX * localX * (3D - 2D * localX);
double smoothZ = localZ * localZ * (3D - 2D * localZ);
double top = interpolate(
keyNoise(seed, cellX, cellZ),
keyNoise(seed, cellX + 1, cellZ),
smoothX
);
double bottom = interpolate(
keyNoise(seed, cellX, cellZ + 1),
keyNoise(seed, cellX + 1, cellZ + 1),
smoothX
);
return interpolate(top, bottom, smoothZ);
}
private static double keyNoise(long seed, int cellX, int cellZ) {
long mixed = avalanche(seed ^ 0x9e3779b97f4a7c15L);
mixed = avalanche(mixed ^ avalanche(cellX + 0x9e3779b97f4a7c15L));
mixed = avalanche(mixed ^ avalanche(cellZ + 0x9e3779b97f4a7c15L));
return (avalanche(mixed) >>> 11) * 0x1.0p-53;
}
private static long avalanche(long value) {
value ^= value >>> 30;
value *= 0xbf58476d1ce4e5b9L;
value ^= value >>> 27;
value *= 0x94d049bb133111ebL;
return value ^ value >>> 31;
}
private static double interpolate(double first, double second, double progress) {
return first + (second - first) * progress;
}
private static HydrologyGeometrySampler geometrySampler(
IrisHydrologyRuntimeContext context,
IrisHydrology hydrology
) {
IrisRiverHydrology rivers = hydrology.getRivers();
IrisSurfaceRiverConfig surface = rivers.getSurface();
IrisSurfaceRiverChannelConfig channel = surface.getChannel();
IrisRiverHydraulicsConfig hydraulics = surface.getHydraulics();
IrisUndergroundRiverConfig underground = rivers.getUnderground();
ProceduralStream<Double> surfaceWidth = styledStream(context, channel.getWidth(), SURFACE_WIDTH_SALT);
ProceduralStream<Double> surfaceDepth = styledStream(context, channel.getDepth(), SURFACE_DEPTH_SALT);
ProceduralStream<Double> surfaceInset = styledStream(context, channel.getSurfaceInset(), SURFACE_INSET_SALT);
ProceduralStream<Double> surfaceBlend = styledStream(
context,
channel.getTerrainBlendWidth(),
SURFACE_BLEND_SALT
);
ProceduralStream<Double> targetPool = styledStream(
context,
hydraulics.getTargetPoolLength(),
TARGET_POOL_SALT
);
ProceduralStream<Double> undergroundLevel = styledStream(
context,
underground.getFluidLevel(),
UNDERGROUND_LEVEL_SALT
);
ProceduralStream<Double> undergroundWidth = styledStream(
context,
underground.getChannelWidth(),
UNDERGROUND_WIDTH_SALT
);
ProceduralStream<Double> undergroundDepth = styledStream(
context,
underground.getDepth(),
UNDERGROUND_DEPTH_SALT
);
ProceduralStream<Double> undergroundHeadroom = styledStream(
context,
underground.getHeadroom(),
UNDERGROUND_HEADROOM_SALT
);
LinkedHashMap<String, ProceduralStream<Double>> deepHeights = new LinkedHashMap<>();
for (IrisDeepFluidConfig deepFluid : hydrology.getDeepFluids()) {
long profileSalt = DEEP_HEIGHT_SALT ^ deepFluid.getId().hashCode();
deepHeights.put(deepFluid.getId(), styledStream(context, deepFluid.getHeight(), profileSalt));
}
int minimumWorldY = context.dimension().getMinHeight();
return request -> {
ProceduralStream<Double> stream = switch (request.field()) {
case SURFACE_WIDTH -> surfaceWidth;
case SURFACE_DEPTH -> surfaceDepth;
case SURFACE_INSET -> surfaceInset;
case SURFACE_BLEND_WIDTH -> surfaceBlend;
case TARGET_POOL_LENGTH -> targetPool;
case UNDERGROUND_FLUID_LEVEL -> undergroundLevel;
case UNDERGROUND_WIDTH -> undergroundWidth;
case UNDERGROUND_DEPTH -> undergroundDepth;
case UNDERGROUND_HEADROOM -> undergroundHeadroom;
case DEEP_FLUID_HEIGHT -> Objects.requireNonNull(
deepHeights.get(request.profileKey()),
"Missing deep-fluid height style for " + request.profileKey() + "."
);
};
double sampled = stream.get(request.x(), request.z());
if (!Double.isFinite(sampled)) {
throw new IllegalStateException(
"Hydrology geometry style returned a non-finite value for " + request.field() + "."
);
}
long rounded = StrictMath.round(sampled);
if (request.field() == HydrologyGeometrySampler.Field.UNDERGROUND_FLUID_LEVEL
|| request.field() == HydrologyGeometrySampler.Field.DEEP_FLUID_HEIGHT) {
rounded = Math.subtractExact(rounded, minimumWorldY);
}
int value = Math.toIntExact(rounded);
return Math.max(request.minimum(), Math.min(request.maximum(), value));
};
}
private static ProceduralStream<Double> styledStream(
IrisHydrologyRuntimeContext context,
IrisStyledRange range,
long salt
) {
return range.stream(new RNG(context.seed() ^ salt), context.data());
}
static HydrologyPlannerSettings createSettings(
IrisDimension dimension,
IrisHydrology hydrology
) {
IrisRiverHydrology rivers = hydrology.getRivers();
IrisRiverRoutingConfig routing = rivers.getRouting();
IrisSurfaceRiverConfig surface = rivers.getSurface();
IrisSurfaceRiverSourceConfig surfaceSources = surface.getSources();
IrisSurfaceRiverChannelConfig channel = surface.getChannel();
IrisRiverHydraulicsConfig hydraulics = surface.getHydraulics();
IrisUndergroundRiverConfig underground = rivers.getUnderground();
IrisUndergroundRiverSourceConfig undergroundSources = underground.getSources();
int routeNodes = maximumRouteNodes(routing);
int minimumWorldY = dimension.getMinHeight();
boolean riversEnabled = rivers.isEnabled();
boolean surfaceEnabled = riversEnabled && surface.isEnabled();
boolean undergroundEnabled = riversEnabled && underground.isEnabled();
HydrologyPlannerSettings.Routing plannerRouting = new HydrologyPlannerSettings.Routing(
routing.getTileSize(),
routing.getSampleSpacing(),
routing.getRefinementSpacing(),
routeNodes,
routing.getMaximumRouteLength(),
new HydrologyPlannerSettings.Branching(
routing.getBranching().getMinimumSurfaceCourseLength(),
routing.getBranching().getMinimumUndergroundCourseLength()
),
1.5D,
24D,
2D,
0.2D
);
HydrologyPlannerSettings.Source plannerSurfaceSources = new HydrologyPlannerSettings.Source(
surfaceEnabled,
surfaceSources.getDensity(),
surfaceSources.getMinimumElevation() - minimumWorldY,
surfaceSources.getMinimumPerTile(),
maximumSources(surfaceSources.getDensity(), surfaceSources.getMinimumPerTile()),
surfaceSources.getMinimumSpacing()
);
HydrologyPlannerSettings.Surface plannerSurface = new HydrologyPlannerSettings.Surface(
surfaceEnabled,
plannerSurfaceSources,
minimumInt(channel.getWidth()),
maximumInt(channel.getWidth()),
minimumInt(channel.getDepth()),
maximumInt(channel.getDepth()),
minimumInt(channel.getSurfaceInset()),
maximumInt(channel.getSurfaceInset()),
channel.getMaximumIncision(),
channel.getShoreWidth(),
minimumInt(channel.getTerrainBlendWidth()),
maximumInt(channel.getTerrainBlendWidth()),
surface.getRidgeTunnels().isEnabled(),
surface.getRidgeTunnels().getMaximumLength(),
surface.getRidgeTunnels().getHeadroom()
);
HydrologyPlannerSettings.Hydraulics plannerHydraulics = new HydrologyPlannerSettings.Hydraulics(
minimumInt(hydraulics.getTargetPoolLength()),
maximumInt(hydraulics.getTargetPoolLength()),
hydraulics.getRiffleDrop(),
hydraulics.getMaximumGradualDrop(),
hydraulics.getMaximumGradualLength(),
hydraulics.getWaterfallMinimumDrop()
);
HydrologyPlannerSettings.Source plannerUndergroundSources = new HydrologyPlannerSettings.Source(
undergroundEnabled,
undergroundSources.getDensity(),
Integer.MIN_VALUE,
undergroundSources.getMinimumPerTile(),
maximumSources(undergroundSources.getDensity(), undergroundSources.getMinimumPerTile()),
undergroundSources.getMinimumSpacing()
);
HydrologyPlannerSettings.Underground plannerUnderground = new HydrologyPlannerSettings.Underground(
undergroundEnabled,
plannerUndergroundSources,
minimumInt(underground.getFluidLevel()) - minimumWorldY,
maximumInt(underground.getFluidLevel()) - minimumWorldY,
minimumInt(underground.getChannelWidth()),
maximumInt(underground.getChannelWidth()),
minimumInt(underground.getDepth()),
maximumInt(underground.getDepth()),
minimumInt(underground.getHeadroom()),
maximumInt(underground.getHeadroom()),
underground.isConnectToExistingCaves()
);
IrisRiverGrottoConfig grottos = rivers.getGrottos();
IrisCoastalRiverGrottoConfig coastal = grottos.getCoastal();
IrisInlandRiverGrottoConfig inland = grottos.getInland();
boolean inlandEnabled = inland.isEnabled()
&& routing.getInlandOutlets().contains(IrisRiverInlandOutlet.SINKHOLE_GROTTO);
HydrologyPlannerSettings.Outlets plannerOutlets = new HydrologyPlannerSettings.Outlets(
routing.isOceanOutlets(),
grotto(coastal.isEnabled(), coastal.getHorizontalRadius(), coastal.getVerticalRadius(),
coastal.getHeadroom(), coastal.getMaximumVolume()),
grotto(inlandEnabled, inland.getHorizontalRadius(), inland.getVerticalRadius(),
inland.getHeadroom(), inland.getMaximumVolume()),
inlandEnabled && inland.isConnectSurfaceRivers(),
Math.max(4, coastal.getVerticalRadius()),
surface.getMouths().getLevelingDistance(),
surface.getMouths().getMaximumOceanApron(),
routing.getMaximumOutletsPerTile()
);
HydrologyPlannerSettings.Geometry plannerGeometry = geometry(rivers.getGeometry());
return new HydrologyPlannerSettings(
dimension.getFluidHeight(),
plannerRouting,
plannerSurface,
plannerHydraulics,
plannerUnderground,
plannerOutlets,
plannerGeometry,
deepFluids(hydrology.getDeepFluids(), minimumWorldY, routing)
);
}
private static HydrologyPlannerSettings.Geometry geometry(IrisRiverGeometryConfig geometry) {
IrisRiverMeanderConfig meanders = geometry.getMeanders();
IrisRiverDropShapeConfig drops = geometry.getDrops();
return new HydrologyPlannerSettings.Geometry(
new HydrologyPlannerSettings.Meanders(
meanders.getPrimaryWavelength(),
meanders.getDetailWavelength(),
meanders.getPrimaryStrength(),
meanders.getDetailStrength(),
meanders.getMaximumOffsetRatio(),
meanders.getSmoothingPasses(),
meanders.getMaximumTurnDegrees()
),
channelShape(geometry.getSurface()),
channelShape(geometry.getUnderground()),
channelShape(geometry.getGrottos()),
new HydrologyPlannerSettings.Drops(
drops.getCascadeRunPerBlock(),
drops.getCascadeExponent(),
drops.getMaximumCascadeStep(),
drops.getFlowWidthRatio(),
drops.getMaximumFlowDepth(),
drops.getBasinWidthRatio(),
drops.getMaximumBasinDepth()
)
);
}
private static HydrologyPlannerSettings.ChannelShape channelShape(IrisRiverChannelShapeConfig shape) {
return new HydrologyPlannerSettings.ChannelShape(
shape.getBedRoundness(),
shape.getBedRoughness(),
shape.getWallRoughness(),
shape.getRoughnessWavelength()
);
}
private static HydrologyPlannerSettings.Grotto grotto(
boolean enabled,
int horizontalRadius,
int verticalRadius,
int headroom,
int maximumVolume
) {
return new HydrologyPlannerSettings.Grotto(
enabled,
horizontalRadius,
verticalRadius,
headroom,
maximumVolume
);
}
private static List<HydrologyPlannerSettings.DeepFluid> deepFluids(
List<IrisDeepFluidConfig> configurations,
int minimumWorldY,
IrisRiverRoutingConfig routing
) {
ArrayList<HydrologyPlannerSettings.DeepFluid> deepFluids = new ArrayList<>();
for (IrisDeepFluidConfig configuration : configurations) {
int radius = configuration.getHorizontalRadius();
int verticalRadius = configuration.getVerticalRadius();
int maximumVolume = boundedVolume(radius, verticalRadius, configuration.getHeadroom());
int maximumPerTile = Math.min(64, maximumSources(configuration.getDensity(), 0));
int maximumChannelLength = maximumDeepChannelLength(configuration, routing);
int minimumChannelLength = maximumChannelLength == 0
? 0
: Math.min(maximumChannelLength, Math.max(routing.getRefinementSpacing(), radius));
deepFluids.add(new HydrologyPlannerSettings.DeepFluid(
configuration.getId(),
configuration.getDensity() > 0D
&& (configuration.isContainedPools() || configuration.isShortChannels()),
configuration.getDensity(),
configuration.getSpacing(),
minimumInt(configuration.getHeight()) - minimumWorldY,
maximumInt(configuration.getHeight()) - minimumWorldY,
radius,
radius,
verticalRadius,
verticalRadius,
minimumChannelLength,
maximumChannelLength,
configuration.getChannelWidth(),
configuration.getDepth(),
configuration.getHeadroom(),
maximumVolume,
maximumPerTile,
configuration.isContainedPools(),
configuration.isShortChannels()
));
}
return List.copyOf(deepFluids);
}
static int maximumDeepChannelLength(
IrisDeepFluidConfig configuration,
IrisRiverRoutingConfig routing
) {
if (!configuration.isShortChannels()) {
return 0;
}
int configuredLength = Math.max(
routing.getRefinementSpacing(), configuration.getSpacing() / 3);
return Math.min(configuredLength, routing.getTileSize() / 2);
}
private static int maximumRouteNodes(IrisRiverRoutingConfig routing) {
int halo = Math.min(routing.getMaximumRouteLength(), routing.getTileSize() / 2);
int alignedHalo = Math.floorDiv(halo, routing.getSampleSpacing()) * routing.getSampleSpacing();
int width = routing.getTileSize() / routing.getSampleSpacing() + 1
+ alignedHalo * 2 / routing.getSampleSpacing();
long nodes = (long) width * width;
if (nodes > 1_000_000L) {
throw new IllegalArgumentException("Hydrology routing lattice exceeds one million nodes.");
}
return (int) nodes;
}
private static int tileCoordinate(long blockCoordinate, int tileSize) {
return Math.toIntExact(Math.floorDiv(blockCoordinate, tileSize));
}
private static int maximumSources(double density, int minimumPerTile) {
int expected = (int) Math.ceil(Math.max(0D, density));
return Math.min(64, Math.max(minimumPerTile, expected));
}
private static int boundedVolume(int radius, int verticalRadius, int headroom) {
long diameter = radius * 2L + 1L;
long volume = diameter * diameter * (verticalRadius + headroom + 1L);
return (int) Math.min(1_048_576L, Math.max(64L, volume));
}
private static int minimumInt(IrisStyledRange range) {
return (int) StrictMath.floor(Math.min(range.getMin(), range.getMax()));
}
private static int maximumInt(IrisStyledRange range) {
return (int) StrictMath.ceil(Math.max(range.getMin(), range.getMax()));
}
private static Set<String> profileKeys(List<IrisRiverProfile> profiles) {
LinkedHashSet<String> keys = new LinkedHashSet<>();
for (IrisRiverProfile profile : profiles) {
if (profile != null && profile.getId() != null && !profile.getId().isBlank()) {
keys.add(profile.getId().trim());
}
}
return Collections.unmodifiableSet(new LinkedHashSet<>(keys));
}
private static String requireBiomeKey(IrisBiome biome) {
if (biome == null || biome.getLoadKey() == null || biome.getLoadKey().isBlank()) {
throw new IllegalStateException("Hydrology terrain sampling requires a loaded parent biome.");
}
return biome.getLoadKey();
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.hydrology.HydrologyCaveVoxelViewFactory;
import art.arcane.iris.engine.object.IrisDimension;
import java.util.Objects;
public record IrisHydrologyRuntimeContext(
long seed,
int worldHeight,
IrisDimension dimension,
IrisData data,
IrisHydrologyNaturalSampleProvider naturalSampleProvider,
IrisHydrologyNaturalHeightProvider naturalHeightProvider,
IrisHydrologyNaturalOceanClassifier naturalOceanClassifier,
HydrologyCaveVoxelViewFactory caveViewFactory
) {
public IrisHydrologyRuntimeContext {
if (worldHeight < 3) {
throw new IllegalArgumentException("worldHeight must be at least three");
}
Objects.requireNonNull(dimension);
Objects.requireNonNull(data);
Objects.requireNonNull(naturalSampleProvider);
Objects.requireNonNull(naturalHeightProvider);
Objects.requireNonNull(naturalOceanClassifier);
Objects.requireNonNull(caveViewFactory);
}
}
@@ -27,9 +27,10 @@ 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.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveStorage;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.util.project.matter.PreObjectMatterCell;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
@@ -49,6 +50,7 @@ import org.jetbrains.annotations.UnmodifiableView;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public interface EngineMantle extends MatterGenerator {
@@ -112,7 +114,7 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean isCarved(int x, int h, int z) {
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(getMantle(), x, h, z);
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(getMantle(), x, h, z);
if (hydrology != null) {
return hydrology.carves();
}
@@ -244,7 +246,7 @@ public interface EngineMantle extends MatterGenerator {
MantleObjectComponent getObjectComponent();
default boolean isCovered(int x, int z) {
int s = getRealRadius();
int s = Math.max(getRadius(), getRealRadius());
for (int i = -s; i <= s; i++) {
for (int j = -s; j <= s; j++) {
@@ -259,42 +261,68 @@ public interface EngineMantle extends MatterGenerator {
return true;
}
default void cleanupChunk(int x, int z) {
if (!isCovered(x, z)) return;
doCleanupChunk(x, z);
default boolean cleanupChunk(int x, int z) {
return cleanupTargetIfCovered(x, z, false);
}
default void forceCleanupChunk(int x, int z) {
default boolean forceCleanupChunk(int x, int z) {
return cleanupTargetIfCovered(x, z, true);
}
default void cleanupChunksCoveredBy(
int newRealX,
int newRealZ,
boolean force,
ChunkCleanupCallback callback
) {
Objects.requireNonNull(callback, "callback");
int radius = Math.max(getRadius(), getRealRadius());
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
int candidateX = newRealX + offsetX;
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
int candidateZ = newRealZ + offsetZ;
if (getMantle().hasFlag(candidateX, candidateZ, MantleFlag.CLEANED)) {
continue;
}
if (cleanupTargetIfCovered(candidateX, candidateZ, force)) {
callback.onChunkCleaned(candidateX, candidateZ);
}
}
}
}
private boolean cleanupTargetIfCovered(int x, int z, boolean force) {
if (getMantle().hasFlag(x, z, MantleFlag.CLEANED) || !isCovered(x, z)) {
return false;
}
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
try {
chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> {
MantleSliceRetention.deleteUnlessRetained(chunk, PlatformBlockState.class);
MantleSliceRetention.deleteUnlessRetained(chunk, String.class);
MantleSliceRetention.deleteUnlessRetained(chunk, UpdateMatter.class);
MantleSliceRetention.deleteUnlessRetained(chunk, MatterCavern.class);
MantleSliceRetention.deleteUnlessRetained(chunk, MatterFluidBody.class);
MantleSliceRetention.deleteUnlessRetained(chunk, MatterMarker.class);
MantleSliceRetention.deleteUnlessRetained(chunk, TreeBlockMaterial.class);
chunk.trimSlices();
});
synchronized (chunk) {
if (chunk.isFlagged(MantleFlag.CLEANED)) {
return false;
}
chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> cleanupSlices(chunk, force));
return true;
}
} finally {
chunk.release();
}
}
private void doCleanupChunk(int x, int z) {
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
try {
chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> {
MantleSliceRetention.deleteUnlessRetained(chunk, PlatformBlockState.class);
MantleSliceRetention.deleteUnlessRetained(chunk, UpdateMatter.class);
MantleSliceRetention.deleteUnlessRetained(chunk, MatterCavern.class);
MantleSliceRetention.deleteUnlessRetained(chunk, MatterFluidBody.class);
chunk.trimSlices();
});
} finally {
chunk.release();
private void cleanupSlices(MantleChunk<Matter> chunk, boolean force) {
MantleSliceRetention.deleteUnlessRetained(chunk, PlatformBlockState.class);
if (force) {
MantleSliceRetention.deleteUnlessRetained(chunk, String.class);
}
MantleSliceRetention.deleteUnlessRetained(chunk, UpdateMatter.class);
MantleSliceRetention.deleteUnlessRetained(chunk, MatterCavern.class);
MantleSliceRetention.deleteUnlessRetained(chunk, MatterFluidBody.class);
if (force) {
MantleSliceRetention.deleteUnlessRetained(chunk, MatterMarker.class);
MantleSliceRetention.deleteUnlessRetained(chunk, TreeBlockMaterial.class);
}
chunk.deleteSlices(PreObjectMatterCell.class);
chunk.trimSlices();
}
default int getUnloadRegionCount() {
@@ -304,4 +332,12 @@ public interface EngineMantle extends MatterGenerator {
default double getAdjustedIdleDuration() {
return getMantle().getAdjustedIdleDuration();
}
@FunctionalInterface
interface ChunkCleanupCallback {
ChunkCleanupCallback NONE = (x, z) -> {
};
void onChunkCleaned(int x, int z);
}
}
@@ -34,6 +34,10 @@ public interface MantleComponent extends Comparable<MantleComponent> {
int getRadius();
default int getOutputRadius() {
return getRadius();
}
default int getInputRadius() {
return 0;
}
@@ -25,7 +25,7 @@ import java.util.List;
*
* @param components every component registered at this priority, enabled or not
* @param passChunkRadius chunk ring this pass must complete: ceilDiv of this pass' widest
* component reach plus every later pass' reach
* component output reach plus every later pass' reach
* @param downstreamBlockRadius block reach summed over all LATER passes only. A component must be
* generated across its own reach plus this, or later passes read
* half-written data from it
@@ -21,17 +21,18 @@ package art.arcane.iris.engine.mantle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.iris.util.project.matter.PreObjectMatterCell;
import com.google.common.collect.ImmutableList;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
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.iris.engine.mantle.components.ObjectPassPlacer;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.function.Function3;
@@ -44,19 +45,27 @@ import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.scheduling.J;
import lombok.Data;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import org.bukkit.util.Vector;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceArray;
import static art.arcane.iris.engine.mantle.EngineMantle.AIR;
@Data
public class MantleWriter implements IObjectPlacer, AutoCloseable {
public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
private static final int OBJECT_COMPONENT_PRIORITY = 2;
private final EngineMantle engineMantle;
private final Mantle<Matter> mantle;
private final int radius;
@@ -64,6 +73,11 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
private final int z;
private final int windowSide;
private final AtomicReferenceArray<MantleChunk<Matter>> window;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
@ToString.Exclude
private final ThreadLocal<Integer> activeComponentPriority = new ThreadLocal<>();
public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) {
this(engineMantle, mantle, x, z, radius, radius * 2, multicore);
@@ -124,6 +138,34 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
return Math.max(1, availableProcessors / 2);
}
public void withComponentPriority(int priority, Runnable task) {
Objects.requireNonNull(task, "task");
Integer previous = activeComponentPriority.get();
activeComponentPriority.set(priority);
try {
task.run();
} finally {
if (previous == null) {
activeComponentPriority.remove();
} else {
activeComponentPriority.set(previous);
}
}
}
@ChunkCoordinates
public void withChunkFence(int chunkX, int chunkZ, Runnable task) {
Objects.requireNonNull(task, "task");
MantleChunk<Matter> chunk = acquireChunk(chunkX, chunkZ);
if (chunk == null) {
throw new IllegalArgumentException("Chunk fence exceeds the prepared Mantle radius at "
+ chunkX + "," + chunkZ);
}
synchronized (chunk) {
task.run();
}
}
private static Set<IrisPosition> getBallooned(Set<IrisPosition> vset, double radius) {
Set<IrisPosition> returnset = new HashSet<>();
int ceilrad = (int) Math.ceil(radius);
@@ -206,16 +248,19 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
MantleChunk<Matter> chunk = acquireChunk(cx, cz);
if (chunk == null) return;
Matter matter = chunk.getOrCreate(y >> 4);
if ((t instanceof PlatformBlockState || t instanceof MatterCavern)
&& hasProtectedHydrology(matter, x, y, z)) {
return;
synchronized (chunk) {
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);
}
Class<?> sliceType = t instanceof PlatformBlockState ? PlatformBlockState.class : matter.getClass(t);
capturePreObjectOriginal(matter, x, y, z, sliceType);
matter.slice(sliceType).set(x & 15, y & 15, z & 15, t);
}
if (t instanceof PlatformBlockState) {
clearDeferredPlacement(matter, x, y, z);
}
Class<?> sliceType = t instanceof PlatformBlockState ? PlatformBlockState.class : matter.getClass(t);
matter.slice(sliceType).set(x & 15, y & 15, z & 15, t);
}
public boolean setDataIfAbsent(int x, int y, int z, MatterCavern value) {
@@ -235,17 +280,28 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
return false;
}
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;
}
synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return false;
}
MatterSlice<MatterCavern> cavernSlice = matter.hasSlice(MatterCavern.class)
? matter.getSlice(MatterCavern.class)
: null;
MatterCavern existing = cavernSlice == null
? null
: cavernSlice.get(x & 15, y & 15, z & 15);
if (existing != null) {
return false;
}
matter.<MatterCavern>slice(MatterCavern.class).set(x & 15, y & 15, z & 15, value);
return true;
capturePreObjectOriginal(matter, x, y, z, MatterCavern.class);
if (cavernSlice == null) {
cavernSlice = matter.slice(MatterCavern.class);
}
cavernSlice.set(x & 15, y & 15, z & 15, value);
return true;
}
}
public boolean carveDataIfAbsent(int x, int y, int z, MatterCavern value) {
@@ -258,20 +314,30 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
return false;
}
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return false;
synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return false;
}
if (matter.hasSlice(PlatformBlockState.class)) {
capturePreObjectOriginal(matter, x, y, z, PlatformBlockState.class);
matter.<PlatformBlockState>getSlice(PlatformBlockState.class)
.set(x & 15, y & 15, z & 15, null);
}
clearDeferredPlacement(matter, x, y, z);
MatterSlice<MatterCavern> cavernSlice = matter.hasSlice(MatterCavern.class)
? matter.getSlice(MatterCavern.class)
: null;
if (cavernSlice != null && cavernSlice.get(x & 15, y & 15, z & 15) != null) {
return false;
}
capturePreObjectOriginal(matter, x, y, z, MatterCavern.class);
if (cavernSlice == null) {
cavernSlice = matter.slice(MatterCavern.class);
}
cavernSlice.set(x & 15, y & 15, z & 15, value);
return true;
}
if (matter.hasSlice(PlatformBlockState.class)) {
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
}
clearDeferredPlacement(matter, x, y, z);
MatterSlice<MatterCavern> cavernSlice = matter.slice(MatterCavern.class);
if (cavernSlice.get(x & 15, y & 15, z & 15) != null) {
return false;
}
cavernSlice.set(x & 15, y & 15, z & 15, value);
return true;
}
public void setForcedCarve(int x, int y, int z, MatterCavern value) {
@@ -283,15 +349,20 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
throw new IllegalStateException("Forced structure carve exceeded its prepared Mantle radius at "
+ x + "," + y + "," + z);
}
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return;
synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return;
}
if (matter.hasSlice(PlatformBlockState.class)) {
capturePreObjectOriginal(matter, x, y, z, PlatformBlockState.class);
matter.<PlatformBlockState>getSlice(PlatformBlockState.class)
.set(x & 15, y & 15, z & 15, null);
}
clearDeferredPlacement(matter, x, y, z);
capturePreObjectOriginal(matter, x, y, z, MatterCavern.class);
matter.<MatterCavern>slice(MatterCavern.class).set(x & 15, y & 15, z & 15, value);
}
if (matter.hasSlice(PlatformBlockState.class)) {
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
}
clearDeferredPlacement(matter, x, y, z);
matter.<MatterCavern>slice(MatterCavern.class).set(x & 15, y & 15, z & 15, value);
}
public void clearBlock(int x, int y, int z) {
@@ -306,17 +377,21 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
if (!chunk.exists(section)) {
return;
}
Matter matter = chunk.get(section);
if (matter == null) {
return;
synchronized (chunk) {
Matter matter = chunk.get(section);
if (matter == null) {
return;
}
if (hasProtectedHydrology(matter, x, y, z)) {
return;
}
if (matter.hasSlice(PlatformBlockState.class)) {
capturePreObjectOriginal(matter, x, y, z, PlatformBlockState.class);
matter.<PlatformBlockState>getSlice(PlatformBlockState.class)
.set(x & 15, y & 15, z & 15, null);
}
clearDeferredPlacement(matter, x, y, z);
}
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);
}
clearDeferredPlacement(matter, x, y, z);
}
public <T> T getData(int x, int y, int z, Class<T> type) {
@@ -355,6 +430,159 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
return matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
}
public <T> T getPrerequisiteDataIfPresent(int x, int y, int z, Class<T> type) {
Objects.requireNonNull(type, "type");
if (y < 0 || y >= mantle.getWorldHeight()) {
return null;
}
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
int section = y >> 4;
if (chunk == null) {
return null;
}
synchronized (chunk) {
if (!chunk.exists(section)) {
return null;
}
Matter matter = chunk.get(section);
if (matter == null) {
return null;
}
return prerequisiteValue(matter, x, y, z, type);
}
}
public PlatformBlockState getPrerequisiteBlock(int x, int y, int z) {
PlatformBlockState block = getPrerequisiteDataIfPresent(x, y, z, PlatformBlockState.class);
return block == null ? AIR : block;
}
public boolean isPrerequisiteCarved(int x, int y, int z) {
HydrologyCaveCell hydrology = getPrerequisiteDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.carves();
}
return getPrerequisiteDataIfPresent(x, y, z, MatterCavern.class) != null;
}
public byte[] getPrerequisiteCarvedColumn(int x, int z, int height) {
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) {
return carvedColumn;
}
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
if (chunk == null) {
return carvedColumn;
}
synchronized (chunk) {
int lastSection = (cappedHeight - 1) >> 4;
for (int section = 0; section <= lastSection; section++) {
if (!chunk.exists(section)) {
continue;
}
Matter matter = chunk.get(section);
if (matter == null) {
continue;
}
MatterSlice<PreObjectMatterCell> journalSlice = matter.hasSlice(PreObjectMatterCell.class)
? matter.getSlice(PreObjectMatterCell.class)
: null;
MatterSlice<MatterCavern> cavernSlice = matter.hasSlice(MatterCavern.class)
? matter.getSlice(MatterCavern.class)
: null;
MatterSlice<HydrologyCaveCell> hydrologySlice = matter.hasSlice(HydrologyCaveCell.class)
? matter.getSlice(HydrologyCaveCell.class)
: null;
int sectionBaseY = section << 4;
int sectionMaxY = Math.min(cappedHeight, sectionBaseY + 16);
for (int y = sectionBaseY; y < sectionMaxY; y++) {
int localY = y & 15;
HydrologyCaveCell hydrology = hydrologySlice == null
? null
: hydrologySlice.get(x & 15, localY, z & 15);
if (hydrology != null) {
carvedColumn[y] = hydrology.carves() ? (byte) 1 : 0;
continue;
}
PreObjectMatterCell cell = journalSlice == null
? null
: journalSlice.get(x & 15, localY, z & 15);
MatterCavern cavern = cell != null && cell.cavernCaptured()
? cell.cavern()
: cavernSlice == null ? null : cavernSlice.get(x & 15, localY, z & 15);
if (cavern != null) {
carvedColumn[y] = 1;
}
}
}
}
return carvedColumn;
}
public boolean restorePrerequisiteData(int x, int y, int z, Class<?> type) {
Objects.requireNonNull(type, "type");
if (y < 0 || y >= mantle.getWorldHeight() || !isPreObjectType(type)) {
return false;
}
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
int section = y >> 4;
if (chunk == null) {
return false;
}
synchronized (chunk) {
if (!chunk.exists(section)) {
return false;
}
Matter matter = chunk.get(section);
PreObjectMatterCell cell = preObjectCell(matter, x, y, z);
if (cell == null || !cell.captures(type)) {
return false;
}
restoreRaw(matter, x, y, z, type, cell.original(type));
return true;
}
}
public boolean restorePrerequisiteCell(int x, int y, int z) {
if (y < 0 || y >= mantle.getWorldHeight()) {
return false;
}
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
int section = y >> 4;
if (chunk == null) {
return false;
}
synchronized (chunk) {
if (!chunk.exists(section)) {
return false;
}
Matter matter = chunk.get(section);
PreObjectMatterCell cell = preObjectCell(matter, x, y, z);
if (cell == null) {
return false;
}
if (cell.blockCaptured()) {
restoreRaw(matter, x, y, z, PlatformBlockState.class, cell.block());
}
if (cell.stringCaptured()) {
restoreRaw(matter, x, y, z, String.class, cell.string());
}
if (cell.cavernCaptured()) {
restoreRaw(matter, x, y, z, MatterCavern.class, cell.cavern());
}
return true;
}
}
public void clearData(int x, int y, int z, Class<?> type) {
if (y < 0 || y >= mantle.getWorldHeight()) {
return;
@@ -366,22 +594,25 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
return;
}
Matter matter = chunk.get(section);
if (matter == null || !matter.hasSlice(type)) {
return;
synchronized (chunk) {
Matter matter = chunk.get(section);
if (matter == null || !matter.hasSlice(type)) {
return;
}
if ((type == PlatformBlockState.class || type == MatterCavern.class)
&& hasProtectedHydrology(matter, x, y, z)) {
return;
}
capturePreObjectOriginal(matter, x, y, z, type);
matter.getSlice(type).set(x & 15, y & 15, z & 15, null);
}
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)) {
if (!matter.hasSlice(HydrologyCaveCell.class)) {
return false;
}
RiverCaveHydrology hydrology = matter.<RiverCaveHydrology>getSlice(RiverCaveHydrology.class)
HydrologyCaveCell hydrology = matter.<HydrologyCaveCell>getSlice(HydrologyCaveCell.class)
.get(x & 15, y & 15, z & 15);
return hydrology != null && hydrology.protectsPlacement();
}
@@ -392,6 +623,108 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
}
}
private void setCustomBlock(int x, int y, int z, PlatformBlockState baseState, Identifier identifier) {
if (y < 0 || y >= mantle.getWorldHeight()) {
return;
}
if (y == 0 && engineMantle.getEngine().getDimension().isBedrock()) {
return;
}
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
if (chunk == null) {
return;
}
synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return;
}
MatterSlice<PlatformBlockState> blockSlice = matter.slice(PlatformBlockState.class);
MatterSlice<Identifier> identifierSlice = matter.slice(Identifier.class);
capturePreObjectOriginal(matter, x, y, z, PlatformBlockState.class);
blockSlice.set(x & 15, y & 15, z & 15, baseState);
identifierSlice.set(x & 15, y & 15, z & 15, identifier);
}
}
private void capturePreObjectOriginal(Matter matter, int x, int y, int z, Class<?> type) {
Integer priority = activeComponentPriority.get();
if (priority == null || priority < OBJECT_COMPONENT_PRIORITY || !isPreObjectType(type)) {
return;
}
int localX = x & 15;
int localY = y & 15;
int localZ = z & 15;
MatterSlice<PreObjectMatterCell> journal = matter.slice(PreObjectMatterCell.class);
PreObjectMatterCell current = journal.get(localX, localY, localZ);
if (current != null && current.captures(type)) {
return;
}
Object original = rawValue(matter, localX, localY, localZ, type);
PreObjectMatterCell updated;
if (type == PlatformBlockState.class) {
updated = current == null
? PreObjectMatterCell.block((PlatformBlockState) original)
: current.captureBlock((PlatformBlockState) original);
} else if (type == String.class) {
updated = current == null
? PreObjectMatterCell.string((String) original)
: current.captureString((String) original);
} else {
updated = current == null
? PreObjectMatterCell.cavern((MatterCavern) original)
: current.captureCavern((MatterCavern) original);
}
journal.set(localX, localY, localZ, updated);
}
private static boolean isPreObjectType(Class<?> type) {
return type == PlatformBlockState.class || type == String.class || type == MatterCavern.class;
}
private static PreObjectMatterCell preObjectCell(Matter matter, int x, int y, int z) {
if (matter == null || !matter.hasSlice(PreObjectMatterCell.class)) {
return null;
}
return matter.<PreObjectMatterCell>getSlice(PreObjectMatterCell.class)
.get(x & 15, y & 15, z & 15);
}
private static <T> T prerequisiteValue(Matter matter, int x, int y, int z, Class<T> type) {
PreObjectMatterCell cell = preObjectCell(matter, x, y, z);
if (cell != null && cell.captures(type)) {
return cell.original(type);
}
if (!matter.hasSlice(type)) {
return null;
}
return matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
}
private static Object rawValue(Matter matter, int x, int y, int z, Class<?> type) {
if (!matter.hasSlice(type)) {
return null;
}
return matter.getSlice(type).get(x, y, z);
}
@SuppressWarnings("unchecked")
private static void restoreRaw(Matter matter, int x, int y, int z, Class<?> type, Object value) {
MatterSlice<Object> slice;
if (value == null) {
if (!matter.hasSlice(type)) {
return;
}
slice = (MatterSlice<Object>) matter.getSlice(type);
} else {
slice = (MatterSlice<Object>) matter.slice(type);
}
slice.set(x & 15, y & 15, z & 15, value);
}
@ChunkCoordinates
public MantleChunk<Matter> acquireChunk(int cx, int cz) {
int index = windowIndex(cx, cz);
@@ -456,8 +789,8 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
String placementKey = s.deferredPlacementKey();
PlatformBlockState baseState = s.placementBaseState();
if (s.isCustom() && placementKey != null && baseState != null) {
setData(x, y, z, baseState);
setData(x, y, z, Identifier.fromString(placementKey));
Identifier identifier = Identifier.fromString(placementKey);
setCustomBlock(x, y, z, baseState, identifier);
return;
}
setData(x, y, z, s);
@@ -480,7 +813,7 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
@Override
public boolean isCarved(int x, int y, int z) {
RiverCaveHydrology hydrology = getDataIfPresent(x, y, z, RiverCaveHydrology.class);
HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.carves();
}
@@ -515,8 +848,8 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
MatterSlice<MatterCavern> cavernSlice = matter.hasSlice(MatterCavern.class)
? matter.getSlice(MatterCavern.class)
: null;
MatterSlice<RiverCaveHydrology> hydrologySlice = matter.hasSlice(RiverCaveHydrology.class)
? matter.getSlice(RiverCaveHydrology.class)
MatterSlice<HydrologyCaveCell> hydrologySlice = matter.hasSlice(HydrologyCaveCell.class)
? matter.getSlice(HydrologyCaveCell.class)
: null;
if (cavernSlice == null && hydrologySlice == null) {
continue;
@@ -524,7 +857,7 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
int sectionBaseY = section << 4;
int sectionMaxY = Math.min(cappedHeight, sectionBaseY + 16);
for (int y = sectionBaseY; y < sectionMaxY; y++) {
RiverCaveHydrology hydrology = hydrologySlice == null
HydrologyCaveCell hydrology = hydrologySlice == null
? null
: hydrologySlice.get(localX, y & 15, localZ);
if (hydrology != null) {
@@ -1,6 +1,7 @@
package art.arcane.iris.engine.mantle;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.ChunkContext;
@@ -43,6 +44,10 @@ public interface MatterGenerator {
if (!getEngine().getDimension().isUseMantle()) {
return;
}
generateMatterWindow(x, z, multicore, context);
}
private void generateMatterWindow(int x, int z, boolean multicore, ChunkContext context) {
MatterGenerationPlan generationPlan = resolveGenerationPlan(x, z, context);
MatterPassPlan[] passPlans = generationPlan.passPlans();
@@ -74,10 +79,10 @@ public interface MatterGenerator {
int[] componentPassRadii = new int[passComponents.size()];
int enabledComponentCount = 0;
for (MantleComponent component : passComponents) {
if (component.isEnabled()) {
if (shouldGenerateComponent(component)) {
// A component must cover its own reach plus every later pass' reach, or a
// later pass reads this component's data from chunks it never wrote.
int componentReach = component.getRadius() + passPlan.downstreamBlockRadius();
int componentReach = component.getOutputRadius() + passPlan.downstreamBlockRadius();
componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0;
enabledComponents[enabledComponentCount++] = component;
}
@@ -89,7 +94,8 @@ public interface MatterGenerator {
// A dispatcher thread runs its components inline: dispatching from inside the pool
// only trades a worker for a blocked worker.
boolean asyncComponents = multicore && !DISPATCHER.ownsCurrentThread();
boolean asyncComponents = multicore
&& !DISPATCHER.ownsCurrentThread();
if (asyncComponents && outstandingTasks == null) {
outstandingTasks = new ArrayList<>();
}
@@ -222,12 +228,12 @@ public interface MatterGenerator {
int passAccessInputRadius = 0;
int passGenerationInputRadius = 0;
for (MantleComponent component : pass.components()) {
if (!component.isEnabled()) {
if (!shouldGenerateComponent(component)) {
continue;
}
int componentRadius = component.getRadius();
passBlockRadius = Math.max(passBlockRadius, componentRadius);
int componentReach = componentRadius + generationDownstreamBlockRadius;
int componentOutputRadius = component.getOutputRadius();
passBlockRadius = Math.max(passBlockRadius, componentOutputRadius);
int componentReach = componentOutputRadius + generationDownstreamBlockRadius;
int invocationChunkRadius = componentReach > 0
? Math.ceilDiv(componentReach, 16)
: 0;
@@ -250,6 +256,15 @@ public interface MatterGenerator {
return new MatterGenerationPlan(plans, writerChunkRadius * 2);
}
private boolean shouldGenerateComponent(MantleComponent component) {
if (!component.isEnabled()) {
return false;
}
Engine engine = getEngine();
EnginePlatformHooks hooks = engine.getPlatformHooks();
return hooks == null || hooks.shouldGenerateMantleComponent(engine, component);
}
private MatterComponentTask runComponentAsync(
MantleChunk<Matter> chunk,
MantleComponent component,
@@ -438,7 +453,10 @@ public interface MatterGenerator {
int chunkZ,
ChunkContext context
) {
chunk.raiseFlagSuspend(component.getFlag(), () -> component.generateLayer(writer, chunkX, chunkZ, context));
chunk.raiseFlagSuspend(component.getFlag(), () -> writer.withComponentPriority(
component.getPriority(),
() -> component.generateLayer(writer, chunkX, chunkZ, context)
));
}
/**
@@ -23,7 +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.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KList;
@@ -72,8 +72,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
discard();
return CommitResult.REJECTED_BOUNDS;
}
RiverCaveHydrology hydrology = delegate.getData(
mutation.x(), mutation.y(), mutation.z(), RiverCaveHydrology.class);
HydrologyCaveCell hydrology = delegate.getData(
mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class);
if (hydrology != null && hydrology.protectsPlacement()) {
discard();
return CommitResult.REJECTED_HYDROLOGY;
@@ -1,69 +0,0 @@
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;
private final double boundaryVariation;
ConfiguredRiverGrottoShape(
long seed,
IrisData data,
IrisGeneratorStyle shapeStyle,
IrisGeneratorStyle warpStyle,
double warpStrength,
double boundaryVariation
) {
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);
this.boundaryVariation = Math.max(0D, Math.min(0.75D, boundaryVariation));
}
@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(-boundaryVariation, boundaryVariation, worldX, worldY, worldZ);
return normalized <= 1D + boundary;
}
}
@@ -472,12 +472,12 @@ public class MantleCarvingComponent extends IrisMantleComponent {
}
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights);
fillFieldHeights(complex.getNaturalHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
Arrays.fill(blendScratch.fieldFluidHeights, complex.getFluidHeight());
fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights,
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
fillFieldObjects(complex.getNaturalTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes);
}
@@ -511,7 +511,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
target[fieldIndex] = B.isFluid(complex.resolveSurfaceFluid(worldX, startZ + fieldZ))
target[fieldIndex] = B.isFluid(complex.getFluidStream().get(worldX, startZ + fieldZ))
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
}
}
@@ -545,36 +545,26 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return 1;
}
private int[] prepareChunkSurfaceHeights(int chunkX, int chunkZ, ChunkContext context, int[] scratch) {
static int[] prepareChunkSurfaceHeights(int chunkX, int chunkZ, ChunkContext context, int[] scratch) {
int[] surfaceHeights = scratch;
int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ);
boolean useContextHeight = context != null
&& context.getHeight() != null
&& context.getX() == baseX
&& context.getZ() == baseZ;
ProceduralStream<Double> naturalHeight = context.getComplex().getNaturalHeightStream();
double[] cachedChunkHeights = null;
if (!useContextHeight && context != null) {
ProceduralStream<Double> heightStream = context.getComplex().getHeightStream();
if (heightStream instanceof ChunkFillableDoubleStream2D cachedHeightStream) {
cachedChunkHeights = BLEND_SCRATCH.get().chunkSurfaceHeightSamples;
cachedHeightStream.fillChunkDoubles(baseX, baseZ, cachedChunkHeights);
}
if (naturalHeight instanceof ChunkFillableDoubleStream2D cachedHeightStream) {
cachedChunkHeights = BLEND_SCRATCH.get().chunkSurfaceHeightSamples;
cachedHeightStream.fillChunkDoubles(baseX, baseZ, cachedChunkHeights);
}
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
int worldX = baseX + localX;
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
int worldZ = baseZ + localZ;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
if (useContextHeight) {
surfaceHeights[columnIndex] = context.getRoundedHeight(localX, localZ);
continue;
}
if (cachedChunkHeights != null) {
surfaceHeights[columnIndex] = (int) Math.round(cachedChunkHeights[(localZ << 4) + localX]);
continue;
}
surfaceHeights[columnIndex] = getEngineMantle().getEngine().getHeight(worldX, worldZ);
surfaceHeights[columnIndex] = (int) Math.round(naturalHeight.getDouble(worldX, worldZ));
}
}
return surfaceHeights;
@@ -0,0 +1,265 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.hydrology.HydrologyCaveVoxelViewFactory;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.function.Function2;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Objects;
import java.util.function.BiConsumer;
public final class MantleHydrologyCaveVoxelView implements CaveVoxelView {
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 BiConsumer<Integer, Integer> chunkLoader;
private final LongOpenHashSet loadedChunks;
private final Long2IntOpenHashMap openFloorCache;
private final Long2IntOpenHashMap surfaceHeightCache;
public MantleHydrologyCaveVoxelView(
Engine engine,
IrisComplex complex,
RiverFootprint footprint
) {
this(
Objects.requireNonNull(engine).getMantle(),
Objects.requireNonNull(complex),
Objects.requireNonNull(footprint)
);
}
public MantleHydrologyCaveVoxelView(
Engine engine,
IrisComplex complex,
HydrologyCaveVoxelViewFactory.PlannedSurface plannedSurface
) {
this(
Objects.requireNonNull(engine).getMantle(),
Objects.requireNonNull(complex),
Objects.requireNonNull(plannedSurface)
);
}
private MantleHydrologyCaveVoxelView(
EngineMantle engineMantle,
IrisComplex complex,
RiverFootprint footprint
) {
this(
engineMantle.getMantle(),
engineMantle.getMantle().getWorldHeight(),
(x, z) -> plannedSurfaceHeight(complex, footprint, x, z),
(chunkX, chunkZ) -> generateCarvingInput(engineMantle, complex, chunkX, chunkZ)
);
}
private MantleHydrologyCaveVoxelView(
EngineMantle engineMantle,
IrisComplex complex,
HydrologyCaveVoxelViewFactory.PlannedSurface plannedSurface
) {
this(
engineMantle.getMantle(),
engineMantle.getMantle().getWorldHeight(),
(x, z) -> plannedSurface.resolve(
x,
z,
(int) Math.round(complex.getNaturalHeightStream().getDouble(x, z))
),
(chunkX, chunkZ) -> generateCarvingInput(engineMantle, complex, chunkX, chunkZ)
);
}
MantleHydrologyCaveVoxelView(
Mantle<Matter> mantle,
int worldHeight,
Function2<Integer, Integer, Integer> surfaceHeight,
BiConsumer<Integer, Integer> chunkLoader
) {
this.mantle = Objects.requireNonNull(mantle);
if (worldHeight < 3) {
throw new IllegalArgumentException("worldHeight must be at least three");
}
this.worldHeight = worldHeight;
this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
this.chunkLoader = Objects.requireNonNull(chunkLoader);
this.loadedChunks = new LongOpenHashSet();
this.openFloorCache = new Long2IntOpenHashMap();
this.openFloorCache.defaultReturnValue(CACHE_MISS);
this.surfaceHeightCache = new Long2IntOpenHashMap();
this.surfaceHeightCache.defaultReturnValue(CACHE_MISS);
}
@Override
public boolean isInWorld(CavePosition position) {
return position.y() > 0 && position.y() < worldHeight - 1;
}
@Override
public CaveVoxel voxelAt(CavePosition position) {
Objects.requireNonNull(position);
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) {
if (!block.isFluid()) {
return CaveVoxel.SOLID;
}
return IrisProceduralBlocks.materialKey(block).endsWith(":lava")
? CaveVoxel.LAVA
: CaveVoxel.INCOMPATIBLE_FLUID;
}
return position.y() > surfaceY(position.x(), position.z())
? CaveVoxel.CAVE_AIR
: CaveVoxel.SOLID;
}
@Override
public boolean isOpenToSurface(CavePosition position) {
if (!isInWorld(position) || voxelAt(position) == CaveVoxel.SOLID) {
return false;
}
if (isAboveTerrainSurface(position)) {
return true;
}
long key = RiverFootprint.pack(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 boolean isAboveTerrainSurface(CavePosition position) {
return position.y() > surfaceY(position.x(), position.z());
}
@Override
public boolean hasAboveTerrainSurface(int x, int z, int minimumY, int maximumY) {
int firstY = Math.max(minimumY, 1);
int lastY = Math.min(maximumY, worldHeight - 2);
return firstY <= lastY && lastY > surfaceY(x, z);
}
private int resolveOpenFloor(int x, int z) {
int top = surfaceY(x, z);
if (voxelAt(new CavePosition(x, top, z)) == 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 = RiverFootprint.pack(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;
long chunkKey = Mantle.key(chunkX, chunkZ);
if (loadedChunks.add(chunkKey)) {
chunkLoader.accept(chunkX, chunkZ);
}
return mantle.get(position.x(), position.y(), position.z(), type);
}
private static int plannedSurfaceHeight(
IrisComplex complex,
RiverFootprint footprint,
int x,
int z
) {
HydrologyColumnSample sample = footprint.sample(x, z).orElse(null);
return sample == null
? (int) Math.round(complex.getNaturalHeightStream().getDouble(x, z))
: sample.terrainHeight();
}
static void generateCarvingInput(
EngineMantle engineMantle,
IrisComplex complex,
int chunkX,
int chunkZ
) {
MantleComponent carving = engineMantle.getRegisteredComponents().get(ReservedFlag.CARVED);
if (carving == null || !carving.isEnabled()) {
throw new IllegalStateException("Hydrology containment requires the carving component");
}
if (!requiresCarvingInput(engineMantle.getMantle(), chunkX, chunkZ)) {
return;
}
ChunkContext context = new ChunkContext(
chunkX << 4,
chunkZ << 4,
complex,
false,
ChunkContext.PrefillPlan.NONE,
null
);
try (MantleWriter writer = new MantleWriter(
engineMantle,
engineMantle.getMantle(),
chunkX,
chunkZ,
0,
false
)) {
MantleChunk<Matter> chunk = writer.acquireChunk(chunkX, chunkZ);
if (chunk == null) {
throw new IllegalStateException("Hydrology containment could not acquire carving input at "
+ chunkX + "," + chunkZ);
}
chunk.raiseFlagSuspend(
ReservedFlag.CARVED,
() -> carving.generateLayer(writer, chunkX, chunkZ, context)
);
}
}
static boolean requiresCarvingInput(Mantle<Matter> mantle, int chunkX, int chunkZ) {
return !mantle.hasFlag(chunkX, chunkZ, ReservedFlag.CARVED);
}
}
Loaded 100 of 352 files, more files were not shown because too many files have changed in this diff. Show more