mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +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:
commit
665b201e41
330 files changed
+60150
-606
No files matched your search
+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>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = 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>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<Long, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||||
private volatile KMap<String, Holder<Biome>> customBiomes;
|
private volatile KMap<String, Holder<Biome>> customBiomes;
|
||||||
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||||
private volatile IrisDimension cacheDimension;
|
private volatile IrisDimension cacheDimension;
|
||||||
@@ -331,8 +332,18 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
if (quartStep == 1) {
|
if (quartStep == 1) {
|
||||||
return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler);
|
return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler);
|
||||||
}
|
}
|
||||||
return super.findBiomeHorizontal(
|
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_ring_biome");
|
||||||
x, y, z, searchRadius, quartStep, allowed, random, false, sampler);
|
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) {
|
static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
|
||||||
@@ -390,6 +401,60 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return resolvedSurfaceHolder;
|
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) {
|
private boolean isGuaranteedSurfaceBiome(int quartY) {
|
||||||
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
|
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -417,6 +482,23 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return holder;
|
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) {
|
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
|
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
|
||||||
if (lease == null) {
|
if (lease == null) {
|
||||||
@@ -506,6 +588,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
noiseBiomeCache.clear();
|
noiseBiomeCache.clear();
|
||||||
structureBiomeCache.clear();
|
structureBiomeCache.clear();
|
||||||
surfaceStructureBiomeCache.clear();
|
surfaceStructureBiomeCache.clear();
|
||||||
|
naturalSurfaceStructureBiomeCache.clear();
|
||||||
customBiomes = refreshedCustomBiomes;
|
customBiomes = refreshedCustomBiomes;
|
||||||
vanillaSpawnBiomes = refreshedSpawnBiomes;
|
vanillaSpawnBiomes = refreshedSpawnBiomes;
|
||||||
cacheDimension = dimension;
|
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_Y = 0"));
|
||||||
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112"));
|
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("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(
|
assertTrue(source.contains(
|
||||||
"return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler)"));
|
"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
|
@Test
|
||||||
|
|||||||
+6
-1
@@ -2,6 +2,11 @@ package art.arcane.iris.api.terrain;
|
|||||||
|
|
||||||
public enum IrisColumnField {
|
public enum IrisColumnField {
|
||||||
SURFACE_HEIGHT,
|
SURFACE_HEIGHT,
|
||||||
|
NATURAL_HEIGHT,
|
||||||
SURFACE_KIND,
|
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
|
@FunctionalInterface
|
||||||
public interface IrisColumnSink {
|
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,
|
UNKNOWN,
|
||||||
LAND,
|
LAND,
|
||||||
SHORE,
|
SHORE,
|
||||||
|
RIVER,
|
||||||
|
RIVER_SHORE,
|
||||||
|
DRY_CHANNEL,
|
||||||
OCEAN,
|
OCEAN,
|
||||||
VOID
|
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.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.framework.StructureReachability;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
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.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
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.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.director.specialhandlers.StructureHandler;
|
||||||
import art.arcane.iris.util.common.format.C;
|
import art.arcane.iris.util.common.format.C;
|
||||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
@@ -68,7 +73,7 @@ import java.util.Set;
|
|||||||
public class CommandFind implements DirectorExecutor {
|
public class CommandFind implements DirectorExecutor {
|
||||||
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
||||||
public void 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,
|
IrisBiome biome,
|
||||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported", defaultValue = "true")
|
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported", defaultValue = "true")
|
||||||
boolean teleport
|
boolean teleport
|
||||||
@@ -116,6 +121,75 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
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)
|
@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(
|
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)
|
@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.PackValidationRegistry;
|
||||||
import art.arcane.iris.core.pack.PackValidationResult;
|
import art.arcane.iris.core.pack.PackValidationResult;
|
||||||
import art.arcane.iris.core.pack.PackValidator;
|
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.DirectorExecutor;
|
||||||
|
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
|
||||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
import art.arcane.volmlib.util.director.annotations.Director;
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
import art.arcane.volmlib.util.director.annotations.Param;
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
@@ -90,6 +93,18 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
persistValidationCache(packsRoot);
|
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"})
|
@Director(description = "Preview or apply unused-resource cleanup", descriptionKey = "iris.director.commandpack.director.preview_apply_unused_resource_cleanup", aliases = {"c"})
|
||||||
public void cleanup(
|
public void cleanup(
|
||||||
@Param(description = "The pack folder name to clean", descriptionKey = "iris.director.commandpack.param.pack_folder_name_clean")
|
@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));
|
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)
|
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
|
||||||
public void profile(
|
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)
|
@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) {
|
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||||
IrisComplex complex = engine.getComplex();
|
IrisComplex complex = engine.getComplex();
|
||||||
File file = switch (type) {
|
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();
|
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||||
case BIOME_SEA -> complex.getSeaBiomeStream().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.IrisColumnField;
|
||||||
import art.arcane.iris.api.terrain.IrisColumnQuery;
|
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.IrisColumnSink;
|
||||||
|
import art.arcane.iris.api.terrain.IrisRiverState;
|
||||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
import art.arcane.iris.api.terrain.IrisTerrainService;
|
||||||
import art.arcane.iris.api.terrain.IrisWorldInfo;
|
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.IrisSurfaceClassifier;
|
||||||
import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory;
|
import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.InferredType;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
@@ -101,13 +105,15 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
int surface = engine.getHeight(blockX, blockZ);
|
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;
|
InferredType inferredType = null;
|
||||||
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
|
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
|
||||||
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
||||||
inferredType = biome == null ? null : biome.getInferredType();
|
inferredType = biome == null ? null : biome.getInferredType();
|
||||||
}
|
}
|
||||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType);
|
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, hydrology);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
reportQueryFault("surfaceKind", world, error);
|
reportQueryFault("surfaceKind", world, error);
|
||||||
return IrisSurfaceKind.UNKNOWN;
|
return IrisSurfaceKind.UNKNOWN;
|
||||||
@@ -224,26 +230,75 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
|||||||
|
|
||||||
EnumSet<IrisColumnField> fields = query.fields();
|
EnumSet<IrisColumnField> fields = query.fields();
|
||||||
boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT);
|
boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT);
|
||||||
|
boolean wantNaturalHeight = fields.contains(IrisColumnField.NATURAL_HEIGHT);
|
||||||
boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND);
|
boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND);
|
||||||
boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY);
|
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 {
|
try {
|
||||||
int minHeight = engine.getMinHeight();
|
int minHeight = engine.getMinHeight();
|
||||||
int fluid = engine.getDimension().getFluidHeight();
|
|
||||||
long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
|
long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
|
||||||
if (engine.isClosed()) {
|
if (engine.isClosed()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HydrologyColumnSample hydrology = wantRiver
|
||||||
|
? hydrologySample(engine, blockX, blockZ)
|
||||||
|
: null;
|
||||||
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
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
|
boolean needsBiome = wantBiome
|
||||||
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
||||||
IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null;
|
IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null;
|
||||||
IrisSurfaceKind kind = wantKind
|
IrisSurfaceKind kind = wantKind
|
||||||
? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType())
|
? IrisSurfaceClassifier.classify(
|
||||||
|
surface,
|
||||||
|
fluid,
|
||||||
|
biome == null ? null : biome.getInferredType(),
|
||||||
|
hydrology
|
||||||
|
)
|
||||||
: IrisSurfaceKind.UNKNOWN;
|
: IrisSurfaceKind.UNKNOWN;
|
||||||
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
|
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 true;
|
||||||
});
|
});
|
||||||
return visited == query.columnCount();
|
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) {
|
private static Optional<String> key(IrisBiome biome) {
|
||||||
String loadKey = biome == null ? null : biome.getLoadKey();
|
String loadKey = biome == null ? null : biome.getLoadKey();
|
||||||
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
|
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
|
||||||
|
|||||||
+28
@@ -1,6 +1,8 @@
|
|||||||
package art.arcane.iris.core.service.terrain;
|
package art.arcane.iris.core.service.terrain;
|
||||||
|
|
||||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
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;
|
import art.arcane.iris.engine.object.InferredType;
|
||||||
|
|
||||||
public final class IrisSurfaceClassifier {
|
public final class IrisSurfaceClassifier {
|
||||||
@@ -22,4 +24,30 @@ public final class IrisSurfaceClassifier {
|
|||||||
|
|
||||||
return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND;
|
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;
|
package art.arcane.iris.core.commands;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
@@ -13,22 +15,31 @@ import java.util.Map;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class CommandObjectFluidHeightTest {
|
public class CommandObjectFluidHeightTest {
|
||||||
@Test
|
@Test
|
||||||
public void absoluteObjectPlacementShiftsFluidHeightFromNegativeMinY() {
|
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() {
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
Engine engine = mock(Engine.class);
|
Engine engine = mock(Engine.class);
|
||||||
IrisDimension dimension = mock(IrisDimension.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<>();
|
Map<Block, BlockData> future = new HashMap<>();
|
||||||
|
|
||||||
when(engine.getMinHeight()).thenReturn(-64);
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
when(engine.getDimension()).thenReturn(dimension);
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
when(dimension.getFluidHeight()).thenReturn(127);
|
when(dimension.getFluidHeight()).thenReturn(127);
|
||||||
|
when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead);
|
||||||
|
when(riverHead.get(12, -7)).thenReturn(131D);
|
||||||
|
|
||||||
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
||||||
|
|
||||||
assertEquals(63, placer.getFluidHeight());
|
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;
|
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.compat.DirectorAnnotationCompatibility;
|
||||||
import art.arcane.volmlib.util.director.runtime.DirectorNodeDescriptor;
|
import art.arcane.volmlib.util.director.runtime.DirectorNodeDescriptor;
|
||||||
import art.arcane.volmlib.util.director.runtime.DirectorParameterDescriptor;
|
import art.arcane.volmlib.util.director.runtime.DirectorParameterDescriptor;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -52,8 +54,23 @@ public class CommandPackOptionalPackContractTest {
|
|||||||
assertFalse(CommandPack.wantsAllPacks("overworld"));
|
assertFalse(CommandPack.wantsAllPacks("overworld"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static java.lang.reflect.Method findMethod(String name) {
|
@Test
|
||||||
for (java.lang.reflect.Method method : CommandPack.class.getDeclaredMethods()) {
|
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)) {
|
if (method.getName().equals(name)) {
|
||||||
return method;
|
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.IrisColumnField;
|
||||||
import art.arcane.iris.api.terrain.IrisColumnQuery;
|
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.IrisSurfaceKind;
|
||||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
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 art.arcane.iris.util.common.plugin.IrisService;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -12,6 +17,7 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@@ -82,9 +88,7 @@ public class IrisTerrainSVCTest {
|
|||||||
IrisTerrainSVC service = new IrisTerrainSVC();
|
IrisTerrainSVC service = new IrisTerrainSVC();
|
||||||
AtomicInteger sinkCalls = new AtomicInteger();
|
AtomicInteger sinkCalls = new AtomicInteger();
|
||||||
|
|
||||||
boolean answered = service.sampleColumns(null, SMALL,
|
boolean answered = service.sampleColumns(null, SMALL, sample -> sinkCalls.incrementAndGet());
|
||||||
(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey)
|
|
||||||
-> sinkCalls.incrementAndGet());
|
|
||||||
|
|
||||||
assertFalse(answered);
|
assertFalse(answered);
|
||||||
assertEquals(0, sinkCalls.get());
|
assertEquals(0, sinkCalls.get());
|
||||||
@@ -97,4 +101,48 @@ public class IrisTerrainSVCTest {
|
|||||||
assertFalse(service.sampleColumns(null, null, null));
|
assertFalse(service.sampleColumns(null, null, null));
|
||||||
assertFalse(service.sampleColumns(null, SMALL, 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;
|
package art.arcane.iris.core.service.terrain;
|
||||||
|
|
||||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
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 art.arcane.iris.engine.object.InferredType;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
@@ -53,9 +59,108 @@ public class IrisSurfaceClassifierTest {
|
|||||||
|
|
||||||
IrisSurfaceKind expected = IrisSurfaceClassifier.classify(surface, FLUID, null);
|
IrisSurfaceKind expected = IrisSurfaceClassifier.classify(surface, FLUID, null);
|
||||||
for (InferredType inferredType : InferredType.values()) {
|
for (InferredType inferredType : InferredType.values()) {
|
||||||
assertEquals("surface=" + surface + " type=" + inferredType,
|
assertEquals(
|
||||||
expected, IrisSurfaceClassifier.classify(surface, FLUID, inferredType));
|
"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}")
|
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
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:
|
// 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
|
// fabric-registry-sync-v0 delays registry freeze past mod init, which is what lets
|
||||||
// IrisFabricBootstrap register the CHUNK_GENERATOR codec in
|
// IrisFabricBootstrap register the CHUNK_GENERATOR codec in
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ dependencies {
|
|||||||
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||||
|
testImplementation(libs.spigot) {
|
||||||
|
transitive = false
|
||||||
|
}
|
||||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||||
compileOnly(libs.spigot) {
|
compileOnly(libs.spigot) {
|
||||||
transitive = false
|
transitive = false
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.IrisEngineMantle;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -41,7 +42,7 @@ public final class MainWorldService {
|
|||||||
"region",
|
"region",
|
||||||
"entities",
|
"entities",
|
||||||
"poi",
|
"poi",
|
||||||
"mantle",
|
IrisEngineMantle.STORAGE_FOLDER_NAME,
|
||||||
"dimensions/minecraft/overworld",
|
"dimensions/minecraft/overworld",
|
||||||
"dimensions/minecraft/the_nether",
|
"dimensions/minecraft/the_nether",
|
||||||
"dimensions/minecraft/the_end",
|
"dimensions/minecraft/the_end",
|
||||||
|
|||||||
+7
-1
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.IrisEngineMantle;
|
||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
@@ -33,7 +34,12 @@ import java.util.List;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public final class ModdedDimensionStorage {
|
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() {
|
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.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.framework.StructureReachability;
|
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.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
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> 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> 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> 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> PACK_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
||||||
static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
||||||
|
|
||||||
@@ -79,7 +82,7 @@ final class ModdedCommandSuggestions {
|
|||||||
try {
|
try {
|
||||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||||
if (engine != null) {
|
if (engine != null) {
|
||||||
return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder);
|
return SharedSuggestionProvider.suggest(reachableBiomeKeys(engine), builder);
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
warnTabFailure("biome keys", context.getSource(), e);
|
warnTabFailure("biome keys", context.getSource(), e);
|
||||||
@@ -87,6 +90,46 @@ final class ModdedCommandSuggestions {
|
|||||||
return builder.buildFuture();
|
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) {
|
private static CompletableFuture<Suggestions> suggestRegionKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||||
ModdedCommandFeedback.tab(context.getSource());
|
ModdedCommandFeedback.tab(context.getSource());
|
||||||
try {
|
try {
|
||||||
|
|||||||
+3
@@ -227,6 +227,9 @@ final class ModdedCommandTree {
|
|||||||
.then(Commands.literal("object")
|
.then(Commands.literal("object")
|
||||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS)
|
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS)
|
||||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key")))))
|
.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")
|
.then(Commands.literal("unregistered")
|
||||||
.executes((CommandContext<CommandSourceStack> context) ->
|
.executes((CommandContext<CommandSourceStack> context) ->
|
||||||
ModdedUnregisteredStructures.print(context.getSource())))
|
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.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.framework.StructureReachability;
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||||
import art.arcane.iris.util.project.context.IrisContext;
|
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.localization.MessageArgument;
|
||||||
import art.arcane.volmlib.util.math.Position2;
|
import art.arcane.volmlib.util.math.Position2;
|
||||||
import com.mojang.datafixers.util.Pair;
|
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)));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
|
||||||
return 0;
|
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());
|
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -137,6 +145,67 @@ final class ModdedLocateCommands {
|
|||||||
return 1;
|
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) {
|
static int gotoStructure(CommandSourceStack source, String keyRaw) {
|
||||||
ServerLevel level = source.getLevel();
|
ServerLevel level = source.getLevel();
|
||||||
Engine engine = IrisModdedCommands.engineFor(level);
|
Engine engine = IrisModdedCommands.engineFor(level);
|
||||||
|
|||||||
+4
-6
@@ -341,8 +341,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
}
|
}
|
||||||
markCompleted();
|
markCompleted();
|
||||||
listener.onChunkGenerated(x, z);
|
listener.onChunkGenerated(x, z);
|
||||||
cleanupMantleChunk(x, z);
|
cleanupMantleChunksCoveredBy(x, z, listener);
|
||||||
listener.onChunkCleaned(x, z);
|
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
} catch (TimeoutException | ExecutionException e) {
|
} catch (TimeoutException | ExecutionException e) {
|
||||||
@@ -406,8 +405,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
onSuccess();
|
onSuccess();
|
||||||
markCompleted();
|
markCompleted();
|
||||||
listener.onChunkGenerated(x, z);
|
listener.onChunkGenerated(x, z);
|
||||||
cleanupMantleChunk(x, z);
|
cleanupMantleChunksCoveredBy(x, z, listener);
|
||||||
listener.onChunkCleaned(x, z);
|
|
||||||
} finally {
|
} finally {
|
||||||
markFinished();
|
markFinished();
|
||||||
semaphore.release();
|
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 {
|
try {
|
||||||
engine.getMantle().forceCleanupChunk(x, z);
|
engine.getMantle().cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
ModdedIrisLog.debug("Iris pregen mantle cleanup skipped for {},{}: {}", x, z, e.toString());
|
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();
|
IrisComplex complex = engine.getComplex();
|
||||||
File file = switch (type) {
|
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();
|
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||||
case BIOME_SEA -> complex.getSeaBiomeStream().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();
|
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
|
||||||
int minHeight = engine.getWorld().minHeight();
|
int minHeight = engine.getWorld().minHeight();
|
||||||
int baseX = chunkX << 4;
|
int baseX = chunkX << 4;
|
||||||
|
|||||||
+11
@@ -187,6 +187,17 @@ public class ModdedGenerationLeaseContractTest {
|
|||||||
assertTrue(logFailure.contains("cause.toString()"));
|
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
|
@Test
|
||||||
public void invalidPregenBoundsFailBeforeRuntimeMutation() throws IOException {
|
public void invalidPregenBoundsFailBeforeRuntimeMutation() throws IOException {
|
||||||
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
|
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;
|
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.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.MantleChunk;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||||
import art.arcane.volmlib.util.matter.Matter;
|
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.junit.Test;
|
||||||
|
import org.mockito.InOrder;
|
||||||
|
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.ArrayList;
|
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.assertFalse;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
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 {
|
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
|
@Test
|
||||||
public void scansWhenPlayersArePresent() {
|
public void scansWhenPlayersArePresent() {
|
||||||
assertTrue(ModdedChunkUpdateService.hasUpdateTargets(true, false));
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private static MantleDataAdapter<Matter> emptyAdapter() {
|
private static MantleDataAdapter<Matter> emptyAdapter() {
|
||||||
return (MantleDataAdapter<Matter>) Proxy.newProxyInstance(
|
return (MantleDataAdapter<Matter>) Proxy.newProxyInstance(
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
|||||||
dependencies {
|
dependencies {
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
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.platform:junit-platform-launcher:6.1.2')
|
||||||
testRuntimeOnly('org.junit.vintage:junit-vintage-engine:6.1.2')
|
testRuntimeOnly('org.junit.vintage:junit-vintage-engine:6.1.2')
|
||||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.render.IrisRenderer;
|
import art.arcane.iris.engine.framework.render.IrisRenderer;
|
||||||
import art.arcane.iris.engine.framework.render.RenderType;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
@@ -69,6 +71,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -484,11 +487,52 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void renderLegend(Graphics2D canvas) {
|
private void renderLegend(Graphics2D canvas) {
|
||||||
if (currentType == RenderType.HEIGHT) {
|
if (currentType == RenderType.RIVER) {
|
||||||
|
renderRiverLegend(canvas);
|
||||||
|
} else if (currentType == RenderType.HEIGHT) {
|
||||||
renderHeightLegend(canvas);
|
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) {
|
private void renderHeightLegend(Graphics2D canvas) {
|
||||||
int width = 244;
|
int width = 244;
|
||||||
int height = 54;
|
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 BIOME_SEA -> DesktopUiMessages.VISION_MODE_BIOME_SEA;
|
||||||
case REGION -> DesktopUiMessages.VISION_MODE_REGION;
|
case REGION -> DesktopUiMessages.VISION_MODE_REGION;
|
||||||
case CAVE_LAND -> DesktopUiMessages.VISION_MODE_CAVE_LAND;
|
case CAVE_LAND -> DesktopUiMessages.VISION_MODE_CAVE_LAND;
|
||||||
|
case RIVER -> DesktopUiMessages.VISION_MODE_RIVER;
|
||||||
case HEIGHT -> DesktopUiMessages.VISION_MODE_HEIGHT;
|
case HEIGHT -> DesktopUiMessages.VISION_MODE_HEIGHT;
|
||||||
case OBJECT_LOAD -> DesktopUiMessages.VISION_MODE_OBJECT_LOAD;
|
case OBJECT_LOAD -> DesktopUiMessages.VISION_MODE_OBJECT_LOAD;
|
||||||
case DECORATOR_LOAD -> DesktopUiMessages.VISION_MODE_DECORATOR_LOAD;
|
case DECORATOR_LOAD -> DesktopUiMessages.VISION_MODE_DECORATOR_LOAD;
|
||||||
|
|||||||
@@ -275,7 +275,8 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
if (!isCurrent(work)) {
|
if (!isCurrent(work)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
while (work.inFlight() < renderWorkerCount) {
|
int admissionLimit = work.frame().spec().type() == RenderType.RIVER ? 1 : renderWorkerCount;
|
||||||
|
while (work.inFlight() < admissionLimit) {
|
||||||
VisibleTile tile = work.nextMissing();
|
VisibleTile tile = work.nextMissing();
|
||||||
if (tile == null) {
|
if (tile == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
|
|||||||
|
|
||||||
import art.arcane.iris.core.IrisWorldStorage;
|
import art.arcane.iris.core.IrisWorldStorage;
|
||||||
import art.arcane.iris.core.WorldSlotKey;
|
import art.arcane.iris.core.WorldSlotKey;
|
||||||
|
import art.arcane.iris.engine.IrisEngineMantle;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
import org.bukkit.configuration.InvalidConfigurationException;
|
import org.bukkit.configuration.InvalidConfigurationException;
|
||||||
@@ -253,7 +254,9 @@ public final class BukkitWorldConfiguration {
|
|||||||
if (Files.isDirectory(worldRoot.resolve("iris"), LinkOption.NOFOLLOW_LINKS)) {
|
if (Files.isDirectory(worldRoot.resolve("iris"), LinkOption.NOFOLLOW_LINKS)) {
|
||||||
return true;
|
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))) {
|
if (hasEntries(worldRoot.resolve(directory))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
|
|||||||
|
|
||||||
import art.arcane.iris.core.IrisWorldStorage;
|
import art.arcane.iris.core.IrisWorldStorage;
|
||||||
import art.arcane.iris.core.WorldSlotKey;
|
import art.arcane.iris.core.WorldSlotKey;
|
||||||
|
import art.arcane.iris.engine.IrisEngineMantle;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.AtomicMoveNotSupportedException;
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
@@ -37,7 +38,9 @@ import java.util.stream.Stream;
|
|||||||
public final class HuskWorldQuarantine {
|
public final class HuskWorldQuarantine {
|
||||||
private static final String IRIS_NAMESPACE = "iris";
|
private static final String IRIS_NAMESPACE = "iris";
|
||||||
private static final Pattern MANAGED_KEY = Pattern.compile("[a-z0-9_-]+");
|
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 Set<String> OS_METADATA_FILES = Set.of(".DS_Store", "Thumbs.db", "desktop.ini");
|
||||||
private static final String APPLE_DOUBLE_PREFIX = "._";
|
private static final String APPLE_DOUBLE_PREFIX = "._";
|
||||||
private static final DateTimeFormatter STAMP = DateTimeFormatter
|
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_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_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_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_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_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");
|
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_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_FPS,
|
||||||
VISION_HELP_GRID, VISION_HELP_BIOME, VISION_HELP_TELEPORT, VISION_HELP_EDITOR, VISION_OPENED,
|
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_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,
|
VISION_MODE_DECORATOR_LOAD, VISION_MODE_CONTINENT, VISION_MODE_LAYER_LOAD, NOISE_TITLE,
|
||||||
NOISE_TITLE_GENERATOR, NOISE_SEARCH, NOISE_STATUS, NOISE_CATEGORY_CUSTOM,
|
NOISE_TITLE_GENERATOR, NOISE_SEARCH, NOISE_STATUS, NOISE_CATEGORY_CUSTOM,
|
||||||
NOISE_CATEGORY_PACK_GENERATORS, NOISE_CATEGORY_SIMPLEX, NOISE_CATEGORY_PERLIN,
|
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);
|
packFolder, dimensionFiles, validateLiveRegistries);
|
||||||
addDistinct(blockingErrors, imageMaps.errors());
|
addDistinct(blockingErrors, imageMaps.errors());
|
||||||
addDistinct(warnings, imageMaps.warnings());
|
addDistinct(warnings, imageMaps.warnings());
|
||||||
|
PackHydrologyValidator.Validation hydrologyValidation =
|
||||||
|
PackHydrologyValidator.validate(packFolder, dimensionFiles);
|
||||||
|
addDistinct(blockingErrors, hydrologyValidation.errors());
|
||||||
|
addDistinct(warnings, hydrologyValidation.warnings());
|
||||||
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
|
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
|
||||||
PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder);
|
PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder);
|
||||||
addDistinct(blockingErrors, lootIssues.errors());
|
addDistinct(blockingErrors, lootIssues.errors());
|
||||||
|
|||||||
@@ -481,8 +481,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
listener.onChunkFailed(x, z);
|
listener.onChunkFailed(x, z);
|
||||||
} else {
|
} else {
|
||||||
listener.onChunkGenerated(x, z);
|
listener.onChunkGenerated(x, z);
|
||||||
cleanupMantleChunk(x, z);
|
cleanupMantleChunksCoveredBy(x, z, listener);
|
||||||
listener.onChunkCleaned(x, z);
|
|
||||||
onChunkCompleted(x, z, chunk);
|
onChunkCompleted(x, z, chunk);
|
||||||
success = true;
|
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();
|
Engine engine = resolveMetricsEngine();
|
||||||
if (engine != null) {
|
if (engine != null) {
|
||||||
try {
|
try {
|
||||||
engine.getMantle().forceCleanupChunk(x, z);
|
engine.getMantle().cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned);
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
|
|||||||
@Accessors(fluent = true, chain = true)
|
@Accessors(fluent = true, chain = true)
|
||||||
public class IrisCreator {
|
public class IrisCreator {
|
||||||
private static final long WORLD_CREATE_TIMEOUT_SECONDS = 120L;
|
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 WORLD_ENTRY_TELEPORT_TIMEOUT_SECONDS = 60L;
|
||||||
private static final long ROLLBACK_PHASE_TIMEOUT_SECONDS = 120L;
|
private static final long ROLLBACK_PHASE_TIMEOUT_SECONDS = 120L;
|
||||||
|
|
||||||
@@ -619,7 +620,7 @@ public class IrisCreator {
|
|||||||
generator.getInitialSpawnReady(),
|
generator.getInitialSpawnReady(),
|
||||||
"Initial spawn preparation future");
|
"Initial spawn preparation future");
|
||||||
try {
|
try {
|
||||||
initialSpawnReady.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
initialSpawnReady.get(INITIAL_SPAWN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
} catch (TimeoutException failure) {
|
} catch (TimeoutException failure) {
|
||||||
throw new TimeoutException("Initial spawn preparation timed out for world \""
|
throw new TimeoutException("Initial spawn preparation timed out for world \""
|
||||||
+ worldName + "\".");
|
+ worldName + "\".");
|
||||||
|
|||||||
@@ -22,14 +22,27 @@ import art.arcane.iris.core.IrisSettings;
|
|||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.engine.data.cache.Cache;
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.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.image.IrisImageMapRuntime;
|
||||||
|
import art.arcane.iris.engine.mantle.components.MantleHydrologyCaveVoxelView;
|
||||||
import art.arcane.iris.engine.object.InferredType;
|
import art.arcane.iris.engine.object.InferredType;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisDecorationPart;
|
import art.arcane.iris.engine.object.IrisDecorationPart;
|
||||||
import art.arcane.iris.engine.object.IrisDecorator;
|
import art.arcane.iris.engine.object.IrisDecorator;
|
||||||
import art.arcane.iris.engine.object.IrisGenerator;
|
import art.arcane.iris.engine.object.IrisGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
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.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.engine.object.IrisShapedGeneratorStyle;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
@@ -58,6 +71,7 @@ import java.util.EnumMap;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -66,8 +80,8 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime"})
|
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime"})
|
||||||
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime"})
|
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime"})
|
||||||
public class IrisComplex implements DataProvider {
|
public class IrisComplex implements DataProvider {
|
||||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||||
@@ -109,14 +123,20 @@ public class IrisComplex implements DataProvider {
|
|||||||
private ProceduralStream<IrisBiome> shoreBiomeStream;
|
private ProceduralStream<IrisBiome> shoreBiomeStream;
|
||||||
private ProceduralStream<IrisBiome> baseBiomeStream;
|
private ProceduralStream<IrisBiome> baseBiomeStream;
|
||||||
private ProceduralStream<UUID> baseBiomeIDStream;
|
private ProceduralStream<UUID> baseBiomeIDStream;
|
||||||
|
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
|
||||||
private ProceduralStream<IrisBiome> trueBiomeStream;
|
private ProceduralStream<IrisBiome> trueBiomeStream;
|
||||||
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
|
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
|
||||||
|
private ProceduralStream<Double> naturalHeightStream;
|
||||||
private ProceduralStream<Double> heightStream;
|
private ProceduralStream<Double> heightStream;
|
||||||
private ProceduralStream<Integer> roundedHeighteightStream;
|
private ProceduralStream<Integer> roundedHeighteightStream;
|
||||||
private ProceduralStream<Double> maxHeightStream;
|
private ProceduralStream<Double> maxHeightStream;
|
||||||
private ProceduralStream<Double> overlayStream;
|
private ProceduralStream<Double> overlayStream;
|
||||||
private ProceduralStream<Double> heightFluidStream;
|
private ProceduralStream<Double> heightFluidStream;
|
||||||
private ProceduralStream<Double> slopeStream;
|
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<Integer> topSurfaceStream;
|
||||||
private ProceduralStream<IrisDecorator> terrainSurfaceDecoration;
|
private ProceduralStream<IrisDecorator> terrainSurfaceDecoration;
|
||||||
private ProceduralStream<IrisDecorator> terrainCeilingDecoration;
|
private ProceduralStream<IrisDecorator> terrainCeilingDecoration;
|
||||||
@@ -127,10 +147,12 @@ public class IrisComplex implements DataProvider {
|
|||||||
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
|
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
|
||||||
private ProceduralStream<PlatformBlockState> rockStream;
|
private ProceduralStream<PlatformBlockState> rockStream;
|
||||||
private ProceduralStream<PlatformBlockState> fluidStream;
|
private ProceduralStream<PlatformBlockState> fluidStream;
|
||||||
|
private Map<String, ProceduralStream<PlatformBlockState>> hydrologyFluidStreams;
|
||||||
private IrisBiome focusBiome;
|
private IrisBiome focusBiome;
|
||||||
private IrisRegion focusRegion;
|
private IrisRegion focusRegion;
|
||||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||||
private Set<IrisBiome> generatorBiomes;
|
private Set<IrisBiome> generatorBiomes;
|
||||||
|
private IrisHydrologyRuntime hydrologyRuntime;
|
||||||
private transient IrisImageMapRuntime imageMapRuntime;
|
private transient IrisImageMapRuntime imageMapRuntime;
|
||||||
// Copy-on-write: reads happen per column on every burst thread; the synchronizedMap
|
// 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
|
// 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
|
//@builder
|
||||||
if (focusRegion != null) {
|
if (focusRegion != null) {
|
||||||
prepareInferredBiomes(focusRegion, preparedRegions);
|
prepareInferredBiomes(focusRegion, preparedRegions);
|
||||||
focusRegion.getAllBiomes(this).forEach(this::registerGenerators);
|
focusRegion.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||||
} else {
|
} else {
|
||||||
engine.getDimension().getRegions().forEach(regionKey -> {
|
engine.getDimension().getRegions().forEach(regionKey -> {
|
||||||
IrisRegion region = data.getRegionLoader().load(regionKey);
|
IrisRegion region = data.getRegionLoader().load(regionKey);
|
||||||
@@ -174,11 +196,11 @@ public class IrisComplex implements DataProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prepareInferredBiomes(region, preparedRegions);
|
prepareInferredBiomes(region, preparedRegions);
|
||||||
region.getAllBiomes(this).forEach(this::registerGenerators);
|
region.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||||
});
|
});
|
||||||
for (IrisRegion region : imageMapRuntime.getMappedRegions()) {
|
for (IrisRegion region : imageMapRuntime.getMappedRegions()) {
|
||||||
prepareInferredBiomes(region, preparedRegions);
|
prepareInferredBiomes(region, preparedRegions);
|
||||||
region.getAllBiomes(this).forEach(this::registerGenerators);
|
region.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||||
}
|
}
|
||||||
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
|
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
|
||||||
}
|
}
|
||||||
@@ -211,6 +233,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
.select(engine.getDimension().getRockPalette().getBlockData(data));
|
.select(engine.getDimension().getRockPalette().getBlockData(data));
|
||||||
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
||||||
.select(engine.getDimension().getFluidPalette().getBlockData(data));
|
.select(engine.getDimension().getFluidPalette().getBlockData(data));
|
||||||
|
hydrologyFluidStreams = createHydrologyFluidStreams(engine.getDimension().getHydrology());
|
||||||
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
||||||
.zoom(engine.getDimension().getRegionZoom());
|
.zoom(engine.getDimension().getRegionZoom());
|
||||||
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||||
@@ -263,28 +286,59 @@ public class IrisComplex implements DataProvider {
|
|||||||
return mapped == null ? biome : mapped;
|
return mapped == null ? biome : mapped;
|
||||||
})
|
})
|
||||||
.cache2D("imageMappedBaseBiomeStream", engine, cacheSize);
|
.cache2D("imageMappedBaseBiomeStream", engine, cacheSize);
|
||||||
heightStream = ProceduralStream.of((x, z) -> {
|
naturalHeightStream = ProceduralStream.of(
|
||||||
double proceduralHeight = getHeight(engine, x, z, engine.getSeedManager().getHeight());
|
(x, z) -> sampleNaturalTerrainHeight(engine, x, z),
|
||||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
Interpolated.DOUBLE
|
||||||
}, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize);
|
).cache2DDouble("naturalHeightStream", engine, cacheSize);
|
||||||
slopeStream = heightStream.slope(3)
|
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||||
.cache2DDouble("slopeStream", engine, cacheSize);
|
|
||||||
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
|
||||||
b -> focusBiome))
|
b -> focusBiome))
|
||||||
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
|
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
|
||||||
.convertAware2D((h, x, z) -> {
|
.convertAware2D((h, x, z) -> {
|
||||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||||
return mapped == null
|
return mapped == null
|
||||||
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight)
|
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight)
|
||||||
: mapped;
|
: 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))
|
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||||
.round();
|
.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))
|
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||||
.convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize);
|
.convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize);
|
||||||
heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
riverDistanceStream = ProceduralStream.ofDouble(this::resolveHydrologyDistance)
|
||||||
.max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize);
|
.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);
|
maxHeightStream = ProceduralStream.ofDouble((x, z) -> height);
|
||||||
terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
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);
|
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize);
|
||||||
@@ -310,6 +364,320 @@ public class IrisComplex implements DataProvider {
|
|||||||
//@done
|
//@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) {
|
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case CAVE:
|
case CAVE:
|
||||||
@@ -370,6 +738,47 @@ public class IrisComplex implements DataProvider {
|
|||||||
return null;
|
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) {
|
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
|
||||||
IrisBiome resolved = resolveSurfaceBiome(
|
IrisBiome resolved = resolveSurfaceBiome(
|
||||||
height,
|
height,
|
||||||
@@ -528,6 +937,38 @@ public class IrisComplex implements DataProvider {
|
|||||||
return h;
|
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) {
|
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);
|
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);
|
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(
|
static Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compileInferredBiomeStreams(
|
||||||
Iterable<IrisRegion> regions,
|
Iterable<IrisRegion> regions,
|
||||||
BiFunction<IrisRegion, InferredType, ProceduralStream<IrisBiome>> compiler
|
BiFunction<IrisRegion, InferredType, ProceduralStream<IrisBiome>> compiler
|
||||||
@@ -1072,6 +1473,8 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
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.MantleCarvingComponent;
|
||||||
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
|
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
|
||||||
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
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.mantle.components.IrisStructureComponent;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
@@ -72,6 +74,7 @@ import java.util.function.Supplier;
|
|||||||
@EqualsAndHashCode(exclude = "engine")
|
@EqualsAndHashCode(exclude = "engine")
|
||||||
@ToString(exclude = "engine")
|
@ToString(exclude = "engine")
|
||||||
public class IrisEngineMantle implements EngineMantle {
|
public class IrisEngineMantle implements EngineMantle {
|
||||||
|
public static final String STORAGE_FOLDER_NAME = "mantle-hydrology";
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private final Mantle<Matter> mantle;
|
private final Mantle<Matter> mantle;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@@ -86,6 +89,7 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
this.mantle = createMantle(engine);
|
this.mantle = createMantle(engine);
|
||||||
components = new KMap<>();
|
components = new KMap<>();
|
||||||
registerComponent(new MantleCarvingComponent(this));
|
registerComponent(new MantleCarvingComponent(this));
|
||||||
|
registerComponent(new MantleHydrologyComponent(this));
|
||||||
object = new MantleObjectComponent(this);
|
object = new MantleObjectComponent(this);
|
||||||
registerComponent(object);
|
registerComponent(object);
|
||||||
registerComponent(new MantleFloatingObjectComponent(this));
|
registerComponent(new MantleFloatingObjectComponent(this));
|
||||||
@@ -121,7 +125,7 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
List<MantleComponent> pass = passes.get(i);
|
List<MantleComponent> pass = passes.get(i);
|
||||||
int passBlockRadius = pass.stream()
|
int passBlockRadius = pass.stream()
|
||||||
.filter(MantleComponent::isEnabled)
|
.filter(MantleComponent::isEnabled)
|
||||||
.mapToInt(MantleComponent::getRadius)
|
.mapToInt(MantleComponent::getOutputRadius)
|
||||||
.max()
|
.max()
|
||||||
.orElse(0);
|
.orElse(0);
|
||||||
int passInputRadius = pass.stream()
|
int passInputRadius = pass.stream()
|
||||||
@@ -174,10 +178,18 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
if (!getDimension().isCarvingEnabled()) {
|
if (!getDimension().isCarvingEnabled()) {
|
||||||
disabled.addIfMissing(ReservedFlag.CARVED);
|
disabled.addIfMissing(ReservedFlag.CARVED);
|
||||||
}
|
}
|
||||||
|
if (disabled.contains(ReservedFlag.CARVED)
|
||||||
|
|| !isHydrologyEnabled(getDimension())) {
|
||||||
|
disabled.addIfMissing(ReservedFlag.RIVER_HYDROLOGY);
|
||||||
|
}
|
||||||
return Set.copyOf(disabled);
|
return Set.copyOf(disabled);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean isHydrologyEnabled(IrisDimension dimension) {
|
||||||
|
return MantleHydrologyComponent.isEnabledFor(dimension);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MantleObjectComponent getObjectComponent() {
|
public MantleObjectComponent getObjectComponent() {
|
||||||
return object;
|
return object;
|
||||||
@@ -185,7 +197,7 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
|
|
||||||
private static Mantle<Matter> createMantle(Engine engine) {
|
private static Mantle<Matter> createMantle(Engine engine) {
|
||||||
IrisMatterSupport.ensureRegistered();
|
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();
|
int worldHeight = engine.getTarget().getHeight();
|
||||||
MantleDataAdapter<Matter> adapter = createDataAdapter(engine::getData);
|
MantleDataAdapter<Matter> adapter = createDataAdapter(engine::getData);
|
||||||
MantleHooks hooks = createHooks(EnginePanic.scoped("world " + engine.getWorld().name()));
|
MantleHooks hooks = createHooks(EnginePanic.scoped("world " + engine.getWorld().name()));
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
engine.getDimension(),
|
engine.getDimension(),
|
||||||
engine.getData(),
|
engine.getData(),
|
||||||
chunkHeight,
|
chunkHeight,
|
||||||
complex.getHeightStream(),
|
complex.getNaturalHeightStream(),
|
||||||
complex.getTrueBiomeStream(),
|
complex.getNaturalTrueBiomeStream(),
|
||||||
complex.getRegionStream(),
|
complex.getRegionStream(),
|
||||||
complex.getRockStream(),
|
complex.getRockStream(),
|
||||||
complex.getImageMapRuntime(),
|
complex.getImageMapRuntime(),
|
||||||
@@ -99,12 +99,12 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
upperDim.getRegions().forEach(regionKey -> {
|
upperDim.getRegions().forEach(regionKey -> {
|
||||||
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
||||||
if (region != null) {
|
if (region != null) {
|
||||||
region.getAllBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||||
biome, dataProvider, allBiomes, generators));
|
biome, dataProvider, allBiomes, generators));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
for (IrisRegion mappedRegion : imageMapRuntime.getMappedRegions()) {
|
for (IrisRegion mappedRegion : imageMapRuntime.getMappedRegions()) {
|
||||||
mappedRegion.getAllBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
mappedRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||||
biome, dataProvider, allBiomes, generators));
|
biome, dataProvider, allBiomes, generators));
|
||||||
}
|
}
|
||||||
for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) {
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||||
import art.arcane.iris.engine.framework.EngineDecorator;
|
import art.arcane.iris.engine.framework.EngineDecorator;
|
||||||
|
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||||
|
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||||
|
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.util.common.data.B;
|
import art.arcane.iris.util.common.data.B;
|
||||||
import art.arcane.iris.util.project.context.ChunkContext;
|
import art.arcane.iris.util.project.context.ChunkContext;
|
||||||
@@ -50,7 +53,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
|||||||
@Getter
|
@Getter
|
||||||
private final EngineDecorator seaFloorDecorator;
|
private final EngineDecorator seaFloorDecorator;
|
||||||
@Getter
|
@Getter
|
||||||
private final EngineDecorator shoreLineDecorator;
|
private final IrisShoreLineDecorator shoreLineDecorator;
|
||||||
private final boolean shouldRay;
|
private final boolean shouldRay;
|
||||||
|
|
||||||
public IrisDecorantActuator(Engine engine) {
|
public IrisDecorantActuator(Engine engine) {
|
||||||
@@ -64,6 +67,84 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
|||||||
seaFloorDecorator = new IrisSeaFloorDecorator(getEngine());
|
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
|
@BlockCoordinates
|
||||||
@Override
|
@Override
|
||||||
public void onActuate(int x, int z, Hunk<PlatformBlockState> output, boolean multicore, ChunkContext context) {
|
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();
|
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||||
|
|
||||||
for (int i = 0; i < output.getWidth(); i++) {
|
for (int i = 0; i < output.getWidth(); i++) {
|
||||||
int height;
|
int height;
|
||||||
int realX = Math.round(x + i);
|
int realX = Math.round(x + i);
|
||||||
@@ -86,23 +166,57 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
|||||||
height = context.getRoundedHeight(i, j);
|
height = context.getRoundedHeight(i, j);
|
||||||
biome = context.getBiome().get(i, j);
|
biome = context.getBiome().get(i, j);
|
||||||
cave = shouldRay ? context.getCave().get(i, j) : null;
|
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())) {
|
if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))
|
if (PREDICATE_SOLID.test(output.get(i, height, j))
|
||||||
&& height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, 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,
|
getSeaSurfaceDecorator().decorate(i, j,
|
||||||
realX, Math.round(i + 1), Math.round(x + i - 1),
|
realX, Math.round(i + 1), Math.round(x + i - 1),
|
||||||
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
|
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
|
||||||
output, biome, getDimension().getFluidHeight(), getEngine().getHeight());
|
output, biome, surfaceFluidHeight, getEngine().getHeight());
|
||||||
getSeaFloorDecorator().decorate(i, j,
|
if (seaSurfaceY < output.getHeight()) {
|
||||||
realX, realZ, output, biome, height + 1,
|
restoreUnsupportedAquaticPlacement(
|
||||||
getDimension().getFluidHeight() + 1);
|
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,
|
getShoreLineDecorator().decorate(i, j,
|
||||||
realX, Math.round(x + i + 1), Math.round(x + i - 1),
|
realX, Math.round(x + i + 1), Math.round(x + i - 1),
|
||||||
realZ, Math.round(z + j + 1), Math.round(z + j - 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() {
|
private boolean shouldRayDecorate() {
|
||||||
return false; // TODO CAVES
|
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.core.loader.IrisData;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.UpperDimensionContext;
|
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.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.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisOreGenerator;
|
import art.arcane.iris.engine.object.IrisOreGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisOreGeneratorBounds;
|
import art.arcane.iris.engine.object.IrisOreGeneratorBounds;
|
||||||
@@ -88,13 +92,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
|||||||
IrisData data = getData();
|
IrisData data = getData();
|
||||||
IrisComplex complex = getComplex();
|
IrisComplex complex = getComplex();
|
||||||
RNG localRng = rng;
|
RNG localRng = rng;
|
||||||
int fluidHeight = dimension.getFluidHeight();
|
|
||||||
int clampedFluidHeight = Math.min(chunkHeight, fluidHeight);
|
|
||||||
boolean bedrockEnabled = dimension.isBedrock();
|
boolean bedrockEnabled = dimension.isBedrock();
|
||||||
boolean hideOres = dimension.isHideOresForHiddenOre();
|
boolean hideOres = dimension.isHideOresForHiddenOre();
|
||||||
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
||||||
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
|
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
|
||||||
ChunkedDataCache<PlatformBlockState> fluidCache = context.getFluid();
|
|
||||||
ChunkedDataCache<PlatformBlockState> rockCache = context.getRock();
|
ChunkedDataCache<PlatformBlockState> rockCache = context.getRock();
|
||||||
int realX = xf + x;
|
int realX = xf + x;
|
||||||
UpperDimensionContext upperContext = getEngine().getUpperContext();
|
UpperDimensionContext upperContext = getEngine().getUpperContext();
|
||||||
@@ -104,19 +105,47 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
|||||||
KList<IrisOreGenerator> dimensionUndergroundOres = hideOres ? null : dimension.getUndergroundOreGenerators();
|
KList<IrisOreGenerator> dimensionUndergroundOres = hideOres ? null : dimension.getUndergroundOreGenerators();
|
||||||
IrisOreGeneratorBounds dimensionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getSurfaceOreGeneratorBounds();
|
IrisOreGeneratorBounds dimensionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getSurfaceOreGeneratorBounds();
|
||||||
IrisOreGeneratorBounds dimensionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getUndergroundOreGeneratorBounds();
|
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++) {
|
for (int zf = 0; zf < chunkDepth; zf++) {
|
||||||
int realZ = zf + z;
|
int realZ = zf + z;
|
||||||
IrisBiome biome = biomeCache.get(xf, zf);
|
IrisBiome biome = biomeCache.get(xf, zf);
|
||||||
IrisRegion region = regionCache.get(xf, zf);
|
IrisRegion region = regionCache.get(xf, zf);
|
||||||
int he = Math.min(chunkHeight, context.getRoundedHeight(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) {
|
if (hf < 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int topY = Math.min(hf, chunkHeight - 1);
|
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 rock = rockCache.get(xf, zf);
|
||||||
PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
|
PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
|
||||||
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
|
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
|
||||||
@@ -158,15 +187,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
|||||||
|
|
||||||
if (i > he && i <= hf) {
|
if (i > he && i <= hf) {
|
||||||
int fdepth = hf - i;
|
int fdepth = hf - i;
|
||||||
if (fblocks == null) {
|
if (hydrologyFluid == null && fblocks == null) {
|
||||||
fblocks = biome.generateSeaLayers(realX, realZ, localRng, hf - he, data);
|
fblocks = biome.generateSeaLayers(realX, realZ, localRng, hf - he, data);
|
||||||
}
|
}
|
||||||
|
h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select(
|
||||||
if (fblocks.hasIndex(fdepth)) {
|
fblocks,
|
||||||
h.setRaw(xf, i, zf, fblocks.get(fdepth));
|
fdepth,
|
||||||
} else {
|
fluid,
|
||||||
h.setRaw(xf, i, zf, fluid);
|
hydrologyFluid != null
|
||||||
}
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,11 +206,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (blocks == null) {
|
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)) {
|
if (blocks.hasIndex(depth + cut)) {
|
||||||
h.setRaw(xf, i, zf, blocks.get(depth));
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,19 +40,46 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
|||||||
@Override
|
@Override
|
||||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double complexFluidHeight = getComplex().getFluidHeight();
|
|
||||||
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
|
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
|
||||||
if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight
|
ProceduralStream<Double> fluidStream = getComplex().getRiverWaterSurfaceStream();
|
||||||
&& Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight
|
if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ))
|
||||||
&& Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight
|
&& Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ))
|
||||||
&& Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) {
|
&& 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;
|
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);
|
RNG rng = getRNG(realX, realZ);
|
||||||
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
||||||
|
|
||||||
@@ -70,6 +97,8 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IrisSurfaceDecorator.AquaticPlacementSnapshot aquaticSnapshot = IrisSurfaceDecorator.captureAquaticPlacement(
|
||||||
|
decorator, getData(), data, x, z, height, max);
|
||||||
if (!decorator.isStacking()) {
|
if (!decorator.isStacking()) {
|
||||||
int targetY = height + 1;
|
int targetY = height + 1;
|
||||||
if (targetY >= data.getHeight()
|
if (targetY >= data.getHeight()
|
||||||
@@ -79,6 +108,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
|||||||
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
|
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
|
||||||
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
|
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
|
||||||
data.set(x, targetY, z, block);
|
data.set(x, targetY, z, block);
|
||||||
|
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -99,6 +129,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
|||||||
PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
|
PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
|
||||||
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
|
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
|
||||||
data.set(x, targetY, z, block);
|
data.set(x, targetY, z, block);
|
||||||
|
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -122,5 +153,6 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
|||||||
}
|
}
|
||||||
data.set(x, targetY, z, block);
|
data.set(x, targetY, z, block);
|
||||||
}
|
}
|
||||||
|
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,11 +18,13 @@
|
|||||||
|
|
||||||
package art.arcane.iris.engine.decorator;
|
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.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.InferredType;
|
import art.arcane.iris.engine.object.InferredType;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisDecorationPart;
|
import art.arcane.iris.engine.object.IrisDecorationPart;
|
||||||
import art.arcane.iris.engine.object.IrisDecorator;
|
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.iris.util.project.hunk.Hunk;
|
||||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
@@ -58,12 +60,15 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
|||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
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) {
|
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) {
|
if (inferredType == InferredType.SHORE && height < fluidHeight) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean underwater = isUnderwater(inferredType, height, fluidHeight);
|
boolean underwater = isUnderwater(inferredType, height, fluidHeight);
|
||||||
|
if (underwater && !hasFluidAbove(data, x, height, z)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
boolean caveSkipFluid = skipsFluid(inferredType);
|
boolean caveSkipFluid = skipsFluid(inferredType);
|
||||||
RNG rng = getRNG(realX, realZ);
|
RNG rng = getRNG(realX, realZ);
|
||||||
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
||||||
@@ -72,6 +77,8 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AquaticPlacementSnapshot aquaticSnapshot = captureAquaticPlacement(
|
||||||
|
decorator, getData(), data, x, z, height, max);
|
||||||
if (decorator.isStacking()) {
|
if (decorator.isStacking()) {
|
||||||
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
|
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
|
||||||
opts.reset();
|
opts.reset();
|
||||||
@@ -79,11 +86,13 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
|||||||
opts.fluidHeight = fluidHeight;
|
opts.fluidHeight = fluidHeight;
|
||||||
opts.caveSkipFluid = caveSkipFluid;
|
opts.caveSkipFluid = caveSkipFluid;
|
||||||
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts);
|
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts);
|
||||||
|
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
|
DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
|
||||||
data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
|
data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
|
||||||
|
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isUnderwater(InferredType inferredType, int height, int fluidHeight) {
|
static boolean isUnderwater(InferredType inferredType, int height, int fluidHeight) {
|
||||||
@@ -93,4 +102,148 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
|||||||
static boolean skipsFluid(InferredType inferredType) {
|
static boolean skipsFluid(InferredType inferredType) {
|
||||||
return inferredType == InferredType.CAVE;
|
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.IrisRegion;
|
||||||
import art.arcane.iris.engine.object.IrisStructure;
|
import art.arcane.iris.engine.object.IrisStructure;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
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.IrisLogging;
|
||||||
import art.arcane.iris.spi.PlatformBiome;
|
import art.arcane.iris.spi.PlatformBiome;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
@@ -244,6 +246,14 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
|
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);
|
MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class);
|
||||||
|
|
||||||
if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) {
|
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) {
|
default PlacedObject getObjectPlacement(int x, int y, int z, MantleChunk<Matter> chunk) {
|
||||||
String objectAt = chunk.get(x & 15, y, z & 15, String.class);
|
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()) {
|
if (objectAt == null || objectAt.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -552,7 +566,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) {
|
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;
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.engine.mantle.MantleComponent;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
|
||||||
public interface EnginePlatformHooks {
|
public interface EnginePlatformHooks {
|
||||||
@@ -70,4 +71,8 @@ public interface EnginePlatformHooks {
|
|||||||
default boolean shouldBypassMantleStages(Engine engine) {
|
default boolean shouldBypassMantleStages(Engine engine) {
|
||||||
return false;
|
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()) {
|
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()) {
|
if (!caveRegions.isEmpty()) {
|
||||||
@@ -341,6 +345,11 @@ public final class HintedLocator<T> implements Locator<T> {
|
|||||||
return SearchPlan.of(coarse, BIOME_STRIDE_CHUNKS, caveExact);
|
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();
|
return SearchPlan.impossible();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,6 +585,36 @@ public final class HintedLocator<T> implements Locator<T> {
|
|||||||
return false;
|
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) {
|
private boolean closurePlacesObject(String rootKey, String objectKey) {
|
||||||
if (rootKey == null || objectKey == null) {
|
if (rootKey == null || objectKey == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.framework;
|
|||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.nms.container.BlockPos;
|
import art.arcane.iris.core.nms.container.BlockPos;
|
||||||
import art.arcane.iris.core.nms.container.Pair;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisObject;
|
import art.arcane.iris.engine.object.IrisObject;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
@@ -53,10 +54,22 @@ public interface Locator<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Locator<IrisBiome> surfaceBiome(String loadKey) {
|
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));
|
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) {
|
static Locator<art.arcane.iris.engine.object.IrisStructure> structure(String key) {
|
||||||
return (e, c) -> IrisStructureLocator.startsInChunk(e, key, c.getX(), c.getZ());
|
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) {
|
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) {
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisStructureAnchorMode;
|
import art.arcane.iris.engine.object.IrisStructureAnchorMode;
|
||||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
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.math.RNG;
|
||||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||||
|
|
||||||
@@ -286,10 +288,12 @@ public final class StructureCaveAnchorResolver {
|
|||||||
int mantleY,
|
int mantleY,
|
||||||
int blockZ
|
int blockZ
|
||||||
) {
|
) {
|
||||||
|
HydrologyCaveCell hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||||
MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ);
|
MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ);
|
||||||
return acceptsAnchorFluid(
|
return acceptsAnchorFluid(
|
||||||
placement.isUnderwater(),
|
placement.isUnderwater(),
|
||||||
cavern,
|
cavern,
|
||||||
|
hydrology,
|
||||||
mantleY,
|
mantleY,
|
||||||
engine.getDimension().getCaveLavaHeight());
|
engine.getDimension().getCaveLavaHeight());
|
||||||
}
|
}
|
||||||
@@ -300,6 +304,19 @@ public final class StructureCaveAnchorResolver {
|
|||||||
int mantleY,
|
int mantleY,
|
||||||
int defaultLavaHeight
|
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()) {
|
if (cavern == null || !cavern.isCavern()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -313,7 +330,15 @@ public final class StructureCaveAnchorResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static MatterCavern cavernAt(Engine engine, int blockX, int mantleY, int blockZ) {
|
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) {
|
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.volmlib.util.collection.KList;
|
||||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
@@ -112,7 +111,7 @@ public class WorldObjectPlacer implements IObjectPlacer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isCarved(int x, int y, int z) {
|
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
|
@Override
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ package art.arcane.iris.engine.framework.render;
|
|||||||
|
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
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.Color;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.DataBufferInt;
|
import java.awt.image.DataBufferInt;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.function.BooleanSupplier;
|
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 BLUE = new Color(45, 91, 156).getRGB();
|
||||||
private static final int YELLOW = new Color(211, 164, 67).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 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 DEEP_WATER = new Color(20, 48, 92).getRGB();
|
||||||
private static final int SHALLOW_WATER = new Color(50, 112, 154).getRGB();
|
private static final int SHALLOW_WATER = new Color(50, 112, 154).getRGB();
|
||||||
private static final int LOWLAND = new Color(78, 128, 76).getRGB();
|
private static final int LOWLAND = new Color(78, 128, 76).getRGB();
|
||||||
@@ -105,9 +134,17 @@ public final class IrisRenderer {
|
|||||||
renderHeightAtlas(pixels, resolution, sx, sz, step, renderer, cancelled);
|
renderHeightAtlas(pixels, resolution, sx, sz, step, renderer, cancelled);
|
||||||
return image;
|
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)) {
|
if (studio && adaptiveStudioType(currentType)) {
|
||||||
renderAdaptiveAtlas(pixels, resolution, sx, sz, step, shader, cancelled);
|
renderAdaptiveAtlas(pixels, resolution, sx, sz, step, shader, cancelled);
|
||||||
|
if (currentType == RenderType.BIOME) {
|
||||||
|
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, true);
|
||||||
|
}
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
int groupSize = sampleGroup(step, resolution);
|
int groupSize = sampleGroup(step, resolution);
|
||||||
@@ -133,6 +170,57 @@ public final class IrisRenderer {
|
|||||||
return image;
|
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) {
|
public static int heightColor(double height, double maximumHeight, double fluidHeight) {
|
||||||
double boundedMaximum = Math.max(1D, maximumHeight);
|
double boundedMaximum = Math.max(1D, maximumHeight);
|
||||||
double boundedHeight = clamp(height, 0D, boundedMaximum);
|
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)));
|
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();
|
IrisComplex complex = renderer.getComplex();
|
||||||
return switch (currentType) {
|
return switch (currentType) {
|
||||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> biomeShader(
|
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 BIOME_SEA -> biomeShader(complex.getSeaBiomeStream(), currentType);
|
||||||
case REGION -> regionShader(complex, currentType);
|
case REGION -> regionShader(complex, currentType);
|
||||||
case CAVE_LAND -> biomeShader(complex.getCaveBiomeStream(), 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
|
case CONTINENT -> studio
|
||||||
? continentShader(complex.getBaseBiomeStream())
|
? continentShader(complex.getBaseBiomeStream())
|
||||||
: this::continentColor;
|
: this::continentColor;
|
||||||
@@ -180,7 +269,7 @@ public final class IrisRenderer {
|
|||||||
return switch (type) {
|
return switch (type) {
|
||||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND,
|
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND,
|
||||||
CONTINENT -> true;
|
CONTINENT -> true;
|
||||||
case HEIGHT -> false;
|
case HEIGHT, RIVER -> false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +313,7 @@ public final class IrisRenderer {
|
|||||||
startX,
|
startX,
|
||||||
startZ,
|
startZ,
|
||||||
step,
|
step,
|
||||||
complex.getHeightStream(),
|
complex.getNaturalHeightStream(),
|
||||||
engine.getHeight(),
|
engine.getHeight(),
|
||||||
fluidHeight,
|
fluidHeight,
|
||||||
cancelled
|
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) {
|
private PixelShader biomeShader(ProceduralStream<IrisBiome> stream, RenderType currentType) {
|
||||||
IdentityHashMap<IrisBiome, Integer> colors = new IdentityHashMap<>();
|
IdentityHashMap<IrisBiome, Integer> colors = new IdentityHashMap<>();
|
||||||
return (double x, double z) -> {
|
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);
|
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) {
|
private int continentColor(double x, double z) {
|
||||||
IrisBiome biome = renderer.getBiome(
|
IrisBiome biome = renderer.getBiome(
|
||||||
(int) Math.round(x),
|
(int) Math.round(x),
|
||||||
|
|||||||
@@ -19,5 +19,5 @@
|
|||||||
package art.arcane.iris.engine.framework.render;
|
package art.arcane.iris.engine.framework.render;
|
||||||
|
|
||||||
public enum RenderType {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 330 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user