mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Hydrology fixes
This commit is contained in:
@@ -510,6 +510,15 @@ public final class ModdedBlockResolution {
|
||||
}
|
||||
|
||||
public static boolean canPlaceOnto(Block mat, Block onto) {
|
||||
if (mat == Blocks.SUGAR_CANE) {
|
||||
return onto == Blocks.SUGAR_CANE || onto == Blocks.GRASS_BLOCK || onto == Blocks.DIRT
|
||||
|| onto == Blocks.COARSE_DIRT || onto == Blocks.PODZOL || onto == Blocks.MYCELIUM
|
||||
|| onto == Blocks.ROOTED_DIRT
|
||||
|| onto == Blocks.MOSS_BLOCK || onto == Blocks.PALE_MOSS_BLOCK || onto == Blocks.MUD
|
||||
|| onto == Blocks.MUDDY_MANGROVE_ROOTS || onto == Blocks.SAND || onto == Blocks.RED_SAND
|
||||
|| onto == Blocks.SUSPICIOUS_SAND;
|
||||
}
|
||||
|
||||
if (mat == Blocks.CACTUS) {
|
||||
return onto == Blocks.CACTUS || onto == Blocks.SAND || onto == Blocks.RED_SAND;
|
||||
}
|
||||
|
||||
+13
@@ -69,6 +69,19 @@ public class ModdedBlockResolutionContractTest {
|
||||
assertTrue(ModdedBlockResolution.isDecorant(Blocks.CACTUS.defaultBlockState()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sugarCanePlacementRequiresNativeSubstrate() {
|
||||
for (Block block : new Block[]{Blocks.SUGAR_CANE, Blocks.GRASS_BLOCK, Blocks.DIRT, Blocks.COARSE_DIRT,
|
||||
Blocks.PODZOL, Blocks.MYCELIUM, Blocks.ROOTED_DIRT, Blocks.MOSS_BLOCK, Blocks.PALE_MOSS_BLOCK,
|
||||
Blocks.MUD, Blocks.MUDDY_MANGROVE_ROOTS, Blocks.SAND, Blocks.RED_SAND, Blocks.SUSPICIOUS_SAND}) {
|
||||
assertTrue(block.toString(), ModdedBlockResolution.canPlaceOnto(Blocks.SUGAR_CANE, block));
|
||||
}
|
||||
for (Block block : new Block[]{Blocks.STONE, Blocks.GRAVEL, Blocks.CLAY, Blocks.DIRT_PATH,
|
||||
Blocks.FARMLAND, Blocks.AIR, Blocks.WATER}) {
|
||||
assertFalse(block.toString(), ModdedBlockResolution.canPlaceOnto(Blocks.SUGAR_CANE, block));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sulfurAndDripstoneTipsReceivePostLoadUpdates() {
|
||||
for (Block block : new Block[]{Blocks.SULFUR_SPIKE, Blocks.POINTED_DRIPSTONE}) {
|
||||
|
||||
@@ -210,6 +210,10 @@ final class PackHydrologyValidator {
|
||||
}
|
||||
|
||||
private static void validateGeometry(String path, JSONObject geometry, List<String> errors) {
|
||||
JSONObject banks3D = nestedObject(geometry, "banks3D", path, errors);
|
||||
if (banks3D != null) {
|
||||
PackRiverBank3DValidator.validate(path + ".banks3D", banks3D, errors);
|
||||
}
|
||||
JSONObject meanders = nestedObject(geometry, "meanders", path, errors);
|
||||
JSONObject surface = nestedObject(geometry, "surface", path, errors);
|
||||
JSONObject underground = nestedObject(geometry, "underground", path, errors);
|
||||
@@ -302,6 +306,25 @@ final class PackHydrologyValidator {
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "confluenceAttraction", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "lengthPreference", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "tributaries", 0, 4, errors);
|
||||
JSONObject regional = routing.optJSONObject("regional");
|
||||
if (routing.has("regional") && regional == null) {
|
||||
errors.add(path + ".regional must be an object.");
|
||||
}
|
||||
if (regional != null) {
|
||||
String regionalPath = path + ".regional";
|
||||
PackJsonFieldChecks.validateOptionalBoolean(regionalPath, regional, "enabled", errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(regionalPath, regional, "sampleSpacing", 128, 1024, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(regionalPath, regional, "minimumLength", 256, 32768, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(regionalPath, regional, "maximumTrunks", 1, 8, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(regionalPath, regional, "maximumCachedBasins", 1, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(regionalPath, regional, "maximumCachedStations", 16384, 1048576, errors);
|
||||
PackJsonFieldChecks.validateOptionalBoolean(regionalPath, regional, "coastalChannels", errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(regionalPath, regional, "coastalChannelChance", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(regionalPath, regional, "maximumCoastalIncision", 1, 32, errors);
|
||||
if (Integer.bitCount(integerValue(regional, "sampleSpacing", 256)) != 1) {
|
||||
errors.add(regionalPath + ".sampleSpacing must be a power of two.");
|
||||
}
|
||||
}
|
||||
|
||||
int tileSize = integerValue(routing, "tileSize", 2048);
|
||||
int sampleSpacing = integerValue(routing, "sampleSpacing", 64);
|
||||
@@ -381,7 +404,7 @@ final class PackHydrologyValidator {
|
||||
boolean surfaceEnabled = booleanValue(surface, "enabled", true);
|
||||
if (surfaceEnabled && budget.maximumExpectedSources() == 0) {
|
||||
warnings.add(path
|
||||
+ " has no natural source budget; only qualifying REQUIRED_HEADWATER cells can admit sources.");
|
||||
+ " has no local natural source budget; local sources require qualifying REQUIRED_HEADWATER cells.");
|
||||
}
|
||||
if (surfaceEnabled && sources != null) {
|
||||
double minimumElevation = doubleValue(sources, "minimumElevation", 88D);
|
||||
@@ -440,6 +463,12 @@ final class PackHydrologyValidator {
|
||||
}
|
||||
|
||||
private static void validateSurfaceBanks(String path, JSONObject banks, List<String> errors) {
|
||||
JSONObject excavation = nestedObject(banks, "excavation", path, errors);
|
||||
if (excavation != null) {
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path + ".excavation", excavation, "maximumDepth", 0, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path + ".excavation", excavation, "maximumWidth", 1, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path + ".excavation", excavation, "maximumVolumePerBlock", 0, 8192, errors);
|
||||
}
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "shoreWidth", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "blendSlope", 0.5D, 12D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "minimumBlendWidth", 1, 64, errors);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisRiverBank3DConfig;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
final class PackRiverBank3DValidator {
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
private PackRiverBank3DValidator() {
|
||||
}
|
||||
|
||||
static void validate(String path, JSONObject banks, List<String> errors) {
|
||||
int previousErrors = errors.size();
|
||||
PackJsonFieldChecks.validateOptionalBoolean(path, banks, "enabled", errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "amplitude", 0D, 32D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "horizontalScale", 4D, 4096D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "verticalScale", 4D, 256D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "maximumOverhang", 1, 16, errors);
|
||||
if (banks.has("seed")) {
|
||||
Object seed = banks.opt("seed");
|
||||
try {
|
||||
if (!(seed instanceof Number)) {
|
||||
throw new NumberFormatException();
|
||||
}
|
||||
new BigDecimal(seed.toString()).longValueExact();
|
||||
} catch (NumberFormatException | ArithmeticException invalid) {
|
||||
errors.add(path + ".seed must be a 64-bit integer.");
|
||||
}
|
||||
}
|
||||
JSONObject normalized = new JSONObject(banks.toString());
|
||||
if (banks.has("densityStyle")) {
|
||||
Object style = banks.opt("densityStyle");
|
||||
if (style instanceof String reference && !reference.isBlank()) {
|
||||
normalized.remove("densityStyle");
|
||||
} else if (!(style instanceof JSONObject)) {
|
||||
errors.add(path + ".densityStyle must be an object or snippet reference.");
|
||||
}
|
||||
}
|
||||
if (errors.size() != previousErrors) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
GSON.fromJson(normalized.toString(), IrisRiverBank3DConfig.class).validate();
|
||||
} catch (JsonParseException | IllegalArgumentException invalid) {
|
||||
errors.add(path + ": " + invalid.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||
import art.arcane.iris.engine.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSnapshot;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyNaturalSample;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
@@ -53,10 +54,12 @@ 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.IrisRiverBank3DConfig;
|
||||
import art.arcane.iris.engine.object.IrisRiverProfile;
|
||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DRuntime;
|
||||
import art.arcane.iris.engine.terrain.HydrologyBankTerrainRuntime;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
@@ -97,8 +100,8 @@ import java.nio.file.Path;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D"})
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
||||
public class IrisComplex implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||
@@ -157,6 +160,7 @@ public class IrisComplex implements DataProvider {
|
||||
private final transient Engine terrainEngine;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private transient Terrain3DRuntime terrain3D;
|
||||
private transient HydrologyBankTerrainRuntime hydrologyBanks3D;
|
||||
private ProceduralStream<Double> unblendedNaturalHeightStream;
|
||||
private final ResolvedTerrainProvider resolvedTerrain;
|
||||
private ProceduralStream<Double> placementHeightStream;
|
||||
@@ -408,9 +412,22 @@ public class IrisComplex implements DataProvider {
|
||||
() -> engine.getPlatformHooks().isMainThread()
|
||||
));
|
||||
hydrologyRuntime.setNeighbourPrefetchEnabled(!engine.isStudio());
|
||||
IrisRiverBank3DConfig banks3D = configuredHydrology.getRivers().getGeometry().getBanks3D();
|
||||
banks3D.validate();
|
||||
if (configuredHydrology.getRivers().isEnabled()
|
||||
&& configuredHydrology.getRivers().getSurface().isEnabled()
|
||||
&& banks3D.isEnabled() && banks3D.getAmplitude() > 0D) {
|
||||
hydrologyBanks3D = new HydrologyBankTerrainRuntime(
|
||||
new HydrologyBankTerrainRuntime.Sources(
|
||||
(x, z) -> naturalBankTerrainColumn(x, z, engine.getHeight()), this::sampleHydrologySnapshot),
|
||||
new HydrologyBankTerrainRuntime.Options(engine.getSeedManager().getBodies(), engine.getHeight(),
|
||||
Math.max(4096, cacheSize), data, banks3D));
|
||||
}
|
||||
}
|
||||
heightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z))
|
||||
ProceduralStream<Double> cachedHeightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z))
|
||||
.cache2DDouble("heightStream", engine, cacheSize);
|
||||
heightStream = ProceduralStream.ofDouble((x, z) -> terrainEngine.getPlatformHooks().isMainThread()
|
||||
? nonblockingTerrainHeight(x, z) : cachedHeightStream.getDouble(x, z));
|
||||
placementHeightStream = ProceduralStream.ofDouble(this::samplePlacementHeight);
|
||||
roundedHeighteightStream = placementHeightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.round();
|
||||
@@ -578,21 +595,22 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
public boolean hasTerrain3D() {
|
||||
return terrain3D != null && terrain3D.active();
|
||||
return terrain3D != null && terrain3D.active() || hydrologyBanks3D != null;
|
||||
}
|
||||
|
||||
public Terrain3DColumn terrainColumn(int x, int z, HydrologyColumnSample hydrology) {
|
||||
if (hydrology != null) {
|
||||
HydrologyColumnLayer layer = hydrology.primarySurfaceLayer().orElse(null);
|
||||
if (layer != null && layer.terrainOwned()) {
|
||||
return null;
|
||||
return hydrologyBanks3D != null && !layer.channel() && layer.feature().type().isSurface()
|
||||
? hydrologyBanks3D.column(x, z) : null;
|
||||
}
|
||||
}
|
||||
return naturalTerrainColumn(x, z);
|
||||
}
|
||||
|
||||
public Terrain3DColumn terrainColumn(int x, int z) {
|
||||
if (terrain3D == null || !terrain3D.active()) {
|
||||
if (!hasTerrain3D()) {
|
||||
return null;
|
||||
}
|
||||
if (terrainEngine != null && terrainEngine.answersFromNaturalTerrain(x, z)) {
|
||||
@@ -601,6 +619,11 @@ public class IrisComplex implements DataProvider {
|
||||
return terrainColumn(x, z, sampleHydrologyColumn(x, z));
|
||||
}
|
||||
|
||||
private Terrain3DColumn naturalBankTerrainColumn(int x, int z, int height) {
|
||||
Terrain3DColumn natural = naturalTerrainColumn(x, z);
|
||||
return natural == null ? Terrain3DColumn.unshaped(naturalHeightStream.getDouble(x, z), height) : natural;
|
||||
}
|
||||
|
||||
public boolean isTerrain3DOpening(int x, int y, int z) {
|
||||
Terrain3DColumn column = terrainColumn(x, z);
|
||||
return column != null && y >= column.minY() && y < column.topY() && !column.isSolid(y)
|
||||
@@ -860,7 +883,28 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
private double resolveHydrologyTerrainHeight(double x, double z) {
|
||||
HydrologyColumnSample sample = hydrologySample(x, z);
|
||||
return sample == null ? naturalHeightStream.get(x, z) : sample.terrainHeight();
|
||||
if (sample == null) {
|
||||
return naturalHeightStream.get(x, z);
|
||||
}
|
||||
HydrologyColumnLayer layer = sample.primarySurfaceLayerOrNull();
|
||||
if (hydrologyBanks3D != null && layer != null && layer.terrainOwned() && !layer.channel()) {
|
||||
return hydrologyBanks3D.column(blockCoordinate(x), blockCoordinate(z)).topY();
|
||||
}
|
||||
return sample.terrainHeight();
|
||||
}
|
||||
|
||||
private double nonblockingTerrainHeight(double x, double z) {
|
||||
HydrologyColumnSnapshot snapshot = sampleHydrologySnapshot(blockCoordinate(x), blockCoordinate(z));
|
||||
if (!snapshot.available() || snapshot.column() == null) {
|
||||
return naturalHeightStream.getDouble(x, z);
|
||||
}
|
||||
HydrologyColumnSample sample = snapshot.column();
|
||||
HydrologyColumnLayer layer = sample.primarySurfaceLayerOrNull();
|
||||
if (hydrologyBanks3D != null && layer != null && layer.terrainOwned() && !layer.channel()) {
|
||||
Optional<Terrain3DColumn> column = hydrologyBanks3D.columnIfReady(blockCoordinate(x), blockCoordinate(z));
|
||||
return column.isPresent() ? column.get().topY() : naturalHeightStream.getDouble(x, z);
|
||||
}
|
||||
return sample.terrainHeight();
|
||||
}
|
||||
|
||||
private double resolveHydrologyDistance(double x, double z) {
|
||||
@@ -919,6 +963,19 @@ public class IrisComplex implements DataProvider {
|
||||
return taperHydrologySample(sample, hydrologyWeight);
|
||||
}
|
||||
|
||||
private HydrologyColumnSnapshot sampleHydrologySnapshot(int x, int z) {
|
||||
if (hydrologyRuntime == null) {
|
||||
return HydrologyColumnSnapshot.ready(null);
|
||||
}
|
||||
double weight = transitionHydrologyWeight(x, z);
|
||||
if (weight == 0D) {
|
||||
return HydrologyColumnSnapshot.ready(null);
|
||||
}
|
||||
HydrologyColumnSnapshot snapshot = hydrologyRuntime.sampleSnapshot(x, z);
|
||||
return !snapshot.available() || snapshot.column() == null || weight == 1D ? snapshot
|
||||
: HydrologyColumnSnapshot.ready(taperHydrologySample(snapshot.column(), weight));
|
||||
}
|
||||
|
||||
static HydrologyColumnSample taperHydrologySample(
|
||||
HydrologyColumnSample sample,
|
||||
double hydrologyWeight
|
||||
@@ -983,9 +1040,22 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
/** Whether the column's hydrology can be sampled without waiting for a plan (see Engine.answersFromNaturalTerrain). */
|
||||
public boolean isHydrologyPlanned(int x, int z) {
|
||||
return hydrologyRuntime == null
|
||||
|| transitionHydrologyWeight(x, z) == 0D
|
||||
|| hydrologyRuntime.isPlanned(x, z);
|
||||
if (hydrologyRuntime == null || transitionHydrologyWeight(x, z) == 0D) {
|
||||
return true;
|
||||
}
|
||||
if (!hydrologyRuntime.isPlanned(x, z)) {
|
||||
return false;
|
||||
}
|
||||
if (hydrologyBanks3D == null || !terrainEngine.getPlatformHooks().isMainThread()) {
|
||||
return true;
|
||||
}
|
||||
HydrologyColumnSnapshot snapshot = sampleHydrologySnapshot(x, z);
|
||||
if (!snapshot.available()) {
|
||||
return false;
|
||||
}
|
||||
HydrologyColumnLayer layer = snapshot.column() == null ? null : snapshot.column().primarySurfaceLayerOrNull();
|
||||
return layer == null || !layer.terrainOwned() || layer.channel()
|
||||
|| hydrologyBanks3D.columnIfReady(x, z).isPresent();
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> getRawHeightStream() {
|
||||
@@ -1893,6 +1963,9 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (hydrologyBanks3D != null) {
|
||||
hydrologyBanks3D.clear();
|
||||
}
|
||||
if (terrain3D != null) {
|
||||
terrain3D.clear();
|
||||
}
|
||||
|
||||
@@ -140,12 +140,12 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
int terrainSpan = terrainColumn == null ? -1 : terrainColumn.spanCount() - 1;
|
||||
int layerSurfaceY = he;
|
||||
int layerCeilingY = 0;
|
||||
int cut = exposeCutStrata
|
||||
boolean exposeRiverStrata = exposeCutStrata
|
||||
&& hydrologyTerrain != null
|
||||
&& hydrologyTerrain.terrainOwned()
|
||||
&& !hydrologyTerrain.channel()
|
||||
? Math.max(0, hydrology.naturalHeight() - he)
|
||||
: 0;
|
||||
&& !hydrologyTerrain.channel();
|
||||
Terrain3DColumn naturalColumn = exposeRiverStrata ? complex.naturalTerrainColumn(realX, realZ) : null;
|
||||
int cut = 0;
|
||||
boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned();
|
||||
IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial(
|
||||
hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial);
|
||||
@@ -227,6 +227,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
continue;
|
||||
}
|
||||
if (blocks == null) {
|
||||
if (exposeRiverStrata) {
|
||||
int naturalSurfaceY = naturalColumn == null ? hydrology.naturalHeight() : naturalColumn.surfaceY(layerSurfaceY);
|
||||
cut = Math.max(0, naturalSurfaceY - layerSurfaceY);
|
||||
}
|
||||
blocks = biome.generateLayers(dimension, realX, realZ, localRng,
|
||||
layerSurfaceY + cut, layerSurfaceY + cut, data, complex);
|
||||
}
|
||||
@@ -242,12 +246,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blocks.hasIndex(depth + cut)) {
|
||||
PlatformBlockState layerBlock = blocks.get(depth + cut);
|
||||
if (roleMaterial != null) {
|
||||
layerBlock = paintHydrologyMaterial(
|
||||
layerBlock, roleMaterial, depth, localRng, realX, i, realZ, data);
|
||||
}
|
||||
PlatformBlockState layerBlock = paintHydrologyMaterial(
|
||||
blocks.hasIndex(depth + cut) ? blocks.get(depth + cut) : null,
|
||||
roleMaterial, depth, localRng, realX, i, realZ, data);
|
||||
if (layerBlock != null) {
|
||||
if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
|
||||
layerBlock = riverBed.getPaddingPalette().get(localRng, realX, i, realZ, data);
|
||||
}
|
||||
|
||||
@@ -55,11 +55,13 @@ final class DecoratorCore {
|
||||
boolean caveSkipFluid;
|
||||
boolean underwater;
|
||||
int fluidHeight;
|
||||
EngineMantle mantle;
|
||||
|
||||
void reset() {
|
||||
caveSkipFluid = false;
|
||||
underwater = false;
|
||||
fluidHeight = 0;
|
||||
mantle = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +140,11 @@ final class DecoratorCore {
|
||||
}
|
||||
}
|
||||
|
||||
if (!IrisSugarCane.canPlace(bd, data, x, height + 1, z, realX, realZ,
|
||||
mantle == null ? null : mantle.getEngine())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (IrisSpeleothems.isSpike(bd)) {
|
||||
placeSingleSpike(bd, data, x, z, height + 1, true, underwater && !caveSkipFluid);
|
||||
return;
|
||||
@@ -190,6 +197,10 @@ final class DecoratorCore {
|
||||
if (bd == null) {
|
||||
return;
|
||||
}
|
||||
if (!IrisSugarCane.canPlace(bd, data, x, height, z, realX, realZ,
|
||||
mantle == null ? null : mantle.getEngine())) {
|
||||
return;
|
||||
}
|
||||
if (IrisSpeleothems.isSpike(bd)) {
|
||||
if (height + 1 < data.getHeight() && allowsSurface(decorator, data.get(x, height + 1, z), irisData)) {
|
||||
placeSingleSpike(bd, data, x, z, height, false, false);
|
||||
@@ -234,6 +245,10 @@ final class DecoratorCore {
|
||||
if (block == null) {
|
||||
break;
|
||||
}
|
||||
if (!IrisSugarCane.canPlace(block, data, x, y, z, realX, realZ,
|
||||
opts.mantle == null ? null : opts.mantle.getEngine())) {
|
||||
break;
|
||||
}
|
||||
if (IrisSpeleothems.isSpike(block)) {
|
||||
if (!IrisSpeleothems.canPlace(block, data, x, z, y, true, opts.underwater)) {
|
||||
break;
|
||||
@@ -284,6 +299,10 @@ final class DecoratorCore {
|
||||
if (block == null) {
|
||||
break;
|
||||
}
|
||||
if (!IrisSugarCane.canPlace(block, data, x, y, z, realX, realZ,
|
||||
mantle == null ? null : mantle.getEngine())) {
|
||||
break;
|
||||
}
|
||||
if (IrisSpeleothems.isSpike(block)) {
|
||||
if (!IrisSpeleothems.canPlace(block, data, x, z, y, false, opts.underwater)) {
|
||||
break;
|
||||
@@ -306,11 +325,15 @@ final class DecoratorCore {
|
||||
static void placeFloatingSimple(IrisDecorator decorator,
|
||||
int xf, int zf, int realX, int realZ,
|
||||
int height, int max, Hunk<PlatformBlockState> data,
|
||||
RNG rng, IrisData irisData) {
|
||||
RNG rng, IrisData irisData, EngineMantle mantle) {
|
||||
PlatformBlockState bd = decorator.pickBlockData(rng, irisData, realX, realZ);
|
||||
if (bd == null) {
|
||||
return;
|
||||
}
|
||||
if (!IrisSugarCane.canPlace(bd, data, xf, height + 1, zf, realX, realZ,
|
||||
mantle == null ? null : mantle.getEngine())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (IrisSpeleothems.isSpike(bd)) {
|
||||
if (max > 1 && allowsSurface(decorator, data.get(xf, height, zf), irisData)) {
|
||||
@@ -346,7 +369,7 @@ final class DecoratorCore {
|
||||
static int placeFloatingStacked(IrisDecorator decorator,
|
||||
int xf, int zf, int realX, int realZ,
|
||||
int height, int max, Hunk<PlatformBlockState> data,
|
||||
RNG rng, IrisData irisData) {
|
||||
RNG rng, IrisData irisData, EngineMantle mantle) {
|
||||
int stack = decorator.getHeight(rng, realX, realZ, irisData);
|
||||
if (decorator.isScaleStack()) {
|
||||
stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack());
|
||||
@@ -368,6 +391,10 @@ final class DecoratorCore {
|
||||
if (bd == null) {
|
||||
break;
|
||||
}
|
||||
if (!IrisSugarCane.canPlace(bd, data, xf, h, zf, realX, realZ,
|
||||
mantle == null ? null : mantle.getEngine())) {
|
||||
break;
|
||||
}
|
||||
if (IrisSpeleothems.isSpike(bd)) {
|
||||
if (!allowsSurface(decorator, data.get(xf, height, zf), irisData)
|
||||
|| !IrisSpeleothems.canPlace(bd, data, xf, zf, h, true, false)) {
|
||||
@@ -450,7 +477,7 @@ final class DecoratorCore {
|
||||
if (!B.canPlaceOnto(decorator, surface)) {
|
||||
return false;
|
||||
}
|
||||
return IrisSpeleothems.isSturdy(surface, true);
|
||||
return IrisSugarCane.isSugarCane(decorator) || IrisSpeleothems.isSturdy(surface, true);
|
||||
}
|
||||
|
||||
static boolean isValidShorelineSupport(IrisDecorator decorator, PlatformBlockState decorant, PlatformBlockState surface) {
|
||||
|
||||
@@ -46,10 +46,12 @@ public class FloatingDecorator {
|
||||
}
|
||||
|
||||
if (!decorator.isStacking()) {
|
||||
DecoratorCore.placeFloatingSimple(decorator, xf, zf, realX, realZ, height, max, data, rng, engine.getData());
|
||||
DecoratorCore.placeFloatingSimple(decorator, xf, zf, realX, realZ, height, max, data,
|
||||
rng, engine.getData(), engine.getMantle());
|
||||
return max > 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
return DecoratorCore.placeFloatingStacked(decorator, xf, zf, realX, realZ, height, max, data, rng, engine.getData());
|
||||
return DecoratorCore.placeFloatingStacked(decorator, xf, zf, realX, realZ, height, max, data,
|
||||
rng, engine.getData(), engine.getMantle());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
return;
|
||||
}
|
||||
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)
|
||||
&& IrisSugarCane.canPlace(block, data, x, targetY, z, realX, realZ, getEngine())) {
|
||||
data.set(x, targetY, z, block);
|
||||
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||
}
|
||||
@@ -126,7 +127,8 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
}
|
||||
|
||||
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)
|
||||
&& IrisSugarCane.canPlace(block, data, x, targetY, z, realX, realZ, getEngine())) {
|
||||
data.set(x, targetY, z, block);
|
||||
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||
}
|
||||
@@ -147,6 +149,9 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
if (block == null) {
|
||||
break;
|
||||
}
|
||||
if (!IrisSugarCane.canPlace(block, data, x, targetY, z, realX, realZ, getEngine())) {
|
||||
break;
|
||||
}
|
||||
if (i == 0 && !DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package art.arcane.iris.engine.decorator;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.TerrainBoundarySignature;
|
||||
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.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
final class IrisSugarCane {
|
||||
private static final int[][] NEIGHBORS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
|
||||
|
||||
private IrisSugarCane() {
|
||||
}
|
||||
|
||||
static boolean isSugarCane(PlatformBlockState state) {
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
String key = state.key();
|
||||
return key != null && (key.equals("minecraft:sugar_cane") || key.startsWith("minecraft:sugar_cane["));
|
||||
}
|
||||
|
||||
static boolean canPlace(PlatformBlockState block, Hunk<PlatformBlockState> data,
|
||||
int x, int y, int z, int worldX, int worldZ, Engine engine) {
|
||||
if (!isSugarCane(block)) {
|
||||
return true;
|
||||
}
|
||||
if (y <= 0 || y >= data.getHeight()) {
|
||||
return false;
|
||||
}
|
||||
int baseY = y - 1;
|
||||
PlatformBlockState support = data.get(x, baseY, z);
|
||||
PlatformBlockState target = data.get(x, y, z);
|
||||
if (support == null || !block.canPlaceOnto(support) || target != null && !target.isAir()) {
|
||||
return false;
|
||||
}
|
||||
while (isSugarCane(support) && baseY > 0) {
|
||||
support = data.get(x, --baseY, z);
|
||||
}
|
||||
if (support == null || isSugarCane(support) || !block.canPlaceOnto(support)) {
|
||||
return false;
|
||||
}
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
int adjacentX = x + offset[0];
|
||||
int adjacentZ = z + offset[1];
|
||||
if (adjacentX >= 0 && adjacentX < data.getWidth()
|
||||
&& adjacentZ >= 0 && adjacentZ < data.getDepth()) {
|
||||
if (supportsAdjacent(data.get(adjacentX, baseY, adjacentZ))) {
|
||||
return true;
|
||||
}
|
||||
} else if (engine != null && plannedWater(engine, worldX + offset[0], baseY, worldZ + offset[1])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean supportsAdjacent(PlatformBlockState state) {
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
String key = state.key();
|
||||
return state.isWater() || state.isWaterLogged() || key != null
|
||||
&& (key.equals("minecraft:frosted_ice") || key.startsWith("minecraft:frosted_ice["));
|
||||
}
|
||||
|
||||
private static boolean plannedWater(Engine engine, int x, int y, int z) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
Optional<TerrainBoundarySignature> resolved = complex.resolvedTerrainColumn(x, z);
|
||||
if (resolved.isPresent()) {
|
||||
String key = resolved.get().geometry().voxelAt(y + engine.getMinHeight()).stateKey();
|
||||
return supportsAdjacent(IrisPlatforms.get().registries().blockOrNull(key));
|
||||
}
|
||||
HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z);
|
||||
HydrologyColumnLayer fluid = sample == null ? null : sample.primarySurfaceFluidLayerOrNull();
|
||||
if (fluid != null) {
|
||||
return y > fluid.bedY() && y <= fluid.fluidHeadY()
|
||||
&& supportsAdjacent(complex.resolveHydrologyFluid(fluid.profileKey(), x, z));
|
||||
}
|
||||
int terrainHeight = (int) Math.round(complex.getHeightStream().getDouble(x, z));
|
||||
int fluidHeight = (int) Math.round(complex.getRiverWaterSurfaceStream().getDouble(x, z));
|
||||
if (y <= terrainHeight || y > fluidHeight) {
|
||||
return false;
|
||||
}
|
||||
IrisBiome biome = complex.getTrueBiomeStream().get(x, z);
|
||||
KList<PlatformBlockState> layers = biome.generateSeaLayers(x, z,
|
||||
new RNG(engine.getSeedManager().getTerrain()), fluidHeight - terrainHeight, engine.getData());
|
||||
int depth = fluidHeight - y;
|
||||
return supportsAdjacent(layers.hasIndex(depth) ? layers.get(depth) : complex.resolveSurfaceFluid(x, z));
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
opts.underwater = underwater;
|
||||
opts.fluidHeight = fluidHeight;
|
||||
opts.caveSkipFluid = caveSkipFluid;
|
||||
opts.mantle = getEngine().getMantle();
|
||||
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts);
|
||||
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||
return;
|
||||
|
||||
@@ -215,7 +215,7 @@ public final class IrisRenderer {
|
||||
public static int hydrologyDiagnosticColor(HydrologyCandidateKind kind) {
|
||||
Objects.requireNonNull(kind, "kind");
|
||||
return switch (kind) {
|
||||
case SOURCE, TRIBUTARY -> PROJECTED_SOURCE;
|
||||
case SOURCE, TRIBUTARY, REGIONAL_SOURCE, COASTAL_CHANNEL -> PROJECTED_SOURCE;
|
||||
case OUTLET -> PROJECTED_OUTLET;
|
||||
case DEEP_FLUID, POOL -> PROJECTED_DEEP_FLUID;
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ final class CaveCandidateBuilder {
|
||||
this.maximumZ = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
private RiverCourse course() {
|
||||
RiverCourse course() {
|
||||
return course;
|
||||
}
|
||||
|
||||
@@ -97,8 +97,19 @@ final class CaveCandidateBuilder {
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
int neighborX = position.x() + offset[0];
|
||||
int neighborZ = position.z() + offset[1];
|
||||
if (validation.ownsSurfaceChannelAt(neighborX, neighborZ, course.id())) {
|
||||
addOpening(neighborX, position.y(), neighborZ);
|
||||
if (actions.containsKey(new CavePosition(neighborX, position.y(), neighborZ))) {
|
||||
continue;
|
||||
}
|
||||
HydrologyColumnSample neighbor = validation.surfaceColumnAt(neighborX, neighborZ);
|
||||
if (neighbor == null) {
|
||||
continue;
|
||||
}
|
||||
for (HydrologyColumnLayer layer : neighbor.layers()) {
|
||||
if (layer.feature().courseId() == course.id() && layer.channel() && layer.fluidOwned()
|
||||
&& layer.bedY() < position.y() && position.y() <= layer.fluidHeadY()) {
|
||||
addOpening(neighborX, position.y(), neighborZ);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +121,13 @@ final class CaveCandidateBuilder {
|
||||
continue;
|
||||
}
|
||||
if (opening.includeNeighborhood()) {
|
||||
addOpeningNeighborhood(position);
|
||||
addOpening(position);
|
||||
for (int[] offset : HydrologyCaveCourseFilter.NEIGHBORS) {
|
||||
int y = position.y() + offset[1];
|
||||
if (y >= opening.minimumY()) {
|
||||
addOpening(position.x() + offset[0], y, position.z() + offset[2]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addOpening(position);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ final class CaveCandidateSpanBuilder {
|
||||
addOpening(x, z, minimumOpeningY, maximumActionY);
|
||||
continue;
|
||||
}
|
||||
addOpening(x, z, minimumOpeningY - 1, maximumActionY + 1);
|
||||
addOpening(x, z, Math.max(minimumOpeningY - 1, opening.minimumY()), maximumActionY + 1);
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumActionY);
|
||||
}
|
||||
@@ -117,16 +117,51 @@ final class CaveCandidateSpanBuilder {
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
int neighborX = x + offset[0];
|
||||
int neighborZ = z + offset[1];
|
||||
if (!validation.ownsSurfaceChannelAt(neighborX, neighborZ, course.id())) {
|
||||
HydrologyColumnSample neighbor = validation.surfaceColumnAt(neighborX, neighborZ);
|
||||
if (neighbor == null) {
|
||||
continue;
|
||||
}
|
||||
for (CaveYSpan span : entry.getValue().spans()) {
|
||||
addOpening(neighborX, neighborZ, span.minimumY(), span.maximumY());
|
||||
for (HydrologyColumnLayer layer : neighbor.layers()) {
|
||||
if (layer.feature().courseId() != course.id() || !layer.channel() || !layer.fluidOwned()) {
|
||||
continue;
|
||||
}
|
||||
for (CaveYSpan span : entry.getValue().spans()) {
|
||||
int minimumY = Math.max(span.minimumY(), layer.bedY() + 1);
|
||||
int maximumY = Math.min(span.maximumY(), layer.fluidHeadY());
|
||||
if (minimumY <= maximumY) {
|
||||
addAdjacentFluidOpening(neighborX, neighborZ, minimumY, maximumY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addAdjacentFluidOpening(int x, int z, int minimumY, int maximumY) {
|
||||
CaveSpanSet owned = actions.get(RiverFootprint.pack(x, z));
|
||||
if (owned == null) {
|
||||
addOpening(x, z, minimumY, maximumY);
|
||||
return;
|
||||
}
|
||||
int cursor = minimumY;
|
||||
for (CaveYSpan span : owned.spans()) {
|
||||
if (span.maximumY() < cursor) {
|
||||
continue;
|
||||
}
|
||||
if (span.minimumY() > maximumY) {
|
||||
break;
|
||||
}
|
||||
if (cursor < span.minimumY()) {
|
||||
addOpening(x, z, cursor, Math.min(maximumY, span.minimumY() - 1));
|
||||
}
|
||||
cursor = Math.max(cursor, span.maximumY() + 1);
|
||||
if (cursor > maximumY) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
addOpening(x, z, cursor, maximumY);
|
||||
}
|
||||
|
||||
HydrologyFeatureType oversizedGrotto(HydrologyCaveCourseFilter.Options options) {
|
||||
if (positionCount(coastalGrottoActions)
|
||||
> options.maximumVolume(HydrologyFeatureType.COASTAL_GROTTO)) {
|
||||
|
||||
@@ -11,7 +11,8 @@ record CaveSurfaceOpening(
|
||||
double interiorZ,
|
||||
long radiusSquared,
|
||||
int minimumY,
|
||||
boolean includeNeighborhood
|
||||
boolean includeNeighborhood,
|
||||
boolean surfaceFall
|
||||
) {
|
||||
static CaveSurfaceOpening create(
|
||||
HydraulicSegment segment,
|
||||
@@ -43,7 +44,8 @@ record CaveSurfaceOpening(
|
||||
point.z() + deltaZ * scale,
|
||||
radiusSquared,
|
||||
minimumY,
|
||||
includeNeighborhood
|
||||
includeNeighborhood,
|
||||
segment.type().isSurface() && segment.fallingFluid()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,6 +55,9 @@ record CaveSurfaceOpening(
|
||||
}
|
||||
|
||||
boolean matchesColumn(HydrologyColumnLayer layer, int x, int z) {
|
||||
if (surfaceFall) {
|
||||
return layer.fallingFluid() && layer.feature().segmentId() == segmentId;
|
||||
}
|
||||
return (includeNeighborhood || layer.feature().segmentId() == segmentId)
|
||||
&& distanceSquared(x, z) <= radiusSquared;
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ final class FootprintMutableColumn {
|
||||
first.connectedFluid() || second.connectedFluid(),
|
||||
first.fallingFluid() || second.fallingFluid(),
|
||||
first.receivingPool() || second.receivingPool(),
|
||||
first.terrainOwned() || second.terrainOwned(),
|
||||
(first.terrainOwned() || second.terrainOwned()) && (!drySurfaceBlend || bedY < naturalHeight),
|
||||
first.fluidOwned() || second.fluidOwned(),
|
||||
first.oceanApron() || second.oceanApron(),
|
||||
first.profileKey(),
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydraulicChannelProfile(double[] widths, double[] depths) {
|
||||
public HydraulicChannelProfile {
|
||||
Objects.requireNonNull(widths, "widths");
|
||||
Objects.requireNonNull(depths, "depths");
|
||||
if (widths.length == 0 || widths.length != depths.length) {
|
||||
throw new IllegalArgumentException("Channel dimensions require equally sized, nonempty station arrays.");
|
||||
}
|
||||
widths = widths.clone();
|
||||
depths = depths.clone();
|
||||
for (int station = 0; station < widths.length; station++) {
|
||||
if (!Double.isFinite(widths[station]) || widths[station] <= 0D
|
||||
|| !Double.isFinite(depths[station]) || depths[station] <= 0D) {
|
||||
throw new IllegalArgumentException("Channel dimensions must be finite and positive.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static HydraulicChannelProfile uniform(double width, double depth) {
|
||||
return new HydraulicChannelProfile(new double[]{width}, new double[]{depth});
|
||||
}
|
||||
|
||||
public static HydraulicChannelProfile range(double[] widths, double[] depths, int from, int to) {
|
||||
Objects.checkFromToIndex(from, to, widths.length);
|
||||
Objects.checkFromToIndex(from, to, depths.length);
|
||||
return new HydraulicChannelProfile(Arrays.copyOfRange(widths, from, to), Arrays.copyOfRange(depths, from, to));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] widths() {
|
||||
return widths.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] depths() {
|
||||
return depths.clone();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return widths.length;
|
||||
}
|
||||
|
||||
public double widthAt(int station) {
|
||||
return widths[widths.length == 1 ? 0 : station];
|
||||
}
|
||||
|
||||
public double depthAt(int station) {
|
||||
return depths[depths.length == 1 ? 0 : station];
|
||||
}
|
||||
|
||||
public double maximumWidth() {
|
||||
double maximum = 0D;
|
||||
for (double width : widths) {
|
||||
maximum = Math.max(maximum, width);
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
public double maximumDepth() {
|
||||
double maximum = 0D;
|
||||
for (double depth : depths) {
|
||||
maximum = Math.max(maximum, depth);
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof HydraulicChannelProfile profile
|
||||
&& Arrays.equals(widths, profile.widths)
|
||||
&& Arrays.equals(depths, profile.depths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * Arrays.hashCode(widths) + Arrays.hashCode(depths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HydraulicChannelProfile[widths=" + Arrays.toString(widths) + ", depths=" + Arrays.toString(depths) + "]";
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,13 @@ public record HydraulicSegment(
|
||||
int depth,
|
||||
boolean fallingFluid,
|
||||
boolean receivingPool,
|
||||
List<HydrologyPoint> centerline
|
||||
List<HydrologyPoint> centerline,
|
||||
HydraulicChannelProfile channelProfile
|
||||
) {
|
||||
public HydraulicSegment {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(centerline, "centerline");
|
||||
Objects.requireNonNull(channelProfile, "channelProfile");
|
||||
centerline = List.copyOf(centerline);
|
||||
if (centerline.isEmpty()) {
|
||||
throw new IllegalArgumentException("A hydraulic segment requires a centerline.");
|
||||
@@ -28,6 +30,12 @@ public record HydraulicSegment(
|
||||
if (width <= 0 || depth <= 0) {
|
||||
throw new IllegalArgumentException("Hydraulic segment width and depth must be positive.");
|
||||
}
|
||||
if (channelProfile.size() != 1 && channelProfile.size() != centerline.size()) {
|
||||
throw new IllegalArgumentException("Channel dimensions must cover every centerline station.");
|
||||
}
|
||||
if (channelProfile.maximumWidth() > width || channelProfile.maximumDepth() > depth) {
|
||||
throw new IllegalArgumentException("Hydraulic segment bounds must contain its channel dimensions.");
|
||||
}
|
||||
int drop = upstreamHeadY - downstreamHeadY;
|
||||
if (fallingFluid && drop == 0) {
|
||||
throw new IllegalArgumentException("Falling fluid requires a positive head transition.");
|
||||
|
||||
@@ -6,5 +6,7 @@ public enum HydrologyCandidateKind {
|
||||
DEEP_FLUID,
|
||||
POOL,
|
||||
/** A later course to an outlet that could not be joined to the outlet's main course. */
|
||||
TRIBUTARY
|
||||
TRIBUTARY,
|
||||
REGIONAL_SOURCE,
|
||||
COASTAL_CHANNEL
|
||||
}
|
||||
|
||||
@@ -21,5 +21,8 @@ public enum HydrologyCandidateRejection {
|
||||
SURFACE_SEGMENT_UNSUPPORTED,
|
||||
SURFACE_SINKHOLE_CLEARANCE,
|
||||
SURFACE_SHAPE_UNSUPPORTED,
|
||||
SURFACE_MOUTH_DISCONNECTED,
|
||||
SURFACE_WATER_CONTAINMENT,
|
||||
SURFACE_BANK_BUDGET,
|
||||
POLICY_EXCLUDED;
|
||||
}
|
||||
|
||||
+59
-41
@@ -38,30 +38,6 @@ final class HydrologyCaveCourseFilter {
|
||||
{1, 0}, {-1, 0}, {0, 1}, {0, -1}
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
static final CaveVoxelView GENERATED_CHANNEL_VIEW = new CaveVoxelView() {
|
||||
@Override
|
||||
public boolean isInWorld(CavePosition position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxel voxelAt(CavePosition position) {
|
||||
return CaveVoxel.UNCONDITIONAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpenToSurface(CavePosition position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAboveTerrainSurface(CavePosition position) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
final HydrologyCaveContainmentPlanner planner;
|
||||
@@ -93,7 +69,7 @@ final class HydrologyCaveCourseFilter {
|
||||
this.planner = new HydrologyCaveContainmentPlanner();
|
||||
this.options = Objects.requireNonNull(options);
|
||||
CaveVoxelView observedView = Objects.requireNonNull(view);
|
||||
this.view = options.connectToExistingCaves() ? observedView : GENERATED_CHANNEL_VIEW;
|
||||
this.view = options.connectToExistingCaves() ? observedView : new GeneratedChannelView(observedView);
|
||||
this.candidateCache = candidateCache;
|
||||
this.validationCache = validationCache;
|
||||
this.plannedSurface = plannedSurface;
|
||||
@@ -438,7 +414,7 @@ final class HydrologyCaveCourseFilter {
|
||||
) {
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> composedActions = null;
|
||||
for (HydrologyColumnSample caveSample : validation.columnsForCourse(course.id())) {
|
||||
int maximumCaveY = maximumCaveY(caveSample, course.id());
|
||||
int maximumCaveY = maximumCaveY(caveSample, course);
|
||||
if (maximumCaveY == Integer.MIN_VALUE) {
|
||||
continue;
|
||||
}
|
||||
@@ -549,14 +525,14 @@ final class HydrologyCaveCourseFilter {
|
||||
&& first.outletId().getAsLong() == second.outletId().getAsLong();
|
||||
}
|
||||
|
||||
int maximumCaveY(HydrologyColumnSample sample, long courseId) {
|
||||
int maximumCaveY(HydrologyColumnSample sample, RiverCourse course) {
|
||||
int maximumY = Integer.MIN_VALUE;
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (layer.feature().courseId() == courseId
|
||||
&& isCaveLayer(layer)
|
||||
if (layer.feature().courseId() == course.id()
|
||||
&& isCaveLayer(layer, course)
|
||||
&& !layer.oceanApron()
|
||||
&& layer.channel()
|
||||
&& layer.terrainOwned()) {
|
||||
&& (layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())) {
|
||||
maximumY = Math.max(maximumY, layer.ceilingY());
|
||||
}
|
||||
}
|
||||
@@ -673,8 +649,8 @@ final class HydrologyCaveCourseFilter {
|
||||
for (HydrologyColumnSample sample : columns) {
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
CaveCandidateSpanBuilder builder = builders.get(layer.feature().courseId());
|
||||
if (builder == null || !isCaveLayer(layer) || layer.oceanApron()
|
||||
|| !layer.channel() || !layer.terrainOwned()) {
|
||||
if (builder == null || !isCaveLayer(layer, builder.course()) || layer.oceanApron()
|
||||
|| !layer.channel() || !(layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())) {
|
||||
continue;
|
||||
}
|
||||
builder.addAction(sample.x(), sample.z(), layer);
|
||||
@@ -705,10 +681,10 @@ final class HydrologyCaveCourseFilter {
|
||||
int maximumCaveY = Integer.MIN_VALUE;
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (layer.feature().courseId() != course.id()
|
||||
|| !isCaveLayer(layer)
|
||||
|| !isCaveLayer(layer, course)
|
||||
|| layer.oceanApron()
|
||||
|| !layer.channel()
|
||||
|| !layer.terrainOwned()) {
|
||||
|| !(layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())) {
|
||||
continue;
|
||||
}
|
||||
builder.addAction(sample.x(), sample.z(), layer);
|
||||
@@ -770,8 +746,8 @@ final class HydrologyCaveCourseFilter {
|
||||
for (HydrologyColumnSample sample : columns) {
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
CaveCandidateBuilder builder = builders.get(layer.feature().courseId());
|
||||
if (builder == null || !isCaveLayer(layer) || layer.oceanApron()
|
||||
|| !layer.channel() || !layer.terrainOwned()) {
|
||||
if (builder == null || !isCaveLayer(layer, builder.course()) || layer.oceanApron()
|
||||
|| !layer.channel() || !(layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())) {
|
||||
continue;
|
||||
}
|
||||
for (int y = layer.bedY() + 1; y <= layer.ceilingY(); y++) {
|
||||
@@ -809,10 +785,10 @@ final class HydrologyCaveCourseFilter {
|
||||
int maximumCaveY = Integer.MIN_VALUE;
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (layer.feature().courseId() != course.id()
|
||||
|| !isCaveLayer(layer)
|
||||
|| !isCaveLayer(layer, course)
|
||||
|| layer.oceanApron()
|
||||
|| !layer.channel()
|
||||
|| !layer.terrainOwned()) {
|
||||
|| !(layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())) {
|
||||
continue;
|
||||
}
|
||||
for (int y = layer.bedY() + 1; y <= layer.ceilingY(); y++) {
|
||||
@@ -858,7 +834,8 @@ final class HydrologyCaveCourseFilter {
|
||||
static HydraulicSegment representativeCaveSegment(RiverCourse course) {
|
||||
HydraulicSegment selected = null;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (!segment.type().isUnderground() && !segment.type().isDeepFluid()) {
|
||||
if (!segment.type().isUnderground() && !segment.type().isDeepFluid()
|
||||
&& !(segment.type().isSurface() && segment.fallingFluid())) {
|
||||
continue;
|
||||
}
|
||||
if (selected == null || candidatePriority(segment.type()) < candidatePriority(selected.type())) {
|
||||
@@ -878,8 +855,16 @@ final class HydrologyCaveCourseFilter {
|
||||
return 2;
|
||||
}
|
||||
|
||||
boolean isCaveLayer(HydrologyColumnLayer layer) {
|
||||
return layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid();
|
||||
boolean isCaveLayer(HydrologyColumnLayer layer, RiverCourse course) {
|
||||
if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) {
|
||||
return true;
|
||||
}
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (segment.id() == layer.feature().segmentId()) {
|
||||
return segment.type().isSurface() && segment.fallingFluid();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
HydrologyCaveAction actionAt(HydrologyColumnLayer layer, int y) {
|
||||
@@ -1213,6 +1198,12 @@ final class HydrologyCaveCourseFilter {
|
||||
List<HydraulicSegment> segments = course.segments();
|
||||
for (int index = 0; index < segments.size(); index++) {
|
||||
HydraulicSegment segment = segments.get(index);
|
||||
if (segment.type().isSurface() && segment.fallingFluid()) {
|
||||
int radius = Math.max(1, segment.width() / 2) + 1;
|
||||
openings.add(CaveSurfaceOpening.create(segment, true, (long) radius * radius,
|
||||
segment.downstreamHeadY() + 2, true));
|
||||
continue;
|
||||
}
|
||||
if (!segment.type().isUnderground()) {
|
||||
continue;
|
||||
}
|
||||
@@ -1259,4 +1250,31 @@ final class HydrologyCaveCourseFilter {
|
||||
}
|
||||
return List.copyOf(openings);
|
||||
}
|
||||
|
||||
private record GeneratedChannelView(CaveVoxelView terrain) implements CaveVoxelView {
|
||||
@Override
|
||||
public boolean isInWorld(CavePosition position) {
|
||||
return terrain.isInWorld(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxel voxelAt(CavePosition position) {
|
||||
return CaveVoxel.UNCONDITIONAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpenToSurface(CavePosition position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAboveTerrainSurface(CavePosition position) {
|
||||
return terrain.isAboveTerrainSurface(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAboveTerrainSurface(int x, int z, int minimumY, int maximumY) {
|
||||
return terrain.hasAboveTerrainSurface(x, z, minimumY, maximumY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,9 +184,9 @@ final class HydrologyChannelGeometry {
|
||||
double distance = rawDistance;
|
||||
if (shape.fallingThroat() && (flowX != 0 || flowZ != 0)) {
|
||||
double flowLength = StrictMath.hypot(flowX, flowZ);
|
||||
double along = StrictMath.abs(deltaX * flowX + deltaZ * flowZ) / flowLength;
|
||||
double along = (deltaX * flowX + deltaZ * flowZ) / flowLength;
|
||||
double across = StrictMath.abs(deltaX * -flowZ + deltaZ * flowX) / flowLength;
|
||||
if (along > 0.75D) {
|
||||
if (StrictMath.abs(along) > 0.75D || segment.type().isSurface() && along < 0D) {
|
||||
return shape.totalRadius() + 1D;
|
||||
}
|
||||
distance = across;
|
||||
@@ -240,6 +240,9 @@ final class HydrologyChannelGeometry {
|
||||
+ coherent * channelShape.wallRoughness() * 0.7D
|
||||
+ detail * channelShape.wallRoughness() * 0.3D;
|
||||
radialScale = Math.max(channelShape.radialMinimum(), Math.min(channelShape.radialMaximum(), radialScale));
|
||||
if (segment.type().isSurface() && shape.fallingThroat()) {
|
||||
radialScale = Math.max(1D, radialScale);
|
||||
}
|
||||
return distance / radialScale;
|
||||
}
|
||||
|
||||
@@ -280,7 +283,8 @@ final class HydrologyChannelGeometry {
|
||||
+ (sideBias - 0.5D) * 0.08D;
|
||||
widthScale = Math.max(1D, Math.min(1.18D, widthScale));
|
||||
double organicDistance = StrictMath.abs(signedCross - thalweg) / widthScale;
|
||||
return Math.min(StrictMath.abs(signedCross), organicDistance);
|
||||
double along = (deltaX * flowX + deltaZ * flowZ) / flowLength;
|
||||
return StrictMath.hypot(along, Math.min(StrictMath.abs(signedCross), organicDistance));
|
||||
}
|
||||
|
||||
HydrologyPlannerSettings.ChannelShape channelShape(HydrologyFeatureType type) {
|
||||
|
||||
@@ -55,9 +55,10 @@ public record HydrologyColumnSample(
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.feature().type().isSurface()
|
||||
&& !layer.oceanApron()
|
||||
&& (layer.terrainOwned() || layer.fluidOwned() || layer.grading() || layer.shore())) {
|
||||
&& (layer.terrainOwned() || layer.fluidOwned() || layer.grading() || layer.shore())
|
||||
&& (naturalHeight < seaLevel || layer.fluidHeadY() > seaLevel)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Naturally submerged columns cannot contain owned surface hydrology writes."
|
||||
"Surface hydrology cannot own submerged ground or raise sea-level water."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -129,7 +130,7 @@ public record HydrologyColumnSample(
|
||||
|
||||
public int terrainHeight() {
|
||||
HydrologyColumnLayer primary = selectSurfaceLayer(false);
|
||||
if (primary == null) {
|
||||
if (primary == null || !primary.terrainOwned()) {
|
||||
return naturalHeight;
|
||||
}
|
||||
if (primary.channel()) {
|
||||
@@ -164,8 +165,6 @@ public record HydrologyColumnSample(
|
||||
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;
|
||||
@@ -178,7 +177,10 @@ public record HydrologyColumnSample(
|
||||
if (layer.channel()) {
|
||||
return 0;
|
||||
}
|
||||
return layer.shore() ? 1 : 2;
|
||||
if (layer.terrainOwned()) {
|
||||
return layer.shore() ? 1 : 2;
|
||||
}
|
||||
return layer.shore() ? 3 : 4;
|
||||
}
|
||||
|
||||
private static int surfaceActionPriority(HydrologyCaveAction action) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public record HydrologyColumnSnapshot(boolean available, HydrologyColumnSample column) {
|
||||
private static final HydrologyColumnSnapshot UNAVAILABLE = new HydrologyColumnSnapshot(false, null);
|
||||
private static final HydrologyColumnSnapshot READY_NATURAL = new HydrologyColumnSnapshot(true, null);
|
||||
|
||||
public HydrologyColumnSnapshot {
|
||||
if (!available && column != null) {
|
||||
throw new IllegalArgumentException("Unavailable hydrology cannot contain a planned column.");
|
||||
}
|
||||
}
|
||||
|
||||
public static HydrologyColumnSnapshot unavailable() {
|
||||
return UNAVAILABLE;
|
||||
}
|
||||
|
||||
public static HydrologyColumnSnapshot ready(HydrologyColumnSample column) {
|
||||
return column == null ? READY_NATURAL : new HydrologyColumnSnapshot(true, column);
|
||||
}
|
||||
}
|
||||
+50
-12
@@ -5,6 +5,7 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCandidate;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveContainmentPlanner;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
|
||||
import it.unimi.dsi.fastutil.longs.LongIterator;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
@@ -112,6 +113,8 @@ final class HydrologyCrossTileResolver {
|
||||
planner.naturalSampler
|
||||
)
|
||||
);
|
||||
seedRegionalSurface(key, footprintCompiler);
|
||||
diagnostics.addAll(footprintCompiler.regionalNetwork.diagnostics());
|
||||
HashMap<Long, List<HydrologyPoint>> refinedEdges = new HashMap<>();
|
||||
HashMap<SourceCompilationKey, SourceCompilation> sourceCompilations = new HashMap<>();
|
||||
settleSelection(
|
||||
@@ -175,11 +178,11 @@ final class HydrologyCrossTileResolver {
|
||||
break;
|
||||
}
|
||||
boolean surfaceChanged = surfaceSelection.advanceAfterPublication(
|
||||
publication.result().courses(),
|
||||
publication.result(),
|
||||
grid
|
||||
);
|
||||
boolean undergroundChanged = undergroundSelection.advanceAfterPublication(
|
||||
publication.result().courses(),
|
||||
publication.result(),
|
||||
grid
|
||||
);
|
||||
if (!surfaceChanged && !undergroundChanged) {
|
||||
@@ -305,7 +308,9 @@ final class HydrologyCrossTileResolver {
|
||||
planner.naturalSampler
|
||||
)
|
||||
);
|
||||
seedRegionalSurface(resolution.draft().key(), footprintCompiler);
|
||||
}
|
||||
result = planner.regional.include(result, footprintCompiler.regionalNetwork);
|
||||
MaterializedHydrology materialized = materializeFinalHydrology(
|
||||
result,
|
||||
diagnostics,
|
||||
@@ -313,6 +318,8 @@ final class HydrologyCrossTileResolver {
|
||||
!resolution.observedRejections().isEmpty()
|
||||
);
|
||||
result = materialized.result();
|
||||
result = clipRegionalPlans(result, footprintCompiler.regionalNetwork,
|
||||
planner.regional.ownerBounds(resolution.draft().key()));
|
||||
RiverFootprint footprint = materialized.footprint();
|
||||
if (footprintCompiler.fullMaterializationCount() < 1) {
|
||||
throw new IllegalStateException("Hydrology publication did not materialize its full footprint.");
|
||||
@@ -332,6 +339,30 @@ final class HydrologyCrossTileResolver {
|
||||
);
|
||||
}
|
||||
|
||||
private void seedRegionalSurface(HydrologyTileKey key, HydrologyFootprintCompiler compiler) {
|
||||
if (!planner.regional.enabled()) {
|
||||
return;
|
||||
}
|
||||
SurfaceBounds bounds = planner.regional.ownerBounds(key);
|
||||
compiler.seedRegionalSurface(planner.regional.coursesIn(bounds), bounds);
|
||||
}
|
||||
|
||||
static HydrologyCaveCourseFilter.Result clipRegionalPlans(HydrologyCaveCourseFilter.Result result,
|
||||
HydrologyRegionalNetwork regional, SurfaceBounds bounds) {
|
||||
if (regional.cavePlans().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
HashSet<Long> regionalIds = new HashSet<>();
|
||||
for (RiverCourse course : regional.courses()) {
|
||||
regionalIds.add(course.id());
|
||||
}
|
||||
ArrayList<HydrologyCavePlan> plans = new ArrayList<>(result.cavePlans().size());
|
||||
for (HydrologyCavePlan plan : result.cavePlans()) {
|
||||
plans.add(regionalIds.contains(plan.source().sourceId()) ? HydrologyRegionalFalls.clip(plan, bounds) : plan);
|
||||
}
|
||||
return new HydrologyCaveCourseFilter.Result(result.nodes(), result.edges(), result.outlets(), result.courses(), List.copyOf(plans));
|
||||
}
|
||||
|
||||
MaterializedHydrology materializeFinalHydrology(
|
||||
HydrologyCaveCourseFilter.Result initial,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||
@@ -339,17 +370,22 @@ final class HydrologyCrossTileResolver {
|
||||
boolean revalidationRequired
|
||||
) {
|
||||
HydrologyCaveCourseFilter.Result current = initial;
|
||||
HashSet<Long> regionalIds = new HashSet<>();
|
||||
for (RiverCourse course : footprintCompiler.regionalNetwork.courses()) {
|
||||
regionalIds.add(course.id());
|
||||
}
|
||||
int maximumPasses = Math.addExact(initial.courses().size(), 1);
|
||||
for (int pass = 0; pass < maximumPasses; pass++) {
|
||||
RiverFootprint footprint = footprintCompiler.compile(current.courses());
|
||||
if (current.cavePlans().isEmpty()) {
|
||||
HydrologyCaveCourseFilter.Result local = HydrologyCaveCourseFilter.withoutCourses(current, regionalIds);
|
||||
if (local.cavePlans().isEmpty()) {
|
||||
return new MaterializedHydrology(current, footprint);
|
||||
}
|
||||
HydrologyFootprintCompiler.ValidationRaster compactValidation = footprintCompiler.compileValidation(
|
||||
current.courses()
|
||||
);
|
||||
if (!revalidationRequired
|
||||
&& compactSurfaceMatchesMaterializedPlans(current, compactValidation, footprint)) {
|
||||
&& compactSurfaceMatchesMaterializedPlans(local, compactValidation, footprint)) {
|
||||
return new MaterializedHydrology(current, footprint);
|
||||
}
|
||||
HydrologyFootprintCompiler.ValidationRaster validation = compactValidation.withMaterializedSurface(
|
||||
@@ -373,13 +409,15 @@ final class HydrologyCrossTileResolver {
|
||||
null,
|
||||
plannedSurface
|
||||
).filter(
|
||||
current.nodes(),
|
||||
current.edges(),
|
||||
current.outlets(),
|
||||
current.courses(),
|
||||
local.nodes(),
|
||||
local.edges(),
|
||||
local.outlets(),
|
||||
local.courses(),
|
||||
validation,
|
||||
diagnostics
|
||||
);
|
||||
revalidated = HydrologyRegionalProtection.rejectPlans(revalidated, footprintCompiler.regionalNetwork, diagnostics);
|
||||
revalidated = planner.regional.include(revalidated, footprintCompiler.regionalNetwork);
|
||||
if (revalidated.courses().equals(current.courses())) {
|
||||
return new MaterializedHydrology(revalidated, footprint);
|
||||
}
|
||||
@@ -596,9 +634,6 @@ final class HydrologyCrossTileResolver {
|
||||
boolean reachesOutlet = terminalType == HydrologyFeatureType.MOUTH
|
||||
|| terminalType == HydrologyFeatureType.COASTAL_GROTTO
|
||||
|| terminalType == HydrologyFeatureType.INLAND_GROTTO;
|
||||
if (!reachesOutlet) {
|
||||
continue;
|
||||
}
|
||||
claims.add(new HydrologyCrossTileSurfaceAdmission.Claim(
|
||||
course.id(),
|
||||
course.outletId().getAsLong(),
|
||||
@@ -691,7 +726,7 @@ final class HydrologyCrossTileResolver {
|
||||
false
|
||||
);
|
||||
diagnostics.addAll(attempt.diagnostics());
|
||||
if (!selection.advanceAfterPublication(attempt.result().courses(), grid)) {
|
||||
if (!selection.advanceAfterPublication(attempt.result(), grid)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -784,6 +819,7 @@ final class HydrologyCrossTileResolver {
|
||||
graph = planner.sourcePlanner.mergeGraphs(surfaceGraph, undergroundGraph);
|
||||
}
|
||||
}
|
||||
graph = new HydrologyRegionalTributaries(planner).connect(graph, courses, footprintCompiler, diagnostics);
|
||||
HydrologySurfaceProfiles.rejectExcludedWetFootprints(footprintCompiler, courses, diagnostics);
|
||||
List<RiverCourse> normalizedTrunkCourses = planner.tributaries.normalizeSharedTrunks(courses, diagnostics);
|
||||
List<RiverCourse> normalizedOutletCourses = planner.tributaries.normalizeOutletContinuations(normalizedTrunkCourses);
|
||||
@@ -796,6 +832,7 @@ final class HydrologyCrossTileResolver {
|
||||
if (includeDeepFluids && grid != null) {
|
||||
planner.featureSites.compileSeaCaves(grid, courses, diagnostics);
|
||||
}
|
||||
HydrologyRegionalProtection.rejectFootprints(courses, footprintCompiler, diagnostics);
|
||||
DraftProfile profile = planner.currentDraftProfile();
|
||||
long rasterStarted = System.nanoTime();
|
||||
HydrologyFootprintCompiler.ValidationRaster validation = footprintCompiler.compileValidation(courses);
|
||||
@@ -837,6 +874,7 @@ final class HydrologyCrossTileResolver {
|
||||
);
|
||||
profile.filterNanos += System.nanoTime() - filterStarted;
|
||||
profile.filterCalls++;
|
||||
containment = HydrologyRegionalProtection.rejectPlans(containment, footprintCompiler.regionalNetwork, diagnostics);
|
||||
return new PublicationAttempt(containment, diagnostics);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ final class HydrologyCrossTileSurfaceAdmission {
|
||||
}
|
||||
|
||||
private static boolean conflicts(Claim first, Claim second) {
|
||||
if (first.outletId() == second.outletId()) {
|
||||
if (first.outletId() == second.outletId() && first.reachesOutlet() && second.reachesOutlet()) {
|
||||
return true;
|
||||
}
|
||||
int minimumSeparation = Math.max(first.sourceSpacing(), second.sourceSpacing());
|
||||
|
||||
+3
-3
@@ -176,7 +176,7 @@ final class HydrologyFeatureSitePlanner {
|
||||
pool.depth(),
|
||||
false,
|
||||
false,
|
||||
centerlinePoints
|
||||
centerlinePoints, HydraulicChannelProfile.uniform(radius * 2, pool.depth())
|
||||
);
|
||||
return new RiverCourse(
|
||||
courseId,
|
||||
@@ -651,7 +651,7 @@ final class HydrologyFeatureSitePlanner {
|
||||
Math.max(deepFluid.channelDepth(), verticalRadius),
|
||||
false,
|
||||
false,
|
||||
List.of(center)
|
||||
List.of(center), HydraulicChannelProfile.uniform(horizontalRadius * 2 + 1, Math.max(deepFluid.channelDepth(), verticalRadius))
|
||||
));
|
||||
}
|
||||
if (deepFluid.shortChannels() && deepFluid.maximumChannelLength() > 0) {
|
||||
@@ -683,7 +683,7 @@ final class HydrologyFeatureSitePlanner {
|
||||
length,
|
||||
planner.settings.routing().refinementSpacing(),
|
||||
site.stableId()
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(deepFluid.channelWidth(), deepFluid.channelDepth())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
+146
-9
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
@@ -41,6 +42,9 @@ final class HydrologyFootprintCompiler {
|
||||
final Map<FootprintRasterStencilKey, FootprintRasterStencil> rasterStencils;
|
||||
final SurfaceFootprintCompiler surfaceCompiler;
|
||||
final LinkedHashMap<FootprintCourseRasterKey, SurfaceFootprint> surfaceFootprints;
|
||||
final Map<Long, SurfaceFootprint> regionalSurfaceFootprints;
|
||||
final Map<Long, HydrologySurfaceDropRaster> regionalDropRasters;
|
||||
HydrologyRegionalNetwork regionalNetwork;
|
||||
|
||||
int fullMaterializationCount;
|
||||
final HydrologyFootprintRasterizer rasterizer;
|
||||
@@ -83,12 +87,23 @@ final class HydrologyFootprintCompiler {
|
||||
this.rasterStencils = new HashMap<>();
|
||||
this.surfaceCompiler = new SurfaceFootprintCompiler(settings, this::sampleTerrainBasis, this.geometrySampler);
|
||||
this.surfaceFootprints = new LinkedHashMap<>(COURSE_FOOTPRINT_CACHE_SIZE, 1F, true);
|
||||
this.regionalSurfaceFootprints = new LinkedHashMap<>();
|
||||
this.regionalDropRasters = new LinkedHashMap<>();
|
||||
this.regionalNetwork = HydrologyRegionalNetwork.EMPTY;
|
||||
this.fullMaterializationCount = 0;
|
||||
this.rasterizer = new HydrologyFootprintRasterizer(this);
|
||||
this.channelGeometry = new HydrologyChannelGeometry(this);
|
||||
}
|
||||
|
||||
SurfaceRasterIndex emptySurfaceRaster() {
|
||||
return new SurfaceRasterIndex(List.<SurfaceFootprint>of(), new Long2ObjectOpenHashMap<>());
|
||||
}
|
||||
|
||||
SurfaceFootprint surfaceFootprint(RiverCourse course) {
|
||||
SurfaceFootprint regional = regionalSurfaceFootprints.get(course.id());
|
||||
if (regional != null) {
|
||||
return regional;
|
||||
}
|
||||
FootprintCourseRasterKey rasterKey = new FootprintCourseRasterKey(
|
||||
course.id(),
|
||||
course.type(),
|
||||
@@ -107,6 +122,21 @@ final class HydrologyFootprintCompiler {
|
||||
return compiled;
|
||||
}
|
||||
|
||||
void seedRegionalSurface(HydrologyRegionalNetwork network, SurfaceBounds bounds) {
|
||||
regionalNetwork = Objects.requireNonNull(network);
|
||||
regionalSurfaceFootprints.clear();
|
||||
regionalDropRasters.clear();
|
||||
for (RiverCourse course : network.courses()) {
|
||||
SurfaceFootprint footprint = surfaceCompiler.compile(course, bounds);
|
||||
if (!footprint.accepted()) {
|
||||
throw new IllegalStateException("Accepted regional river changed during bounded footprint compilation: " + course.id());
|
||||
}
|
||||
regionalSurfaceFootprints.put(course.id(), footprint);
|
||||
regionalDropRasters.put(course.id(), HydrologySurfaceDropRaster.compile(settings,
|
||||
HydrologyOceanReceiver.forCourse(settings, this::sampleTerrainBasis, course), geometrySampler, course, bounds));
|
||||
}
|
||||
}
|
||||
|
||||
RiverFootprint compile(List<RiverCourse> courses) {
|
||||
fullMaterializationCount++;
|
||||
if (courses.isEmpty()) {
|
||||
@@ -158,6 +188,9 @@ final class HydrologyFootprintCompiler {
|
||||
HydrologyColumnSample first = firstSamples.putIfAbsent(packed, sample);
|
||||
if (first != null) {
|
||||
validateMatchingTerrainMetadata(first, sample);
|
||||
FootprintMutableColumn merged = new FootprintMutableColumn(first);
|
||||
merged.merge(sample);
|
||||
firstSamples.put(packed, merged.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,6 +200,20 @@ final class HydrologyFootprintCompiler {
|
||||
);
|
||||
}
|
||||
|
||||
ValidationRaster compileSurfaceDropValidation(RiverCourse course, SurfaceFootprint surface,
|
||||
HydrologySurfaceDropRaster drops) {
|
||||
Objects.requireNonNull(course, "course");
|
||||
Objects.requireNonNull(surface, "surface");
|
||||
Objects.requireNonNull(drops, "drops");
|
||||
Long2ObjectOpenHashMap<HydrologyColumnSample> samples = new Long2ObjectOpenHashMap<>();
|
||||
for (HydrologyColumnSample sample : drops.columns()) {
|
||||
samples.put(RiverFootprint.pack(sample.x(), sample.z()), sample);
|
||||
}
|
||||
FootprintValidationCourseRaster raster = new FootprintValidationCourseRaster(
|
||||
course.id(), drops.columns(), surface);
|
||||
return new ValidationRaster(List.of(raster), new SurfaceRasterIndex(List.of(surface), samples));
|
||||
}
|
||||
|
||||
int fullMaterializationCount() {
|
||||
return fullMaterializationCount;
|
||||
}
|
||||
@@ -190,16 +237,20 @@ final class HydrologyFootprintCompiler {
|
||||
for (SurfaceLayerColumn column : surface.columns()) {
|
||||
addLayer(columns, column.x(), column.z(), column.terrain(), column.layer());
|
||||
}
|
||||
boolean boundedRegionalDrops = addRegionalDrops(columns, course.id());
|
||||
SurfaceRasterIndex courseSurface = new SurfaceRasterIndex(List.of(surface), new Long2ObjectOpenHashMap<>());
|
||||
for (int segmentIndex = 0; segmentIndex < course.segments().size(); segmentIndex++) {
|
||||
HydraulicSegment segment = course.segments().get(segmentIndex);
|
||||
if (boundedRegionalDrops && segment.type().isSurface() && segment.fallingFluid()) {
|
||||
continue;
|
||||
}
|
||||
boolean firstSegment = segmentIndex == 0;
|
||||
boolean clipStart = segmentIndex > 0
|
||||
&& rasterizer.segmentsJoin(course.segments().get(segmentIndex - 1), segment);
|
||||
boolean clipEnd = segmentIndex + 1 < course.segments().size()
|
||||
&& rasterizer.segmentsJoin(segment, course.segments().get(segmentIndex + 1));
|
||||
if (caveCourse && (segment.type().isUnderground()
|
||||
|| segment.type().isDeepFluid())) {
|
||||
|| segment.type().isDeepFluid() || segment.type().isSurface() && segment.fallingFluid())) {
|
||||
rasterizer.rasterizeSegment(
|
||||
columns,
|
||||
course,
|
||||
@@ -226,7 +277,8 @@ final class HydrologyFootprintCompiler {
|
||||
|
||||
boolean hasCaveSegment(RiverCourse course) {
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (segment.type().isUnderground() || segment.type().isDeepFluid()) {
|
||||
if (segment.type().isUnderground() || segment.type().isDeepFluid()
|
||||
|| segment.type().isSurface() && segment.fallingFluid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -246,6 +298,21 @@ final class HydrologyFootprintCompiler {
|
||||
return List.copyOf(built);
|
||||
}
|
||||
|
||||
void mergeSurfaceDrop(Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> columns,
|
||||
Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> dropColumns) {
|
||||
List<HydrologyColumnSample> constrained = HydrologySurfaceDropBankBounds.constrain(
|
||||
buildValidationColumns(dropColumns), settings.surface().banks().erosion().excavation().maximumWidth());
|
||||
for (HydrologyColumnSample sample : constrained) {
|
||||
long key = RiverFootprint.pack(sample.x(), sample.z());
|
||||
FootprintMutableColumn column = columns.get(key);
|
||||
if (column == null) {
|
||||
columns.put(key, new FootprintMutableColumn(sample));
|
||||
} else {
|
||||
column.merge(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void validateMatchingTerrainMetadata(
|
||||
HydrologyColumnSample first,
|
||||
HydrologyColumnSample second
|
||||
@@ -278,10 +345,12 @@ final class HydrologyFootprintCompiler {
|
||||
for (SurfaceLayerColumn column : surface.columns()) {
|
||||
addLayer(columns, column.x(), column.z(), column.terrain(), column.layer());
|
||||
}
|
||||
boolean boundedRegionalDrops = addRegionalDrops(columns, course.id());
|
||||
SurfaceRasterIndex courseSurface = new SurfaceRasterIndex(List.of(surface), new Long2ObjectOpenHashMap<>());
|
||||
for (int segmentIndex = 0; segmentIndex < course.segments().size(); segmentIndex++) {
|
||||
HydraulicSegment segment = course.segments().get(segmentIndex);
|
||||
if (SurfaceFootprintCompiler.exposedSegment(segment)) {
|
||||
if (SurfaceFootprintCompiler.exposedSegment(segment)
|
||||
|| boundedRegionalDrops && segment.type().isSurface() && segment.fallingFluid()) {
|
||||
continue;
|
||||
}
|
||||
boolean clipStart = segmentIndex > 0
|
||||
@@ -307,6 +376,19 @@ final class HydrologyFootprintCompiler {
|
||||
return footprint;
|
||||
}
|
||||
|
||||
private boolean addRegionalDrops(Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> columns, long courseId) {
|
||||
HydrologySurfaceDropRaster drops = regionalDropRasters.get(courseId);
|
||||
if (drops == null) {
|
||||
return false;
|
||||
}
|
||||
for (HydrologyColumnSample sample : drops.columns()) {
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
addLayer(columns, sample.x(), sample.z(), sampleTerrainBasis(sample.x(), sample.z()), layer);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
RiverFootprint build(Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> columns) {
|
||||
LinkedHashMap<Long, HydrologyColumnSample> immutable = new LinkedHashMap<>();
|
||||
for (Long2ObjectMap.Entry<FootprintMutableColumn> entry : columns.long2ObjectEntrySet()) {
|
||||
@@ -479,19 +561,37 @@ final class HydrologyFootprintCompiler {
|
||||
channel,
|
||||
ellipsoidScale
|
||||
);
|
||||
boolean surfaceDrop = segment.type().isSurface() && segment.fallingFluid();
|
||||
if (surfaceDrop && channel && !falling) {
|
||||
int maximumIncision = terrain.surfacePolicy().maximumIncision(settings.surface().maximumIncision());
|
||||
int allowedIncision = Math.min(maximumIncision,
|
||||
(int) StrictMath.floor(maximumIncision * terrain.incisionMultiplier()));
|
||||
bed = Math.max(bed, Math.min(shape.fluidHead(), terrain.naturalHeight() - allowedIncision));
|
||||
if (bed == shape.fluidHead()) {
|
||||
channel = false;
|
||||
shore = true;
|
||||
grading = true;
|
||||
}
|
||||
}
|
||||
if (surfaceDrop && !channel) {
|
||||
HydrologyPlannerSettings.Excavation excavation = settings.surface().banks().erosion().excavation();
|
||||
double bankDistance = Math.max(0D, distance - shape.channelRadius());
|
||||
bed = bankDistance > excavation.maximumWidth() ? terrain.naturalHeight()
|
||||
: Math.max(bed, terrain.naturalHeight() - excavation.maximumDepth());
|
||||
}
|
||||
int fluidHead = channel ? shape.fluidHead() : bed;
|
||||
int ceiling = channel ? localCeiling(shape, ellipsoidScale, segment, channelShape, worldX, worldZ) : fluidHead;
|
||||
if (channel && terrainRoofedLayer(course, segment)) {
|
||||
ceiling = Math.max(fluidHead, Math.min(ceiling, terrain.naturalHeight() - 1));
|
||||
}
|
||||
boolean terrainOwned = !falling;
|
||||
boolean terrainOwned = !falling && (!surfaceDrop || channel || bed < terrain.naturalHeight());
|
||||
return new HydrologyColumnLayer(
|
||||
feature,
|
||||
bed,
|
||||
fluidHead,
|
||||
ceiling,
|
||||
channel,
|
||||
terrainOwned && shore,
|
||||
!falling && shore,
|
||||
terrainOwned && (grading || shore),
|
||||
channel,
|
||||
channel && falling,
|
||||
@@ -826,11 +926,16 @@ final class HydrologyFootprintCompiler {
|
||||
return sampled;
|
||||
}
|
||||
|
||||
boolean elevatedSeaLevelSurfaceColumn(HydraulicSegment segment, HydrologyTerrainSample terrain, int fluidHead) {
|
||||
return segment.type().isSurface() && terrain.naturalHeight() <= settings.seaLevel()
|
||||
&& fluidHead > settings.seaLevel();
|
||||
}
|
||||
|
||||
boolean naturallySubmergedSurfaceColumn(
|
||||
HydraulicSegment segment,
|
||||
HydrologyTerrainSample terrain
|
||||
) {
|
||||
return segment.type().isSurface() && terrain.naturalHeight() <= settings.seaLevel();
|
||||
return segment.type().isSurface() && terrain.naturalHeight() < settings.seaLevel();
|
||||
}
|
||||
|
||||
boolean oceanApronEligible(HydraulicSegment segment, double distance) {
|
||||
@@ -1046,6 +1151,11 @@ final class HydrologyFootprintCompiler {
|
||||
return new MaterializedSurface(materializedSurface);
|
||||
}
|
||||
|
||||
HydrologyColumnSample surfaceColumnAt(int x, int z) {
|
||||
return materializedSurface == null ? surfaceRaster.fluidColumns.get(RiverFootprint.pack(x, z))
|
||||
: materializedSurface.sample(x, z).orElse(null);
|
||||
}
|
||||
|
||||
HydrologyColumnSample surfaceColumnAt(int x, int z, int naturalHeight) {
|
||||
if (materializedSurface != null) {
|
||||
HydrologyColumnSample sample = materializedSurface.sample(x, z).orElse(null);
|
||||
@@ -1111,12 +1221,13 @@ final class HydrologyFootprintCompiler {
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
HydrologyColumnSample sample = footprint.sample(x, z).orElse(null);
|
||||
return sample != null && sample.primarySurfaceLayer().isPresent();
|
||||
return sample != null && sample.primarySurfaceLayer().filter(HydrologyColumnLayer::terrainOwned).isPresent();
|
||||
}
|
||||
}
|
||||
|
||||
final class SurfaceRasterIndex implements HydrologyCaveVoxelViewFactory.PlannedSurface {
|
||||
private final Long2ObjectOpenHashMap<HydrologyColumnSample> surfaceColumns;
|
||||
private final Long2ObjectOpenHashMap<HydrologyColumnSample> fluidColumns;
|
||||
private final Long2IntOpenHashMap validationNaturalHeights;
|
||||
|
||||
private SurfaceRasterIndex(
|
||||
@@ -1124,7 +1235,13 @@ final class HydrologyFootprintCompiler {
|
||||
Long2ObjectOpenHashMap<HydrologyColumnSample> validationSamples
|
||||
) {
|
||||
Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> merged = new Long2ObjectLinkedOpenHashMap<>();
|
||||
for (SurfaceFootprint footprint : footprints) {
|
||||
ArrayList<SurfaceFootprint> allFootprints = new ArrayList<>(footprints);
|
||||
for (SurfaceFootprint regional : regionalSurfaceFootprints.values()) {
|
||||
if (!allFootprints.contains(regional)) {
|
||||
allFootprints.add(regional);
|
||||
}
|
||||
}
|
||||
for (SurfaceFootprint footprint : allFootprints) {
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
addLayer(merged, column.x(), column.z(), column.terrain(), column.layer());
|
||||
}
|
||||
@@ -1133,6 +1250,18 @@ final class HydrologyFootprintCompiler {
|
||||
for (Long2ObjectMap.Entry<FootprintMutableColumn> entry : merged.long2ObjectEntrySet()) {
|
||||
surfaceColumns.put(entry.getLongKey(), entry.getValue().build());
|
||||
}
|
||||
for (HydrologySurfaceDropRaster drops : regionalDropRasters.values()) {
|
||||
for (HydrologyColumnSample sample : drops.columns()) {
|
||||
addSurfaceFluid(merged, sample);
|
||||
}
|
||||
}
|
||||
for (HydrologyColumnSample sample : validationSamples.values()) {
|
||||
addSurfaceFluid(merged, sample);
|
||||
}
|
||||
this.fluidColumns = new Long2ObjectOpenHashMap<>(merged.size());
|
||||
for (Long2ObjectMap.Entry<FootprintMutableColumn> entry : merged.long2ObjectEntrySet()) {
|
||||
fluidColumns.put(entry.getLongKey(), entry.getValue().build());
|
||||
}
|
||||
this.validationNaturalHeights = new Long2IntOpenHashMap(validationSamples.size());
|
||||
for (HydrologyColumnSample sample : validationSamples.values()) {
|
||||
validationNaturalHeights.put(
|
||||
@@ -1142,6 +1271,14 @@ final class HydrologyFootprintCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
private void addSurfaceFluid(Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> columns, HydrologyColumnSample sample) {
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (layer.publishesSurfaceFluid()) {
|
||||
addLayer(columns, sample.x(), sample.z(), sampleTerrainBasis(sample.x(), sample.z()), layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int resolve(int x, int z, int naturalHeight) {
|
||||
long packed = RiverFootprint.pack(x, z);
|
||||
@@ -1158,7 +1295,7 @@ final class HydrologyFootprintCompiler {
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
HydrologyColumnSample sample = surfaceColumns.get(RiverFootprint.pack(x, z));
|
||||
return sample != null && sample.primarySurfaceLayer().isPresent();
|
||||
return sample != null && sample.primarySurfaceLayer().filter(HydrologyColumnLayer::terrainOwned).isPresent();
|
||||
}
|
||||
|
||||
private HydrologyColumnSample surfaceColumnAt(int x, int z, int naturalHeight) {
|
||||
|
||||
+26
-14
@@ -22,6 +22,8 @@ final class HydrologyFootprintRasterizer {
|
||||
boolean validationOnly,
|
||||
HydrologyFootprintCompiler.SurfaceRasterIndex plannedSurface
|
||||
) {
|
||||
Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> segmentColumns = segment.type().isSurface() && segment.fallingFluid()
|
||||
? new Long2ObjectLinkedOpenHashMap<>() : columns;
|
||||
List<HydrologyPoint> centerline = continuousCenterline(segment);
|
||||
if (segment.fallingFluid() && centerline.size() > 1) {
|
||||
HydrologyPoint throat = centerline.getFirst();
|
||||
@@ -36,7 +38,7 @@ final class HydrologyFootprintRasterizer {
|
||||
false
|
||||
);
|
||||
rasterizePoint(
|
||||
columns,
|
||||
segmentColumns,
|
||||
course,
|
||||
segment,
|
||||
throat,
|
||||
@@ -52,7 +54,7 @@ final class HydrologyFootprintRasterizer {
|
||||
plannedSurface
|
||||
);
|
||||
rasterizeSweptSegment(
|
||||
columns,
|
||||
segmentColumns,
|
||||
course,
|
||||
segment,
|
||||
List.copyOf(centerline.subList(1, centerline.size())),
|
||||
@@ -62,11 +64,14 @@ final class HydrologyFootprintRasterizer {
|
||||
validationOnly,
|
||||
plannedSurface
|
||||
);
|
||||
if (segmentColumns != columns) {
|
||||
compiler.mergeSurfaceDrop(columns, segmentColumns);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (sweptChannel(segment, centerline)) {
|
||||
rasterizeSweptSegment(
|
||||
columns,
|
||||
segmentColumns,
|
||||
course,
|
||||
segment,
|
||||
centerline,
|
||||
@@ -93,7 +98,7 @@ final class HydrologyFootprintRasterizer {
|
||||
int flowX = compiler.flowDelta(centerline, pointIndex, true);
|
||||
int flowZ = compiler.flowDelta(centerline, pointIndex, false);
|
||||
rasterizePoint(
|
||||
columns,
|
||||
segmentColumns,
|
||||
course,
|
||||
segment,
|
||||
point,
|
||||
@@ -109,6 +114,9 @@ final class HydrologyFootprintRasterizer {
|
||||
plannedSurface
|
||||
);
|
||||
}
|
||||
if (segmentColumns != columns) {
|
||||
compiler.mergeSurfaceDrop(columns, segmentColumns);
|
||||
}
|
||||
}
|
||||
|
||||
boolean sweptChannel(
|
||||
@@ -172,10 +180,10 @@ final class HydrologyFootprintRasterizer {
|
||||
HydrologyFeatureRef[][] pointFeatures = new HydrologyFeatureRef[centerline.size()][HydrologyFootprintCompiler.FEATURE_ROLE_COUNT];
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
if (!withinLongitudinalBounds(centerline, x, z, clipStart, clipEnd)) {
|
||||
if (!withinLongitudinalBounds(centerline, segment.start(), x, z, clipStart, clipEnd)) {
|
||||
continue;
|
||||
}
|
||||
FootprintCenterlineProjection projection = projectCenterline(centerline, x, z);
|
||||
FootprintCenterlineProjection projection = projectCenterline(centerline, segment.start(), x, z);
|
||||
int pointIndex = projection.pointIndex();
|
||||
HydrologyPoint point = centerline.get(pointIndex);
|
||||
boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1;
|
||||
@@ -232,7 +240,7 @@ final class HydrologyFootprintRasterizer {
|
||||
&& segment.type() != HydrologyFeatureType.WATERFALL
|
||||
&& segment.type() != HydrologyFeatureType.CASCADE;
|
||||
HydrologyTerrainSample terrain = exactSlope ? compiler.sampleTerrain(x, z) : compiler.sampleTerrainBasis(x, z);
|
||||
if (terrain == null) {
|
||||
if (terrain == null || compiler.elevatedSeaLevelSurfaceColumn(segment, terrain, shape.fluidHead())) {
|
||||
continue;
|
||||
}
|
||||
boolean naturalOcean = classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|
||||
@@ -246,7 +254,7 @@ final class HydrologyFootprintRasterizer {
|
||||
continue;
|
||||
}
|
||||
boolean source = firstSegment && pointIndex == 0;
|
||||
if (validationOnly && segment.type().isSurface()) {
|
||||
if (validationOnly && segment.type().isSurface() && !segment.fallingFluid()) {
|
||||
compiler.feature(course, segment, x, shape.fluidHead(), z, flowX, flowZ,
|
||||
source && deltaX == 0 && deltaZ == 0, channel, shore, grading,
|
||||
false, false, pointFeatures[pointIndex]);
|
||||
@@ -280,7 +288,7 @@ final class HydrologyFootprintRasterizer {
|
||||
}
|
||||
}
|
||||
|
||||
FootprintCenterlineProjection projectCenterline(List<HydrologyPoint> centerline, int x, int z) {
|
||||
FootprintCenterlineProjection projectCenterline(List<HydrologyPoint> centerline, HydrologyPoint incoming, int x, int z) {
|
||||
if (centerline.size() == 1) {
|
||||
HydrologyPoint point = centerline.getFirst();
|
||||
return new FootprintCenterlineProjection(
|
||||
@@ -288,8 +296,8 @@ final class HydrologyFootprintRasterizer {
|
||||
point.x(),
|
||||
point.z(),
|
||||
StrictMath.hypot(x - point.x(), z - point.z()),
|
||||
1,
|
||||
0
|
||||
point.x() - incoming.x(),
|
||||
point.z() - incoming.z()
|
||||
);
|
||||
}
|
||||
int selectedPoint = 0;
|
||||
@@ -345,13 +353,17 @@ final class HydrologyFootprintRasterizer {
|
||||
|
||||
boolean withinLongitudinalBounds(
|
||||
List<HydrologyPoint> centerline,
|
||||
HydrologyPoint incoming,
|
||||
int x,
|
||||
int z,
|
||||
boolean clipStart,
|
||||
boolean clipEnd
|
||||
) {
|
||||
if (centerline.size() < 2) {
|
||||
return true;
|
||||
HydrologyPoint point = centerline.getFirst();
|
||||
long dot = (long) (x - point.x()) * (point.x() - incoming.x())
|
||||
+ (long) (z - point.z()) * (point.z() - incoming.z());
|
||||
return (!clipStart || dot >= 0L) && (!clipEnd || dot <= 0L);
|
||||
}
|
||||
HydrologyPoint start = centerline.getFirst();
|
||||
HydrologyPoint afterStart = centerline.get(1);
|
||||
@@ -474,7 +486,7 @@ final class HydrologyFootprintRasterizer {
|
||||
HydrologyTerrainSample terrain = exactSlope
|
||||
? compiler.sampleTerrain(x, z)
|
||||
: compiler.sampleTerrainBasis(x, z);
|
||||
if (terrain == null) {
|
||||
if (terrain == null || compiler.elevatedSeaLevelSurfaceColumn(segment, terrain, shape.fluidHead())) {
|
||||
continue;
|
||||
}
|
||||
boolean naturalOcean = classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|
||||
@@ -514,7 +526,7 @@ final class HydrologyFootprintRasterizer {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (validationOnly && !underground) {
|
||||
if (validationOnly && !underground && !segment.fallingFluid()) {
|
||||
compiler.feature(
|
||||
course,
|
||||
segment,
|
||||
|
||||
+12
-7
@@ -10,7 +10,8 @@ final class HydrologyOceanBoundaryRefiner {
|
||||
static Result refine(
|
||||
List<HydrologyPoint> crossing,
|
||||
HydrologyTerrainSampler terrainSampler,
|
||||
HydrologyRoutingTerrainSampler routingSampler
|
||||
HydrologyRoutingTerrainSampler routingSampler,
|
||||
int seaLevel
|
||||
) {
|
||||
Objects.requireNonNull(crossing, "crossing");
|
||||
Objects.requireNonNull(terrainSampler, "terrainSampler");
|
||||
@@ -18,7 +19,6 @@ final class HydrologyOceanBoundaryRefiner {
|
||||
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 =
|
||||
@@ -27,13 +27,18 @@ final class HydrologyOceanBoundaryRefiner {
|
||||
return null;
|
||||
}
|
||||
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN) {
|
||||
HydrologyTerrainSample landwardTerrain = terrainSampler.sample(previous.x(), previous.z());
|
||||
if (landwardTerrain == null || landwardTerrain.ocean()) {
|
||||
return null;
|
||||
for (int landward = index - 1; landward >= 0; landward--) {
|
||||
HydrologyPoint previous = crossing.get(landward);
|
||||
HydrologyTerrainSample landwardTerrain = terrainSampler.sample(previous.x(), previous.z());
|
||||
if (landwardTerrain == null || landwardTerrain.ocean()) {
|
||||
return null;
|
||||
}
|
||||
if (landwardTerrain.naturalHeight() >= seaLevel) {
|
||||
return new Result(previous, point, landwardTerrain);
|
||||
}
|
||||
}
|
||||
return new Result(previous, point, landwardTerrain);
|
||||
return null;
|
||||
}
|
||||
previous = point;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class HydrologyOceanReceiver implements HydrologyTerrainSampler {
|
||||
private static final int MAXIMUM_SAMPLES = 65_536;
|
||||
private static final int[][] CARDINALS = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
|
||||
|
||||
private final HydrologyTerrainSampler source;
|
||||
private final LongOpenHashSet receiving;
|
||||
private final int seaLevel;
|
||||
|
||||
private HydrologyOceanReceiver(HydrologyTerrainSampler source, LongOpenHashSet receiving, int seaLevel) {
|
||||
this.source = source;
|
||||
this.receiving = receiving;
|
||||
this.seaLevel = seaLevel;
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSampler forOutlet(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler source, RiverOutlet outlet) {
|
||||
if (!outlet.directOcean() || outlet.type() != HydrologyFeatureType.MOUTH) {
|
||||
return source;
|
||||
}
|
||||
return forConnection(settings, source, outlet.landwardPoint(), outlet.connectionPoint());
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSampler forConnection(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler source,
|
||||
HydrologyPoint landward, HydrologyPoint receiving) {
|
||||
HydrologyTerrainSample shore = source.sample(landward.x(), landward.z());
|
||||
if (shore == null || shore.ocean() || shore.naturalHeight() < settings.seaLevel()) {
|
||||
return source;
|
||||
}
|
||||
double width = settings.surface().maximumWidth() * shore.widthMultiplier()
|
||||
* settings.surface().banks().mouthFlareRatio();
|
||||
return create(settings, source, List.of(new Mouth(
|
||||
List.of(landward, receiving), width)));
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSampler forCourse(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler source, RiverCourse course) {
|
||||
if (course.type() != RiverCourseType.SURFACE || course.segments().isEmpty()) {
|
||||
return source;
|
||||
}
|
||||
ArrayList<Mouth> mouths = new ArrayList<>(2);
|
||||
for (int index = 0; index < course.segments().size(); index++) {
|
||||
if (index != 0 && index != course.segments().size() - 1) {
|
||||
continue;
|
||||
}
|
||||
Mouth mouth = mouth(settings, source, course.segments().get(index));
|
||||
if (mouth != null) {
|
||||
mouths.add(mouth);
|
||||
}
|
||||
}
|
||||
return create(settings, source, mouths);
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSampler forMouth(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler source, HydraulicSegment segment) {
|
||||
Mouth mouth = mouth(settings, source, segment);
|
||||
return mouth == null ? source : create(settings, source, List.of(mouth));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sample(int blockX, int blockZ) {
|
||||
return source.sample(blockX, blockZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean receivingWater(int blockX, int blockZ, int queriedSeaLevel) {
|
||||
return queriedSeaLevel == seaLevel && receiving.contains(RiverFootprint.pack(blockX, blockZ))
|
||||
|| source.receivingWater(blockX, blockZ, queriedSeaLevel);
|
||||
}
|
||||
|
||||
private static Mouth mouth(HydrologyPlannerSettings settings, HydrologyTerrainSampler source,
|
||||
HydraulicSegment segment) {
|
||||
if (segment.type() != HydrologyFeatureType.MOUTH || segment.upstreamHeadY() != settings.seaLevel()
|
||||
|| segment.downstreamHeadY() != settings.seaLevel()) {
|
||||
return null;
|
||||
}
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>(segment.centerline());
|
||||
HydrologyPoint first = points.getFirst();
|
||||
HydrologyTerrainSample terrain = source.sample(first.x(), first.z());
|
||||
if (terrain != null && terrain.ocean() && terrain.naturalHeight() < settings.seaLevel()) {
|
||||
Collections.reverse(points);
|
||||
}
|
||||
double width = segment.width();
|
||||
for (int station = 0; station < segment.centerline().size(); station++) {
|
||||
width = Math.max(width, segment.channelProfile().widthAt(station));
|
||||
}
|
||||
return new Mouth(List.copyOf(points), width);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler create(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler source, List<Mouth> mouths) {
|
||||
if (mouths.isEmpty()) {
|
||||
return source;
|
||||
}
|
||||
Samples samples = new Samples(source);
|
||||
LongOpenHashSet receiving = new LongOpenHashSet();
|
||||
for (Mouth mouth : mouths) {
|
||||
LongArrayList run = submergedRun(mouth.points(), samples, settings.seaLevel());
|
||||
if (run.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
receiving.addAll(run);
|
||||
double radius = mouth.width() * settings.surface().banks().channel().outlineMaximumRatio() / 2D
|
||||
+ settings.outlets().maximumOceanApron() + 2D;
|
||||
extendHalo(receiving, run, samples, settings.seaLevel(), radius);
|
||||
}
|
||||
return receiving.isEmpty() ? source : new HydrologyOceanReceiver(source, receiving, settings.seaLevel());
|
||||
}
|
||||
|
||||
private static LongArrayList submergedRun(List<HydrologyPoint> points, Samples samples, int seaLevel) {
|
||||
LongArrayList run = new LongArrayList();
|
||||
if (points.size() < 2) {
|
||||
return run;
|
||||
}
|
||||
HydrologyPoint endpoint = points.getLast();
|
||||
HydrologyTerrainSample ocean = samples.sample(endpoint.x(), endpoint.z());
|
||||
if (ocean == null || !ocean.ocean() || ocean.naturalHeight() >= seaLevel) {
|
||||
return run;
|
||||
}
|
||||
long previous = RiverFootprint.pack(points.getFirst().x(), points.getFirst().z());
|
||||
for (int index = 0; index + 1 < points.size(); index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint second = points.get(index + 1);
|
||||
long steps = Math.max(Math.abs((long) second.x() - first.x()), Math.abs((long) second.z() - first.z()));
|
||||
if (steps > MAXIMUM_SAMPLES) {
|
||||
return new LongArrayList();
|
||||
}
|
||||
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = steps == 0L ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(first.x() + ((long) second.x() - first.x()) * progress);
|
||||
int z = (int) StrictMath.round(first.z() + ((long) second.z() - first.z()) * progress);
|
||||
HydrologyTerrainSample terrain = samples.sample(x, z);
|
||||
if (terrain == null) {
|
||||
return new LongArrayList();
|
||||
}
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
if (terrain.naturalHeight() >= seaLevel) {
|
||||
if (!run.isEmpty()) {
|
||||
return new LongArrayList();
|
||||
}
|
||||
} else {
|
||||
if (!run.isEmpty() && !cardinalBridge(run, previous, key, samples, seaLevel)) {
|
||||
return new LongArrayList();
|
||||
}
|
||||
if (run.isEmpty() || run.getLong(run.size() - 1) != key) {
|
||||
run.add(key);
|
||||
}
|
||||
}
|
||||
previous = key;
|
||||
}
|
||||
}
|
||||
return run;
|
||||
}
|
||||
|
||||
private static boolean cardinalBridge(LongArrayList run, long previous, long next,
|
||||
Samples samples, int seaLevel) {
|
||||
int previousX = RiverFootprint.unpackX(previous);
|
||||
int previousZ = RiverFootprint.unpackZ(previous);
|
||||
int nextX = RiverFootprint.unpackX(next);
|
||||
int nextZ = RiverFootprint.unpackZ(next);
|
||||
if (previousX == nextX || previousZ == nextZ) {
|
||||
return true;
|
||||
}
|
||||
long first = RiverFootprint.pack(previousX, nextZ);
|
||||
long second = RiverFootprint.pack(nextX, previousZ);
|
||||
for (long bridge : new long[] {Math.min(first, second), Math.max(first, second)}) {
|
||||
HydrologyTerrainSample terrain = samples.sample(RiverFootprint.unpackX(bridge), RiverFootprint.unpackZ(bridge));
|
||||
if (terrain != null && terrain.naturalHeight() < seaLevel) {
|
||||
run.add(bridge);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void extendHalo(LongOpenHashSet receiving, LongArrayList run, Samples samples,
|
||||
int seaLevel, double radius) {
|
||||
long first = run.getLong(0);
|
||||
int anchorX = RiverFootprint.unpackX(first);
|
||||
int anchorZ = RiverFootprint.unpackZ(first);
|
||||
double squaredRadius = radius * radius;
|
||||
LongOpenHashSet visited = new LongOpenHashSet();
|
||||
LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
|
||||
for (int index = 0; index < run.size(); index++) {
|
||||
long key = run.getLong(index);
|
||||
double dx = (double) RiverFootprint.unpackX(key) - anchorX;
|
||||
double dz = (double) RiverFootprint.unpackZ(key) - anchorZ;
|
||||
if (dx * dx + dz * dz <= squaredRadius && visited.add(key)) {
|
||||
queue.enqueue(key);
|
||||
}
|
||||
}
|
||||
while (!queue.isEmpty() && samples.available()) {
|
||||
long key = queue.dequeueLong();
|
||||
int x = RiverFootprint.unpackX(key);
|
||||
int z = RiverFootprint.unpackZ(key);
|
||||
for (int[] offset : CARDINALS) {
|
||||
long nextX = (long) x + offset[0];
|
||||
long nextZ = (long) z + offset[1];
|
||||
double dx = nextX - anchorX;
|
||||
double dz = nextZ - anchorZ;
|
||||
if (nextX < Integer.MIN_VALUE || nextX > Integer.MAX_VALUE
|
||||
|| nextZ < Integer.MIN_VALUE || nextZ > Integer.MAX_VALUE
|
||||
|| dx * dx + dz * dz > squaredRadius) {
|
||||
continue;
|
||||
}
|
||||
long next = RiverFootprint.pack((int) nextX, (int) nextZ);
|
||||
if (!visited.add(next)) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample terrain = samples.sample((int) nextX, (int) nextZ);
|
||||
if (terrain != null && terrain.naturalHeight() < seaLevel) {
|
||||
receiving.add(next);
|
||||
queue.enqueue(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Mouth(List<HydrologyPoint> points, double width) {
|
||||
}
|
||||
|
||||
private static final class Samples {
|
||||
private final HydrologyTerrainSampler source;
|
||||
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> samples = new Long2ObjectOpenHashMap<>();
|
||||
|
||||
private Samples(HydrologyTerrainSampler source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z) {
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
if (samples.containsKey(key)) {
|
||||
return samples.get(key);
|
||||
}
|
||||
if (!available()) {
|
||||
return null;
|
||||
}
|
||||
HydrologyTerrainSample terrain = source.sample(x, z);
|
||||
samples.put(key, terrain);
|
||||
return terrain;
|
||||
}
|
||||
|
||||
private boolean available() {
|
||||
return samples.size() < MAXIMUM_SAMPLES;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,13 +142,14 @@ final class HydrologyOutletPlanner {
|
||||
continue;
|
||||
}
|
||||
HydrologyOceanBoundaryRefiner.Result boundary = refineOceanBoundary(land, ocean);
|
||||
if (boundary == null
|
||||
if (boundary == null) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample receiving = planner.sampleBasisWithoutSlope(boundary.oceanPoint().x(), boundary.oceanPoint().z());
|
||||
if (receiving == null || !boundary.landwardTerrain().drainsInto(receiving)
|
||||
|| !boundary.landwardTerrain().transitAllowed()
|
||||
|| !boundary.landwardTerrain().outletAllowed()
|
||||
|| surface && !HydrologySurfaceProfiles.sharesProfile(
|
||||
boundary.landwardTerrain(),
|
||||
planner.sampleBasisWithoutSlope(boundary.oceanPoint().x(), boundary.oceanPoint().z())
|
||||
)) {
|
||||
|| surface && !HydrologySurfaceProfiles.sharesProfile(boundary.landwardTerrain(), receiving)) {
|
||||
continue;
|
||||
}
|
||||
HydrologyFeatureType type = coastalOutletType(boundary.landwardTerrain());
|
||||
@@ -478,7 +479,7 @@ final class HydrologyOutletPlanner {
|
||||
|
||||
HydrologyOceanBoundaryRefiner.Result refineOceanBoundary(HydrologyGridNode land, HydrologyGridNode ocean) {
|
||||
List<HydrologyPoint> crossing = planner.segments.line(land.naturalPoint(), ocean.naturalPoint(), 1);
|
||||
return HydrologyOceanBoundaryRefiner.refine(crossing, planner.sampler, planner.routingSampler);
|
||||
return HydrologyOceanBoundaryRefiner.refine(crossing, planner.sampler, planner.routingSampler, planner.settings.seaLevel());
|
||||
}
|
||||
|
||||
HydrologyFeatureType coastalOutletType(HydrologyTerrainSample land) {
|
||||
|
||||
@@ -61,6 +61,7 @@ public final class HydrologyPlanner {
|
||||
final HydrologyUndergroundCoursePlanner undergroundCourses;
|
||||
final HydrologySegmentBuilder segments;
|
||||
final HydrologyFeatureSitePlanner featureSites;
|
||||
final HydrologyRegionalPlanner regional;
|
||||
|
||||
public HydrologyPlanner(long worldSeed, HydrologyPlannerSettings settings, HydrologyTerrainSampler sampler) {
|
||||
this(
|
||||
@@ -193,6 +194,7 @@ public final class HydrologyPlanner {
|
||||
this.undergroundCourses = new HydrologyUndergroundCoursePlanner(this);
|
||||
this.segments = new HydrologySegmentBuilder(this);
|
||||
this.featureSites = new HydrologyFeatureSitePlanner(this);
|
||||
this.regional = new HydrologyRegionalPlanner(this);
|
||||
}
|
||||
|
||||
public long worldSeed() {
|
||||
@@ -246,6 +248,7 @@ public final class HydrologyPlanner {
|
||||
|
||||
void clearOwnerDrafts() {
|
||||
resolvedOwners.invalidateAll();
|
||||
regional.clear();
|
||||
}
|
||||
|
||||
void reuseResolvedTile(HydrologyTile tile) {
|
||||
@@ -262,6 +265,7 @@ public final class HydrologyPlanner {
|
||||
tile.courses(),
|
||||
tile.cavePlans()
|
||||
);
|
||||
result = regional.withoutRegional(result, tile.key());
|
||||
HydrologyOwnerDraft draft = new HydrologyOwnerDraft(
|
||||
tile.key(),
|
||||
result,
|
||||
|
||||
@@ -20,7 +20,7 @@ public record HydrologyPlannerSettings(
|
||||
SeaCaves seaCaves,
|
||||
SurfacePolicyBounds surfacePolicyBounds
|
||||
) {
|
||||
private static final long PLAN_FORMAT_REVISION = 6L;
|
||||
private static final long PLAN_FORMAT_REVISION = 8L;
|
||||
private static final int MAXIMUM_CROSS_TILE_COLOR_PERIOD = 4;
|
||||
|
||||
public HydrologyPlannerSettings {
|
||||
@@ -52,7 +52,7 @@ public record HydrologyPlannerSettings(
|
||||
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 Routing(2048, 64, 8192, 8192, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
|
||||
new Surface(true, surfaceSources, 4, 8, 2, 4, 10, 1.5D, Banks.defaults()),
|
||||
new Hydraulics(8),
|
||||
Underground.of(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1),
|
||||
@@ -216,9 +216,11 @@ public record HydrologyPlannerSettings(
|
||||
double slopePenalty,
|
||||
double confluenceAttraction,
|
||||
double lengthPreference,
|
||||
int tributaries
|
||||
int tributaries,
|
||||
Regional regional
|
||||
) {
|
||||
public Routing {
|
||||
Objects.requireNonNull(regional, "regional");
|
||||
if (tileSize < 32 || sampleSpacing < 4 || tileSize % sampleSpacing != 0) {
|
||||
throw new IllegalArgumentException("Routing sizes must form an exact bounded tile lattice.");
|
||||
}
|
||||
@@ -230,6 +232,9 @@ public record HydrologyPlannerSettings(
|
||||
if (maximumRouteNodes < latticeNodes || maximumRouteNodes > 1_000_000 || maximumRouteLength < 1) {
|
||||
throw new IllegalArgumentException("maximumRouteNodes must contain the complete lattice and remain bounded.");
|
||||
}
|
||||
if (regional.enabled() && maximumRouteLength > 32_768) {
|
||||
throw new IllegalArgumentException("Regional route length cannot exceed 32768 blocks.");
|
||||
}
|
||||
if (minimumSurfaceCourseLength < 0 || minimumSurfaceCourseLength > 32_768
|
||||
|| minimumUndergroundCourseLength < 0 || minimumUndergroundCourseLength > 32_768) {
|
||||
throw new IllegalArgumentException("Hydrology course lengths are invalid.");
|
||||
@@ -265,6 +270,38 @@ public record HydrologyPlannerSettings(
|
||||
}
|
||||
}
|
||||
|
||||
public record Regional(
|
||||
boolean enabled,
|
||||
int sampleSpacing,
|
||||
int minimumLength,
|
||||
int maximumTrunks,
|
||||
int maximumCachedBasins,
|
||||
int maximumCachedStations,
|
||||
boolean coastalChannels,
|
||||
double coastalChannelChance,
|
||||
int maximumCoastalIncision
|
||||
) {
|
||||
public Regional {
|
||||
if (sampleSpacing < 128 || sampleSpacing > 1024 || Integer.bitCount(sampleSpacing) != 1
|
||||
|| minimumLength < 256 || minimumLength > 32768
|
||||
|| maximumTrunks < 1 || maximumTrunks > 8
|
||||
|| maximumCachedBasins < 1 || maximumCachedBasins > 64
|
||||
|| maximumCachedStations < 16384 || maximumCachedStations > 1048576
|
||||
|| !Double.isFinite(coastalChannelChance) || coastalChannelChance < 0D || coastalChannelChance > 1D
|
||||
|| maximumCoastalIncision < 1 || maximumCoastalIncision > 32) {
|
||||
throw new IllegalArgumentException("Regional drainage settings exceed their bounded ranges.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Regional disabled() {
|
||||
return new Regional(false, 256, 2048, 2, 8, 131072, false, 0.25D, 8);
|
||||
}
|
||||
|
||||
public int basinSize(Routing routing) {
|
||||
return Math.max(sampleSpacing * 4, Integer.highestOneBit(routing.maximumRouteLength()));
|
||||
}
|
||||
}
|
||||
|
||||
public record Source(
|
||||
boolean enabled,
|
||||
double density,
|
||||
@@ -451,9 +488,11 @@ public record HydrologyPlannerSettings(
|
||||
double cliffFraction,
|
||||
IrisRiverBedProfile bedProfile,
|
||||
double shoreRise,
|
||||
double blendBaseWidth
|
||||
double blendBaseWidth,
|
||||
Excavation excavation
|
||||
) {
|
||||
public Erosion {
|
||||
Objects.requireNonNull(excavation, "excavation");
|
||||
if (smoothingRadius < 0
|
||||
|| !Double.isFinite(thalwegFraction) || thalwegFraction < 0D || thalwegFraction >= 1D
|
||||
|| !Double.isFinite(blendCurve) || blendCurve <= 0D
|
||||
@@ -470,20 +509,33 @@ public record HydrologyPlannerSettings(
|
||||
}
|
||||
|
||||
public static Erosion defaults() {
|
||||
return new Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D);
|
||||
return new Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D, Excavation.defaults());
|
||||
}
|
||||
|
||||
/** The erosion amounts with the smooth valley side, bowl bed and flat shore bench they always had. */
|
||||
public static Erosion of(boolean enabled, int smoothingRadius, double thalwegFraction, double blendCurve, double bedNoise) {
|
||||
return new Erosion(enabled, smoothingRadius, thalwegFraction, blendCurve, bedNoise,
|
||||
IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D);
|
||||
IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D, Excavation.defaults());
|
||||
}
|
||||
|
||||
// Enum hash codes are identity based and change between runs; the plan fingerprint mixes this hash in.
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(enabled, smoothingRadius, thalwegFraction, blendCurve, bedNoise, style.name(),
|
||||
terraceSteps, cliffFraction, bedProfile.name(), shoreRise, blendBaseWidth);
|
||||
terraceSteps, cliffFraction, bedProfile.name(), shoreRise, blendBaseWidth, excavation);
|
||||
}
|
||||
}
|
||||
|
||||
public record Excavation(int maximumDepth, int maximumWidth, int maximumVolumePerBlock) {
|
||||
public Excavation {
|
||||
if (maximumDepth < 0 || maximumDepth > 64 || maximumWidth < 1 || maximumWidth > 64
|
||||
|| maximumVolumePerBlock < 0 || maximumVolumePerBlock > 8192) {
|
||||
throw new IllegalArgumentException("Surface bank excavation limits are invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Excavation defaults() {
|
||||
return new Excavation(8, 16, 256);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceCenterline;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class HydrologyRegionalConnectivity {
|
||||
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
private final Long2LongOpenHashMap water = new Long2LongOpenHashMap();
|
||||
private final Long2ObjectOpenHashMap<LongArrayList> disjoint = new Long2ObjectOpenHashMap<>();
|
||||
|
||||
void add(List<SurfaceLayerColumn> columns) {
|
||||
for (SurfaceLayerColumn column : columns) {
|
||||
HydrologyColumnLayer layer = column.layer();
|
||||
if (layer.fluidOwned() && layer.bedY() < layer.fluidHeadY()) {
|
||||
addFluidColumn(column.x(), column.z(), layer.bedY(), layer.fluidHeadY());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void add(HydrologyColumnSample column) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (layer.fluidOwned() && layer.bedY() < layer.fluidHeadY()) {
|
||||
addFluidColumn(column.x(), column.z(), layer.bedY(), layer.fluidHeadY());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addFluidColumn(int x, int z, int bed, int head) {
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
long interval = levels(bed, head);
|
||||
if (!water.containsKey(key)) {
|
||||
water.put(key, interval);
|
||||
return;
|
||||
}
|
||||
long previous = water.get(key);
|
||||
LongArrayList gaps = disjoint.get(key);
|
||||
if (gaps == null && bed <= (int) previous && (int) (previous >> 32) <= head) {
|
||||
water.put(key, levels(Math.min(bed, (int) (previous >> 32)), Math.max(head, (int) previous)));
|
||||
return;
|
||||
}
|
||||
if (gaps == null) {
|
||||
gaps = new LongArrayList(3);
|
||||
gaps.add(previous);
|
||||
disjoint.put(key, gaps);
|
||||
}
|
||||
gaps.add(interval);
|
||||
gaps.sort(null);
|
||||
int retained = 1;
|
||||
for (int index = 1; index < gaps.size(); index++) {
|
||||
long first = gaps.getLong(retained - 1);
|
||||
long next = gaps.getLong(index);
|
||||
if ((int) (next >> 32) <= (int) first) {
|
||||
gaps.set(retained - 1, levels((int) (first >> 32), Math.max((int) first, (int) next)));
|
||||
} else {
|
||||
gaps.set(retained++, next);
|
||||
}
|
||||
}
|
||||
gaps.size(retained);
|
||||
water.put(key, gaps.getLong(0));
|
||||
if (retained == 1) {
|
||||
disjoint.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
boolean connected(RiverCourse course, HydrologyTerrainSampler sampler, HydrologyPlannerSettings settings) {
|
||||
if (water.isEmpty() || !disjoint.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
LongOpenHashSet visited = new LongOpenHashSet(water.size());
|
||||
LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
|
||||
long start = water.keySet().iterator().nextLong();
|
||||
queue.enqueue(start);
|
||||
visited.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
long current = queue.dequeueLong();
|
||||
long currentLevels = water.get(current);
|
||||
int currentBed = (int) (currentLevels >> 32);
|
||||
int currentHead = (int) currentLevels;
|
||||
int x = RiverFootprint.unpackX(current);
|
||||
int z = RiverFootprint.unpackZ(current);
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
long next = RiverFootprint.pack(x + offset[0], z + offset[1]);
|
||||
if (!water.containsKey(next) || visited.contains(next)) {
|
||||
continue;
|
||||
}
|
||||
long levels = water.get(next);
|
||||
if (Math.max(currentBed, (int) (levels >> 32)) >= Math.min(currentHead, (int) levels)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(next);
|
||||
queue.enqueue(next);
|
||||
}
|
||||
}
|
||||
if (visited.size() != water.size()) {
|
||||
return false;
|
||||
}
|
||||
HydraulicSegment first = course.segments().getFirst();
|
||||
HydraulicSegment last = course.segments().getLast();
|
||||
return reaches(last, false, sampler, settings, visited)
|
||||
&& (first.type() != HydrologyFeatureType.MOUTH || reaches(first, true, sampler, settings, visited));
|
||||
}
|
||||
|
||||
private boolean reaches(HydraulicSegment mouth, boolean upstream, HydrologyTerrainSampler sampler,
|
||||
HydrologyPlannerSettings settings, LongOpenHashSet visited) {
|
||||
int seaLevel = settings.seaLevel();
|
||||
if (mouth.type() != HydrologyFeatureType.MOUTH || mouth.upstreamHeadY() != seaLevel
|
||||
|| mouth.downstreamHeadY() != seaLevel) {
|
||||
return false;
|
||||
}
|
||||
HydrologyPoint endpoint = upstream ? mouth.start() : mouth.end();
|
||||
HydrologyTerrainSample terrain = sampler.sample(endpoint.x(), endpoint.z());
|
||||
if (terrain == null || !terrain.ocean() || terrain.naturalHeight() >= seaLevel) {
|
||||
return false;
|
||||
}
|
||||
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forMouth(settings, sampler, mouth);
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(mouth.centerline());
|
||||
int step = upstream ? 1 : -1;
|
||||
int previousX = endpoint.x();
|
||||
int previousZ = endpoint.z();
|
||||
for (int station = upstream ? 0 : centerline.size() - 1; station >= 0 && station < centerline.size(); station += step) {
|
||||
int x = centerline.x()[station];
|
||||
int z = centerline.z()[station];
|
||||
if (x != previousX && z != previousZ) {
|
||||
boolean firstBridge = receiver.receivingWater(previousX, z, seaLevel);
|
||||
boolean secondBridge = receiver.receivingWater(x, previousZ, seaLevel);
|
||||
if (!firstBridge && !secondBridge) {
|
||||
return false;
|
||||
}
|
||||
if (firstBridge && touchesOwnedWater(previousX, z, seaLevel, visited)
|
||||
|| secondBridge && touchesOwnedWater(x, previousZ, seaLevel, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!receiver.receivingWater(x, z, seaLevel)) {
|
||||
return false;
|
||||
}
|
||||
if (touchesOwnedWater(x, z, seaLevel, visited)) {
|
||||
return true;
|
||||
}
|
||||
previousX = x;
|
||||
previousZ = z;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean touchesOwnedWater(int x, int z, int seaLevel, LongOpenHashSet visited) {
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
long key = RiverFootprint.pack(x + offset[0], z + offset[1]);
|
||||
if (visited.contains(key) && (int) water.get(key) == seaLevel) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static long levels(int bed, int head) {
|
||||
return ((long) bed << 32) | (head & 0xffffffffL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
record HydrologyRegionalCourseGraph(List<DrainageNode> nodes, List<DrainageEdge> edges, int discharge) {
|
||||
private static final long NODE_SALT = 0x5245475055424c49L;
|
||||
|
||||
HydrologyRegionalCourseGraph {
|
||||
nodes = List.copyOf(nodes);
|
||||
edges = List.copyOf(edges);
|
||||
}
|
||||
|
||||
static HydrologyRegionalCourseGraph create(HydrologyTerrainSampler sampler, Options options) {
|
||||
List<HydrologyPoint> path = options.path();
|
||||
double[] distance = new double[path.size()];
|
||||
IntArrayList selected = new IntArrayList();
|
||||
selected.add(0);
|
||||
double next = options.spacing();
|
||||
for (int index = 1; index < path.size(); index++) {
|
||||
HydrologyPoint previous = path.get(index - 1);
|
||||
HydrologyPoint point = path.get(index);
|
||||
distance[index] = distance[index - 1] + StrictMath.hypot(
|
||||
(double) point.x() - previous.x(), (double) point.z() - previous.z());
|
||||
if (distance[index] >= next || index == path.size() - 1) {
|
||||
selected.add(index);
|
||||
next = distance[index] + options.spacing();
|
||||
}
|
||||
}
|
||||
ArrayList<DrainageNode> nodes = new ArrayList<>(selected.size());
|
||||
ArrayList<DrainageEdge> edges = new ArrayList<>(Math.max(0, selected.size() - 1));
|
||||
double length = distance[distance.length - 1];
|
||||
int contribution = 1;
|
||||
for (int index = 0; index < selected.size(); index++) {
|
||||
int station = selected.getInt(index);
|
||||
HydrologyPoint point = path.get(station);
|
||||
HydrologyTerrainSample terrain = Objects.requireNonNull(sampler.sample(point.x(), point.z()),
|
||||
"Accepted regional drainage terrain");
|
||||
DrainageNode node = new DrainageNode(HydrologyHash.mix(options.courseId(), NODE_SALT, index, point.x(), point.z()),
|
||||
point.x(), point.z(), terrain, length - distance[station], options.outletId());
|
||||
nodes.add(node);
|
||||
contribution = Math.max(contribution, options.flow().contributionAt(point.x(), point.z()));
|
||||
if (index > 0) {
|
||||
DrainageNode upstream = nodes.get(index - 1);
|
||||
edges.add(new DrainageEdge(HydrologyHash.mix(options.courseId(), upstream.id(), node.id()),
|
||||
upstream.id(), node.id(), options.outletId(), upstream.potential() - node.potential(),
|
||||
contribution, 0, List.of(upstream.naturalPoint(), node.naturalPoint())));
|
||||
}
|
||||
}
|
||||
return new HydrologyRegionalCourseGraph(nodes, edges, contribution);
|
||||
}
|
||||
|
||||
record Options(long courseId, long outletId, int spacing, List<HydrologyPoint> path, HydrologyRegionalFlow flow) {
|
||||
Options {
|
||||
path = List.copyOf(path);
|
||||
Objects.requireNonNull(flow, "Regional drainage flow");
|
||||
if (spacing < 1 || path.size() < 2) {
|
||||
throw new IllegalArgumentException("Regional drainage requires positive spacing and a complete path.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelPrecondition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class HydrologyRegionalFalls implements HydrologyCaveVoxelViewFactory.PlannedSurface {
|
||||
private static final int WINDOW_SIZE = 128;
|
||||
private static final int MAXIMUM_ROOF_WINDOWS = 4;
|
||||
private static final int[][] NEIGHBORS = {{0, 0}, {1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final RiverCourse course;
|
||||
private final HydrologySurfaceDropRaster drops;
|
||||
private final HydrologyTerrainSampler receiver;
|
||||
private final LongOpenHashSet witnessColumns;
|
||||
private final ArrayList<SurfaceLayerColumn> exposed;
|
||||
private final SurfaceFootprintCompiler surface;
|
||||
private final LinkedHashMap<HydrologyTileKey, Map<Long, HydrologyColumnSample>> roofWindows;
|
||||
|
||||
HydrologyRegionalFalls(HydrologyPlanner planner, RiverCourse course) {
|
||||
this.planner = planner;
|
||||
this.course = course;
|
||||
this.receiver = HydrologyOceanReceiver.forCourse(planner.settings, this::sample, course);
|
||||
this.drops = HydrologySurfaceDropRaster.compile(planner.settings, receiver, planner.geometrySampler, course);
|
||||
this.witnessColumns = new LongOpenHashSet();
|
||||
this.exposed = new ArrayList<>();
|
||||
this.surface = new SurfaceFootprintCompiler(planner.settings, this::sample, planner.geometrySampler);
|
||||
this.roofWindows = new LinkedHashMap<>(MAXIMUM_ROOF_WINDOWS + 1, 1F, true);
|
||||
for (HydrologyColumnSample column : drops.columns()) {
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
witnessColumns.add(RiverFootprint.pack(column.x() + offset[0], column.z() + offset[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void retain(SurfaceFootprint footprint) {
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (witnessColumns.contains(RiverFootprint.pack(column.x(), column.z()))) {
|
||||
exposed.add(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<HydrologyCavePlan> validate(HydrologyRegionalNetwork network, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
if (drops.columns().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(planner.settings, receiver, planner.geometrySampler);
|
||||
HydrologyFootprintCompiler.ValidationRaster validation = compiler.compileSurfaceDropValidation(course,
|
||||
new SurfaceFootprint(exposed, 0, null, 0, 0L), drops);
|
||||
HydrologyObservedPlannedSurface planned = new HydrologyObservedPlannedSurface(this);
|
||||
CaveVoxelView view = Objects.requireNonNull(planner.caveViewFactory.create(planned));
|
||||
ArrayList<HydrologyDiagnosticCandidate> failures = new ArrayList<>();
|
||||
HydrologyCaveCourseFilter.Result result = new HydrologyCaveCourseFilter(view,
|
||||
new HydrologyCaveCourseFilter.Options(planner.settings.underground().connectToExistingCaves(),
|
||||
planner.settings.outlets().coastalGrotto().maximumVolume(),
|
||||
planner.settings.outlets().inlandGrotto().maximumVolume()), null, null, planned)
|
||||
.filter(network.nodes(), network.edges(), network.outlets(), List.of(course), validation, failures);
|
||||
for (HydrologyDiagnosticCandidate failure : failures) {
|
||||
if (diagnostics.size() >= 64) {
|
||||
break;
|
||||
}
|
||||
diagnostics.add(new HydrologyDiagnosticCandidate(failure.id(), HydrologyCandidateKind.REGIONAL_SOURCE,
|
||||
failure.projectedType(), failure.point(), failure.rejection(), failure.detail()));
|
||||
}
|
||||
return result.courses().isEmpty() ? null : result.cavePlans();
|
||||
}
|
||||
|
||||
static HydrologyCavePlan clip(HydrologyCavePlan plan, SurfaceBounds bounds) {
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
plan.forEachActionIn(bounds.minimumX(), bounds.minimumZ(), Math.addExact(bounds.maximumX(), 1),
|
||||
Math.addExact(bounds.maximumZ(), 1), actions::put);
|
||||
plan.allPreconditionsIn(bounds.minimumX(), bounds.minimumZ(), Math.addExact(bounds.maximumX(), 1),
|
||||
Math.addExact(bounds.maximumZ(), 1), (position, precondition) -> {
|
||||
preconditions.put(position, precondition);
|
||||
return true;
|
||||
});
|
||||
return new HydrologyCavePlan(plan.source(), plan.rejection(), actions, preconditions, plan.arbitrationWinnerSourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int resolve(int x, int z, int naturalHeight) {
|
||||
HydrologyColumnSample column = column(x, z);
|
||||
return column == null ? naturalHeight : column.terrainHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
HydrologyColumnSample column = column(x, z);
|
||||
HydrologyColumnLayer layer = column == null ? null : column.primarySurfaceLayerOrNull();
|
||||
return layer != null && layer.terrainOwned();
|
||||
}
|
||||
|
||||
private HydrologyColumnSample column(int x, int z) {
|
||||
HydrologyTileKey key = HydrologyTileKey.fromBlock(x, z, WINDOW_SIZE);
|
||||
Map<Long, HydrologyColumnSample> columns = roofWindows.get(key);
|
||||
if (columns == null) {
|
||||
SurfaceBounds bounds = new SurfaceBounds(key.minimumBlockX(WINDOW_SIZE), key.minimumBlockZ(WINDOW_SIZE),
|
||||
Math.addExact(key.minimumBlockX(WINDOW_SIZE), WINDOW_SIZE - 1),
|
||||
Math.addExact(key.minimumBlockZ(WINDOW_SIZE), WINDOW_SIZE - 1));
|
||||
LinkedHashMap<Long, HydrologyColumnSample> built = new LinkedHashMap<>();
|
||||
for (SurfaceLayerColumn column : surface.compile(course, bounds).columns()) {
|
||||
HydrologyTerrainSample terrain = column.terrain();
|
||||
HydrologyColumnSample value = new HydrologyColumnSample(column.x(), column.z(), terrain.naturalHeight(),
|
||||
planner.settings.seaLevel(), terrain.ocean(), terrain.parentBiomeKey(), List.of(column.layer()));
|
||||
built.merge(RiverFootprint.pack(column.x(), column.z()), value, HydrologyRegionalPlanner::mergeColumn);
|
||||
}
|
||||
columns = Map.copyOf(built);
|
||||
roofWindows.put(key, columns);
|
||||
if (roofWindows.size() > MAXIMUM_ROOF_WINDOWS) {
|
||||
roofWindows.remove(roofWindows.sequencedKeySet().getFirst());
|
||||
}
|
||||
}
|
||||
return columns.get(RiverFootprint.pack(x, z));
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z) {
|
||||
return planner.naturalSampler == null ? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class HydrologyRegionalFlow {
|
||||
private final List<HydrologyPoint> points;
|
||||
private final int[] contributions;
|
||||
private final int maximum;
|
||||
|
||||
HydrologyRegionalFlow(List<HydrologyPoint> points, int[] accumulated) {
|
||||
if (points.size() != accumulated.length) {
|
||||
throw new IllegalArgumentException("Regional flow requires one contribution per guide point.");
|
||||
}
|
||||
this.points = List.copyOf(points);
|
||||
this.contributions = new int[accumulated.length];
|
||||
int previous = 1;
|
||||
for (int index = 0; index < accumulated.length; index++) {
|
||||
previous = Math.max(previous, accumulated[index]);
|
||||
contributions[index] = previous;
|
||||
}
|
||||
this.maximum = previous;
|
||||
}
|
||||
|
||||
double fraction(int x, int z) {
|
||||
return StrictMath.sqrt(contribution(x, z) / maximum);
|
||||
}
|
||||
|
||||
int contributionAt(int x, int z) {
|
||||
return Math.max(1, (int) StrictMath.round(contribution(x, z)));
|
||||
}
|
||||
|
||||
private double contribution(int x, int z) {
|
||||
double nearestDistance = Double.POSITIVE_INFINITY;
|
||||
double contribution = contributions[0];
|
||||
for (int index = 0; index + 1 < points.size(); index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint next = points.get(index + 1);
|
||||
double deltaX = next.x() - first.x();
|
||||
double deltaZ = next.z() - first.z();
|
||||
double length = deltaX * deltaX + deltaZ * deltaZ;
|
||||
double progress = length <= 0D ? 0D : Math.max(0D, Math.min(1D,
|
||||
((x - first.x()) * deltaX + (z - first.z()) * deltaZ) / length));
|
||||
double distanceX = x - first.x() - deltaX * progress;
|
||||
double distanceZ = z - first.z() - deltaZ * progress;
|
||||
double distance = distanceX * distanceX + distanceZ * distanceZ;
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
contribution = contributions[index] + (contributions[index + 1] - contributions[index]) * progress;
|
||||
}
|
||||
}
|
||||
return contribution;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.PriorityQueue;
|
||||
|
||||
final class HydrologyRegionalGraph {
|
||||
private static final int LABELS_PER_GRID_NODE = 64;
|
||||
private static final int MAXIMUM_LABELS = 1_048_576;
|
||||
private static final Comparator<Label> LABEL_ORDER = Comparator.comparingInt((Label label) -> label.state.requiredHead())
|
||||
.thenComparingDouble(label -> label.state.cost()).thenComparingDouble(label -> label.state.length())
|
||||
.thenComparingInt(label -> label.state.index()).thenComparingInt(label -> label.state.root())
|
||||
.thenComparingInt(label -> label.state.id());
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final HydrologyRegionalHydraulics hydraulics;
|
||||
|
||||
HydrologyRegionalGraph(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
this.hydraulics = new HydrologyRegionalHydraulics(planner.settings);
|
||||
}
|
||||
|
||||
Tree route(HydrologySampledGrid grid, List<OutletCandidate> outlets, boolean coastal) {
|
||||
int count = grid.nodes().size();
|
||||
int maximumLabels = (int) Math.min(MAXIMUM_LABELS, Math.max(count, (long) count * LABELS_PER_GRID_NODE));
|
||||
ArrayList<List<Label>> frontiers = new ArrayList<>(count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
frontiers.add(new ArrayList<>());
|
||||
}
|
||||
ArrayList<Label> labels = new ArrayList<>(count);
|
||||
PriorityQueue<Label> pending = new PriorityQueue<>(LABEL_ORDER);
|
||||
for (int outletIndex = 0; outletIndex < outlets.size(); outletIndex++) {
|
||||
int index = outlets.get(outletIndex).landIndex();
|
||||
Label label = new Label(new LabelState(labels.size(), index, planner.settings.seaLevel(),
|
||||
0D, 0D, outletIndex), null);
|
||||
admit(label, frontiers.get(index), labels, pending);
|
||||
}
|
||||
HydrologyPlannerSettings.Inlet inlet = planner.settings.surface().banks().inlet();
|
||||
double possibleInletLength = (inlet.length() + inlet.length() / 2D) * StrictMath.sqrt(2D);
|
||||
while (!pending.isEmpty() && labels.size() < maximumLabels) {
|
||||
Label current = pending.remove();
|
||||
if (!current.active) {
|
||||
continue;
|
||||
}
|
||||
HydrologyGridNode downstream = grid.node(current.state.index());
|
||||
if (current.downstream == null) {
|
||||
HydrologyPoint landward = outlets.get(current.state.root()).outlet().landwardPoint();
|
||||
HydrologyTerrainSample terrain = planner.sampler.sample(landward.x(), landward.z());
|
||||
downstream = new HydrologyGridNode(downstream.index(), downstream.gridX(), downstream.gridZ(),
|
||||
landward.x(), landward.z(), downstream.id(), terrain);
|
||||
}
|
||||
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
|
||||
if (labels.size() >= maximumLabels) {
|
||||
break;
|
||||
}
|
||||
HydrologyGridNode upstream = grid.nodeAt(downstream.gridX() + offset.x(), downstream.gridZ() + offset.z());
|
||||
if (upstream == null || !allowed(upstream.terrain(), downstream.terrain(), coastal)) {
|
||||
continue;
|
||||
}
|
||||
double distance = StrictMath.hypot(upstream.x() - downstream.x(), upstream.z() - downstream.z());
|
||||
double nextLength = current.state.length() + distance;
|
||||
if (nextLength > planner.settings.routing().maximumRouteLength()) {
|
||||
continue;
|
||||
}
|
||||
int nodeMinimum = possibleInletLength > 0D && nextLength < possibleInletLength
|
||||
? hydraulics.inletMinimumHead(upstream.terrain()) : hydraulics.minimumHead(upstream.terrain());
|
||||
int needed = coastal ? planner.settings.seaLevel() : Math.max(current.state.requiredHead(), nodeMinimum);
|
||||
if (!coastal && needed > hydraulics.maximumHead(upstream.terrain())) {
|
||||
continue;
|
||||
}
|
||||
double candidate = current.state.cost() + edgeCost(upstream, downstream, distance, coastal);
|
||||
Label label = new Label(new LabelState(labels.size(), upstream.index(), needed, candidate,
|
||||
nextLength, current.state.root()), current);
|
||||
admit(label, frontiers.get(upstream.index()), labels, pending);
|
||||
}
|
||||
}
|
||||
Label[] selected = new Label[count];
|
||||
int[] accumulation = new int[labels.size()];
|
||||
for (int index = 0; index < count; index++) {
|
||||
for (Label label : frontiers.get(index)) {
|
||||
if (selected[index] == null || LABEL_ORDER.compare(label, selected[index]) < 0) {
|
||||
selected[index] = label;
|
||||
}
|
||||
}
|
||||
if (selected[index] != null && grid.node(index).terrain().surfaceSourceAllowed()) {
|
||||
accumulation[selected[index].state.id()]++;
|
||||
}
|
||||
}
|
||||
for (int index = labels.size() - 1; index >= 0; index--) {
|
||||
Label label = labels.get(index);
|
||||
if (label.downstream != null) {
|
||||
accumulation[label.downstream.state.id()] += accumulation[label.state.id()];
|
||||
}
|
||||
}
|
||||
return new Tree(selected, accumulation, List.copyOf(outlets));
|
||||
}
|
||||
|
||||
List<Integer> path(Tree tree, int source) {
|
||||
ArrayList<Integer> indices = new ArrayList<>();
|
||||
Label current = tree.selected()[source];
|
||||
while (current != null) {
|
||||
indices.add(current.state.index());
|
||||
current = current.downstream;
|
||||
}
|
||||
return List.copyOf(indices);
|
||||
}
|
||||
|
||||
private static void admit(Label candidate, List<Label> frontier, List<Label> labels, PriorityQueue<Label> pending) {
|
||||
for (Label existing : frontier) {
|
||||
if (dominates(existing, candidate)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (int index = frontier.size() - 1; index >= 0; index--) {
|
||||
Label existing = frontier.get(index);
|
||||
if (dominates(candidate, existing)) {
|
||||
existing.active = false;
|
||||
frontier.remove(index);
|
||||
}
|
||||
}
|
||||
frontier.add(candidate);
|
||||
labels.add(candidate);
|
||||
pending.add(candidate);
|
||||
}
|
||||
|
||||
private static boolean dominates(Label first, Label second) {
|
||||
if ((first.downstream == null || second.downstream == null)
|
||||
&& (first.downstream != null || second.downstream != null || first.state.root() != second.state.root())) {
|
||||
return false;
|
||||
}
|
||||
return first.state.requiredHead() <= second.state.requiredHead()
|
||||
&& first.state.cost() <= second.state.cost() && first.state.length() <= second.state.length();
|
||||
}
|
||||
|
||||
private boolean allowed(HydrologyTerrainSample upstream, HydrologyTerrainSample downstream, boolean coastal) {
|
||||
if (upstream.ocean() || upstream.naturalHeight() < planner.settings.seaLevel() || !upstream.transitAllowed() || !upstream.drainsInto(downstream)
|
||||
|| !HydrologySurfaceProfiles.sharesProfile(upstream, downstream)) {
|
||||
return false;
|
||||
}
|
||||
if (coastal) {
|
||||
int incision = upstream.naturalHeight() - planner.settings.seaLevel() + planner.settings.surface().minimumDepth();
|
||||
return incision <= planner.settings.routing().regional().maximumCoastalIncision()
|
||||
&& downstream.drainsInto(upstream);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private double edgeCost(HydrologyGridNode upstream, HydrologyGridNode downstream, double distance, boolean coastal) {
|
||||
HydrologyPlannerSettings.Routing routing = planner.settings.routing();
|
||||
HydrologyTerrainSample first = upstream.terrain();
|
||||
HydrologyTerrainSample second = downstream.terrain();
|
||||
double rise = Math.max(0D, second.naturalHeight() - first.naturalHeight());
|
||||
double slope = (first.slope() + second.slope()) * 0.5D;
|
||||
double incision = Math.max(0D, first.naturalHeight() - planner.settings.seaLevel());
|
||||
double resistance = coastal ? incision * incision * routing.uphillPenalty() : rise * routing.uphillPenalty();
|
||||
double policy = Math.max(0.01D, (first.routingMultiplier() + second.routingMultiplier()) * 0.5D);
|
||||
return Math.max(0.001D, (distance + resistance + slope * routing.slopePenalty()
|
||||
+ first.routingCost() + second.routingCost()) * policy);
|
||||
}
|
||||
|
||||
record Tree(Label[] selected, int[] accumulation, List<OutletCandidate> outlets) {
|
||||
int root(int source) {
|
||||
return selected[source] == null ? -1 : selected[source].state.root();
|
||||
}
|
||||
|
||||
double cost(int source) {
|
||||
return selected[source] == null ? Double.POSITIVE_INFINITY : selected[source].state.cost();
|
||||
}
|
||||
|
||||
double length(int source) {
|
||||
return selected[source] == null ? 0D : selected[source].state.length();
|
||||
}
|
||||
|
||||
int[] contributions(int source) {
|
||||
int count = 0;
|
||||
for (Label label = selected[source]; label != null; label = label.downstream) {
|
||||
count++;
|
||||
}
|
||||
int[] contributions = new int[count];
|
||||
int index = 0;
|
||||
for (Label label = selected[source]; label != null; label = label.downstream) {
|
||||
contributions[index++] = accumulation[label.state.id()];
|
||||
}
|
||||
return contributions;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Label {
|
||||
private final LabelState state;
|
||||
private final Label downstream;
|
||||
private boolean active = true;
|
||||
|
||||
Label(LabelState state, Label downstream) {
|
||||
this.state = state;
|
||||
this.downstream = downstream;
|
||||
}
|
||||
}
|
||||
|
||||
record LabelState(int id, int index, int requiredHead, double cost, double length, int root) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceBankSupport;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
final class HydrologyRegionalHydraulics {
|
||||
private static final int MAXIMUM_WIDTH_CANDIDATES = 8;
|
||||
private static final int MAXIMUM_ADDITIONAL_SAMPLES = 2048;
|
||||
private static final int MAXIMUM_BANK_RADIUS = 96;
|
||||
|
||||
private final HydrologyPlannerSettings settings;
|
||||
private final SurfaceBankSupport bankSupport;
|
||||
private final double minimumHalfWidth;
|
||||
private final double maximumProbedHalfWidth;
|
||||
private final double[] candidateHalfWidths;
|
||||
|
||||
HydrologyRegionalHydraulics(HydrologyPlannerSettings settings) {
|
||||
this.settings = settings;
|
||||
this.bankSupport = new SurfaceBankSupport(settings.surface(), settings.seaLevel());
|
||||
HydrologyPlannerSettings.Surface surface = settings.surface();
|
||||
this.minimumHalfWidth = surface.minimumWidth() / 2D;
|
||||
double authored = surface.maximumWidth() / 2D;
|
||||
double expanded = surface.maximumWidth();
|
||||
this.maximumProbedHalfWidth = Math.min(expanded, Math.max(minimumHalfWidth,
|
||||
(MAXIMUM_BANK_RADIUS - 2D) / (1D + surface.banks().roughness())));
|
||||
this.candidateHalfWidths = candidates(authored, midpoint(minimumHalfWidth, authored),
|
||||
maximumProbedHalfWidth, midpoint(minimumHalfWidth, maximumProbedHalfWidth));
|
||||
}
|
||||
|
||||
int minimumHead(HydrologyTerrainSample terrain) {
|
||||
return minimumHead(terrain, terrain.surfacePolicy().maximumIncision(settings.surface().maximumIncision()));
|
||||
}
|
||||
|
||||
int inletMinimumHead(HydrologyTerrainSample terrain) {
|
||||
return minimumHead(terrain, Math.max(terrain.surfacePolicy().maximumIncision(settings.surface().maximumIncision()),
|
||||
settings.surface().banks().inlet().maximumIncision()));
|
||||
}
|
||||
|
||||
int maximumHead(HydrologyTerrainSample terrain) {
|
||||
return Math.max(settings.seaLevel(), terrain.naturalHeight() - settings.surface().banks().sink());
|
||||
}
|
||||
|
||||
int supportedHead(HeadStation station, HydrologyTerrainSampler sampler) {
|
||||
int centerHead = maximumHead(station.terrain());
|
||||
if (centerHead >= station.incomingHead()) {
|
||||
return station.incomingHead();
|
||||
}
|
||||
int supported = support(station, sampler, minimumHalfWidth, centerHead);
|
||||
if (supported >= station.incomingHead()) {
|
||||
return station.incomingHead();
|
||||
}
|
||||
SampleBudget budget = new SampleBudget(sampler);
|
||||
double localMinimum = effectiveHalfWidth(settings.surface().minimumWidth(), station.terrain().widthMultiplier());
|
||||
double localMaximum = effectiveHalfWidth(settings.surface().maximumWidth(), station.terrain().widthMultiplier());
|
||||
if (additional(localMaximum)) {
|
||||
supported = support(station, budget, localMaximum, supported);
|
||||
}
|
||||
if (supported < station.incomingHead() && localMinimum != localMaximum && additional(localMinimum)) {
|
||||
supported = support(station, budget, localMinimum, supported);
|
||||
}
|
||||
double localMiddle = midpoint(localMinimum, localMaximum);
|
||||
if (supported < station.incomingHead() && localMiddle != localMinimum && localMiddle != localMaximum && additional(localMiddle)) {
|
||||
supported = support(station, budget, localMiddle, supported);
|
||||
}
|
||||
for (double halfWidth : candidateHalfWidths) {
|
||||
if (supported >= station.incomingHead() || budget.remaining == 0) {
|
||||
break;
|
||||
}
|
||||
supported = support(station, budget, halfWidth, supported);
|
||||
}
|
||||
return Math.min(station.incomingHead(), supported);
|
||||
}
|
||||
|
||||
private int support(HeadStation station, HydrologyTerrainSampler sampler, double halfWidth, int supportedHead) {
|
||||
SurfaceBankSupport.Station crossSection = new SurfaceBankSupport.Station(station.x(), station.z(),
|
||||
station.tangentX(), station.tangentZ(), halfWidth);
|
||||
SurfaceBankSupport.CrossSection support = bankSupport.crossSection(sampler, crossSection, true);
|
||||
if (support.blocked() || support.minimum() - settings.surface().banks().sink() <= supportedHead) {
|
||||
return supportedHead;
|
||||
}
|
||||
SurfaceBankSupport.Perimeter perimeter = bankSupport.perimeter(sampler, crossSection, support.minimum());
|
||||
if (!perimeter.complete()) {
|
||||
return supportedHead;
|
||||
}
|
||||
int supported = perimeter.minimum() - settings.surface().banks().sink();
|
||||
return Math.max(supportedHead, supported);
|
||||
}
|
||||
|
||||
private double effectiveHalfWidth(int width, double multiplier) {
|
||||
return Math.max(settings.surface().minimumWidth(), Math.min(settings.surface().maximumWidth() * 2D, width * multiplier)) / 2D;
|
||||
}
|
||||
|
||||
private boolean additional(double halfWidth) {
|
||||
if (halfWidth <= minimumHalfWidth || halfWidth > maximumProbedHalfWidth) {
|
||||
return false;
|
||||
}
|
||||
for (double candidate : candidateHalfWidths) {
|
||||
if (candidate == halfWidth) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private double[] candidates(double... widths) {
|
||||
double[] distinct = new double[MAXIMUM_WIDTH_CANDIDATES - 4];
|
||||
int size = 0;
|
||||
for (double width : widths) {
|
||||
if (!Double.isFinite(width) || width <= minimumHalfWidth || width > maximumProbedHalfWidth) {
|
||||
continue;
|
||||
}
|
||||
boolean duplicate = false;
|
||||
for (int index = 0; index < size; index++) {
|
||||
duplicate |= distinct[index] == width;
|
||||
}
|
||||
if (!duplicate && size < distinct.length) {
|
||||
distinct[size++] = width;
|
||||
}
|
||||
}
|
||||
return Arrays.copyOf(distinct, size);
|
||||
}
|
||||
|
||||
private static double midpoint(double first, double second) {
|
||||
return first + (second - first) / 2D;
|
||||
}
|
||||
|
||||
private int minimumHead(HydrologyTerrainSample terrain, int incision) {
|
||||
int permitted = Math.min(incision, (int) StrictMath.floor(incision * terrain.incisionMultiplier()));
|
||||
int depth = (int) StrictMath.round(Math.max(1D, Math.min(settings.surface().maximumDepth() * 2D,
|
||||
settings.surface().minimumDepth() * terrain.depthMultiplier())));
|
||||
return Math.max(settings.seaLevel(), terrain.naturalHeight() - permitted + depth);
|
||||
}
|
||||
|
||||
record HeadStation(int x, int z, double tangentX, double tangentZ, HydrologyTerrainSample terrain, int incomingHead) {
|
||||
}
|
||||
|
||||
private static final class SampleBudget implements HydrologyTerrainSampler {
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
private int remaining = MAXIMUM_ADDITIONAL_SAMPLES;
|
||||
|
||||
private SampleBudget(HydrologyTerrainSampler sampler) {
|
||||
this.sampler = sampler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sample(int blockX, int blockZ) {
|
||||
if (remaining == 0) {
|
||||
return null;
|
||||
}
|
||||
remaining--;
|
||||
return sampler.sample(blockX, blockZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class HydrologyRegionalMorphology {
|
||||
private static final int MAXIMUM_STATIONS = 129;
|
||||
private static final double MAXIMUM_LATERAL_REACH = 64D;
|
||||
private static final double GENTLE_GRADE = 1D / 64D;
|
||||
private static final double STEEP_GRADE = 0.25D;
|
||||
private static final double[] LATERAL_OFFSETS = {-1D, -0.5D, 0.5D, 1D};
|
||||
|
||||
private final Location[] locations;
|
||||
private final double[] freedom;
|
||||
|
||||
private HydrologyRegionalMorphology(Location[] locations, double[] freedom) {
|
||||
this.locations = locations;
|
||||
this.freedom = freedom;
|
||||
}
|
||||
|
||||
static HydrologyRegionalMorphology sample(List<HydrologyPoint> guide, HydrologyTerrainSampler sampler,
|
||||
HydrologyPlannerSettings settings) {
|
||||
double length = 0D;
|
||||
for (int index = 1; index < guide.size(); index++) {
|
||||
length += StrictMath.hypot((double) guide.get(index).x() - guide.get(index - 1).x(),
|
||||
(double) guide.get(index).z() - guide.get(index - 1).z());
|
||||
}
|
||||
if (length <= 0D || length > settings.routing().maximumRouteLength()) {
|
||||
return new HydrologyRegionalMorphology(new Location[0], new double[0]);
|
||||
}
|
||||
int intervals = Math.min(MAXIMUM_STATIONS - 1,
|
||||
Math.max(1, (int) StrictMath.ceil(length / settings.routing().regional().sampleSpacing())));
|
||||
Location[] locations = locations(guide, length, intervals);
|
||||
HydrologyTerrainSample[] centers = new HydrologyTerrainSample[locations.length];
|
||||
Map<Long, HydrologyTerrainSample> sampled = new HashMap<>();
|
||||
for (int index = 0; index < locations.length; index++) {
|
||||
Location location = locations[index];
|
||||
centers[index] = sample(sampler, sampled, location.x(), location.z());
|
||||
}
|
||||
double reach = Math.min(MAXIMUM_LATERAL_REACH, Math.max(settings.surface().maximumWidth() * 2D,
|
||||
settings.routing().regional().sampleSpacing() / 8D));
|
||||
double[] freedom = new double[locations.length];
|
||||
for (int index = 0; index < locations.length; index++) {
|
||||
HydrologyTerrainSample center = centers[index];
|
||||
if (!land(center, settings.seaLevel())) {
|
||||
continue;
|
||||
}
|
||||
int before = Math.max(0, index - 1);
|
||||
int after = Math.min(locations.length - 1, index + 1);
|
||||
if (centers[before] == null || centers[after] == null) {
|
||||
continue;
|
||||
}
|
||||
double distance = length / intervals;
|
||||
double grade = Math.max(StrictMath.abs((double) center.naturalHeight() - centers[before].naturalHeight()),
|
||||
StrictMath.abs((double) centers[after].naturalHeight() - center.naturalHeight())) / distance;
|
||||
double gradeFreedom = 1D - smooth((grade - GENTLE_GRADE) / (STEEP_GRADE - GENTLE_GRADE));
|
||||
freedom[index] = gradeFreedom * room(locations[index], center, sampler, sampled, settings, reach);
|
||||
}
|
||||
return new HydrologyRegionalMorphology(locations, smooth(freedom));
|
||||
}
|
||||
|
||||
double freedomAt(double x, double z) {
|
||||
double nearest = Double.POSITIVE_INFINITY;
|
||||
double selected = 0D;
|
||||
for (int index = 0; index + 1 < locations.length; index++) {
|
||||
Location first = locations[index];
|
||||
Location second = locations[index + 1];
|
||||
double deltaX = (double) second.x() - first.x();
|
||||
double deltaZ = (double) second.z() - first.z();
|
||||
double squared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
double progress = squared == 0D ? 0D : Math.max(0D, Math.min(1D,
|
||||
((x - first.x()) * deltaX + (z - first.z()) * deltaZ) / squared));
|
||||
double offsetX = x - first.x() - deltaX * progress;
|
||||
double offsetZ = z - first.z() - deltaZ * progress;
|
||||
double distance = offsetX * offsetX + offsetZ * offsetZ;
|
||||
if (distance < nearest) {
|
||||
nearest = distance;
|
||||
selected = freedom[index] + (freedom[index + 1] - freedom[index]) * smooth(progress);
|
||||
}
|
||||
}
|
||||
return Math.max(0D, Math.min(1D, selected));
|
||||
}
|
||||
|
||||
private static Location[] locations(List<HydrologyPoint> guide, double length, int intervals) {
|
||||
Location[] locations = new Location[intervals + 1];
|
||||
int segment = 0;
|
||||
double traversed = 0D;
|
||||
for (int index = 0; index <= intervals; index++) {
|
||||
double distance = length * index / intervals;
|
||||
HydrologyPoint first = guide.get(segment);
|
||||
HydrologyPoint second = guide.get(segment + 1);
|
||||
double span = StrictMath.hypot((double) second.x() - first.x(), (double) second.z() - first.z());
|
||||
while (segment + 2 < guide.size() && (span == 0D || traversed + span < distance)) {
|
||||
traversed += span;
|
||||
segment++;
|
||||
first = guide.get(segment);
|
||||
second = guide.get(segment + 1);
|
||||
span = StrictMath.hypot((double) second.x() - first.x(), (double) second.z() - first.z());
|
||||
}
|
||||
double progress = span == 0D ? 0D : Math.max(0D, Math.min(1D, (distance - traversed) / span));
|
||||
locations[index] = new Location(
|
||||
(int) StrictMath.round(first.x() + ((double) second.x() - first.x()) * progress),
|
||||
(int) StrictMath.round(first.z() + ((double) second.z() - first.z()) * progress),
|
||||
span == 0D ? 1D : (second.x() - first.x()) / span,
|
||||
span == 0D ? 0D : (second.z() - first.z()) / span);
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
|
||||
private static double room(Location location, HydrologyTerrainSample center, HydrologyTerrainSampler sampler,
|
||||
Map<Long, HydrologyTerrainSample> sampled, HydrologyPlannerSettings settings, double reach) {
|
||||
int incision = center.surfacePolicy().maximumIncision(settings.surface().maximumIncision());
|
||||
double allowance = Math.max(1D, Math.min(incision, StrictMath.floor(incision * center.incisionMultiplier())));
|
||||
double room = 1D;
|
||||
for (double scale : LATERAL_OFFSETS) {
|
||||
double offset = reach * scale;
|
||||
int x = (int) StrictMath.round(location.x() - location.tangentZ() * offset);
|
||||
int z = (int) StrictMath.round(location.z() + location.tangentX() * offset);
|
||||
HydrologyTerrainSample side = sample(sampler, sampled, x, z);
|
||||
if (!land(side, settings.seaLevel()) || !center.drainsInto(side) || !side.drainsInto(center)
|
||||
|| !HydrologySurfaceProfiles.sharesProfile(center, side)) {
|
||||
return 0D;
|
||||
}
|
||||
double relief = StrictMath.abs((double) side.naturalHeight() - center.naturalHeight());
|
||||
room = Math.min(room, 1D - smooth((relief - allowance / 4D) / allowance));
|
||||
}
|
||||
return room;
|
||||
}
|
||||
|
||||
private static boolean land(HydrologyTerrainSample sample, int seaLevel) {
|
||||
return sample != null && !sample.ocean() && sample.naturalHeight() >= seaLevel && sample.transitAllowed();
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample sample(HydrologyTerrainSampler sampler, Map<Long, HydrologyTerrainSample> sampled,
|
||||
int x, int z) {
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
if (!sampled.containsKey(key)) {
|
||||
sampled.put(key, sampler.sample(x, z));
|
||||
}
|
||||
return sampled.get(key);
|
||||
}
|
||||
|
||||
private static double[] smooth(double[] values) {
|
||||
double[] smoothed = new double[values.length];
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
double total = 0D;
|
||||
int weight = 0;
|
||||
for (int sample = Math.max(0, index - 2); sample <= Math.min(values.length - 1, index + 2); sample++) {
|
||||
int strength = 3 - Math.abs(sample - index);
|
||||
total += values[sample] * strength;
|
||||
weight += strength;
|
||||
}
|
||||
smoothed[index] = total / weight;
|
||||
}
|
||||
return smoothed;
|
||||
}
|
||||
|
||||
private static double smooth(double value) {
|
||||
double bounded = Math.max(0D, Math.min(1D, value));
|
||||
return bounded * bounded * (3D - 2D * bounded);
|
||||
}
|
||||
|
||||
private record Location(int x, int z, double tangentX, double tangentZ) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record HydrologyRegionalNetwork(
|
||||
List<DrainageNode> nodes,
|
||||
List<DrainageEdge> edges,
|
||||
List<RiverOutlet> outlets,
|
||||
List<RiverCourse> courses,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||
List<HydrologyCavePlan> cavePlans
|
||||
) {
|
||||
static final HydrologyRegionalNetwork EMPTY = new HydrologyRegionalNetwork(List.of(), List.of(), List.of(), List.of(), List.of(), List.of());
|
||||
|
||||
HydrologyRegionalNetwork {
|
||||
nodes = List.copyOf(nodes);
|
||||
edges = List.copyOf(edges);
|
||||
outlets = List.copyOf(outlets);
|
||||
courses = List.copyOf(courses);
|
||||
diagnostics = List.copyOf(diagnostics);
|
||||
cavePlans = List.copyOf(cavePlans);
|
||||
}
|
||||
|
||||
int stations() {
|
||||
int count = 0;
|
||||
for (RiverCourse course : courses) {
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
count = Math.addExact(count, segment.centerline().size());
|
||||
}
|
||||
}
|
||||
for (HydrologyCavePlan plan : cavePlans) {
|
||||
count = Math.addExact(count, plan.baselinePreconditions().size());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceCenterline;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseBuilder;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceExcavationMetrics;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
|
||||
final class HydrologyRegionalPlanner {
|
||||
private static final long BASIN_SALT = 0x524547424153494eL;
|
||||
private static final long COURSE_SALT = 0x524547434f555253L;
|
||||
private static final long NODE_SALT = 0x5245474e4f444553L;
|
||||
private static final int MAXIMUM_SOURCE_TRIALS = 24;
|
||||
private static final int MAXIMUM_DIAGNOSTICS = 64;
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final HydrologyPlannerSettings.Regional settings;
|
||||
private final HydrologyRegionalGraph graph;
|
||||
private final HydrologyRegionalRoute routes;
|
||||
private final Cache<HydrologyTileKey, HydrologyRegionalNetwork> drafts;
|
||||
|
||||
HydrologyRegionalPlanner(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
this.settings = planner.settings.routing().regional();
|
||||
this.graph = new HydrologyRegionalGraph(planner);
|
||||
this.routes = new HydrologyRegionalRoute(planner);
|
||||
int minimumWeight = Math.max(1, Math.ceilDiv(settings.maximumCachedStations(), settings.maximumCachedBasins()));
|
||||
this.drafts = Caffeine.newBuilder()
|
||||
.maximumWeight(settings.maximumCachedStations())
|
||||
.weigher((HydrologyTileKey key, HydrologyRegionalNetwork network) -> Math.max(minimumWeight, network.stations()))
|
||||
.build();
|
||||
}
|
||||
|
||||
boolean enabled() {
|
||||
return settings.enabled() && planner.settings.surface().enabled()
|
||||
&& planner.settings.surface().sources().enabled() && planner.settings.outlets().oceanEnabled()
|
||||
&& settings.minimumLength() <= planner.settings.routing().maximumRouteLength();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
drafts.invalidateAll();
|
||||
routes.clear();
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z) {
|
||||
return planner.naturalSampler == null ? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z);
|
||||
}
|
||||
|
||||
HydrologyRegionalNetwork coursesIn(SurfaceBounds bounds) {
|
||||
if (!enabled()) {
|
||||
return HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
int basinSize = settings.basinSize(planner.settings.routing());
|
||||
int reach = settings.sampleSpacing() * 2 + footprintRadius();
|
||||
int minimumX = Math.floorDiv(bounds.minimumX() - reach, basinSize);
|
||||
int maximumX = Math.floorDiv(bounds.maximumX() + reach, basinSize);
|
||||
int minimumZ = Math.floorDiv(bounds.minimumZ() - reach, basinSize);
|
||||
int maximumZ = Math.floorDiv(bounds.maximumZ() + reach, basinSize);
|
||||
HydrologyTileKey primary = HydrologyTileKey.fromBlock(
|
||||
(int) (((long) bounds.minimumX() + bounds.maximumX()) / 2L),
|
||||
(int) (((long) bounds.minimumZ() + bounds.maximumZ()) / 2L), basinSize);
|
||||
ArrayList<HydrologyRegionalNetwork> networks = new ArrayList<>();
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
HydrologyTileKey key = new HydrologyTileKey(x, z);
|
||||
HydrologyRegionalNetwork draft = draft(key);
|
||||
ArrayList<RiverCourse> accepted = new ArrayList<>();
|
||||
for (RiverCourse course : draft.courses()) {
|
||||
if (intersects(course, bounds, footprintRadius()) && admitted(key, course)) {
|
||||
accepted.add(course);
|
||||
}
|
||||
}
|
||||
if (!accepted.isEmpty() || key.equals(primary)) {
|
||||
HydrologyRegionalNetwork selected = select(draft, accepted);
|
||||
networks.add(new HydrologyRegionalNetwork(selected.nodes(), selected.edges(), selected.outlets(),
|
||||
selected.courses(), key.equals(primary) ? draft.diagnostics() : List.of(), selected.cavePlans()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return merge(networks);
|
||||
}
|
||||
|
||||
RiverFootprint materialize(HydrologyRegionalNetwork network, SurfaceBounds bounds) {
|
||||
if (network.courses().isEmpty()) {
|
||||
return RiverFootprint.empty();
|
||||
}
|
||||
SurfaceFootprintCompiler compiler = new SurfaceFootprintCompiler(planner.settings,
|
||||
this::sample, planner.geometrySampler);
|
||||
LinkedHashMap<Long, HydrologyColumnSample> columns = new LinkedHashMap<>();
|
||||
for (RiverCourse course : network.courses()) {
|
||||
SurfaceFootprint footprint = compiler.compile(course, bounds);
|
||||
if (!footprint.accepted()) {
|
||||
throw new IllegalStateException("An accepted regional course changed its bounded surface admission: " + course.id());
|
||||
}
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
HydrologyTerrainSample terrain = column.terrain();
|
||||
HydrologyColumnSample sample = new HydrologyColumnSample(column.x(), column.z(), terrain.naturalHeight(),
|
||||
planner.settings.seaLevel(), terrain.ocean(), terrain.parentBiomeKey(), List.of(column.layer()));
|
||||
columns.merge(RiverFootprint.pack(column.x(), column.z()), sample, HydrologyRegionalPlanner::mergeColumn);
|
||||
}
|
||||
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(planner.settings,
|
||||
HydrologyOceanReceiver.forCourse(planner.settings, this::sample, course), planner.geometrySampler, course, bounds);
|
||||
for (HydrologyColumnSample column : drops.columns()) {
|
||||
columns.merge(RiverFootprint.pack(column.x(), column.z()), column, HydrologyRegionalPlanner::mergeColumn);
|
||||
}
|
||||
}
|
||||
return new RiverFootprint(columns);
|
||||
}
|
||||
|
||||
HydrologyRegionalNetwork draft(HydrologyTileKey key) {
|
||||
return drafts.get(key, this::compile);
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork compile(HydrologyTileKey key) {
|
||||
HydrologySampledGrid grid = sample(key);
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
HydrologyGridNode center = grid.nodes().get(grid.nodes().size() / 2);
|
||||
ArrayList<OutletCandidate> outlets = planner.outletPlanner.oceanOutletCandidates(grid, true);
|
||||
outlets.removeIf(outlet -> outlet.outlet().type() != HydrologyFeatureType.MOUTH);
|
||||
outlets.sort(Comparator.comparingLong(outlet -> HydrologyHash.mix(planner.worldSeed, BASIN_SALT,
|
||||
key.tileX(), key.tileZ(), outlet.outlet().id())));
|
||||
if (outlets.isEmpty()) {
|
||||
diagnostic(diagnostics, center.id(), center.naturalPoint(), false, HydrologyCandidateRejection.NO_LEGAL_OUTLET, 0);
|
||||
return new HydrologyRegionalNetwork(List.of(), List.of(), List.of(), List.of(), diagnostics, List.of());
|
||||
}
|
||||
ArrayList<HydrologyRegionalNetwork> accepted = new ArrayList<>();
|
||||
long basinSeed = HydrologyHash.mix(planner.worldSeed, BASIN_SALT, key.tileX(), key.tileZ());
|
||||
if (settings.coastalChannels() && HydrologyHash.unit(basinSeed) < settings.coastalChannelChance()) {
|
||||
ArrayList<HydrologyDiagnosticCandidate> coastalDiagnostics = new ArrayList<>();
|
||||
HydrologyRegionalNetwork coastal = coastalChannel(grid, outlets, basinSeed, coastalDiagnostics);
|
||||
diagnostics.addAll(coastalDiagnostics.subList(0, Math.min(16, coastalDiagnostics.size())));
|
||||
if (!coastal.courses().isEmpty()) {
|
||||
accepted.add(coastal);
|
||||
}
|
||||
}
|
||||
ArrayList<OutletCandidate> roots = spacedOutlets(outlets, settings.maximumTrunks() * 2);
|
||||
HydrologyRegionalGraph.Tree tree = graph.route(grid, roots, false);
|
||||
ArrayList<Integer> sources = new ArrayList<>();
|
||||
for (HydrologyGridNode node : grid.nodes()) {
|
||||
if (grid.owns(node.x(), node.z()) && tree.root(node.index()) >= 0
|
||||
&& tree.length(node.index()) >= settings.minimumLength()
|
||||
&& planner.sourcePlanner.rawSourceEligible(node.terrain(), planner.settings.surface().sources(), true)) {
|
||||
sources.add(node.index());
|
||||
}
|
||||
}
|
||||
if (sources.isEmpty()) {
|
||||
double longest = 0D;
|
||||
for (HydrologyGridNode node : grid.nodes()) {
|
||||
if (grid.owns(node.x(), node.z()) && tree.root(node.index()) >= 0) {
|
||||
longest = Math.max(longest, tree.length(node.index()));
|
||||
}
|
||||
}
|
||||
diagnostic(diagnostics, center.id(), center.naturalPoint(), false,
|
||||
longest == 0D ? HydrologyCandidateRejection.NO_DRAINAGE_PATH
|
||||
: longest < settings.minimumLength() ? HydrologyCandidateRejection.COURSE_TOO_SHORT
|
||||
: HydrologyCandidateRejection.POLICY_EXCLUDED, (int) StrictMath.round(longest));
|
||||
}
|
||||
Set<Long> usedOutlets = new HashSet<>();
|
||||
for (HydrologyRegionalNetwork network : accepted) {
|
||||
for (RiverOutlet outlet : network.outlets()) {
|
||||
usedOutlets.add(outlet.id());
|
||||
}
|
||||
}
|
||||
HydrologyRegionalSourceTrials sourceTrials = new HydrologyRegionalSourceTrials(grid, tree, sources);
|
||||
for (int trials = 0; trials < MAXIMUM_SOURCE_TRIALS; trials++) {
|
||||
if (accepted.size() >= settings.maximumTrunks()) {
|
||||
break;
|
||||
}
|
||||
OptionalInt selected = sourceTrials.next(usedOutlets);
|
||||
if (selected.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
int source = selected.getAsInt();
|
||||
OutletCandidate outlet = tree.outlets().get(tree.root(source));
|
||||
HydrologyRegionalNetwork candidate = build(grid, tree, source, null, basinSeed, diagnostics);
|
||||
if (!candidate.courses().isEmpty() && !conflicts(candidate.courses().getFirst(), merge(accepted).courses())) {
|
||||
accepted.add(candidate);
|
||||
usedOutlets.add(outlet.outlet().id());
|
||||
}
|
||||
}
|
||||
HydrologyRegionalNetwork network = merge(accepted);
|
||||
return new HydrologyRegionalNetwork(network.nodes(), network.edges(), network.outlets(), network.courses(), diagnostics, network.cavePlans());
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork coastalChannel(HydrologySampledGrid grid, List<OutletCandidate> outlets,
|
||||
long basinSeed, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
int maximum = Math.min(8, outlets.size());
|
||||
int pairs = 0;
|
||||
double longest = 0D;
|
||||
for (int destination = 0; destination < maximum; destination++) {
|
||||
if (!bidirectionalMouth(outlets.get(destination).outlet())) {
|
||||
continue;
|
||||
}
|
||||
HydrologyRegionalGraph.Tree tree = graph.route(grid, List.of(outlets.get(destination)), true);
|
||||
ArrayList<OutletCandidate> origins = new ArrayList<>(outlets);
|
||||
origins.sort(Comparator.comparingDouble((OutletCandidate outlet) -> tree.length(outlet.landIndex())).reversed()
|
||||
.thenComparingLong(outlet -> outlet.outlet().id()));
|
||||
int trials = 0;
|
||||
for (OutletCandidate origin : origins) {
|
||||
if (trials++ >= 8) {
|
||||
break;
|
||||
}
|
||||
int source = origin.landIndex();
|
||||
if (!bidirectionalMouth(origin.outlet())) {
|
||||
continue;
|
||||
}
|
||||
if (grid.owns(grid.node(source).x(), grid.node(source).z())
|
||||
&& opposingCoasts(origin.outlet(), outlets.get(destination).outlet())) {
|
||||
pairs++;
|
||||
longest = Math.max(longest, tree.length(source));
|
||||
}
|
||||
if (!grid.owns(grid.node(source).x(), grid.node(source).z()) || tree.root(source) < 0
|
||||
|| !opposingCoasts(origin.outlet(), outlets.get(destination).outlet())
|
||||
|| tree.length(source) < settings.minimumLength()
|
||||
|| origin.outlet().connectionPoint().distanceSquared2D(outlets.get(destination).outlet().connectionPoint())
|
||||
< (long) settings.minimumLength() * settings.minimumLength() / 4L) {
|
||||
continue;
|
||||
}
|
||||
HydrologyRegionalNetwork network = build(grid, tree, source, origin, basinSeed, diagnostics);
|
||||
if (!network.courses().isEmpty()) {
|
||||
return network;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (diagnostics.isEmpty()) {
|
||||
HydrologyGridNode center = grid.nodes().get(grid.nodes().size() / 2);
|
||||
diagnostic(diagnostics, basinSeed, center.naturalPoint(), true,
|
||||
pairs == 0 ? HydrologyCandidateRejection.NO_LEGAL_OUTLET
|
||||
: longest == 0D ? HydrologyCandidateRejection.NO_DRAINAGE_PATH
|
||||
: HydrologyCandidateRejection.COURSE_TOO_SHORT,
|
||||
(int) StrictMath.round(longest));
|
||||
}
|
||||
return HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
|
||||
boolean bidirectionalMouth(RiverOutlet outlet) {
|
||||
HydrologyTerrainSample land = sample(outlet.landwardPoint().x(), outlet.landwardPoint().z());
|
||||
HydrologyTerrainSample ocean = sample(outlet.connectionPoint().x(), outlet.connectionPoint().z());
|
||||
return land != null && ocean != null && land.drainsInto(ocean) && ocean.drainsInto(land);
|
||||
}
|
||||
|
||||
private static boolean opposingCoasts(RiverOutlet first, RiverOutlet second) {
|
||||
double firstX = first.connectionPoint().x() - first.landwardPoint().x();
|
||||
double firstZ = first.connectionPoint().z() - first.landwardPoint().z();
|
||||
double secondX = second.connectionPoint().x() - second.landwardPoint().x();
|
||||
double secondZ = second.connectionPoint().z() - second.landwardPoint().z();
|
||||
return firstX * secondX + firstZ * secondZ < 0D;
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork build(HydrologySampledGrid grid, HydrologyRegionalGraph.Tree tree,
|
||||
int source, OutletCandidate origin, long basinSeed,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
List<Integer> indices = graph.path(tree, source);
|
||||
OutletCandidate destination = tree.outlets().get(tree.root(source));
|
||||
RiverOutlet outlet = destination.outlet();
|
||||
ArrayList<HydrologyPoint> guide = new ArrayList<>(indices.size() + 1);
|
||||
ArrayList<String> profiles = new ArrayList<>(grid.node(source).terrain().preferredProfileKeys());
|
||||
for (int index : indices) {
|
||||
HydrologyGridNode node = grid.node(index);
|
||||
guide.add(node.naturalPoint());
|
||||
profiles.retainAll(node.terrain().preferredProfileKeys());
|
||||
}
|
||||
profiles.retainAll(sample(outlet.connectionPoint().x(), outlet.connectionPoint().z()).preferredProfileKeys());
|
||||
if (origin != null) {
|
||||
profiles.retainAll(sample(origin.outlet().connectionPoint().x(), origin.outlet().connectionPoint().z()).preferredProfileKeys());
|
||||
guide.set(0, origin.outlet().landwardPoint());
|
||||
}
|
||||
if (profiles.isEmpty() || guide.size() < 2) {
|
||||
diagnostic(diagnostics, grid.node(source).id(), grid.node(source).naturalPoint(), origin != null,
|
||||
HydrologyCandidateRejection.POLICY_EXCLUDED, guide.size());
|
||||
return HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
guide.set(guide.size() - 1, outlet.landwardPoint());
|
||||
String profile = profiles.get(HydrologyHash.between(HydrologyHash.mix(basinSeed, source), 0, profiles.size() - 1));
|
||||
BuildContext context = new BuildContext(grid.node(source), origin, basinSeed, outlet, profile,
|
||||
new HydrologyRegionalFlow(guide, tree.contributions(source)),
|
||||
HydrologyRegionalMorphology.sample(guide, this::sample, planner.settings));
|
||||
HydrologyRegionalRoute.Attempt selected = routes.select(guide, profile, origin != null,
|
||||
HydrologyOceanReceiver.forOutlet(planner.settings, this::sample, outlet), candidate -> attempt(context, candidate));
|
||||
if (selected.rejection() != null) {
|
||||
if (diagnostics.size() < MAXIMUM_DIAGNOSTICS) {
|
||||
diagnostics.add(selected.rejection());
|
||||
}
|
||||
} else if (selected.refinement().rejection() != null) {
|
||||
HydrologyRegionalRoute.Refinement rejected = selected.refinement();
|
||||
diagnostic(diagnostics, grid.node(source).id(), rejected.failure(), origin != null, rejected.rejection(), rejected.detail());
|
||||
}
|
||||
return selected.network();
|
||||
}
|
||||
|
||||
private HydrologyRegionalRoute.Attempt attempt(BuildContext context, HydrologyRegionalRoute.Refinement refinement) {
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
HydrologyRegionalNetwork network = build(context, refinement, diagnostics);
|
||||
if (network.courses().isEmpty() && diagnostics.isEmpty()) {
|
||||
diagnostic(diagnostics, context.source().id(), refinement.points().getFirst(), context.origin() != null,
|
||||
HydrologyCandidateRejection.NO_DRAINAGE_PATH, 0);
|
||||
}
|
||||
return new HydrologyRegionalRoute.Attempt(refinement, network, network.courses().isEmpty() ? diagnostics.getFirst() : null);
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork build(BuildContext context, HydrologyRegionalRoute.Refinement refinement,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
RiverOutlet outlet = context.outlet();
|
||||
OutletCandidate origin = context.origin();
|
||||
String profile = context.profile();
|
||||
List<HydrologyPoint> path = refinement.points();
|
||||
if (refinement.oceanEntry() != null) {
|
||||
HydrologyRegionalRoute.OceanEntry entry = refinement.oceanEntry();
|
||||
outlet = new RiverOutlet(HydrologyHash.mix(outlet.id(), entry.landward().x(), entry.landward().z(),
|
||||
entry.receiving().x(), entry.receiving().z()), HydrologyFeatureType.MOUTH, outlet.drainageNodeId(),
|
||||
entry.landward(), entry.receiving(), planner.settings.seaLevel(), true);
|
||||
}
|
||||
long courseId = HydrologyHash.mix(context.basinSeed(), COURSE_SALT, context.source().id(), outlet.id(), origin == null ? 0L : origin.outlet().id());
|
||||
HydrologyRegionalFlow flow = context.flow();
|
||||
HydrologyGeometrySampler geometry = request -> regionalGeometry(request, context);
|
||||
ArrayList<HydraulicSegment> segments;
|
||||
if (origin == null) {
|
||||
SurfaceCourseResult built = new SurfaceCourseBuilder(planner.settings.surface(),
|
||||
HydrologyOceanReceiver.forOutlet(planner.settings, this::sample, outlet), geometry, planner.settings.seaLevel()).build(
|
||||
planner.worldSeed, courseId, profile, path, SurfaceTerminal.OCEAN_MOUTH,
|
||||
planner.settings.seaLevel(), settings.minimumLength());
|
||||
if (!built.accepted()) {
|
||||
diagnostic(diagnostics, courseId, path.getFirst(), false, built.rejection(), built.rejectionDetail());
|
||||
return HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
segments = new ArrayList<>(built.segments());
|
||||
segments.add(mouth(courseId, built.pathEnd(), outlet.connectionPoint(), built.lastWidth(), built.lastDepth(), 1));
|
||||
} else {
|
||||
segments = coastalSegments(courseId, profile, path, origin.outlet(), outlet, geometry);
|
||||
}
|
||||
HydrologyRegionalCourseGraph published = HydrologyRegionalCourseGraph.create(this::sample,
|
||||
new HydrologyRegionalCourseGraph.Options(courseId, outlet.id(), settings.sampleSpacing(), path, flow));
|
||||
List<DrainageNode> nodes = published.nodes();
|
||||
List<DrainageEdge> edges = published.edges();
|
||||
RiverCourse course = new RiverCourse(courseId, RiverCourseType.SURFACE, OptionalLong.of(nodes.getFirst().id()),
|
||||
OptionalLong.of(outlet.id()), profile, published.discharge(), edges, segments);
|
||||
HydrologyRegionalFalls falls = new HydrologyRegionalFalls(planner, course);
|
||||
if (!validFootprints(course, diagnostics, falls)) {
|
||||
return HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
RiverOutlet publishedOutlet = publishedOutlet(outlet, nodes.getLast().id());
|
||||
HydrologyRegionalNetwork network = new HydrologyRegionalNetwork(nodes, edges, origin == null ? List.of(publishedOutlet)
|
||||
: List.of(publishedOutlet(origin.outlet(), nodes.getFirst().id()), publishedOutlet),
|
||||
List.of(course), List.of(), List.of());
|
||||
List<HydrologyCavePlan> plans = falls.validate(network, diagnostics);
|
||||
return plans == null ? HydrologyRegionalNetwork.EMPTY : new HydrologyRegionalNetwork(nodes, edges,
|
||||
network.outlets(), network.courses(), List.of(), plans);
|
||||
}
|
||||
|
||||
private static RiverOutlet publishedOutlet(RiverOutlet outlet, long nodeId) {
|
||||
return new RiverOutlet(outlet.id(), outlet.type(), nodeId, outlet.landwardPoint(), outlet.connectionPoint(),
|
||||
outlet.seaLevel(), outlet.directOcean());
|
||||
}
|
||||
|
||||
private int regionalGeometry(HydrologyGeometrySampler.Request request, BuildContext context) {
|
||||
if (request.field() != HydrologyGeometrySampler.Field.SURFACE_WIDTH) {
|
||||
return planner.geometrySampler.sample(request);
|
||||
}
|
||||
int authored = planner.geometrySampler.sample(request);
|
||||
double fraction = 0.25D + 0.75D * context.flow().fraction(request.x(), request.z());
|
||||
double sampled = request.minimum() + (request.maximum() - request.minimum()) * fraction;
|
||||
double width = sampled * 0.75D + authored * 0.25D;
|
||||
double freedom = context.morphology().freedomAt(request.x(), request.z());
|
||||
double constrained = request.minimum() + (width - request.minimum()) * freedom;
|
||||
return Math.max(request.minimum(), Math.min(request.maximum(), (int) StrictMath.round(constrained)));
|
||||
}
|
||||
|
||||
private ArrayList<HydraulicSegment> coastalSegments(long courseId, String profile, List<HydrologyPoint> path,
|
||||
RiverOutlet origin, RiverOutlet destination, HydrologyGeometrySampler geometry) {
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>();
|
||||
int seaLevel = planner.settings.seaLevel();
|
||||
HydrologyPlannerSettings.Surface surface = planner.settings.surface();
|
||||
HydrologyPoint first = HydrologyPlanner.withY(path.getFirst(), seaLevel);
|
||||
double width = coastalWidth(profile, first, geometry);
|
||||
int depth = geometry.sample(HydrologyGeometrySampler.Field.SURFACE_DEPTH, profile, first.x(), first.z(), surface.minimumDepth(), surface.maximumDepth());
|
||||
segments.add(mouth(courseId, origin.connectionPoint(), first, width, depth, 0));
|
||||
for (int index = 0; index + 1 < path.size(); index++) {
|
||||
HydrologyPoint point = path.get(index);
|
||||
HydrologyPoint next = path.get(index + 1);
|
||||
depth = geometry.sample(HydrologyGeometrySampler.Field.SURFACE_DEPTH, profile, point.x(), point.z(), surface.minimumDepth(), surface.maximumDepth());
|
||||
double nextWidth = coastalWidth(profile, next, geometry);
|
||||
int nextDepth = geometry.sample(HydrologyGeometrySampler.Field.SURFACE_DEPTH, profile, next.x(), next.z(), surface.minimumDepth(), surface.maximumDepth());
|
||||
segments.add(new HydraulicSegment(HydrologyHash.mix(courseId, index, COURSE_SALT), courseId,
|
||||
HydrologyFeatureType.SURFACE_POOL, seaLevel, seaLevel, (int) StrictMath.ceil(Math.max(width, nextWidth)), Math.max(depth, nextDepth), false, false,
|
||||
List.of(HydrologyPlanner.withY(point, seaLevel), HydrologyPlanner.withY(next, seaLevel)),
|
||||
new HydraulicChannelProfile(new double[] {width, nextWidth}, new double[] {depth, nextDepth})));
|
||||
width = nextWidth;
|
||||
depth = nextDepth;
|
||||
}
|
||||
segments.add(mouth(courseId, HydrologyPlanner.withY(path.getLast(), seaLevel), destination.connectionPoint(), width, depth, 1));
|
||||
return segments;
|
||||
}
|
||||
|
||||
private double coastalWidth(String profile, HydrologyPoint point, HydrologyGeometrySampler geometry) {
|
||||
HydrologyPlannerSettings.Surface surface = planner.settings.surface();
|
||||
int authored = geometry.sample(HydrologyGeometrySampler.Field.SURFACE_WIDTH, profile, point.x(), point.z(),
|
||||
surface.minimumWidth(), surface.maximumWidth());
|
||||
return Math.max(surface.minimumWidth(), Math.min(surface.maximumWidth() * 2D,
|
||||
authored * sample(point.x(), point.z()).widthMultiplier()));
|
||||
}
|
||||
|
||||
private HydraulicSegment mouth(long courseId, HydrologyPoint start, HydrologyPoint end, double width, int depth, int side) {
|
||||
int head = planner.settings.seaLevel();
|
||||
return new HydraulicSegment(HydrologyHash.mix(courseId, HydrologyFeatureType.MOUTH.ordinal(), side), courseId,
|
||||
HydrologyFeatureType.MOUTH, head, head, (int) StrictMath.ceil(width), depth, false, false,
|
||||
List.of(HydrologyPlanner.withY(start, head), HydrologyPlanner.withY(end, head)), HydraulicChannelProfile.uniform(width, depth));
|
||||
}
|
||||
|
||||
private boolean validFootprints(RiverCourse course, List<HydrologyDiagnosticCandidate> diagnostics, HydrologyRegionalFalls falls) {
|
||||
double length = 0D;
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
points.addAll(segment.centerline());
|
||||
for (int index = 1; index < segment.centerline().size(); index++) {
|
||||
length += StrictMath.sqrt(segment.centerline().get(index).distanceSquared2D(segment.centerline().get(index - 1)));
|
||||
}
|
||||
}
|
||||
boolean coastal = course.segments().getFirst().type() == HydrologyFeatureType.MOUTH;
|
||||
HydrologyPoint source = course.segments().getFirst().start();
|
||||
if (length < settings.minimumLength() || length > planner.settings.routing().maximumRouteLength()) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, length < settings.minimumLength()
|
||||
? HydrologyCandidateRejection.COURSE_TOO_SHORT : HydrologyCandidateRejection.ROUTE_LIMIT,
|
||||
(int) StrictMath.ceil(length));
|
||||
return false;
|
||||
}
|
||||
SurfaceFootprintCompiler compiler = new SurfaceFootprintCompiler(planner.settings,
|
||||
this::sample, planner.geometrySampler);
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(points);
|
||||
int[] excavation = new int[centerline.size()];
|
||||
HydrologyRegionalConnectivity connectivity = new HydrologyRegionalConnectivity();
|
||||
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forCourse(planner.settings, this::sample, course);
|
||||
for (SurfaceBounds bounds : courseWindows(course)) {
|
||||
SurfaceFootprint footprint = compiler.compile(course, bounds);
|
||||
if (!footprint.accepted()) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, footprint.rejection(), footprint.rejectionDetail());
|
||||
return false;
|
||||
}
|
||||
falls.retain(footprint);
|
||||
connectivity.add(footprint.columns());
|
||||
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(planner.settings,
|
||||
receiver, planner.geometrySampler, course, bounds);
|
||||
SurfaceExcavationMetrics.accumulate(course, centerline, footprint, drops, bounds, excavation);
|
||||
for (HydrologyColumnSample column : drops.columns()) {
|
||||
connectivity.add(column);
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (layer.fluidOwned() && !sample(column.x(), column.z()).preferredProfileKeys().contains(course.profileKey())) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, HydrologyCandidateRejection.POLICY_EXCLUDED, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (column.layer().fluidOwned() && !column.terrain().preferredProfileKeys().contains(course.profileKey())) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, HydrologyCandidateRejection.POLICY_EXCLUDED, column.station());
|
||||
return false;
|
||||
}
|
||||
if (coastal && column.layer().fluidOwned()
|
||||
&& column.terrain().naturalHeight() - column.layer().bedY() > settings.maximumCoastalIncision()) {
|
||||
diagnostic(diagnostics, course.id(), source, true, HydrologyCandidateRejection.SURFACE_BANK_BUDGET,
|
||||
column.terrain().naturalHeight() - column.layer().bedY());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
int volume = SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, excavation);
|
||||
if (volume > planner.settings.surface().banks().erosion().excavation().maximumVolumePerBlock()) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, HydrologyCandidateRejection.SURFACE_BANK_BUDGET, volume);
|
||||
return false;
|
||||
}
|
||||
if (!connectivity.connected(course, this::sample, planner.settings)) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<SurfaceBounds> courseWindows(RiverCourse course) {
|
||||
int tileSize = planner.settings.routing().tileSize();
|
||||
int radius = footprintRadius();
|
||||
Set<HydrologyTileKey> keys = new HashSet<>();
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
for (int z = Math.floorDiv(point.z() - radius, tileSize); z <= Math.floorDiv(point.z() + radius, tileSize); z++) {
|
||||
for (int x = Math.floorDiv(point.x() - radius, tileSize); x <= Math.floorDiv(point.x() + radius, tileSize); x++) {
|
||||
keys.add(new HydrologyTileKey(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ArrayList<HydrologyTileKey> ordered = new ArrayList<>(keys);
|
||||
ordered.sort(HydrologyTileKey::compareTo);
|
||||
ArrayList<SurfaceBounds> windows = new ArrayList<>(ordered.size());
|
||||
for (HydrologyTileKey key : ordered) {
|
||||
windows.add(tileBounds(key));
|
||||
}
|
||||
return List.copyOf(windows);
|
||||
}
|
||||
|
||||
SurfaceBounds tileBounds(HydrologyTileKey key) {
|
||||
int size = planner.settings.routing().tileSize();
|
||||
return new SurfaceBounds(key.minimumBlockX(size), key.minimumBlockZ(size),
|
||||
Math.addExact(key.minimumBlockX(size), size - 1), Math.addExact(key.minimumBlockZ(size), size - 1));
|
||||
}
|
||||
|
||||
SurfaceBounds ownerBounds(HydrologyTileKey key) {
|
||||
SurfaceBounds tile = tileBounds(key);
|
||||
int reach = planner.settings.publicationRadius() + 1;
|
||||
return new SurfaceBounds(Math.subtractExact(tile.minimumX(), reach), Math.subtractExact(tile.minimumZ(), reach),
|
||||
Math.addExact(tile.maximumX(), reach), Math.addExact(tile.maximumZ(), reach));
|
||||
}
|
||||
|
||||
HydrologyCaveCourseFilter.Result include(HydrologyCaveCourseFilter.Result local, HydrologyRegionalNetwork regional) {
|
||||
if (regional.courses().isEmpty()) {
|
||||
return local;
|
||||
}
|
||||
HydrologyRegionalNetwork combined = merge(List.of(new HydrologyRegionalNetwork(local.nodes(), local.edges(),
|
||||
local.outlets(), local.courses(), List.of(), local.cavePlans()), regional));
|
||||
return new HydrologyCaveCourseFilter.Result(combined.nodes(), combined.edges(), combined.outlets(),
|
||||
combined.courses(), combined.cavePlans());
|
||||
}
|
||||
|
||||
HydrologyCaveCourseFilter.Result withoutRegional(HydrologyCaveCourseFilter.Result result, HydrologyTileKey key) {
|
||||
if (!enabled()) {
|
||||
return result;
|
||||
}
|
||||
Set<Long> regionalIds = new HashSet<>();
|
||||
for (RiverCourse course : coursesIn(ownerBounds(key)).courses()) {
|
||||
regionalIds.add(course.id());
|
||||
}
|
||||
ArrayList<RiverCourse> local = new ArrayList<>();
|
||||
for (RiverCourse course : result.courses()) {
|
||||
if (!regionalIds.contains(course.id())) {
|
||||
local.add(course);
|
||||
}
|
||||
}
|
||||
ArrayList<HydrologyCavePlan> plans = new ArrayList<>();
|
||||
for (HydrologyCavePlan plan : result.cavePlans()) {
|
||||
if (!regionalIds.contains(plan.source().sourceId())) {
|
||||
plans.add(plan);
|
||||
}
|
||||
}
|
||||
return new HydrologyCaveCourseFilter.Result(result.nodes(), result.edges(), result.outlets(), local, plans);
|
||||
}
|
||||
|
||||
private int footprintRadius() {
|
||||
return (int) StrictMath.ceil(planner.settings.surface().maximumWidth() * 2D
|
||||
+ Math.max(planner.settings.surface().shoreWidth(), planner.settings.widestShoreBiomeWidth())
|
||||
+ Math.min(planner.settings.surface().banks().maximumBlendWidth(),
|
||||
planner.settings.surface().banks().erosion().excavation().maximumWidth()) + 4D);
|
||||
}
|
||||
|
||||
private HydrologySampledGrid sample(HydrologyTileKey key) {
|
||||
int spacing = settings.sampleSpacing();
|
||||
int size = settings.basinSize(planner.settings.routing());
|
||||
int ownerX = key.minimumBlockX(size);
|
||||
int ownerZ = key.minimumBlockZ(size);
|
||||
int minimumX = Math.subtractExact(ownerX, spacing);
|
||||
int minimumZ = Math.subtractExact(ownerZ, spacing);
|
||||
int width = size / spacing + 3;
|
||||
HydrologyTerrainSample[] terrain = planner.routingSampler.sampleGrid(
|
||||
new HydrologyRoutingTerrainSampler.GridRequest(minimumX, minimumZ, width, spacing));
|
||||
ArrayList<HydrologyGridNode> nodes = new ArrayList<>(terrain.length);
|
||||
for (int index = 0; index < terrain.length; index++) {
|
||||
int x = minimumX + index % width * spacing;
|
||||
int z = minimumZ + index / width * spacing;
|
||||
nodes.add(new HydrologyGridNode(index, index % width, index / width, x, z,
|
||||
HydrologyHash.mix(planner.worldSeed, NODE_SALT, key.tileX(), key.tileZ(), x, z), terrain[index]));
|
||||
}
|
||||
return new HydrologySampledGrid(minimumX, minimumZ, ownerX, ownerZ, size, width, spacing, List.copyOf(nodes));
|
||||
}
|
||||
|
||||
private ArrayList<OutletCandidate> spacedOutlets(List<OutletCandidate> candidates, int maximum) {
|
||||
ArrayList<OutletCandidate> selected = new ArrayList<>();
|
||||
long spacing = Math.max(settings.minimumLength() / 2, settings.sampleSpacing() * 2);
|
||||
for (OutletCandidate candidate : candidates) {
|
||||
boolean overlap = false;
|
||||
for (OutletCandidate existing : selected) {
|
||||
if (existing.outlet().landwardPoint().distanceSquared2D(candidate.outlet().landwardPoint()) < spacing * spacing) {
|
||||
overlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!overlap) {
|
||||
selected.add(candidate);
|
||||
if (selected.size() == maximum) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private boolean admitted(HydrologyTileKey owner, RiverCourse course) {
|
||||
long rank = HydrologyHash.mix(planner.worldSeed, BASIN_SALT, owner.tileX(), owner.tileZ());
|
||||
int size = settings.basinSize(planner.settings.routing());
|
||||
int reach = settings.sampleSpacing() * 2 + footprintRadius();
|
||||
for (int z = owner.tileZ() - 1; z <= owner.tileZ() + 1; z++) {
|
||||
for (int x = owner.tileX() - 1; x <= owner.tileX() + 1; x++) {
|
||||
HydrologyTileKey other = new HydrologyTileKey(x, z);
|
||||
if (other.equals(owner)) {
|
||||
continue;
|
||||
}
|
||||
SurfaceBounds otherBounds = new SurfaceBounds(other.minimumBlockX(size), other.minimumBlockZ(size),
|
||||
Math.addExact(other.minimumBlockX(size), size - 1), Math.addExact(other.minimumBlockZ(size), size - 1));
|
||||
if (!intersects(course, otherBounds, reach)) {
|
||||
continue;
|
||||
}
|
||||
long otherRank = HydrologyHash.mix(planner.worldSeed, BASIN_SALT, x, z);
|
||||
if (Long.compareUnsigned(otherRank, rank) < 0 && conflicts(course, draft(other).courses())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean conflicts(RiverCourse course, List<RiverCourse> existing) {
|
||||
HydrologyCrossTileSurfaceAdmission.Claim current = claim(course);
|
||||
ArrayList<HydrologyCrossTileSurfaceAdmission.RankedClaim> blockers = new ArrayList<>(existing.size());
|
||||
for (RiverCourse other : existing) {
|
||||
blockers.add(new HydrologyCrossTileSurfaceAdmission.RankedClaim(new HydrologyTileKey(0, 0), 0, claim(other)));
|
||||
}
|
||||
return !HydrologyCrossTileSurfaceAdmission.admit(List.of(current), blockers).rejections().isEmpty();
|
||||
}
|
||||
|
||||
private HydrologyCrossTileSurfaceAdmission.Claim claim(RiverCourse course) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
int width = 1;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
width = Math.max(width, segment.width());
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
if (points.isEmpty() || points.getLast().distanceSquared2D(point) >= 256L) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
HydrologyPoint last = course.segments().getLast().end();
|
||||
if (points.size() < 2 || !points.getLast().equals(last)) {
|
||||
points.add(last);
|
||||
}
|
||||
int bankWidth = planner.settings.surface().banks().erosion().excavation().maximumWidth();
|
||||
int completeWidth = width + 2 * ((int) StrictMath.ceil(planner.settings.surface().shoreWidth()) + bankWidth);
|
||||
return new HydrologyCrossTileSurfaceAdmission.Claim(course.id(), course.outletId().orElse(course.id()), last,
|
||||
true, completeWidth, points, 0);
|
||||
}
|
||||
|
||||
private static boolean intersects(RiverCourse course, SurfaceBounds bounds, int radius) {
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
if (point.x() >= bounds.minimumX() - radius && point.x() <= bounds.maximumX() + radius
|
||||
&& point.z() >= bounds.minimumZ() - radius && point.z() <= bounds.maximumZ() + radius) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static HydrologyColumnSample mergeColumn(HydrologyColumnSample first, HydrologyColumnSample second) {
|
||||
ArrayList<HydrologyColumnLayer> layers = new ArrayList<>(first.layers());
|
||||
for (HydrologyColumnLayer layer : second.layers()) {
|
||||
if (!layers.contains(layer)) {
|
||||
layers.add(layer);
|
||||
}
|
||||
}
|
||||
return new HydrologyColumnSample(first.x(), first.z(), first.naturalHeight(), first.seaLevel(), first.ocean(),
|
||||
first.parentBiomeKey(), layers);
|
||||
}
|
||||
|
||||
private static HydrologyRegionalNetwork select(HydrologyRegionalNetwork network, List<RiverCourse> courses) {
|
||||
Set<Long> nodeIds = new HashSet<>();
|
||||
Set<Long> outletIds = new HashSet<>();
|
||||
ArrayList<DrainageEdge> edges = new ArrayList<>();
|
||||
for (RiverCourse course : courses) {
|
||||
nodeIds.add(course.sourceNodeId().orElseThrow());
|
||||
outletIds.add(course.outletId().orElseThrow());
|
||||
for (DrainageEdge edge : course.drainageEdges()) {
|
||||
nodeIds.add(edge.upstreamNodeId());
|
||||
nodeIds.add(edge.downstreamNodeId());
|
||||
edges.add(edge);
|
||||
}
|
||||
}
|
||||
ArrayList<DrainageNode> nodes = new ArrayList<>();
|
||||
for (DrainageNode node : network.nodes()) {
|
||||
if (nodeIds.contains(node.id())) {
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
ArrayList<RiverOutlet> outlets = new ArrayList<>();
|
||||
for (RiverOutlet outlet : network.outlets()) {
|
||||
if (outletIds.contains(outlet.id()) || courses.stream().anyMatch(course ->
|
||||
course.segments().getFirst().type() == HydrologyFeatureType.MOUTH
|
||||
&& course.segments().getFirst().start().distanceSquared2D(outlet.connectionPoint()) == 0L)) {
|
||||
outlets.add(outlet);
|
||||
}
|
||||
}
|
||||
Set<Long> courseIds = new HashSet<>();
|
||||
for (RiverCourse course : courses) { courseIds.add(course.id()); }
|
||||
ArrayList<HydrologyCavePlan> plans = new ArrayList<>();
|
||||
for (HydrologyCavePlan plan : network.cavePlans()) {
|
||||
if (courseIds.contains(plan.source().sourceId())) { plans.add(plan); }
|
||||
}
|
||||
return new HydrologyRegionalNetwork(nodes, edges, outlets, courses, List.of(), plans);
|
||||
}
|
||||
|
||||
private static void diagnostic(List<HydrologyDiagnosticCandidate> diagnostics, long id, HydrologyPoint point,
|
||||
boolean coastal, HydrologyCandidateRejection rejection, int detail) {
|
||||
if (diagnostics.size() >= MAXIMUM_DIAGNOSTICS) {
|
||||
return;
|
||||
}
|
||||
HydrologyCandidateKind kind = coastal ? HydrologyCandidateKind.COASTAL_CHANNEL : HydrologyCandidateKind.REGIONAL_SOURCE;
|
||||
diagnostics.add(new HydrologyDiagnosticCandidate(HydrologyHash.mix(id, BASIN_SALT, rejection.ordinal()), kind,
|
||||
coastal ? HydrologyFeatureType.MOUTH : HydrologyFeatureType.SURFACE_POOL, point, rejection, detail));
|
||||
}
|
||||
|
||||
private static HydrologyRegionalNetwork merge(List<HydrologyRegionalNetwork> networks) {
|
||||
Map<Long, DrainageNode> nodes = new LinkedHashMap<>();
|
||||
Map<Long, DrainageEdge> edges = new LinkedHashMap<>();
|
||||
Map<Long, RiverOutlet> outlets = new LinkedHashMap<>();
|
||||
Map<Long, RiverCourse> courses = new LinkedHashMap<>();
|
||||
Map<Long, HydrologyDiagnosticCandidate> diagnostics = new LinkedHashMap<>();
|
||||
Map<Long, HydrologyCavePlan> plans = new LinkedHashMap<>();
|
||||
for (HydrologyRegionalNetwork network : networks) {
|
||||
for (DrainageNode node : network.nodes()) { nodes.put(node.id(), node); }
|
||||
for (DrainageEdge edge : network.edges()) { edges.put(edge.id(), edge); }
|
||||
for (RiverOutlet outlet : network.outlets()) { outlets.put(outlet.id(), outlet); }
|
||||
for (RiverCourse course : network.courses()) { courses.put(course.id(), course); }
|
||||
for (HydrologyDiagnosticCandidate candidate : network.diagnostics()) { diagnostics.put(candidate.id(), candidate); }
|
||||
for (HydrologyCavePlan plan : network.cavePlans()) { plans.put(plan.source().sourceId(), plan); }
|
||||
}
|
||||
return new HydrologyRegionalNetwork(new ArrayList<>(nodes.values()), new ArrayList<>(edges.values()),
|
||||
new ArrayList<>(outlets.values()), new ArrayList<>(courses.values()), new ArrayList<>(diagnostics.values()), new ArrayList<>(plans.values()));
|
||||
}
|
||||
|
||||
private record BuildContext(HydrologyGridNode source, OutletCandidate origin, long basinSeed, RiverOutlet outlet,
|
||||
String profile, HydrologyRegionalFlow flow, HydrologyRegionalMorphology morphology) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
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.HydrologyCaveConflictPolicy;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
final class HydrologyRegionalProtection {
|
||||
private HydrologyRegionalProtection() {
|
||||
}
|
||||
|
||||
static void rejectFootprints(List<RiverCourse> courses, HydrologyFootprintCompiler compiler,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
HydrologyRegionalNetwork regional = compiler.regionalNetwork;
|
||||
if (regional.cavePlans().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, String> profiles = profiles(regional);
|
||||
Iterator<RiverCourse> iterator = courses.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
RiverCourse course = iterator.next();
|
||||
RiverFootprint footprint = compiler.compileCourse(course);
|
||||
for (HydrologyCavePlan plan : regional.cavePlans()) {
|
||||
if (changesWitness(course.profileKey(), footprint, profiles.get(plan.source().sourceId()), plan)) {
|
||||
diagnostics.add(diagnostic(course, plan.source().sourceId()));
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static HydrologyCaveCourseFilter.Result rejectPlans(HydrologyCaveCourseFilter.Result result,
|
||||
HydrologyRegionalNetwork regional,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
if (regional.cavePlans().isEmpty() || result.cavePlans().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
Map<Long, String> profiles = profiles(regional);
|
||||
Map<Long, RiverCourse> courses = new HashMap<>();
|
||||
for (RiverCourse course : result.courses()) {
|
||||
courses.put(course.id(), course);
|
||||
}
|
||||
Set<Long> rejected = new HashSet<>();
|
||||
for (HydrologyCavePlan local : result.cavePlans()) {
|
||||
RiverCourse course = courses.get(local.source().sourceId());
|
||||
for (HydrologyCavePlan fixed : regional.cavePlans()) {
|
||||
String regionalProfile = profiles.get(fixed.source().sourceId());
|
||||
if (HydrologyCaveConflictPolicy.hasIncompatibleOverlap(course.profileKey(), local.actions(),
|
||||
regionalProfile, fixed.actions()) || changesWitness(course.profileKey(), local, regionalProfile, fixed)) {
|
||||
rejected.add(course.id());
|
||||
diagnostics.add(diagnostic(course, fixed.source().sourceId()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return HydrologyCaveCourseFilter.withoutCourses(result, rejected);
|
||||
}
|
||||
|
||||
static boolean changesWitness(String profile, RiverFootprint footprint, String regionalProfile, HydrologyCavePlan plan) {
|
||||
for (HydrologyColumnSample column : footprint.columns().values()) {
|
||||
if (!plan.allPreconditionsIn(column.x(), column.z(), Math.addExact(column.x(), 1), Math.addExact(column.z(), 1),
|
||||
(position, precondition) -> preservesColumn(profile, column, regionalProfile, plan, position))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean preservesColumn(String profile, HydrologyColumnSample column, String regionalProfile,
|
||||
HydrologyCavePlan plan, CavePosition position) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
HydrologyCaveAction action = actionAt(column, layer, position.y());
|
||||
if (action != null && (!profile.equals(regionalProfile) || plan.actions().get(position) != action)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static HydrologyCaveAction actionAt(HydrologyColumnSample column, HydrologyColumnLayer layer, int y) {
|
||||
if (y <= layer.bedY() || layer.oceanApron()) {
|
||||
return null;
|
||||
}
|
||||
if (layer.channel() && layer.fluidOwned() && (layer.terrainOwned() || layer.fallingFluid())
|
||||
&& y <= layer.fluidHeadY()) {
|
||||
return layer.fallingFluid() && y < layer.fluidHeadY()
|
||||
? HydrologyCaveAction.FALLING_FLUID : HydrologyCaveAction.WET_SOURCE;
|
||||
}
|
||||
if (!layer.terrainOwned()) {
|
||||
return null;
|
||||
}
|
||||
int ceiling = layer.feature().type().isSurface() ? column.naturalHeight() : layer.ceilingY();
|
||||
return y <= ceiling ? HydrologyCaveAction.DRY_AIR : null;
|
||||
}
|
||||
|
||||
private static boolean changesWitness(String profile, HydrologyCavePlan local, String regionalProfile, HydrologyCavePlan regional) {
|
||||
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : local.actions().entrySet()) {
|
||||
if (entry.getValue() != HydrologyCaveAction.SEAL_GUARD && regional.baselinePreconditions().containsKey(entry.getKey())
|
||||
&& (!profile.equals(regionalProfile) || regional.actions().get(entry.getKey()) != entry.getValue())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Map<Long, String> profiles(HydrologyRegionalNetwork regional) {
|
||||
Map<Long, String> profiles = new HashMap<>();
|
||||
for (RiverCourse course : regional.courses()) {
|
||||
profiles.put(course.id(), course.profileKey());
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
private static HydrologyDiagnosticCandidate diagnostic(RiverCourse course, long regionalId) {
|
||||
HydraulicSegment segment = course.segments().getFirst();
|
||||
return new HydrologyDiagnosticCandidate(HydrologyHash.mix(course.id(), regionalId, 0x52454750524f5445L),
|
||||
HydrologyCandidateKind.SOURCE, segment.type(), segment.start(), HydrologyCandidateRejection.CAVE_CONTAINMENT, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,637 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.util.project.noise.SimplexNoise;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
final class HydrologyRegionalRoute {
|
||||
private static final double SAMPLE_SPACING = 4D;
|
||||
private static final double SIMPLEX_COORDINATE_SCALE = 100D;
|
||||
private static final int MAXIMUM_RECEIVER_SAMPLES = 65536;
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final HydrologyRegionalTerrainRefiner terrainRefiner;
|
||||
private final HydrologyRegionalHydraulics hydraulics;
|
||||
private final SimplexNoise bends;
|
||||
private final SimplexNoise details;
|
||||
|
||||
HydrologyRegionalRoute(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
this.terrainRefiner = new HydrologyRegionalTerrainRefiner(planner);
|
||||
this.hydraulics = new HydrologyRegionalHydraulics(planner.settings);
|
||||
this.bends = new SimplexNoise(HydrologyHash.mix(planner.worldSeed, 0x52454742454e4453L));
|
||||
this.details = new SimplexNoise(HydrologyHash.mix(planner.worldSeed, 0x5245474445544149L));
|
||||
}
|
||||
|
||||
void clear() {
|
||||
terrainRefiner.clear();
|
||||
}
|
||||
|
||||
Attempt select(List<HydrologyPoint> guide, String profile, boolean coastal, HydrologyTerrainSampler receiver,
|
||||
CandidateAdmission admission) {
|
||||
HydrologyRegionalMorphology morphology = HydrologyRegionalMorphology.sample(guide, terrainRefiner::sample, planner.settings);
|
||||
Refinement topology = validateCorridor(guide, profile, coastal);
|
||||
OceanEntry resolved = topology.rejection() == null ? topology.oceanEntry() : null;
|
||||
if (resolved != null) {
|
||||
guide = topology.points();
|
||||
receiver = HydrologyOceanReceiver.forConnection(planner.settings, terrainRefiner::sample,
|
||||
resolved.landward(), resolved.receiving());
|
||||
}
|
||||
CandidateSelection selection = new CandidateSelection(admission, resolved);
|
||||
List<HydrologyPoint> repaired = terrainRefiner.refine(guide, profile, coastal, receiver);
|
||||
if (!repaired.isEmpty() && !repaired.equals(guide)) {
|
||||
Attempt accepted = selectCurve(repaired, profile, coastal, receiver, selection, morphology);
|
||||
if (accepted != null) {
|
||||
return accepted;
|
||||
}
|
||||
}
|
||||
Attempt accepted = selectCurve(guide, profile, coastal, receiver, selection, morphology);
|
||||
return accepted == null ? selection.failure : accepted;
|
||||
}
|
||||
|
||||
private Refinement retainReceivingEntry(Refinement refined, OceanEntry resolved) {
|
||||
if (resolved == null || refined.rejection() != null || refined.oceanEntry() != null) {
|
||||
return refined;
|
||||
}
|
||||
if (refined.points().getLast().distanceSquared2D(resolved.landward()) > 0L) {
|
||||
return new Refinement(List.of(), refined.points().getLast(),
|
||||
HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
|
||||
}
|
||||
double length = 0D;
|
||||
for (int index = 1; index < refined.points().size(); index++) {
|
||||
length += StrictMath.sqrt(refined.points().get(index - 1).distanceSquared2D(refined.points().get(index)));
|
||||
}
|
||||
if (length < planner.settings.routing().regional().minimumLength()) {
|
||||
return new Refinement(List.of(), refined.points().getLast(),
|
||||
HydrologyCandidateRejection.COURSE_TOO_SHORT, (int) StrictMath.floor(length), null);
|
||||
}
|
||||
return new Refinement(refined.points(), null, null, 0, resolved);
|
||||
}
|
||||
|
||||
private Attempt selectCurve(List<HydrologyPoint> guide, String profile, boolean coastal,
|
||||
HydrologyTerrainSampler receiver, CandidateSelection selection, HydrologyRegionalMorphology morphology) {
|
||||
for (double strength : new double[]{1D, 0.5D, 0D}) {
|
||||
List<HydrologyPoint> points = smooth(interpolate(guide, strength, morphology));
|
||||
Attempt accepted = selection.consider(validate(points, profile, coastal, receiver));
|
||||
if (accepted != null) {
|
||||
return accepted;
|
||||
}
|
||||
}
|
||||
List<HydrologyPoint> linear = interpolateLinear(guide);
|
||||
Refinement constrained = validate(linear, profile, coastal, receiver);
|
||||
Attempt accepted = selection.consider(constrained);
|
||||
if (accepted != null) {
|
||||
return accepted;
|
||||
}
|
||||
if (constrained.rejection() == HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED) {
|
||||
for (double radius : new double[]{16D, 32D, 64D, 128D, 256D}) {
|
||||
accepted = selection.consider(validateRounded(resample(roundBends(linear, radius)), profile, coastal, receiver));
|
||||
if (accepted != null) {
|
||||
return accepted;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<HydrologyPoint> interpolateLinear(List<HydrologyPoint> guide) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
for (int index = 0; index + 1 < guide.size(); index++) {
|
||||
HydrologyPoint start = guide.get(index);
|
||||
HydrologyPoint end = guide.get(index + 1);
|
||||
int steps = Math.max(1, (int) StrictMath.ceil(StrictMath.sqrt(start.distanceSquared2D(end)) / SAMPLE_SPACING));
|
||||
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = step / (double) steps;
|
||||
HydrologyPoint point = new HydrologyPoint(
|
||||
(int) StrictMath.round(start.x() + (end.x() - start.x()) * progress),
|
||||
(int) StrictMath.round(start.y() + (end.y() - start.y()) * progress),
|
||||
(int) StrictMath.round(start.z() + (end.z() - start.z()) * progress));
|
||||
if (points.isEmpty() || points.getLast().distanceSquared2D(point) > 0L) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
private List<CurvePoint> roundBends(List<HydrologyPoint> points, double radius) {
|
||||
double[] stations = new double[points.size()];
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
stations[index] = stations[index - 1] + StrictMath.sqrt(points.get(index - 1).distanceSquared2D(points.get(index)));
|
||||
}
|
||||
double length = stations[stations.length - 1];
|
||||
radius = Math.min(radius, length / 2D);
|
||||
HydrologyPoint start = points.getFirst();
|
||||
HydrologyPoint end = points.getLast();
|
||||
ArrayList<CurvePoint> rounded = new ArrayList<>(points.size());
|
||||
rounded.add(CurvePoint.of(points.getFirst()));
|
||||
int first = 0;
|
||||
for (int index = 1; index + 1 < points.size(); index++) {
|
||||
while (stations[first] < stations[index] - radius) {
|
||||
first++;
|
||||
}
|
||||
double weightedX = 0D;
|
||||
double weightedZ = 0D;
|
||||
double totalWeight = 0D;
|
||||
for (int sample = first; sample < points.size() && stations[sample] < stations[index] + radius; sample++) {
|
||||
double weight = 1D - StrictMath.abs(stations[sample] - stations[index]) / radius;
|
||||
weightedX += points.get(sample).x() * weight;
|
||||
weightedZ += points.get(sample).z() * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
for (int sample = 1; sample < points.size() && stations[sample] + stations[index] < radius; sample++) {
|
||||
double weight = 1D - (stations[sample] + stations[index]) / radius;
|
||||
weightedX += (2D * start.x() - points.get(sample).x()) * weight;
|
||||
weightedZ += (2D * start.z() - points.get(sample).z()) * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
for (int sample = points.size() - 2; sample >= 0 && 2D * length - stations[sample] - stations[index] < radius; sample--) {
|
||||
double weight = 1D - (2D * length - stations[sample] - stations[index]) / radius;
|
||||
weightedX += (2D * end.x() - points.get(sample).x()) * weight;
|
||||
weightedZ += (2D * end.z() - points.get(sample).z()) * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
HydrologyPoint point = points.get(index);
|
||||
CurvePoint smoothed = new CurvePoint(weightedX / totalWeight, point.y(), weightedZ / totalWeight);
|
||||
if (rounded.getLast().distance(smoothed) > 0D) {
|
||||
rounded.add(smoothed);
|
||||
}
|
||||
}
|
||||
if (rounded.getLast().distance(CurvePoint.of(points.getLast())) > 0D) {
|
||||
rounded.add(CurvePoint.of(points.getLast()));
|
||||
}
|
||||
return List.copyOf(rounded);
|
||||
}
|
||||
|
||||
private List<CurvePoint> resample(List<CurvePoint> points) {
|
||||
ArrayList<CurvePoint> sampled = new ArrayList<>(points.size());
|
||||
sampled.add(points.getFirst());
|
||||
double nextSample = SAMPLE_SPACING;
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
CurvePoint first = points.get(index - 1);
|
||||
CurvePoint second = points.get(index);
|
||||
double length = first.distance(second);
|
||||
if (length == 0D) {
|
||||
continue;
|
||||
}
|
||||
while (nextSample <= length) {
|
||||
double progress = nextSample / length;
|
||||
CurvePoint point = new CurvePoint(first.x() + (second.x() - first.x()) * progress,
|
||||
first.y() + (second.y() - first.y()) * progress,
|
||||
first.z() + (second.z() - first.z()) * progress);
|
||||
if (sampled.getLast().distance(point) > 0D) {
|
||||
sampled.add(point);
|
||||
}
|
||||
nextSample += SAMPLE_SPACING;
|
||||
}
|
||||
nextSample -= length;
|
||||
}
|
||||
if (sampled.getLast().distance(points.getLast()) > 0D) {
|
||||
sampled.add(points.getLast());
|
||||
}
|
||||
return List.copyOf(sampled);
|
||||
}
|
||||
|
||||
private List<HydrologyPoint> interpolate(List<HydrologyPoint> guide, double strength, HydrologyRegionalMorphology morphology) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
double spacing = planner.settings.routing().regional().sampleSpacing();
|
||||
HydrologyPlannerSettings.Meanders meanders = planner.settings.geometry().meanders();
|
||||
double scale = spacing / planner.settings.routing().sampleSpacing();
|
||||
double primaryWavelength = meanders.primaryWavelength() * scale * 5D;
|
||||
double detailWavelength = meanders.detailWavelength() * scale * 8D;
|
||||
for (int index = 0; index + 1 < guide.size(); index++) {
|
||||
HydrologyPoint before = guide.get(Math.max(0, index - 1));
|
||||
HydrologyPoint start = guide.get(index);
|
||||
HydrologyPoint end = guide.get(index + 1);
|
||||
HydrologyPoint after = guide.get(Math.min(guide.size() - 1, index + 2));
|
||||
int steps = Math.max(1, (int) StrictMath.ceil(StrictMath.sqrt(start.distanceSquared2D(end)) / SAMPLE_SPACING));
|
||||
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = step / (double) steps;
|
||||
double x = spline(before.x(), start.x(), end.x(), after.x(), progress);
|
||||
double z = spline(before.z(), start.z(), end.z(), after.z(), progress);
|
||||
double tangentX = end.x() - before.x() + (after.x() - start.x() - end.x() + before.x()) * progress;
|
||||
double tangentZ = end.z() - before.z() + (after.z() - start.z() - end.z() + before.z()) * progress;
|
||||
double tangentLength = Math.max(1D, StrictMath.hypot(tangentX, tangentZ));
|
||||
double endpointDistance = Math.min(index + progress, guide.size() - 1D - index - progress);
|
||||
double envelope = Math.min(1D, endpointDistance / 2D);
|
||||
double height = start.y() + (end.y() - start.y()) * progress;
|
||||
double primary = bends.noiseSigned(x * SIMPLEX_COORDINATE_SCALE / primaryWavelength,
|
||||
height * SIMPLEX_COORDINATE_SCALE / 96D, z * SIMPLEX_COORDINATE_SCALE / primaryWavelength);
|
||||
double detail = details.noiseSigned(x * SIMPLEX_COORDINATE_SCALE / detailWavelength,
|
||||
height * SIMPLEX_COORDINATE_SCALE / 48D, z * SIMPLEX_COORDINATE_SCALE / detailWavelength);
|
||||
double freedom = morphology.freedomAt(x, z);
|
||||
double broad = primary * meanders.primaryStrength() + detail * meanders.detailStrength() * 0.35D;
|
||||
double tight = detail * (meanders.primaryStrength() + meanders.detailStrength() * 0.35D);
|
||||
double signal = broad * freedom + tight * (1D - freedom);
|
||||
double displacement = Math.max(-1D, Math.min(1D, signal * 2D))
|
||||
* spacing * meanders.maximumOffsetRatio() * envelope * strength * (0.2D + 0.8D * freedom);
|
||||
int pointX = (int) StrictMath.round(x - tangentZ / tangentLength * displacement);
|
||||
int pointZ = (int) StrictMath.round(z + tangentX / tangentLength * displacement);
|
||||
if (points.isEmpty() || points.getLast().x() != pointX || points.getLast().z() != pointZ) {
|
||||
points.add(new HydrologyPoint(pointX, (int) StrictMath.round(height), pointZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
private List<HydrologyPoint> smooth(List<HydrologyPoint> points) {
|
||||
for (int pass = 0; pass < planner.settings.geometry().meanders().smoothingPasses(); pass++) {
|
||||
ArrayList<HydrologyPoint> result = new ArrayList<>(points.size());
|
||||
result.add(points.getFirst());
|
||||
for (int index = 1; index + 1 < points.size(); index++) {
|
||||
HydrologyPoint previous = points.get(index - 1);
|
||||
HydrologyPoint point = points.get(index);
|
||||
HydrologyPoint next = points.get(index + 1);
|
||||
HydrologyPoint smoothed = new HydrologyPoint(
|
||||
(int) StrictMath.round((previous.x() + point.x() * 2D + next.x()) / 4D), point.y(),
|
||||
(int) StrictMath.round((previous.z() + point.z() * 2D + next.z()) / 4D));
|
||||
if (smoothed.distanceSquared2D(result.getLast()) > 0L) {
|
||||
result.add(smoothed);
|
||||
}
|
||||
}
|
||||
if (points.getLast().distanceSquared2D(result.getLast()) > 0L) {
|
||||
result.add(points.getLast());
|
||||
}
|
||||
points = result;
|
||||
}
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
private Refinement validate(List<HydrologyPoint> points, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
if (points.size() < 2) {
|
||||
return new Refinement(List.of(), points.isEmpty() ? new HydrologyPoint(0, 0, 0) : points.getFirst(),
|
||||
HydrologyCandidateRejection.COURSE_TOO_SHORT, points.size(), null);
|
||||
}
|
||||
Refinement corridor = validateHydraulics(validateCorridor(points, profile, coastal), profile, coastal, receiver);
|
||||
if (corridor.rejection() != null) {
|
||||
return corridor;
|
||||
}
|
||||
points = corridor.points();
|
||||
for (int index = 1; index + 1 < points.size(); index++) {
|
||||
double turn = turn(points, index);
|
||||
if (turn > planner.settings.geometry().meanders().maximumTurnDegrees()) {
|
||||
return new Refinement(List.of(), points.get(index), HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED,
|
||||
(int) StrictMath.ceil(turn), null);
|
||||
}
|
||||
}
|
||||
return corridor;
|
||||
}
|
||||
|
||||
private Refinement validateRounded(List<CurvePoint> curve, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>(curve.size());
|
||||
ArrayList<CurvePoint> distinct = new ArrayList<>(curve.size());
|
||||
for (CurvePoint point : curve) {
|
||||
HydrologyPoint rounded = point.rounded();
|
||||
if (points.isEmpty() || points.getLast().distanceSquared2D(rounded) > 0L) {
|
||||
points.add(rounded);
|
||||
distinct.add(point);
|
||||
}
|
||||
}
|
||||
if (points.size() < 2) {
|
||||
return new Refinement(List.of(), curve.getFirst().rounded(), HydrologyCandidateRejection.COURSE_TOO_SHORT, points.size(), null);
|
||||
}
|
||||
Refinement corridor = validateHydraulics(validateCorridor(points, profile, coastal), profile, coastal, receiver);
|
||||
if (corridor.rejection() != null) {
|
||||
return corridor;
|
||||
}
|
||||
if (corridor.oceanEntry() != null) {
|
||||
int last = corridor.points().size() - 1;
|
||||
HydrologyPoint end = corridor.points().getLast();
|
||||
if (points.get(last).distanceSquared2D(end) == 0L) {
|
||||
distinct = new ArrayList<>(distinct.subList(0, last + 1));
|
||||
} else {
|
||||
CurvePoint first = distinct.get(last - 1);
|
||||
CurvePoint second = distinct.get(last);
|
||||
double progress = StrictMath.sqrt(points.get(last - 1).distanceSquared2D(end)
|
||||
/ (double) points.get(last - 1).distanceSquared2D(points.get(last)));
|
||||
distinct = new ArrayList<>(distinct.subList(0, last));
|
||||
distinct.add(new CurvePoint(first.x() + (second.x() - first.x()) * progress,
|
||||
first.y() + (second.y() - first.y()) * progress,
|
||||
first.z() + (second.z() - first.z()) * progress));
|
||||
}
|
||||
}
|
||||
for (int index = 1; index + 1 < distinct.size(); index++) {
|
||||
CurvePoint previous = distinct.get(Math.max(0, index - 4));
|
||||
CurvePoint point = distinct.get(index);
|
||||
CurvePoint next = distinct.get(Math.min(distinct.size() - 1, index + 4));
|
||||
double firstX = point.x() - previous.x();
|
||||
double firstZ = point.z() - previous.z();
|
||||
double nextX = next.x() - point.x();
|
||||
double nextZ = next.z() - point.z();
|
||||
double turn = StrictMath.toDegrees(StrictMath.abs(StrictMath.atan2(firstX * nextZ - firstZ * nextX,
|
||||
firstX * nextX + firstZ * nextZ)));
|
||||
if (turn > planner.settings.geometry().meanders().maximumTurnDegrees()) {
|
||||
return new Refinement(List.of(), point.rounded(), HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED,
|
||||
(int) StrictMath.ceil(turn), null);
|
||||
}
|
||||
}
|
||||
return corridor;
|
||||
}
|
||||
|
||||
private Refinement validateCorridor(List<HydrologyPoint> points, String profile, boolean coastal) {
|
||||
Set<Long> visited = new HashSet<>();
|
||||
HydrologyTerrainSample previousTerrain = null;
|
||||
HydrologyPoint previousPoint = null;
|
||||
double length = 0D;
|
||||
for (int index = 0; index + 1 < points.size(); index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint second = points.get(index + 1);
|
||||
int steps = Math.max(Math.abs(second.x() - first.x()), Math.abs(second.z() - first.z()));
|
||||
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(first.x() + (second.x() - first.x()) * progress);
|
||||
int z = (int) StrictMath.round(first.z() + (second.z() - first.z()) * progress);
|
||||
HydrologyTerrainSample terrain = terrainRefiner.sample(x, z);
|
||||
HydrologyPoint failure = new HydrologyPoint(x, terrain == null ? first.y() : terrain.naturalHeight(), z);
|
||||
if (!visited.add(RiverFootprint.pack(x, z))) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, 2, null);
|
||||
}
|
||||
double completedLength = length
|
||||
+ (previousPoint == null ? 0D : StrictMath.sqrt(first.distanceSquared2D(previousPoint)));
|
||||
if (terrain != null && terrain.ocean() && previousPoint != null
|
||||
&& receivingEntry(previousTerrain, terrain, profile, coastal, completedLength)) {
|
||||
ArrayList<HydrologyPoint> prefix = new ArrayList<>(points.subList(0, index + 1));
|
||||
if (prefix.getLast().distanceSquared2D(previousPoint) > 0L) {
|
||||
prefix.add(previousPoint);
|
||||
}
|
||||
int seaLevel = planner.settings.seaLevel();
|
||||
OceanEntry entry = new OceanEntry(new HydrologyPoint(previousPoint.x(), seaLevel, previousPoint.z()),
|
||||
new HydrologyPoint(x, seaLevel, z));
|
||||
return new Refinement(List.copyOf(prefix), null, null, 0, entry);
|
||||
}
|
||||
if (terrain != null && !terrain.ocean() && terrain.naturalHeight() < planner.settings.seaLevel()
|
||||
&& previousPoint != null && completedLength >= planner.settings.routing().regional().minimumLength()
|
||||
&& completedLength <= planner.settings.routing().maximumRouteLength()) {
|
||||
return receivingTail(points, new Shore(index, step, previousPoint, previousTerrain), profile, coastal);
|
||||
}
|
||||
double traversedLength = length + StrictMath.sqrt(first.distanceSquared2D(failure));
|
||||
if (traversedLength > planner.settings.routing().maximumRouteLength()) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.ROUTE_LIMIT,
|
||||
(int) StrictMath.ceil(traversedLength), null);
|
||||
}
|
||||
if (terrain == null || !terrain.transitAllowed() || !terrain.preferredProfileKeys().contains(profile)) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.POLICY_EXCLUDED, 0, null);
|
||||
}
|
||||
if (terrain.ocean() || terrain.naturalHeight() < planner.settings.seaLevel()) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, 1, null);
|
||||
}
|
||||
if (previousTerrain != null && (!previousTerrain.drainsInto(terrain)
|
||||
|| coastal && !terrain.drainsInto(previousTerrain))) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.CONFINED_NO_OUTLET, 0, null);
|
||||
}
|
||||
if (coastal && terrain.naturalHeight() - planner.settings.seaLevel()
|
||||
+ planner.settings.surface().minimumDepth()
|
||||
> planner.settings.routing().regional().maximumCoastalIncision()) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.SURFACE_BANK_BUDGET,
|
||||
terrain.naturalHeight() - planner.settings.seaLevel() + planner.settings.surface().minimumDepth(), null);
|
||||
}
|
||||
previousTerrain = terrain;
|
||||
previousPoint = failure;
|
||||
}
|
||||
length += StrictMath.sqrt(first.distanceSquared2D(second));
|
||||
}
|
||||
return new Refinement(points, null, null, 0, null);
|
||||
}
|
||||
|
||||
private Refinement receivingTail(List<HydrologyPoint> points, Shore shore, String profile, boolean coastal) {
|
||||
HydrologyPoint previousPoint = shore.point();
|
||||
HydrologyTerrainSample previous = shore.terrain();
|
||||
HydrologyPoint firstWet = null;
|
||||
int samples = 0;
|
||||
for (int index = shore.segment(); index + 1 < points.size(); index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint last = points.get(index + 1);
|
||||
int steps = Math.max(Math.abs(last.x() - first.x()), Math.abs(last.z() - first.z()));
|
||||
for (int step = index == shore.segment() ? shore.step() : 1; step <= steps; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(first.x() + (last.x() - first.x()) * progress);
|
||||
int z = (int) StrictMath.round(first.z() + (last.z() - first.z()) * progress);
|
||||
if (++samples > MAXIMUM_RECEIVER_SAMPLES) {
|
||||
return new Refinement(List.of(), previousPoint, HydrologyCandidateRejection.ROUTE_LIMIT, samples, null);
|
||||
}
|
||||
HydrologyTerrainSample terrain = terrainRefiner.sample(x, z);
|
||||
HydrologyPoint point = new HydrologyPoint(x, terrain == null ? first.y() : terrain.naturalHeight(), z);
|
||||
if (terrain == null || terrain.naturalHeight() >= planner.settings.seaLevel()) {
|
||||
return new Refinement(List.of(), point, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, 1, null);
|
||||
}
|
||||
if (!terrain.preferredProfileKeys().contains(profile)) {
|
||||
return new Refinement(List.of(), point, HydrologyCandidateRejection.POLICY_EXCLUDED, 0, null);
|
||||
}
|
||||
if (!previous.drainsInto(terrain) || coastal && !terrain.drainsInto(previous)) {
|
||||
return new Refinement(List.of(), point, HydrologyCandidateRejection.CONFINED_NO_OUTLET, 0, null);
|
||||
}
|
||||
if (previousPoint.x() != x && previousPoint.z() != z) {
|
||||
samples += 2;
|
||||
if (samples > MAXIMUM_RECEIVER_SAMPLES
|
||||
|| !wetBridge(previousPoint.x(), z, previous, terrain, profile, coastal)
|
||||
&& !wetBridge(x, previousPoint.z(), previous, terrain, profile, coastal)) {
|
||||
return new Refinement(List.of(), point, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
|
||||
}
|
||||
}
|
||||
if (firstWet == null) {
|
||||
firstWet = point;
|
||||
}
|
||||
if (terrain.ocean()) {
|
||||
return completedReceivingTail(points, shore, firstWet, point, profile, coastal);
|
||||
}
|
||||
previousPoint = point;
|
||||
previous = terrain;
|
||||
}
|
||||
}
|
||||
return new Refinement(List.of(), previousPoint, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
|
||||
}
|
||||
|
||||
private boolean wetBridge(int x, int z, HydrologyTerrainSample previous, HydrologyTerrainSample next,
|
||||
String profile, boolean coastal) {
|
||||
HydrologyTerrainSample bridge = terrainRefiner.sample(x, z);
|
||||
return bridge != null && bridge.naturalHeight() < planner.settings.seaLevel()
|
||||
&& bridge.preferredProfileKeys().contains(profile) && previous.drainsInto(bridge) && bridge.drainsInto(next)
|
||||
&& (!coastal || next.drainsInto(bridge) && bridge.drainsInto(previous));
|
||||
}
|
||||
|
||||
private Refinement completedReceivingTail(List<HydrologyPoint> points, Shore shore,
|
||||
HydrologyPoint firstWet, HydrologyPoint receiving, String profile, boolean coastal) {
|
||||
int seaLevel = planner.settings.seaLevel();
|
||||
HydrologyPoint landward = new HydrologyPoint(shore.point().x(), seaLevel, shore.point().z());
|
||||
receiving = new HydrologyPoint(receiving.x(), seaLevel, receiving.z());
|
||||
HydrologyTerrainSampler proof = HydrologyOceanReceiver.forConnection(planner.settings,
|
||||
terrainRefiner::sample, landward, receiving);
|
||||
if (!shore.terrain().outletAllowed() || !proof.receivingWater(firstWet.x(), firstWet.z(), seaLevel)
|
||||
|| !receivingChordAllowed(shore, receiving, profile, coastal)) {
|
||||
return new Refinement(List.of(), firstWet, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
|
||||
}
|
||||
ArrayList<HydrologyPoint> prefix = new ArrayList<>(points.subList(0, shore.segment() + 1));
|
||||
if (prefix.getLast().distanceSquared2D(shore.point()) > 0L) {
|
||||
prefix.add(shore.point());
|
||||
}
|
||||
return new Refinement(List.copyOf(prefix), null, null, 0, new OceanEntry(landward, receiving));
|
||||
}
|
||||
|
||||
private boolean receivingChordAllowed(Shore shore, HydrologyPoint end, String profile, boolean coastal) {
|
||||
HydrologyPoint start = shore.point();
|
||||
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
|
||||
if (steps > MAXIMUM_RECEIVER_SAMPLES) {
|
||||
return false;
|
||||
}
|
||||
HydrologyTerrainSample previous = shore.terrain();
|
||||
HydrologyPoint previousPoint = start;
|
||||
for (int step = 1; step <= steps; step++) {
|
||||
double progress = step / (double) steps;
|
||||
int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress);
|
||||
int z = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress);
|
||||
HydrologyTerrainSample terrain = terrainRefiner.sample(x, z);
|
||||
if (terrain == null || terrain.naturalHeight() >= planner.settings.seaLevel()
|
||||
|| !terrain.preferredProfileKeys().contains(profile) || !previous.drainsInto(terrain)
|
||||
|| coastal && !terrain.drainsInto(previous)) {
|
||||
return false;
|
||||
}
|
||||
if (previousPoint.x() != x && previousPoint.z() != z
|
||||
&& !wetBridge(previousPoint.x(), z, previous, terrain, profile, coastal)
|
||||
&& !wetBridge(x, previousPoint.z(), previous, terrain, profile, coastal)) {
|
||||
return false;
|
||||
}
|
||||
previous = terrain;
|
||||
previousPoint = new HydrologyPoint(x, terrain.naturalHeight(), z);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Refinement validateHydraulics(Refinement corridor, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
if (corridor.rejection() != null || coastal) {
|
||||
return corridor;
|
||||
}
|
||||
List<HydrologyPoint> points = corridor.points();
|
||||
int count = 1;
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
count += Math.max(Math.abs(points.get(index).x() - points.get(index - 1).x()),
|
||||
Math.abs(points.get(index).z() - points.get(index - 1).z()));
|
||||
}
|
||||
HydrologyPlannerSettings.Inlet inlet = planner.settings.surface().banks().inlet();
|
||||
boolean oceanMouth = corridor.oceanEntry() != null || terrainRefiner.receivingTerminal(points.getLast(), profile, false, receiver);
|
||||
int reach = oceanMouth ? Math.min(inlet.length(), (int) StrictMath.floor(count * inlet.courseFraction())) : 0;
|
||||
int rampStart = count - reach - reach / 2;
|
||||
int availableHead = Integer.MAX_VALUE;
|
||||
int station = 0;
|
||||
HydrologyTerrainSampler bankSampler = terrainRefiner::sample;
|
||||
for (int index = 0; index + 1 < points.size(); index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint second = points.get(index + 1);
|
||||
int steps = Math.max(Math.abs(second.x() - first.x()), Math.abs(second.z() - first.z()));
|
||||
double distance = StrictMath.sqrt(first.distanceSquared2D(second));
|
||||
double tangentX = distance == 0D ? 1D : (second.x() - first.x()) / distance;
|
||||
double tangentZ = distance == 0D ? 0D : (second.z() - first.z()) / distance;
|
||||
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(first.x() + (second.x() - first.x()) * progress);
|
||||
int z = (int) StrictMath.round(first.z() + (second.z() - first.z()) * progress);
|
||||
HydrologyTerrainSample terrain = terrainRefiner.sample(x, z);
|
||||
availableHead = hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
|
||||
x, z, tangentX, tangentZ, terrain, availableHead), bankSampler);
|
||||
int requiredHead = station >= rampStart ? hydraulics.inletMinimumHead(terrain) : hydraulics.minimumHead(terrain);
|
||||
if (requiredHead > availableHead) {
|
||||
return new Refinement(List.of(), new HydrologyPoint(x, terrain.naturalHeight(), z),
|
||||
HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, requiredHead - availableHead, null);
|
||||
}
|
||||
station++;
|
||||
}
|
||||
}
|
||||
return corridor;
|
||||
}
|
||||
|
||||
private boolean receivingEntry(HydrologyTerrainSample land, HydrologyTerrainSample ocean,
|
||||
String profile, boolean coastal, double length) {
|
||||
return length >= planner.settings.routing().regional().minimumLength()
|
||||
&& ocean.naturalHeight() < planner.settings.seaLevel()
|
||||
&& ocean.preferredProfileKeys().contains(profile)
|
||||
&& land.outletAllowed() && land.drainsInto(ocean) && (!coastal || ocean.drainsInto(land));
|
||||
}
|
||||
|
||||
private double turn(List<HydrologyPoint> points, int index) {
|
||||
HydrologyPoint previous = points.get(Math.max(0, index - 4));
|
||||
HydrologyPoint point = points.get(index);
|
||||
HydrologyPoint next = points.get(Math.min(points.size() - 1, index + 4));
|
||||
double firstX = point.x() - previous.x();
|
||||
double firstZ = point.z() - previous.z();
|
||||
double nextX = next.x() - point.x();
|
||||
double nextZ = next.z() - point.z();
|
||||
return StrictMath.toDegrees(StrictMath.abs(StrictMath.atan2(firstX * nextZ - firstZ * nextX,
|
||||
firstX * nextX + firstZ * nextZ)));
|
||||
}
|
||||
|
||||
private static double spline(double before, double start, double end, double after, double progress) {
|
||||
return 0.5D * ((2D * start) + (-before + end) * progress
|
||||
+ (2D * before - 5D * start + 4D * end - after) * progress * progress
|
||||
+ (-before + 3D * start - 3D * end + after) * progress * progress * progress);
|
||||
}
|
||||
|
||||
private final class CandidateSelection {
|
||||
private final CandidateAdmission admission;
|
||||
private final OceanEntry resolved;
|
||||
private final Set<Refinement> attempted = new HashSet<>();
|
||||
private Attempt failure;
|
||||
private boolean finalFailure;
|
||||
|
||||
private CandidateSelection(CandidateAdmission admission, OceanEntry resolved) {
|
||||
this.admission = admission;
|
||||
this.resolved = resolved;
|
||||
}
|
||||
|
||||
private Attempt consider(Refinement candidate) {
|
||||
candidate = retainReceivingEntry(candidate, resolved);
|
||||
if (candidate.rejection() != null) {
|
||||
if (!finalFailure) {
|
||||
failure = new Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!attempted.add(candidate)) {
|
||||
return null;
|
||||
}
|
||||
Attempt result = admission.evaluate(candidate);
|
||||
if (result.accepted()) {
|
||||
return result;
|
||||
}
|
||||
failure = result;
|
||||
finalFailure = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface CandidateAdmission {
|
||||
Attempt evaluate(Refinement candidate);
|
||||
}
|
||||
|
||||
record Attempt(Refinement refinement, HydrologyRegionalNetwork network, HydrologyDiagnosticCandidate rejection) {
|
||||
boolean accepted() {
|
||||
return refinement.rejection() == null && rejection == null;
|
||||
}
|
||||
}
|
||||
|
||||
private record Shore(int segment, int step, HydrologyPoint point, HydrologyTerrainSample terrain) {
|
||||
}
|
||||
|
||||
private record CurvePoint(double x, double y, double z) {
|
||||
private static CurvePoint of(HydrologyPoint point) {
|
||||
return new CurvePoint(point.x(), point.y(), point.z());
|
||||
}
|
||||
|
||||
private double distance(CurvePoint other) {
|
||||
return StrictMath.hypot(x - other.x(), z - other.z());
|
||||
}
|
||||
|
||||
private HydrologyPoint rounded() {
|
||||
return new HydrologyPoint((int) StrictMath.round(x), (int) StrictMath.round(y), (int) StrictMath.round(z));
|
||||
}
|
||||
}
|
||||
|
||||
record OceanEntry(HydrologyPoint landward, HydrologyPoint receiving) {
|
||||
}
|
||||
|
||||
record Refinement(List<HydrologyPoint> points, HydrologyPoint failure,
|
||||
HydrologyCandidateRejection rejection, int detail, OceanEntry oceanEntry) {
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
|
||||
final class HydrologyRegionalSourceTrials {
|
||||
private static final int LENGTH_BANDS = 4;
|
||||
|
||||
private final HydrologySampledGrid grid;
|
||||
private final List<Catchment> catchments;
|
||||
private int cursor;
|
||||
|
||||
HydrologyRegionalSourceTrials(HydrologySampledGrid grid, HydrologyRegionalGraph.Tree tree, List<Integer> sources) {
|
||||
this.grid = grid;
|
||||
this.catchments = new ArrayList<>(tree.outlets().size());
|
||||
ArrayList<Integer> sorted = new ArrayList<>(sources);
|
||||
sorted.sort(Comparator.comparingDouble((Integer source) -> tree.length(source)).reversed()
|
||||
.thenComparingLong(source -> grid.node(source).id()));
|
||||
for (int root = 0; root < tree.outlets().size(); root++) {
|
||||
Catchment catchment = new Catchment(tree.outlets().get(root).outlet().id());
|
||||
double minimum = Double.POSITIVE_INFINITY;
|
||||
double maximum = 0D;
|
||||
for (int source : sorted) {
|
||||
if (tree.root(source) == root) {
|
||||
minimum = Math.min(minimum, tree.length(source));
|
||||
maximum = Math.max(maximum, tree.length(source));
|
||||
}
|
||||
}
|
||||
if (maximum == 0D) {
|
||||
continue;
|
||||
}
|
||||
double range = Math.max(1D, maximum - minimum);
|
||||
for (int source : sorted) {
|
||||
if (tree.root(source) == root) {
|
||||
int band = Math.min(LENGTH_BANDS - 1,
|
||||
(int) ((maximum - tree.length(source)) * LENGTH_BANDS / range));
|
||||
catchment.bands.get(band).add(source);
|
||||
}
|
||||
}
|
||||
catchments.add(catchment);
|
||||
}
|
||||
}
|
||||
|
||||
OptionalInt next(Set<Long> usedOutlets) {
|
||||
for (int checked = 0; checked < catchments.size(); checked++) {
|
||||
Catchment catchment = catchments.get(cursor++ % catchments.size());
|
||||
if (usedOutlets.contains(catchment.outlet)) {
|
||||
continue;
|
||||
}
|
||||
for (int band = 0; band < LENGTH_BANDS; band++) {
|
||||
List<Integer> candidates = catchment.bands.get(catchment.cursor++ % LENGTH_BANDS);
|
||||
if (!candidates.isEmpty()) {
|
||||
int source = candidates.remove(farthest(candidates, catchment.attempted));
|
||||
catchment.attempted.add(grid.node(source).naturalPoint());
|
||||
return OptionalInt.of(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
private int farthest(List<Integer> candidates, List<HydrologyPoint> attempted) {
|
||||
int selected = 0;
|
||||
long separation = -1L;
|
||||
for (int index = 0; index < candidates.size(); index++) {
|
||||
HydrologyPoint point = grid.node(candidates.get(index)).naturalPoint();
|
||||
long nearest = Long.MAX_VALUE;
|
||||
for (HydrologyPoint previous : attempted) {
|
||||
nearest = Math.min(nearest, point.distanceSquared2D(previous));
|
||||
}
|
||||
if (nearest > separation) {
|
||||
selected = index;
|
||||
separation = nearest;
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static final class Catchment {
|
||||
private final long outlet;
|
||||
private final List<List<Integer>> bands;
|
||||
private final List<HydrologyPoint> attempted;
|
||||
private int cursor;
|
||||
|
||||
private Catchment(long outlet) {
|
||||
this.outlet = outlet;
|
||||
this.bands = new ArrayList<>(LENGTH_BANDS);
|
||||
this.attempted = new ArrayList<>();
|
||||
for (int band = 0; band < LENGTH_BANDS; band++) {
|
||||
bands.add(new ArrayList<>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
|
||||
final class HydrologyRegionalTerrainRefiner {
|
||||
private static final int GRID_SPACING = 16;
|
||||
private static final int MAXIMUM_EXPANSIONS = 4096;
|
||||
private static final int MAXIMUM_SAMPLES = 65536;
|
||||
private static final int MAXIMUM_SKIPPED_WAYPOINTS = 2;
|
||||
private static final int[][] NEIGHBOURS = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, 1}, {-1, 1}, {-1, -1}, {1, -1}};
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final Cache<Edge, Reach> edges;
|
||||
private final HydrologyRegionalHydraulics hydraulics;
|
||||
private final Cache<Long, Terrain> terrain;
|
||||
|
||||
HydrologyRegionalTerrainRefiner(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
this.hydraulics = new HydrologyRegionalHydraulics(planner.settings);
|
||||
this.edges = Caffeine.newBuilder().maximumSize(128).build();
|
||||
this.terrain = Caffeine.newBuilder().maximumSize(MAXIMUM_SAMPLES).build();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
edges.invalidateAll();
|
||||
terrain.invalidateAll();
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sample(int x, int z) {
|
||||
return terrain.get(RiverFootprint.pack(x, z), key -> new Terrain(planner.naturalSampler == null
|
||||
? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z))).sample();
|
||||
}
|
||||
|
||||
List<HydrologyPoint> refine(List<HydrologyPoint> guide, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
if (guide.size() < 2) {
|
||||
return List.of();
|
||||
}
|
||||
double maximumLength = planner.settings.routing().maximumRouteLength();
|
||||
double remaining = 0D;
|
||||
for (int index = 1; index < guide.size(); index++) {
|
||||
remaining += distance(guide.get(index - 1), guide.get(index));
|
||||
if (remaining > maximumLength) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
Terminal terminal = terminal(guide.getLast(), profile, coastal, receiver);
|
||||
int[] requiredHead = requiredHeads(guide, terminal, MAXIMUM_SKIPPED_WAYPOINTS + 1);
|
||||
if (requiredHead.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
int availableHead = Integer.MAX_VALUE;
|
||||
double length = 0D;
|
||||
ArrayList<HydrologyPoint> refined = new ArrayList<>();
|
||||
refined.add(guide.getFirst());
|
||||
Map<Long, Integer> stations = new HashMap<>();
|
||||
stations.put(RiverFootprint.pack(guide.getFirst().x(), guide.getFirst().z()), 0);
|
||||
for (int index = 1; index < guide.size(); index++) {
|
||||
int start = index - 1;
|
||||
Reach reach = null;
|
||||
int selected = index;
|
||||
for (int candidate = index; candidate < guide.size() && candidate <= index + MAXIMUM_SKIPPED_WAYPOINTS; candidate++) {
|
||||
Edge edge = new Edge(guide.get(start), guide.get(candidate), profile, coastal,
|
||||
requiredHead[candidate], availableHead, terminal);
|
||||
reach = edges.get(edge, this::route);
|
||||
if (!reach.points().isEmpty()) {
|
||||
selected = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (reach.points().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<HydrologyPoint> points = reach.points();
|
||||
availableHead = reach.availableHead();
|
||||
for (int skipped = start; skipped < selected; skipped++) {
|
||||
remaining -= distance(guide.get(skipped), guide.get(skipped + 1));
|
||||
}
|
||||
index = selected;
|
||||
for (int point = 1; point < points.size(); point++) {
|
||||
HydrologyPoint next = points.get(point);
|
||||
long key = RiverFootprint.pack(next.x(), next.z());
|
||||
Integer previous = stations.get(key);
|
||||
if (previous != null) {
|
||||
while (refined.size() > previous + 1) {
|
||||
HydrologyPoint removed = refined.removeLast();
|
||||
stations.remove(RiverFootprint.pack(removed.x(), removed.z()));
|
||||
length -= distance(refined.getLast(), removed);
|
||||
}
|
||||
} else {
|
||||
length += distance(refined.getLast(), next);
|
||||
stations.put(key, refined.size());
|
||||
refined.add(next);
|
||||
}
|
||||
if (length + Math.max(0D, remaining) > maximumLength) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
return simplify(refined, profile, coastal, terminal);
|
||||
}
|
||||
|
||||
boolean receivingTerminal(HydrologyPoint point, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
HydrologyTerrainSample land = sample(point.x(), point.z());
|
||||
if (land == null || land.ocean() || land.naturalHeight() < planner.settings.seaLevel() || !land.outletAllowed()) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < 4; index++) {
|
||||
int[] offset = NEIGHBOURS[index];
|
||||
HydrologyTerrainSample ocean = sample(point.x() + offset[0], point.z() + offset[1]);
|
||||
if (ocean != null && receiver.receivingWater(point.x() + offset[0], point.z() + offset[1], planner.settings.seaLevel())
|
||||
&& ocean.preferredProfileKeys().contains(profile) && land.drainsInto(ocean)
|
||||
&& (!coastal || ocean.drainsInto(land))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Terminal terminal(HydrologyPoint point, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
int inlet = receivingTerminal(point, profile, coastal, receiver) ? planner.settings.surface().banks().inlet().length() : 0;
|
||||
return new Terminal(point, inlet + inlet / 2);
|
||||
}
|
||||
|
||||
private int minimumHead(HydrologyTerrainSample terrain, int x, int z, Terminal terminal) {
|
||||
boolean inlet = terminal.possibleStations() > 0
|
||||
&& Math.max(Math.abs(x - terminal.landward().x()), Math.abs(z - terminal.landward().z())) < terminal.possibleStations();
|
||||
return inlet ? hydraulics.inletMinimumHead(terrain) : hydraulics.minimumHead(terrain);
|
||||
}
|
||||
|
||||
private int[] requiredHeads(List<HydrologyPoint> points, Terminal terminal, int maximumAdvance) {
|
||||
int[] heads = new int[points.size()];
|
||||
for (int index = points.size() - 1; index >= 0; index--) {
|
||||
HydrologyPoint point = points.get(index);
|
||||
HydrologyTerrainSample terrain = sample(point.x(), point.z());
|
||||
if (terrain == null) {
|
||||
return new int[0];
|
||||
}
|
||||
int required = planner.settings.seaLevel();
|
||||
if (index + 1 < points.size()) {
|
||||
required = Integer.MAX_VALUE;
|
||||
for (int downstream = index + 1; downstream < points.size() && downstream <= index + maximumAdvance; downstream++) {
|
||||
required = Math.min(required, heads[downstream]);
|
||||
}
|
||||
}
|
||||
heads[index] = Math.max(required, minimumHead(terrain, point.x(), point.z(), terminal));
|
||||
}
|
||||
return heads;
|
||||
}
|
||||
|
||||
private List<HydrologyPoint> simplify(List<HydrologyPoint> points, String profile, boolean coastal, Terminal terminal) {
|
||||
double maximumReach = Math.min(512D, planner.settings.routing().regional().sampleSpacing() * 2D);
|
||||
Map<Long, HydrologyTerrainSample> samples = new HashMap<>();
|
||||
ArrayList<HydrologyPoint> simplified = new ArrayList<>();
|
||||
int current = 0;
|
||||
int[] requiredHead = requiredHeads(points, terminal, 1);
|
||||
if (requiredHead.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
int availableHead = Integer.MAX_VALUE;
|
||||
simplified.add(points.getFirst());
|
||||
while (current + 1 < points.size()) {
|
||||
int farthest = current + 1;
|
||||
double reach = distance(points.get(current), points.get(farthest));
|
||||
while (farthest + 1 < points.size()) {
|
||||
double next = distance(points.get(farthest), points.get(farthest + 1));
|
||||
if (reach + next > maximumReach) {
|
||||
break;
|
||||
}
|
||||
reach += next;
|
||||
farthest++;
|
||||
}
|
||||
int selected = current + 1;
|
||||
Traversal selectedTraversal = traverse(points.get(current), points.get(selected),
|
||||
new Edge(points.get(current), points.get(selected), profile, coastal, requiredHead[selected], availableHead, terminal),
|
||||
samples, availableHead);
|
||||
for (int candidate = farthest; candidate > current + 1; candidate--) {
|
||||
Edge shortcut = new Edge(points.get(current), points.get(candidate), profile, coastal,
|
||||
requiredHead[candidate], availableHead, terminal);
|
||||
Traversal traversal = traverse(shortcut.start(), shortcut.end(), shortcut, samples, availableHead);
|
||||
if (traversal.cost() == 0D) {
|
||||
selected = candidate;
|
||||
selectedTraversal = traversal;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Double.isFinite(selectedTraversal.cost())) {
|
||||
return List.copyOf(points);
|
||||
}
|
||||
availableHead = selectedTraversal.availableHead();
|
||||
current = selected;
|
||||
simplified.add(points.get(current));
|
||||
}
|
||||
return List.copyOf(simplified);
|
||||
}
|
||||
|
||||
private Reach route(Edge edge) {
|
||||
Map<Long, HydrologyTerrainSample> samples = new HashMap<>();
|
||||
Traversal direct = traverse(edge.start(), edge.end(), edge, samples, edge.maximumHead());
|
||||
if (direct.cost() == 0D) {
|
||||
return new Reach(List.of(edge.start(), edge.end()), direct.availableHead());
|
||||
}
|
||||
int margin = planner.settings.routing().regional().sampleSpacing();
|
||||
int minimumX = Math.min(edge.start().x(), edge.end().x()) - margin;
|
||||
int maximumX = Math.max(edge.start().x(), edge.end().x()) + margin;
|
||||
int minimumZ = Math.min(edge.start().z(), edge.end().z()) - margin;
|
||||
int maximumZ = Math.max(edge.start().z(), edge.end().z()) + margin;
|
||||
Map<Long, List<Node>> visited = new HashMap<>();
|
||||
PriorityQueue<Pending> pending = new PriorityQueue<>(Comparator.comparingDouble(Pending::estimate)
|
||||
.thenComparingLong(Pending::key)
|
||||
.thenComparing(Comparator.comparingInt(Pending::availableHead).reversed()));
|
||||
long firstKey = RiverFootprint.pack(edge.start().x(), edge.start().z());
|
||||
long lastKey = RiverFootprint.pack(edge.end().x(), edge.end().z());
|
||||
Search search = new Search(edge, samples, visited, pending);
|
||||
Node first = new Node(edge.start(), 0D, edge.maximumHead(), null);
|
||||
ArrayList<Node> firstLabels = new ArrayList<>(1);
|
||||
firstLabels.add(first);
|
||||
visited.put(firstKey, firstLabels);
|
||||
pending.add(new Pending(firstKey, first, distance(edge.start(), edge.end())));
|
||||
for (int expanded = 0; !pending.isEmpty() && expanded < MAXIMUM_EXPANSIONS; expanded++) {
|
||||
Pending next = pending.remove();
|
||||
Node current = next.node();
|
||||
if (!retained(visited.get(next.key()), current)) {
|
||||
continue;
|
||||
}
|
||||
if (next.key() == lastKey) {
|
||||
return new Reach(path(current), current.availableHead());
|
||||
}
|
||||
if (distance(current.point(), edge.end()) <= GRID_SPACING * StrictMath.sqrt(2D)) {
|
||||
offer(search, current, edge.end());
|
||||
}
|
||||
for (int[] offset : NEIGHBOURS) {
|
||||
int x = current.point().x() + offset[0] * GRID_SPACING;
|
||||
int z = current.point().z() + offset[1] * GRID_SPACING;
|
||||
if (x < minimumX || x > maximumX || z < minimumZ || z > maximumZ) {
|
||||
continue;
|
||||
}
|
||||
offer(search, current, new HydrologyPoint(x, current.point().y(), z));
|
||||
}
|
||||
}
|
||||
return new Reach(List.of(), edge.maximumHead());
|
||||
}
|
||||
|
||||
private void offer(Search search, Node current, HydrologyPoint target) {
|
||||
HydrologyTerrainSample terrain = sample(target.x(), target.z(), search.samples());
|
||||
if (!allowed(terrain, search.edge())) {
|
||||
return;
|
||||
}
|
||||
HydrologyPoint point = new HydrologyPoint(target.x(), terrain.naturalHeight(), target.z());
|
||||
double cost = current.cost() + distance(current.point(), point)
|
||||
+ Math.max(0D, point.y() - current.point().y()) * planner.settings.routing().uphillPenalty()
|
||||
+ Math.max(0D, terrain.routingCost() * terrain.routingMultiplier());
|
||||
long key = RiverFootprint.pack(point.x(), point.z());
|
||||
List<Node> labels = search.visited().get(key);
|
||||
if (dominated(labels, cost, current.availableHead())) {
|
||||
return;
|
||||
}
|
||||
Traversal traversal = traverse(current.point(), point, search.edge(), search.samples(), current.availableHead());
|
||||
cost += traversal.cost();
|
||||
if (!Double.isFinite(cost) || dominated(labels, cost, traversal.availableHead())) {
|
||||
return;
|
||||
}
|
||||
cost += clearanceCost(point, search.edge(), search.samples());
|
||||
if (dominated(labels, cost, traversal.availableHead())) {
|
||||
return;
|
||||
}
|
||||
if (labels == null) {
|
||||
labels = new ArrayList<>(2);
|
||||
search.visited().put(key, labels);
|
||||
}
|
||||
for (int index = labels.size() - 1; index >= 0; index--) {
|
||||
Node previous = labels.get(index);
|
||||
if (cost <= previous.cost() && traversal.availableHead() >= previous.availableHead()) {
|
||||
labels.remove(index);
|
||||
}
|
||||
}
|
||||
Node retained = new Node(point, cost, traversal.availableHead(), current);
|
||||
labels.add(retained);
|
||||
search.pending().add(new Pending(key, retained, cost + distance(point, search.edge().end())));
|
||||
}
|
||||
|
||||
private static boolean dominated(List<Node> labels, double cost, int availableHead) {
|
||||
if (labels == null) {
|
||||
return false;
|
||||
}
|
||||
for (Node previous : labels) {
|
||||
if (previous.cost() <= cost && previous.availableHead() >= availableHead) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean retained(List<Node> labels, Node current) {
|
||||
for (Node retained : labels) {
|
||||
if (retained == current) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Traversal traverse(HydrologyPoint start, HydrologyPoint end, Edge edge,
|
||||
Map<Long, HydrologyTerrainSample> samples, int availableHead) {
|
||||
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
|
||||
HydrologyTerrainSample previous = null;
|
||||
double distance = distance(start, end);
|
||||
double tangentX = distance == 0D ? 1D : (end.x() - start.x()) / distance;
|
||||
double tangentZ = distance == 0D ? 0D : (end.z() - start.z()) / distance;
|
||||
HydrologyTerrainSampler bankSampler = (x, z) -> sample(x, z, samples);
|
||||
double cost = 0D;
|
||||
for (int step = 0; step <= steps; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress);
|
||||
int z = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress);
|
||||
HydrologyTerrainSample terrain = sample(x, z, samples);
|
||||
if (!allowed(terrain, edge) || previous != null && (!previous.drainsInto(terrain)
|
||||
|| edge.coastal() && !terrain.drainsInto(previous))) {
|
||||
return new Traversal(Double.POSITIVE_INFINITY, availableHead);
|
||||
}
|
||||
if (!edge.coastal()) {
|
||||
availableHead = hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
|
||||
x, z, tangentX, tangentZ, terrain, availableHead), bankSampler);
|
||||
if (availableHead < Math.max(edge.minimumHead(), minimumHead(terrain, x, z, edge.terminal()))) {
|
||||
return new Traversal(Double.POSITIVE_INFINITY, availableHead);
|
||||
}
|
||||
}
|
||||
cost += ridgeCost(x, z, terrain, edge);
|
||||
previous = terrain;
|
||||
}
|
||||
return new Traversal(cost, availableHead);
|
||||
}
|
||||
|
||||
private double ridgeCost(int x, int z, HydrologyTerrainSample terrain, Edge edge) {
|
||||
if (edge.coastal()) {
|
||||
return 0D;
|
||||
}
|
||||
double deltaX = edge.end().x() - edge.start().x();
|
||||
double deltaZ = edge.end().z() - edge.start().z();
|
||||
double squared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
double progress = squared == 0D ? 0D : Math.max(0D, Math.min(1D,
|
||||
((x - edge.start().x()) * deltaX + (z - edge.start().z()) * deltaZ) / squared));
|
||||
double envelope = edge.start().y() + (edge.end().y() - edge.start().y()) * progress;
|
||||
int incision = terrain.surfacePolicy().maximumIncision(planner.settings.surface().maximumIncision());
|
||||
double permitted = Math.min(incision, StrictMath.floor(incision * terrain.incisionMultiplier()));
|
||||
double excess = terrain.naturalHeight() - envelope + planner.settings.surface().minimumDepth()
|
||||
+ planner.settings.surface().banks().sink() - permitted;
|
||||
return Math.max(0D, excess) * Math.max(1D, planner.settings.routing().uphillPenalty());
|
||||
}
|
||||
|
||||
private double clearanceCost(HydrologyPoint point, Edge edge, Map<Long, HydrologyTerrainSample> samples) {
|
||||
double radius = planner.settings.surface().maximumWidth() / 2D
|
||||
* (1D + planner.settings.surface().banks().roughness()) + 1D;
|
||||
int blocked = 0;
|
||||
for (int[] offset : NEIGHBOURS) {
|
||||
double scale = offset[0] != 0 && offset[1] != 0 ? radius / StrictMath.sqrt(2D) : radius;
|
||||
int x = point.x() + (int) StrictMath.round(offset[0] * scale);
|
||||
int z = point.z() + (int) StrictMath.round(offset[1] * scale);
|
||||
if (!allowed(sample(x, z, samples), edge)) {
|
||||
blocked++;
|
||||
}
|
||||
}
|
||||
return blocked * GRID_SPACING / 2D;
|
||||
}
|
||||
|
||||
private boolean allowed(HydrologyTerrainSample terrain, Edge edge) {
|
||||
return terrain != null && !terrain.ocean() && terrain.naturalHeight() >= planner.settings.seaLevel() && terrain.transitAllowed()
|
||||
&& terrain.preferredProfileKeys().contains(edge.profile())
|
||||
&& (!edge.coastal() || terrain.naturalHeight() - planner.settings.seaLevel()
|
||||
+ planner.settings.surface().minimumDepth()
|
||||
<= planner.settings.routing().regional().maximumCoastalIncision());
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z, Map<Long, HydrologyTerrainSample> samples) {
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
HydrologyTerrainSample sampled = samples.get(key);
|
||||
if (sampled != null || samples.containsKey(key) || samples.size() >= MAXIMUM_SAMPLES) {
|
||||
return sampled;
|
||||
}
|
||||
sampled = sample(x, z);
|
||||
samples.put(key, sampled);
|
||||
return sampled;
|
||||
}
|
||||
|
||||
private static List<HydrologyPoint> path(Node current) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
while (current != null) {
|
||||
points.add(current.point());
|
||||
current = current.parent();
|
||||
}
|
||||
Collections.reverse(points);
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
private static double distance(HydrologyPoint first, HydrologyPoint second) {
|
||||
return StrictMath.sqrt(first.distanceSquared2D(second));
|
||||
}
|
||||
|
||||
private record Edge(HydrologyPoint start, HydrologyPoint end, String profile, boolean coastal,
|
||||
int minimumHead, int maximumHead, Terminal terminal) {
|
||||
}
|
||||
|
||||
private record Terminal(HydrologyPoint landward, int possibleStations) {
|
||||
}
|
||||
|
||||
private record Reach(List<HydrologyPoint> points, int availableHead) {
|
||||
}
|
||||
|
||||
private record Traversal(double cost, int availableHead) {
|
||||
}
|
||||
|
||||
private record Terrain(HydrologyTerrainSample sample) {
|
||||
}
|
||||
|
||||
private record Search(Edge edge, Map<Long, HydrologyTerrainSample> samples, Map<Long, List<Node>> visited,
|
||||
PriorityQueue<Pending> pending) {
|
||||
}
|
||||
|
||||
private record Node(HydrologyPoint point, double cost, int availableHead, Node parent) {
|
||||
}
|
||||
|
||||
private record Pending(long key, Node node, double estimate) {
|
||||
private int availableHead() {
|
||||
return node.availableHead();
|
||||
}
|
||||
}
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
final class HydrologyRegionalTributaries {
|
||||
private static final long JUNCTION_SALT = 0x5245474a554e4354L;
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
|
||||
HydrologyRegionalTributaries(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
}
|
||||
|
||||
CompiledGraph connect(CompiledGraph graph, List<RiverCourse> courses,
|
||||
HydrologyFootprintCompiler footprints, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
HydrologyRegionalNetwork regional = footprints.regionalNetwork;
|
||||
if (regional.courses().isEmpty()) {
|
||||
return graph;
|
||||
}
|
||||
LinkedHashMap<Long, DrainageNode> nodes = new LinkedHashMap<>();
|
||||
LinkedHashMap<Long, DrainageEdge> edges = new LinkedHashMap<>();
|
||||
LinkedHashMap<Long, RiverOutlet> outlets = new LinkedHashMap<>();
|
||||
for (DrainageNode node : graph.nodes()) { nodes.put(node.id(), node); }
|
||||
for (DrainageEdge edge : graph.edges()) { edges.put(edge.id(), edge); }
|
||||
for (RiverOutlet outlet : graph.outlets()) { outlets.put(outlet.id(), outlet); }
|
||||
Map<Long, DrainageNode> regionalNodes = new HashMap<>();
|
||||
Map<Long, RiverOutlet> regionalOutlets = new HashMap<>();
|
||||
for (DrainageNode node : regional.nodes()) { regionalNodes.put(node.id(), node); }
|
||||
for (RiverOutlet outlet : regional.outlets()) { regionalOutlets.put(outlet.id(), outlet); }
|
||||
ListIterator<RiverCourse> iterator = courses.listIterator();
|
||||
while (iterator.hasNext()) {
|
||||
RiverCourse local = iterator.next();
|
||||
if (local.type() != RiverCourseType.SURFACE || !planner.regional.conflicts(local, regional.courses())) {
|
||||
continue;
|
||||
}
|
||||
DrainageNode source = nodes.get(local.sourceNodeId().orElseThrow());
|
||||
Junction accepted = null;
|
||||
for (RiverCourse stem : regional.courses()) {
|
||||
if (!planner.regional.conflicts(local, List.of(stem)) || source == null) {
|
||||
continue;
|
||||
}
|
||||
RiverOutlet outlet = regionalOutlets.get(stem.outletId().orElseThrow());
|
||||
accepted = build(local, source, new Receiver(stem, outlet, regionalNodes), footprints, diagnostics);
|
||||
if (accepted != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (accepted == null || conflictsOtherStem(accepted.course(), regional.courses())) {
|
||||
iterator.remove();
|
||||
planner.tributaries.addTributaryDiagnostic(local.id(), local.segments().getFirst().start(),
|
||||
HydrologyCandidateRejection.NO_DRAINAGE_PATH, 0, diagnostics);
|
||||
continue;
|
||||
}
|
||||
iterator.set(accepted.course());
|
||||
for (DrainageNode node : accepted.nodes()) { nodes.put(node.id(), node); }
|
||||
for (DrainageEdge edge : accepted.course().drainageEdges()) { edges.put(edge.id(), edge); }
|
||||
outlets.put(accepted.outlet().id(), accepted.outlet());
|
||||
}
|
||||
return new CompiledGraph(List.copyOf(nodes.values()), List.copyOf(edges.values()),
|
||||
List.copyOf(outlets.values()), graph.edgeByUpstream());
|
||||
}
|
||||
|
||||
private Junction build(RiverCourse local, DrainageNode source, Receiver receiver,
|
||||
HydrologyFootprintCompiler footprints, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
if (receiver.outlet() == null || local.drainageEdges().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<HydrologyPoint> points = points(local);
|
||||
ArrayList<DrainageEdge> pairs = new ArrayList<>(Math.max(0, points.size() - 1));
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
pairs.add(local.drainageEdges().getFirst());
|
||||
}
|
||||
HydrologyCoursePath path = new HydrologyCoursePath(points, pairs, local.drainageEdges(), receiver.outlet(),
|
||||
false, false, null);
|
||||
HydrologyGridNode sourceNode = new HydrologyGridNode(0, 0, 0, source.x(), source.z(), source.id(), source.terrain());
|
||||
RiverCourse tributary = planner.tributaries.buildSurfaceTributary(
|
||||
new SurfaceCourseDraft(sourceNode, local.id(), local.profileKey(), path), receiver.course(), diagnostics);
|
||||
if (tributary == null || !confined(tributary)) {
|
||||
return null;
|
||||
}
|
||||
SurfaceFootprint footprint = footprints.surfaceFootprint(tributary);
|
||||
if (!footprint.accepted() || !joinsWater(tributary, footprint, receiver.course())) {
|
||||
if (!footprint.accepted()) {
|
||||
planner.tributaries.addTributaryDiagnostic(local.id(), local.segments().getFirst().start(),
|
||||
footprint.rejection(), footprint.rejectionDetail(), diagnostics);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return reroot(tributary, source, receiver);
|
||||
}
|
||||
|
||||
private boolean conflictsOtherStem(RiverCourse course, List<RiverCourse> stems) {
|
||||
for (RiverCourse stem : stems) {
|
||||
if (stem.outletId().getAsLong() != course.outletId().getAsLong()
|
||||
&& planner.regional.conflicts(course, List.of(stem))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean joinsWater(RiverCourse tributary, SurfaceFootprint footprint, RiverCourse stem) {
|
||||
HydrologyPoint end = tributary.segments().getLast().end();
|
||||
SurfaceBounds bounds = new SurfaceBounds(end.x(), end.z(), end.x(), end.z());
|
||||
SurfaceFootprint receiving = new SurfaceFootprintCompiler(planner.settings,
|
||||
planner::sampleBasisWithoutSlope, planner.geometrySampler).compile(stem, bounds);
|
||||
HydrologyColumnLayer receiver = null;
|
||||
for (SurfaceLayerColumn column : receiving.columns()) {
|
||||
if (column.layer().fluidOwned()) {
|
||||
receiver = column.layer();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!receiving.accepted() || receiver == null) {
|
||||
return false;
|
||||
}
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
HydrologyColumnLayer layer = column.layer();
|
||||
if (column.x() == end.x() && column.z() == end.z() && layer.fluidOwned()) {
|
||||
return layer.fluidHeadY() == receiver.fluidHeadY()
|
||||
&& Math.max(layer.bedY(), receiver.bedY()) < layer.fluidHeadY();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean confined(RiverCourse course) {
|
||||
HydrologyTerrainSample previous = null;
|
||||
List<HydrologyPoint> points = points(course);
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
HydrologyPoint start = points.get(index - 1);
|
||||
HydrologyPoint end = points.get(index);
|
||||
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
|
||||
for (int step = index == 1 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress);
|
||||
int z = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress);
|
||||
HydrologyTerrainSample terrain = planner.sampleBasisWithoutSlope(x, z);
|
||||
if (terrain == null || terrain.ocean() || !terrain.transitAllowed()
|
||||
|| !terrain.preferredProfileKeys().contains(course.profileKey())
|
||||
|| previous != null && !previous.drainsInto(terrain)) {
|
||||
return false;
|
||||
}
|
||||
previous = terrain;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Junction reroot(RiverCourse tributary, DrainageNode source, Receiver receiver) {
|
||||
HydrologyPoint end = tributary.segments().getLast().end();
|
||||
DrainageEdge nearest = null;
|
||||
double nearestDistance = Double.POSITIVE_INFINITY;
|
||||
for (DrainageEdge edge : receiver.course().drainageEdges()) {
|
||||
HydrologyPoint start = edge.centerline().getFirst();
|
||||
HydrologyPoint finish = edge.centerline().getLast();
|
||||
double deltaX = finish.x() - start.x();
|
||||
double deltaZ = finish.z() - start.z();
|
||||
double squared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
double progress = squared == 0D ? 0D : Math.max(0D, Math.min(1D,
|
||||
((end.x() - start.x()) * deltaX + (end.z() - start.z()) * deltaZ) / squared));
|
||||
double distance = StrictMath.hypot(end.x() - start.x() - deltaX * progress,
|
||||
end.z() - start.z() - deltaZ * progress);
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
nearest = edge;
|
||||
}
|
||||
}
|
||||
if (nearest == null) {
|
||||
return null;
|
||||
}
|
||||
DrainageNode downstream = receiver.nodes().get(nearest.downstreamNodeId());
|
||||
HydrologyTerrainSample junctionTerrain = planner.sampleBasisWithoutSlope(end.x(), end.z());
|
||||
long outletId = receiver.outlet().id();
|
||||
long sourceId = HydrologyHash.mix(tributary.id(), JUNCTION_SALT, source.id());
|
||||
long junctionId = HydrologyHash.mix(tributary.id(), JUNCTION_SALT, end.x(), end.z());
|
||||
double connectorLength = Math.max(1D, StrictMath.sqrt(end.distanceSquared2D(downstream.naturalPoint())));
|
||||
double sourceLength = Math.max(1D, length(points(tributary)));
|
||||
DrainageNode junction = new DrainageNode(junctionId, end.x(), end.z(), junctionTerrain,
|
||||
downstream.potential() + connectorLength, outletId);
|
||||
DrainageNode origin = new DrainageNode(sourceId, source.x(), source.z(), source.terrain(),
|
||||
junction.potential() + sourceLength, outletId);
|
||||
DrainageEdge branch = new DrainageEdge(HydrologyHash.mix(sourceId, junctionId), sourceId, junctionId,
|
||||
outletId, sourceLength, tributary.discharge(), 0, points(tributary));
|
||||
DrainageEdge connector = new DrainageEdge(HydrologyHash.mix(junctionId, downstream.id()), junctionId, downstream.id(),
|
||||
outletId, connectorLength, tributary.discharge(), 0, List.of(end, downstream.naturalPoint()));
|
||||
RiverCourse connected = new RiverCourse(tributary.id(), tributary.type(), OptionalLong.of(sourceId),
|
||||
OptionalLong.of(outletId), tributary.profileKey(), tributary.discharge(), List.of(branch, connector), tributary.segments());
|
||||
return new Junction(connected, List.of(origin, junction, downstream), receiver.outlet());
|
||||
}
|
||||
|
||||
private static List<HydrologyPoint> points(RiverCourse course) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (!segment.type().isSurface()) {
|
||||
break;
|
||||
}
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
if (points.isEmpty() || point.distanceSquared2D(points.getLast()) > 0L) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
private static double length(List<HydrologyPoint> points) {
|
||||
double length = 0D;
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
length += StrictMath.sqrt(points.get(index).distanceSquared2D(points.get(index - 1)));
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
private record Receiver(RiverCourse course, RiverOutlet outlet, Map<Long, DrainageNode> nodes) {
|
||||
}
|
||||
|
||||
private record Junction(RiverCourse course, List<DrainageNode> nodes, RiverOutlet outlet) {
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ final class HydrologySegmentBuilder {
|
||||
depth,
|
||||
false,
|
||||
false,
|
||||
centerline
|
||||
centerline, HydraulicChannelProfile.uniform(width, depth)
|
||||
));
|
||||
return true;
|
||||
}
|
||||
@@ -114,7 +114,7 @@ final class HydrologySegmentBuilder {
|
||||
flowDepth,
|
||||
true,
|
||||
true,
|
||||
List.of(lip, receiver)
|
||||
List.of(lip, receiver), HydraulicChannelProfile.uniform(flowWidth, flowDepth)
|
||||
));
|
||||
addGradedTransitionSegment(
|
||||
courseId,
|
||||
@@ -227,7 +227,7 @@ final class HydrologySegmentBuilder {
|
||||
dropDepth,
|
||||
false,
|
||||
!continuousSurfaceBore,
|
||||
graded
|
||||
graded, HydraulicChannelProfile.uniform(dropWidth, dropDepth)
|
||||
));
|
||||
if (!continuousSurfaceBore) {
|
||||
addGradedTransitionSegment(
|
||||
@@ -334,7 +334,7 @@ final class HydrologySegmentBuilder {
|
||||
depth,
|
||||
false,
|
||||
false,
|
||||
List.copyOf(centerline)
|
||||
List.copyOf(centerline), HydraulicChannelProfile.uniform(width, depth)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -461,7 +461,6 @@ final class HydrologySegmentBuilder {
|
||||
width,
|
||||
depth,
|
||||
pathEnd,
|
||||
landward,
|
||||
segments
|
||||
);
|
||||
return;
|
||||
@@ -540,20 +539,8 @@ final class HydrologySegmentBuilder {
|
||||
int width,
|
||||
int depth,
|
||||
HydrologyPoint pathEnd,
|
||||
HydrologyPoint landward,
|
||||
List<HydraulicSegment> segments
|
||||
) {
|
||||
if (pathEnd.x() != landward.x() || pathEnd.z() != landward.z()) {
|
||||
addFlatSegment(
|
||||
courseId,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
surfaceHead,
|
||||
width,
|
||||
depth,
|
||||
line(pathEnd, landward, planner.settings.routing().refinementSpacing()),
|
||||
segments
|
||||
);
|
||||
}
|
||||
int seaLevel = outletHead(outlet);
|
||||
HydrologyPoint connection = HydrologyPlanner.withY(outlet.connectionPoint(), seaLevel);
|
||||
if (surfaceHead > seaLevel) {
|
||||
@@ -564,7 +551,7 @@ final class HydrologySegmentBuilder {
|
||||
seaLevel,
|
||||
width,
|
||||
depth,
|
||||
List.of(landward, connection),
|
||||
List.of(pathEnd, connection),
|
||||
true,
|
||||
segments
|
||||
);
|
||||
@@ -635,7 +622,7 @@ final class HydrologySegmentBuilder {
|
||||
throatDepth,
|
||||
false,
|
||||
true,
|
||||
gradedDescent
|
||||
gradedDescent, HydraulicChannelProfile.uniform(descentWidth, throatDepth)
|
||||
));
|
||||
addFlatSegment(
|
||||
courseId,
|
||||
@@ -712,7 +699,7 @@ final class HydrologySegmentBuilder {
|
||||
depth,
|
||||
false,
|
||||
false,
|
||||
centerline
|
||||
centerline, HydraulicChannelProfile.uniform(width, depth)
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseBuilder;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
|
||||
|
||||
@@ -151,7 +152,11 @@ final class HydrologySurfaceCoursePlanner {
|
||||
}
|
||||
int outletHead = planner.segments.outletHead(outlet);
|
||||
int terminalHead = surfaceSinkhole ? Math.addExact(outletHead, 1) : outletHead;
|
||||
SurfaceCourseResult result = planner.surfaceCourseBuilder.build(
|
||||
SurfaceCourseBuilder builder = terminal == SurfaceTerminal.OCEAN_MOUTH
|
||||
? new SurfaceCourseBuilder(planner.settings.surface(), HydrologyOceanReceiver.forOutlet(
|
||||
planner.settings, planner::sampleBasisWithoutSlope, outlet), planner.geometrySampler, planner.settings.seaLevel())
|
||||
: planner.surfaceCourseBuilder;
|
||||
SurfaceCourseResult result = builder.build(
|
||||
planner.worldSeed,
|
||||
courseId,
|
||||
profileKey,
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
final class HydrologySurfaceDropBankBounds {
|
||||
private HydrologySurfaceDropBankBounds() {
|
||||
}
|
||||
|
||||
static List<HydrologyColumnSample> constrain(List<HydrologyColumnSample> columns, int maximumWidth) {
|
||||
Int2ObjectOpenHashMap<IntArrayList> wetRows = wetRows(columns);
|
||||
ArrayList<HydrologyColumnSample> constrained = new ArrayList<>(columns.size());
|
||||
for (HydrologyColumnSample column : columns) {
|
||||
if (!hasDryCut(column) || withinWidth(wetRows, column.x(), column.z(), maximumWidth)) {
|
||||
constrained.add(column);
|
||||
continue;
|
||||
}
|
||||
ArrayList<HydrologyColumnLayer> layers = new ArrayList<>(column.layers().size());
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
layers.add(!layer.channel() && layer.terrainOwned()
|
||||
? naturalLayer(layer, column.naturalHeight()) : layer);
|
||||
}
|
||||
constrained.add(new HydrologyColumnSample(column.x(), column.z(), column.naturalHeight(),
|
||||
column.seaLevel(), column.ocean(), column.parentBiomeKey(), layers));
|
||||
}
|
||||
return List.copyOf(constrained);
|
||||
}
|
||||
|
||||
private static Int2ObjectOpenHashMap<IntArrayList> wetRows(List<HydrologyColumnSample> columns) {
|
||||
Int2ObjectOpenHashMap<IntArrayList> rows = new Int2ObjectOpenHashMap<>();
|
||||
for (HydrologyColumnSample column : columns) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (layer.channel() && layer.fluidOwned() && layer.bedY() < layer.fluidHeadY()) {
|
||||
rows.computeIfAbsent(column.z(), ignored -> new IntArrayList()).add(column.x());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (IntArrayList row : rows.values()) {
|
||||
Arrays.sort(row.elements(), 0, row.size());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static boolean hasDryCut(HydrologyColumnSample column) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (!layer.channel() && layer.terrainOwned() && layer.bedY() < column.naturalHeight()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean withinWidth(Int2ObjectOpenHashMap<IntArrayList> wetRows, int x, int z, int maximumWidth) {
|
||||
long maximumDistance = (long) maximumWidth * maximumWidth;
|
||||
for (int deltaZ = 0; deltaZ <= maximumWidth; deltaZ++) {
|
||||
long remainingDistance = maximumDistance - (long) deltaZ * deltaZ;
|
||||
if (nearWetX(wetRows.get(z - deltaZ), x, remainingDistance)
|
||||
|| deltaZ > 0 && nearWetX(wetRows.get(z + deltaZ), x, remainingDistance)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean nearWetX(IntArrayList row, int x, long maximumDistance) {
|
||||
if (row == null) {
|
||||
return false;
|
||||
}
|
||||
int found = Arrays.binarySearch(row.elements(), 0, row.size(), x);
|
||||
if (found >= 0) {
|
||||
return true;
|
||||
}
|
||||
int next = -found - 1;
|
||||
if (next < row.size()) {
|
||||
long distance = (long) row.getInt(next) - x;
|
||||
if (distance * distance <= maximumDistance) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (next > 0) {
|
||||
long distance = (long) row.getInt(next - 1) - x;
|
||||
return distance * distance <= maximumDistance;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static HydrologyColumnLayer naturalLayer(HydrologyColumnLayer layer, int naturalHeight) {
|
||||
return new HydrologyColumnLayer(layer.feature(), naturalHeight, naturalHeight, naturalHeight,
|
||||
false, layer.shore(), false, false, false, false, false, false, false,
|
||||
layer.profileKey(), layer.surfaceBiomeKey(), layer.mouthBiomeKey(), layer.shoreBiomeKey(),
|
||||
layer.bankBiomeKey(), layer.floodedCaveBiomeKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceCenterline;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class HydrologySurfaceDropRaster {
|
||||
private static final HydrologySurfaceDropRaster EMPTY = new HydrologySurfaceDropRaster(List.of(), Map.of());
|
||||
|
||||
private final List<HydrologyColumnSample> columns;
|
||||
private final Map<Long, HydrologyColumnSample> indexed;
|
||||
private final Map<Long, OceanReceiver> oceanReceivers;
|
||||
|
||||
private HydrologySurfaceDropRaster(List<HydrologyColumnSample> columns, Map<Long, OceanReceiver> oceanReceivers) {
|
||||
this.columns = List.copyOf(columns);
|
||||
Map<Long, HydrologyColumnSample> indexed = new HashMap<>(columns.size());
|
||||
for (HydrologyColumnSample sample : columns) {
|
||||
indexed.put(RiverFootprint.pack(sample.x(), sample.z()), sample);
|
||||
}
|
||||
this.indexed = Map.copyOf(indexed);
|
||||
this.oceanReceivers = Map.copyOf(oceanReceivers);
|
||||
}
|
||||
|
||||
public static HydrologySurfaceDropRaster empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public static HydrologySurfaceDropRaster compile(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler sampler,
|
||||
HydrologyGeometrySampler geometry,
|
||||
RiverCourse course) {
|
||||
return compile(settings, sampler, geometry, course, null);
|
||||
}
|
||||
|
||||
public static HydrologySurfaceDropRaster compile(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler sampler,
|
||||
HydrologyGeometrySampler geometry,
|
||||
RiverCourse course,
|
||||
SurfaceBounds bounds) {
|
||||
Objects.requireNonNull(settings, "settings");
|
||||
Objects.requireNonNull(sampler, "sampler");
|
||||
Objects.requireNonNull(geometry, "geometry");
|
||||
Objects.requireNonNull(course, "course");
|
||||
HydrologyFootprintCompiler compiler = null;
|
||||
Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> columns = new Long2ObjectLinkedOpenHashMap<>();
|
||||
HydrologyFootprintCompiler.SurfaceRasterIndex plannedSurface = null;
|
||||
for (int index = 0; index < course.segments().size(); index++) {
|
||||
HydraulicSegment segment = course.segments().get(index);
|
||||
if (!segment.type().isSurface() || !segment.fallingFluid()
|
||||
|| bounds != null && !intersects(settings, segment, bounds)) {
|
||||
continue;
|
||||
}
|
||||
if (compiler == null) {
|
||||
compiler = new HydrologyFootprintCompiler(settings, sampler, geometry);
|
||||
plannedSurface = compiler.emptySurfaceRaster();
|
||||
}
|
||||
boolean clipStart = index > 0 && compiler.rasterizer.segmentsJoin(course.segments().get(index - 1), segment);
|
||||
boolean clipEnd = index + 1 < course.segments().size()
|
||||
&& compiler.rasterizer.segmentsJoin(segment, course.segments().get(index + 1));
|
||||
compiler.rasterizer.rasterizeSegment(columns, course, segment, index == 0, clipStart, clipEnd,
|
||||
true, plannedSurface);
|
||||
}
|
||||
if (compiler == null) {
|
||||
return EMPTY;
|
||||
}
|
||||
if (bounds != null) {
|
||||
columns.long2ObjectEntrySet().removeIf(entry -> !bounds.contains(
|
||||
RiverFootprint.unpackX(entry.getLongKey()), RiverFootprint.unpackZ(entry.getLongKey())));
|
||||
}
|
||||
Map<Long, OceanReceiver> oceanReceivers = oceanReceivers(settings, sampler, course, bounds);
|
||||
return columns.isEmpty() && oceanReceivers.isEmpty() ? EMPTY
|
||||
: new HydrologySurfaceDropRaster(compiler.buildValidationColumns(columns), oceanReceivers);
|
||||
}
|
||||
|
||||
public List<HydrologyColumnSample> columns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnSample> sample(int x, int z) {
|
||||
return Optional.ofNullable(indexed.get(RiverFootprint.pack(x, z)));
|
||||
}
|
||||
|
||||
public boolean connects(int x, int z, int head) {
|
||||
HydrologyColumnSample sample = indexed.get(RiverFootprint.pack(x, z));
|
||||
if (sample == null) {
|
||||
return false;
|
||||
}
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (layer.channel() && layer.fluidOwned() && !layer.oceanApron()
|
||||
&& layer.bedY() < head && head <= layer.fluidHeadY()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean containsRequiredFluid(RiverCourse course, SurfaceBounds bounds) {
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (!segment.type().isSurface() || !segment.fallingFluid()) {
|
||||
continue;
|
||||
}
|
||||
HydrologyPoint start = segment.start();
|
||||
if ((bounds == null || bounds.contains(start.x(), start.z()))
|
||||
&& !connects(start.x(), start.z(), segment.upstreamHeadY())) {
|
||||
return false;
|
||||
}
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(segment.centerline());
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
int x = centerline.x()[station];
|
||||
int z = centerline.z()[station];
|
||||
if ((bounds == null || bounds.contains(x, z)) && !connects(x, z, segment.downstreamHeadY())
|
||||
&& !receivingOcean(segment, x, z)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean receivingOcean(HydraulicSegment segment, int x, int z) {
|
||||
OceanReceiver receiver = oceanReceivers.get(segment.id());
|
||||
return receiver != null && receiver.x() == x && receiver.z() == z
|
||||
&& receiver.bedY() < segment.downstreamHeadY() && segment.downstreamHeadY() <= receiver.headY();
|
||||
}
|
||||
|
||||
private static Map<Long, OceanReceiver> oceanReceivers(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler sampler,
|
||||
RiverCourse course, SurfaceBounds bounds) {
|
||||
List<HydraulicSegment> segments = course.segments();
|
||||
if (segments.size() < 2) {
|
||||
return Map.of();
|
||||
}
|
||||
HydraulicSegment terminal = segments.getLast();
|
||||
boolean mouth = terminal.type() == HydrologyFeatureType.MOUTH;
|
||||
if (!mouth && terminal.type() != HydrologyFeatureType.COASTAL_GROTTO
|
||||
|| mouth && terminal.upstreamHeadY() != settings.seaLevel()) {
|
||||
return Map.of();
|
||||
}
|
||||
HydraulicSegment fall = segments.get(segments.size() - 2);
|
||||
HydrologyPoint receiver = mouth ? terminal.start() : terminal.end();
|
||||
if (!fall.type().isSurface() || !fall.fallingFluid()
|
||||
|| fall.downstreamHeadY() != settings.seaLevel() || terminal.downstreamHeadY() != settings.seaLevel()
|
||||
|| fall.end().distanceSquared2D(receiver) != 0L
|
||||
|| bounds != null && !bounds.contains(receiver.x(), receiver.z())) {
|
||||
return Map.of();
|
||||
}
|
||||
HydrologyTerrainSample terrain = sampler.sample(receiver.x(), receiver.z());
|
||||
if (terrain == null || terrain.naturalHeight() >= settings.seaLevel()
|
||||
|| (mouth ? !sampler.receivingWater(receiver.x(), receiver.z(), settings.seaLevel()) : !terrain.ocean())) {
|
||||
return Map.of();
|
||||
}
|
||||
return Map.of(fall.id(), new OceanReceiver(receiver.x(), receiver.z(), terrain.naturalHeight(), settings.seaLevel()));
|
||||
}
|
||||
|
||||
private static boolean intersects(HydrologyPlannerSettings settings, HydraulicSegment segment, SurfaceBounds bounds) {
|
||||
double radius = Math.max(segment.width(), settings.geometry().drops().basinWidth(segment.width())) / 2D
|
||||
+ settings.surface().shoreWidth() + settings.surface().banks().maximumBlendWidth() + 2D;
|
||||
int minimumX = Integer.MAX_VALUE;
|
||||
int maximumX = Integer.MIN_VALUE;
|
||||
int minimumZ = Integer.MAX_VALUE;
|
||||
int maximumZ = Integer.MIN_VALUE;
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
minimumX = Math.min(minimumX, point.x());
|
||||
maximumX = Math.max(maximumX, point.x());
|
||||
minimumZ = Math.min(minimumZ, point.z());
|
||||
maximumZ = Math.max(maximumZ, point.z());
|
||||
}
|
||||
return minimumX - radius <= bounds.maximumX() && maximumX + radius >= bounds.minimumX()
|
||||
&& minimumZ - radius <= bounds.maximumZ() && maximumZ + radius >= bounds.minimumZ();
|
||||
}
|
||||
|
||||
private record OceanReceiver(int x, int z, int bedY, int headY) {
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,20 @@ final class HydrologySurfaceProfiles {
|
||||
continue;
|
||||
}
|
||||
SurfaceFootprint footprint = compiler.surfaceFootprint(course);
|
||||
if (!footprint.accepted()) {
|
||||
iterator.remove();
|
||||
rejectedOutlets.add(course.outletId().orElseThrow());
|
||||
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||
HydrologyHash.mix(course.id(), HydrologySourcePlanner.DIAGNOSTIC_SALT,
|
||||
footprint.rejection().ordinal()),
|
||||
HydrologyCandidateKind.SOURCE,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
course.segments().getFirst().start(),
|
||||
footprint.rejection(),
|
||||
footprint.rejectionDetail()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
int excluded = 0;
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
HydrologyColumnLayer layer = column.layer();
|
||||
|
||||
@@ -3,4 +3,9 @@ package art.arcane.iris.engine.hydrology;
|
||||
@FunctionalInterface
|
||||
public interface HydrologyTerrainSampler {
|
||||
HydrologyTerrainSample sample(int blockX, int blockZ);
|
||||
|
||||
default boolean receivingWater(int blockX, int blockZ, int seaLevel) {
|
||||
HydrologyTerrainSample terrain = sample(blockX, blockZ);
|
||||
return terrain != null && terrain.ocean() && terrain.naturalHeight() < seaLevel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,8 +278,9 @@ public final class HydrologyTile {
|
||||
if (!coursesById.containsKey(courseId)) {
|
||||
throw new IllegalArgumentException("Footprint references an unaccepted river course: " + courseId);
|
||||
}
|
||||
if (layer.oceanApron() || !layer.channel() || !layer.terrainOwned()
|
||||
|| !isCaveLayer(layer)) {
|
||||
if (layer.oceanApron() || !layer.channel()
|
||||
|| !(layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())
|
||||
|| !isCaveLayer(layer, coursesById.get(courseId))) {
|
||||
continue;
|
||||
}
|
||||
Map<CavePosition, HydrologyCaveAction> courseActions = expectedActions.computeIfAbsent(
|
||||
@@ -433,15 +434,24 @@ public final class HydrologyTile {
|
||||
|
||||
private boolean isCaveBearing(RiverCourse course) {
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (segment.type().isUnderground() || segment.type().isDeepFluid()) {
|
||||
if (segment.type().isUnderground() || segment.type().isDeepFluid()
|
||||
|| segment.type().isSurface() && segment.fallingFluid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCaveLayer(HydrologyColumnLayer layer) {
|
||||
return layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid();
|
||||
private boolean isCaveLayer(HydrologyColumnLayer layer, RiverCourse course) {
|
||||
if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) {
|
||||
return true;
|
||||
}
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (segment.id() == layer.feature().segmentId()) {
|
||||
return segment.type().isSurface() && segment.fallingFluid();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private HydrologyCaveAction actionAt(HydrologyColumnLayer layer, int y) {
|
||||
|
||||
@@ -262,6 +262,35 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
return chunkColumns(blockX, blockZ).columnAt(blockX, blockZ);
|
||||
}
|
||||
|
||||
public HydrologyColumnSnapshot columnSnapshot(int blockX, int blockZ) {
|
||||
requireOpen();
|
||||
if (!waitsForbidden()) {
|
||||
return HydrologyColumnSnapshot.ready(columnAt(blockX, blockZ).orElse(null));
|
||||
}
|
||||
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
|
||||
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
|
||||
LocalChunkColumns local = localChunkColumns.get();
|
||||
if (local != null && local.epoch() == cacheEpoch.get()
|
||||
&& local.columns().chunkX() == chunkX && local.columns().chunkZ() == chunkZ) {
|
||||
return HydrologyColumnSnapshot.ready(local.columns().columnAt(blockX, blockZ).orElse(null));
|
||||
}
|
||||
ChunkColumns composed = composedChunks.getIfPresent(CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ)));
|
||||
if (composed != null) {
|
||||
return HydrologyColumnSnapshot.ready(composed.columnAt(blockX, blockZ).orElse(null));
|
||||
}
|
||||
ArrayList<HydrologyTileKey> keys = relevantKeys(chunkX, chunkZ);
|
||||
ArrayList<HydrologyTile> snapshots = new ArrayList<>(keys.size());
|
||||
for (HydrologyTileKey key : keys) {
|
||||
HydrologyTile tile = tiles.getIfPresent(key);
|
||||
if (tile == null) {
|
||||
requestUnplanned(chunkX, chunkZ);
|
||||
return HydrologyColumnSnapshot.unavailable();
|
||||
}
|
||||
snapshots.add(tile);
|
||||
}
|
||||
return HydrologyColumnSnapshot.ready(composeColumn(blockX, blockZ, keys, snapshots).orElse(null));
|
||||
}
|
||||
|
||||
public void prepareChunkColumns(int blockX, int blockZ) {
|
||||
chunkColumns(blockX, blockZ);
|
||||
}
|
||||
|
||||
+11
-11
@@ -213,9 +213,9 @@ final class HydrologyTributaryPlanner {
|
||||
ArrayList<HydrologyPoint> stemStations = new ArrayList<>();
|
||||
ArrayList<Integer> stemWidths = new ArrayList<>();
|
||||
for (HydraulicSegment segment : stem.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
stemStations.add(point);
|
||||
stemWidths.add(segment.width());
|
||||
for (int pointIndex = 0; pointIndex < segment.centerline().size(); pointIndex++) {
|
||||
stemStations.add(segment.centerline().get(pointIndex));
|
||||
stemWidths.add((int) StrictMath.ceil(segment.channelProfile().widthAt(pointIndex)));
|
||||
}
|
||||
}
|
||||
int minimumStemIndex = 0;
|
||||
@@ -340,9 +340,9 @@ final class HydrologyTributaryPlanner {
|
||||
downstream,
|
||||
segment.width(),
|
||||
segment.depth(),
|
||||
segment.fallingFluid(),
|
||||
segment.receivingPool(),
|
||||
List.copyOf(centerline)
|
||||
upstream > downstream && segment.fallingFluid(),
|
||||
upstream > downstream && segment.receivingPool(),
|
||||
List.copyOf(centerline), segment.channelProfile()
|
||||
));
|
||||
if (segment.upstreamHeadY() >= level) {
|
||||
break;
|
||||
@@ -417,9 +417,9 @@ final class HydrologyTributaryPlanner {
|
||||
downstream,
|
||||
segment.width(),
|
||||
segment.depth(),
|
||||
segment.fallingFluid(),
|
||||
segment.receivingPool(),
|
||||
List.copyOf(centerline)
|
||||
upstream > downstream && segment.fallingFluid(),
|
||||
upstream > downstream && segment.receivingPool(),
|
||||
List.copyOf(centerline), segment.channelProfile()
|
||||
));
|
||||
}
|
||||
if (settled) {
|
||||
@@ -531,8 +531,8 @@ final class HydrologyTributaryPlanner {
|
||||
type,
|
||||
lastSegment.downstreamHeadY(),
|
||||
stemHead,
|
||||
lastSegment.width(),
|
||||
lastSegment.depth(),
|
||||
(int) StrictMath.ceil(lastSegment.channelProfile().widthAt(lastSegment.centerline().size() - 1)),
|
||||
(int) StrictMath.ceil(lastSegment.channelProfile().depthAt(lastSegment.centerline().size() - 1)),
|
||||
List.of(end, new HydrologyPoint(joinStation.x(), stemHead, joinStation.z())),
|
||||
true,
|
||||
segments
|
||||
|
||||
@@ -87,15 +87,16 @@ final class SourceSelection {
|
||||
return !selectedCandidateIndices.isEmpty();
|
||||
}
|
||||
|
||||
boolean advanceAfterPublication(List<RiverCourse> acceptedCourses, HydrologySampledGrid grid) {
|
||||
Set<Long> acceptedSourceNodeIds = acceptedSourceNodeIds(acceptedCourses);
|
||||
boolean advanceAfterPublication(HydrologyCaveCourseFilter.Result result, HydrologySampledGrid grid) {
|
||||
AcceptedSources acceptedSources = acceptedSources(result);
|
||||
boolean changed = false;
|
||||
int selectedPosition = 0;
|
||||
while (selectedPosition < selectedCandidateIndices.size()) {
|
||||
int candidateIndex = selectedCandidateIndices.get(selectedPosition);
|
||||
SourceCandidate candidate = candidates.get(candidateIndex);
|
||||
long sourceNodeId = grid.node(candidate.nodeIndex()).id();
|
||||
if (acceptedSourceNodeIds.contains(sourceNodeId)) {
|
||||
HydrologyGridNode source = grid.node(candidate.nodeIndex());
|
||||
if (acceptedSources.nodeIds().contains(source.id())
|
||||
|| surface && acceptedSources.coordinates().contains(RiverFootprint.pack(source.x(), source.z()))) {
|
||||
if (candidate.required()) {
|
||||
attemptedCandidates[candidateIndex] = true;
|
||||
}
|
||||
@@ -131,15 +132,23 @@ final class SourceSelection {
|
||||
return changed;
|
||||
}
|
||||
|
||||
private Set<Long> acceptedSourceNodeIds(List<RiverCourse> acceptedCourses) {
|
||||
private AcceptedSources acceptedSources(HydrologyCaveCourseFilter.Result result) {
|
||||
RiverCourseType expectedType = surface ? RiverCourseType.SURFACE : RiverCourseType.UNDERGROUND;
|
||||
HashSet<Long> accepted = new HashSet<>();
|
||||
for (RiverCourse course : acceptedCourses) {
|
||||
for (RiverCourse course : result.courses()) {
|
||||
if (course.type() == expectedType && course.sourceNodeId().isPresent()) {
|
||||
accepted.add(course.sourceNodeId().getAsLong());
|
||||
}
|
||||
}
|
||||
return accepted;
|
||||
HashSet<Long> coordinates = new HashSet<>();
|
||||
if (surface) {
|
||||
for (DrainageNode node : result.nodes()) {
|
||||
if (accepted.contains(node.id())) {
|
||||
coordinates.add(RiverFootprint.pack(node.x(), node.z()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AcceptedSources(accepted, coordinates);
|
||||
}
|
||||
|
||||
private int nextRequiredCandidate() {
|
||||
@@ -198,4 +207,7 @@ final class SourceSelection {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private record AcceptedSources(Set<Long> nodeIds, Set<Long> coordinates) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
final class StudioHydrologyTileStore {
|
||||
private static final int SCHEMA_VERSION = 2;
|
||||
private static final int SCHEMA_VERSION = 3;
|
||||
private static final long MAXIMUM_COMPRESSED_BYTES = 128L * 1024L * 1024L;
|
||||
private static final long MAXIMUM_DECOMPRESSED_BYTES = 512L * 1024L * 1024L;
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
|
||||
+18
-4
@@ -22,6 +22,7 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
private final BasisProvider basisProvider;
|
||||
private final IrisHydrologyNaturalHeightProvider heightProvider;
|
||||
private final IrisHydrologyNaturalOceanClassifier oceanClassifier;
|
||||
private final int seaLevel;
|
||||
private final int maximumEntries;
|
||||
private final SamplingOptions samplingOptions;
|
||||
private final Object lock;
|
||||
@@ -35,6 +36,7 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
this.basisProvider = Objects.requireNonNull(sources.basisProvider(), "basisProvider");
|
||||
this.heightProvider = Objects.requireNonNull(sources.heightProvider(), "heightProvider");
|
||||
this.oceanClassifier = Objects.requireNonNull(sources.oceanClassifier(), "oceanClassifier");
|
||||
this.seaLevel = sources.seaLevel();
|
||||
int maximumEntries = samplingOptions.maximumEntries();
|
||||
this.maximumEntries = maximumEntries;
|
||||
this.lock = new Object();
|
||||
@@ -86,9 +88,16 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
NaturalClassification sampled = oceanClassifier.isOcean(blockX, blockZ)
|
||||
? NaturalClassification.OCEAN
|
||||
: NaturalClassification.LAND;
|
||||
NaturalClassification sampled = NaturalClassification.LAND;
|
||||
if (oceanClassifier.isOcean(blockX, blockZ)) {
|
||||
double height = naturalHeight(packed, blockX, blockZ);
|
||||
if (!Double.isFinite(height)) {
|
||||
return NaturalClassification.UNAVAILABLE;
|
||||
}
|
||||
if (physicalOcean(true, height, seaLevel)) {
|
||||
sampled = NaturalClassification.OCEAN;
|
||||
}
|
||||
}
|
||||
synchronized (lock) {
|
||||
TerrainBasis basis = bases.getAndMoveToLast(packed);
|
||||
if (basis != null) {
|
||||
@@ -176,6 +185,10 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
return Math.sqrt(deltaX * deltaX + deltaZ * deltaZ);
|
||||
}
|
||||
|
||||
static boolean physicalOcean(boolean oceanIntent, double naturalHeight, int seaLevel) {
|
||||
return oceanIntent && Double.isFinite(naturalHeight) && StrictMath.round(naturalHeight) < seaLevel;
|
||||
}
|
||||
|
||||
double localSlope(int blockX, int blockZ, double naturalHeight) {
|
||||
double easternHeight = naturalHeight(blockX + 3, blockZ);
|
||||
double southernHeight = naturalHeight(blockX, blockZ + 3);
|
||||
@@ -317,7 +330,8 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
record Sources(
|
||||
BasisProvider basisProvider,
|
||||
IrisHydrologyNaturalHeightProvider heightProvider,
|
||||
IrisHydrologyNaturalOceanClassifier oceanClassifier
|
||||
IrisHydrologyNaturalOceanClassifier oceanClassifier,
|
||||
int seaLevel
|
||||
) {
|
||||
Sources {
|
||||
Objects.requireNonNull(basisProvider, "basisProvider");
|
||||
|
||||
+30
-7
@@ -7,6 +7,7 @@ import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSnapshot;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticCandidate;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticRenderSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||
@@ -125,7 +126,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
IrisHydrologyRoutingTerrainSampler.Sources terrainSources = new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
this::createTerrainBasis,
|
||||
(int x, int z) -> context.naturalHeightProvider().sample(x, z),
|
||||
(int x, int z) -> context.naturalOceanClassifier().isOcean(x, z)
|
||||
(int x, int z) -> context.naturalOceanClassifier().isOcean(x, z),
|
||||
settings.seaLevel()
|
||||
);
|
||||
this.routingTerrainSampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
terrainSources,
|
||||
@@ -183,6 +185,10 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
return cache.columnAt(blockX, blockZ);
|
||||
}
|
||||
|
||||
public HydrologyColumnSnapshot sampleSnapshot(int x, int z) {
|
||||
return cache.columnSnapshot(x, z);
|
||||
}
|
||||
|
||||
/** Plans a bounded window of tiles touching the block area ahead of time, nearest the centre first. */
|
||||
public void prefetchArea(int minimumBlockX, int minimumBlockZ, int maximumBlockX, int maximumBlockZ, int centreBlockX, int centreBlockZ) {
|
||||
cache.prefetchArea(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ, centreBlockX, centreBlockZ);
|
||||
@@ -560,9 +566,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
"Hydrology natural sample provider returned null at " + x + "," + z
|
||||
);
|
||||
double sampledNaturalHeight = naturalSample.naturalHeight();
|
||||
double resolvedNaturalHeight = Double.isFinite(sampledNaturalHeight)
|
||||
? sampledNaturalHeight
|
||||
: rawNaturalHeight;
|
||||
double resolvedNaturalHeight = Double.isFinite(rawNaturalHeight)
|
||||
? rawNaturalHeight
|
||||
: sampledNaturalHeight;
|
||||
if (!Double.isFinite(resolvedNaturalHeight)) {
|
||||
// A non-finite height is a transient fault in a shared sampling cache, not a fact about
|
||||
// the terrain: sample the column once more before giving the tile up.
|
||||
@@ -579,7 +585,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
));
|
||||
}
|
||||
int naturalHeight = (int) StrictMath.round(resolvedNaturalHeight);
|
||||
boolean ocean = naturalSample.ocean();
|
||||
boolean ocean = IrisHydrologyRoutingTerrainSampler.physicalOcean(
|
||||
naturalSample.ocean(), resolvedNaturalHeight, settings.seaLevel());
|
||||
IrisBiome biome = naturalSample.biome();
|
||||
IrisRegion region = naturalSample.region();
|
||||
ResolvedPolicy resolvedPolicy = resolvePolicy(region, biome);
|
||||
@@ -877,7 +884,18 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
routing.getSlopePenalty(),
|
||||
routing.getConfluenceAttraction(),
|
||||
routing.getLengthPreference(),
|
||||
routing.getTributaries()
|
||||
routing.getTributaries(),
|
||||
new HydrologyPlannerSettings.Regional(
|
||||
routing.getRegional().isEnabled(),
|
||||
routing.getRegional().getSampleSpacing(),
|
||||
routing.getRegional().getMinimumLength(),
|
||||
routing.getRegional().getMaximumTrunks(),
|
||||
routing.getRegional().getMaximumCachedBasins(),
|
||||
routing.getRegional().getMaximumCachedStations(),
|
||||
routing.getRegional().isCoastalChannels(),
|
||||
routing.getRegional().getCoastalChannelChance(),
|
||||
routing.getRegional().getMaximumCoastalIncision()
|
||||
)
|
||||
);
|
||||
HydrologyPlannerSettings.Source plannerSurfaceSources = new HydrologyPlannerSettings.Source(
|
||||
surfaceEnabled,
|
||||
@@ -927,7 +945,12 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
erosion.getCliffFraction(),
|
||||
erosion.getBedProfile(),
|
||||
banks.getShoreRise(),
|
||||
banks.getBlendBaseWidth()
|
||||
banks.getBlendBaseWidth(),
|
||||
new HydrologyPlannerSettings.Excavation(
|
||||
banks.getExcavation().getMaximumDepth(),
|
||||
banks.getExcavation().getMaximumWidth(),
|
||||
banks.getExcavation().getMaximumVolumePerBlock()
|
||||
)
|
||||
),
|
||||
new HydrologyPlannerSettings.Ponds(
|
||||
pond(surface.getPonds().getSource()),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.RiverFootprint;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -8,10 +9,17 @@ import java.util.Objects;
|
||||
public final class ErosionField {
|
||||
private final Long2ObjectOpenHashMap<SurfaceColumn> columns;
|
||||
private final int uncontainedWetCells;
|
||||
private final HydrologyCandidateRejection rejection;
|
||||
private final int rejectionDetail;
|
||||
private final long bankExcavation;
|
||||
|
||||
ErosionField(Long2ObjectOpenHashMap<SurfaceColumn> columns, int uncontainedWetCells) {
|
||||
ErosionField(Long2ObjectOpenHashMap<SurfaceColumn> columns, int uncontainedWetCells,
|
||||
HydrologyCandidateRejection rejection, int rejectionDetail, long bankExcavation) {
|
||||
this.columns = Objects.requireNonNull(columns, "columns");
|
||||
this.uncontainedWetCells = uncontainedWetCells;
|
||||
this.rejection = rejection;
|
||||
this.rejectionDetail = rejectionDetail;
|
||||
this.bankExcavation = bankExcavation;
|
||||
}
|
||||
|
||||
public Long2ObjectOpenHashMap<SurfaceColumn> columns() {
|
||||
@@ -29,4 +37,16 @@ public final class ErosionField {
|
||||
public int size() {
|
||||
return columns.size();
|
||||
}
|
||||
|
||||
public HydrologyCandidateRejection rejection() {
|
||||
return rejection;
|
||||
}
|
||||
|
||||
public int rejectionDetail() {
|
||||
return rejectionDetail;
|
||||
}
|
||||
|
||||
public long bankExcavation() {
|
||||
return bankExcavation;
|
||||
}
|
||||
}
|
||||
|
||||
+178
-24
@@ -1,7 +1,9 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyHash;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.engine.hydrology.HydrologySurfaceDropRaster;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
|
||||
import art.arcane.iris.engine.hydrology.RiverFootprint;
|
||||
@@ -11,6 +13,8 @@ import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -62,6 +66,23 @@ public final class ErosionFieldCompiler {
|
||||
int apronLimit,
|
||||
HydrologyPlannerSettings.Ponds ponds
|
||||
) {
|
||||
return compile(courseSeed, centerline, channel, valley, terminal, apronLimit, ponds, SurfaceRasterContext.none());
|
||||
}
|
||||
|
||||
public ErosionField compile(
|
||||
long courseSeed,
|
||||
SurfaceCenterline centerline,
|
||||
ChannelProfile channel,
|
||||
ValleyProfile valley,
|
||||
SurfaceTerminal terminal,
|
||||
int apronLimit,
|
||||
HydrologyPlannerSettings.Ponds ponds,
|
||||
SurfaceRasterContext context
|
||||
) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
SurfaceBounds bounds = context.bounds();
|
||||
SurfaceBounds samplingBounds = bounds == null ? null : bounds.expand(2);
|
||||
SurfaceRasterContext samplingContext = new SurfaceRasterContext(samplingBounds, context.drops(), context.boundary());
|
||||
HydrologyPlannerSettings.Banks banks = surface.banks();
|
||||
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
|
||||
HydrologyPlannerSettings.Channel channelSettings = banks.channel();
|
||||
@@ -72,6 +93,7 @@ public final class ErosionFieldCompiler {
|
||||
double shoreRise = erosion.shoreRise();
|
||||
int sink = banks.sink();
|
||||
boolean[] basin = basins(valley, channel, count);
|
||||
double[] bendOffsets = SurfaceBendProfile.offsets(centerline, channel);
|
||||
double[][] blendWidth = blendWidths(centerline, channel, valley, count);
|
||||
Long2IntOpenHashMap nearest = new Long2IntOpenHashMap();
|
||||
nearest.defaultReturnValue(-1);
|
||||
@@ -85,8 +107,15 @@ public final class ErosionFieldCompiler {
|
||||
int radius = (int) StrictMath.ceil(channel.halfWidth()[station] * (1D + roughness) + Math.max(stationShore + widest, stationBand)) + 1;
|
||||
int stationX = centerline.x()[station];
|
||||
int stationZ = centerline.z()[station];
|
||||
for (int deltaZ = -radius; deltaZ <= radius; deltaZ++) {
|
||||
for (int deltaX = -radius; deltaX <= radius; deltaX++) {
|
||||
if (samplingBounds != null && !samplingBounds.intersects(stationX, stationZ, radius)) {
|
||||
continue;
|
||||
}
|
||||
int minimumZ = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumZ() - stationZ);
|
||||
int maximumZ = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumZ() - stationZ);
|
||||
int minimumX = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumX() - stationX);
|
||||
int maximumX = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumX() - stationX);
|
||||
for (int deltaZ = minimumZ; deltaZ <= maximumZ; deltaZ++) {
|
||||
for (int deltaX = minimumX; deltaX <= maximumX; deltaX++) {
|
||||
int cellX = stationX + deltaX;
|
||||
int cellZ = stationZ + deltaZ;
|
||||
double cellDistance = centerline.distanceToSegment(station, cellX, cellZ);
|
||||
@@ -114,8 +143,11 @@ public final class ErosionFieldCompiler {
|
||||
double cellDistance = distance.get(key);
|
||||
int cellX = RiverFootprint.unpackX(key);
|
||||
int cellZ = RiverFootprint.unpackZ(key);
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
int head = valley.head()[station];
|
||||
if (!context.boundary().allows(cellX, cellZ) || context.drops().connects(cellX, cellZ, head)) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
double halfWidth = channel.halfWidth()[station];
|
||||
double outline = halfWidth * (1D + roughness * SurfaceNoise.signed(
|
||||
courseSeed ^ OUTLINE_SALT, cellX, cellZ, banks.roughnessWavelength()));
|
||||
@@ -123,8 +155,10 @@ public final class ErosionFieldCompiler {
|
||||
Math.max(0.5D, channelSettings.outlineMinimumRatio() * halfWidth),
|
||||
Math.min(channelSettings.outlineMaximumRatio() * halfWidth, outline));
|
||||
boolean wet = cellDistance <= outline + 0.25D;
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
|
||||
if (terrain != null && wet && terminal == SurfaceTerminal.OCEAN_MOUTH
|
||||
boolean mouthLand = terminal == SurfaceTerminal.OCEAN_MOUTH && head == seaLevel
|
||||
&& SurfaceCellAdmission.mouthLand(terrain, seaLevel);
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel) && !mouthLand) {
|
||||
if (sampler.receivingWater(cellX, cellZ, seaLevel) && wet && terminal == SurfaceTerminal.OCEAN_MOUTH
|
||||
&& station >= valley.exposedStations() - 1
|
||||
&& station - oceanStart < apronLimit
|
||||
&& cellDistance <= apronLimit + 0.25D) {
|
||||
@@ -136,6 +170,8 @@ public final class ErosionFieldCompiler {
|
||||
int natural = terrain.naturalHeight();
|
||||
if (wet) {
|
||||
double normalized = Math.min(1D, cellDistance / Math.max(0.5D, outline));
|
||||
normalized = SurfaceBendProfile.normalizedDistance(normalized,
|
||||
side(centerline, station, cellX, cellZ), bendOffsets[station]);
|
||||
double depth = channel.depth()[station];
|
||||
double local = 1D + (depth - 1D) * bedFactor(normalized, erosion)
|
||||
+ erosion.bedNoise() * roughness * SurfaceNoise.signed(courseSeed ^ BED_SALT, cellX, cellZ, banks.roughnessWavelength())
|
||||
@@ -155,8 +191,16 @@ public final class ErosionFieldCompiler {
|
||||
// geometric shore with the bank biome.
|
||||
boolean shoreBiome = cellDistance <= outline + terrain.shoreBiomeWidth(shoreWidth) + 0.25D;
|
||||
SurfaceRole dryRole = shoreBiome ? SurfaceRole.SHORE : SurfaceRole.BANK;
|
||||
double bankDistance = cellDistance - outline;
|
||||
if (bankDistance > erosion.excavation().maximumWidth()) {
|
||||
if (shoreBiome) {
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (cellDistance <= outline + shore + 0.25D) {
|
||||
int height = Math.min(natural, bankTop + (int) StrictMath.round(shoreRise * benchProgress(cellDistance, outline, shore)));
|
||||
height = boundedBankHeight(natural, height, erosion.excavation());
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
|
||||
continue;
|
||||
}
|
||||
@@ -170,6 +214,7 @@ public final class ErosionFieldCompiler {
|
||||
}
|
||||
double side = side(centerline, station, cellX, cellZ);
|
||||
double width = blendWidth[station][side < 0D ? 0 : 1];
|
||||
width = Math.min(width, Math.max(0.25D, erosion.excavation().maximumWidth() - shore));
|
||||
double progress = (cellDistance - outline - shore) / width;
|
||||
if (progress >= 1D) {
|
||||
if (shoreBiome) {
|
||||
@@ -178,17 +223,18 @@ public final class ErosionFieldCompiler {
|
||||
continue;
|
||||
}
|
||||
int height = Math.min(natural, (int) StrictMath.round(shoreTop + cut * blend(progress, erosion)));
|
||||
height = boundedBankHeight(natural, height, erosion.excavation());
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
|
||||
}
|
||||
if (count > 0) {
|
||||
pond(columns, wetKeys, courseSeed ^ SOURCE_POND_SALT, ponds.source(), centerline, 0, valley.head()[0]);
|
||||
pond(columns, wetKeys, courseSeed ^ SOURCE_POND_SALT, ponds.source(), centerline, 0, valley.head()[0], samplingContext);
|
||||
if (terminal == SurfaceTerminal.SINKHOLE) {
|
||||
pond(columns, wetKeys, courseSeed ^ TERMINAL_POND_SALT, ponds.terminal(), centerline, count - 1, valley.head()[count - 1]);
|
||||
pond(columns, wetKeys, courseSeed ^ TERMINAL_POND_SALT, ponds.terminal(), centerline, count - 1, valley.head()[count - 1], samplingContext);
|
||||
}
|
||||
}
|
||||
int uncontained = contain(columns, wetKeys, sink);
|
||||
int uncontained = contain(columns, wetKeys, sink, bounds, context.drops());
|
||||
connectSteps(columns, wetKeys);
|
||||
return new ErosionField(columns, uncontained);
|
||||
return validate(columns, wetKeys, centerline, terminal, bounds, uncontained);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,8 +251,10 @@ public final class ErosionFieldCompiler {
|
||||
HydrologyPlannerSettings.Pond pond,
|
||||
SurfaceCenterline centerline,
|
||||
int station,
|
||||
int head
|
||||
int head,
|
||||
SurfaceRasterContext context
|
||||
) {
|
||||
SurfaceBounds bounds = context.bounds();
|
||||
if (!pond.enabled()) {
|
||||
return;
|
||||
}
|
||||
@@ -219,6 +267,10 @@ public final class ErosionFieldCompiler {
|
||||
int sink = banks.sink();
|
||||
int centreX = centerline.x()[station];
|
||||
int centreZ = centerline.z()[station];
|
||||
int maximumReach = pond.maximumRadius() * 2 + banks.maximumBlendWidth() + (int) StrictMath.ceil(shoreWidth) + 2;
|
||||
if (bounds != null && !bounds.intersects(centreX, centreZ, maximumReach)) {
|
||||
return;
|
||||
}
|
||||
// One bowl, one beach: the pond takes the shore width and the erosion switch its centre asks for.
|
||||
HydrologyTerrainSample centre = sampler.sample(centreX, centreZ);
|
||||
double shore = centre == null ? shoreWidth : centre.shoreWidth(shoreWidth);
|
||||
@@ -238,6 +290,11 @@ public final class ErosionFieldCompiler {
|
||||
}
|
||||
int cellX = centreX + deltaX;
|
||||
int cellZ = centreZ + deltaZ;
|
||||
if (bounds != null && !bounds.contains(cellX, cellZ)
|
||||
|| !context.boundary().allows(cellX, cellZ)
|
||||
|| context.drops().connects(cellX, cellZ, head)) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
|
||||
continue;
|
||||
@@ -271,8 +328,12 @@ public final class ErosionFieldCompiler {
|
||||
if (existing != null && existing.role() == SurfaceRole.CHANNEL) {
|
||||
continue;
|
||||
}
|
||||
if (distance - outline > erosion.excavation().maximumWidth()) {
|
||||
continue;
|
||||
}
|
||||
if (distance <= outline + shore + 0.25D) {
|
||||
int height = Math.min(natural, bankTop + (int) StrictMath.round(shoreRise * benchProgress(distance, outline, shore)));
|
||||
height = boundedBankHeight(natural, height, erosion.excavation());
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, height, height, false));
|
||||
continue;
|
||||
}
|
||||
@@ -282,11 +343,13 @@ public final class ErosionFieldCompiler {
|
||||
}
|
||||
double width = Math.max(banks.minimumBlendWidth(),
|
||||
Math.min(banks.maximumBlendWidth(), cut * banks.blendSlope() + erosion.blendBaseWidth()));
|
||||
width = Math.min(width, Math.max(0.25D, erosion.excavation().maximumWidth() - shore));
|
||||
double progress = (distance - outline - shore) / width;
|
||||
if (progress >= 1D) {
|
||||
continue;
|
||||
}
|
||||
int height = Math.min(natural, (int) StrictMath.round(shoreTop + cut * blend(progress, erosion)));
|
||||
height = boundedBankHeight(natural, height, erosion.excavation());
|
||||
if (existing != null && existing.height() <= height) {
|
||||
continue;
|
||||
}
|
||||
@@ -424,12 +487,15 @@ public final class ErosionFieldCompiler {
|
||||
* Every dry cell touching water, diagonals included, keeps the lip; only cardinal neighbours can
|
||||
* spill, so only they count as uncontained.
|
||||
*/
|
||||
private int contain(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys, int sink) {
|
||||
private int contain(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys, int sink, SurfaceBounds bounds, HydrologySurfaceDropRaster drops) {
|
||||
int uncontained = 0;
|
||||
for (int index = 0; index < wetKeys.size(); index++) {
|
||||
SurfaceColumn wet = columns.get(wetKeys.getLong(index));
|
||||
uncontained += lip(columns, wet, CARDINALS, sink, true);
|
||||
lip(columns, wet, DIAGONALS, sink, false);
|
||||
if (bounds != null && !bounds.contains(wet.x(), wet.z())) {
|
||||
continue;
|
||||
}
|
||||
uncontained += lip(columns, wet, CARDINALS, sink, true, drops);
|
||||
lip(columns, wet, DIAGONALS, sink, false, drops);
|
||||
}
|
||||
return uncontained;
|
||||
}
|
||||
@@ -437,26 +503,31 @@ public final class ErosionFieldCompiler {
|
||||
/**
|
||||
* The bank beside water must reach the natural ground the head was taken from: the water's own
|
||||
* block level plus the sink. A neighbour lower than that is raised to it. The solver takes the head
|
||||
* from the banks it samples at each station, so a cell it never sampled (between stations, on the
|
||||
* outside of a bend) can lie a block lower than the water beside it: such a cell may be raised one
|
||||
* block above its natural height, a lip no taller than the old fixed one, rather than let the
|
||||
* water spill. A deeper shortfall stays uncontained and is counted.
|
||||
* from the banks it samples at each station. A missed low cell can retain its natural ground,
|
||||
* but cannot be raised above it. A shortfall below the water head stays uncontained and is counted.
|
||||
*/
|
||||
private int lip(
|
||||
Long2ObjectOpenHashMap<SurfaceColumn> columns,
|
||||
SurfaceColumn wet,
|
||||
int[][] offsets,
|
||||
int sink,
|
||||
boolean countSpills
|
||||
boolean countSpills,
|
||||
HydrologySurfaceDropRaster drops
|
||||
) {
|
||||
int uncontained = 0;
|
||||
for (int[] offset : offsets) {
|
||||
long key = RiverFootprint.pack(wet.x() + offset[0], wet.z() + offset[1]);
|
||||
int neighbourX = wet.x() + offset[0];
|
||||
int neighbourZ = wet.z() + offset[1];
|
||||
if (drops.connects(neighbourX, neighbourZ, wet.headY())) {
|
||||
continue;
|
||||
}
|
||||
long key = RiverFootprint.pack(neighbourX, neighbourZ);
|
||||
SurfaceColumn neighbour = columns.get(key);
|
||||
if (neighbour == null) {
|
||||
// Water that meets the sea at the mouth is not spilling; anything else beyond the field is.
|
||||
if (countSpills && wet.headY() != seaLevel
|
||||
&& SurfaceCellAdmission.writable(sampler.sample(wet.x() + offset[0], wet.z() + offset[1]), seaLevel)) {
|
||||
HydrologyTerrainSample terrain = sampler.sample(wet.x() + offset[0], wet.z() + offset[1]);
|
||||
if (countSpills && (terrain == null || terrain.naturalHeight() < wet.headY())
|
||||
&& !(wet.headY() == seaLevel && sampler.receivingWater(neighbourX, neighbourZ, seaLevel))) {
|
||||
uncontained++;
|
||||
}
|
||||
continue;
|
||||
@@ -466,7 +537,7 @@ public final class ErosionFieldCompiler {
|
||||
}
|
||||
int required = wet.headY() + sink;
|
||||
if (neighbour.height() < required) {
|
||||
int ceiling = neighbour.terrain().naturalHeight() + (countSpills ? 1 : 0);
|
||||
int ceiling = neighbour.terrain().naturalHeight();
|
||||
int raised = Math.min(ceiling, required);
|
||||
neighbour = neighbour.withHeight(raised);
|
||||
columns.put(key, neighbour);
|
||||
@@ -485,14 +556,97 @@ public final class ErosionFieldCompiler {
|
||||
return count;
|
||||
}
|
||||
for (int station = Math.max(0, valley.exposedStations() - 1); station < count; station++) {
|
||||
HydrologyTerrainSample center = sampler.sample(centerline.x()[station], centerline.z()[station]);
|
||||
if (!SurfaceCellAdmission.writable(center, seaLevel)) {
|
||||
if (sampler.receivingWater(centerline.x()[station], centerline.z()[station], seaLevel)) {
|
||||
return station;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int boundedBankHeight(int natural, int target, HydrologyPlannerSettings.Excavation limits) {
|
||||
return Math.max(natural - limits.maximumDepth(), Math.min(natural, target));
|
||||
}
|
||||
|
||||
private ErosionField validate(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys,
|
||||
SurfaceCenterline centerline, SurfaceTerminal terminal, SurfaceBounds bounds, int uncontained) {
|
||||
long excavation = 0L;
|
||||
int[] stationVolumes = new int[centerline.size()];
|
||||
int deepest = 0;
|
||||
for (SurfaceColumn column : columns.values()) {
|
||||
if (column.apron() || column.role() == SurfaceRole.CHANNEL
|
||||
|| bounds != null && !bounds.contains(column.x(), column.z())) {
|
||||
continue;
|
||||
}
|
||||
int cut = Math.max(0, column.terrain().naturalHeight() - column.height());
|
||||
excavation += cut;
|
||||
deepest = Math.max(deepest, cut);
|
||||
stationVolumes[column.station()] += cut;
|
||||
}
|
||||
int largestSection = SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, stationVolumes);
|
||||
HydrologyPlannerSettings.Excavation limits = surface.banks().erosion().excavation();
|
||||
if (deepest > limits.maximumDepth() || largestSection > limits.maximumVolumePerBlock()) {
|
||||
return new ErosionField(columns, uncontained, HydrologyCandidateRejection.SURFACE_BANK_BUDGET,
|
||||
largestSection, excavation);
|
||||
}
|
||||
if (uncontained > 0) {
|
||||
return new ErosionField(columns, uncontained, HydrologyCandidateRejection.SURFACE_WATER_CONTAINMENT,
|
||||
uncontained, excavation);
|
||||
}
|
||||
if (bounds == null && terminal == SurfaceTerminal.OCEAN_MOUTH && !connectedToOcean(columns, wetKeys)) {
|
||||
return new ErosionField(columns, 0, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, excavation);
|
||||
}
|
||||
return new ErosionField(columns, uncontained, null, 0, excavation);
|
||||
}
|
||||
|
||||
private boolean connectedToOcean(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys) {
|
||||
if (wetKeys.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
long firstKey = wetKeys.getLong(0);
|
||||
for (int index = 1; index < wetKeys.size(); index++) {
|
||||
long key = wetKeys.getLong(index);
|
||||
SurfaceColumn candidate = columns.get(key);
|
||||
SurfaceColumn first = columns.get(firstKey);
|
||||
if (candidate.station() < first.station() || candidate.station() == first.station() && key < firstKey) {
|
||||
firstKey = key;
|
||||
}
|
||||
}
|
||||
LongOpenHashSet visited = new LongOpenHashSet(wetKeys.size());
|
||||
LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
|
||||
visited.add(firstKey);
|
||||
queue.enqueue(firstKey);
|
||||
boolean ocean = false;
|
||||
while (!queue.isEmpty()) {
|
||||
SurfaceColumn current = columns.get(queue.dequeueLong());
|
||||
for (int[] offset : CARDINALS) {
|
||||
int x = current.x() + offset[0];
|
||||
int z = current.z() + offset[1];
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
SurfaceColumn next = columns.get(key);
|
||||
if (current.headY() == seaLevel && current.height() < seaLevel
|
||||
&& sampler.receivingWater(x, z, seaLevel)) {
|
||||
ocean = true;
|
||||
}
|
||||
if (next == null || next.role() != SurfaceRole.CHANNEL || next.height() >= next.headY()
|
||||
|| Math.max(current.height(), next.height()) >= Math.min(current.headY(), next.headY())) {
|
||||
continue;
|
||||
}
|
||||
if (visited.add(key)) {
|
||||
queue.enqueue(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ocean) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < wetKeys.size(); index++) {
|
||||
if (!visited.contains(wetKeys.getLong(index))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean[] basins(ValleyProfile valley, ChannelProfile channel, int count) {
|
||||
HydrologyPlannerSettings.Flow flow = surface.banks().flow();
|
||||
boolean[] basin = new boolean[count];
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SurfaceBankSupport {
|
||||
static final int BANK_LOOKAHEAD = 2;
|
||||
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
private final int seaLevel;
|
||||
|
||||
public SurfaceBankSupport(HydrologyPlannerSettings.Surface surface, int seaLevel) {
|
||||
this.surface = Objects.requireNonNull(surface, "surface");
|
||||
this.seaLevel = seaLevel;
|
||||
}
|
||||
|
||||
public CrossSection crossSection(HydrologyTerrainSampler sampler, Station station, boolean oceanMouth) {
|
||||
double roughness = surface.banks().roughness();
|
||||
double outline = station.halfWidth() * (1D + roughness);
|
||||
// Every cell from the narrowest possible outline outward can end up beside water, so it joins the minimum.
|
||||
double innerOutline = station.halfWidth() * (1D - roughness);
|
||||
double reach = outline + 2D;
|
||||
double normalX = -station.tangentZ();
|
||||
double normalZ = station.tangentX();
|
||||
int minimum = Integer.MAX_VALUE;
|
||||
int maximum = Integer.MIN_VALUE;
|
||||
for (double offset = -reach; offset <= reach; offset += 0.5D) {
|
||||
int cellX = (int) StrictMath.round(station.x() + normalX * offset);
|
||||
int cellZ = (int) StrictMath.round(station.z() + normalZ * offset);
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
boolean writable = oceanMouth ? SurfaceCellAdmission.mouthLand(terrain, seaLevel)
|
||||
: SurfaceCellAdmission.writable(terrain, seaLevel);
|
||||
if (!writable) {
|
||||
if (oceanMouth && sampler.receivingWater(cellX, cellZ, seaLevel)) {
|
||||
minimum = Math.min(minimum, seaLevel);
|
||||
maximum = Math.max(maximum, seaLevel);
|
||||
continue;
|
||||
}
|
||||
return new CrossSection(0, 0, true);
|
||||
}
|
||||
if (Math.abs(offset) < innerOutline - 0.75D) {
|
||||
continue;
|
||||
}
|
||||
minimum = Math.min(minimum, terrain.naturalHeight());
|
||||
maximum = Math.max(maximum, terrain.naturalHeight());
|
||||
}
|
||||
return new CrossSection(minimum, maximum, false);
|
||||
}
|
||||
|
||||
public Perimeter perimeter(HydrologyTerrainSampler sampler, Station station, int ceiling) {
|
||||
double roughness = surface.banks().roughness();
|
||||
HydrologyPlannerSettings.Channel settings = surface.banks().channel();
|
||||
double halfWidth = station.halfWidth();
|
||||
double inner = Math.max(0D, halfWidth * Math.max(settings.outlineMinimumRatio(),
|
||||
Math.min(settings.outlineMaximumRatio(), 1D - roughness)) - 0.75D);
|
||||
double outer = halfWidth * Math.max(settings.outlineMinimumRatio(),
|
||||
Math.min(settings.outlineMaximumRatio(), 1D + roughness)) + 2D;
|
||||
int radius = (int) StrictMath.ceil(outer);
|
||||
double innerSquared = inner * inner;
|
||||
double outerSquared = outer * outer;
|
||||
int minimum = ceiling;
|
||||
boolean complete = true;
|
||||
int cliffFloor = ceiling - surface.banks().waterfallMinimumDrop();
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
double rowOuter = outerSquared - (double) dz * dz;
|
||||
if (rowOuter < 0D) {
|
||||
continue;
|
||||
}
|
||||
int rowRadius = (int) StrictMath.floor(StrictMath.sqrt(rowOuter));
|
||||
int innerRadius = innerSquared > (double) dz * dz
|
||||
? (int) StrictMath.ceil(StrictMath.sqrt(innerSquared - (double) dz * dz)) : 0;
|
||||
for (int dx = -rowRadius; dx <= rowRadius; dx++) {
|
||||
if (innerRadius > 0 && dx > -innerRadius && dx < innerRadius) {
|
||||
dx = innerRadius - 1;
|
||||
continue;
|
||||
}
|
||||
double along = dx * station.tangentX() + dz * station.tangentZ();
|
||||
if (StrictMath.abs(along) > BANK_LOOKAHEAD + 0.5D) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample terrain = sampler.sample(station.x() + dx, station.z() + dz);
|
||||
complete &= terrain != null;
|
||||
if (SurfaceCellAdmission.writable(terrain, seaLevel)
|
||||
&& terrain.naturalHeight() > cliffFloor) {
|
||||
minimum = Math.min(minimum, terrain.naturalHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Perimeter(minimum, complete);
|
||||
}
|
||||
|
||||
public record Perimeter(int minimum, boolean complete) {
|
||||
}
|
||||
|
||||
public record Station(int x, int z, double tangentX, double tangentZ, double halfWidth) {
|
||||
}
|
||||
|
||||
public record CrossSection(int minimum, int maximum, boolean blocked) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
final class SurfaceBendProfile {
|
||||
private static final double MAXIMUM_OFFSET = 0.35D;
|
||||
private static final double CURVATURE_GAIN = 2.5D;
|
||||
private static final int MINIMUM_WINDOW = 8;
|
||||
private static final int MAXIMUM_WINDOW = 32;
|
||||
private static final int SMOOTHING_RADIUS = 4;
|
||||
|
||||
private SurfaceBendProfile() {
|
||||
}
|
||||
|
||||
static double[] offsets(SurfaceCenterline centerline, ChannelProfile channel) {
|
||||
int count = centerline.size();
|
||||
double[] offsets = new double[count];
|
||||
double[] distance = new double[count];
|
||||
double[] turns = new double[count];
|
||||
for (int station = 1; station < count; station++) {
|
||||
distance[station] = distance[station - 1] + StrictMath.hypot(
|
||||
centerline.x()[station] - centerline.x()[station - 1],
|
||||
centerline.z()[station] - centerline.z()[station - 1]);
|
||||
double cross = centerline.tangentX()[station - 1] * centerline.tangentZ()[station]
|
||||
- centerline.tangentZ()[station - 1] * centerline.tangentX()[station];
|
||||
double dot = centerline.tangentX()[station - 1] * centerline.tangentX()[station]
|
||||
+ centerline.tangentZ()[station - 1] * centerline.tangentZ()[station];
|
||||
turns[station] = turns[station - 1] + StrictMath.atan2(cross, dot);
|
||||
}
|
||||
for (int station = 1; station + 1 < count; station++) {
|
||||
double halfWidth = channel.halfWidth()[station];
|
||||
int window = Math.max(MINIMUM_WINDOW, Math.min(MAXIMUM_WINDOW, (int) StrictMath.ceil(halfWidth * 3D)));
|
||||
int from = Math.max(0, station - window);
|
||||
int to = Math.min(count - 1, station + window);
|
||||
double curvature = (turns[to] - turns[from]) / Math.max(1D, distance[to] - distance[from]);
|
||||
double fade = SurfaceNoise.smoothStep(Math.min(1D, Math.min(station, count - 1D - station) / window));
|
||||
offsets[station] = Math.max(-MAXIMUM_OFFSET, Math.min(MAXIMUM_OFFSET,
|
||||
-curvature * halfWidth * CURVATURE_GAIN)) * fade;
|
||||
}
|
||||
return smooth(offsets);
|
||||
}
|
||||
|
||||
static double normalizedDistance(double distance, double side, double offset) {
|
||||
if (offset == 0D) {
|
||||
return distance;
|
||||
}
|
||||
double signed = StrictMath.copySign(distance, side);
|
||||
double shifted = signed >= offset ? (signed - offset) / (1D - offset)
|
||||
: (offset - signed) / (1D + offset);
|
||||
return Math.max(0D, Math.min(1D, shifted));
|
||||
}
|
||||
|
||||
private static double[] smooth(double[] offsets) {
|
||||
if (offsets.length < 2) {
|
||||
return offsets;
|
||||
}
|
||||
double[] smoothed = new double[offsets.length];
|
||||
double total = 0D;
|
||||
for (int station = 0; station <= Math.min(SMOOTHING_RADIUS, offsets.length - 1); station++) {
|
||||
total += offsets[station];
|
||||
}
|
||||
for (int station = 0; station < offsets.length; station++) {
|
||||
if (station > 0) {
|
||||
if (station + SMOOTHING_RADIUS < offsets.length) {
|
||||
total += offsets[station + SMOOTHING_RADIUS];
|
||||
}
|
||||
if (station > SMOOTHING_RADIUS) {
|
||||
total -= offsets[station - SMOOTHING_RADIUS - 1];
|
||||
}
|
||||
}
|
||||
int from = Math.max(0, station - SMOOTHING_RADIUS);
|
||||
int to = Math.min(offsets.length - 1, station + SMOOTHING_RADIUS);
|
||||
smoothed[station] = Math.max(-MAXIMUM_OFFSET, Math.min(MAXIMUM_OFFSET, total / (to - from + 1)));
|
||||
}
|
||||
smoothed[0] = 0D;
|
||||
smoothed[smoothed.length - 1] = 0D;
|
||||
return smoothed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
public record SurfaceBounds(int minimumX, int minimumZ, int maximumX, int maximumZ) {
|
||||
public SurfaceBounds {
|
||||
if (minimumX > maximumX || minimumZ > maximumZ) {
|
||||
throw new IllegalArgumentException("Surface bounds must be ordered.");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contains(int x, int z) {
|
||||
return x >= minimumX && x <= maximumX && z >= minimumZ && z <= maximumZ;
|
||||
}
|
||||
|
||||
public boolean intersects(int x, int z, int radius) {
|
||||
return (long) x + radius >= minimumX && (long) x - radius <= maximumX
|
||||
&& (long) z + radius >= minimumZ && (long) z - radius <= maximumZ;
|
||||
}
|
||||
|
||||
public SurfaceBounds expand(int distance) {
|
||||
return new SurfaceBounds(Math.subtractExact(minimumX, distance), Math.subtractExact(minimumZ, distance),
|
||||
Math.addExact(maximumX, distance), Math.addExact(maximumZ, distance));
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,8 @@ public final class SurfaceCellAdmission {
|
||||
public static boolean writable(HydrologyTerrainSample terrain, int seaLevel) {
|
||||
return terrain != null && !terrain.ocean() && terrain.naturalHeight() > seaLevel;
|
||||
}
|
||||
|
||||
public static boolean mouthLand(HydrologyTerrainSample terrain, int seaLevel) {
|
||||
return terrain != null && !terrain.ocean() && terrain.naturalHeight() >= seaLevel;
|
||||
}
|
||||
}
|
||||
|
||||
+55
-4
@@ -1,12 +1,16 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicChannelProfile;
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyGeometrySampler;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPoint;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -77,8 +81,8 @@ public final class SurfaceCourseBuilder {
|
||||
width[exposed] = width[last];
|
||||
depth[exposed] = depth[last];
|
||||
}
|
||||
List<HydraulicSegment> segments = SurfaceSegmentLabeler.label(
|
||||
worldSeed, courseId, x, z, head, width, depth, surface.banks());
|
||||
List<HydraulicSegment> segments = fallingTransitions(SurfaceSegmentLabeler.label(
|
||||
worldSeed, courseId, x, z, head, width, depth, surface.banks()));
|
||||
if (segments.isEmpty()) {
|
||||
return SurfaceCourseResult.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT, stations);
|
||||
}
|
||||
@@ -87,11 +91,58 @@ public final class SurfaceCourseBuilder {
|
||||
return new SurfaceCourseResult(
|
||||
segments,
|
||||
head[lastStation],
|
||||
last.width(),
|
||||
last.depth(),
|
||||
Math.max(1, (int) StrictMath.ceil(last.channelProfile().widthAt(last.centerline().size() - 1))),
|
||||
Math.max(1, (int) StrictMath.ceil(last.channelProfile().depthAt(last.centerline().size() - 1))),
|
||||
new HydrologyPoint(x[lastStation], head[lastStation], z[lastStation]),
|
||||
null,
|
||||
0
|
||||
);
|
||||
}
|
||||
private List<HydraulicSegment> fallingTransitions(List<HydraulicSegment> labelled) {
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(labelled);
|
||||
for (int index = 0; index + 1 < segments.size(); index++) {
|
||||
HydraulicSegment fall = segments.get(index);
|
||||
HydraulicSegment receiver = segments.get(index + 1);
|
||||
if (fall.type() != HydrologyFeatureType.WATERFALL || fall.centerline().size() != 2
|
||||
|| receiver.type() != HydrologyFeatureType.SURFACE_POOL || receiver.centerline().size() < 2) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample terrain = sampler.sample(fall.end().x(), fall.end().z());
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
|
||||
continue;
|
||||
}
|
||||
int incision = terrain.surfacePolicy().maximumIncision(surface.maximumIncision());
|
||||
incision = Math.min(incision, (int) StrictMath.floor(incision * terrain.incisionMultiplier()));
|
||||
if (terrain.naturalHeight() - fall.downstreamHeadY() + fall.depth() > incision) {
|
||||
continue;
|
||||
}
|
||||
HydrologyPoint throat = new HydrologyPoint(fall.end().x(), fall.upstreamHeadY(), fall.end().z());
|
||||
HydrologyPoint outflow = receiver.centerline().get(1);
|
||||
HydrologyPlannerSettings.Channel channel = surface.banks().channel();
|
||||
double outlineRatio = Math.max(channel.outlineMinimumRatio(),
|
||||
Math.min(channel.outlineMaximumRatio(), 1D + surface.banks().roughness()));
|
||||
double inletWidth = fall.channelProfile().maximumWidth();
|
||||
if (index > 0) {
|
||||
HydraulicChannelProfile approach = segments.get(index - 1).channelProfile();
|
||||
inletWidth = Math.max(inletWidth, approach.widthAt(approach.size() - 1));
|
||||
}
|
||||
HydraulicChannelProfile fallProfile = new HydraulicChannelProfile(
|
||||
new double[]{StrictMath.ceil(inletWidth * outlineRatio + 2D), receiver.channelProfile().widthAt(1)},
|
||||
new double[]{fall.channelProfile().maximumDepth(), receiver.channelProfile().depthAt(1)});
|
||||
segments.set(index, new HydraulicSegment(fall.id(), fall.courseId(), fall.type(),
|
||||
fall.upstreamHeadY(), fall.downstreamHeadY(),
|
||||
Math.max(1, (int) StrictMath.ceil(fallProfile.maximumWidth())),
|
||||
Math.max(1, (int) StrictMath.ceil(fallProfile.maximumDepth())), true, true,
|
||||
List.of(throat, outflow), fallProfile));
|
||||
HydraulicChannelProfile receiverProfile = receiver.channelProfile().size() == 1 ? receiver.channelProfile()
|
||||
: HydraulicChannelProfile.range(receiver.channelProfile().widths(), receiver.channelProfile().depths(),
|
||||
1, receiver.channelProfile().size());
|
||||
segments.set(index + 1, new HydraulicSegment(receiver.id(), receiver.courseId(), receiver.type(),
|
||||
receiver.upstreamHeadY(), receiver.downstreamHeadY(), receiver.width(), receiver.depth(),
|
||||
receiver.fallingFluid(), receiver.receivingPool(),
|
||||
receiver.centerline().subList(1, receiver.centerline().size()), receiverProfile));
|
||||
}
|
||||
return List.copyOf(segments);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPoint;
|
||||
import art.arcane.iris.engine.hydrology.HydrologySurfaceDropRaster;
|
||||
import art.arcane.iris.engine.hydrology.RiverCourse;
|
||||
import art.arcane.iris.engine.hydrology.RiverFootprint;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongIterator;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SurfaceExcavationMetrics {
|
||||
private static final int REACH_LENGTH = 16;
|
||||
private static final long WET_COLUMN = Long.MIN_VALUE;
|
||||
|
||||
private SurfaceExcavationMetrics() {
|
||||
}
|
||||
|
||||
public static long accumulate(RiverCourse course, SurfaceCenterline centerline, SurfaceFootprint footprint,
|
||||
HydrologySurfaceDropRaster drops, SurfaceBounds bounds, int[] stationVolumes) {
|
||||
Objects.requireNonNull(course, "course");
|
||||
Objects.requireNonNull(centerline, "centerline");
|
||||
Objects.requireNonNull(footprint, "footprint");
|
||||
Objects.requireNonNull(drops, "drops");
|
||||
Objects.requireNonNull(stationVolumes, "stationVolumes");
|
||||
if (stationVolumes.length != centerline.size()) {
|
||||
throw new IllegalArgumentException("Excavation volumes must match centerline stations.");
|
||||
}
|
||||
Long2LongOpenHashMap cuts = new Long2LongOpenHashMap(footprint.columns().size() + drops.columns().size());
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (bounds == null || bounds.contains(column.x(), column.z())) {
|
||||
accumulate(cuts, column.x(), column.z(), column.terrain().naturalHeight(), column.layer(), column.station());
|
||||
}
|
||||
}
|
||||
Long2ObjectOpenHashMap<StationRange> ranges = dropStations(course);
|
||||
for (HydrologyColumnSample column : drops.columns()) {
|
||||
if (bounds != null && !bounds.contains(column.x(), column.z())) {
|
||||
continue;
|
||||
}
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (layer.oceanApron() || !layer.channel() && !layer.terrainOwned()) {
|
||||
continue;
|
||||
}
|
||||
StationRange range = ranges.get(layer.feature().segmentId());
|
||||
if (range != null) {
|
||||
int station = layer.channel() ? range.first() : nearestStation(centerline, range, column.x(), column.z());
|
||||
accumulate(cuts, column.x(), column.z(), column.naturalHeight(), layer, station);
|
||||
}
|
||||
}
|
||||
}
|
||||
long total = 0L;
|
||||
LongIterator iterator = cuts.values().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
long encoded = iterator.nextLong();
|
||||
if (encoded == WET_COLUMN) {
|
||||
continue;
|
||||
}
|
||||
int station = (int) (encoded >>> 32);
|
||||
int depth = (int) encoded;
|
||||
stationVolumes[station] = (int) Math.min(Integer.MAX_VALUE, (long) stationVolumes[station] + depth);
|
||||
total += depth;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public static int maximumVolumePerBlock(SurfaceCenterline centerline, int[] stationVolumes) {
|
||||
Objects.requireNonNull(centerline, "centerline");
|
||||
Objects.requireNonNull(stationVolumes, "stationVolumes");
|
||||
if (stationVolumes.length != centerline.size()) {
|
||||
throw new IllegalArgumentException("Excavation volumes must match centerline stations.");
|
||||
}
|
||||
double[] distance = new double[stationVolumes.length];
|
||||
long[] volume = new long[stationVolumes.length + 1];
|
||||
for (int station = 0; station < stationVolumes.length; station++) {
|
||||
if (stationVolumes[station] < 0) {
|
||||
throw new IllegalArgumentException("Excavation volumes cannot be negative.");
|
||||
}
|
||||
double run = station == 0 ? 0D : StrictMath.hypot(
|
||||
(double) centerline.x()[station] - centerline.x()[station - 1],
|
||||
(double) centerline.z()[station] - centerline.z()[station - 1]);
|
||||
distance[station] = station == 0 ? 0D : distance[station - 1] + run;
|
||||
volume[station + 1] = volume[station] + stationVolumes[station];
|
||||
}
|
||||
int largestSection = 0;
|
||||
int first = 0;
|
||||
int last = 0;
|
||||
for (int station = 0; station < stationVolumes.length; station++) {
|
||||
while (first < station && distance[station] - distance[first] > REACH_LENGTH / 2D) {
|
||||
first++;
|
||||
}
|
||||
while (last + 1 < stationVolumes.length && distance[last + 1] - distance[station] <= REACH_LENGTH / 2D) {
|
||||
last++;
|
||||
}
|
||||
double section = StrictMath.ceil((volume[last + 1] - volume[first]) / (double) REACH_LENGTH);
|
||||
largestSection = Math.max(largestSection, (int) Math.min(Integer.MAX_VALUE, section));
|
||||
}
|
||||
return largestSection;
|
||||
}
|
||||
|
||||
private static void accumulate(Long2LongOpenHashMap cuts, int x, int z, int naturalHeight,
|
||||
HydrologyColumnLayer layer, int station) {
|
||||
if (layer.oceanApron()) {
|
||||
return;
|
||||
}
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
if (layer.channel() && layer.fluidOwned() && layer.fluidHeadY() > layer.bedY()) {
|
||||
cuts.put(key, WET_COLUMN);
|
||||
return;
|
||||
}
|
||||
if (layer.channel() || !layer.terrainOwned()) {
|
||||
return;
|
||||
}
|
||||
int depth = Math.max(0, naturalHeight - layer.bedY());
|
||||
long previous = cuts.get(key);
|
||||
if (depth > 0 && previous != WET_COLUMN
|
||||
&& (depth > (int) previous || depth == (int) previous && station < (int) (previous >>> 32))) {
|
||||
cuts.put(key, (long) station << 32 | depth & 0xffffffffL);
|
||||
}
|
||||
}
|
||||
|
||||
private static Long2ObjectOpenHashMap<StationRange> dropStations(RiverCourse course) {
|
||||
Long2ObjectOpenHashMap<StationRange> ranges = new Long2ObjectOpenHashMap<>();
|
||||
int station = 0;
|
||||
HydrologyPoint previous = null;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
int first = station;
|
||||
for (int index = 0; index < segment.centerline().size(); index++) {
|
||||
HydrologyPoint point = segment.centerline().get(index);
|
||||
if (previous != null) {
|
||||
station += Math.max(Math.abs(point.x() - previous.x()), Math.abs(point.z() - previous.z()));
|
||||
}
|
||||
if (index == 0) {
|
||||
first = station;
|
||||
}
|
||||
previous = point;
|
||||
}
|
||||
if (segment.type().isSurface() && segment.fallingFluid()) {
|
||||
ranges.put(segment.id(), new StationRange(first, station));
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
private static int nearestStation(SurfaceCenterline centerline, StationRange range, int x, int z) {
|
||||
int nearest = range.first();
|
||||
long minimumDistance = Long.MAX_VALUE;
|
||||
for (int station = range.first(); station <= range.last(); station++) {
|
||||
long deltaX = (long) x - centerline.x()[station];
|
||||
long deltaZ = (long) z - centerline.z()[station];
|
||||
long distance = deltaX * deltaX + deltaZ * deltaZ;
|
||||
if (distance < minimumDistance) {
|
||||
minimumDistance = distance;
|
||||
nearest = station;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
private record StationRange(int first, int last) {
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,6 @@ public final class SurfaceFeatureRefs {
|
||||
HydraulicSegment segment,
|
||||
SurfaceRole role,
|
||||
boolean source,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
int flowX,
|
||||
int flowZ
|
||||
) {
|
||||
@@ -48,9 +45,9 @@ public final class SurfaceFeatureRefs {
|
||||
segment.type(),
|
||||
courseId,
|
||||
segment.id(),
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
segment.start().x(),
|
||||
segment.start().y(),
|
||||
segment.start().z(),
|
||||
featureFlowX,
|
||||
featureFlowZ,
|
||||
source
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record SurfaceFootprint(List<SurfaceLayerColumn> columns, int uncontainedWetCells) {
|
||||
public record SurfaceFootprint(List<SurfaceLayerColumn> columns, int uncontainedWetCells,
|
||||
HydrologyCandidateRejection rejection, int rejectionDetail, long bankExcavation) {
|
||||
public SurfaceFootprint {
|
||||
columns = List.copyOf(Objects.requireNonNull(columns, "columns"));
|
||||
}
|
||||
|
||||
public static SurfaceFootprint empty() {
|
||||
return new SurfaceFootprint(List.of(), 0);
|
||||
return new SurfaceFootprint(List.of(), 0, null, 0, 0L);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return columns.isEmpty();
|
||||
}
|
||||
|
||||
public boolean accepted() {
|
||||
return rejection == null;
|
||||
}
|
||||
}
|
||||
|
||||
+166
-42
@@ -1,19 +1,24 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicChannelProfile;
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyGeometrySampler;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyHash;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyOceanReceiver;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPoint;
|
||||
import art.arcane.iris.engine.hydrology.HydrologySurfaceDropRaster;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
|
||||
import art.arcane.iris.engine.hydrology.RiverCourse;
|
||||
import art.arcane.iris.engine.hydrology.RiverCourseType;
|
||||
import art.arcane.iris.engine.hydrology.RiverFootprint;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
@@ -42,66 +47,169 @@ public final class SurfaceFootprintCompiler {
|
||||
}
|
||||
|
||||
public SurfaceFootprint compile(RiverCourse course) {
|
||||
return compile(course, null);
|
||||
}
|
||||
|
||||
public SurfaceFootprint compile(RiverCourse course, SurfaceBounds bounds) {
|
||||
boolean pool = course.type() == RiverCourseType.SURFACE_POOL;
|
||||
if (course.type() != RiverCourseType.SURFACE && !pool) {
|
||||
return SurfaceFootprint.empty();
|
||||
}
|
||||
ArrayList<HydraulicSegment> exposed = new ArrayList<>();
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (!exposedSegment(segment)) {
|
||||
break;
|
||||
HydrologyTerrainSampler receivingSampler = HydrologyOceanReceiver.forCourse(settings, sampler, course);
|
||||
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(settings, receivingSampler, geometry, course,
|
||||
bounds == null ? null : bounds.expand(2));
|
||||
int[] offsets = stationOffsets(course.segments());
|
||||
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>();
|
||||
int uncontained = 0;
|
||||
long excavation = 0L;
|
||||
HydrologyCandidateRejection rejection = null;
|
||||
int detail = 0;
|
||||
int first = 0;
|
||||
while (first < course.segments().size()) {
|
||||
if (!exposedSegment(course.segments().get(first))) {
|
||||
first++;
|
||||
continue;
|
||||
}
|
||||
exposed.add(segment);
|
||||
int after = first + 1;
|
||||
while (after < course.segments().size() && exposedSegment(course.segments().get(after))) {
|
||||
after++;
|
||||
}
|
||||
SurfaceFootprint run = compileRun(course, first, after, offsets[first], bounds, drops, pool, receivingSampler);
|
||||
columns.addAll(run.columns());
|
||||
uncontained += run.uncontainedWetCells();
|
||||
excavation += run.bankExcavation();
|
||||
if (rejection == null && run.rejection() != null) {
|
||||
rejection = run.rejection();
|
||||
detail = run.rejectionDetail();
|
||||
}
|
||||
first = after;
|
||||
}
|
||||
if (exposed.isEmpty()) {
|
||||
return SurfaceFootprint.empty();
|
||||
if (rejection == null && !drops.containsRequiredFluid(course, bounds)) {
|
||||
rejection = HydrologyCandidateRejection.SURFACE_DROP_UNSUPPORTED;
|
||||
}
|
||||
SurfaceFootprint footprint = new SurfaceFootprint(columns, uncontained, rejection, detail, excavation);
|
||||
if (drops.columns().isEmpty()) {
|
||||
return footprint;
|
||||
}
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
points.addAll(segment.centerline());
|
||||
}
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(points);
|
||||
int[] stationVolumes = new int[centerline.size()];
|
||||
excavation = SurfaceExcavationMetrics.accumulate(course, centerline, footprint, drops, bounds, stationVolumes);
|
||||
int volume = SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, stationVolumes);
|
||||
if (rejection == null && volume > settings.surface().banks().erosion().excavation().maximumVolumePerBlock()) {
|
||||
rejection = HydrologyCandidateRejection.SURFACE_BANK_BUDGET;
|
||||
detail = volume;
|
||||
}
|
||||
return new SurfaceFootprint(columns, uncontained, rejection, detail, excavation);
|
||||
}
|
||||
|
||||
private SurfaceFootprint compileRun(RiverCourse course, int first, int after, int stationOffset,
|
||||
SurfaceBounds bounds, HydrologySurfaceDropRaster drops, boolean pool,
|
||||
HydrologyTerrainSampler receivingSampler) {
|
||||
List<HydraulicSegment> exposed = course.segments().subList(first, after);
|
||||
Stations stations = stations(exposed);
|
||||
if (stations.count() < 1) {
|
||||
return SurfaceFootprint.empty();
|
||||
}
|
||||
SurfaceTerminal terminal = pool ? SurfaceTerminal.SINKHOLE : terminal(course, exposed);
|
||||
boolean fallingEnd = after < course.segments().size() && course.segments().get(after).fallingFluid();
|
||||
SurfaceTerminal terminal = pool ? SurfaceTerminal.SINKHOLE
|
||||
: fallingEnd ? SurfaceTerminal.TRIBUTARY : terminal(course, exposed);
|
||||
boolean coastalChannel = first == 0 && after == course.segments().size()
|
||||
&& exposed.getFirst().type() == HydrologyFeatureType.MOUTH
|
||||
&& exposed.getLast().type() == HydrologyFeatureType.MOUTH;
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(stations.points());
|
||||
ChannelProfile channel = pool
|
||||
? poolProfile(exposed.getFirst(), centerline)
|
||||
: new ChannelProfileBuilder(settings.surface(), sampler, geometry)
|
||||
.build(centerline, course.profileKey(), terminal == SurfaceTerminal.OCEAN_MOUTH);
|
||||
ChannelProfile channel = pool ? poolProfile(exposed.getFirst(), centerline) : acceptedProfile(stations, centerline);
|
||||
String poolBiome = pool ? poolBiome(course.profileKey()) : null;
|
||||
ValleyProfile valley = ValleyProfile.fromHeads(stations.head(), stations.exposedStations());
|
||||
ErosionField field = new ErosionFieldCompiler(settings.surface(), sampler, settings.seaLevel()).compile(
|
||||
HydrologyHash.mix(course.id(), COURSE_SEED_SALT),
|
||||
centerline,
|
||||
channel,
|
||||
valley,
|
||||
terminal,
|
||||
settings.outlets().maximumOceanApron(),
|
||||
pool ? HydrologyPlannerSettings.Ponds.none() : settings.surface().banks().ponds()
|
||||
);
|
||||
HydrologyPlannerSettings.Ponds disabled = HydrologyPlannerSettings.Ponds.none();
|
||||
HydrologyPlannerSettings.Ponds ponds = pool || coastalChannel ? disabled
|
||||
: new HydrologyPlannerSettings.Ponds(first == 0 ? settings.surface().banks().ponds().source() : disabled.source(),
|
||||
fallingEnd ? disabled.terminal() : settings.surface().banks().ponds().terminal());
|
||||
SurfaceRunBoundary boundary = runBoundary(course, first, after, centerline, channel);
|
||||
ErosionField field = new ErosionFieldCompiler(settings.surface(), receivingSampler, settings.seaLevel()).compile(
|
||||
HydrologyHash.mix(course.id(), COURSE_SEED_SALT), centerline, channel, valley, terminal,
|
||||
settings.outlets().maximumOceanApron(), ponds, new SurfaceRasterContext(bounds, drops, boundary));
|
||||
ArrayList<SurfaceColumn> ordered = new ArrayList<>(field.columns().values());
|
||||
ordered.sort(Comparator
|
||||
.comparingInt(SurfaceColumn::station)
|
||||
ordered.sort(Comparator.comparingInt(SurfaceColumn::station)
|
||||
.thenComparingLong((SurfaceColumn column) -> RiverFootprint.pack(column.x(), column.z())));
|
||||
SurfaceFeatureRefs features = new SurfaceFeatureRefs(course.id());
|
||||
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>(ordered.size());
|
||||
for (SurfaceColumn column : ordered) {
|
||||
if (bounds != null && !bounds.contains(column.x(), column.z())) {
|
||||
continue;
|
||||
}
|
||||
HydraulicSegment segment = exposed.get(stations.segmentIndex()[column.station()]);
|
||||
int flowX = (int) StrictMath.round(centerline.tangentX()[column.station()]);
|
||||
int flowZ = (int) StrictMath.round(centerline.tangentZ()[column.station()]);
|
||||
boolean source = column.station() == 0 && column.role() == SurfaceRole.CHANNEL && !column.apron()
|
||||
boolean source = first == 0 && !coastalChannel && column.station() == 0
|
||||
&& column.role() == SurfaceRole.CHANNEL && !column.apron()
|
||||
&& column.x() == centerline.x()[0] && column.z() == centerline.z()[0];
|
||||
int y = column.role() == SurfaceRole.CHANNEL ? column.headY() : column.height();
|
||||
HydrologyFeatureRef feature = features.feature(segment, column.role(), source, column.x(), y, column.z(), flowX, flowZ);
|
||||
HydrologyFeatureRef feature = features.feature(segment, column.role(), source, flowX, flowZ);
|
||||
String biomeOverride = pool ? poolBiome == null ? column.terrain().parentBiomeKey() : poolBiome : null;
|
||||
columns.add(new SurfaceLayerColumn(
|
||||
column.x(),
|
||||
column.z(),
|
||||
column.terrain(),
|
||||
layer(feature, column, course.profileKey(), biomeOverride),
|
||||
column.role(),
|
||||
column.apron()
|
||||
));
|
||||
columns.add(new SurfaceLayerColumn(column.x(), column.z(), column.terrain(),
|
||||
layer(feature, column, course.profileKey(), biomeOverride), column.role(), column.apron(),
|
||||
stationOffset + column.station()));
|
||||
}
|
||||
return new SurfaceFootprint(columns, field.uncontainedWetCells());
|
||||
return new SurfaceFootprint(columns, field.uncontainedWetCells(), field.rejection(), field.rejectionDetail(), field.bankExcavation());
|
||||
}
|
||||
|
||||
private SurfaceRunBoundary runBoundary(RiverCourse course, int first, int after,
|
||||
SurfaceCenterline centerline, ChannelProfile channel) {
|
||||
SurfaceRunBoundary.Plane start = null;
|
||||
SurfaceRunBoundary.Plane end = null;
|
||||
if (first > 0 && course.segments().get(first - 1).fallingFluid()) {
|
||||
HydraulicSegment fall = course.segments().get(first - 1);
|
||||
start = SurfaceRunBoundary.Plane.between(fall.end(), fall.start(), fall.end(), boundaryRadius(channel, 0));
|
||||
}
|
||||
if (after < course.segments().size() && course.segments().get(after).fallingFluid()) {
|
||||
HydraulicSegment fall = course.segments().get(after);
|
||||
HydrologyPoint last = course.segments().get(after - 1).end();
|
||||
HydrologyPoint direction = last.x() == fall.start().x() && last.z() == fall.start().z() ? fall.end() : fall.start();
|
||||
end = SurfaceRunBoundary.Plane.between(fall.start(), last, direction, boundaryRadius(channel, centerline.size() - 1));
|
||||
}
|
||||
return new SurfaceRunBoundary(start, end);
|
||||
}
|
||||
|
||||
private double boundaryRadius(ChannelProfile channel, int station) {
|
||||
return channel.halfWidth()[station] * (1D + settings.surface().banks().roughness())
|
||||
+ settings.surface().banks().maximumBlendWidth() + settings.surface().shoreWidth() + 2D;
|
||||
}
|
||||
|
||||
private static int[] stationOffsets(List<HydraulicSegment> segments) {
|
||||
int[] offsets = new int[segments.size()];
|
||||
int station = 0;
|
||||
HydrologyPoint previous = null;
|
||||
for (int segmentIndex = 0; segmentIndex < segments.size(); segmentIndex++) {
|
||||
List<HydrologyPoint> points = segments.get(segmentIndex).centerline();
|
||||
for (int pointIndex = 0; pointIndex < points.size(); pointIndex++) {
|
||||
HydrologyPoint point = points.get(pointIndex);
|
||||
if (previous != null) {
|
||||
station += Math.max(Math.abs(point.x() - previous.x()), Math.abs(point.z() - previous.z()));
|
||||
}
|
||||
if (pointIndex == 0) {
|
||||
offsets[segmentIndex] = station;
|
||||
}
|
||||
previous = point;
|
||||
}
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
private ChannelProfile acceptedProfile(Stations stations, SurfaceCenterline centerline) {
|
||||
int count = centerline.size();
|
||||
double[] halfWidth = new double[count];
|
||||
double[] depth = new double[count];
|
||||
double[] bank = new double[count];
|
||||
for (int station = 0; station < count; station++) {
|
||||
halfWidth[station] = Math.max(0.5D, stations.width()[station] / 2D);
|
||||
depth[station] = Math.max(1D, stations.depth()[station]);
|
||||
HydrologyTerrainSample terrain = sampler.sample(centerline.x()[station], centerline.z()[station]);
|
||||
bank[station] = terrain == null ? 1D : terrain.bankMultiplier();
|
||||
}
|
||||
return new ChannelProfile(halfWidth, depth, bank);
|
||||
}
|
||||
|
||||
private ChannelProfile poolProfile(HydraulicSegment segment, SurfaceCenterline centerline) {
|
||||
@@ -163,7 +271,7 @@ public final class SurfaceFootprintCompiler {
|
||||
channel,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
channel || column.height() != terrain.naturalHeight(),
|
||||
channel,
|
||||
false,
|
||||
profileKey,
|
||||
@@ -187,13 +295,15 @@ public final class SurfaceFootprintCompiler {
|
||||
return SurfaceTerminal.COASTAL_GROTTO;
|
||||
}
|
||||
}
|
||||
return SurfaceTerminal.SINKHOLE;
|
||||
return course.surfaceSinkholeContinuation() ? SurfaceTerminal.SINKHOLE : SurfaceTerminal.TRIBUTARY;
|
||||
}
|
||||
|
||||
private static Stations stations(List<HydraulicSegment> exposed) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
IntArrayList segmentIndices = new IntArrayList();
|
||||
IntArrayList heads = new IntArrayList();
|
||||
DoubleArrayList widths = new DoubleArrayList();
|
||||
DoubleArrayList depths = new DoubleArrayList();
|
||||
int exposedStations = 0;
|
||||
for (int segmentIndex = 0; segmentIndex < exposed.size(); segmentIndex++) {
|
||||
HydraulicSegment segment = exposed.get(segmentIndex);
|
||||
@@ -202,7 +312,8 @@ public final class SurfaceFootprintCompiler {
|
||||
for (int pointIndex = 0; pointIndex < centerline.size(); pointIndex++) {
|
||||
HydrologyPoint current = centerline.get(pointIndex);
|
||||
if (pointIndex == 0) {
|
||||
appendStation(points, segmentIndices, heads, current, segmentIndex);
|
||||
appendStation(points, segmentIndices, heads, widths, depths, current, segmentIndex,
|
||||
segment.channelProfile().widthAt(pointIndex), segment.channelProfile().depthAt(pointIndex));
|
||||
continue;
|
||||
}
|
||||
HydrologyPoint previous = centerline.get(pointIndex - 1);
|
||||
@@ -214,22 +325,32 @@ public final class SurfaceFootprintCompiler {
|
||||
(int) StrictMath.round(previous.y() + (current.y() - previous.y()) * progress),
|
||||
(int) StrictMath.round(previous.z() + (current.z() - previous.z()) * progress)
|
||||
);
|
||||
appendStation(points, segmentIndices, heads, cell, segmentIndex);
|
||||
HydraulicChannelProfile profile = segment.channelProfile();
|
||||
double width = profile.widthAt(pointIndex - 1)
|
||||
+ (profile.widthAt(pointIndex) - profile.widthAt(pointIndex - 1)) * progress;
|
||||
double depth = profile.depthAt(pointIndex - 1)
|
||||
+ (profile.depthAt(pointIndex) - profile.depthAt(pointIndex - 1)) * progress;
|
||||
appendStation(points, segmentIndices, heads, widths, depths, cell, segmentIndex, width, depth);
|
||||
}
|
||||
}
|
||||
if (!mouth) {
|
||||
exposedStations = points.size();
|
||||
}
|
||||
}
|
||||
return new Stations(points, segmentIndices.toIntArray(), heads.toIntArray(), exposedStations);
|
||||
return new Stations(points, segmentIndices.toIntArray(), heads.toIntArray(), widths.toDoubleArray(),
|
||||
depths.toDoubleArray(), exposedStations);
|
||||
}
|
||||
|
||||
private static void appendStation(
|
||||
ArrayList<HydrologyPoint> points,
|
||||
IntArrayList segmentIndices,
|
||||
IntArrayList heads,
|
||||
DoubleArrayList widths,
|
||||
DoubleArrayList depths,
|
||||
HydrologyPoint cell,
|
||||
int segmentIndex
|
||||
int segmentIndex,
|
||||
double width,
|
||||
double depth
|
||||
) {
|
||||
if (!points.isEmpty()) {
|
||||
HydrologyPoint last = points.getLast();
|
||||
@@ -240,9 +361,12 @@ public final class SurfaceFootprintCompiler {
|
||||
points.add(cell);
|
||||
segmentIndices.add(segmentIndex);
|
||||
heads.add(cell.y());
|
||||
widths.add(width);
|
||||
depths.add(depth);
|
||||
}
|
||||
|
||||
private record Stations(List<HydrologyPoint> points, int[] segmentIndex, int[] head, int exposedStations) {
|
||||
private record Stations(List<HydrologyPoint> points, int[] segmentIndex, int[] head, double[] width,
|
||||
double[] depth, int exposedStations) {
|
||||
private int count() {
|
||||
return points.size();
|
||||
}
|
||||
|
||||
@@ -11,11 +11,15 @@ public record SurfaceLayerColumn(
|
||||
HydrologyTerrainSample terrain,
|
||||
HydrologyColumnLayer layer,
|
||||
SurfaceRole role,
|
||||
boolean apron
|
||||
boolean apron,
|
||||
int station
|
||||
) {
|
||||
public SurfaceLayerColumn {
|
||||
Objects.requireNonNull(terrain, "terrain");
|
||||
Objects.requireNonNull(layer, "layer");
|
||||
Objects.requireNonNull(role, "role");
|
||||
if (station < 0) {
|
||||
throw new IllegalArgumentException("A surface column requires a nonnegative station.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologySurfaceDropRaster;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record SurfaceRasterContext(SurfaceBounds bounds, HydrologySurfaceDropRaster drops, SurfaceRunBoundary boundary) {
|
||||
public SurfaceRasterContext {
|
||||
Objects.requireNonNull(drops, "drops");
|
||||
Objects.requireNonNull(boundary, "boundary");
|
||||
}
|
||||
|
||||
public static SurfaceRasterContext none() {
|
||||
return bounded(null);
|
||||
}
|
||||
|
||||
public static SurfaceRasterContext bounded(SurfaceBounds bounds) {
|
||||
return new SurfaceRasterContext(bounds, HydrologySurfaceDropRaster.empty(), SurfaceRunBoundary.unbounded());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPoint;
|
||||
|
||||
public record SurfaceRunBoundary(Plane start, Plane end) {
|
||||
public static SurfaceRunBoundary unbounded() {
|
||||
return new SurfaceRunBoundary(null, null);
|
||||
}
|
||||
|
||||
public boolean allows(int x, int z) {
|
||||
return (start == null || start.projection(x, z) >= 0L)
|
||||
&& (end == null || end.projection(x, z) <= 0L);
|
||||
}
|
||||
|
||||
public record Plane(int x, int z, int flowX, int flowZ, double radius) {
|
||||
public static Plane between(HydrologyPoint origin, HydrologyPoint upstream, HydrologyPoint downstream, double radius) {
|
||||
return new Plane(origin.x(), origin.z(), downstream.x() - upstream.x(), downstream.z() - upstream.z(), radius);
|
||||
}
|
||||
|
||||
private long projection(int pointX, int pointZ) {
|
||||
long dx = (long) pointX - x;
|
||||
long dz = (long) pointZ - z;
|
||||
return (double) dx * dx + (double) dz * dz > radius * radius ? 0L : dx * flowX + dz * flowZ;
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-8
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicChannelProfile;
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyHash;
|
||||
@@ -80,16 +81,12 @@ public final class SurfaceSegmentLabeler {
|
||||
int startStation = run.start;
|
||||
int endStation = run.end + 1;
|
||||
ArrayList<HydrologyPoint> centerline = new ArrayList<>(endStation - startStation + 1);
|
||||
double totalWidth = 0D;
|
||||
double totalDepth = 0D;
|
||||
for (int station = startStation; station <= endStation; station++) {
|
||||
centerline.add(new HydrologyPoint(x[station], head[station], z[station]));
|
||||
totalWidth += width[station];
|
||||
totalDepth += depth[station];
|
||||
}
|
||||
int stations = endStation - startStation + 1;
|
||||
int segmentWidth = Math.max(1, (int) StrictMath.round(totalWidth / stations));
|
||||
int segmentDepth = Math.max(1, (int) StrictMath.round(totalDepth / stations));
|
||||
HydraulicChannelProfile channelProfile = HydraulicChannelProfile.range(width, depth, startStation, endStation + 1);
|
||||
int segmentWidth = Math.max(1, (int) StrictMath.ceil(channelProfile.maximumWidth()));
|
||||
int segmentDepth = Math.max(1, (int) StrictMath.ceil(channelProfile.maximumDepth()));
|
||||
int index = segments.size();
|
||||
segments.add(new HydraulicSegment(
|
||||
HydrologyHash.mix(worldSeed, SEGMENT_SALT, courseId, index, run.type.ordinal()),
|
||||
@@ -101,7 +98,8 @@ public final class SurfaceSegmentLabeler {
|
||||
segmentDepth,
|
||||
false,
|
||||
false,
|
||||
centerline
|
||||
centerline,
|
||||
channelProfile
|
||||
));
|
||||
}
|
||||
return List.copyOf(segments);
|
||||
|
||||
+64
-31
@@ -8,11 +8,12 @@ import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ValleyProfileSolver {
|
||||
private static final int BANK_LOOKAHEAD = 2;
|
||||
private static final int BANK_LOOKAHEAD = SurfaceBankSupport.BANK_LOOKAHEAD;
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
private final int seaLevel;
|
||||
private final int minimumCourseLength;
|
||||
private final SurfaceBankSupport bankSupport;
|
||||
|
||||
public ValleyProfileSolver(
|
||||
HydrologyPlannerSettings.Surface surface,
|
||||
@@ -24,6 +25,7 @@ public final class ValleyProfileSolver {
|
||||
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
||||
this.seaLevel = seaLevel;
|
||||
this.minimumCourseLength = minimumCourseLength;
|
||||
this.bankSupport = new SurfaceBankSupport(surface, seaLevel);
|
||||
}
|
||||
|
||||
public ValleyProfile solve(
|
||||
@@ -41,50 +43,37 @@ public final class ValleyProfileSolver {
|
||||
double roughness = surface.banks().roughness();
|
||||
int exposed = count;
|
||||
for (int station = 0; station < count; station++) {
|
||||
double outline = channel.halfWidth()[station] * (1D + roughness);
|
||||
// Every cell from the narrowest possible outline outward can end up beside water, so it joins the minimum.
|
||||
double innerOutline = channel.halfWidth()[station] * (1D - roughness);
|
||||
double reach = outline + 2D;
|
||||
int stationX = centerline.x()[station];
|
||||
int stationZ = centerline.z()[station];
|
||||
HydrologyTerrainSample center = sampler.sample(stationX, stationZ);
|
||||
if (!SurfaceCellAdmission.writable(center, seaLevel)) {
|
||||
if (!writable(center, terminal)) {
|
||||
exposed = station;
|
||||
break;
|
||||
}
|
||||
centerNatural[station] = center.naturalHeight();
|
||||
channelIncision[station] = center.surfacePolicy().maximumIncision(surface.maximumIncision());
|
||||
incisionMultiplier[station] = center.incisionMultiplier();
|
||||
double normalX = centerline.normalX(station);
|
||||
double normalZ = centerline.normalZ(station);
|
||||
int minimum = Integer.MAX_VALUE;
|
||||
int maximum = Integer.MIN_VALUE;
|
||||
boolean blocked = false;
|
||||
for (double offset = -reach; offset <= reach && !blocked; offset += 0.5D) {
|
||||
int cellX = (int) StrictMath.round(stationX + normalX * offset);
|
||||
int cellZ = (int) StrictMath.round(stationZ + normalZ * offset);
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
if (Math.abs(offset) < innerOutline - 0.75D) {
|
||||
continue;
|
||||
}
|
||||
minimum = Math.min(minimum, terrain.naturalHeight());
|
||||
maximum = Math.max(maximum, terrain.naturalHeight());
|
||||
}
|
||||
if (blocked) {
|
||||
SurfaceBankSupport.CrossSection section = bankSupport.crossSection(sampler,
|
||||
station(centerline, channel, station), terminal == SurfaceTerminal.OCEAN_MOUTH);
|
||||
if (section.blocked()) {
|
||||
exposed = station;
|
||||
break;
|
||||
}
|
||||
crossMin[station] = minimum;
|
||||
crossMax[station] = maximum;
|
||||
crossMin[station] = section.minimum();
|
||||
crossMax[station] = section.maximum();
|
||||
}
|
||||
if (exposed < 2 || exposed < minimumCourseLength) {
|
||||
return ValleyProfile.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT, exposed);
|
||||
if (terminal == SurfaceTerminal.COASTAL_GROTTO) {
|
||||
while (exposed > 0 && !terminalCapSupported(centerline, channel, exposed - 1)) {
|
||||
exposed--;
|
||||
}
|
||||
}
|
||||
if (exposed < count && terminal != SurfaceTerminal.OCEAN_MOUTH) {
|
||||
double exposedLength = exposedLength(centerline, exposed);
|
||||
if (exposed < 2 || exposedLength < minimumCourseLength) {
|
||||
return ValleyProfile.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT,
|
||||
(int) StrictMath.floor(exposedLength));
|
||||
}
|
||||
if (exposed < count && terminal != SurfaceTerminal.OCEAN_MOUTH
|
||||
&& terminal != SurfaceTerminal.COASTAL_GROTTO) {
|
||||
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_EXPOSURE, exposed);
|
||||
}
|
||||
// The channel's rounded end caps reach past the first and last stations; the ground under
|
||||
@@ -96,6 +85,9 @@ public final class ValleyProfileSolver {
|
||||
int sink = surface.banks().sink();
|
||||
int[] head = new int[count];
|
||||
for (int station = 0; station < exposed; station++) {
|
||||
int incomingCeiling = station == 0 ? crossMin[station]
|
||||
: Math.min(crossMin[station], head[station - 1] + sink);
|
||||
crossMin[station] = bankSupport.perimeter(sampler, station(centerline, channel, station), incomingCeiling).minimum();
|
||||
// The cells beside a station's water include cells the next stations own, so on ground
|
||||
// that falls gently along the course the head steps down a station early rather than
|
||||
// onto a bank the lip cannot raise above its natural height. A larger drop is a step or
|
||||
@@ -177,6 +169,41 @@ public final class ValleyProfileSolver {
|
||||
return new ValleyProfile(head, crossMin, crossMax, centerNatural, exposed, null, 0);
|
||||
}
|
||||
|
||||
private static SurfaceBankSupport.Station station(SurfaceCenterline centerline, ChannelProfile channel, int station) {
|
||||
return new SurfaceBankSupport.Station(centerline.x()[station], centerline.z()[station],
|
||||
centerline.tangentX()[station], centerline.tangentZ()[station], channel.halfWidth()[station]);
|
||||
}
|
||||
|
||||
private static double exposedLength(SurfaceCenterline centerline, int exposed) {
|
||||
double length = 0D;
|
||||
for (int station = 1; station < exposed; station++) {
|
||||
length += StrictMath.hypot((double) centerline.x()[station] - centerline.x()[station - 1],
|
||||
(double) centerline.z()[station] - centerline.z()[station - 1]);
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
private boolean terminalCapSupported(SurfaceCenterline centerline, ChannelProfile channel, int station) {
|
||||
double reach = channel.halfWidth()[station] * (1D + surface.banks().roughness()) + 2D;
|
||||
int radius = (int) StrictMath.ceil(reach);
|
||||
double reachSquared = reach * reach;
|
||||
double tangentX = centerline.tangentX()[station];
|
||||
double tangentZ = centerline.tangentZ()[station];
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
for (int dx = -radius; dx <= radius; dx++) {
|
||||
if ((double) dx * dx + (double) dz * dz > reachSquared
|
||||
|| dx * tangentX + dz * tangentZ < 0D) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample terrain = sampler.sample(centerline.x()[station] + dx, centerline.z()[station] + dz);
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean cutFits(
|
||||
int station,
|
||||
int head,
|
||||
@@ -188,6 +215,12 @@ public final class ValleyProfileSolver {
|
||||
return cut(station, head, channel, centerNatural) <= permitted(station, incisionMultiplier, maximumIncision);
|
||||
}
|
||||
|
||||
private boolean writable(HydrologyTerrainSample terrain, SurfaceTerminal terminal) {
|
||||
return terminal == SurfaceTerminal.OCEAN_MOUTH
|
||||
? SurfaceCellAdmission.mouthLand(terrain, seaLevel)
|
||||
: SurfaceCellAdmission.writable(terrain, seaLevel);
|
||||
}
|
||||
|
||||
/** How far below the natural ground the bed under a head sits at a station. */
|
||||
private static int cut(int station, int head, ChannelProfile channel, int[] centerNatural) {
|
||||
return centerNatural[station] - (head - (int) StrictMath.round(channel.depth()[station]));
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCaveVoxelViewFactory;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.RiverFootprint;
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
|
||||
@@ -227,7 +228,7 @@ public final class MantleHydrologyCaveVoxelView implements CaveVoxelView {
|
||||
int surfaceHeight = sample == null
|
||||
? (int) Math.round(complex.getNaturalHeightStream().getDouble(x, z))
|
||||
: sample.terrainHeight();
|
||||
return new TerrainColumn(surfaceHeight, sample != null && sample.primarySurfaceLayer().isPresent());
|
||||
return new TerrainColumn(surfaceHeight, sample != null && sample.primarySurfaceLayer().filter(HydrologyColumnLayer::terrainOwned).isPresent());
|
||||
}
|
||||
|
||||
static void generateCarvingInput(
|
||||
|
||||
+24
-5
@@ -192,11 +192,12 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
|
||||
Map<CavePosition, PlannedCell> surfaceGuards = new HashMap<>();
|
||||
for (HydrologyColumnSample sample : samples.values()) {
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (layer.oceanApron() || !layer.terrainOwned()) {
|
||||
if (layer.oceanApron() || !ownsVolume(layer)) {
|
||||
continue;
|
||||
}
|
||||
validateLayer(layer, worldHeight);
|
||||
if (layer.channel() && isCaveLayer(layer)) {
|
||||
if (layer.channel() && (isCaveLayer(layer)
|
||||
|| hasAcceptedVolume(sample, layer, cavePlansByCourseId.get(layer.feature().courseId())))) {
|
||||
addCaveVolume(sample, layer, cavePlansByCourseId, caveCells);
|
||||
}
|
||||
}
|
||||
@@ -280,7 +281,7 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
|
||||
if (sample != null) {
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (layer.feature().courseId() == plannedCaveCell.courseId()
|
||||
&& isCaveLayer(layer)
|
||||
&& layer.channel() && ownsVolume(layer) && !layer.oceanApron()
|
||||
&& position.y() > layer.bedY()
|
||||
&& position.y() <= layer.ceilingY()) {
|
||||
return layer.floodedCaveBiomeKey();
|
||||
@@ -618,8 +619,8 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
|
||||
}
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (layer.feature().courseId() == plan.source().sourceId()
|
||||
&& layer.channel() && layer.terrainOwned() && !layer.oceanApron()
|
||||
&& isCaveLayer(layer)) {
|
||||
&& layer.channel() && ownsVolume(layer) && !layer.oceanApron()
|
||||
&& (isCaveLayer(layer) || hasAcceptedVolume(sample, layer, plan))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -785,6 +786,24 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean ownsVolume(HydrologyColumnLayer layer) {
|
||||
return layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned();
|
||||
}
|
||||
|
||||
private static boolean hasAcceptedVolume(HydrologyColumnSample sample, HydrologyColumnLayer layer,
|
||||
HydrologyCavePlan plan) {
|
||||
if (plan == null || !plan.accepted()) {
|
||||
return false;
|
||||
}
|
||||
for (int y = layer.bedY() + 1; y <= layer.ceilingY(); y++) {
|
||||
HydrologyCaveAction action = plan.actions().get(new CavePosition(sample.x(), y, sample.z()));
|
||||
if (action != null && action != HydrologyCaveAction.SEAL_GUARD) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isCaveLayer(HydrologyColumnLayer layer) {
|
||||
return layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Three-dimensional rock shelves retained above the approved riverbank cut.")
|
||||
@Data
|
||||
public class IrisRiverBank3DConfig {
|
||||
@Desc("Retain connected volumetric shelves in eroded riverbanks.")
|
||||
private boolean enabled = true;
|
||||
|
||||
@Desc("Seed offset for the riverbank density field.")
|
||||
private long seed = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(32)
|
||||
@Desc("Maximum shelf height above the approved bank surface. Does not deepen the river cut.")
|
||||
private double amplitude = 8;
|
||||
|
||||
@MinNumber(4)
|
||||
@MaxNumber(4096)
|
||||
@Desc("Horizontal density feature size in blocks at the style's default zoom.")
|
||||
private double horizontalScale = 48;
|
||||
|
||||
@MinNumber(4)
|
||||
@MaxNumber(256)
|
||||
@Desc("Vertical density feature size in blocks. Short scales form separate rock shelves.")
|
||||
private double verticalScale = 8;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(16)
|
||||
@Desc("Maximum horizontal distance in blocks from an overhang to a solid supporting bank.")
|
||||
private int maximumOverhang = 8;
|
||||
|
||||
@Desc("Signed XYZ density controlling retained bank rock. Supports generator styles and fractures.")
|
||||
private IrisGeneratorStyle densityStyle = new IrisGeneratorStyle(NoiseStyle.SIMPLEX);
|
||||
|
||||
public void validate() {
|
||||
requireRange("amplitude", amplitude, 0D, 32D);
|
||||
requireRange("horizontalScale", horizontalScale, 4D, 4096D);
|
||||
requireRange("verticalScale", verticalScale, 4D, 256D);
|
||||
requireRange("maximumOverhang", maximumOverhang, 1D, 16D);
|
||||
if (densityStyle == null) {
|
||||
throw new IllegalArgumentException("geometry.banks3D.densityStyle must be a generator style.");
|
||||
}
|
||||
validateStyle(densityStyle, 0);
|
||||
}
|
||||
|
||||
private static void validateStyle(IrisGeneratorStyle style, int depth) {
|
||||
if (depth > 32 || style.getStyle() == null) {
|
||||
throw new IllegalArgumentException("geometry.banks3D.densityStyle must use known styles with at most 32 nested fractures.");
|
||||
}
|
||||
requireRange("densityStyle.zoom", style.getZoom(), 0.00001D, Double.MAX_VALUE);
|
||||
requireRange("densityStyle.cellularFrequency", style.getCellularFrequency(), 0D, Double.MAX_VALUE);
|
||||
requireRange("densityStyle.cellularZoom", style.getCellularZoom(), 0.00001D, Double.MAX_VALUE);
|
||||
requireRange("densityStyle.multiplier", style.getMultiplier(), 0.00001D, Double.MAX_VALUE);
|
||||
requireRange("densityStyle.exponent", style.getExponent(), 0.01562D, 64D);
|
||||
requireRange("densityStyle.cacheSize", style.getCacheSize(), 0D, 8192D);
|
||||
if (style.getFracture() != null) {
|
||||
validateStyle(style.getFracture(), depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireRange(String field, double value, double minimum, double maximum) {
|
||||
if (!Double.isFinite(value) || value < minimum || value > maximum) {
|
||||
throw new IllegalArgumentException("geometry.banks3D." + field + " must be finite and between "
|
||||
+ minimum + " and " + maximum + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
@Desc("Bounds terrain removal outside the wet river channel.")
|
||||
public class IrisRiverExcavationConfig {
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("Maximum blocks removed vertically from a dry bank column. Zero preserves dry bank heights.")
|
||||
private int maximumDepth = 8;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("Maximum horizontal distance from the channel outline affected by shore and bank excavation.")
|
||||
private int maximumWidth = 16;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8192)
|
||||
@Desc("Maximum dry bank blocks removed per block, measured over sixteen-block reaches. Courses exceeding this budget are rejected.")
|
||||
private int maximumVolumePerBlock = 256;
|
||||
}
|
||||
@@ -16,6 +16,9 @@ public class IrisRiverGeometryConfig {
|
||||
@Desc("Surface river bed shape for waterfall and cascade throats, and the wall shape shared with the erosion compiler; roughness fields default to the channel roughness.")
|
||||
private IrisSurfaceRiverShapeConfig surface = new IrisSurfaceRiverShapeConfig();
|
||||
|
||||
@Desc("Three-dimensional bank shelves above the protected riverbed and waterline.")
|
||||
private IrisRiverBank3DConfig banks3D = new IrisRiverBank3DConfig();
|
||||
|
||||
@Desc("Underground channel bed and wall shape.")
|
||||
private IrisRiverChannelShapeConfig underground = new IrisRiverChannelShapeConfig();
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
@Desc("Regional drainage trunks shared across generation tiles and lowland channels between two coasts.")
|
||||
public class IrisRiverRegionalConfig {
|
||||
@Desc("Plan regional drainage before local rivers.")
|
||||
private boolean enabled = true;
|
||||
|
||||
@MinNumber(128)
|
||||
@MaxNumber(1024)
|
||||
@Desc("Coarse regional terrain sample spacing in blocks; a power of two.")
|
||||
private int sampleSpacing = 256;
|
||||
|
||||
@MinNumber(256)
|
||||
@MaxNumber(32768)
|
||||
@Desc("Minimum length of a regional trunk or coastal channel.")
|
||||
private int minimumLength = 2048;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@Desc("Maximum regional trunks accepted in one drainage basin.")
|
||||
private int maximumTrunks = 2;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("Maximum regional basin graphs retained in memory.")
|
||||
private int maximumCachedBasins = 8;
|
||||
|
||||
@MinNumber(16384)
|
||||
@MaxNumber(1048576)
|
||||
@Desc("Maximum complete hydraulic stations retained across cached regional basins.")
|
||||
private int maximumCachedStations = 131072;
|
||||
|
||||
@Desc("Allow a sea-level lowland channel with an ocean connection at both ends.")
|
||||
private boolean coastalChannels = true;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Chance that a basin tries a coastal channel before ordinary drainage trunks.")
|
||||
private double coastalChannelChance = 0.25D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(32)
|
||||
@Desc("Maximum terrain incision along a channel between two coasts.")
|
||||
private int maximumCoastalIncision = 8;
|
||||
}
|
||||
@@ -85,4 +85,7 @@ public class IrisRiverRoutingConfig {
|
||||
@MaxNumber(4)
|
||||
@Desc("Extra surface courses an outlet may accept as tributaries joining its main river; 0 keeps one river per outlet.")
|
||||
private int tributaries = 1;
|
||||
|
||||
@Desc("Regional drainage and channels between coasts, planned above the local tile lattice.")
|
||||
private IrisRiverRegionalConfig regional = new IrisRiverRegionalConfig();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ public class IrisSurfaceRiverBankConfig {
|
||||
@Desc("Widest eroded band outside the shore, in blocks.")
|
||||
private int maximumBlendWidth = 32;
|
||||
|
||||
@Desc("Limits dry bank cut depth, width, and excavated volume independently of the wet channel.")
|
||||
private IrisRiverExcavationConfig excavation = new IrisRiverExcavationConfig();
|
||||
|
||||
@Desc("Show the biome's deeper layers on eroded banks instead of the surface layer.")
|
||||
private boolean exposeCutStrata = true;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public class IrisSurfaceRiverErosionConfig {
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.95)
|
||||
@Desc("Share of the channel half-width that stays at full bed depth before the bed rises to the edge; higher is a flatter, broader bed.")
|
||||
@Desc("Share of the distance from the thalweg to each waterline that stays at full bed depth before the bed rises to the edge; higher is a flatter, broader bed.")
|
||||
private double thalwegFraction = 0.45D;
|
||||
|
||||
@MinNumber(0.25)
|
||||
@@ -48,6 +48,6 @@ public class IrisSurfaceRiverErosionConfig {
|
||||
@Desc("Share of the eroded band kept level at the bank top before the vertical wall when style is CLIFF; ignored by every other style.")
|
||||
private double cliffFraction = 0.5D;
|
||||
|
||||
@Desc("Cross-section of the wet channel bed from the centerline out to the waterline. BOWL is the profile used before this field existed.")
|
||||
@Desc("Cross-section of the wet channel bed from the thalweg out to the waterline. Curves shift the thalweg toward the outside bank within the existing depth limit.")
|
||||
private IrisRiverBedProfile bedProfile = IrisRiverBedProfile.BOWL;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
package art.arcane.iris.engine.terrain;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSnapshot;
|
||||
import art.arcane.iris.engine.object.IrisRiverBank3DConfig;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class HydrologyBankTerrainRuntime {
|
||||
private static final int STEP = 2;
|
||||
private static final long DENSITY_SALT = 0x485944524f42414eL;
|
||||
private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
|
||||
|
||||
private final Sources sources;
|
||||
private final Options options;
|
||||
private final double amplitude;
|
||||
private final double horizontalFrequency;
|
||||
private final double verticalFrequency;
|
||||
private final int maximumOverhang;
|
||||
private final NoiseSource noise;
|
||||
private final Long2ObjectLinkedOpenHashMap<BankColumn> columns = new Long2ObjectLinkedOpenHashMap<>();
|
||||
private long columnsEpoch;
|
||||
|
||||
public HydrologyBankTerrainRuntime(Sources sources, Options options) {
|
||||
this(sources, new Compiled(options, compileNoise(options)));
|
||||
}
|
||||
|
||||
private HydrologyBankTerrainRuntime(Sources sources, Compiled compiled) {
|
||||
this.sources = Objects.requireNonNull(sources, "Bank terrain sources");
|
||||
options = compiled.options();
|
||||
IrisRiverBank3DConfig config = options.config();
|
||||
config.validate();
|
||||
amplitude = config.isEnabled() ? config.getAmplitude() : 0D;
|
||||
horizontalFrequency = 64D / config.getHorizontalScale();
|
||||
verticalFrequency = 64D / config.getVerticalScale();
|
||||
maximumOverhang = config.getMaximumOverhang();
|
||||
noise = Objects.requireNonNull(compiled.noise(), "Bank density noise");
|
||||
}
|
||||
|
||||
static HydrologyBankTerrainRuntime withNoise(Sources sources, Options options, NoiseSource noise) {
|
||||
return new HydrologyBankTerrainRuntime(sources, new Compiled(options, noise));
|
||||
}
|
||||
|
||||
public Terrain3DColumn column(int x, int z) {
|
||||
Terrain3DColumn resolved = resolveColumn(x, z);
|
||||
return resolved == null ? sources.natural().column(x, z) : resolved;
|
||||
}
|
||||
|
||||
public Optional<Terrain3DColumn> columnIfReady(int x, int z) {
|
||||
return Optional.ofNullable(resolveColumn(x, z));
|
||||
}
|
||||
|
||||
private Terrain3DColumn resolveColumn(int x, int z) {
|
||||
BankColumn bank = rawColumn(x, z);
|
||||
if (bank == null) {
|
||||
return null;
|
||||
}
|
||||
Terrain3DColumn resolved = bank.resolved;
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
Terrain3DColumn raw = bank.raw;
|
||||
IntArrayList boundaries = new IntArrayList(raw.spanCount() * 2);
|
||||
boundaries.add(raw.ceiling(0));
|
||||
boundaries.add(raw.floor(0));
|
||||
for (int span = 1; span < raw.spanCount(); span++) {
|
||||
Support support = supported(x, z, raw.ceiling(span), raw.floor(span));
|
||||
if (support == Support.UNAVAILABLE) {
|
||||
return null;
|
||||
}
|
||||
if (support == Support.SUPPORTED) {
|
||||
boundaries.add(raw.ceiling(span));
|
||||
boundaries.add(raw.floor(span));
|
||||
}
|
||||
}
|
||||
resolved = boundaries.size() == raw.spanCount() * 2 ? raw
|
||||
: new Terrain3DColumn(raw.baseHeight(), raw.minY(), true, boundaries.toIntArray());
|
||||
bank.resolved = resolved;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
synchronized (columns) {
|
||||
columnsEpoch++;
|
||||
columns.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static NoiseSource compileNoise(Options options) {
|
||||
IrisRiverBank3DConfig config = options.config();
|
||||
config.validate();
|
||||
if (!config.isEnabled() || config.getAmplitude() == 0D) {
|
||||
return (x, y, z) -> 0D;
|
||||
}
|
||||
CNG generator = config.getDensityStyle().createNoCache(
|
||||
new RNG(options.seed() ^ config.getSeed() ^ DENSITY_SALT), options.data());
|
||||
return generator::noiseFastSigned3D;
|
||||
}
|
||||
|
||||
private BankColumn rawColumn(int x, int z) {
|
||||
long key = (long) x << 32 ^ z & 0xffffffffL;
|
||||
long loadEpoch;
|
||||
synchronized (columns) {
|
||||
BankColumn existing = columns.getAndMoveToLast(key);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
loadEpoch = columnsEpoch;
|
||||
}
|
||||
BankColumn computed = createColumn(x, z);
|
||||
if (computed == null) {
|
||||
return null;
|
||||
}
|
||||
synchronized (columns) {
|
||||
if (loadEpoch != columnsEpoch) {
|
||||
return null;
|
||||
}
|
||||
BankColumn existing = columns.getAndMoveToLast(key);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
columns.putAndMoveToLast(key, computed);
|
||||
if (columns.size() > options.maximumColumns()) {
|
||||
columns.removeFirst();
|
||||
}
|
||||
}
|
||||
return computed;
|
||||
}
|
||||
|
||||
private BankColumn createColumn(int x, int z) {
|
||||
HydrologyColumnSnapshot snapshot = sources.hydrology().sample(x, z);
|
||||
if (!snapshot.available()) {
|
||||
return null;
|
||||
}
|
||||
Terrain3DColumn natural = Objects.requireNonNull(sources.natural().column(x, z), "Natural bank column");
|
||||
HydrologyColumnSample hydrology = snapshot.column();
|
||||
HydrologyColumnLayer layer = hydrology == null ? null : hydrology.primarySurfaceLayerOrNull();
|
||||
if (layer == null || !layer.terrainOwned()) {
|
||||
return new BankColumn(natural, natural);
|
||||
}
|
||||
int base = Math.clamp(hydrology.terrainHeight(), 0, options.height() - 1);
|
||||
if (layer.channel() || !layer.feature().type().isSurface() || amplitude == 0D) {
|
||||
Terrain3DColumn flat = Terrain3DColumn.unshaped(base, options.height());
|
||||
return new BankColumn(flat, flat);
|
||||
}
|
||||
if (base >= natural.topY()) {
|
||||
return new BankColumn(natural, natural);
|
||||
}
|
||||
double rise = Math.min(amplitude, natural.topY() - base);
|
||||
IntArrayList boundaries = new IntArrayList(natural.spanCount() * 2 + 8);
|
||||
boundaries.add(0);
|
||||
boolean previous = true;
|
||||
int maximum = Math.min(natural.topY(), options.height() - 1);
|
||||
int preserved = maximum + 1;
|
||||
for (int span = 1; span < natural.spanCount(); span++) {
|
||||
if (natural.ceiling(span) > base) {
|
||||
preserved = natural.ceiling(span);
|
||||
break;
|
||||
}
|
||||
}
|
||||
double[] density = sampleColumn(x, z, base + 1, Math.min(maximum, base + (int) Math.ceil(rise)));
|
||||
for (int y = base + 1; y <= maximum; y++) {
|
||||
boolean solid = natural.isSolid(y) && (y >= preserved || y - base <= rise
|
||||
&& y - base <= rise * (0.5D + 0.5D * density[y - base - 1]));
|
||||
if (solid != previous) {
|
||||
boundaries.add(solid ? y : y - 1);
|
||||
previous = solid;
|
||||
}
|
||||
}
|
||||
if (previous) {
|
||||
boundaries.add(maximum);
|
||||
}
|
||||
Terrain3DColumn raw = new Terrain3DColumn(natural.baseHeight(), base + 1, true, boundaries.toIntArray());
|
||||
return new BankColumn(raw, raw.spanCount() == 1 ? raw : null);
|
||||
}
|
||||
|
||||
private Support supported(int x, int z, int bottom, int top) {
|
||||
for (int y = bottom; y <= top; y++) {
|
||||
for (int[] direction : DIRECTIONS) {
|
||||
for (int distance = 1; distance <= maximumOverhang; distance++) {
|
||||
long sampleX = (long) x + direction[0] * distance;
|
||||
long sampleZ = (long) z + direction[1] * distance;
|
||||
if (sampleX < Integer.MIN_VALUE || sampleX > Integer.MAX_VALUE
|
||||
|| sampleZ < Integer.MIN_VALUE || sampleZ > Integer.MAX_VALUE) {
|
||||
break;
|
||||
}
|
||||
BankColumn sampled = rawColumn((int) sampleX, (int) sampleZ);
|
||||
if (sampled == null) {
|
||||
return Support.UNAVAILABLE;
|
||||
}
|
||||
Terrain3DColumn neighbour = sampled.raw;
|
||||
if (y <= neighbour.floor(0)) {
|
||||
return Support.SUPPORTED;
|
||||
}
|
||||
if (!neighbour.isSolid(y)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Support.UNSUPPORTED;
|
||||
}
|
||||
|
||||
private double[] sampleColumn(int x, int z, int minimum, int maximum) {
|
||||
int lowerX = Math.floorDiv(x, STEP) * STEP;
|
||||
int lowerZ = Math.floorDiv(z, STEP) * STEP;
|
||||
double fractionX = (x - lowerX) / (double) STEP;
|
||||
double fractionZ = (z - lowerZ) / (double) STEP;
|
||||
int gridY = Math.floorDiv(minimum, STEP) * STEP;
|
||||
double lower = interpolatePlane(lowerX, gridY, lowerZ, fractionX, fractionZ);
|
||||
double upper = interpolatePlane(lowerX, gridY + STEP, lowerZ, fractionX, fractionZ);
|
||||
double[] density = new double[maximum - minimum + 1];
|
||||
for (int y = minimum; y <= maximum; y++) {
|
||||
if (y >= gridY + STEP) {
|
||||
gridY += STEP;
|
||||
lower = upper;
|
||||
upper = interpolatePlane(lowerX, gridY + STEP, lowerZ, fractionX, fractionZ);
|
||||
}
|
||||
double fractionY = (y - gridY) / (double) STEP;
|
||||
density[y - minimum] = lower + (upper - lower) * fractionY;
|
||||
}
|
||||
return density;
|
||||
}
|
||||
|
||||
private double interpolatePlane(int x, int y, int z, double fractionX, double fractionZ) {
|
||||
double northWest = noise(x, y, z);
|
||||
double northEast = noise((long) x + STEP, y, z);
|
||||
double southWest = noise(x, y, (long) z + STEP);
|
||||
double southEast = noise((long) x + STEP, y, (long) z + STEP);
|
||||
double north = northWest + (northEast - northWest) * fractionX;
|
||||
double south = southWest + (southEast - southWest) * fractionX;
|
||||
return north + (south - north) * fractionZ;
|
||||
}
|
||||
|
||||
private double noise(long x, int y, long z) {
|
||||
double value = noise.sample(x * horizontalFrequency, y * verticalFrequency, z * horizontalFrequency);
|
||||
if (!Double.isFinite(value)) {
|
||||
throw new IllegalStateException("Nonfinite riverbank density at " + x + "," + y + "," + z + ".");
|
||||
}
|
||||
return Math.clamp(value, -1D, 1D);
|
||||
}
|
||||
|
||||
public record Sources(NaturalSource natural, HydrologySource hydrology) {
|
||||
public Sources {
|
||||
Objects.requireNonNull(natural, "Natural terrain source");
|
||||
Objects.requireNonNull(hydrology, "Hydrology source");
|
||||
}
|
||||
}
|
||||
|
||||
public record Options(long seed, int height, int maximumColumns, IrisData data, IrisRiverBank3DConfig config) {
|
||||
public Options {
|
||||
Objects.requireNonNull(config, "Bank geometry configuration");
|
||||
if (height < 2 || maximumColumns < 1) {
|
||||
throw new IllegalArgumentException("Bank terrain height and cache size must be positive.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface NaturalSource {
|
||||
Terrain3DColumn column(int x, int z);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HydrologySource {
|
||||
HydrologyColumnSnapshot sample(int x, int z);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface NoiseSource {
|
||||
double sample(double x, double y, double z);
|
||||
}
|
||||
|
||||
private record Compiled(Options options, NoiseSource noise) {
|
||||
}
|
||||
|
||||
private enum Support {
|
||||
SUPPORTED,
|
||||
UNSUPPORTED,
|
||||
UNAVAILABLE
|
||||
}
|
||||
|
||||
private static final class BankColumn {
|
||||
private final Terrain3DColumn raw;
|
||||
private volatile Terrain3DColumn resolved;
|
||||
|
||||
private BankColumn(Terrain3DColumn raw, Terrain3DColumn resolved) {
|
||||
this.raw = raw;
|
||||
this.resolved = resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PackRiverBank3DValidatorTest {
|
||||
@Test
|
||||
public void acceptsVolumetricStylesAndFullLengthSeeds() {
|
||||
List<String> errors = validate("""
|
||||
{"enabled":true,"seed":-9223372036854775808,"amplitude":32,
|
||||
"horizontalScale":4,"verticalScale":256,"maximumOverhang":16,
|
||||
"densityStyle":{"style":"STRATA","fracture":{"style":"SIMPLEX","zoom":2}}}
|
||||
""");
|
||||
assertTrue(errors.toString(), errors.isEmpty());
|
||||
assertTrue(validate("{\"densityStyle\":\"river/bank-rock\"}").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsInvalidBoundsTypesAndStyles() {
|
||||
for (String fields : List.of("\"seed\":1.5", "\"seed\":9223372036854775808",
|
||||
"\"enabled\":\"true\"", "\"amplitude\":-1", "\"horizontalScale\":0",
|
||||
"\"verticalScale\":257", "\"maximumOverhang\":17", "\"maximumOverhang\":1.5",
|
||||
"\"densityStyle\":null", "\"densityStyle\":{\"style\":\"invalid\"}",
|
||||
"\"densityStyle\":{\"zoom\":0}")) {
|
||||
assertFalse(fields, validate("{" + fields + "}").isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> validate(String json) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
PackRiverBank3DValidator.validate("geometry.banks3D", new JSONObject(json), errors);
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,11 @@ public class IrisRiverSchemaTest {
|
||||
JSONObject rivers = referencedProperties(definitions, properties.getJSONObject("rivers"));
|
||||
JSONObject routing = referencedProperties(definitions, rivers.getJSONObject("routing"));
|
||||
JSONObject geometry = referencedProperties(definitions, rivers.getJSONObject("geometry"));
|
||||
JSONObject bankVolume = referencedProperties(definitions, geometry.getJSONObject("banks3D"));
|
||||
assertEquals(32D, bankVolume.getJSONObject("amplitude").getDouble("maximum"), 0D);
|
||||
assertEquals(4D, bankVolume.getJSONObject("verticalScale").getDouble("minimum"), 0D);
|
||||
assertEquals(16, bankVolume.getJSONObject("maximumOverhang").getInt("maximum"));
|
||||
assertSnippetBackedObjectReference(bankVolume.getJSONObject("densityStyle"));
|
||||
JSONObject drops = referencedProperties(definitions, geometry.getJSONObject("drops"));
|
||||
JSONObject surface = referencedProperties(definitions, rivers.getJSONObject("surface"));
|
||||
JSONObject sources = referencedProperties(definitions, surface.getJSONObject("sources"));
|
||||
|
||||
@@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "43b3add0a559f7006e2973344f39db933bbfb1be889f3c5d616a7bf035a9db39";
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "6dd947428034d7feb5549a37f9b7061c5552601779611587967451e4e54fb984";
|
||||
|
||||
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||
IrisDimension.class,
|
||||
|
||||
+88
@@ -1,15 +1,29 @@
|
||||
package art.arcane.iris.engine.actuator;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.SeedManager;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
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.IrisDecorationStep;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisHydrology;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisRiverMaterialConfig;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.context.ChunkedDataCache;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Rule;
|
||||
@@ -25,6 +39,7 @@ import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -208,6 +223,79 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
||||
verify(fixture.palette, never()).get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deepBankAndShoreCutsStillReceiveTheirSurfaceMaterial() {
|
||||
for (HydrologyColumnLayer layer : List.of(shoreLayer(), bankLayer())) {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
IrisRegion region = new IrisRegion();
|
||||
ChunkContext context = mock(ChunkContext.class);
|
||||
PlatformBlockState rock = mock(PlatformBlockState.class);
|
||||
PlatformBlockState grass = mock(PlatformBlockState.class);
|
||||
PlatformBlockState painted = mock(PlatformBlockState.class);
|
||||
when(rock.key()).thenReturn("minecraft:stone");
|
||||
when(grass.key()).thenReturn("minecraft:grass_block");
|
||||
when(painted.key()).thenReturn("minecraft:dirt");
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(palette.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), eq(data))).thenReturn(painted);
|
||||
IrisRiverMaterialConfig material = new IrisRiverMaterialConfig().setEnabled(true).setDepth(2).setPalette(palette);
|
||||
IrisHydrology hydrology = new IrisHydrology();
|
||||
hydrology.getRivers().getSurface().getBanks().setShoreMaterial(material).setBankMaterial(material);
|
||||
IrisDimension dimension = new IrisDimension().setHydrology(hydrology).setBedrock(false).setHideOresForHiddenOre(true);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getData()).thenReturn(data);
|
||||
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
|
||||
when(complex.getImageMapRuntime()).thenReturn(mock(IrisImageMapRuntime.class));
|
||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 0D));
|
||||
when(complex.sampleHydrologyColumn(11, -4)).thenReturn(new HydrologyColumnSample(
|
||||
11, -4, 80, 0, false, "parent", List.of(layer)));
|
||||
when(context.getRoundedHeight(0, 0)).thenReturn(60);
|
||||
when(context.getBiome()).thenReturn(new ChunkedDataCache<>(
|
||||
ProceduralStream.of((x, z) -> biome, Interpolated.of(value -> 0D, value -> biome)), 11, -4, false));
|
||||
when(context.getRegion()).thenReturn(new ChunkedDataCache<>(
|
||||
ProceduralStream.of((x, z) -> region, Interpolated.of(value -> 0D, value -> region)), 11, -4, false));
|
||||
when(context.getRock()).thenReturn(new ChunkedDataCache<>(
|
||||
ProceduralStream.of((x, z) -> rock, Interpolated.of(value -> 0D, value -> rock)), 11, -4, false));
|
||||
when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class),
|
||||
anyInt(), anyInt(), eq(data), eq(complex))).thenReturn(new KList<>(List.of(grass)));
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 64, 1);
|
||||
|
||||
new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context);
|
||||
|
||||
assertSame(painted, output.get(0, 60, 0));
|
||||
assertSame(painted, output.get(0, 59, 0));
|
||||
assertSame(rock, output.get(0, 58, 0));
|
||||
material.setEnabled(false);
|
||||
new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context);
|
||||
assertSame(rock, output.get(0, 60, 0));
|
||||
|
||||
Terrain3DColumn carved = mock(Terrain3DColumn.class);
|
||||
when(carved.spanCount()).thenReturn(2);
|
||||
when(carved.floor(1)).thenReturn(60);
|
||||
when(carved.ceiling(1)).thenReturn(50);
|
||||
when(carved.floor(0)).thenReturn(40);
|
||||
when(carved.ceiling(0)).thenReturn(0);
|
||||
Terrain3DColumn natural = mock(Terrain3DColumn.class);
|
||||
when(natural.surfaceY(60)).thenReturn(60);
|
||||
when(natural.surfaceY(40)).thenReturn(44);
|
||||
when(complex.terrainColumn(eq(11), eq(-4), any(HydrologyColumnSample.class))).thenReturn(carved);
|
||||
when(complex.naturalTerrainColumn(11, -4)).thenReturn(natural);
|
||||
when(biome.generateCeilingLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class),
|
||||
anyInt(), anyInt(), eq(data), eq(complex))).thenReturn(new KList<>());
|
||||
new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context);
|
||||
assertSame(grass, output.get(0, 60, 0));
|
||||
assertSame(rock, output.get(0, 40, 0));
|
||||
material.setEnabled(true);
|
||||
new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context);
|
||||
assertSame(painted, output.get(0, 60, 0));
|
||||
assertSame(painted, output.get(0, 40, 0));
|
||||
assertSame(rock, output.get(0, 38, 0));
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyColumnLayer channelLayer() {
|
||||
return surfaceLayer(true, false);
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ public class DecoratorCoreTest {
|
||||
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 2, 1);
|
||||
int placed = DecoratorCore.placeFloatingStacked(
|
||||
decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data);
|
||||
decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data, null);
|
||||
|
||||
assertEquals(1, placed);
|
||||
assertSame(decorant, output.get(0, 1, 0));
|
||||
@@ -344,7 +344,7 @@ public class DecoratorCoreTest {
|
||||
output.set(0, 2, 0, occupied);
|
||||
|
||||
DecoratorCore.placeFloatingSimple(
|
||||
decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data);
|
||||
decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data, null);
|
||||
|
||||
assertSame(air, output.get(0, 1, 0));
|
||||
assertSame(occupied, output.get(0, 2, 0));
|
||||
|
||||
@@ -154,6 +154,40 @@ public class IrisShoreLineDecoratorTest {
|
||||
assertSame(targetAir, output.get(0, FLUID_HEIGHT + 1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedShoreCaneRequiresWaterBesideTheActualSubstrate() {
|
||||
Fixture fixture = createFixture(false, false);
|
||||
PlatformBlockState support = sturdyState();
|
||||
PlatformBlockState air = airState();
|
||||
PlatformBlockState water = mock(PlatformBlockState.class);
|
||||
when(water.isWater()).thenReturn(true);
|
||||
when(fixture.decorant.key()).thenReturn("minecraft:sugar_cane[age=0]");
|
||||
when(fixture.decorant.canPlaceOnto(support)).thenReturn(true);
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(3, FLUID_HEIGHT + 3, 3);
|
||||
output.set(1, FLUID_HEIGHT, 1, support);
|
||||
output.set(1, FLUID_HEIGHT + 1, 1, air);
|
||||
|
||||
fixture.shoreline.decorateAcceptedShore(1, 1, 1, 1, output, fixture.biome, FLUID_HEIGHT, output.getHeight());
|
||||
assertSame(air, output.get(1, FLUID_HEIGHT + 1, 1));
|
||||
output.set(0, FLUID_HEIGHT, 1, water);
|
||||
fixture.shoreline.decorateAcceptedShore(1, 1, 1, 1, output, fixture.biome, FLUID_HEIGHT, output.getHeight());
|
||||
assertSame(fixture.decorant, output.get(1, FLUID_HEIGHT + 1, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forcedShoreCaneCannotUseStone() {
|
||||
Fixture fixture = createFixture(true, false);
|
||||
PlatformBlockState stone = sturdyState();
|
||||
PlatformBlockState air = airState();
|
||||
when(fixture.decorant.key()).thenReturn("minecraft:sugar_cane[age=0]");
|
||||
when(fixture.decorant.canPlaceOnto(stone)).thenReturn(false);
|
||||
Hunk<PlatformBlockState> output = output(stone, air);
|
||||
|
||||
fixture.shoreline.decorateAcceptedShore(0, 0, 0, 0, output, fixture.biome, FLUID_HEIGHT, output.getHeight());
|
||||
|
||||
assertSame(air, output.get(0, FLUID_HEIGHT + 1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedWaterloggedStackRestoresEveryOriginalBlock() {
|
||||
Fixture fixture = createFixture(false, false);
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package art.arcane.iris.engine.decorator;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
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.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.object.IrisDecorator;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisSugarCanePlacementTest {
|
||||
@Test
|
||||
public void floatingSingleRequiresSuitableSoilAndAdjacentWater() {
|
||||
verifySupport(Placement.FLOATING_SINGLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floatingStackRequiresSuitableSoilAndAdjacentWater() {
|
||||
verifySupport(Placement.FLOATING_STACK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ceilingSingleRequiresSuitableSoilAndAdjacentWater() {
|
||||
verifySupport(Placement.CEILING_SINGLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ceilingStackRequiresSuitableSoilAndAdjacentWater() {
|
||||
verifySupport(Placement.CEILING_STACK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floatingCaneUsesAcceptedWaterAcrossTheChunkBoundary() {
|
||||
for (Placement placement : List.of(Placement.FLOATING_SINGLE, Placement.FLOATING_STACK)) {
|
||||
Fixture fixture = new Fixture(1);
|
||||
fixture.output.set(0, 3, 0, fixture.soil);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(fixture.engine.getComplex()).thenReturn(complex);
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
11L, HydrologyFeatureType.RIFFLE, 12L, 13L, -17, 3, -16, 1, 0, false);
|
||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(feature, 1, 3, 3,
|
||||
true, false, false, true, false, false, true, true, false,
|
||||
"water", "river", "mouth", "shore", "bank", "cave");
|
||||
when(complex.sampleHydrologyColumn(-17, -16)).thenReturn(new HydrologyColumnSample(
|
||||
-17, -16, 6, 0, false, "parent", List.of(layer)));
|
||||
when(complex.resolveHydrologyFluid("water", -17, -16)).thenReturn(fixture.water);
|
||||
|
||||
fixture.place(placement);
|
||||
|
||||
assertSame(fixture.cane, fixture.output.get(0, 4, 0));
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifySupport(Placement placement) {
|
||||
Fixture fixture = new Fixture(3);
|
||||
fixture.output.set(1, 3, 1, fixture.stone);
|
||||
fixture.output.set(0, 3, 1, fixture.water);
|
||||
fixture.place(placement);
|
||||
assertSame(fixture.air, fixture.output.get(1, 4, 1));
|
||||
|
||||
fixture.output.set(1, 3, 1, fixture.soil);
|
||||
fixture.output.set(0, 3, 1, fixture.air);
|
||||
fixture.place(placement);
|
||||
assertSame(fixture.air, fixture.output.get(1, 4, 1));
|
||||
|
||||
fixture.output.set(0, 3, 1, fixture.water);
|
||||
fixture.place(placement);
|
||||
assertSame(fixture.cane, fixture.output.get(1, 4, 1));
|
||||
}
|
||||
|
||||
private static PlatformBlockState state(String key) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
when(state.key()).thenReturn(key);
|
||||
return state;
|
||||
}
|
||||
|
||||
private enum Placement {
|
||||
FLOATING_SINGLE,
|
||||
FLOATING_STACK,
|
||||
CEILING_SINGLE,
|
||||
CEILING_STACK
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final PlatformBlockState air = state("minecraft:air");
|
||||
private final PlatformBlockState stone = state("minecraft:stone");
|
||||
private final PlatformBlockState soil = state("minecraft:dirt");
|
||||
private final PlatformBlockState water = state("minecraft:water");
|
||||
private final PlatformBlockState cane = state("minecraft:sugar_cane[age=0]");
|
||||
private final IrisDecorator decorator = mock(IrisDecorator.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final EngineMantle mantle = mock(EngineMantle.class);
|
||||
private final Hunk<PlatformBlockState> output;
|
||||
private final int local;
|
||||
|
||||
private Fixture(int size) {
|
||||
local = size / 2;
|
||||
output = Hunk.newArrayHunk(size, 8, size);
|
||||
when(air.isAir()).thenReturn(true);
|
||||
when(water.isWater()).thenReturn(true);
|
||||
when(cane.canPlaceOnto(soil)).thenReturn(true);
|
||||
when(cane.canPlaceOnto(cane)).thenReturn(true);
|
||||
when(mantle.getEngine()).thenReturn(engine);
|
||||
when(decorator.isForcePlace()).thenReturn(true);
|
||||
when(decorator.getHeight(any(), anyDouble(), anyDouble(), any())).thenReturn(2);
|
||||
when(decorator.pickBlockData(any(), any(), anyDouble(), anyDouble())).thenReturn(cane);
|
||||
when(decorator.pickBlockDataTop(any(), any(), anyDouble(), anyDouble())).thenReturn(cane);
|
||||
output.fill(air);
|
||||
}
|
||||
|
||||
private void place(Placement placement) {
|
||||
int world = -16 + local;
|
||||
switch (placement) {
|
||||
case FLOATING_SINGLE -> DecoratorCore.placeFloatingSimple(decorator, local, local, world, world,
|
||||
3, 4, output, new RNG(1L), null, mantle);
|
||||
case FLOATING_STACK -> DecoratorCore.placeFloatingStacked(decorator, local, local, world, world,
|
||||
3, 4, output, new RNG(1L), null, mantle);
|
||||
case CEILING_SINGLE -> DecoratorCore.placeSingleAt(decorator, local, local, world, 4, world,
|
||||
output, new RNG(1L), null, false, mantle);
|
||||
case CEILING_STACK -> DecoratorCore.placeStackDown(decorator, local, local, world, world,
|
||||
4, 0, output, new RNG(1L), null, 2, new DecoratorCore.PlaceOpts(), mantle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package art.arcane.iris.engine.decorator;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.SeedManager;
|
||||
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.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDecorator;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisSugarCaneTest {
|
||||
@Test
|
||||
public void localCaneRequiresSuitableSubstrateAndWaterAtItsBase() {
|
||||
Fixture fixture = new Fixture(3);
|
||||
fixture.output.set(1, 3, 1, fixture.soil);
|
||||
assertFalse(fixture.place(1, 1));
|
||||
fixture.output.set(0, 4, 1, fixture.water);
|
||||
assertFalse(fixture.place(1, 1));
|
||||
fixture.output.set(0, 3, 1, fixture.water);
|
||||
assertTrue(fixture.place(1, 1));
|
||||
when(fixture.cane.canPlaceOnto(fixture.soil)).thenReturn(false);
|
||||
assertFalse(fixture.place(1, 1));
|
||||
verifyNoInteractions(fixture.engine);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lavaAndOrdinaryIceDoNotHydrateCane() {
|
||||
Fixture fixture = new Fixture(3);
|
||||
fixture.output.set(1, 3, 1, fixture.soil);
|
||||
for (String key : new String[]{"minecraft:lava", "minecraft:ice", "minecraft:stone"}) {
|
||||
fixture.output.set(0, 3, 1, state(key));
|
||||
assertFalse(key, fixture.place(1, 1));
|
||||
}
|
||||
fixture.output.set(0, 3, 1, state("minecraft:frosted_ice[age=0]"));
|
||||
assertTrue(fixture.place(1, 1));
|
||||
PlatformBlockState waterlogged = state("minecraft:oak_stairs[waterlogged=true]");
|
||||
when(waterlogged.isWaterLogged()).thenReturn(true);
|
||||
fixture.output.set(0, 3, 1, waterlogged);
|
||||
assertTrue(fixture.place(1, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void caneStackKeepsItsValidBaseAndCannotReplaceWater() {
|
||||
Fixture fixture = new Fixture(3);
|
||||
when(fixture.cane.canPlaceOnto(fixture.cane)).thenReturn(true);
|
||||
fixture.output.set(1, 3, 1, fixture.cane);
|
||||
assertFalse(fixture.place(1, 1));
|
||||
fixture.output.set(1, 2, 1, fixture.soil);
|
||||
fixture.output.set(0, 2, 1, fixture.water);
|
||||
assertTrue(fixture.place(1, 1));
|
||||
fixture.output.set(1, 4, 1, fixture.water);
|
||||
assertFalse(fixture.place(1, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void crossChunkWaterUsesAcceptedHydrologyAtTheCorrectElevation() {
|
||||
Fixture fixture = new Fixture(1);
|
||||
fixture.plannedTerrain();
|
||||
fixture.output.set(0, 3, 0, fixture.soil);
|
||||
HydrologyColumnLayer layer = wetLayer(1, 3);
|
||||
when(fixture.complex.sampleHydrologyColumn(-17, -16)).thenReturn(new HydrologyColumnSample(
|
||||
-17, -16, 6, 0, false, "parent", List.of(layer)));
|
||||
when(fixture.complex.resolveHydrologyFluid("water", -17, -16)).thenReturn(fixture.water);
|
||||
|
||||
assertTrue(fixture.place(0, 0));
|
||||
PlatformBlockState lava = state("minecraft:lava");
|
||||
when(fixture.complex.resolveHydrologyFluid("water", -17, -16)).thenReturn(lava);
|
||||
assertFalse(fixture.place(0, 0));
|
||||
when(fixture.complex.sampleHydrologyColumn(-17, -16)).thenReturn(new HydrologyColumnSample(
|
||||
-17, -16, 6, 0, false, "parent", List.of(wetLayer(1, 2))));
|
||||
assertFalse(fixture.place(0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void localBlocksOverridePlannedWaterWithoutReadingAnotherChunk() {
|
||||
Fixture fixture = new Fixture(3);
|
||||
fixture.output.set(1, 3, 1, fixture.soil);
|
||||
fixture.output.set(0, 3, 1, state("minecraft:stone"));
|
||||
assertFalse(fixture.place(1, 1));
|
||||
verifyNoInteractions(fixture.engine);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forcedSurfaceDecorationStillRejectsCaneOnStone() {
|
||||
Fixture fixture = new Fixture(3);
|
||||
IrisDecorator decorator = mock(IrisDecorator.class);
|
||||
PlatformBlockState stone = state("minecraft:stone");
|
||||
PlatformBlockState air = state("minecraft:air");
|
||||
when(air.isAir()).thenReturn(true);
|
||||
when(decorator.isForcePlace()).thenReturn(true);
|
||||
when(decorator.pickBlockData(any(), any(), anyDouble(), anyDouble())).thenReturn(fixture.cane);
|
||||
fixture.output.set(1, 3, 1, stone);
|
||||
fixture.output.set(1, 4, 1, air);
|
||||
fixture.output.set(0, 3, 1, fixture.water);
|
||||
|
||||
DecoratorCore.placeSurfaceSingle(decorator, 1, 1, -15, 3, -15,
|
||||
fixture.output, new RNG(3L), null, false, false, null);
|
||||
|
||||
assertSame(air, fixture.output.get(1, 4, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void naturalCrossChunkWaterHonorsAuthoredSeaLayers() {
|
||||
Fixture fixture = new Fixture(1);
|
||||
fixture.plannedTerrain();
|
||||
fixture.output.set(0, 3, 0, fixture.soil);
|
||||
when(fixture.complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 1D));
|
||||
when(fixture.complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 3D));
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
when(fixture.complex.getTrueBiomeStream()).thenReturn(
|
||||
ProceduralStream.of((x, z) -> biome, Interpolated.of(value -> 0D, value -> biome)));
|
||||
when(fixture.engine.getSeedManager()).thenReturn(mock(SeedManager.class));
|
||||
PlatformBlockState ice = state("minecraft:ice");
|
||||
when(biome.generateSeaLayers(anyDouble(), anyDouble(), any(), anyInt(), any()))
|
||||
.thenReturn(new KList<>(List.of(ice)));
|
||||
assertFalse(fixture.place(0, 0));
|
||||
when(biome.generateSeaLayers(anyDouble(), anyDouble(), any(), anyInt(), any()))
|
||||
.thenReturn(new KList<>(List.of(fixture.water)));
|
||||
assertTrue(fixture.place(0, 0));
|
||||
}
|
||||
|
||||
private static HydrologyColumnLayer wetLayer(int bed, int head) {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
11L, HydrologyFeatureType.RIFFLE, 12L, 13L, -17, head, -16, 1, 0, false);
|
||||
return new HydrologyColumnLayer(feature, bed, head, head,
|
||||
true, false, false, true, false, false, true, true, false,
|
||||
"water", "river", "mouth", "shore", "bank", "cave");
|
||||
}
|
||||
|
||||
private static PlatformBlockState state(String key) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
when(state.key()).thenReturn(key);
|
||||
return state;
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final IrisComplex complex = mock(IrisComplex.class);
|
||||
private final PlatformBlockState cane = state("minecraft:sugar_cane[age=0]");
|
||||
private final PlatformBlockState soil = state("minecraft:dirt");
|
||||
private final PlatformBlockState water = state("minecraft:water");
|
||||
private final Hunk<PlatformBlockState> output;
|
||||
|
||||
private Fixture(int size) {
|
||||
output = Hunk.newArrayHunk(size, 6, size);
|
||||
when(cane.canPlaceOnto(soil)).thenReturn(true);
|
||||
when(water.isWater()).thenReturn(true);
|
||||
}
|
||||
|
||||
private void plannedTerrain() {
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 3D));
|
||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 0D));
|
||||
}
|
||||
|
||||
private boolean place(int x, int z) {
|
||||
return IrisSugarCane.canPlace(cane, output, x, 4, z, -16 + x, -16 + z, engine);
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-26
@@ -48,7 +48,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
new HydrologyPoint(0, 20, 8),
|
||||
new HydrologyPoint(8, 20, 8),
|
||||
new HydrologyPoint(16, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverCourse course = course(ridge);
|
||||
HydrologyColumnLayer layer = layer(ridge, 8, 8);
|
||||
@@ -105,7 +105,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
new HydrologyPoint(0, 20, 8),
|
||||
new HydrologyPoint(8, 20, 8),
|
||||
new HydrologyPoint(16, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
|
||||
@@ -148,7 +148,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
new HydrologyPoint(0, 20, 8),
|
||||
new HydrologyPoint(8, 20, 8),
|
||||
new HydrologyPoint(16, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverCourse course = course(ridge);
|
||||
List<HydrologyColumnSample> columns = List.copyOf(
|
||||
@@ -235,7 +235,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
new HydrologyPoint(0, 20, 8),
|
||||
new HydrologyPoint(8, 20, 8),
|
||||
new HydrologyPoint(16, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
|
||||
@@ -271,7 +271,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
List.of(
|
||||
new HydrologyPoint(0, 20, 8),
|
||||
new HydrologyPoint(16, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
CavePosition exposedBoundary = new CavePosition(8, 25, 8);
|
||||
AtomicInteger voxelLoads = new AtomicInteger();
|
||||
@@ -310,7 +310,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
List.of(
|
||||
new HydrologyPoint(0, 20, 8),
|
||||
new HydrologyPoint(8, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverCourse course = course(ridge);
|
||||
RiverFootprint footprint = footprint(terrain, layer(ridge, 0, 8));
|
||||
@@ -347,7 +347,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 20, 0))
|
||||
List.of(new HydrologyPoint(0, 20, 0)), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
HydraulicSegment drop = new HydraulicSegment(
|
||||
173L,
|
||||
@@ -363,7 +363,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
new HydrologyPoint(0, 20, 0),
|
||||
new HydrologyPoint(6, 19, 0),
|
||||
new HydrologyPoint(12, 18, 0)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
HydraulicSegment surfacePool = new HydraulicSegment(
|
||||
174L,
|
||||
@@ -375,7 +375,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(12, 18, 0))
|
||||
List.of(new HydrologyPoint(12, 18, 0)), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverCourse course = new RiverCourse(
|
||||
171L,
|
||||
@@ -418,7 +418,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
List.of(
|
||||
new HydrologyPoint(0, 20, 8),
|
||||
new HydrologyPoint(8, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverCourse course = undergroundCourse(tunnel);
|
||||
RiverFootprint footprint = footprint(terrain, layer(tunnel, 8, 8));
|
||||
@@ -467,7 +467,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
List.of(
|
||||
new HydrologyPoint(0, 20, 8),
|
||||
new HydrologyPoint(8, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverCourse course = new RiverCourse(
|
||||
pool.courseId(),
|
||||
@@ -516,7 +516,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
List.of(
|
||||
new HydrologyPoint(0, 20, 0),
|
||||
new HydrologyPoint(12, 20, 0)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverCourse course = course(ridge);
|
||||
Random random = new Random(0x485944524f4c4f47L);
|
||||
@@ -576,7 +576,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(8, 76, 8))
|
||||
List.of(new HydrologyPoint(8, 76, 8)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse course = course(surface);
|
||||
RiverFootprint footprint = footprint(terrain, surfaceLayer(surface, 8, 8));
|
||||
@@ -615,7 +615,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
new HydrologyPoint(0, 20, 8),
|
||||
new HydrologyPoint(8, 20, 8),
|
||||
new HydrologyPoint(16, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverFootprint footprint = footprint(terrain, layer(ridge, 8, 8));
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
@@ -778,7 +778,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 20, 8), new HydrologyPoint(1, 20, 8))
|
||||
List.of(new HydrologyPoint(0, 20, 8), new HydrologyPoint(1, 20, 8)), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
HydraulicSegment grotto = grotto(142L, 140L, HydrologyFeatureType.INLAND_GROTTO);
|
||||
RiverCourse course = new RiverCourse(
|
||||
@@ -831,7 +831,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(8, 20, 8), new HydrologyPoint(9, 20, 8))
|
||||
List.of(new HydrologyPoint(8, 20, 8), new HydrologyPoint(9, 20, 8)), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverCourse course = undergroundCourse(tunnel);
|
||||
HydrologyColumnLayer oversized = caveLayer(
|
||||
@@ -890,7 +890,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selfCarvedUndergroundCourseDoesNotMaterializeObservedCavesOrBoundaryGuards() {
|
||||
public void selfCarvedUndergroundCourseChecksTerrainWithoutLoadingExistingCaves() {
|
||||
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(80, 0D, "parent");
|
||||
HydraulicSegment tunnel = new HydraulicSegment(
|
||||
161L,
|
||||
@@ -902,15 +902,16 @@ public class HydrologyCaveCourseFilterTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(8, 20, 8), new HydrologyPoint(9, 20, 8))
|
||||
List.of(new HydrologyPoint(8, 20, 8), new HydrologyPoint(9, 20, 8)), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverCourse course = undergroundCourse(tunnel);
|
||||
RiverFootprint footprint = footprint(terrain, layer(tunnel, 8, 8), layer(tunnel, 9, 8));
|
||||
AtomicInteger observedLoads = new AtomicInteger();
|
||||
AtomicInteger terrainChecks = new AtomicInteger();
|
||||
CaveVoxelView observedView = new CaveVoxelView() {
|
||||
@Override
|
||||
public boolean isInWorld(CavePosition position) {
|
||||
observedLoads.incrementAndGet();
|
||||
terrainChecks.incrementAndGet();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -928,7 +929,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
|
||||
@Override
|
||||
public boolean isAboveTerrainSurface(CavePosition position) {
|
||||
observedLoads.incrementAndGet();
|
||||
terrainChecks.incrementAndGet();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -952,6 +953,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
.allMatch((CaveVoxelPrecondition precondition) -> precondition.voxel() == CaveVoxel.UNCONDITIONAL));
|
||||
assertTrue(diagnostics.isEmpty());
|
||||
assertEquals(0, observedLoads.get());
|
||||
assertTrue(terrainChecks.get() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -970,7 +972,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
List.of(
|
||||
new HydrologyPoint(0, 20, 8),
|
||||
new HydrologyPoint(8, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
HydraulicSegment drop = new HydraulicSegment(
|
||||
103L,
|
||||
@@ -985,7 +987,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
List.of(
|
||||
new HydrologyPoint(8, 20, 8),
|
||||
new HydrologyPoint(9, 18, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
HydraulicSegment surface = new HydraulicSegment(
|
||||
104L,
|
||||
@@ -997,7 +999,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(9, 18, 8))
|
||||
List.of(new HydrologyPoint(9, 18, 8)), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
RiverCourse course = new RiverCourse(
|
||||
101L,
|
||||
@@ -1045,7 +1047,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
List.of(
|
||||
new HydrologyPoint(8, 20, 8),
|
||||
new HydrologyPoint(24, 20, 8)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
HydrologyColumnLayer local = layer(ridge, 15, 8);
|
||||
HydrologyColumnLayer remote = layer(ridge, 16, 8);
|
||||
@@ -1095,7 +1097,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
|
||||
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse seaCave = new RiverCourse(
|
||||
920L,
|
||||
@@ -1258,7 +1260,7 @@ public class HydrologyCaveCourseFilterTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(8, 20, 8))
|
||||
List.of(new HydrologyPoint(8, 20, 8)), HydraulicChannelProfile.uniform(2, 2)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyCoastalDropReservoirTest {
|
||||
private static final HydrologyPlannerSettings SETTINGS = HydrologyPlannerSettings.defaults();
|
||||
|
||||
@Test
|
||||
public void aDeclaredCoastalGrottoReceivesTheFallThroughUnownedNaturalOceanWater() {
|
||||
HydrologyTerrainSampler terrain = terrain(54);
|
||||
RiverCourse course = course(SETTINGS.seaLevel(), 1, true);
|
||||
HydrologyGeometrySampler geometry = HydrologyGeometrySampler.deterministic(terrain);
|
||||
HydrologySurfaceDropRaster full = HydrologySurfaceDropRaster.compile(SETTINGS, terrain, geometry, course);
|
||||
|
||||
assertTrue(full.connects(0, 0, 92));
|
||||
assertTrue(full.connects(0, 0, SETTINGS.seaLevel()));
|
||||
assertFalse(full.connects(1, 0, SETTINGS.seaLevel()));
|
||||
assertTrue(full.containsRequiredFluid(course, null));
|
||||
for (HydrologyColumnSample column : full.columns()) {
|
||||
assertFalse(column.ocean());
|
||||
}
|
||||
SurfaceBounds receivingWindow = new SurfaceBounds(1, -2, 2, 2);
|
||||
HydrologySurfaceDropRaster bounded = HydrologySurfaceDropRaster.compile(
|
||||
SETTINGS, terrain, geometry, course, receivingWindow);
|
||||
assertTrue(bounded.columns().isEmpty());
|
||||
assertTrue(bounded.containsRequiredFluid(course, receivingWindow));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anOceanBiomeOrUnrelatedGrottoCannotReplaceTheRequiredFluidInterval() {
|
||||
HydrologyTerrainSampler dryOceanBiome = terrain(SETTINGS.seaLevel());
|
||||
RiverCourse exact = course(SETTINGS.seaLevel(), 1, true);
|
||||
assertFalse(HydrologySurfaceDropRaster.compile(SETTINGS, dryOceanBiome,
|
||||
HydrologyGeometrySampler.deterministic(dryOceanBiome), exact).containsRequiredFluid(exact, null));
|
||||
|
||||
HydrologyTerrainSampler ocean = terrain(54);
|
||||
for (RiverCourse invalid : List.of(course(SETTINGS.seaLevel(), 1, false),
|
||||
course(SETTINGS.seaLevel(), 2, true), course(SETTINGS.seaLevel() + 1, 1, true))) {
|
||||
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(
|
||||
SETTINGS, ocean, HydrologyGeometrySampler.deterministic(ocean), invalid);
|
||||
assertFalse(raster.containsRequiredFluid(invalid, null));
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler terrain(int oceanFloor) {
|
||||
return (x, z) -> x >= 1 ? HydrologyTerrainSample.ocean(oceanFloor, "ocean")
|
||||
: HydrologyTerrainSample.openLand(92, 0D, "land");
|
||||
}
|
||||
|
||||
private static RiverCourse course(int head, int grottoEnd, boolean grotto) {
|
||||
HydraulicSegment falling = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
|
||||
92, head, 4, 3, true, true,
|
||||
List.of(new HydrologyPoint(0, 92, 0), new HydrologyPoint(1, head, 0)),
|
||||
HydraulicChannelProfile.uniform(4, 3));
|
||||
HydraulicSegment chamber = new HydraulicSegment(5L, 1L, HydrologyFeatureType.COASTAL_GROTTO,
|
||||
head, head, 4, 3, false, false,
|
||||
List.of(new HydrologyPoint(0, head, 0), new HydrologyPoint(grottoEnd, head, 0)),
|
||||
HydraulicChannelProfile.uniform(4, 3));
|
||||
return new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
|
||||
"default", 1, List.of(), grotto ? List.of(falling, chamber) : List.of(falling));
|
||||
}
|
||||
}
|
||||
+11
@@ -8,6 +8,17 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyCrossTileSurfaceAdmissionTest {
|
||||
@Test
|
||||
public void separatedTributariesCanJoinTheSameRegionalOutlet() {
|
||||
HydrologyCrossTileSurfaceAdmission.Claim first = claim(10L, 100L,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 63, 0)), false, 128);
|
||||
HydrologyCrossTileSurfaceAdmission.Claim second = claim(20L, 100L,
|
||||
List.of(new HydrologyPoint(0, 70, 512), new HydrologyPoint(256, 63, 512)), false, 128);
|
||||
assertTrue(HydrologyCrossTileSurfaceAdmission.admit(List.of(first), List.of(
|
||||
new HydrologyCrossTileSurfaceAdmission.RankedClaim(new HydrologyTileKey(0, 1), 0, second)))
|
||||
.rejections().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsClusteredIndependentMouths() {
|
||||
HydrologyCrossTileSurfaceAdmission.Claim current = claim(
|
||||
|
||||
+70
-69
@@ -80,7 +80,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
? 63 : 70;
|
||||
HydraulicSegment segment = new HydraulicSegment(
|
||||
9001L, 9000L, type, head, head, 6, 3, false, false,
|
||||
List.of(new HydrologyPoint(-12, head, -3), new HydrologyPoint(32, head, 5)));
|
||||
List.of(new HydrologyPoint(-12, head, -3), new HydrologyPoint(32, head, 5)), HydraulicChannelProfile.uniform(6, 3));
|
||||
RiverCourse course = course(9000L,
|
||||
type.isSurface() ? RiverCourseType.SURFACE : RiverCourseType.UNDERGROUND, segment);
|
||||
SlopeCountingSampler original = new SlopeCountingSampler(false);
|
||||
@@ -124,7 +124,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
4,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0)), HydraulicChannelProfile.uniform(10, 4)
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(
|
||||
z < 0 ? 54 : 82,
|
||||
@@ -158,7 +158,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse course = new RiverCourse(
|
||||
10L,
|
||||
@@ -206,7 +206,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
5,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(96, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(96, 70, 0)), HydraulicChannelProfile.uniform(12, 5)
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
|
||||
RiverFootprint footprint = new HydrologyFootprintCompiler(
|
||||
@@ -269,7 +269,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
RiverCourse course = course(14L, RiverCourseType.SURFACE, segment);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(70, 0D, "parent");
|
||||
@@ -328,7 +328,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
8,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, -20, 0))
|
||||
List.of(new HydrologyPoint(0, -20, 0)), HydraulicChannelProfile.uniform(40, 8)
|
||||
);
|
||||
RiverCourse course = new RiverCourse(
|
||||
14L,
|
||||
@@ -382,7 +382,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(16, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(16, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse course = course(110L, RiverCourseType.SURFACE, segment);
|
||||
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(80, 1D, "parent");
|
||||
@@ -425,7 +425,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydraulicSegment undergroundSegment = new HydraulicSegment(
|
||||
31L,
|
||||
@@ -437,7 +437,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(20, 50, 0))
|
||||
List.of(new HydrologyPoint(20, 50, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse surface = course(20L, RiverCourseType.SURFACE, surfaceSegment);
|
||||
RiverCourse underground = course(30L, RiverCourseType.UNDERGROUND, undergroundSegment);
|
||||
@@ -479,7 +479,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse course = course(40L, RiverCourseType.SURFACE, segment);
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
@@ -531,7 +531,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
10,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(12, 10)
|
||||
);
|
||||
RiverCourse course = course(42L, RiverCourseType.SURFACE, segment);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(80, 1D, "parent");
|
||||
@@ -584,7 +584,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(96, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(96, 70, 0)), HydraulicChannelProfile.uniform(12, 3)
|
||||
);
|
||||
RiverCourse course = course(46L, RiverCourseType.SURFACE, segment);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(84, 1D, "parent");
|
||||
@@ -613,7 +613,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
true,
|
||||
true,
|
||||
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(1, 68, 0))
|
||||
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(1, 68, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse course = course(50L, RiverCourseType.SURFACE, segment);
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
@@ -692,7 +692,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
new HydrologyPoint(0, 48, 0),
|
||||
new HydrologyPoint(12, 48, 5),
|
||||
new HydrologyPoint(24, 48, 0)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(12, 2)
|
||||
);
|
||||
RiverCourse course = course(60L, RiverCourseType.UNDERGROUND, segment);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(84, 1D, "parent");
|
||||
@@ -749,7 +749,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
)
|
||||
);
|
||||
RiverCourse underground = course(
|
||||
@@ -765,7 +765,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(96, 40, 0))
|
||||
List.of(new HydrologyPoint(96, 40, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
)
|
||||
);
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
@@ -823,7 +823,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydraulicSegment waterfall = new HydraulicSegment(
|
||||
221L,
|
||||
@@ -835,7 +835,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
true,
|
||||
true,
|
||||
List.of(new HydrologyPoint(0, 74, 0))
|
||||
List.of(new HydrologyPoint(0, 74, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(80, 1D, "parent");
|
||||
RiverFootprint footprint = new HydrologyFootprintCompiler(
|
||||
@@ -865,7 +865,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(-2, 70, 0), new HydrologyPoint(0, 70, 0))
|
||||
List.of(new HydrologyPoint(-2, 70, 0), new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydraulicSegment lower = new HydraulicSegment(
|
||||
232L,
|
||||
@@ -877,7 +877,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
true,
|
||||
List.of(new HydrologyPoint(0, 68, 0), new HydrologyPoint(2, 68, 0))
|
||||
List.of(new HydrologyPoint(0, 68, 0), new HydrologyPoint(2, 68, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(80, 1D, "parent");
|
||||
RiverFootprint footprint = new HydrologyFootprintCompiler(
|
||||
@@ -920,7 +920,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse course = course(60L, RiverCourseType.SURFACE, segment);
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
@@ -974,7 +974,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 63, 0))
|
||||
List.of(new HydrologyPoint(0, 63, 0)), HydraulicChannelProfile.uniform(6, 2)
|
||||
);
|
||||
RiverCourse course = course(70L, RiverCourseType.SURFACE, segment);
|
||||
|
||||
@@ -1000,7 +1000,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void naturallySubmergedLandAtAMouthPublishesOnlyANonOwningApron() {
|
||||
public void mouthCutsDrySeaLevelLandIntoAnOwnedChannel() {
|
||||
HydraulicSegment mouth = new HydraulicSegment(
|
||||
81L,
|
||||
80L,
|
||||
@@ -1011,16 +1011,16 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 63, 0))
|
||||
List.of(new HydrologyPoint(0, 63, 0)), HydraulicChannelProfile.uniform(6, 2)
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(63, 0D, "shore");
|
||||
RiverFootprint footprint = compiler(terrain).compile(List.of(course(80L, RiverCourseType.SURFACE, mouth)));
|
||||
|
||||
HydrologyColumnSample sample = footprint.sample(0, 0).orElseThrow();
|
||||
assertEquals(63, sample.terrainHeight());
|
||||
assertTrue(sample.layers().stream().allMatch(HydrologyColumnLayer::oceanApron));
|
||||
assertTrue(sample.layers().stream().noneMatch(HydrologyColumnLayer::terrainOwned));
|
||||
assertTrue(sample.layers().stream().noneMatch(HydrologyColumnLayer::fluidOwned));
|
||||
assertEquals(61, sample.terrainHeight());
|
||||
assertTrue(sample.layers().stream().noneMatch(HydrologyColumnLayer::oceanApron));
|
||||
assertTrue(sample.layers().stream().allMatch(HydrologyColumnLayer::terrainOwned));
|
||||
assertTrue(sample.layers().stream().allMatch(HydrologyColumnLayer::fluidOwned));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1035,7 +1035,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydraulicSegment cave = new HydraulicSegment(
|
||||
303L,
|
||||
@@ -1047,7 +1047,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(8, 66, 0), new HydrologyPoint(12, 66, 0))
|
||||
List.of(new HydrologyPoint(8, 66, 0), new HydrologyPoint(12, 66, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse course = new RiverCourse(
|
||||
301L,
|
||||
@@ -1112,7 +1112,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0)), HydraulicChannelProfile.uniform(6, 3)
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(
|
||||
x < 4 ? 90 : 77,
|
||||
@@ -1159,7 +1159,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(-8, 70, 0), new HydrologyPoint(8, 70, 0))
|
||||
List.of(new HydrologyPoint(-8, 70, 0), new HydrologyPoint(8, 70, 0)), HydraulicChannelProfile.uniform(6, 2)
|
||||
),
|
||||
new HydraulicSegment(
|
||||
403L,
|
||||
@@ -1171,7 +1171,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(8, 60, 0), new HydrologyPoint(14, 60, 0))
|
||||
List.of(new HydrologyPoint(8, 60, 0), new HydrologyPoint(14, 60, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
),
|
||||
new HydraulicSegment(
|
||||
404L,
|
||||
@@ -1183,7 +1183,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(14, 63, 0), new HydrologyPoint(20, 63, 0))
|
||||
List.of(new HydrologyPoint(14, 63, 0), new HydrologyPoint(20, 63, 0)), HydraulicChannelProfile.uniform(6, 2)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1200,7 +1200,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 74, -4), new HydrologyPoint(12, 74, -4))
|
||||
List.of(new HydrologyPoint(0, 74, -4), new HydrologyPoint(12, 74, -4)), HydraulicChannelProfile.uniform(4, 2)
|
||||
)
|
||||
);
|
||||
RiverCourse underground = new RiverCourse(
|
||||
@@ -1222,7 +1222,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(4, 45, 2), new HydrologyPoint(16, 45, 2))
|
||||
List.of(new HydrologyPoint(4, 45, 2), new HydrologyPoint(16, 45, 2)), HydraulicChannelProfile.uniform(4, 2)
|
||||
),
|
||||
new HydraulicSegment(
|
||||
423L,
|
||||
@@ -1234,7 +1234,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(16, 63, 2), new HydrologyPoint(20, 63, 2))
|
||||
List.of(new HydrologyPoint(16, 63, 2), new HydrologyPoint(20, 63, 2)), HydraulicChannelProfile.uniform(4, 2)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1256,9 +1256,9 @@ public class HydrologyFootprintCompilerTest {
|
||||
).compile(courses);
|
||||
|
||||
for (HydrologyColumnSample sample : full.columns().values()) {
|
||||
assertEquals(sample.primarySurfaceLayer().isPresent(),
|
||||
assertEquals(sample.primarySurfaceLayer().filter(HydrologyColumnLayer::terrainOwned).isPresent(),
|
||||
validation.plannedSurface().ownsTerrain(sample.x(), sample.z()));
|
||||
assertEquals(sample.primarySurfaceLayer().isPresent(),
|
||||
assertEquals(sample.primarySurfaceLayer().filter(HydrologyColumnLayer::terrainOwned).isPresent(),
|
||||
validation.withMaterializedSurface(full).plannedSurface().ownsTerrain(sample.x(), sample.z()));
|
||||
assertEquals(
|
||||
"planned surface at " + sample.x() + "," + sample.z(),
|
||||
@@ -1319,7 +1319,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
new HydrologyPoint(0, 70, 0),
|
||||
new HydrologyPoint(32, 70, 0),
|
||||
new HydrologyPoint(64, 70, 0)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
HydraulicSegment first = new HydraulicSegment(
|
||||
502L,
|
||||
@@ -1331,7 +1331,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(32, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(32, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
HydraulicSegment second = new HydraulicSegment(
|
||||
503L,
|
||||
@@ -1343,7 +1343,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(32, 70, 0), new HydrologyPoint(64, 70, 0))
|
||||
List.of(new HydrologyPoint(32, 70, 0), new HydrologyPoint(64, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
|
||||
RiverFootprint continuousFootprint = compiler(terrain).compile(List.of(new RiverCourse(
|
||||
@@ -1398,7 +1398,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(32, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(32, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
HydraulicSegment second = new HydraulicSegment(
|
||||
512L,
|
||||
@@ -1410,7 +1410,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(32, 70, 0), new HydrologyPoint(32, 70, 32))
|
||||
List.of(new HydrologyPoint(32, 70, 0), new HydrologyPoint(32, 70, 32)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
|
||||
RiverFootprint footprint = compiler(terrain).compile(List.of(new RiverCourse(
|
||||
@@ -1442,10 +1442,11 @@ public class HydrologyFootprintCompilerTest {
|
||||
70,
|
||||
70,
|
||||
8,
|
||||
3,
|
||||
4,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(64, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0), new HydrologyPoint(64, 70, 0)),
|
||||
new HydraulicChannelProfile(new double[]{8D, 8D, 8D}, new double[]{4D, 3D, 3D})
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
|
||||
RiverCourse course = course(520L, RiverCourseType.SURFACE, segment);
|
||||
@@ -1481,7 +1482,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 74, 0), new HydrologyPoint(24, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 74, 0), new HydrologyPoint(24, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
|
||||
RiverFootprint footprint = compiler(terrain).compile(List.of(course(527L, RiverCourseType.SURFACE, cascade)));
|
||||
@@ -1507,7 +1508,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aOneBlockLowBankIsLippedAndADeeperOneIsReportedWithoutRaising() {
|
||||
public void lowBanksKeepTheirNaturalHeightAndReportUncontainedWater() {
|
||||
HydraulicSegment channel = new HydraulicSegment(
|
||||
529L,
|
||||
528L,
|
||||
@@ -1518,7 +1519,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
RiverCourse course = course(528L, RiverCourseType.SURFACE, channel);
|
||||
HydrologyTerrainSampler highTerrain = (int x, int z) ->
|
||||
@@ -1547,12 +1548,12 @@ public class HydrologyFootprintCompilerTest {
|
||||
HydrologyColumnSample deepBank = deepBankFootprint.sample(12, lowBankZ).orElseThrow();
|
||||
|
||||
assertEquals(0, unsupportedBankCells(highFootprint, 528L));
|
||||
// One block short of the water: a one-block lip holds it and nothing is reported.
|
||||
// A low bank remains below the water and is reported without raising terrain.
|
||||
assertEquals(69, lowBank.naturalHeight());
|
||||
assertEquals(70, lowBank.terrainHeight());
|
||||
assertEquals(0, unsupportedBankCells(lowBankFootprint, 528L));
|
||||
assertEquals(69, lowBank.terrainHeight());
|
||||
assertTrue(unsupportedBankCells(lowBankFootprint, 528L) > 0);
|
||||
for (HydrologyColumnSample sample : lowBankFootprint.columns().values()) {
|
||||
assertTrue(sample.terrainHeight() <= sample.naturalHeight() + 1);
|
||||
assertTrue(sample.terrainHeight() <= sample.naturalHeight());
|
||||
}
|
||||
// Two blocks short: the bank keeps its natural height and the edge is reported.
|
||||
assertEquals(68, deepBank.naturalHeight());
|
||||
@@ -1572,7 +1573,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
RiverCourse course = course(528L, RiverCourseType.SURFACE, channel);
|
||||
HydrologyFootprintCompiler compiler = compiler((int x, int z) ->
|
||||
@@ -1605,7 +1606,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
new HydrologyPoint(2, 72, 0),
|
||||
new HydrologyPoint(3, 71, 0),
|
||||
new HydrologyPoint(4, 70, 0)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
RiverCourse course = course(529L, RiverCourseType.SURFACE, cascade);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.ocean(82, "ocean");
|
||||
@@ -1626,7 +1627,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(32, 63, 0))
|
||||
List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(32, 63, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
HydraulicSegment mouth = new HydraulicSegment(
|
||||
532L,
|
||||
@@ -1638,7 +1639,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(32, 63, 0), new HydrologyPoint(64, 63, 0))
|
||||
List.of(new HydrologyPoint(32, 63, 0), new HydrologyPoint(64, 63, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> x >= 64
|
||||
? HydrologyTerrainSample.ocean(48, "ocean")
|
||||
@@ -1696,7 +1697,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(48, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(48, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
HydraulicSegment sinkhole = new HydraulicSegment(
|
||||
542L,
|
||||
@@ -1708,7 +1709,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
true,
|
||||
List.of(new HydrologyPoint(48, 70, 0), new HydrologyPoint(52, 60, 0))
|
||||
List.of(new HydrologyPoint(48, 70, 0), new HydrologyPoint(52, 60, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydraulicSegment grotto = new HydraulicSegment(
|
||||
543L,
|
||||
@@ -1720,7 +1721,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
4,
|
||||
false,
|
||||
true,
|
||||
List.of(new HydrologyPoint(52, 60, 0))
|
||||
List.of(new HydrologyPoint(52, 60, 0)), HydraulicChannelProfile.uniform(8, 4)
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
|
||||
RiverFootprint footprint = compiler(terrain).compile(List.of(new RiverCourse(
|
||||
@@ -1761,7 +1762,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(64, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(64, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydraulicSegment receiver = new HydraulicSegment(
|
||||
552L,
|
||||
@@ -1773,7 +1774,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
true,
|
||||
List.of(new HydrologyPoint(64, 70, 0), new HydrologyPoint(80, 69, 0))
|
||||
List.of(new HydrologyPoint(64, 70, 0), new HydrologyPoint(80, 69, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydraulicSegment outflow = new HydraulicSegment(
|
||||
553L,
|
||||
@@ -1785,7 +1786,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(80, 69, 0), new HydrologyPoint(160, 69, 0))
|
||||
List.of(new HydrologyPoint(80, 69, 0), new HydrologyPoint(160, 69, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
|
||||
RiverFootprint footprint = compiler(terrain).compile(List.of(new RiverCourse(
|
||||
@@ -1876,7 +1877,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(96, 63, 0), new HydrologyPoint(112, 63, 0))
|
||||
List.of(new HydrologyPoint(96, 63, 0), new HydrologyPoint(112, 63, 0)), HydraulicChannelProfile.uniform(8, 3)
|
||||
);
|
||||
RiverFootprint mouthFootprint = new HydrologyFootprintCompiler(
|
||||
seaCaveSettings(8, 12),
|
||||
@@ -1966,7 +1967,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
|
||||
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
return new RiverCourse(
|
||||
id,
|
||||
@@ -1992,10 +1993,10 @@ public class HydrologyFootprintCompilerTest {
|
||||
12,
|
||||
true,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(32, 70, 0))
|
||||
List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(32, 70, 0)), HydraulicChannelProfile.uniform(16, 12)
|
||||
);
|
||||
RiverCourse course = course(120L, RiverCourseType.SURFACE, segment);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(96, 1D, "parent");
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(70, 1D, "parent");
|
||||
|
||||
RiverFootprint defaults = compile(HydrologyPlannerSettings.defaults(), terrain, course);
|
||||
RiverFootprint explicit = compile(withFlow(new HydrologyPlannerSettings.Flow(0.65D, 2, 2D, 1)), terrain, course);
|
||||
@@ -2071,7 +2072,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 48, 0))
|
||||
List.of(new HydrologyPoint(0, 48, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse course = course(140L, RiverCourseType.UNDERGROUND, chamber);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(84, 1D, "parent");
|
||||
@@ -2137,7 +2138,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
new HydrologyPoint(0, 48, 0),
|
||||
new HydrologyPoint(bend / 2, 48, 5),
|
||||
new HydrologyPoint(bend, 48, 0)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(12, 2)
|
||||
);
|
||||
return new RiverCourse(
|
||||
id,
|
||||
|
||||
@@ -67,7 +67,7 @@ public class HydrologyModelTest {
|
||||
2,
|
||||
false,
|
||||
true,
|
||||
centerline
|
||||
centerline, HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
assertEquals(6, graded.drop());
|
||||
assertFalse(graded.fallingFluid());
|
||||
@@ -81,7 +81,7 @@ public class HydrologyModelTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
centerline
|
||||
centerline, HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
assertFalse(intermediate.receivingPool());
|
||||
HydraulicSegment accepted = new HydraulicSegment(
|
||||
@@ -94,7 +94,7 @@ public class HydrologyModelTest {
|
||||
2,
|
||||
true,
|
||||
true,
|
||||
centerline
|
||||
centerline, HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
assertEquals(6, accepted.drop());
|
||||
}
|
||||
@@ -111,7 +111,7 @@ public class HydrologyModelTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 80, 0))
|
||||
List.of(new HydrologyPoint(0, 80, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydraulicSegment sinkhole = new HydraulicSegment(
|
||||
2L,
|
||||
@@ -123,7 +123,7 @@ public class HydrologyModelTest {
|
||||
2,
|
||||
true,
|
||||
true,
|
||||
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(1, 74, 0))
|
||||
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(1, 74, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
HydraulicSegment grotto = new HydraulicSegment(
|
||||
3L,
|
||||
@@ -135,7 +135,7 @@ public class HydrologyModelTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(1, 74, 0))
|
||||
List.of(new HydrologyPoint(1, 74, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
|
||||
RiverCourse accepted = new RiverCourse(
|
||||
@@ -160,7 +160,7 @@ public class HydrologyModelTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 80, 0))
|
||||
List.of(new HydrologyPoint(0, 80, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse containedApproach = new RiverCourse(
|
||||
9L,
|
||||
@@ -197,7 +197,7 @@ public class HydrologyModelTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
|
||||
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0)), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
|
||||
RiverCourse seaCave = new RiverCourse(
|
||||
@@ -288,7 +288,7 @@ public class HydrologyModelTest {
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyColumnSample(
|
||||
4,
|
||||
9,
|
||||
63,
|
||||
62,
|
||||
63,
|
||||
false,
|
||||
"shore_parent",
|
||||
@@ -298,7 +298,7 @@ public class HydrologyModelTest {
|
||||
HydrologyColumnSample raised = new HydrologyColumnSample(
|
||||
4,
|
||||
9,
|
||||
64,
|
||||
63,
|
||||
63,
|
||||
false,
|
||||
"shore_parent",
|
||||
@@ -394,6 +394,23 @@ public class HydrologyModelTest {
|
||||
assertEquals(72, sample.terrainHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uncutShoreRetainsItsBiomeWithoutOverridingAnOwnedBank() {
|
||||
HydrologyColumnLayer shore = surfaceLayer(feature(HydrologyFeatureType.SURFACE_POOL, 30L, 75),
|
||||
75, 75, false, true, true, false, false);
|
||||
HydrologyColumnLayer bank = surfaceLayer(feature(HydrologyFeatureType.SURFACE_POOL, 31L, 72),
|
||||
72, 72, false, false, true, true, false);
|
||||
HydrologyColumnSample uncut = new HydrologyColumnSample(0, 0, 75, 63, false, "parent", List.of(shore));
|
||||
HydrologyColumnSample cut = new HydrologyColumnSample(0, 0, 75, 63, false, "parent", List.of(shore, bank));
|
||||
|
||||
assertEquals(shore, uncut.primarySurfaceLayer().orElseThrow());
|
||||
assertEquals("shore", uncut.primarySurfaceLayer().orElseThrow().biomeKey());
|
||||
assertEquals(75, uncut.terrainHeight());
|
||||
assertTrue(uncut.primarySurfaceFluidLayer().isEmpty());
|
||||
assertEquals(bank, cut.primarySurfaceLayer().orElseThrow());
|
||||
assertEquals(72, cut.terrainHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedSurfaceArbitrationPrefersChannelThenHydraulicType() {
|
||||
HydrologyColumnLayer grading = surfaceLayer(
|
||||
@@ -620,7 +637,7 @@ public class HydrologyModelTest {
|
||||
new HydrologyPoint(0, 20, 0),
|
||||
new HydrologyPoint(500, 20, 0),
|
||||
new HydrologyPoint(1000, 20, 0)
|
||||
)
|
||||
), HydraulicChannelProfile.uniform(4, 2)
|
||||
);
|
||||
RiverCourse course = new RiverCourse(
|
||||
201L,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user