mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-19 23:14:10 +00:00
Merge terrain-first hydrology into master
Surface rivers v2: valley-first eroded channels, spring pools, standing pools, tributaries, bed padding, ring-ordered feature search, schema v2 hydrology configuration and the transect probe.
This commit is contained in:
+85
-2
@@ -62,6 +62,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private volatile KMap<String, Holder<Biome>> customBiomes;
|
||||
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||
private volatile IrisDimension cacheDimension;
|
||||
@@ -331,8 +332,18 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
if (quartStep == 1) {
|
||||
return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler);
|
||||
}
|
||||
return super.findBiomeHorizontal(
|
||||
x, y, z, searchRadius, quartStep, allowed, random, false, sampler);
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_ring_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure ring biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
return findNaturalSurfaceBiomeHorizontal(
|
||||
x, y, z, searchRadius, quartStep, allowed, random);
|
||||
}
|
||||
}
|
||||
|
||||
static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
|
||||
@@ -390,6 +401,60 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return resolvedSurfaceHolder;
|
||||
}
|
||||
|
||||
private Pair<BlockPos, Holder<Biome>> findNaturalSurfaceBiomeHorizontal(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
int searchRadius,
|
||||
int quartStep,
|
||||
Predicate<Holder<Biome>> allowed,
|
||||
RandomSource random
|
||||
) {
|
||||
int centerQuartX = QuartPos.fromBlock(x);
|
||||
int centerQuartZ = QuartPos.fromBlock(z);
|
||||
int quartRadius = QuartPos.fromBlock(searchRadius);
|
||||
Pair<BlockPos, Holder<Biome>> selected = null;
|
||||
int matches = 0;
|
||||
for (int radius = 0; radius <= quartRadius; radius += quartStep) {
|
||||
for (int offsetZ = -radius; offsetZ <= radius; offsetZ += quartStep) {
|
||||
for (int offsetX = -radius; offsetX <= radius; offsetX += quartStep) {
|
||||
int quartX = centerQuartX + offsetX;
|
||||
int quartZ = centerQuartZ + offsetZ;
|
||||
Holder<Biome> holder = getNaturalSurfaceStructureBiomeHolder(quartX, quartZ);
|
||||
if (!allowed.test(holder)) {
|
||||
continue;
|
||||
}
|
||||
if (selected == null || random.nextInt(matches + 1) == 0) {
|
||||
selected = Pair.of(new BlockPos(
|
||||
QuartPos.toBlock(quartX),
|
||||
y,
|
||||
QuartPos.toBlock(quartZ)), holder);
|
||||
}
|
||||
matches++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private Holder<Biome> getNaturalSurfaceStructureBiomeHolder(int x, int z) {
|
||||
long columnKey = packColumnKey(x, z);
|
||||
Holder<Biome> cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
Holder<Biome> resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z);
|
||||
Holder<Biome> existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent(
|
||||
columnKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
naturalSurfaceStructureBiomeCache.clear();
|
||||
}
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private boolean isGuaranteedSurfaceBiome(int quartY) {
|
||||
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
|
||||
return false;
|
||||
@@ -417,6 +482,23 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return holder;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveNaturalSurfaceStructureBiomeHolder(int x, int z) {
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
IrisBiome irisBiome = engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ);
|
||||
if (irisBiome == null) {
|
||||
throw new IllegalStateException("Iris returned no natural structure biome at block "
|
||||
+ blockX + "," + blockZ);
|
||||
}
|
||||
Holder<Biome> holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey());
|
||||
if (holder == null) {
|
||||
throw new IllegalStateException("Iris natural structure biome derivative '"
|
||||
+ irisBiome.getStructureDerivativeKey() + "' is not registered at block "
|
||||
+ blockX + "," + blockZ);
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
|
||||
if (lease == null) {
|
||||
@@ -506,6 +588,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
noiseBiomeCache.clear();
|
||||
structureBiomeCache.clear();
|
||||
surfaceStructureBiomeCache.clear();
|
||||
naturalSurfaceStructureBiomeCache.clear();
|
||||
customBiomes = refreshedCustomBiomes;
|
||||
vanillaSpawnBiomes = refreshedSpawnBiomes;
|
||||
cacheDimension = dimension;
|
||||
|
||||
+28
-1
@@ -45,9 +45,36 @@ public class CustomBiomeSourceStructureContractTest {
|
||||
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_Y = 0"));
|
||||
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112"));
|
||||
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4"));
|
||||
assertTrue(source.contains("x, y, z, searchRadius, quartStep, allowed, random, false, sampler"));
|
||||
assertTrue(source.contains(
|
||||
"return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler)"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_ring_biome\")"));
|
||||
assertTrue(source.contains("findNaturalSurfaceBiomeHorizontal("));
|
||||
assertTrue(source.contains("radius += quartStep"));
|
||||
assertTrue(source.contains("offsetZ += quartStep"));
|
||||
assertTrue(source.contains("offsetX += quartStep"));
|
||||
assertTrue(source.contains("random.nextInt(matches + 1) == 0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyConcentricRingSuitabilityUsesTheNaturalTerrainStream() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
int realResolutionStart = source.indexOf("private Holder<Biome> resolveSurfaceStructureBiomeHolder(");
|
||||
int naturalResolutionStart = source.indexOf(
|
||||
"private Holder<Biome> resolveNaturalSurfaceStructureBiomeHolder(");
|
||||
int resolutionEnd = source.indexOf("public Holder<Biome> getVisibleNoiseBiome(", naturalResolutionStart);
|
||||
|
||||
assertTrue(realResolutionStart >= 0);
|
||||
assertTrue(naturalResolutionStart > realResolutionStart);
|
||||
assertTrue(resolutionEnd > naturalResolutionStart);
|
||||
String realResolution = source.substring(realResolutionStart, naturalResolutionStart);
|
||||
String naturalResolution = source.substring(naturalResolutionStart, resolutionEnd);
|
||||
|
||||
assertTrue(realResolution.contains("engine.getComplex().getTrueBiomeStream().get(blockX, blockZ)"));
|
||||
assertFalse(realResolution.contains("getNaturalTrueBiomeStream()"));
|
||||
assertTrue(naturalResolution.contains(
|
||||
"engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ)"));
|
||||
assertFalse(naturalResolution.contains("getTrueBiomeStream()"));
|
||||
assertFalse(source.contains("studioBootstrapSurfaceStructureBiomeCache"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+6
-1
@@ -2,6 +2,11 @@ package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisColumnField {
|
||||
SURFACE_HEIGHT,
|
||||
NATURAL_HEIGHT,
|
||||
SURFACE_KIND,
|
||||
BIOME_KEY
|
||||
BIOME_KEY,
|
||||
RIVER_STATE,
|
||||
RIVER_DISTANCE,
|
||||
RIVER_FLOW,
|
||||
RIVER_WATER_SURFACE_Y
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record IrisColumnSample(
|
||||
int blockX,
|
||||
int blockZ,
|
||||
int surfaceHeight,
|
||||
int naturalHeight,
|
||||
IrisSurfaceKind surfaceKind,
|
||||
String biomeKey,
|
||||
IrisRiverState riverState,
|
||||
double riverDistance,
|
||||
int riverFlow,
|
||||
int riverWaterSurfaceY
|
||||
) {
|
||||
public static final int UNAVAILABLE_HEIGHT = Integer.MIN_VALUE;
|
||||
public static final double UNAVAILABLE_RIVER_DISTANCE = Double.NaN;
|
||||
public static final int UNAVAILABLE_RIVER_FLOW = -1;
|
||||
|
||||
public IrisColumnSample {
|
||||
Objects.requireNonNull(surfaceKind, "surfaceKind");
|
||||
Objects.requireNonNull(riverState, "riverState");
|
||||
biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey;
|
||||
if (!Double.isNaN(riverDistance) && (!Double.isFinite(riverDistance) || riverDistance < 0D)) {
|
||||
throw new IllegalArgumentException("riverDistance must be non-negative, finite, or unavailable");
|
||||
}
|
||||
if (riverFlow < UNAVAILABLE_RIVER_FLOW) {
|
||||
throw new IllegalArgumentException("riverFlow must be non-negative or unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSurfaceHeight() {
|
||||
return surfaceHeight != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
|
||||
public boolean hasNaturalHeight() {
|
||||
return naturalHeight != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
|
||||
public boolean hasSurfaceKind() {
|
||||
return surfaceKind != IrisSurfaceKind.UNKNOWN;
|
||||
}
|
||||
|
||||
public boolean hasBiomeKey() {
|
||||
return biomeKey != null;
|
||||
}
|
||||
|
||||
public boolean hasRiverState() {
|
||||
return riverState != IrisRiverState.NONE;
|
||||
}
|
||||
|
||||
public boolean hasRiverDistance() {
|
||||
return !Double.isNaN(riverDistance);
|
||||
}
|
||||
|
||||
public boolean hasRiverFlow() {
|
||||
return riverFlow != UNAVAILABLE_RIVER_FLOW;
|
||||
}
|
||||
|
||||
public boolean hasRiverWaterSurfaceY() {
|
||||
return riverWaterSurfaceY != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,5 +2,5 @@ package art.arcane.iris.api.terrain;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface IrisColumnSink {
|
||||
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
|
||||
void accept(IrisColumnSample sample);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisRiverState {
|
||||
NONE,
|
||||
WET,
|
||||
DRY
|
||||
}
|
||||
@@ -4,6 +4,9 @@ public enum IrisSurfaceKind {
|
||||
UNKNOWN,
|
||||
LAND,
|
||||
SHORE,
|
||||
RIVER,
|
||||
RIVER_SHORE,
|
||||
DRY_CHANNEL,
|
||||
OCEAN,
|
||||
VOID
|
||||
}
|
||||
|
||||
+75
-1
@@ -28,6 +28,9 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
@@ -37,7 +40,9 @@ import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.HydrologyTypeHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
@@ -68,7 +73,7 @@ import java.util.Set;
|
||||
public class CommandFind implements DirectorExecutor {
|
||||
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
||||
public void biome(
|
||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look")
|
||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
||||
IrisBiome biome,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
@@ -116,6 +121,75 @@ public class CommandFind implements DirectorExecutor {
|
||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find an accepted hydrology feature")
|
||||
public void river(
|
||||
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class)
|
||||
String type,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine activeEngine = engine();
|
||||
VolmitSender commandSender = sender();
|
||||
Player target = player();
|
||||
if (activeEngine == null || commandSender == null || target == null) {
|
||||
if (commandSender != null) {
|
||||
commandSender.sendMessage(C.RED + "Run this command from an Iris world.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
||||
if (runtime == null) {
|
||||
commandSender.sendMessage(C.YELLOW + "Hydrology is not active in this world.");
|
||||
return;
|
||||
}
|
||||
HydrologyFeatureQuery query;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
} catch (IllegalArgumentException error) {
|
||||
commandSender.sendMessage(C.RED + error.getMessage());
|
||||
return;
|
||||
}
|
||||
Location origin = target.getLocation();
|
||||
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime.maximumFeatureSearchDistance(
|
||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
||||
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
||||
J.a(() -> {
|
||||
try {
|
||||
HydrologyFeatureRef feature = runtime.nearestFeature(
|
||||
query.types(),
|
||||
query.profileKey(),
|
||||
origin.getBlockX(),
|
||||
origin.getBlockZ(),
|
||||
maximumDistance,
|
||||
(int visited) -> sendStructureMessage(target, commandSender,
|
||||
C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...")
|
||||
).orElse(null);
|
||||
if (feature == null) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No accepted " + type + " hydrology feature found within "
|
||||
+ maximumDistance + " blocks.");
|
||||
return;
|
||||
}
|
||||
int worldY = feature.y() + activeEngine.getDimension().getMinHeight();
|
||||
String label = type + " hydrology feature";
|
||||
if (!teleport) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + worldY + ", " + feature.z() + ".");
|
||||
return;
|
||||
}
|
||||
Location destination = new Location(
|
||||
target.getWorld(), feature.x(), worldY, feature.z());
|
||||
prepareStructureTeleport(
|
||||
target, target.getWorld(), commandSender, label, destination, true);
|
||||
} catch (Throwable error) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate accepted hydrology feature '" + type + "'.", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", descriptionKey = "iris.director.commandfind.director.find_structure_vanilla_key_like_minecraft_village_plains_minecraft_stronghold_imported_iris", sync = true)
|
||||
public void structure(
|
||||
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", descriptionKey = "iris.director.commandfind.param.structure_look_e_g_minecraft_village_plains_minecraft_stronghold_minecraft_ancient_city", customHandler = StructureHandler.class)
|
||||
|
||||
@@ -25,7 +25,10 @@ import art.arcane.iris.core.pack.PackValidationCache;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
@@ -90,6 +93,18 @@ public class CommandPack implements DirectorExecutor {
|
||||
persistValidationCache(packsRoot);
|
||||
}
|
||||
|
||||
@Director(description = "Package a validated dimension into a compressed format", descriptionKey = "iris.director.commandstudio.director.package_dimension_into_compressed_format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", descriptionKey = "iris.director.commandstudio.param.dimension_pack_compress", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_obfuscate_pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@Param(name = "minify", description = "Whether or not to minify the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_minify_pack", defaultValue = "true")
|
||||
boolean minify
|
||||
) {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Director(description = "Preview or apply unused-resource cleanup", descriptionKey = "iris.director.commandpack.director.preview_apply_unused_resource_cleanup", aliases = {"c"})
|
||||
public void cleanup(
|
||||
@Param(description = "The pack folder name to clean", descriptionKey = "iris.director.commandpack.param.pack_folder_name_clean")
|
||||
|
||||
@@ -420,18 +420,6 @@ public class CommandStudio implements DirectorExecutor {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
||||
}
|
||||
|
||||
@Director(description = "Package a dimension into a compressed format", descriptionKey = "iris.director.commandstudio.director.package_dimension_into_compressed_format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", descriptionKey = "iris.director.commandstudio.param.dimension_pack_compress", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_obfuscate_pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@Param(name = "minify", description = "Whether or not to minify the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_minify_pack", defaultValue = "true")
|
||||
boolean minify
|
||||
) {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
|
||||
public void profile(
|
||||
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
|
||||
+1
-1
@@ -318,7 +318,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File file = switch (type) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
|
||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
|
||||
+77
-5
@@ -2,7 +2,9 @@ package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisColumnField;
|
||||
import art.arcane.iris.api.terrain.IrisColumnQuery;
|
||||
import art.arcane.iris.api.terrain.IrisColumnSample;
|
||||
import art.arcane.iris.api.terrain.IrisColumnSink;
|
||||
import art.arcane.iris.api.terrain.IrisRiverState;
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
||||
import art.arcane.iris.api.terrain.IrisWorldInfo;
|
||||
@@ -13,6 +15,8 @@ import art.arcane.iris.core.service.terrain.IrisSampleLimits;
|
||||
import art.arcane.iris.core.service.terrain.IrisSurfaceClassifier;
|
||||
import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory;
|
||||
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.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
@@ -101,13 +105,15 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
|
||||
try {
|
||||
int surface = engine.getHeight(blockX, blockZ);
|
||||
int fluid = engine.getDimension().getFluidHeight();
|
||||
HydrologyColumnSample hydrology = hydrologySample(engine, blockX, blockZ);
|
||||
int fluid = (int) Math.round(
|
||||
engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
||||
InferredType inferredType = null;
|
||||
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
|
||||
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
||||
inferredType = biome == null ? null : biome.getInferredType();
|
||||
}
|
||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType);
|
||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, hydrology);
|
||||
} catch (Throwable error) {
|
||||
reportQueryFault("surfaceKind", world, error);
|
||||
return IrisSurfaceKind.UNKNOWN;
|
||||
@@ -224,26 +230,75 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
|
||||
EnumSet<IrisColumnField> fields = query.fields();
|
||||
boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT);
|
||||
boolean wantNaturalHeight = fields.contains(IrisColumnField.NATURAL_HEIGHT);
|
||||
boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND);
|
||||
boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY);
|
||||
boolean wantRiverState = fields.contains(IrisColumnField.RIVER_STATE);
|
||||
boolean wantRiverDistance = fields.contains(IrisColumnField.RIVER_DISTANCE);
|
||||
boolean wantRiverFlow = fields.contains(IrisColumnField.RIVER_FLOW);
|
||||
boolean wantRiverWaterSurface = fields.contains(IrisColumnField.RIVER_WATER_SURFACE_Y);
|
||||
boolean wantRiver = wantKind || wantRiverState || wantRiverDistance || wantRiverFlow
|
||||
|| wantRiverWaterSurface;
|
||||
|
||||
try {
|
||||
int minHeight = engine.getMinHeight();
|
||||
int fluid = engine.getDimension().getFluidHeight();
|
||||
long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
|
||||
if (engine.isClosed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HydrologyColumnSample hydrology = wantRiver
|
||||
? hydrologySample(engine, blockX, blockZ)
|
||||
: null;
|
||||
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
||||
int fluid = wantRiver
|
||||
? (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ))
|
||||
: engine.getDimension().getFluidHeight();
|
||||
boolean needsBiome = wantBiome
|
||||
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
||||
IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null;
|
||||
IrisSurfaceKind kind = wantKind
|
||||
? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType())
|
||||
? IrisSurfaceClassifier.classify(
|
||||
surface,
|
||||
fluid,
|
||||
biome == null ? null : biome.getInferredType(),
|
||||
hydrology
|
||||
)
|
||||
: IrisSurfaceKind.UNKNOWN;
|
||||
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
|
||||
sink.accept(blockX, blockZ, wantHeight ? surface + minHeight : -1, kind, biomeKey);
|
||||
int natural = wantNaturalHeight
|
||||
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
|
||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||
HydrologyColumnLayer surfaceLayer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
boolean riverPresent = surfaceLayer != null;
|
||||
IrisRiverState riverState = wantRiverState
|
||||
? riverState(hydrology)
|
||||
: IrisRiverState.NONE;
|
||||
double riverDistance = wantRiverDistance && riverPresent
|
||||
? engine.getComplex().getRiverDistanceStream().get(blockX, blockZ)
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||
int riverFlow = wantRiverFlow && riverPresent
|
||||
? (int) Math.round(engine.getComplex().getRiverFlowStream().get(blockX, blockZ))
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||
int riverWaterSurfaceY = wantRiverWaterSurface
|
||||
&& surfaceLayer != null
|
||||
&& surfaceLayer.connectedFluid()
|
||||
? fluid + minHeight
|
||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||
sink.accept(new IrisColumnSample(
|
||||
blockX,
|
||||
blockZ,
|
||||
wantHeight ? surface + minHeight : IrisColumnSample.UNAVAILABLE_HEIGHT,
|
||||
natural,
|
||||
kind,
|
||||
biomeKey,
|
||||
riverState,
|
||||
riverDistance,
|
||||
riverFlow,
|
||||
riverWaterSurfaceY
|
||||
));
|
||||
return true;
|
||||
});
|
||||
return visited == query.columnCount();
|
||||
@@ -253,6 +308,23 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
}
|
||||
}
|
||||
|
||||
static IrisRiverState riverState(HydrologyColumnSample hydrology) {
|
||||
HydrologyColumnLayer layer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
if (layer == null || !layer.channel()) {
|
||||
return IrisRiverState.NONE;
|
||||
}
|
||||
return layer.connectedFluid() ? IrisRiverState.WET : IrisRiverState.DRY;
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample hydrologySample(Engine engine, int blockX, int blockZ) {
|
||||
if (engine.getComplex().getHydrologyRuntime() == null) {
|
||||
return null;
|
||||
}
|
||||
return engine.getComplex().getHydrologyRuntime().sample(blockX, blockZ).orElse(null);
|
||||
}
|
||||
|
||||
private static Optional<String> key(IrisBiome biome) {
|
||||
String loadKey = biome == null ? null : biome.getLoadKey();
|
||||
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
|
||||
|
||||
+28
@@ -1,6 +1,8 @@
|
||||
package art.arcane.iris.core.service.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
|
||||
public final class IrisSurfaceClassifier {
|
||||
@@ -22,4 +24,30 @@ public final class IrisSurfaceClassifier {
|
||||
|
||||
return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND;
|
||||
}
|
||||
|
||||
public static IrisSurfaceKind classify(
|
||||
int engineSurfaceHeight,
|
||||
int engineFluidHeight,
|
||||
InferredType inferredType,
|
||||
HydrologyColumnSample hydrology
|
||||
) {
|
||||
if (engineSurfaceHeight <= 0) {
|
||||
return IrisSurfaceKind.VOID;
|
||||
}
|
||||
HydrologyColumnLayer layer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
if (layer != null) {
|
||||
if (layer.shore()) {
|
||||
return IrisSurfaceKind.RIVER_SHORE;
|
||||
}
|
||||
if (layer.channel()) {
|
||||
return layer.connectedFluid() ? IrisSurfaceKind.RIVER : IrisSurfaceKind.DRY_CHANNEL;
|
||||
}
|
||||
if (layer.grading()) {
|
||||
return IrisSurfaceKind.LAND;
|
||||
}
|
||||
}
|
||||
return classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
||||
}
|
||||
}
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class IrisColumnSampleTest {
|
||||
@Test
|
||||
public void unavailableFieldsHaveUnambiguousSentinels() {
|
||||
IrisColumnSample sample = sample(
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT,
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT,
|
||||
IrisSurfaceKind.UNKNOWN,
|
||||
null,
|
||||
IrisRiverState.NONE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_FLOW,
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT
|
||||
);
|
||||
|
||||
assertFalse(sample.hasSurfaceHeight());
|
||||
assertFalse(sample.hasNaturalHeight());
|
||||
assertFalse(sample.hasSurfaceKind());
|
||||
assertFalse(sample.hasBiomeKey());
|
||||
assertFalse(sample.hasRiverState());
|
||||
assertFalse(sample.hasRiverDistance());
|
||||
assertFalse(sample.hasRiverFlow());
|
||||
assertFalse(sample.hasRiverWaterSurfaceY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeWorldHeightsRemainAvailableValues() {
|
||||
IrisColumnSample sample = sample(
|
||||
-1,
|
||||
-64,
|
||||
IrisSurfaceKind.DRY_CHANNEL,
|
||||
"test:river",
|
||||
IrisRiverState.DRY,
|
||||
0D,
|
||||
0,
|
||||
-1
|
||||
);
|
||||
|
||||
assertTrue(sample.hasSurfaceHeight());
|
||||
assertTrue(sample.hasNaturalHeight());
|
||||
assertTrue(sample.hasSurfaceKind());
|
||||
assertTrue(sample.hasBiomeKey());
|
||||
assertTrue(sample.hasRiverState());
|
||||
assertTrue(sample.hasRiverDistance());
|
||||
assertTrue(sample.hasRiverFlow());
|
||||
assertTrue(sample.hasRiverWaterSurfaceY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankBiomeKeysNormalizeToUnavailable() {
|
||||
IrisColumnSample sample = sample(
|
||||
64,
|
||||
65,
|
||||
IrisSurfaceKind.LAND,
|
||||
" ",
|
||||
IrisRiverState.NONE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_FLOW,
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT
|
||||
);
|
||||
|
||||
assertNull(sample.biomeKey());
|
||||
assertFalse(sample.hasBiomeKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theSinkReceivesTheTypedSample() {
|
||||
IrisColumnSample sample = sample(
|
||||
64,
|
||||
65,
|
||||
IrisSurfaceKind.RIVER,
|
||||
"test:river",
|
||||
IrisRiverState.WET,
|
||||
0.5D,
|
||||
3,
|
||||
67
|
||||
);
|
||||
AtomicReference<IrisColumnSample> received = new AtomicReference<>();
|
||||
IrisColumnSink sink = received::set;
|
||||
|
||||
sink.accept(sample);
|
||||
|
||||
assertSame(sample, received.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidHydrologyValuesAreRejected() {
|
||||
assertInvalid(Double.POSITIVE_INFINITY, 1);
|
||||
assertInvalid(-0.1D, 1);
|
||||
assertInvalid(0D, -2);
|
||||
}
|
||||
|
||||
private static void assertInvalid(double distance, int flow) {
|
||||
try {
|
||||
sample(
|
||||
64,
|
||||
65,
|
||||
IrisSurfaceKind.RIVER,
|
||||
"test:river",
|
||||
IrisRiverState.WET,
|
||||
distance,
|
||||
flow,
|
||||
67
|
||||
);
|
||||
fail("Expected invalid hydrology values to be rejected");
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertTrue(expected.getMessage().startsWith("river"));
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisColumnSample sample(
|
||||
int surfaceHeight,
|
||||
int naturalHeight,
|
||||
IrisSurfaceKind surfaceKind,
|
||||
String biomeKey,
|
||||
IrisRiverState riverState,
|
||||
double riverDistance,
|
||||
int riverFlow,
|
||||
int riverWaterSurfaceY
|
||||
) {
|
||||
return new IrisColumnSample(
|
||||
12,
|
||||
-7,
|
||||
surfaceHeight,
|
||||
naturalHeight,
|
||||
surfaceKind,
|
||||
biomeKey,
|
||||
riverState,
|
||||
riverDistance,
|
||||
riverFlow,
|
||||
riverWaterSurfaceY
|
||||
);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class CommandFindBiomeContractTest {
|
||||
@Test
|
||||
public void biomeLookupUsesReachableBiomeHandler() throws Exception {
|
||||
Method method = CommandFind.class.getMethod("biome", IrisBiome.class, boolean.class);
|
||||
Param parameter = method.getParameters()[0].getAnnotation(Param.class);
|
||||
|
||||
assertEquals(ReachableBiomeHandler.class, parameter.customHandler());
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -1,8 +1,10 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -13,22 +15,31 @@ import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CommandObjectFluidHeightTest {
|
||||
@Test
|
||||
public void absoluteObjectPlacementShiftsFluidHeightFromNegativeMinY() {
|
||||
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() {
|
||||
World world = mock(World.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ProceduralStream<Double> riverHead = mock(ProceduralStream.class);
|
||||
Map<Block, BlockData> future = new HashMap<>();
|
||||
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(dimension.getFluidHeight()).thenReturn(127);
|
||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead);
|
||||
when(riverHead.get(12, -7)).thenReturn(131D);
|
||||
|
||||
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
||||
|
||||
assertEquals(63, placer.getFluidHeight());
|
||||
assertEquals(67, placer.getFluidHeight(12, -7));
|
||||
verify(riverHead).get(12, -7);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-2
@@ -1,10 +1,12 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.director.compat.DirectorAnnotationCompatibility;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorNodeDescriptor;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorParameterDescriptor;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -52,8 +54,23 @@ public class CommandPackOptionalPackContractTest {
|
||||
assertFalse(CommandPack.wantsAllPacks("overworld"));
|
||||
}
|
||||
|
||||
private static java.lang.reflect.Method findMethod(String name) {
|
||||
for (java.lang.reflect.Method method : CommandPack.class.getDeclaredMethods()) {
|
||||
@Test
|
||||
public void packageCommandIsOwnedByPackNamespace() throws Exception {
|
||||
Method method = CommandPack.class.getDeclaredMethod(
|
||||
"pkg",
|
||||
IrisDimension.class,
|
||||
boolean.class,
|
||||
boolean.class
|
||||
);
|
||||
DirectorNodeDescriptor descriptor = DirectorAnnotationCompatibility.fromMethod(method).orElseThrow();
|
||||
assertTrue(descriptor.getAliases().contains("package"));
|
||||
for (Method studioMethod : CommandStudio.class.getDeclaredMethods()) {
|
||||
assertFalse("Studio must not retain the package command", studioMethod.getName().equals("pkg"));
|
||||
}
|
||||
}
|
||||
|
||||
private static Method findMethod(String name) {
|
||||
for (Method method : CommandPack.class.getDeclaredMethods()) {
|
||||
if (method.getName().equals(name)) {
|
||||
return method;
|
||||
}
|
||||
|
||||
+51
-3
@@ -2,8 +2,13 @@ package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisColumnField;
|
||||
import art.arcane.iris.api.terrain.IrisColumnQuery;
|
||||
import art.arcane.iris.api.terrain.IrisRiverState;
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
@@ -12,6 +17,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -82,9 +88,7 @@ public class IrisTerrainSVCTest {
|
||||
IrisTerrainSVC service = new IrisTerrainSVC();
|
||||
AtomicInteger sinkCalls = new AtomicInteger();
|
||||
|
||||
boolean answered = service.sampleColumns(null, SMALL,
|
||||
(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey)
|
||||
-> sinkCalls.incrementAndGet());
|
||||
boolean answered = service.sampleColumns(null, SMALL, sample -> sinkCalls.incrementAndGet());
|
||||
|
||||
assertFalse(answered);
|
||||
assertEquals(0, sinkCalls.get());
|
||||
@@ -97,4 +101,48 @@ public class IrisTerrainSVCTest {
|
||||
assertFalse(service.sampleColumns(null, null, null));
|
||||
assertFalse(service.sampleColumns(null, SMALL, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedHydrologyMapsToThePublicDiagnosticStates() {
|
||||
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(null));
|
||||
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(true)));
|
||||
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(false)));
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample river(boolean connectedFluid) {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
1L,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
2L,
|
||||
3L,
|
||||
0,
|
||||
63,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
false
|
||||
);
|
||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
||||
feature,
|
||||
60,
|
||||
63,
|
||||
63,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
connectedFluid,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
connectedFluid,
|
||||
false,
|
||||
"water",
|
||||
"river",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"cave"
|
||||
);
|
||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
||||
}
|
||||
}
|
||||
|
||||
+107
-2
@@ -1,9 +1,15 @@
|
||||
package art.arcane.iris.core.service.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -53,9 +59,108 @@ public class IrisSurfaceClassifierTest {
|
||||
|
||||
IrisSurfaceKind expected = IrisSurfaceClassifier.classify(surface, FLUID, null);
|
||||
for (InferredType inferredType : InferredType.values()) {
|
||||
assertEquals("surface=" + surface + " type=" + inferredType,
|
||||
expected, IrisSurfaceClassifier.classify(surface, FLUID, inferredType));
|
||||
assertEquals(
|
||||
"surface=" + surface + " type=" + inferredType,
|
||||
expected,
|
||||
IrisSurfaceClassifier.classify(surface, FLUID, inferredType)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedSurfaceRolesOverrideGenericOceanAndLandKinds() {
|
||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
63,
|
||||
InferredType.SEA,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
63,
|
||||
InferredType.SEA,
|
||||
hydrology(HydrologyFeatureType.MOUTH, true, false, true)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify(
|
||||
64,
|
||||
63,
|
||||
InferredType.SHORE,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, true, false)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
60,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, false)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
60,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, false, false)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void voidClassificationWinsOverAcceptedRiverGeometry() {
|
||||
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
|
||||
0,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentRejectedCandidatesDoNotCreatePublicRiverKinds() {
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
||||
64,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample hydrology(
|
||||
HydrologyFeatureType type,
|
||||
boolean channel,
|
||||
boolean shore,
|
||||
boolean connectedFluid
|
||||
) {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
1L,
|
||||
type,
|
||||
2L,
|
||||
3L,
|
||||
0,
|
||||
63,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
false
|
||||
);
|
||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
||||
feature,
|
||||
60,
|
||||
63,
|
||||
63,
|
||||
channel,
|
||||
shore,
|
||||
!channel && !shore,
|
||||
connectedFluid,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
connectedFluid && channel,
|
||||
false,
|
||||
"water",
|
||||
"river",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"cave"
|
||||
);
|
||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,9 @@ dependencies {
|
||||
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
// registrySync and resourceLoader are LOAD-BEARING despite zero imports anywhere in Iris:
|
||||
// fabric-registry-sync-v0 delays registry freeze past mod init, which is what lets
|
||||
// IrisFabricBootstrap register the CHUNK_GENERATOR codec in
|
||||
|
||||
@@ -132,6 +132,9 @@ dependencies {
|
||||
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||
compileOnly(libs.spigot) {
|
||||
transitive = false
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -41,7 +42,7 @@ public final class MainWorldService {
|
||||
"region",
|
||||
"entities",
|
||||
"poi",
|
||||
"mantle",
|
||||
IrisEngineMantle.STORAGE_FOLDER_NAME,
|
||||
"dimensions/minecraft/overworld",
|
||||
"dimensions/minecraft/the_nether",
|
||||
"dimensions/minecraft/the_end",
|
||||
|
||||
+7
-1
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.Level;
|
||||
@@ -33,7 +34,12 @@ import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class ModdedDimensionStorage {
|
||||
private static final List<String> CHUNK_DATA_FOLDERS = List.of("region", "entities", "poi", "mantle");
|
||||
private static final List<String> CHUNK_DATA_FOLDERS = List.of(
|
||||
"region",
|
||||
"entities",
|
||||
"poi",
|
||||
IrisEngineMantle.STORAGE_FOLDER_NAME
|
||||
);
|
||||
|
||||
private ModdedDimensionStorage() {
|
||||
}
|
||||
|
||||
+44
-1
@@ -25,6 +25,8 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
@@ -62,6 +64,7 @@ final class ModdedCommandSuggestions {
|
||||
static final SuggestionProvider<CommandSourceStack> OBJECT_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> POI_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder);
|
||||
static final SuggestionProvider<CommandSourceStack> HYDROLOGY_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestHydrologyTypes(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
||||
|
||||
@@ -79,7 +82,7 @@ final class ModdedCommandSuggestions {
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder);
|
||||
return SharedSuggestionProvider.suggest(reachableBiomeKeys(engine), builder);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
warnTabFailure("biome keys", context.getSource(), e);
|
||||
@@ -87,6 +90,46 @@ final class ModdedCommandSuggestions {
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestHydrologyTypes(
|
||||
CommandContext<CommandSourceStack> context,
|
||||
SuggestionsBuilder builder
|
||||
) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
if (engine != null && engine.getComplex().getHydrologyRuntime() != null) {
|
||||
return SharedSuggestionProvider.suggest(
|
||||
engine.getComplex().getHydrologyRuntime().featureQueryKeys(), builder);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
warnTabFailure("hydrology types", context.getSource(), error);
|
||||
}
|
||||
return SharedSuggestionProvider.suggest(
|
||||
HydrologyFeatureQuery.suggestions(List.of()), builder);
|
||||
}
|
||||
|
||||
static Set<String> reachableBiomeKeys(Engine engine) {
|
||||
return reachableBiomeKeys(engine.getAllBiomes());
|
||||
}
|
||||
|
||||
static Set<String> reachableBiomeKeys(Iterable<IrisBiome> biomes) {
|
||||
Set<String> keys = new TreeSet<>();
|
||||
for (IrisBiome biome : biomes) {
|
||||
if (biome != null && biome.getLoadKey() != null && !biome.getLoadKey().isBlank()) {
|
||||
keys.add(biome.getLoadKey());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static boolean isReachableBiome(Engine engine, String biomeKey) {
|
||||
return isReachableBiome(engine.getAllBiomes(), biomeKey);
|
||||
}
|
||||
|
||||
static boolean isReachableBiome(Iterable<IrisBiome> biomes, String biomeKey) {
|
||||
return biomeKey != null && reachableBiomeKeys(biomes).contains(biomeKey.trim());
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestRegionKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
|
||||
+3
@@ -227,6 +227,9 @@ final class ModdedCommandTree {
|
||||
.then(Commands.literal("object")
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("river")
|
||||
.then(Commands.argument("type", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.HYDROLOGY_TYPES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoRiver(context.getSource(), StringArgumentType.getString(context, "type")))))
|
||||
.then(Commands.literal("unregistered")
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
ModdedUnregisteredStructures.print(context.getSource())))
|
||||
|
||||
+69
@@ -29,12 +29,16 @@ import art.arcane.iris.engine.framework.Locator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
@@ -87,6 +91,10 @@ final class ModdedLocateCommands {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
|
||||
return 0;
|
||||
}
|
||||
if (!ModdedCommandSuggestions.isReachableBiome(engine, biome.getLoadKey())) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", biome.getLoadKey())));
|
||||
return 0;
|
||||
}
|
||||
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
|
||||
return 1;
|
||||
}
|
||||
@@ -137,6 +145,67 @@ final class ModdedLocateCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int gotoRiver(CommandSourceStack source, String type) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, "This command can only be used by a player.");
|
||||
return 0;
|
||||
}
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
||||
if (runtime == null) {
|
||||
IrisModdedCommands.fail(source, "Hydrology is not active in this dimension.");
|
||||
return 0;
|
||||
}
|
||||
HydrologyFeatureQuery query;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
} catch (IllegalArgumentException error) {
|
||||
IrisModdedCommands.fail(source, error.getMessage());
|
||||
return 0;
|
||||
}
|
||||
int originX = player.blockPosition().getX();
|
||||
int originZ = player.blockPosition().getZ();
|
||||
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
|
||||
MinecraftServer server = source.getServer();
|
||||
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
|
||||
MultiBurst.burst.completeValueAsync(() -> runtime.nearestFeature(
|
||||
query.types(), query.profileKey(), originX, originZ, maximumDistance,
|
||||
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
|
||||
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null))
|
||||
.whenComplete((HydrologyFeatureRef feature, Throwable error) -> server.execute(() -> {
|
||||
if (error != null) {
|
||||
ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
|
||||
IrisModdedCommands.fail(source,
|
||||
"Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
||||
return;
|
||||
}
|
||||
if (feature == null) {
|
||||
IrisModdedCommands.fail(source,
|
||||
"No accepted " + type + " hydrology feature found within "
|
||||
+ maximumDistance + " blocks.");
|
||||
return;
|
||||
}
|
||||
int worldY = feature.y() + engine.getDimension().getMinHeight() + 2;
|
||||
teleportToStructure(
|
||||
source,
|
||||
level,
|
||||
player,
|
||||
feature.x(),
|
||||
worldY,
|
||||
feature.z(),
|
||||
type + " hydrology feature"
|
||||
);
|
||||
}));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int gotoStructure(CommandSourceStack source, String keyRaw) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
|
||||
+4
-6
@@ -341,8 +341,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
markCompleted();
|
||||
listener.onChunkGenerated(x, z);
|
||||
cleanupMantleChunk(x, z);
|
||||
listener.onChunkCleaned(x, z);
|
||||
cleanupMantleChunksCoveredBy(x, z, listener);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
@@ -406,8 +405,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
onSuccess();
|
||||
markCompleted();
|
||||
listener.onChunkGenerated(x, z);
|
||||
cleanupMantleChunk(x, z);
|
||||
listener.onChunkCleaned(x, z);
|
||||
cleanupMantleChunksCoveredBy(x, z, listener);
|
||||
} finally {
|
||||
markFinished();
|
||||
semaphore.release();
|
||||
@@ -494,9 +492,9 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupMantleChunk(int x, int z) {
|
||||
private void cleanupMantleChunksCoveredBy(int x, int z, PregenListener listener) {
|
||||
try {
|
||||
engine.getMantle().forceCleanupChunk(x, z);
|
||||
engine.getMantle().cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned);
|
||||
} catch (Throwable e) {
|
||||
ModdedIrisLog.debug("Iris pregen mantle cleanup skipped for {},{}: {}", x, z, e.toString());
|
||||
}
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
}
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File file = switch (type) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
|
||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
|
||||
+1
-1
@@ -301,7 +301,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
}
|
||||
|
||||
private void runUpdatePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
||||
void runUpdatePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
||||
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
int baseX = chunkX << 4;
|
||||
|
||||
+11
@@ -187,6 +187,17 @@ public class ModdedGenerationLeaseContractTest {
|
||||
assertTrue(logFailure.contains("cause.toString()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenReportsOnlyCleanupCallbacksFromCoveredTargets() throws IOException {
|
||||
String source = source("art/arcane/iris/modded/command/ModdedPregenMethod.java");
|
||||
String cleanup = method(source, "private void cleanupMantleChunksCoveredBy(");
|
||||
|
||||
assertTrue(source.contains("cleanupMantleChunksCoveredBy(x, z, listener);"));
|
||||
assertFalse(source.contains("cleanupMantleChunk(x, z);"));
|
||||
assertTrue(cleanup.contains("cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned)"));
|
||||
assertFalse(cleanup.contains("forceCleanupChunk"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidPregenBoundsFailBeforeRuntimeMutation() throws IOException {
|
||||
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisModdedBiomeCommandTest {
|
||||
@Test
|
||||
public void biomeKeysAreSortedAndLimitedToReachableBiomes() {
|
||||
KList<IrisBiome> reachable = new KList<>();
|
||||
IrisBiome river = new IrisBiome();
|
||||
river.setLoadKey("river");
|
||||
IrisBiome natural = new IrisBiome();
|
||||
natural.setLoadKey("natural");
|
||||
reachable.add(river);
|
||||
reachable.add(natural);
|
||||
assertEquals(
|
||||
List.of("natural", "river"),
|
||||
List.copyOf(ModdedCommandSuggestions.reachableBiomeKeys(reachable))
|
||||
);
|
||||
assertTrue(ModdedCommandSuggestions.isReachableBiome(reachable, "river"));
|
||||
assertFalse(ModdedCommandSuggestions.isReachableBiome(reachable, "unused"));
|
||||
}
|
||||
}
|
||||
+79
@@ -1,11 +1,27 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
import art.arcane.volmlib.util.matter.slices.UpdateMatter;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LiquidBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.material.FlowingFluid;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
@@ -15,8 +31,42 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedChunkUpdateServiceTest {
|
||||
@Test
|
||||
public void fallingFluidUpdateRetainsLevelEightStateAndRequestsPhysics() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.minHeight()).thenReturn(-64);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(world);
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
when(level.getMinY()).thenReturn(-64);
|
||||
when(level.getMaxY()).thenReturn(320);
|
||||
BlockPos position = new BlockPos(15, -59, 14);
|
||||
BlockState falling = Blocks.WATER.defaultBlockState().setValue(LiquidBlock.LEVEL, 8);
|
||||
when(level.getBlockState(position)).thenReturn(falling);
|
||||
UpdateRecordingMantleChunk chunk = new UpdateRecordingMantleChunk();
|
||||
|
||||
new ModdedChunkUpdateService().runUpdatePass(engine, level, 0, 0, chunk);
|
||||
|
||||
assertEquals(8, falling.getValue(LiquidBlock.LEVEL).intValue());
|
||||
if (falling.getFluidState().hasProperty(FlowingFluid.FALLING)) {
|
||||
assertTrue(falling.getFluidState().getValue(FlowingFluid.FALLING));
|
||||
}
|
||||
InOrder order = inOrder(level);
|
||||
order.verify(level).setBlock(eq(position), eq(Blocks.AIR.defaultBlockState()), anyInt());
|
||||
order.verify(level).setBlock(position, falling, Block.UPDATE_ALL);
|
||||
assertTrue(chunk.deleted());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scansWhenPlayersArePresent() {
|
||||
assertTrue(ModdedChunkUpdateService.hasUpdateTargets(true, false));
|
||||
@@ -84,6 +134,35 @@ public class ModdedChunkUpdateServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class UpdateRecordingMantleChunk extends MantleChunk<Matter> {
|
||||
private boolean deleted;
|
||||
|
||||
private UpdateRecordingMantleChunk() {
|
||||
super(1, 0, 0, emptyAdapter(), MantleHooks.NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void iterate(
|
||||
Class<T> type,
|
||||
Consumer4<Integer, Integer, Integer, T> iterator
|
||||
) {
|
||||
if (type == MatterUpdate.class) {
|
||||
iterator.accept(-1, 5, -2, type.cast(UpdateMatter.ON));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSlices(Class<?> type) {
|
||||
if (type == MatterUpdate.class) {
|
||||
deleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deleted() {
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static MantleDataAdapter<Matter> emptyAdapter() {
|
||||
return (MantleDataAdapter<Matter>) Proxy.newProxyInstance(
|
||||
|
||||
@@ -106,6 +106,9 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
||||
dependencies {
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher:6.1.2')
|
||||
testRuntimeOnly('org.junit.vintage:junit-vintage-engine:6.1.2')
|
||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||
|
||||
@@ -23,6 +23,8 @@ 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;
|
||||
@@ -69,6 +71,7 @@ import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
@@ -484,11 +487,52 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
}
|
||||
|
||||
private void renderLegend(Graphics2D canvas) {
|
||||
if (currentType == RenderType.HEIGHT) {
|
||||
if (currentType == RenderType.RIVER) {
|
||||
renderRiverLegend(canvas);
|
||||
} else if (currentType == RenderType.HEIGHT) {
|
||||
renderHeightLegend(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderRiverLegend(Graphics2D canvas) {
|
||||
HydrologyFeatureType[] types = HydrologyFeatureType.values();
|
||||
HydrologyCandidateKind[] candidateKinds = HydrologyCandidateKind.values();
|
||||
int lineHeight = 18;
|
||||
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);
|
||||
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 = 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);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderHeightLegend(Graphics2D canvas) {
|
||||
int width = 244;
|
||||
int height = 54;
|
||||
@@ -1037,6 +1081,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
case BIOME_SEA -> DesktopUiMessages.VISION_MODE_BIOME_SEA;
|
||||
case REGION -> DesktopUiMessages.VISION_MODE_REGION;
|
||||
case CAVE_LAND -> DesktopUiMessages.VISION_MODE_CAVE_LAND;
|
||||
case RIVER -> DesktopUiMessages.VISION_MODE_RIVER;
|
||||
case HEIGHT -> DesktopUiMessages.VISION_MODE_HEIGHT;
|
||||
case OBJECT_LOAD -> DesktopUiMessages.VISION_MODE_OBJECT_LOAD;
|
||||
case DECORATOR_LOAD -> DesktopUiMessages.VISION_MODE_DECORATOR_LOAD;
|
||||
|
||||
@@ -275,7 +275,8 @@ final class VisionRenderController implements AutoCloseable {
|
||||
if (!isCurrent(work)) {
|
||||
return;
|
||||
}
|
||||
while (work.inFlight() < renderWorkerCount) {
|
||||
int admissionLimit = work.frame().spec().type() == RenderType.RIVER ? 1 : renderWorkerCount;
|
||||
while (work.inFlight() < admissionLimit) {
|
||||
VisibleTile tile = work.nextMissing();
|
||||
if (tile == null) {
|
||||
return;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,6 +52,7 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_MODE_BIOME_SEA = TextKey.of("iris.desktop.vision.mode.biome_sea", "Biome sea");
|
||||
public static final TextKey VISION_MODE_REGION = TextKey.of("iris.desktop.vision.mode.region", "Region");
|
||||
public static final TextKey VISION_MODE_CAVE_LAND = TextKey.of("iris.desktop.vision.mode.cave_land", "Cave land");
|
||||
public static final TextKey VISION_MODE_RIVER = TextKey.of("iris.desktop.vision.mode.river", "River network");
|
||||
public static final TextKey VISION_MODE_HEIGHT = TextKey.of("iris.desktop.vision.mode.height", "Height");
|
||||
public static final TextKey VISION_MODE_OBJECT_LOAD = TextKey.of("iris.desktop.vision.mode.object_load", "Object load");
|
||||
public static final TextKey VISION_MODE_DECORATOR_LOAD = TextKey.of("iris.desktop.vision.mode.decorator_load", "Decorator load");
|
||||
@@ -171,7 +172,7 @@ public final class DesktopUiMessages {
|
||||
VISION_HELP_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_FPS,
|
||||
VISION_HELP_GRID, VISION_HELP_BIOME, VISION_HELP_TELEPORT, VISION_HELP_EDITOR, VISION_OPENED,
|
||||
VISION_TELEPORTING, VISION_MODE_BIOME, VISION_MODE_BIOME_LAND, VISION_MODE_BIOME_SEA,
|
||||
VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD,
|
||||
VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_RIVER, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD,
|
||||
VISION_MODE_DECORATOR_LOAD, VISION_MODE_CONTINENT, VISION_MODE_LAYER_LOAD, NOISE_TITLE,
|
||||
NOISE_TITLE_GENERATOR, NOISE_SEARCH, NOISE_STATUS, NOISE_CATEGORY_CUSTOM,
|
||||
NOISE_CATEGORY_PACK_GENERATORS, NOISE_CATEGORY_SIMPLEX, NOISE_CATEGORY_PERLIN,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,6 +84,10 @@ public final class PackValidator {
|
||||
packFolder, dimensionFiles, validateLiveRegistries);
|
||||
addDistinct(blockingErrors, imageMaps.errors());
|
||||
addDistinct(warnings, imageMaps.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,14 +22,27 @@ 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;
|
||||
import art.arcane.iris.engine.object.IrisDecorator;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
|
||||
import art.arcane.iris.engine.object.IrisSurfacePoolConfig;
|
||||
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.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -58,6 +71,7 @@ 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;
|
||||
@@ -66,8 +80,8 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "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);
|
||||
@@ -109,14 +123,20 @@ public class IrisComplex implements DataProvider {
|
||||
private ProceduralStream<IrisBiome> shoreBiomeStream;
|
||||
private ProceduralStream<IrisBiome> baseBiomeStream;
|
||||
private ProceduralStream<UUID> baseBiomeIDStream;
|
||||
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
|
||||
private ProceduralStream<IrisBiome> trueBiomeStream;
|
||||
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
|
||||
private ProceduralStream<Double> naturalHeightStream;
|
||||
private ProceduralStream<Double> heightStream;
|
||||
private ProceduralStream<Integer> roundedHeighteightStream;
|
||||
private ProceduralStream<Double> maxHeightStream;
|
||||
private ProceduralStream<Double> overlayStream;
|
||||
private ProceduralStream<Double> heightFluidStream;
|
||||
private ProceduralStream<Double> slopeStream;
|
||||
private ProceduralStream<Double> riverDistanceStream;
|
||||
private ProceduralStream<Double> riverFlowStream;
|
||||
private ProceduralStream<Double> riverCarveWeightStream;
|
||||
private ProceduralStream<Double> riverWaterSurfaceStream;
|
||||
private ProceduralStream<Integer> topSurfaceStream;
|
||||
private ProceduralStream<IrisDecorator> terrainSurfaceDecoration;
|
||||
private ProceduralStream<IrisDecorator> terrainCeilingDecoration;
|
||||
@@ -127,10 +147,12 @@ public class IrisComplex implements DataProvider {
|
||||
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
|
||||
private ProceduralStream<PlatformBlockState> rockStream;
|
||||
private ProceduralStream<PlatformBlockState> fluidStream;
|
||||
private Map<String, ProceduralStream<PlatformBlockState>> hydrologyFluidStreams;
|
||||
private IrisBiome focusBiome;
|
||||
private IrisRegion focusRegion;
|
||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||
private Set<IrisBiome> generatorBiomes;
|
||||
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
|
||||
@@ -166,7 +188,7 @@ public class IrisComplex implements DataProvider {
|
||||
//@builder
|
||||
if (focusRegion != null) {
|
||||
prepareInferredBiomes(focusRegion, preparedRegions);
|
||||
focusRegion.getAllBiomes(this).forEach(this::registerGenerators);
|
||||
focusRegion.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
} else {
|
||||
engine.getDimension().getRegions().forEach(regionKey -> {
|
||||
IrisRegion region = data.getRegionLoader().load(regionKey);
|
||||
@@ -174,11 +196,11 @@ public class IrisComplex implements DataProvider {
|
||||
return;
|
||||
}
|
||||
prepareInferredBiomes(region, preparedRegions);
|
||||
region.getAllBiomes(this).forEach(this::registerGenerators);
|
||||
region.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
});
|
||||
for (IrisRegion region : imageMapRuntime.getMappedRegions()) {
|
||||
prepareInferredBiomes(region, preparedRegions);
|
||||
region.getAllBiomes(this).forEach(this::registerGenerators);
|
||||
region.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
}
|
||||
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
|
||||
}
|
||||
@@ -211,6 +233,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));
|
||||
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);
|
||||
@@ -263,28 +286,59 @@ public class IrisComplex implements DataProvider {
|
||||
return mapped == null ? biome : mapped;
|
||||
})
|
||||
.cache2D("imageMappedBaseBiomeStream", engine, cacheSize);
|
||||
heightStream = ProceduralStream.of((x, z) -> {
|
||||
double proceduralHeight = getHeight(engine, x, z, engine.getSeedManager().getHeight());
|
||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
||||
}, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize);
|
||||
slopeStream = heightStream.slope(3)
|
||||
.cache2DDouble("slopeStream", engine, cacheSize);
|
||||
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||
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("trueBiomeStream-focus", engine, cacheSize) : heightStream
|
||||
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
|
||||
.convertAware2D((h, x, z) -> {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null
|
||||
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight)
|
||||
: mapped;
|
||||
})
|
||||
.cache2D("trueBiomeStream", engine, cacheSize);
|
||||
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
|
||||
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
|
||||
if (hydrologyActive(configuredHydrology)) {
|
||||
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
|
||||
engine.getSeedManager().getBodies(),
|
||||
engine.getHeight(),
|
||||
engine.getDimension(),
|
||||
data,
|
||||
(x, z, naturalHeight) -> sampleHydrologyNatural(engine, x, z, naturalHeight),
|
||||
(x, z) -> naturalHeightStream.getDouble(x, z),
|
||||
this::sampleNaturalOcean,
|
||||
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint)
|
||||
));
|
||||
}
|
||||
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();
|
||||
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
|
||||
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||
b -> focusBiome))
|
||||
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
|
||||
.convertAware2D((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);
|
||||
heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize);
|
||||
riverDistanceStream = ProceduralStream.ofDouble(this::resolveHydrologyDistance)
|
||||
.cache2DDouble("riverDistanceStream", engine, cacheSize);
|
||||
riverFlowStream = ProceduralStream.ofDouble(this::resolveHydrologyFlow)
|
||||
.cache2DDouble("riverFlowStream", engine, cacheSize);
|
||||
riverCarveWeightStream = ProceduralStream.ofDouble(this::resolveHydrologyCarveWeight)
|
||||
.cache2DDouble("riverCarveWeightStream", engine, cacheSize);
|
||||
riverWaterSurfaceStream = ProceduralStream.ofDouble(this::resolveHydrologyFluidSurface)
|
||||
.cache2DDouble("riverWaterSurfaceStream", engine, cacheSize);
|
||||
heightFluidStream = ProceduralStream.ofDouble((x, z) -> Math.max(
|
||||
heightStream.get(x, z),
|
||||
riverWaterSurfaceStream.get(x, z)
|
||||
))
|
||||
.cache2DDouble("heightFluidStream", engine, cacheSize);
|
||||
maxHeightStream = ProceduralStream.ofDouble((x, z) -> height);
|
||||
terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize);
|
||||
@@ -310,6 +364,320 @@ public class IrisComplex implements DataProvider {
|
||||
//@done
|
||||
}
|
||||
|
||||
static InferredType resolveNaturalInferredType(
|
||||
ProceduralStream<InferredType> bridgeStream,
|
||||
IrisBiome focusBiome,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
if (focusBiome != null) {
|
||||
return focusBiome.getInferredType();
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
for (IrisSurfacePoolConfig pool : hydrology.getSurfacePools()) {
|
||||
if (pool == null) {
|
||||
throw new IllegalArgumentException("hydrology.surfacePools cannot contain null entries");
|
||||
}
|
||||
String profileKey = requireHydrologyKey(pool.getId(), "hydrology.surfacePools[].id");
|
||||
ProceduralStream<PlatformBlockState> stream = configuredFluidStream(
|
||||
pool.getFluidPalette(),
|
||||
rng.nextParallelRNG(9900 + streamIndex++),
|
||||
"hydrology surface pool " + 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(
|
||||
IrisMaterialPalette palette,
|
||||
RNG fluidRng,
|
||||
String configurationName
|
||||
) {
|
||||
Objects.requireNonNull(palette, configurationName + " fluidPalette must be configured");
|
||||
KList<PlatformBlockState> blocks = palette.getBlockData(data);
|
||||
if (blocks.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
configurationName + " fluidPalette must resolve at least one fluid block");
|
||||
}
|
||||
for (PlatformBlockState block : blocks) {
|
||||
if (block == null || !block.isFluid()) {
|
||||
throw new IllegalArgumentException(
|
||||
configurationName + " fluidPalette may contain only fluid blocks");
|
||||
}
|
||||
}
|
||||
return palette.getLayerGenerator(fluidRng, data).stream().select(blocks);
|
||||
}
|
||||
|
||||
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) {
|
||||
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.75D : 0.5D;
|
||||
}
|
||||
|
||||
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 boolean hasHydrologyChannelOrShore(int x, int z) {
|
||||
HydrologyColumnLayer layer = surfaceLayer(x, z);
|
||||
return layer != null && (layer.channel() || layer.shore());
|
||||
}
|
||||
|
||||
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
|
||||
switch (type) {
|
||||
case CAVE:
|
||||
@@ -370,6 +738,47 @@ public class IrisComplex implements DataProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 && layer == null) {
|
||||
return mapped;
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (layer.biomeKey() == null) {
|
||||
return implode(biome.withInferredType(InferredType.LAND), x, z);
|
||||
}
|
||||
InferredType inferredType = layer.shore()
|
||||
? InferredType.SHORE
|
||||
: layer.connectedFluid() ? InferredType.SEA : InferredType.LAND;
|
||||
return implode(biome.withInferredType(inferredType), x, z);
|
||||
}
|
||||
return fixBiomeType(
|
||||
terrainHeight,
|
||||
baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight
|
||||
);
|
||||
}
|
||||
|
||||
static String hydrologySurfaceBiomeKey(HydrologyColumnSample sample) {
|
||||
Objects.requireNonNull(sample, "sample");
|
||||
HydrologyColumnLayer layer = sample.primarySurfaceLayer().orElse(null);
|
||||
if (layer == null) {
|
||||
return sample.parentBiomeKey();
|
||||
}
|
||||
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) {
|
||||
IrisBiome resolved = resolveSurfaceBiome(
|
||||
height,
|
||||
@@ -528,6 +937,38 @@ public class IrisComplex implements DataProvider {
|
||||
return h;
|
||||
}
|
||||
|
||||
private NoiseBounds naturalHeightBounds(
|
||||
Engine engine,
|
||||
KList<IrisShapedGeneratorStyle> overlayNoise,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
double minimum = fluidHeight;
|
||||
double maximum = fluidHeight;
|
||||
for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) {
|
||||
NoiseBounds bounds = gridSampleBounds(
|
||||
engine,
|
||||
frozenInterpolators[interpolatorIndex],
|
||||
interpolatorIndex,
|
||||
frozenGenerators[interpolatorIndex],
|
||||
x,
|
||||
z
|
||||
);
|
||||
minimum += Math.min(bounds.min(), bounds.max());
|
||||
maximum += Math.max(bounds.min(), bounds.max());
|
||||
}
|
||||
for (IrisShapedGeneratorStyle style : overlayNoise) {
|
||||
minimum += Math.min(style.getMin(), style.getMax());
|
||||
maximum += Math.max(style.getMin(), style.getMax());
|
||||
}
|
||||
minimum = imageMapRuntime.sampleTerrainHeight(x, z, minimum);
|
||||
maximum = imageMapRuntime.sampleTerrainHeight(x, z, maximum);
|
||||
return new NoiseBounds(
|
||||
Math.max(0D, Math.min(engine.getHeight(), Math.min(minimum, maximum))),
|
||||
Math.max(0D, Math.min(engine.getHeight(), Math.max(minimum, maximum)))
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -540,46 +981,6 @@ public class IrisComplex implements DataProvider {
|
||||
preparedRegions.add(region);
|
||||
}
|
||||
|
||||
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));
|
||||
};
|
||||
}
|
||||
|
||||
static Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compileInferredBiomeStreams(
|
||||
Iterable<IrisRegion> regions,
|
||||
BiFunction<IrisRegion, InferredType, ProceduralStream<IrisBiome>> compiler
|
||||
@@ -1072,6 +1473,8 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (hydrologyRuntime != null) {
|
||||
hydrologyRuntime.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ import art.arcane.iris.engine.mantle.MantlePass;
|
||||
import art.arcane.iris.engine.mantle.components.MantleCarvingComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleHydrologyComponent;
|
||||
import art.arcane.iris.engine.mantle.components.IrisStructureComponent;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -72,6 +74,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 final Engine engine;
|
||||
private final Mantle<Matter> mantle;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@@ -86,6 +89,7 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
this.mantle = createMantle(engine);
|
||||
components = new KMap<>();
|
||||
registerComponent(new MantleCarvingComponent(this));
|
||||
registerComponent(new MantleHydrologyComponent(this));
|
||||
object = new MantleObjectComponent(this);
|
||||
registerComponent(object);
|
||||
registerComponent(new MantleFloatingObjectComponent(this));
|
||||
@@ -121,7 +125,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()
|
||||
@@ -174,10 +178,18 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
if (!getDimension().isCarvingEnabled()) {
|
||||
disabled.addIfMissing(ReservedFlag.CARVED);
|
||||
}
|
||||
if (disabled.contains(ReservedFlag.CARVED)
|
||||
|| !isHydrologyEnabled(getDimension())) {
|
||||
disabled.addIfMissing(ReservedFlag.RIVER_HYDROLOGY);
|
||||
}
|
||||
return Set.copyOf(disabled);
|
||||
});
|
||||
}
|
||||
|
||||
static boolean isHydrologyEnabled(IrisDimension dimension) {
|
||||
return MantleHydrologyComponent.isEnabledFor(dimension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MantleObjectComponent getObjectComponent() {
|
||||
return object;
|
||||
@@ -185,7 +197,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()));
|
||||
|
||||
@@ -66,8 +66,8 @@ public class UpperDimensionContext implements DataProvider {
|
||||
engine.getDimension(),
|
||||
engine.getData(),
|
||||
chunkHeight,
|
||||
complex.getHeightStream(),
|
||||
complex.getTrueBiomeStream(),
|
||||
complex.getNaturalHeightStream(),
|
||||
complex.getNaturalTrueBiomeStream(),
|
||||
complex.getRegionStream(),
|
||||
complex.getRockStream(),
|
||||
complex.getImageMapRuntime(),
|
||||
@@ -99,12 +99,12 @@ public class UpperDimensionContext implements DataProvider {
|
||||
upperDim.getRegions().forEach(regionKey -> {
|
||||
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
||||
if (region != null) {
|
||||
region.getAllBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
}
|
||||
});
|
||||
for (IrisRegion mappedRegion : imageMapRuntime.getMappedRegions()) {
|
||||
mappedRegion.getAllBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
mappedRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
}
|
||||
for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) {
|
||||
|
||||
@@ -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,6 +26,9 @@ 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.util.common.data.B;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
@@ -50,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) {
|
||||
@@ -64,6 +67,84 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
seaFloorDecorator = new IrisSeaFloorDecorator(getEngine());
|
||||
}
|
||||
|
||||
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
|
||||
@Override
|
||||
public void onActuate(int x, int z, Hunk<PlatformBlockState> output, boolean multicore, ChunkContext context) {
|
||||
@@ -72,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);
|
||||
@@ -86,23 +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;
|
||||
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 < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))
|
||||
&& height + 1 < output.getHeight() && B.isWater(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, getDimension().getFluidHeight(), getEngine().getHeight());
|
||||
getSeaFloorDecorator().decorate(i, j,
|
||||
realX, realZ, output, biome, height + 1,
|
||||
getDimension().getFluidHeight() + 1);
|
||||
output, biome, surfaceFluidHeight, getEngine().getHeight());
|
||||
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 (height == getDimension().getFluidHeight()) {
|
||||
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),
|
||||
@@ -140,4 +254,10 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
private boolean shouldRayDecorate() {
|
||||
return false; // TODO CAVES
|
||||
}
|
||||
|
||||
enum ShorelineDecorationMode {
|
||||
NONE,
|
||||
LEGACY,
|
||||
ACCEPTED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,11 @@ 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.IrisProceduralBlocks;
|
||||
import art.arcane.iris.engine.object.IrisSurfaceRiverBedConfig;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisOreGenerator;
|
||||
import art.arcane.iris.engine.object.IrisOreGeneratorBounds;
|
||||
@@ -88,13 +92,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
IrisData data = getData();
|
||||
IrisComplex complex = getComplex();
|
||||
RNG localRng = rng;
|
||||
int fluidHeight = dimension.getFluidHeight();
|
||||
int clampedFluidHeight = Math.min(chunkHeight, fluidHeight);
|
||||
boolean bedrockEnabled = dimension.isBedrock();
|
||||
boolean hideOres = dimension.isHideOresForHiddenOre();
|
||||
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
||||
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
|
||||
ChunkedDataCache<PlatformBlockState> fluidCache = context.getFluid();
|
||||
ChunkedDataCache<PlatformBlockState> rockCache = context.getRock();
|
||||
int realX = xf + x;
|
||||
UpperDimensionContext upperContext = getEngine().getUpperContext();
|
||||
@@ -104,19 +105,47 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
KList<IrisOreGenerator> dimensionUndergroundOres = hideOres ? null : dimension.getUndergroundOreGenerators();
|
||||
IrisOreGeneratorBounds dimensionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getSurfaceOreGeneratorBounds();
|
||||
IrisOreGeneratorBounds dimensionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getUndergroundOreGeneratorBounds();
|
||||
boolean exposeCutStrata = dimension.getHydrology() != null
|
||||
&& dimension.getHydrology().getRivers().getSurface().getBanks().isExposeCutStrata();
|
||||
IrisSurfaceRiverBedConfig riverBed = dimension.getHydrology() == null
|
||||
? null
|
||||
: dimension.getHydrology().getRivers().getSurface().getBed();
|
||||
boolean padRiverBed = riverBed != null && !riverBed.isAllowGravityBlocks();
|
||||
|
||||
for (int zf = 0; zf < chunkDepth; zf++) {
|
||||
int realZ = zf + z;
|
||||
IrisBiome biome = biomeCache.get(xf, zf);
|
||||
IrisRegion region = regionCache.get(xf, zf);
|
||||
int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf));
|
||||
int hf = Math.max(clampedFluidHeight, he);
|
||||
int surfaceFluidHeight = Math.min(
|
||||
chunkHeight,
|
||||
(int) Math.round(complex.getRiverWaterSurfaceStream().get(realX, realZ))
|
||||
);
|
||||
int hf = Math.max(surfaceFluidHeight, he);
|
||||
if (hf < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int topY = Math.min(hf, chunkHeight - 1);
|
||||
PlatformBlockState fluid = fluidCache.get(xf, zf);
|
||||
HydrologyColumnSample hydrology = complex.getHydrologyRuntime() == null
|
||||
? null
|
||||
: complex.getHydrologyRuntime().sample(realX, realZ).orElse(null);
|
||||
HydrologyColumnLayer hydrologyFluid = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceFluidLayer().orElse(null);
|
||||
HydrologyColumnLayer hydrologyTerrain = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
int cut = exposeCutStrata
|
||||
&& hydrologyTerrain != null
|
||||
&& hydrologyTerrain.terrainOwned()
|
||||
&& !hydrologyTerrain.channel()
|
||||
? Math.max(0, hydrology.naturalHeight() - he)
|
||||
: 0;
|
||||
boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned();
|
||||
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();
|
||||
@@ -158,15 +187,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;
|
||||
}
|
||||
|
||||
@@ -177,11 +206,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
continue;
|
||||
}
|
||||
if (blocks == null) {
|
||||
blocks = biome.generateLayers(dimension, realX, realZ, localRng, he, he, data, complex);
|
||||
blocks = biome.generateLayers(dimension, realX, realZ, localRng, he + cut, he + cut, data, complex);
|
||||
}
|
||||
|
||||
if (blocks.hasIndex(depth)) {
|
||||
h.setRaw(xf, i, zf, blocks.get(depth));
|
||||
if (blocks.hasIndex(depth + cut)) {
|
||||
PlatformBlockState layerBlock = blocks.get(depth + cut);
|
||||
if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
|
||||
layerBlock = riverBed.getPaddingPalette().get(localRng, realX, i, realZ, data);
|
||||
}
|
||||
h.setRaw(xf, i, zf, layerBlock);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,19 +40,46 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
@Override
|
||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
|
||||
if (height != getDimension().getFluidHeight()) {
|
||||
double localFluidHeight = getComplex().getRiverWaterSurfaceStream().get(realX, realZ);
|
||||
if (height != Math.round(localFluidHeight)) {
|
||||
return;
|
||||
}
|
||||
|
||||
double complexFluidHeight = getComplex().getFluidHeight();
|
||||
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
|
||||
if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight
|
||||
&& Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight
|
||||
&& Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight
|
||||
&& Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) {
|
||||
ProceduralStream<Double> fluidStream = getComplex().getRiverWaterSurfaceStream();
|
||||
if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ))
|
||||
&& Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ))
|
||||
&& Math.round(heightStream.get(realX, realZ1)) >= Math.round(fluidStream.get(realX, realZ1))
|
||||
&& Math.round(heightStream.get(realX, realZ_1)) >= Math.round(fluidStream.get(realX, realZ_1))) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -70,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()
|
||||
@@ -79,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;
|
||||
}
|
||||
@@ -99,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;
|
||||
}
|
||||
@@ -122,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;
|
||||
@@ -58,12 +60,15 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
@BlockCoordinates
|
||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||
Hunk<PlatformBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
|
||||
int fluidHeight = getDimension().getFluidHeight();
|
||||
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
|
||||
if (inferredType == InferredType.SHORE && height < fluidHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,6 +42,8 @@ import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.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;
|
||||
@@ -244,6 +246,14 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
|
||||
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(
|
||||
getMantle().getMantle(), x, y, z);
|
||||
if (hydrology != null && !hydrology.floodedBiomeKey().isEmpty()) {
|
||||
IrisBiome biome = getData().getBiomeLoader().load(hydrology.floodedBiomeKey());
|
||||
if (biome != null) {
|
||||
return biome;
|
||||
}
|
||||
}
|
||||
MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class);
|
||||
|
||||
if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) {
|
||||
@@ -445,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;
|
||||
}
|
||||
@@ -552,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());
|
||||
}
|
||||
|
||||
+4
-1
@@ -157,7 +157,10 @@ public final class NativeStructurePlacementPlanner {
|
||||
}
|
||||
|
||||
static boolean isSubmerged(Engine engine, int blockX, int blockZ) {
|
||||
return engine.getHeight(blockX, blockZ, true) < engine.getDimension().getFluidHeight();
|
||||
int localFluidHeight = engine.getComplex() == null
|
||||
? engine.getDimension().getFluidHeight()
|
||||
: (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
||||
return engine.getHeight(blockX, blockZ, true) < localFluidHeight;
|
||||
}
|
||||
|
||||
private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) {
|
||||
|
||||
+26
-1
@@ -3,6 +3,8 @@ package art.arcane.iris.engine.framework;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisStructureAnchorMode;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.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;
|
||||
|
||||
@@ -286,10 +288,12 @@ public final class StructureCaveAnchorResolver {
|
||||
int mantleY,
|
||||
int blockZ
|
||||
) {
|
||||
HydrologyCaveCell hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||
MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ);
|
||||
return acceptsAnchorFluid(
|
||||
placement.isUnderwater(),
|
||||
cavern,
|
||||
hydrology,
|
||||
mantleY,
|
||||
engine.getDimension().getCaveLavaHeight());
|
||||
}
|
||||
@@ -300,6 +304,19 @@ public final class StructureCaveAnchorResolver {
|
||||
int mantleY,
|
||||
int defaultLavaHeight
|
||||
) {
|
||||
return acceptsAnchorFluid(underwater, cavern, null, mantleY, defaultLavaHeight);
|
||||
}
|
||||
|
||||
static boolean acceptsAnchorFluid(
|
||||
boolean underwater,
|
||||
MatterCavern cavern,
|
||||
HydrologyCaveCell hydrology,
|
||||
int mantleY,
|
||||
int defaultLavaHeight
|
||||
) {
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
return false;
|
||||
}
|
||||
if (cavern == null || !cavern.isCavern()) {
|
||||
return false;
|
||||
}
|
||||
@@ -313,7 +330,15 @@ public final class StructureCaveAnchorResolver {
|
||||
}
|
||||
|
||||
private static MatterCavern cavernAt(Engine engine, int blockX, int mantleY, int blockZ) {
|
||||
return engine.getMantle().getMantle().get(blockX, mantleY, blockZ, MatterCavern.class);
|
||||
MatterCavern baseline = engine.getMantle().getMantle()
|
||||
.get(blockX, mantleY, blockZ, MatterCavern.class);
|
||||
HydrologyCaveCell hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||
return hydrology == null ? baseline : hydrology.asCavern();
|
||||
}
|
||||
|
||||
private static HydrologyCaveCell hydrologyAt(Engine engine, int blockX, int mantleY, int blockZ) {
|
||||
return HydrologyCaveStorage.getIfPresent(
|
||||
engine.getMantle().getMantle(), blockX, mantleY, blockZ);
|
||||
}
|
||||
|
||||
static int toMantleY(int worldY, int worldMinHeight) {
|
||||
|
||||
@@ -21,7 +21,6 @@ import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -112,7 +111,7 @@ public class WorldObjectPlacer implements IObjectPlacer {
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return mantle.getMantle().get(x, y, z, MatterCavern.class) != null;
|
||||
return mantle.isCarved(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,6 +20,13 @@ 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;
|
||||
@@ -29,9 +36,13 @@ 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;
|
||||
@@ -40,6 +51,24 @@ 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_WATERFALL = new Color(190, 236, 255).getRGB();
|
||||
private static final int CONFIRMED_DEEP_LAVA = new Color(194, 50, 22).getRGB();
|
||||
private static final int STANDING_POOL = new Color(232, 120, 40).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();
|
||||
private static final int LOWLAND = new Color(78, 128, 76).getRGB();
|
||||
@@ -105,9 +134,17 @@ public final class IrisRenderer {
|
||||
renderHeightAtlas(pixels, resolution, sx, sz, step, renderer, cancelled);
|
||||
return image;
|
||||
}
|
||||
PixelShader shader = shader(currentType, studio);
|
||||
PixelShader shader = shader(currentType, step, studio);
|
||||
if (studio && currentType == RenderType.RIVER) {
|
||||
Arrays.fill(pixels, NO_RIVER);
|
||||
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, false);
|
||||
return image;
|
||||
}
|
||||
if (studio && adaptiveStudioType(currentType)) {
|
||||
renderAdaptiveAtlas(pixels, resolution, sx, sz, step, shader, cancelled);
|
||||
if (currentType == RenderType.BIOME) {
|
||||
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, true);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
int groupSize = sampleGroup(step, resolution);
|
||||
@@ -133,6 +170,57 @@ public final class IrisRenderer {
|
||||
return image;
|
||||
}
|
||||
|
||||
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 DEEP_POOL, DEEP_CHANNEL -> CONFIRMED_DEEP_LAVA;
|
||||
case STANDING_POOL -> STANDING_POOL;
|
||||
};
|
||||
}
|
||||
|
||||
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, POOL -> PROJECTED_DEEP_FLUID;
|
||||
};
|
||||
}
|
||||
|
||||
public static int heightColor(double height, double maximumHeight, double fluidHeight) {
|
||||
double boundedMaximum = Math.max(1D, maximumHeight);
|
||||
double boundedHeight = clamp(height, 0D, boundedMaximum);
|
||||
@@ -160,7 +248,7 @@ public final class IrisRenderer {
|
||||
return Math.max(1, Math.min(resolution, (int) Math.floor(16D / absoluteStep)));
|
||||
}
|
||||
|
||||
private PixelShader shader(RenderType currentType, boolean studio) {
|
||||
private PixelShader shader(RenderType currentType, double step, boolean studio) {
|
||||
IrisComplex complex = renderer.getComplex();
|
||||
return switch (currentType) {
|
||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> biomeShader(
|
||||
@@ -169,7 +257,8 @@ public final class IrisRenderer {
|
||||
case BIOME_SEA -> biomeShader(complex.getSeaBiomeStream(), currentType);
|
||||
case REGION -> regionShader(complex, currentType);
|
||||
case CAVE_LAND -> biomeShader(complex.getCaveBiomeStream(), currentType);
|
||||
case HEIGHT -> heightShader(complex.getHeightStream());
|
||||
case HEIGHT -> heightShader(studio ? complex.getNaturalHeightStream() : complex.getHeightStream());
|
||||
case RIVER -> (double x, double z) -> riverColor(complex, x, z, step);
|
||||
case CONTINENT -> studio
|
||||
? continentShader(complex.getBaseBiomeStream())
|
||||
: this::continentColor;
|
||||
@@ -180,7 +269,7 @@ public final class IrisRenderer {
|
||||
return switch (type) {
|
||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND,
|
||||
CONTINENT -> true;
|
||||
case HEIGHT -> false;
|
||||
case HEIGHT, RIVER -> false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -224,7 +313,7 @@ public final class IrisRenderer {
|
||||
startX,
|
||||
startZ,
|
||||
step,
|
||||
complex.getHeightStream(),
|
||||
complex.getNaturalHeightStream(),
|
||||
engine.getHeight(),
|
||||
fluidHeight,
|
||||
cancelled
|
||||
@@ -237,6 +326,212 @@ public final class IrisRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderRiverAtlas(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
IrisComplex complex,
|
||||
BooleanSupplier cancelled,
|
||||
boolean composite
|
||||
) {
|
||||
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) {
|
||||
int height = Math.min(blockPixels, resolution - pixelZ);
|
||||
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
|
||||
renderRiverBlock(
|
||||
pixels,
|
||||
resolution,
|
||||
startX,
|
||||
startZ,
|
||||
step,
|
||||
runtime,
|
||||
cancelled,
|
||||
composite,
|
||||
!composite,
|
||||
headwaters,
|
||||
pixelX,
|
||||
pixelZ,
|
||||
Math.min(blockPixels, resolution - pixelX),
|
||||
height
|
||||
);
|
||||
}
|
||||
}
|
||||
renderHeadwaterDirections(pixels, resolution, startX, startZ, step, headwaters);
|
||||
}
|
||||
|
||||
private static void renderRiverBlock(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
IrisHydrologyRuntime runtime,
|
||||
BooleanSupplier cancelled,
|
||||
boolean composite,
|
||||
boolean diagnostics,
|
||||
Map<Long, HydrologyFeatureRef> headwaters,
|
||||
int pixelX,
|
||||
int pixelZ,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
checkCancelled(cancelled);
|
||||
HydrologyRenderSample sample = runtime.sampleRenderFootprint(
|
||||
startX + pixelX * step,
|
||||
startZ + pixelZ * step,
|
||||
startX + (pixelX + width) * step,
|
||||
startZ + (pixelZ + height) * step
|
||||
);
|
||||
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 = 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, diagnostics,
|
||||
headwaters,
|
||||
pixelX, pixelZ, leftWidth, topHeight);
|
||||
if (rightWidth > 0) {
|
||||
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, diagnostics,
|
||||
headwaters,
|
||||
pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
|
||||
if (rightWidth > 0) {
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, diagnostics,
|
||||
headwaters,
|
||||
pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
IdentityHashMap<IrisBiome, Integer> colors = new IdentityHashMap<>();
|
||||
return (double x, double z) -> {
|
||||
@@ -271,6 +566,50 @@ public final class IrisRenderer {
|
||||
return (double x, double z) -> heightColor(stream.getDouble(x, z), maximumHeight, fluidHeight);
|
||||
}
|
||||
|
||||
private int riverColor(IrisComplex complex, double x, double z, double step) {
|
||||
IrisHydrologyRuntime runtime = complex.getHydrologyRuntime();
|
||||
if (runtime == null) {
|
||||
return NO_RIVER;
|
||||
}
|
||||
double endX = x + step;
|
||||
double endZ = z + step;
|
||||
HydrologyRenderSample sample = runtime.sampleRenderFootprint(
|
||||
StrictMath.min(x, endX),
|
||||
StrictMath.min(z, endZ),
|
||||
StrictMath.max(x, endX),
|
||||
StrictMath.max(z, endZ)
|
||||
);
|
||||
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) {
|
||||
IrisBiome biome = renderer.getBiome(
|
||||
(int) Math.round(x),
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
package art.arcane.iris.engine.framework.render;
|
||||
|
||||
public enum RenderType {
|
||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
|
||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, RIVER, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
|
||||
}
|
||||
|
||||
@@ -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,8 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public enum HydrologyCandidateKind {
|
||||
SOURCE,
|
||||
OUTLET,
|
||||
DEEP_FLUID,
|
||||
POOL
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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,
|
||||
OUTLET_LEVEL,
|
||||
CAVE_CONTAINMENT,
|
||||
VOLUME_LIMIT,
|
||||
SURFACE_HEAD_RANGE,
|
||||
SURFACE_HEAD_DISTRIBUTION,
|
||||
SURFACE_EXPOSURE,
|
||||
SURFACE_DROP_UNSUPPORTED,
|
||||
SURFACE_CORRIDOR_UNSUPPORTED,
|
||||
SURFACE_TRANSITION_UNSUPPORTED,
|
||||
SURFACE_SEGMENT_UNSUPPORTED,
|
||||
SURFACE_SINKHOLE_CLEARANCE,
|
||||
SURFACE_SHAPE_UNSUPPORTED,
|
||||
POLICY_EXCLUDED;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+13
@@ -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 bankBiomeKey,
|
||||
String floodedCaveBiomeKey
|
||||
) {
|
||||
public HydrologyColumnLayer {
|
||||
Objects.requireNonNull(feature, "feature");
|
||||
profileKey = requireKey(profileKey, "profileKey");
|
||||
surfaceBiomeKey = requireKey(surfaceBiomeKey, "surfaceBiomeKey");
|
||||
mouthBiomeKey = requireKey(mouthBiomeKey, "mouthBiomeKey");
|
||||
shoreBiomeKey = requireKey(shoreBiomeKey, "shoreBiomeKey");
|
||||
bankBiomeKey = requireKey(bankBiomeKey, "bankBiomeKey");
|
||||
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 bankBiomeKey;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/** Every owned channel water column is published so steps between stations receive fluid updates. */
|
||||
public boolean publishesSurfaceFluid() {
|
||||
return feature.type().isSurface()
|
||||
&& !oceanApron
|
||||
&& channel
|
||||
&& fluidOwned
|
||||
&& (terrainOwned || fallingFluid)
|
||||
&& 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,193 @@
|
||||
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."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+137
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+163
@@ -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,19 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydrologyDiagnosticCandidate(
|
||||
long id,
|
||||
HydrologyCandidateKind kind,
|
||||
HydrologyFeatureType projectedType,
|
||||
HydrologyPoint point,
|
||||
HydrologyCandidateRejection rejection,
|
||||
int detail
|
||||
) {
|
||||
public HydrologyDiagnosticCandidate {
|
||||
Objects.requireNonNull(kind, "kind");
|
||||
Objects.requireNonNull(projectedType, "projectedType");
|
||||
Objects.requireNonNull(point, "point");
|
||||
Objects.requireNonNull(rejection, "rejection");
|
||||
}
|
||||
}
|
||||
+28
@@ -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,100 @@
|
||||
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.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 Set<HydrologyFeatureType> PROFILED = Set.of(
|
||||
HydrologyFeatureType.DEEP_POOL,
|
||||
HydrologyFeatureType.DEEP_CHANNEL,
|
||||
HydrologyFeatureType.STANDING_POOL
|
||||
);
|
||||
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(PROFILED, 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("deep", DEEP);
|
||||
builtIns.put("pool", Set.of(HydrologyFeatureType.STANDING_POOL));
|
||||
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,67 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public enum HydrologyFeatureType {
|
||||
SURFACE_POOL,
|
||||
RIFFLE,
|
||||
CASCADE,
|
||||
WATERFALL,
|
||||
// No planner produces a ridge bore any more. The constant stays because its ordinal seeds the
|
||||
// segment, outlet and feature ids of every constant after it on the cave and deep-fluid paths.
|
||||
RIDGE_BORE,
|
||||
UNDERGROUND_POOL,
|
||||
UNDERGROUND_DROP,
|
||||
SINKHOLE,
|
||||
COASTAL_GROTTO,
|
||||
INLAND_GROTTO,
|
||||
MOUTH,
|
||||
DEEP_POOL,
|
||||
DEEP_CHANNEL,
|
||||
// A standing surface pool (lava or any fluid) with no course: a bowl cut into the ground with a lip.
|
||||
STANDING_POOL;
|
||||
|
||||
public boolean isSurface() {
|
||||
return switch (this) {
|
||||
case SURFACE_POOL, RIFFLE, CASCADE, WATERFALL, MOUTH, STANDING_POOL -> 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;
|
||||
case STANDING_POOL -> 13;
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
||||
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(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.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 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_BLEND_WIDTH,
|
||||
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;
|
||||
|
||||
public final class HydrologyHash {
|
||||
private HydrologyHash() {
|
||||
}
|
||||
|
||||
public static long mix(long seed, long... values) {
|
||||
long mixed = avalanche(seed ^ 0x9e3779b97f4a7c15L);
|
||||
for (long value : values) {
|
||||
mixed = avalanche(mixed ^ avalanche(value + 0x9e3779b97f4a7c15L));
|
||||
}
|
||||
return mixed;
|
||||
}
|
||||
|
||||
public 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);
|
||||
}
|
||||
|
||||
public static double unit(long value) {
|
||||
return (avalanche(value) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public interface HydrologyNaturalTerrainSampler extends HydrologyRoutingTerrainSampler {
|
||||
HydrologyTerrainSample sampleBasis(int blockX, int blockZ);
|
||||
}
|
||||
+48
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
+47
@@ -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,586 @@
|
||||
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,
|
||||
List<SurfacePool> surfacePools
|
||||
) {
|
||||
private static final long PLAN_FORMAT_REVISION = 6L;
|
||||
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);
|
||||
surfacePools = surfacePools == null ? List.of() : List.copyOf(surfacePools);
|
||||
int publicationRadius = publicationRadius(routing, surface, underground, outlets, geometry, deepFluids, surfacePools);
|
||||
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, 8192, 8192, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, 0),
|
||||
new Surface(true, surfaceSources, 4, 8, 2, 4, 10, 1.5D, Banks.defaults()),
|
||||
new Hydraulics(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(),
|
||||
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()));
|
||||
}
|
||||
for (SurfacePool pool : surfacePools) {
|
||||
hash = HydrologyHash.mix(hash, pool.hashCode(), HydrologyHash.text(pool.id()));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
public int publicationRadius() {
|
||||
return publicationRadius(routing, surface, underground, outlets, geometry, deepFluids, surfacePools);
|
||||
}
|
||||
|
||||
int crossTileColorPeriod() {
|
||||
return crossTileColorPeriod(publicationRadius(), routing.tileSize());
|
||||
}
|
||||
|
||||
private static int publicationRadius(
|
||||
Routing routing,
|
||||
Surface surface,
|
||||
Underground underground,
|
||||
Outlets outlets,
|
||||
Geometry geometry,
|
||||
List<DeepFluid> deepFluids,
|
||||
List<SurfacePool> surfacePools
|
||||
) {
|
||||
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 blendWidth = surface.banks().maximumBlendWidth();
|
||||
int surfaceRadius = (int) StrictMath.ceil(
|
||||
surface.maximumWidth() / 2D + surface.shoreWidth() + blendWidth
|
||||
);
|
||||
surfaceRadius = Math.max(
|
||||
surfaceRadius,
|
||||
(int) StrictMath.ceil(geometry.drops().basinWidth(
|
||||
geometry.drops().flowWidth(surface.maximumWidth())
|
||||
) / 2D + surface.shoreWidth() + blendWidth)
|
||||
);
|
||||
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));
|
||||
}
|
||||
for (SurfacePool pool : surfacePools) {
|
||||
if (!pool.enabled()) {
|
||||
continue;
|
||||
}
|
||||
radius = Math.max(radius, pool.maximumRadius() + (int) StrictMath.ceil(surface.shoreWidth())
|
||||
+ surface.banks().maximumBlendWidth());
|
||||
}
|
||||
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 maximumRouteNodes,
|
||||
int maximumRouteLength,
|
||||
int minimumSurfaceCourseLength,
|
||||
int minimumUndergroundCourseLength,
|
||||
double valleyPreference,
|
||||
double uphillPenalty,
|
||||
double slopePenalty,
|
||||
double confluenceAttraction,
|
||||
double lengthPreference,
|
||||
int tributaries
|
||||
) {
|
||||
public Routing {
|
||||
if (tileSize < 32 || sampleSpacing < 4 || tileSize % sampleSpacing != 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 (minimumSurfaceCourseLength < 0 || minimumSurfaceCourseLength > 32_768
|
||||
|| minimumUndergroundCourseLength < 0 || minimumUndergroundCourseLength > 32_768) {
|
||||
throw new IllegalArgumentException("Hydrology course lengths are invalid.");
|
||||
}
|
||||
if (minimumSurfaceCourseLength > maximumRouteLength
|
||||
|| 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");
|
||||
requireFiniteNonNegative(lengthPreference, "lengthPreference");
|
||||
if (tributaries < 0) {
|
||||
throw new IllegalArgumentException("tributaries must not be negative.");
|
||||
}
|
||||
}
|
||||
|
||||
// Route refinement is derived from the lattice, never authored.
|
||||
public static int refinementSpacing(int sampleSpacing) {
|
||||
if (sampleSpacing % 4 == 0) {
|
||||
return 4;
|
||||
}
|
||||
return sampleSpacing % 2 == 0 ? 2 : 1;
|
||||
}
|
||||
|
||||
public int refinementSpacing() {
|
||||
return refinementSpacing(sampleSpacing);
|
||||
}
|
||||
|
||||
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 maximumIncision,
|
||||
double shoreWidth,
|
||||
Banks banks
|
||||
) {
|
||||
public Surface {
|
||||
if (sources == null || banks == null) {
|
||||
throw new IllegalArgumentException("Surface source and bank settings are required.");
|
||||
}
|
||||
requireRange(minimumWidth, maximumWidth, 1, 256, "surface width");
|
||||
requireRange(minimumDepth, maximumDepth, 1, 64, "surface depth");
|
||||
if (maximumIncision < 0 || !Double.isFinite(shoreWidth) || shoreWidth < 0D || shoreWidth > 32D) {
|
||||
throw new IllegalArgumentException("Surface incision and shore width are invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record Banks(
|
||||
int inset,
|
||||
int freeboard,
|
||||
double blendSlope,
|
||||
int minimumBlendWidth,
|
||||
int maximumBlendWidth,
|
||||
double roughness,
|
||||
int roughnessWavelength,
|
||||
int cascadeRun,
|
||||
int waterfallMinimumDrop,
|
||||
double mouthFlareRatio,
|
||||
double springWidthRatio,
|
||||
int springLength,
|
||||
boolean exposeCutStrata
|
||||
) {
|
||||
// Structural invariants only; authoring bounds live in the pack validator.
|
||||
public Banks {
|
||||
if (inset < 0 || freeboard < 0
|
||||
|| !Double.isFinite(blendSlope) || blendSlope <= 0D
|
||||
|| minimumBlendWidth < 1 || maximumBlendWidth < minimumBlendWidth
|
||||
|| !Double.isFinite(roughness) || roughness < 0D || roughness > 1D
|
||||
|| roughnessWavelength < 1
|
||||
|| cascadeRun < 1
|
||||
|| waterfallMinimumDrop < 1
|
||||
|| !Double.isFinite(mouthFlareRatio) || mouthFlareRatio < 1D
|
||||
|| !Double.isFinite(springWidthRatio) || springWidthRatio < 1D
|
||||
|| springLength < 1) {
|
||||
throw new IllegalArgumentException("Surface bank settings are invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Banks defaults() {
|
||||
return new Banks(1, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** A standing surface pool profile: a bowl of the given radius and depth, filled with its own fluid. */
|
||||
public record SurfacePool(
|
||||
String id,
|
||||
boolean enabled,
|
||||
double density,
|
||||
int spacing,
|
||||
int minimumRadius,
|
||||
int maximumRadius,
|
||||
int depth,
|
||||
int maximumPerTile,
|
||||
String biomeKey
|
||||
) {
|
||||
public SurfacePool {
|
||||
if (id == null || id.isBlank()) {
|
||||
throw new IllegalArgumentException("Surface pool id must not be blank.");
|
||||
}
|
||||
id = id.trim();
|
||||
if (!Double.isFinite(density) || density < 0D || spacing < 1
|
||||
|| minimumRadius < 1 || maximumRadius < minimumRadius || depth < 1 || maximumPerTile < 0) {
|
||||
throw new IllegalArgumentException("Surface pool settings are invalid.");
|
||||
}
|
||||
biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// The routing gates read one hydraulic threshold: the drop that makes a reach a waterfall.
|
||||
public record Hydraulics(int waterfallMinimumDrop) {
|
||||
public Hydraulics {
|
||||
if (waterfallMinimumDrop < 1) {
|
||||
throw new IllegalArgumentException("waterfallMinimumDrop must be at least one block.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
+21
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+107
@@ -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,235 @@
|
||||
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,
|
||||
double bankMultiplier,
|
||||
String parentBiomeKey,
|
||||
String surfaceBiomeKey,
|
||||
String mouthBiomeKey,
|
||||
String shoreBiomeKey,
|
||||
String bankBiomeKey,
|
||||
String floodedCaveBiomeKey,
|
||||
List<String> preferredProfileKeys,
|
||||
List<String> surfacePoolKeys
|
||||
) {
|
||||
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");
|
||||
requireFinitePositive(bankMultiplier, "bankMultiplier");
|
||||
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);
|
||||
bankBiomeKey = normalizeKey(bankBiomeKey, parentBiomeKey);
|
||||
floodedCaveBiomeKey = normalizeKey(floodedCaveBiomeKey, surfaceBiomeKey);
|
||||
preferredProfileKeys = normalizeProfiles(preferredProfileKeys);
|
||||
surfacePoolKeys = surfacePoolKeys == null ? List.of() : List.copyOf(surfacePoolKeys);
|
||||
}
|
||||
|
||||
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,
|
||||
1D,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
List.of("default"),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
1D,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
List.of("default"),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
public HydrologyTerrainSample withSurfacePoolKeys(List<String> replacementPoolKeys) {
|
||||
return new HydrologyTerrainSample(
|
||||
naturalHeight,
|
||||
slope,
|
||||
ocean,
|
||||
caveAvailable,
|
||||
caveFloorY,
|
||||
caveFluidY,
|
||||
transitAllowed,
|
||||
outletAllowed,
|
||||
surfaceSourceAllowed,
|
||||
surfaceSourceRequired,
|
||||
undergroundSourceAllowed,
|
||||
undergroundSourceRequired,
|
||||
routingCost,
|
||||
surfaceSourceWeight,
|
||||
undergroundSourceWeight,
|
||||
widthMultiplier,
|
||||
depthMultiplier,
|
||||
incisionMultiplier,
|
||||
routingMultiplier,
|
||||
bankMultiplier,
|
||||
parentBiomeKey,
|
||||
surfaceBiomeKey,
|
||||
mouthBiomeKey,
|
||||
shoreBiomeKey,
|
||||
bankBiomeKey,
|
||||
floodedCaveBiomeKey,
|
||||
preferredProfileKeys,
|
||||
replacementPoolKeys
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
bankMultiplier,
|
||||
parentBiomeKey,
|
||||
surfaceBiomeKey,
|
||||
mouthBiomeKey,
|
||||
shoreBiomeKey,
|
||||
bankBiomeKey,
|
||||
floodedCaveBiomeKey,
|
||||
preferredProfileKeys,
|
||||
surfacePoolKeys
|
||||
);
|
||||
}
|
||||
|
||||
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,108 @@
|
||||
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);
|
||||
boolean independent = type == RiverCourseType.DEEP_FLUID || type == RiverCourseType.SURFACE_POOL;
|
||||
if (independent && (sourceNodeId.isPresent() || outletId.isPresent())) {
|
||||
throw new IllegalArgumentException("Independent deep-fluid and pool courses cannot reference drainage nodes or river outlets.");
|
||||
}
|
||||
if (!independent && (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()
|
||||
|| 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,8 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public enum RiverCourseType {
|
||||
SURFACE,
|
||||
UNDERGROUND,
|
||||
DEEP_FLUID,
|
||||
SURFACE_POOL
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record CaveVoxelPrecondition(CaveVoxel voxel, boolean openToSurface) {
|
||||
public CaveVoxelPrecondition {
|
||||
Objects.requireNonNull(voxel);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user