mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Polish surface rivers: estuaries, ponds, erosion, sea caves
Surface rivers gain a drowned inlet at the mouth, a spring pond and an inland terminal pond, a configurable erosion profile (thalweg fraction, blend curve, bed noise, smoothing) and a per-area shore biome width; banks lose freeboard and inset for a single sink. Underground courses accept tributaries, coastal grottos can be admitted as standalone sea caves, and a region or biome can confine the rivers born in it. Tile planning forks its ring work through HydrologyForkJoin. Pack validation, the schema, the probes and the tests follow.
This commit is contained in:
@@ -183,9 +183,10 @@ final class PackHydrologyValidator {
|
||||
if (geometry != null) {
|
||||
validateGeometry(path + ".geometry", geometry, errors);
|
||||
}
|
||||
int minimumSurfaceCourseLength = routing == null ? 384 : integerValue(routing, "minimumSurfaceCourseLength", 384);
|
||||
SourceBudget surfaceBudget = surface == null
|
||||
? SourceBudget.defaults()
|
||||
: validateSurface(path + ".surface", surface, dimension, errors, warnings);
|
||||
: validateSurface(path + ".surface", surface, dimension, minimumSurfaceCourseLength, errors, warnings);
|
||||
SourceBudget undergroundBudget = underground == null
|
||||
? SourceBudget.defaults()
|
||||
: validateUnderground(path + ".underground", underground, dimension, errors, warnings);
|
||||
@@ -276,6 +277,7 @@ final class PackHydrologyValidator {
|
||||
errors
|
||||
);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "maximumOutletsPerTile", 1, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "maximumCoastalOutletsPerTile", 0, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalBoolean(path, routing, "oceanOutlets", errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "valleyPreference", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "uphillPenalty", 0D, 128D, errors);
|
||||
@@ -320,6 +322,7 @@ final class PackHydrologyValidator {
|
||||
String path,
|
||||
JSONObject surface,
|
||||
JSONObject dimension,
|
||||
int minimumSurfaceCourseLength,
|
||||
List<String> errors,
|
||||
List<String> warnings
|
||||
) {
|
||||
@@ -330,6 +333,8 @@ final class PackHydrologyValidator {
|
||||
JSONObject flow = nestedObject(surface, "flow", path, errors);
|
||||
JSONObject mouths = nestedObject(surface, "mouths", path, errors);
|
||||
JSONObject bed = nestedObject(surface, "bed", path, errors);
|
||||
JSONObject erosion = nestedObject(surface, "erosion", path, errors);
|
||||
JSONObject ponds = nestedObject(surface, "ponds", path, errors);
|
||||
|
||||
SourceBudget budget = sources == null
|
||||
? SourceBudget.defaults()
|
||||
@@ -344,11 +349,17 @@ final class PackHydrologyValidator {
|
||||
validateSurfaceFlow(path + ".flow", flow, errors);
|
||||
}
|
||||
if (mouths != null) {
|
||||
validateMouths(path + ".mouths", mouths, errors);
|
||||
validateMouths(path + ".mouths", mouths, minimumSurfaceCourseLength, errors);
|
||||
}
|
||||
if (bed != null) {
|
||||
validateSurfaceBed(path + ".bed", bed, errors);
|
||||
}
|
||||
if (erosion != null) {
|
||||
validateSurfaceErosion(path + ".erosion", erosion, errors);
|
||||
}
|
||||
if (ponds != null) {
|
||||
validateSurfacePonds(path + ".ponds", ponds, errors);
|
||||
}
|
||||
|
||||
boolean surfaceEnabled = booleanValue(surface, "enabled", true);
|
||||
if (surfaceEnabled && budget.maximumExpectedSources() == 0) {
|
||||
@@ -368,7 +379,7 @@ final class PackHydrologyValidator {
|
||||
private static void validateSurfaceChannel(String path, JSONObject channel, List<String> errors) {
|
||||
validateRange(path, channel, "width", 1D, 128D, 4D, 8D, errors);
|
||||
validateRange(path, channel, "depth", 1D, 64D, 2D, 4D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "inset", 0, 3, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "sink", 0, 3, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "maximumIncision", 1, 32, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "springWidthRatio", 1D, 4D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "springLength", 4, 96, errors);
|
||||
@@ -376,8 +387,33 @@ final class PackHydrologyValidator {
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "roughnessWavelength", 4, 64, errors);
|
||||
}
|
||||
|
||||
private static void validateSurfaceErosion(String path, JSONObject erosion, List<String> errors) {
|
||||
PackJsonFieldChecks.validateOptionalBoolean(path, erosion, "enabled", errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, erosion, "smoothingRadius", 0, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, erosion, "thalwegFraction", 0D, 0.95D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, erosion, "blendCurve", 0.25D, 4D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, erosion, "bedNoise", 0D, 2D, errors);
|
||||
}
|
||||
|
||||
private static void validateSurfacePonds(String path, JSONObject ponds, List<String> errors) {
|
||||
for (String end : new String[] {"source", "terminal"}) {
|
||||
JSONObject pond = nestedObject(ponds, end, path, errors);
|
||||
if (pond == null) {
|
||||
continue;
|
||||
}
|
||||
String pondPath = path + "." + end;
|
||||
PackJsonFieldChecks.validateOptionalBoolean(pondPath, pond, "enabled", errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(pondPath, pond, "minimumRadius", 1, 32, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(pondPath, pond, "maximumRadius", 1, 32, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(pondPath, pond, "depth", 1, 8, errors);
|
||||
if (pond.has("minimumRadius") && pond.has("maximumRadius")
|
||||
&& pond.optInt("minimumRadius") > pond.optInt("maximumRadius")) {
|
||||
errors.add(pondPath + ".minimumRadius must not exceed maximumRadius");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateSurfaceBanks(String path, JSONObject banks, List<String> errors) {
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "freeboard", 0, 4, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "shoreWidth", 0.5D, 6D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "blendSlope", 0.5D, 12D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "minimumBlendWidth", 1, 64, errors);
|
||||
@@ -426,9 +462,16 @@ final class PackHydrologyValidator {
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateMouths(String path, JSONObject mouths, List<String> errors) {
|
||||
private static void validateMouths(String path, JSONObject mouths, int minimumSurfaceCourseLength, List<String> errors) {
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, mouths, "flareRatio", 1D, 4D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "maximumOceanApron", 0, 32, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "inletLength", 0, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "inletDepth", 0, 16, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "maximumIncision", 0, 128, errors);
|
||||
// The drowned reach is the end of a river, never the whole of one.
|
||||
if (mouths.has("inletLength") && integerValue(mouths, "inletLength", 64) > minimumSurfaceCourseLength) {
|
||||
errors.add(path + ".inletLength must not exceed routing.minimumSurfaceCourseLength.");
|
||||
}
|
||||
}
|
||||
|
||||
private static SourceBudget validateUnderground(
|
||||
@@ -554,6 +597,22 @@ final class PackHydrologyValidator {
|
||||
errors.add(path + ".poolLevel must be SEA_LEVEL.");
|
||||
}
|
||||
}
|
||||
if (coastal) {
|
||||
JSONObject seaCaves = nestedObject(grotto, "seaCaves", path, errors);
|
||||
if (seaCaves != null) {
|
||||
String seaCavePath = path + ".seaCaves";
|
||||
PackJsonFieldChecks.validateOptionalBoolean(seaCavePath, seaCaves, "enabled", errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "maximumPerTile", 0, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "minimumSpacing", 16, 8192, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "minimumCoastHeight", 1, 128, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "depth", 0, 128, errors);
|
||||
int horizontalRadius = integerValue(grotto, "horizontalRadius", DEFAULT_COASTAL_GROTTO.getHorizontalRadius());
|
||||
int minimumSpacing = integerValue(seaCaves, "minimumSpacing", DEFAULT_COASTAL_GROTTO.getSeaCaves().getMinimumSpacing());
|
||||
if (minimumSpacing < horizontalRadius * 2) {
|
||||
errors.add(seaCavePath + ".minimumSpacing must be at least twice " + path + ".horizontalRadius.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int verticalRadius = integerValue(
|
||||
grotto,
|
||||
@@ -902,6 +961,8 @@ final class PackHydrologyValidator {
|
||||
validateNullableDouble(path, policy, "incisionMultiplier", 0D, 16D, errors);
|
||||
validateNullableDouble(path, policy, "routingMultiplier", 0D, 64D, errors);
|
||||
validateNullableDouble(path, policy, "bankMultiplier", 0D, 4D, errors);
|
||||
validateNullableDouble(path, policy, "shoreBiomeWidth", 0D, 32D, errors);
|
||||
validateNullableBoolean(path, policy, "confined", errors);
|
||||
return policy;
|
||||
}
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ public final class IrisRenderer {
|
||||
public static int hydrologyDiagnosticColor(HydrologyCandidateKind kind) {
|
||||
Objects.requireNonNull(kind, "kind");
|
||||
return switch (kind) {
|
||||
case SOURCE -> PROJECTED_SOURCE;
|
||||
case SOURCE, TRIBUTARY -> PROJECTED_SOURCE;
|
||||
case OUTLET -> PROJECTED_OUTLET;
|
||||
case DEEP_FLUID, POOL -> PROJECTED_DEEP_FLUID;
|
||||
};
|
||||
|
||||
@@ -4,5 +4,7 @@ public enum HydrologyCandidateKind {
|
||||
SOURCE,
|
||||
OUTLET,
|
||||
DEEP_FLUID,
|
||||
POOL
|
||||
POOL,
|
||||
/** A later course to an outlet that could not be joined to the outlet's main course. */
|
||||
TRIBUTARY
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.hydrology;
|
||||
public enum HydrologyCandidateRejection {
|
||||
NO_LEGAL_OUTLET,
|
||||
NO_DRAINAGE_PATH,
|
||||
CONFINED_NO_OUTLET,
|
||||
ROUTE_LIMIT,
|
||||
COURSE_TOO_SHORT,
|
||||
SOURCE_QUOTA,
|
||||
|
||||
+93
-9
@@ -998,6 +998,9 @@ final class HydrologyFootprintCompiler {
|
||||
continue;
|
||||
}
|
||||
addLayer(columns, x, z, terrain, layer);
|
||||
if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
addAdjacentSeaApron(columns, course, segment, point, x, z, flowX, flowZ, pointFeatures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1010,9 +1013,10 @@ final class HydrologyFootprintCompiler {
|
||||
int z,
|
||||
SurfaceRasterIndex plannedSurface
|
||||
) {
|
||||
if (!terrainRoofedSurfaceTransition(course, segment)) {
|
||||
if (!terrainRoofedLayer(course, segment)) {
|
||||
return layer;
|
||||
}
|
||||
boolean seaCave = course.type() == RiverCourseType.SEA_CAVE;
|
||||
int plannedTerrainHeight = plannedSurface.resolve(
|
||||
x,
|
||||
z,
|
||||
@@ -1031,6 +1035,10 @@ final class HydrologyFootprintCompiler {
|
||||
if (neighborTerrain == null) {
|
||||
continue;
|
||||
}
|
||||
if (seaCave && neighborTerrain.ocean()) {
|
||||
// The sea face is the intended opening; the seabed never lowers the chamber roof.
|
||||
continue;
|
||||
}
|
||||
if (plannedSurface.ownsSurfaceChannelAt(neighborX, neighborZ, course.id())) {
|
||||
continue;
|
||||
}
|
||||
@@ -1041,6 +1049,10 @@ final class HydrologyFootprintCompiler {
|
||||
);
|
||||
maximumCeiling = Math.min(maximumCeiling, neighborSurface - 1);
|
||||
}
|
||||
if (seaCave && Math.min(layer.ceilingY(), maximumCeiling) <= layer.fluidHeadY()) {
|
||||
// A sea-cave column needs air between the sea and its roof; a roof at or below sea level is not carved.
|
||||
return null;
|
||||
}
|
||||
if (maximumCeiling == Integer.MAX_VALUE || layer.ceilingY() <= maximumCeiling) {
|
||||
return layer;
|
||||
}
|
||||
@@ -1158,7 +1170,7 @@ final class HydrologyFootprintCompiler {
|
||||
);
|
||||
int fluidHead = channel ? shape.fluidHead() : bed;
|
||||
int ceiling = channel ? localCeiling(shape, ellipsoidScale) : fluidHead;
|
||||
if (channel && terrainRoofedSurfaceTransition(course, segment)) {
|
||||
if (channel && terrainRoofedLayer(course, segment)) {
|
||||
ceiling = Math.max(fluidHead, Math.min(ceiling, terrain.naturalHeight() - 1));
|
||||
}
|
||||
boolean terrainOwned = !falling;
|
||||
@@ -1235,13 +1247,24 @@ final class HydrologyFootprintCompiler {
|
||||
HydrologyTerrainSample terrain,
|
||||
HydrologyFeatureRef[] pointFeatures
|
||||
) {
|
||||
if (segment.type() != HydrologyFeatureType.MOUTH
|
||||
&& segment.type() != HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
return;
|
||||
}
|
||||
if (distance > settings.outlets().maximumOceanApron() + 0.25D) {
|
||||
if (!oceanApronEligible(segment, distance)) {
|
||||
return;
|
||||
}
|
||||
addOceanApronLayer(columns, course, segment, point, x, z, flowX, flowZ, terrain, pointFeatures);
|
||||
}
|
||||
|
||||
private void addOceanApronLayer(
|
||||
Long2ObjectLinkedOpenHashMap<MutableColumn> columns,
|
||||
RiverCourse course,
|
||||
HydraulicSegment segment,
|
||||
HydrologyPoint point,
|
||||
int x,
|
||||
int z,
|
||||
int flowX,
|
||||
int flowZ,
|
||||
HydrologyTerrainSample terrain,
|
||||
HydrologyFeatureRef[] pointFeatures
|
||||
) {
|
||||
int fluidHead = Math.min(settings.seaLevel(), point.y());
|
||||
HydrologyFeatureRef feature = feature(
|
||||
course,
|
||||
@@ -1396,7 +1419,14 @@ final class HydrologyFootprintCompiler {
|
||||
return shape.fluidHead() + localExtent;
|
||||
}
|
||||
|
||||
private boolean terrainRoofedSurfaceTransition(RiverCourse course, HydraulicSegment segment) {
|
||||
/**
|
||||
* Layers whose ceiling follows the terrain: the underground transition of a surface river, and the
|
||||
* chamber of a standalone sea cave, which must never break the surface above it.
|
||||
*/
|
||||
private boolean terrainRoofedLayer(RiverCourse course, HydraulicSegment segment) {
|
||||
if (course.type() == RiverCourseType.SEA_CAVE) {
|
||||
return segment.type() == HydrologyFeatureType.COASTAL_GROTTO;
|
||||
}
|
||||
return course.type() == RiverCourseType.SURFACE
|
||||
&& (segment.type() == HydrologyFeatureType.UNDERGROUND_POOL
|
||||
|| segment.type() == HydrologyFeatureType.UNDERGROUND_DROP);
|
||||
@@ -1680,7 +1710,61 @@ final class HydrologyFootprintCompiler {
|
||||
private boolean oceanApronEligible(HydraulicSegment segment, double distance) {
|
||||
return (segment.type() == HydrologyFeatureType.MOUTH
|
||||
|| segment.type() == HydrologyFeatureType.COASTAL_GROTTO)
|
||||
&& distance <= settings.outlets().maximumOceanApron() + 0.25D;
|
||||
&& distance <= apronReach(segment) + 0.25D;
|
||||
}
|
||||
|
||||
/**
|
||||
* How far from the centerline the ocean apron reaches. A coastal grotto chamber is as wide as its
|
||||
* horizontal radius, so its apron reaches at least that far: every sea column the chamber touches
|
||||
* must carry the apron that declares the sea face an intentional opening.
|
||||
*/
|
||||
private int apronReach(HydraulicSegment segment) {
|
||||
int reach = settings.outlets().maximumOceanApron();
|
||||
if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
return Math.max(reach, settings.outlets().coastalGrotto().horizontalRadius());
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
/**
|
||||
* The organic chamber boundary can put a sea column one block past the apron reach while the land
|
||||
* column beside it is still carved; the apron follows the chamber there so the face stays open.
|
||||
*/
|
||||
private void addAdjacentSeaApron(
|
||||
Long2ObjectLinkedOpenHashMap<MutableColumn> columns,
|
||||
RiverCourse course,
|
||||
HydraulicSegment segment,
|
||||
HydrologyPoint point,
|
||||
int x,
|
||||
int z,
|
||||
int flowX,
|
||||
int flowZ,
|
||||
HydrologyFeatureRef[] pointFeatures
|
||||
) {
|
||||
for (int[] offset : HORIZONTAL_NEIGHBORS) {
|
||||
int neighborX = x + offset[0];
|
||||
int neighborZ = z + offset[1];
|
||||
HydrologyTerrainSample neighborTerrain = sampleTerrainBasis(neighborX, neighborZ);
|
||||
if (neighborTerrain == null) {
|
||||
continue;
|
||||
}
|
||||
if (classifyNatural(neighborX, neighborZ) != HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|
||||
&& !neighborTerrain.ocean()) {
|
||||
continue;
|
||||
}
|
||||
addOceanApronLayer(
|
||||
columns,
|
||||
course,
|
||||
segment,
|
||||
point,
|
||||
neighborX,
|
||||
neighborZ,
|
||||
flowX,
|
||||
flowZ,
|
||||
neighborTerrain,
|
||||
pointFeatures
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyFeatureRef feature(
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
|
||||
/**
|
||||
* Runs a batch of independent planning tasks in parallel without ever parking a pool worker on work
|
||||
* that lives in another pool. On a ForkJoin worker the tasks are forked into that worker's own pool
|
||||
* and joined with work helping, so a saturated pool still finishes them on the joining thread; on any
|
||||
* other thread they run on the fallback executor, or inline when there is none. Results keep the
|
||||
* task order and a task's unchecked exception is rethrown as is.
|
||||
*/
|
||||
public final class HydrologyForkJoin {
|
||||
private HydrologyForkJoin() {
|
||||
}
|
||||
|
||||
public static <T> List<T> invokeAll(List<Callable<T>> tasks, Executor fallback) {
|
||||
Objects.requireNonNull(tasks, "tasks");
|
||||
if (tasks.size() < 2 || fallback == null && !(Thread.currentThread() instanceof ForkJoinWorkerThread)) {
|
||||
return runInline(tasks);
|
||||
}
|
||||
if (Thread.currentThread() instanceof ForkJoinWorkerThread) {
|
||||
return forkJoin(tasks);
|
||||
}
|
||||
return runOn(tasks, fallback);
|
||||
}
|
||||
|
||||
private static <T> List<T> runInline(List<Callable<T>> tasks) {
|
||||
ArrayList<T> results = new ArrayList<>(tasks.size());
|
||||
for (Callable<T> task : tasks) {
|
||||
results.add(call(task));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static <T> List<T> forkJoin(List<Callable<T>> tasks) {
|
||||
ArrayList<ForkJoinTask<T>> forked = new ArrayList<>(tasks.size());
|
||||
for (Callable<T> task : tasks) {
|
||||
forked.add(ForkJoinTask.adapt(task));
|
||||
}
|
||||
ForkJoinTask.invokeAll(forked);
|
||||
ArrayList<T> results = new ArrayList<>(tasks.size());
|
||||
for (ForkJoinTask<T> task : forked) {
|
||||
results.add(task.join());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static <T> List<T> runOn(List<Callable<T>> tasks, Executor executor) {
|
||||
ArrayList<CompletableFuture<T>> futures = new ArrayList<>(tasks.size());
|
||||
for (Callable<T> task : tasks) {
|
||||
futures.add(CompletableFuture.supplyAsync(() -> call(task), executor));
|
||||
}
|
||||
ArrayList<T> results = new ArrayList<>(tasks.size());
|
||||
for (CompletableFuture<T> future : futures) {
|
||||
try {
|
||||
results.add(future.join());
|
||||
} catch (CompletionException failure) {
|
||||
throw unwrap(failure.getCause());
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static <T> T call(Callable<T> task) {
|
||||
try {
|
||||
return task.call();
|
||||
} catch (RuntimeException | Error failure) {
|
||||
throw failure;
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Hydrology planning task failed.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static RuntimeException unwrap(Throwable cause) {
|
||||
if (cause instanceof RuntimeException runtimeException) {
|
||||
return runtimeException;
|
||||
}
|
||||
if (cause instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
return new IllegalStateException("Hydrology planning task failed.", cause);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+175
-19
@@ -11,7 +11,9 @@ public record HydrologyPlannerSettings(
|
||||
Outlets outlets,
|
||||
Geometry geometry,
|
||||
List<DeepFluid> deepFluids,
|
||||
List<SurfacePool> surfacePools
|
||||
List<SurfacePool> surfacePools,
|
||||
double widestShoreBiomeWidth,
|
||||
SeaCaves seaCaves
|
||||
) {
|
||||
private static final long PLAN_FORMAT_REVISION = 6L;
|
||||
private static final int MAXIMUM_CROSS_TILE_COLOR_PERIOD = 4;
|
||||
@@ -26,7 +28,13 @@ public record HydrologyPlannerSettings(
|
||||
}
|
||||
deepFluids = deepFluids == null ? List.of() : List.copyOf(deepFluids);
|
||||
surfacePools = surfacePools == null ? List.of() : List.copyOf(surfacePools);
|
||||
int publicationRadius = publicationRadius(routing, surface, underground, outlets, geometry, deepFluids, surfacePools);
|
||||
if (!Double.isFinite(widestShoreBiomeWidth) || widestShoreBiomeWidth < 0D) {
|
||||
throw new IllegalArgumentException("widestShoreBiomeWidth must be finite and non-negative.");
|
||||
}
|
||||
if (seaCaves == null) {
|
||||
throw new IllegalArgumentException("Sea cave settings are required.");
|
||||
}
|
||||
int publicationRadius = publicationRadius(routing, surface, underground, outlets, geometry, deepFluids, surfacePools, widestShoreBiomeWidth, seaCaves);
|
||||
if (crossTileColorPeriod(publicationRadius, routing.tileSize()) > MAXIMUM_CROSS_TILE_COLOR_PERIOD) {
|
||||
throw new IllegalArgumentException("Hydrology publication envelope exceeds the bounded cross-tile admission period.");
|
||||
}
|
||||
@@ -40,7 +48,7 @@ public record HydrologyPlannerSettings(
|
||||
new Routing(2048, 64, 8192, 8192, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, 0),
|
||||
new Surface(true, surfaceSources, 4, 8, 2, 4, 10, 1.5D, Banks.defaults()),
|
||||
new Hydraulics(8),
|
||||
new Underground(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true),
|
||||
new Underground(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1),
|
||||
new Outlets(
|
||||
true,
|
||||
new Grotto(true, 18, 8, 8, 32768),
|
||||
@@ -49,11 +57,14 @@ public record HydrologyPlannerSettings(
|
||||
12,
|
||||
64,
|
||||
8,
|
||||
12
|
||||
12,
|
||||
2
|
||||
),
|
||||
Geometry.defaults(),
|
||||
List.of(),
|
||||
List.of()
|
||||
List.of(),
|
||||
0D,
|
||||
SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +77,9 @@ public record HydrologyPlannerSettings(
|
||||
hydraulics.hashCode(),
|
||||
underground.hashCode(),
|
||||
outlets.hashCode(),
|
||||
geometry.hashCode()
|
||||
geometry.hashCode(),
|
||||
Double.doubleToLongBits(widestShoreBiomeWidth),
|
||||
seaCaves.hashCode()
|
||||
);
|
||||
for (DeepFluid deepFluid : deepFluids) {
|
||||
hash = HydrologyHash.mix(hash, deepFluid.hashCode(), HydrologyHash.text(deepFluid.id()));
|
||||
@@ -78,7 +91,7 @@ public record HydrologyPlannerSettings(
|
||||
}
|
||||
|
||||
public int publicationRadius() {
|
||||
return publicationRadius(routing, surface, underground, outlets, geometry, deepFluids, surfacePools);
|
||||
return publicationRadius(routing, surface, underground, outlets, geometry, deepFluids, surfacePools, widestShoreBiomeWidth, seaCaves);
|
||||
}
|
||||
|
||||
int crossTileColorPeriod() {
|
||||
@@ -92,8 +105,11 @@ public record HydrologyPlannerSettings(
|
||||
Outlets outlets,
|
||||
Geometry geometry,
|
||||
List<DeepFluid> deepFluids,
|
||||
List<SurfacePool> surfacePools
|
||||
List<SurfacePool> surfacePools,
|
||||
double widestShoreBiomeWidth,
|
||||
SeaCaves seaCaves
|
||||
) {
|
||||
double shoreReach = Math.max(surface.shoreWidth(), widestShoreBiomeWidth);
|
||||
int alignedHalo = Math.floorDiv(
|
||||
Math.min(
|
||||
routing.maximumRouteLength(),
|
||||
@@ -106,13 +122,13 @@ public record HydrologyPlannerSettings(
|
||||
if (surface.enabled() && surface.sources().enabled()) {
|
||||
int blendWidth = surface.banks().maximumBlendWidth();
|
||||
int surfaceRadius = (int) StrictMath.ceil(
|
||||
surface.maximumWidth() / 2D + surface.shoreWidth() + blendWidth
|
||||
surface.maximumWidth() * surface.banks().mouthFlareRatio() / 2D + shoreReach + blendWidth
|
||||
);
|
||||
surfaceRadius = Math.max(
|
||||
surfaceRadius,
|
||||
(int) StrictMath.ceil(geometry.drops().basinWidth(
|
||||
geometry.drops().flowWidth(surface.maximumWidth())
|
||||
) / 2D + surface.shoreWidth() + blendWidth)
|
||||
) / 2D + shoreReach + blendWidth)
|
||||
);
|
||||
surfaceRadius = Math.max(surfaceRadius, outlets.coastalGrotto().horizontalRadius());
|
||||
surfaceRadius = Math.max(surfaceRadius, outlets.inlandGrotto().horizontalRadius());
|
||||
@@ -144,9 +160,16 @@ public record HydrologyPlannerSettings(
|
||||
if (!pool.enabled()) {
|
||||
continue;
|
||||
}
|
||||
radius = Math.max(radius, pool.maximumRadius() + (int) StrictMath.ceil(surface.shoreWidth())
|
||||
radius = Math.max(radius, pool.maximumRadius() + (int) StrictMath.ceil(shoreReach)
|
||||
+ surface.banks().maximumBlendWidth());
|
||||
}
|
||||
if (seaCaves.enabled() && seaCaves.maximumPerTile() > 0) {
|
||||
// A chamber hangs off a shoreline that may lie a halo away, swept inland by its depth.
|
||||
radius = Math.max(radius, Math.addExact(alignedHalo, Math.addExact(
|
||||
outlets.coastalGrotto().horizontalRadius(),
|
||||
Math.addExact(seaCaves.depth(), 1)
|
||||
)));
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
@@ -260,9 +283,12 @@ public record HydrologyPlannerSettings(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code sink} is how many blocks the water surface sits below the lowest natural ground beside
|
||||
* the channel; zero keeps the water flush with the bank and the bank top always meets the water.
|
||||
*/
|
||||
public record Banks(
|
||||
int inset,
|
||||
int freeboard,
|
||||
int sink,
|
||||
double blendSlope,
|
||||
int minimumBlendWidth,
|
||||
int maximumBlendWidth,
|
||||
@@ -271,13 +297,19 @@ public record HydrologyPlannerSettings(
|
||||
int cascadeRun,
|
||||
int waterfallMinimumDrop,
|
||||
double mouthFlareRatio,
|
||||
Inlet inlet,
|
||||
double springWidthRatio,
|
||||
int springLength,
|
||||
boolean exposeCutStrata
|
||||
boolean exposeCutStrata,
|
||||
Erosion erosion,
|
||||
Ponds ponds
|
||||
) {
|
||||
// Structural invariants only; authoring bounds live in the pack validator.
|
||||
public Banks {
|
||||
if (inset < 0 || freeboard < 0
|
||||
if (erosion == null || ponds == null || inlet == null) {
|
||||
throw new IllegalArgumentException("Surface erosion, inlet and pond settings are required.");
|
||||
}
|
||||
if (sink < 0
|
||||
|| !Double.isFinite(blendSlope) || blendSlope <= 0D
|
||||
|| minimumBlendWidth < 1 || maximumBlendWidth < minimumBlendWidth
|
||||
|| !Double.isFinite(roughness) || roughness < 0D || roughness > 1D
|
||||
@@ -292,7 +324,98 @@ public record HydrologyPlannerSettings(
|
||||
}
|
||||
|
||||
public static Banks defaults() {
|
||||
return new Banks(1, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true);
|
||||
return new Banks(0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, Inlet.defaults(), 2.5D, 24, true, Erosion.defaults(), Ponds.defaults());
|
||||
}
|
||||
|
||||
public Banks withInlet(Inlet inlet) {
|
||||
return new Banks(sink, blendSlope, minimumBlendWidth, maximumBlendWidth, roughness, roughnessWavelength,
|
||||
cascadeRun, waterfallMinimumDrop, mouthFlareRatio, inlet, springWidthRatio, springLength,
|
||||
exposeCutStrata, erosion, ponds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The drowned reach where a surface river meets the sea. Over the last {@code length} blocks before
|
||||
* the coast the water is held at sea level, the channel widens toward the mouth flare and its bed
|
||||
* deepens by {@code depth}; the stations above it grade down one block per station into the inlet.
|
||||
* The inlet and its approach may be cut up to {@code maximumIncision} deep instead of the channel
|
||||
* cap, so a coastal rise no longer rejects the course; a rise the cap cannot pass ends the inlet
|
||||
* there, and the inlet never takes more than half the exposed course. A zero length is the plain
|
||||
* crossing: the head drops to sea level at the coast, the channel neither widens nor deepens.
|
||||
*/
|
||||
public record Inlet(
|
||||
int length,
|
||||
int depth,
|
||||
int maximumIncision
|
||||
) {
|
||||
public Inlet {
|
||||
if (length < 0 || length > 1024 || depth < 0 || depth > 64 || maximumIncision < 0 || maximumIncision > 512) {
|
||||
throw new IllegalArgumentException("Surface inlet settings are invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Inlet none() {
|
||||
return new Inlet(0, 0, 0);
|
||||
}
|
||||
|
||||
public static Inlet defaults() {
|
||||
return new Inlet(64, 3, 32);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How the ground around a channel is eroded into a valley. {@code smoothingRadius} is the run of
|
||||
* stations the blend width is averaged over, {@code thalwegFraction} the share of the channel
|
||||
* half-width that stays at full bed depth, {@code blendCurve} the exponent on the blend progress
|
||||
* (below one hollows the valley sides, above one steepens them near the shore), and
|
||||
* {@code bedNoise} the share of the channel roughness applied to the bed. Disabled erosion keeps
|
||||
* only the wet channel, the shore band and the containing lip.
|
||||
*/
|
||||
public record Erosion(
|
||||
boolean enabled,
|
||||
int smoothingRadius,
|
||||
double thalwegFraction,
|
||||
double blendCurve,
|
||||
double bedNoise
|
||||
) {
|
||||
public Erosion {
|
||||
if (smoothingRadius < 0
|
||||
|| !Double.isFinite(thalwegFraction) || thalwegFraction < 0D || thalwegFraction >= 1D
|
||||
|| !Double.isFinite(blendCurve) || blendCurve <= 0D
|
||||
|| !Double.isFinite(bedNoise) || bedNoise < 0D) {
|
||||
throw new IllegalArgumentException("Surface erosion settings are invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Erosion defaults() {
|
||||
return new Erosion(true, 12, 0.45D, 1D, 0.5D);
|
||||
}
|
||||
}
|
||||
|
||||
/** A pond at one end of a surface course: a round bowl holding the course's head at that end. */
|
||||
public record Pond(boolean enabled, int minimumRadius, int maximumRadius, int depth) {
|
||||
public Pond {
|
||||
if (minimumRadius < 1 || maximumRadius < minimumRadius || depth < 1) {
|
||||
throw new IllegalArgumentException("Surface pond settings are invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The ponds at a surface course's source and at an inland terminal. */
|
||||
public record Ponds(Pond source, Pond terminal) {
|
||||
public Ponds {
|
||||
if (source == null || terminal == null) {
|
||||
throw new IllegalArgumentException("Surface pond settings are required.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Ponds defaults() {
|
||||
return new Ponds(new Pond(true, 6, 12, 3), new Pond(true, 4, 7, 3));
|
||||
}
|
||||
|
||||
/** No pond at either end: what a standing pool or a test channel asks for. */
|
||||
public static Ponds none() {
|
||||
return new Ponds(new Pond(false, 1, 1, 1), new Pond(false, 1, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +444,33 @@ public record HydrologyPlannerSettings(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sea caves: coastal grottos that open from the ocean into the coast without a river. A tile keeps at
|
||||
* most {@code maximumPerTile} of them, the steepest owned coast first, at least {@code minimumSpacing}
|
||||
* apart, only where the coast stands {@code minimumCoastHeight} above the sea, each chamber swept
|
||||
* {@code depth} blocks inland from the shoreline. Chamber size and volume come from the coastal grotto.
|
||||
*/
|
||||
public record SeaCaves(
|
||||
boolean enabled,
|
||||
int maximumPerTile,
|
||||
int minimumSpacing,
|
||||
int minimumCoastHeight,
|
||||
int depth
|
||||
) {
|
||||
public SeaCaves {
|
||||
if (maximumPerTile < 0 || maximumPerTile > 64
|
||||
|| minimumSpacing < 16 || minimumSpacing > 8192
|
||||
|| minimumCoastHeight < 1 || minimumCoastHeight > 128
|
||||
|| depth < 0 || depth > 128) {
|
||||
throw new IllegalArgumentException("Sea cave bounds are invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
public static SeaCaves disabled() {
|
||||
return new SeaCaves(false, 0, 16, 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// The routing gates read one hydraulic threshold: the drop that makes a reach a waterfall.
|
||||
public record Hydraulics(int waterfallMinimumDrop) {
|
||||
public Hydraulics {
|
||||
@@ -341,7 +491,8 @@ public record HydrologyPlannerSettings(
|
||||
int maximumDepth,
|
||||
int minimumHeadroom,
|
||||
int maximumHeadroom,
|
||||
boolean connectToExistingCaves
|
||||
boolean connectToExistingCaves,
|
||||
int tributaries
|
||||
) {
|
||||
public Underground {
|
||||
if (sources == null || minimumFluidY > maximumFluidY) {
|
||||
@@ -350,6 +501,9 @@ public record HydrologyPlannerSettings(
|
||||
requireRange(minimumWidth, maximumWidth, 1, 256, "underground width");
|
||||
requireRange(minimumDepth, maximumDepth, 1, 64, "underground depth");
|
||||
requireRange(minimumHeadroom, maximumHeadroom, 1, 128, "underground headroom");
|
||||
if (tributaries < 0 || tributaries > 4) {
|
||||
throw new IllegalArgumentException("underground tributaries must be between 0 and 4.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +515,8 @@ public record HydrologyPlannerSettings(
|
||||
int coastalCliffMinimumHeight,
|
||||
int mouthLevelingDistance,
|
||||
int maximumOceanApron,
|
||||
int maximumPerTile
|
||||
int maximumPerTile,
|
||||
int maximumCoastalPerTile
|
||||
) {
|
||||
public Outlets {
|
||||
if (coastalGrotto == null || inlandGrotto == null) {
|
||||
@@ -371,7 +526,8 @@ public record HydrologyPlannerSettings(
|
||||
throw new IllegalArgumentException("Surface sinkholes require inland grotto outlets.");
|
||||
}
|
||||
if (coastalCliffMinimumHeight < 0 || maximumOceanApron < 0 || maximumOceanApron > 64
|
||||
|| mouthLevelingDistance < 0 || maximumPerTile < 1 || maximumPerTile > 256) {
|
||||
|| mouthLevelingDistance < 0 || maximumPerTile < 1 || maximumPerTile > 256
|
||||
|| maximumCoastalPerTile < 0 || maximumCoastalPerTile > 256) {
|
||||
throw new IllegalArgumentException("Outlet bounds are invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,16 @@ public record HydrologyTerrainSample(
|
||||
String bankBiomeKey,
|
||||
String floodedCaveBiomeKey,
|
||||
List<String> preferredProfileKeys,
|
||||
List<String> surfacePoolKeys
|
||||
List<String> surfacePoolKeys,
|
||||
double shoreBiomeWidth,
|
||||
String confinesKey
|
||||
) {
|
||||
/**
|
||||
* {@code shoreBiomeWidth} is the width in blocks of the shore biome band beside a surface river at this
|
||||
* column; {@code NaN} means the column has no policy value and the geometric shore width applies.
|
||||
* {@code confinesKey} names the region or biome a river passing this column must stay inside, or is
|
||||
* null where rivers may flow anywhere.
|
||||
*/
|
||||
public HydrologyTerrainSample {
|
||||
requireFiniteNonNegative(slope, "slope");
|
||||
requireFiniteNonNegative(routingCost, "routingCost");
|
||||
@@ -53,6 +61,20 @@ public record HydrologyTerrainSample(
|
||||
floodedCaveBiomeKey = normalizeKey(floodedCaveBiomeKey, surfaceBiomeKey);
|
||||
preferredProfileKeys = normalizeProfiles(preferredProfileKeys);
|
||||
surfacePoolKeys = surfacePoolKeys == null ? List.of() : List.copyOf(surfacePoolKeys);
|
||||
if (!Double.isNaN(shoreBiomeWidth) && (!Double.isFinite(shoreBiomeWidth) || shoreBiomeWidth < 0D)) {
|
||||
throw new IllegalArgumentException("shoreBiomeWidth must be NaN, or finite and non-negative.");
|
||||
}
|
||||
confinesKey = confinesKey == null || confinesKey.isBlank() ? null : confinesKey;
|
||||
}
|
||||
|
||||
/** The shore biome band width at this column, or {@code fallback} when no policy set one. */
|
||||
public double shoreBiomeWidth(double fallback) {
|
||||
return Double.isNaN(shoreBiomeWidth) ? fallback : shoreBiomeWidth;
|
||||
}
|
||||
|
||||
/** Whether a river at {@code upstream} may drain into this column without leaving its confines. */
|
||||
public boolean drainsInto(HydrologyTerrainSample downstream) {
|
||||
return confinesKey == null || confinesKey.equals(downstream.confinesKey());
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSample openLand(int naturalHeight, double slope, String parentBiomeKey) {
|
||||
@@ -84,7 +106,9 @@ public record HydrologyTerrainSample(
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
List.of("default"),
|
||||
List.of()
|
||||
List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,7 +141,9 @@ public record HydrologyTerrainSample(
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
List.of("default"),
|
||||
List.of()
|
||||
List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,7 +176,9 @@ public record HydrologyTerrainSample(
|
||||
bankBiomeKey,
|
||||
floodedCaveBiomeKey,
|
||||
preferredProfileKeys,
|
||||
replacementPoolKeys
|
||||
replacementPoolKeys,
|
||||
shoreBiomeWidth,
|
||||
confinesKey
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,7 +211,9 @@ public record HydrologyTerrainSample(
|
||||
bankBiomeKey,
|
||||
floodedCaveBiomeKey,
|
||||
preferredProfileKeys,
|
||||
surfacePoolKeys
|
||||
surfacePoolKeys,
|
||||
shoreBiomeWidth,
|
||||
confinesKey
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
@@ -12,11 +10,11 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public final class HydrologyTileCache implements AutoCloseable {
|
||||
@@ -32,7 +30,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
private final AtomicLong cacheEpoch;
|
||||
private final ThreadLocal<LocalChunkColumns> localChunkColumns;
|
||||
private final Executor prefetchExecutor;
|
||||
private final Set<HydrologyTileKey> prefetching;
|
||||
private final ConcurrentHashMap<HydrologyTileKey, CompletableFuture<HydrologyTile>> planning;
|
||||
|
||||
public HydrologyTileCache(HydrologyPlanner planner) {
|
||||
this(planner, DEFAULT_MAXIMUM_ENTRIES);
|
||||
@@ -49,7 +47,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
public HydrologyTileCache(HydrologyPlanner planner, int maximumEntries, Executor prefetchExecutor) {
|
||||
this.planner = Objects.requireNonNull(planner, "planner");
|
||||
this.prefetchExecutor = prefetchExecutor;
|
||||
this.prefetching = ConcurrentHashMap.newKeySet();
|
||||
this.planning = new ConcurrentHashMap<>();
|
||||
if (maximumEntries < 1) {
|
||||
throw new IllegalArgumentException("maximumEntries must be positive.");
|
||||
}
|
||||
@@ -64,8 +62,34 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
this.localChunkColumns = new ThreadLocal<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* The tile, planned on the prefetch executor when there is one so that a caller that gets
|
||||
* interrupted or cancelled (a map render, a command) never aborts a plan every other caller is
|
||||
* waiting for; without an executor the tile is planned on the caller.
|
||||
*/
|
||||
public HydrologyTile get(HydrologyTileKey key) {
|
||||
Objects.requireNonNull(key, "key");
|
||||
if (prefetchExecutor == null || plansInline()) {
|
||||
return planInline(key);
|
||||
}
|
||||
return awaitTile(planAsync(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* A pool worker plans on its own thread. Parking the workers of one pool on tasks of another is how
|
||||
* two saturated pools deadlock each other (generation workers waiting for planning that waits for
|
||||
* generation workers), and a ForkJoin worker keeps the planner's nested fan-out alive by helping
|
||||
* with its own forks instead of waiting for them.
|
||||
*/
|
||||
private static boolean plansInline() {
|
||||
return Thread.currentThread() instanceof ForkJoinWorkerThread;
|
||||
}
|
||||
|
||||
private HydrologyTile planInline(HydrologyTileKey key) {
|
||||
if (tiles.getIfPresent(key) == null && planning.containsKey(key)) {
|
||||
IrisLogging.debug("Hydrology tile %d,%d awaited by %s while the planning pool plans it",
|
||||
key.tileX(), key.tileZ(), Thread.currentThread().getName());
|
||||
}
|
||||
return tiles.get(key, this::planOrEmpty);
|
||||
}
|
||||
|
||||
@@ -73,12 +97,16 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
* A tile whose planning throws is published without rivers instead of failing the chunks that
|
||||
* asked for it: one bad column must not take the chunk system down. The failure is logged in
|
||||
* full so the cause can be traced, and the empty tile is cached like any other so the session
|
||||
* stays consistent.
|
||||
* stays consistent. A plan that was interrupted is not a bad tile: it is rethrown without being
|
||||
* cached so the next request plans it again.
|
||||
*/
|
||||
private HydrologyTile planOrEmpty(HydrologyTileKey key) {
|
||||
try {
|
||||
return planner.plan(key);
|
||||
} catch (RuntimeException failure) {
|
||||
if (interrupted(failure)) {
|
||||
throw failure;
|
||||
}
|
||||
IrisLogging.error("Hydrology tile " + key.tileX() + "," + key.tileZ()
|
||||
+ " failed to plan and generates without rivers: " + failure.getMessage());
|
||||
IrisLogging.reportError(failure);
|
||||
@@ -86,6 +114,21 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean interrupted(Throwable failure) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
return true;
|
||||
}
|
||||
for (Throwable cause = failure; cause != null; cause = cause.getCause()) {
|
||||
if (cause instanceof InterruptedException || cause instanceof java.io.InterruptedIOException) {
|
||||
return true;
|
||||
}
|
||||
if (cause.getCause() == cause) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
|
||||
return chunkColumns(blockX, blockZ).columnAt(blockX, blockZ);
|
||||
}
|
||||
@@ -94,6 +137,31 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
chunkColumns(blockX, blockZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether every tile the column's chunk composes from is already planned. A caller that must not
|
||||
* wait (the server thread answering a height or biome query) uses this before sampling: when the
|
||||
* answer is false the missing tiles are handed to the prefetch executor so a later query finds
|
||||
* them, and nothing is planned on the caller.
|
||||
*/
|
||||
public boolean isPlanned(int blockX, int blockZ) {
|
||||
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
|
||||
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
|
||||
if (composedChunks.getIfPresent(CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ))) != null) {
|
||||
return true;
|
||||
}
|
||||
boolean planned = true;
|
||||
for (HydrologyTileKey key : relevantKeys(chunkX, chunkZ)) {
|
||||
if (tiles.getIfPresent(key) != null) {
|
||||
continue;
|
||||
}
|
||||
planned = false;
|
||||
if (prefetchExecutor != null) {
|
||||
prefetch(key);
|
||||
}
|
||||
}
|
||||
return planned;
|
||||
}
|
||||
|
||||
public HydrologyRenderSample renderAt(int blockX, int blockZ) {
|
||||
return columnAt(blockX, blockZ)
|
||||
.map(HydrologyColumnSample::renderSample)
|
||||
@@ -146,7 +214,8 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
return columns;
|
||||
}
|
||||
|
||||
private ChunkColumns composeChunkColumns(int chunkX, int chunkZ) {
|
||||
/** The tiles a chunk's columns compose from: every tile within the publication radius of the chunk. */
|
||||
private ArrayList<HydrologyTileKey> relevantKeys(int chunkX, int chunkZ) {
|
||||
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
|
||||
int minimumBlockZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
|
||||
int tileSize = planner.settings().routing().tileSize();
|
||||
@@ -163,7 +232,18 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
relevantKeys.add(new HydrologyTileKey(tileX, tileZ));
|
||||
}
|
||||
}
|
||||
List<HydrologyTile> relevantTiles = loadTiles(relevantKeys);
|
||||
return relevantKeys;
|
||||
}
|
||||
|
||||
private ChunkColumns composeChunkColumns(int chunkX, int chunkZ) {
|
||||
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
|
||||
int minimumBlockZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
|
||||
ArrayList<HydrologyTileKey> relevantKeys = relevantKeys(chunkX, chunkZ);
|
||||
int minimumTileX = relevantKeys.getFirst().tileX();
|
||||
int maximumTileX = relevantKeys.getLast().tileX();
|
||||
int minimumTileZ = relevantKeys.getFirst().tileZ();
|
||||
int maximumTileZ = relevantKeys.getLast().tileZ();
|
||||
List<HydrologyTile> relevantTiles = tiles(relevantKeys);
|
||||
prefetchNeighbours(minimumTileX, maximumTileX, minimumTileZ, maximumTileZ);
|
||||
HydrologyColumnSample[] columns = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
|
||||
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
|
||||
@@ -234,60 +314,69 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
}
|
||||
|
||||
private void prefetch(HydrologyTileKey key) {
|
||||
if (tiles.getIfPresent(key) != null || !prefetching.add(key)) {
|
||||
return;
|
||||
planAsync(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plans every tile that is not cached yet on the prefetch executor and returns all of them in key
|
||||
* order. At most half the cache bound is in flight at once so a wide request cannot evict tiles
|
||||
* before the caller has read them; a tile another caller is already planning is joined rather than
|
||||
* planned twice. Without a prefetch executor the tiles are planned inline on the caller.
|
||||
*/
|
||||
public List<HydrologyTile> tiles(List<HydrologyTileKey> keys) {
|
||||
Objects.requireNonNull(keys, "keys");
|
||||
HydrologyTile[] loaded = new HydrologyTile[keys.size()];
|
||||
if (prefetchExecutor == null || plansInline()) {
|
||||
for (int index = 0; index < loaded.length; index++) {
|
||||
loaded[index] = planInline(keys.get(index));
|
||||
}
|
||||
return List.of(loaded);
|
||||
}
|
||||
int batch = Math.max(1, maximumEntries / 2);
|
||||
ArrayList<CompletableFuture<HydrologyTile>> futures = new ArrayList<>(Math.min(batch, loaded.length));
|
||||
for (int start = 0; start < loaded.length; start += batch) {
|
||||
int end = Math.min(loaded.length, start + batch);
|
||||
futures.clear();
|
||||
for (int index = start; index < end; index++) {
|
||||
futures.add(planAsync(keys.get(index)));
|
||||
}
|
||||
for (int index = start; index < end; index++) {
|
||||
loaded[index] = awaitTile(futures.get(index - start));
|
||||
}
|
||||
}
|
||||
return List.of(loaded);
|
||||
}
|
||||
|
||||
/**
|
||||
* The tile, planned on the prefetch executor unless it is cached or already being planned, in which
|
||||
* case the caller shares that plan instead of occupying a second executor thread with it.
|
||||
*/
|
||||
private CompletableFuture<HydrologyTile> planAsync(HydrologyTileKey key) {
|
||||
HydrologyTile present = tiles.getIfPresent(key);
|
||||
if (present != null) {
|
||||
return CompletableFuture.completedFuture(present);
|
||||
}
|
||||
CompletableFuture<HydrologyTile> future = new CompletableFuture<>();
|
||||
CompletableFuture<HydrologyTile> existing = planning.putIfAbsent(key, future);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
try {
|
||||
prefetchExecutor.execute(() -> {
|
||||
try {
|
||||
get(key);
|
||||
} catch (RuntimeException failure) {
|
||||
future.complete(planInline(key));
|
||||
} catch (Throwable failure) {
|
||||
future.completeExceptionally(failure);
|
||||
IrisLogging.reportError(failure);
|
||||
} finally {
|
||||
prefetching.remove(key);
|
||||
planning.remove(key, future);
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException rejected) {
|
||||
prefetching.remove(key);
|
||||
planning.remove(key, future);
|
||||
future.completeExceptionally(rejected);
|
||||
}
|
||||
}
|
||||
|
||||
private List<HydrologyTile> loadTiles(List<HydrologyTileKey> keys) {
|
||||
if (keys.size() < 2
|
||||
|| !IrisPlatforms.isBound()) {
|
||||
ArrayList<HydrologyTile> loaded = new ArrayList<>(keys.size());
|
||||
for (HydrologyTileKey key : keys) {
|
||||
loaded.add(get(key));
|
||||
}
|
||||
return List.copyOf(loaded);
|
||||
}
|
||||
// Tiles already planned are answered in place; only a tile that still needs planning is
|
||||
// worth a round trip through the pool, and most chunks find every tile they need cached.
|
||||
HydrologyTile[] loaded = new HydrologyTile[keys.size()];
|
||||
ArrayList<CompletableFuture<HydrologyTile>> futures = null;
|
||||
for (int index = 0; index < loaded.length; index++) {
|
||||
HydrologyTileKey key = keys.get(index);
|
||||
HydrologyTile present = tiles.getIfPresent(key);
|
||||
if (present != null) {
|
||||
loaded[index] = present;
|
||||
continue;
|
||||
}
|
||||
if (futures == null) {
|
||||
futures = new ArrayList<>(loaded.length);
|
||||
}
|
||||
int slot = index;
|
||||
CompletableFuture<HydrologyTile> future = CompletableFuture.supplyAsync(() -> get(key), MultiBurst.hydrology);
|
||||
futures.add(future.thenApply(tile -> {
|
||||
loaded[slot] = tile;
|
||||
return tile;
|
||||
}));
|
||||
}
|
||||
if (futures != null) {
|
||||
for (CompletableFuture<HydrologyTile> future : futures) {
|
||||
awaitTile(future);
|
||||
}
|
||||
}
|
||||
return List.of(loaded);
|
||||
return future;
|
||||
}
|
||||
|
||||
private static HydrologyTile awaitTile(CompletableFuture<HydrologyTile> future) {
|
||||
|
||||
@@ -38,9 +38,11 @@ public record RiverCourse(
|
||||
}
|
||||
}
|
||||
validateSinkholeContinuation(type, segments);
|
||||
boolean independent = type == RiverCourseType.DEEP_FLUID || type == RiverCourseType.SURFACE_POOL;
|
||||
boolean independent = type == RiverCourseType.DEEP_FLUID
|
||||
|| type == RiverCourseType.SURFACE_POOL
|
||||
|| type == RiverCourseType.SEA_CAVE;
|
||||
if (independent && (sourceNodeId.isPresent() || outletId.isPresent())) {
|
||||
throw new IllegalArgumentException("Independent deep-fluid and pool courses cannot reference drainage nodes or river outlets.");
|
||||
throw new IllegalArgumentException("Independent deep-fluid, pool and sea-cave courses cannot reference drainage nodes or river outlets.");
|
||||
}
|
||||
if (!independent && (sourceNodeId.isEmpty() || outletId.isEmpty())) {
|
||||
throw new IllegalArgumentException("Surface and underground courses require a source and outlet.");
|
||||
|
||||
@@ -4,5 +4,7 @@ public enum RiverCourseType {
|
||||
SURFACE,
|
||||
UNDERGROUND,
|
||||
DEEP_FLUID,
|
||||
SURFACE_POOL
|
||||
SURFACE_POOL,
|
||||
// A coastal grotto the sea opens into on its own: no drainage source, no river outlet.
|
||||
SEA_CAVE
|
||||
}
|
||||
|
||||
+7
-1
@@ -21,11 +21,17 @@ public record EffectiveRiverPolicy(
|
||||
double depthMultiplier,
|
||||
double incisionMultiplier,
|
||||
double routingMultiplier,
|
||||
double bankMultiplier
|
||||
double bankMultiplier,
|
||||
Double shoreBiomeWidth,
|
||||
RiverConfinement confinement
|
||||
) {
|
||||
public EffectiveRiverPolicy {
|
||||
placement = Objects.requireNonNull(placement);
|
||||
routing = Objects.requireNonNull(routing);
|
||||
confinement = Objects.requireNonNull(confinement);
|
||||
if (shoreBiomeWidth != null && (!Double.isFinite(shoreBiomeWidth) || shoreBiomeWidth < 0D)) {
|
||||
throw new IllegalArgumentException("shoreBiomeWidth must be finite and non-negative.");
|
||||
}
|
||||
profiles = List.copyOf(profiles);
|
||||
surfaceBiomes = List.copyOf(surfaceBiomes);
|
||||
mouthBiomes = List.copyOf(mouthBiomes);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package art.arcane.iris.engine.hydrology.policy;
|
||||
|
||||
/** Which area a confined river policy binds courses to: the region or the biome that set it, or none. */
|
||||
public enum RiverConfinement {
|
||||
NONE,
|
||||
REGION,
|
||||
BIOME
|
||||
}
|
||||
+16
-5
@@ -27,9 +27,9 @@ public final class RiverPolicyResolver {
|
||||
IrisRiverPolicy biomePolicy
|
||||
) {
|
||||
State state = new State();
|
||||
state.apply(dimensionPolicy);
|
||||
state.apply(regionPolicy);
|
||||
state.apply(biomePolicy);
|
||||
state.apply(dimensionPolicy, RiverConfinement.REGION);
|
||||
state.apply(regionPolicy, RiverConfinement.REGION);
|
||||
state.apply(biomePolicy, RiverConfinement.BIOME);
|
||||
return state.build();
|
||||
}
|
||||
|
||||
@@ -49,11 +49,20 @@ public final class RiverPolicyResolver {
|
||||
private double incisionMultiplier = 1D;
|
||||
private double routingMultiplier = 1D;
|
||||
private double bankMultiplier = 1D;
|
||||
private Double shoreBiomeWidth = null;
|
||||
private RiverConfinement confinement = RiverConfinement.NONE;
|
||||
|
||||
private void apply(IrisRiverPolicy policy) {
|
||||
/** {@code scope} is the confinement a {@code confined: true} at this level binds courses to. */
|
||||
private void apply(IrisRiverPolicy policy, RiverConfinement scope) {
|
||||
if (policy == null) {
|
||||
return;
|
||||
}
|
||||
if (policy.getShoreBiomeWidth() != null) {
|
||||
shoreBiomeWidth = policy.getShoreBiomeWidth();
|
||||
}
|
||||
if (policy.getConfined() != null) {
|
||||
confinement = policy.getConfined() ? scope : RiverConfinement.NONE;
|
||||
}
|
||||
if (policy.getPlacement() != null) {
|
||||
placement = policy.getPlacement();
|
||||
}
|
||||
@@ -117,7 +126,9 @@ public final class RiverPolicyResolver {
|
||||
depthMultiplier,
|
||||
incisionMultiplier,
|
||||
routingMultiplier,
|
||||
bankMultiplier
|
||||
bankMultiplier,
|
||||
shoreBiomeWidth,
|
||||
confinement
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-31
@@ -4,6 +4,7 @@ import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyNaturalTerrainSampler;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyRoutingTerrainSampler;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyForkJoin;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import it.unimi.dsi.fastutil.longs.Long2DoubleLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
@@ -11,8 +12,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrainSampler, AutoCloseable {
|
||||
@@ -214,27 +214,19 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
return;
|
||||
}
|
||||
int rowsPerWorker = Math.ceilDiv(planeWidth, workerCount);
|
||||
ArrayList<CompletableFuture<Void>> futures = new ArrayList<>(workerCount);
|
||||
ArrayList<Callable<Void>> rows = new ArrayList<>(workerCount);
|
||||
for (int workerIndex = 0; workerIndex < workerCount; workerIndex++) {
|
||||
int minimumRow = workerIndex * rowsPerWorker;
|
||||
if (minimumRow >= planeWidth) {
|
||||
break;
|
||||
}
|
||||
int maximumRow = Math.min(planeWidth, minimumRow + rowsPerWorker);
|
||||
futures.add(CompletableFuture.runAsync(
|
||||
() -> fillBasisRows(
|
||||
plane,
|
||||
planeWidth,
|
||||
minimumX,
|
||||
minimumZ,
|
||||
spacing,
|
||||
minimumRow,
|
||||
maximumRow
|
||||
),
|
||||
samplingOptions.executor()
|
||||
));
|
||||
rows.add(() -> {
|
||||
fillBasisRows(plane, planeWidth, minimumX, minimumZ, spacing, minimumRow, maximumRow);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
await(futures);
|
||||
HydrologyForkJoin.invokeAll(rows, samplingOptions.executor());
|
||||
}
|
||||
|
||||
private void fillBasisRows(
|
||||
@@ -264,21 +256,6 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
return Math.min(planeWidth, samplingOptions.maximumWorkers());
|
||||
}
|
||||
|
||||
private static void await(List<CompletableFuture<Void>> futures) {
|
||||
CompletableFuture<?>[] completions = futures.toArray(new CompletableFuture<?>[futures.size()]);
|
||||
try {
|
||||
CompletableFuture.allOf(completions).join();
|
||||
} catch (CompletionException failure) {
|
||||
Throwable cause = failure.getCause();
|
||||
if (cause instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (cause instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException("Hydrology routing terrain sampling failed.", cause);
|
||||
}
|
||||
}
|
||||
|
||||
private void evictOldest(Long2ObjectLinkedOpenHashMap<?> cache) {
|
||||
if (cache.size() <= maximumEntries) {
|
||||
|
||||
+125
-18
@@ -18,37 +18,43 @@ import art.arcane.iris.engine.hydrology.HydrologyTileKey;
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.engine.hydrology.policy.EffectiveRiverPolicy;
|
||||
import art.arcane.iris.engine.hydrology.policy.RiverConfinement;
|
||||
import art.arcane.iris.engine.hydrology.policy.RiverPolicyResolver;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisCoastalRiverGrottoConfig;
|
||||
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
|
||||
import art.arcane.iris.engine.object.IrisSurfacePoolConfig;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisHydrology;
|
||||
import art.arcane.iris.engine.object.IrisInlandRiverGrottoConfig;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisRiverGrottoConfig;
|
||||
import art.arcane.iris.engine.object.IrisRiverChannelShapeConfig;
|
||||
import art.arcane.iris.engine.object.IrisRiverDropShapeConfig;
|
||||
import art.arcane.iris.engine.object.IrisRiverGeometryConfig;
|
||||
import art.arcane.iris.engine.object.IrisRiverGrottoConfig;
|
||||
import art.arcane.iris.engine.object.IrisRiverHydrology;
|
||||
import art.arcane.iris.engine.object.IrisRiverInlandOutlet;
|
||||
import art.arcane.iris.engine.object.IrisRiverMeanderConfig;
|
||||
import art.arcane.iris.engine.object.IrisRiverPolicy;
|
||||
import art.arcane.iris.engine.object.IrisRiverProfile;
|
||||
import art.arcane.iris.engine.object.IrisRiverRoutingConfig;
|
||||
import art.arcane.iris.engine.object.IrisRiverRoutingMode;
|
||||
import art.arcane.iris.engine.object.IrisSeaCaveConfig;
|
||||
import art.arcane.iris.engine.object.IrisStyledRange;
|
||||
import art.arcane.iris.engine.object.IrisSurfacePoolConfig;
|
||||
import art.arcane.iris.engine.object.IrisSurfaceRiverBankConfig;
|
||||
import art.arcane.iris.engine.object.IrisSurfaceRiverChannelConfig;
|
||||
import art.arcane.iris.engine.object.IrisSurfaceRiverFlowConfig;
|
||||
import art.arcane.iris.engine.object.IrisSurfaceRiverConfig;
|
||||
import art.arcane.iris.engine.object.IrisSurfaceRiverFlowConfig;
|
||||
import art.arcane.iris.engine.object.IrisSurfaceRiverPondConfig;
|
||||
import art.arcane.iris.engine.object.IrisSurfaceRiverSourceConfig;
|
||||
import art.arcane.iris.engine.object.IrisUndergroundRiverConfig;
|
||||
import art.arcane.iris.engine.object.IrisUndergroundRiverSourceConfig;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -60,6 +66,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
@@ -84,6 +91,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
private final Set<String> profileKeys;
|
||||
private final String defaultProfileKey;
|
||||
private final Object terrainSampleLock;
|
||||
private final Set<HydrologyTileKey> unplannedQueries = ConcurrentHashMap.newKeySet();
|
||||
private final LinkedHashMap<Long, HydrologyTerrainSample> terrainSamples;
|
||||
|
||||
public IrisHydrologyRuntime(IrisHydrologyRuntimeContext context) {
|
||||
@@ -94,7 +102,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
this.defaultProfileKey = profileKeys.isEmpty() ? "default" : profileKeys.iterator().next();
|
||||
this.terrainSampleLock = new Object();
|
||||
this.terrainSamples = new LinkedHashMap<>(MAXIMUM_TERRAIN_SAMPLES, 0.75F, true);
|
||||
this.settings = createSettings(context.dimension(), hydrology);
|
||||
this.settings = createSettings(context.dimension(), hydrology, context::data);
|
||||
HydrologyGeometrySampler geometrySampler = geometrySampler(context, hydrology);
|
||||
IrisHydrologyRoutingTerrainSampler.Sources terrainSources = new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
this::createTerrainBasis,
|
||||
@@ -115,6 +123,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
context.caveViewFactory()
|
||||
);
|
||||
this.cache = new HydrologyTileCache(planner, MAXIMUM_CACHE_TILES, IrisPlatforms.isBound() ? MultiBurst.hydrology : null);
|
||||
IrisLogging.debug("Hydrology runtime: tileSize=%d publicationRadius=%d planningThreads=%d",
|
||||
settings.routing().tileSize(), settings.publicationRadius(), MultiBurst.hydrology.parallelism());
|
||||
}
|
||||
|
||||
public HydrologyPlannerSettings settings() {
|
||||
@@ -125,6 +135,25 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the column can be sampled without planning: true when its tiles are cached. Otherwise the
|
||||
* planning pool is asked for them and the caller should answer from natural terrain for now.
|
||||
*/
|
||||
public boolean isPlanned(double x, double z) {
|
||||
int blockX = (int) StrictMath.floor(x);
|
||||
int blockZ = (int) StrictMath.floor(z);
|
||||
boolean planned = cache.isPlanned(blockX, blockZ);
|
||||
if (!planned) {
|
||||
int tileSize = settings.routing().tileSize();
|
||||
HydrologyTileKey key = new HydrologyTileKey(Math.floorDiv(blockX, tileSize), Math.floorDiv(blockZ, tileSize));
|
||||
if (unplannedQueries.add(key)) {
|
||||
IrisLogging.debug("Hydrology tile %d,%d queried before it was planned; natural terrain answers until the plan lands",
|
||||
key.tileX(), key.tileZ());
|
||||
}
|
||||
}
|
||||
return planned;
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnSample> sample(double x, double z) {
|
||||
int blockX = (int) StrictMath.floor(x);
|
||||
int blockZ = (int) StrictMath.floor(z);
|
||||
@@ -300,8 +329,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks tiles outward from the origin, planning each on demand, and stops as soon as no unvisited
|
||||
* ring can hold a nearer feature. {@code progress} receives the running tile count after every ring.
|
||||
* Walks tiles outward from the origin, planning each ring's tiles together on the hydrology pool, and
|
||||
* stops as soon as no unvisited ring can hold a nearer feature. {@code progress} receives the running
|
||||
* tile count after every ring.
|
||||
*/
|
||||
public Optional<HydrologyFeatureRef> nearestFeature(
|
||||
Set<HydrologyFeatureType> types,
|
||||
@@ -334,8 +364,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
if (nearest != null && lowerBound * lowerBound > nearestDistanceSquared) {
|
||||
break;
|
||||
}
|
||||
for (HydrologyTileKey key : HydrologyFeatureSearch.ring(originTileX, originTileZ, ring)) {
|
||||
HydrologyTile tile = cache.get(key);
|
||||
for (HydrologyTile tile : cache.tiles(HydrologyFeatureSearch.ring(originTileX, originTileZ, ring))) {
|
||||
visited++;
|
||||
HydrologyFeatureRef feature = tile.nearestFeature(
|
||||
types, profileKey, x, z, maximumDistance).orElse(null);
|
||||
@@ -455,6 +484,12 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
? sampledNaturalHeight
|
||||
: rawNaturalHeight;
|
||||
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.
|
||||
double resampledNaturalHeight = context.naturalHeightProvider().sample(x, z);
|
||||
if (Double.isFinite(resampledNaturalHeight)) {
|
||||
return createTerrainBasis(x, z, resampledNaturalHeight);
|
||||
}
|
||||
throw new IllegalStateException(nonFiniteHeightMessage(
|
||||
x,
|
||||
z,
|
||||
@@ -527,11 +562,22 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
selectKey(policy.bankBiomes(), parentBiomeKey, biomePatchNoise, 4),
|
||||
selectKey(policy.floodedCaveBiomes(), parentBiomeKey, biomePatchNoise, 5),
|
||||
profiles,
|
||||
policy.surfacePools()
|
||||
policy.surfacePools(),
|
||||
policy.shoreBiomeWidth() == null ? Double.NaN : policy.shoreBiomeWidth(),
|
||||
confinesKey(policy.confinement(), region, biome)
|
||||
);
|
||||
return new IrisHydrologyRoutingTerrainSampler.TerrainBasis(resolvedNaturalHeight, terrain);
|
||||
}
|
||||
|
||||
/** The area a river at this column must stay inside, named so that neighbouring areas never match. */
|
||||
static String confinesKey(RiverConfinement confinement, IrisRegion region, IrisBiome biome) {
|
||||
return switch (confinement) {
|
||||
case NONE -> null;
|
||||
case REGION -> region == null ? null : "region:" + region.getLoadKey();
|
||||
case BIOME -> biome == null ? null : "biome:" + biome.getLoadKey();
|
||||
};
|
||||
}
|
||||
|
||||
private List<String> validProfiles(List<String> requested) {
|
||||
if (requested == null || requested.isEmpty()) {
|
||||
return List.of(defaultProfileKey);
|
||||
@@ -685,7 +731,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
|
||||
static HydrologyPlannerSettings createSettings(
|
||||
IrisDimension dimension,
|
||||
IrisHydrology hydrology
|
||||
IrisHydrology hydrology,
|
||||
DataProvider data
|
||||
) {
|
||||
IrisRiverHydrology rivers = hydrology.getRivers();
|
||||
IrisRiverRoutingConfig routing = rivers.getRouting();
|
||||
@@ -733,8 +780,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
channel.getMaximumIncision(),
|
||||
banks.getShoreWidth(),
|
||||
new HydrologyPlannerSettings.Banks(
|
||||
channel.getInset(),
|
||||
banks.getFreeboard(),
|
||||
channel.getSink(),
|
||||
banks.getBlendSlope(),
|
||||
banks.getMinimumBlendWidth(),
|
||||
banks.getMaximumBlendWidth(),
|
||||
@@ -743,9 +789,25 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
flow.getCascadeRun(),
|
||||
flow.getWaterfallMinimumDrop(),
|
||||
surface.getMouths().getFlareRatio(),
|
||||
new HydrologyPlannerSettings.Inlet(
|
||||
surface.getMouths().getInletLength(),
|
||||
surface.getMouths().getInletDepth(),
|
||||
surface.getMouths().getMaximumIncision()
|
||||
),
|
||||
channel.getSpringWidthRatio(),
|
||||
channel.getSpringLength(),
|
||||
banks.isExposeCutStrata()
|
||||
banks.isExposeCutStrata(),
|
||||
new HydrologyPlannerSettings.Erosion(
|
||||
surface.getErosion().isEnabled(),
|
||||
surface.getErosion().getSmoothingRadius(),
|
||||
surface.getErosion().getThalwegFraction(),
|
||||
surface.getErosion().getBlendCurve(),
|
||||
surface.getErosion().getBedNoise()
|
||||
),
|
||||
new HydrologyPlannerSettings.Ponds(
|
||||
pond(surface.getPonds().getSource()),
|
||||
pond(surface.getPonds().getTerminal())
|
||||
)
|
||||
)
|
||||
);
|
||||
HydrologyPlannerSettings.Hydraulics plannerHydraulics =
|
||||
@@ -769,7 +831,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
maximumInt(underground.getDepth()),
|
||||
minimumInt(underground.getHeadroom()),
|
||||
maximumInt(underground.getHeadroom()),
|
||||
underground.isConnectToExistingCaves()
|
||||
underground.isConnectToExistingCaves(),
|
||||
underground.getTributaries()
|
||||
);
|
||||
IrisRiverGrottoConfig grottos = rivers.getGrottos();
|
||||
IrisCoastalRiverGrottoConfig coastal = grottos.getCoastal();
|
||||
@@ -786,7 +849,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
Math.max(4, coastal.getVerticalRadius()),
|
||||
underground.getMouthLevelingDistance(),
|
||||
surface.getMouths().getMaximumOceanApron(),
|
||||
routing.getMaximumOutletsPerTile()
|
||||
routing.getMaximumOutletsPerTile(),
|
||||
routing.getMaximumCoastalOutletsPerTile()
|
||||
);
|
||||
HydrologyPlannerSettings.Geometry plannerGeometry = geometry(
|
||||
rivers.getGeometry(),
|
||||
@@ -806,10 +870,32 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
plannerOutlets,
|
||||
plannerGeometry,
|
||||
deepFluids(hydrology.getDeepFluids(), minimumWorldY, routing, plannerRouting.refinementSpacing()),
|
||||
surfacePools(hydrology.getSurfacePools())
|
||||
surfacePools(hydrology.getSurfacePools()),
|
||||
widestShoreBiomeWidth(dimension, data, banks.getShoreWidth()),
|
||||
seaCaves(coastal)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The widest shore biome band any policy of the dimension asks for, never below the geometric shore
|
||||
* width, so the publication envelope reaches every column a river can give shore content to.
|
||||
*/
|
||||
static double widestShoreBiomeWidth(IrisDimension dimension, DataProvider data, double shoreWidth) {
|
||||
double widest = Math.max(0D, shoreWidth);
|
||||
widest = Math.max(widest, policyShoreBiomeWidth(dimension.getRiverPolicy()));
|
||||
for (IrisRegion region : dimension.getAllRegions(data)) {
|
||||
widest = Math.max(widest, policyShoreBiomeWidth(region.getRiverPolicy()));
|
||||
}
|
||||
for (IrisBiome biome : dimension.getAllBiomes(data)) {
|
||||
widest = Math.max(widest, policyShoreBiomeWidth(biome.getRiverPolicy()));
|
||||
}
|
||||
return widest;
|
||||
}
|
||||
|
||||
private static double policyShoreBiomeWidth(IrisRiverPolicy policy) {
|
||||
return policy == null || policy.getShoreBiomeWidth() == null ? 0D : policy.getShoreBiomeWidth();
|
||||
}
|
||||
|
||||
private static List<HydrologyPlannerSettings.SurfacePool> surfacePools(List<IrisSurfacePoolConfig> configurations) {
|
||||
ArrayList<HydrologyPlannerSettings.SurfacePool> pools = new ArrayList<>();
|
||||
for (IrisSurfacePoolConfig configuration : configurations) {
|
||||
@@ -884,6 +970,18 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
);
|
||||
}
|
||||
|
||||
// A sea cave is a coastal grotto without a river, so it needs the coastal grotto itself enabled.
|
||||
private static HydrologyPlannerSettings.SeaCaves seaCaves(IrisCoastalRiverGrottoConfig coastal) {
|
||||
IrisSeaCaveConfig seaCaves = coastal.getSeaCaves();
|
||||
return new HydrologyPlannerSettings.SeaCaves(
|
||||
coastal.isEnabled() && seaCaves.isEnabled(),
|
||||
seaCaves.getMaximumPerTile(),
|
||||
seaCaves.getMinimumSpacing(),
|
||||
seaCaves.getMinimumCoastHeight(),
|
||||
seaCaves.getDepth()
|
||||
);
|
||||
}
|
||||
|
||||
private static List<HydrologyPlannerSettings.DeepFluid> deepFluids(
|
||||
List<IrisDeepFluidConfig> configurations,
|
||||
int minimumWorldY,
|
||||
@@ -966,6 +1064,15 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
return (int) Math.min(1_048_576L, Math.max(64L, volume));
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings.Pond pond(IrisSurfaceRiverPondConfig config) {
|
||||
return new HydrologyPlannerSettings.Pond(
|
||||
config.isEnabled(),
|
||||
config.getMinimumRadius(),
|
||||
config.getMaximumRadius(),
|
||||
config.getDepth()
|
||||
);
|
||||
}
|
||||
|
||||
private static int minimumInt(IrisStyledRange range) {
|
||||
return (int) StrictMath.floor(Math.min(range.getMin(), range.getMax()));
|
||||
}
|
||||
|
||||
+14
-6
@@ -9,7 +9,6 @@ import java.util.Objects;
|
||||
|
||||
public final class ChannelProfileBuilder {
|
||||
private static final int SMOOTHING_RADIUS = 16;
|
||||
private static final int MOUTH_FLARE = 32;
|
||||
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
@@ -34,6 +33,7 @@ public final class ChannelProfileBuilder {
|
||||
double[] width = new double[count];
|
||||
double[] depth = new double[count];
|
||||
double[] bank = new double[count];
|
||||
int coast = count;
|
||||
for (int station = 0; station < count; station++) {
|
||||
int stationX = centerline.x()[station];
|
||||
int stationZ = centerline.z()[station];
|
||||
@@ -56,6 +56,9 @@ public final class ChannelProfileBuilder {
|
||||
surface.maximumDepth()
|
||||
);
|
||||
HydrologyTerrainSample terrain = sampler.sample(stationX, stationZ);
|
||||
if (coast == count && (terrain == null || terrain.ocean())) {
|
||||
coast = station;
|
||||
}
|
||||
double widthMultiplier = terrain == null ? 1D : terrain.widthMultiplier();
|
||||
double depthMultiplier = terrain == null ? 1D : terrain.depthMultiplier();
|
||||
bank[station] = terrain == null ? 1D : terrain.bankMultiplier();
|
||||
@@ -74,12 +77,17 @@ public final class ChannelProfileBuilder {
|
||||
smoothWidth[station] *= 1D + (localRatio - 1D) * remaining;
|
||||
smoothDepth[station] += remaining;
|
||||
}
|
||||
if (directOcean) {
|
||||
int flare = Math.min(MOUTH_FLARE, count / 3);
|
||||
int flareStart = count - flare;
|
||||
for (int station = Math.max(0, flareStart); station < count; station++) {
|
||||
double progress = SurfaceNoise.smoothStep((station - flareStart) / (double) Math.max(1, flare - 1));
|
||||
// The inlet: over its length before the coast the channel widens toward the mouth flare and
|
||||
// its bed deepens by the inlet depth, so the estuary is wider and deeper than the river. The
|
||||
// flare completes at the shoreline, the first station in the sea, and the stations past it keep it.
|
||||
HydrologyPlannerSettings.Inlet inlet = surface.banks().inlet();
|
||||
if (directOcean && inlet.length() > 0 && coast > 1) {
|
||||
int flare = Math.min(inlet.length(), coast - 1);
|
||||
int flareStart = coast - flare;
|
||||
for (int station = flareStart; station < count; station++) {
|
||||
double progress = SurfaceNoise.smoothStep(Math.min(1D, (station - flareStart) / (double) Math.max(1, flare - 1)));
|
||||
smoothWidth[station] *= 1D + (surface.banks().mouthFlareRatio() - 1D) * progress;
|
||||
smoothDepth[station] += inlet.depth() * progress;
|
||||
}
|
||||
}
|
||||
double[] halfWidth = new double[count];
|
||||
|
||||
+213
-24
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyHash;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
|
||||
@@ -12,12 +13,13 @@ import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ErosionFieldCompiler {
|
||||
private static final int BLEND_SMOOTHING_RADIUS = 12;
|
||||
private static final double THALWEG_FRACTION = 0.45D;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final int[][] DIAGONALS = {{1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
|
||||
private static final long OUTLINE_SALT = 0x4f55544c494e45L;
|
||||
private static final long BED_SALT = 0x424544L;
|
||||
private static final long SOURCE_POND_SALT = 0x504f4e4453524345L;
|
||||
private static final long TERMINAL_POND_SALT = 0x504f4e4454524dL;
|
||||
private static final int POND_RIM_SAMPLES_PER_BLOCK = 8;
|
||||
private static final double EPSILON = 1.0E-9D;
|
||||
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
@@ -41,12 +43,29 @@ public final class ErosionFieldCompiler {
|
||||
ValleyProfile valley,
|
||||
SurfaceTerminal terminal,
|
||||
int apronLimit
|
||||
) {
|
||||
return compile(courseSeed, centerline, channel, valley, terminal, apronLimit, surface.banks().ponds());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ponds the ponds to dig at the course ends; a river passes the configured ones, a standing
|
||||
* pool passes none
|
||||
*/
|
||||
public ErosionField compile(
|
||||
long courseSeed,
|
||||
SurfaceCenterline centerline,
|
||||
ChannelProfile channel,
|
||||
ValleyProfile valley,
|
||||
SurfaceTerminal terminal,
|
||||
int apronLimit,
|
||||
HydrologyPlannerSettings.Ponds ponds
|
||||
) {
|
||||
HydrologyPlannerSettings.Banks banks = surface.banks();
|
||||
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
|
||||
int count = terminal == SurfaceTerminal.OCEAN_MOUTH ? centerline.size() : valley.exposedStations();
|
||||
double roughness = banks.roughness();
|
||||
double shoreWidth = surface.shoreWidth();
|
||||
int freeboard = banks.freeboard();
|
||||
int sink = banks.sink();
|
||||
boolean[] basin = basins(valley, channel, count);
|
||||
double[][] blendWidth = blendWidths(centerline, channel, valley, count);
|
||||
Long2IntOpenHashMap nearest = new Long2IntOpenHashMap();
|
||||
@@ -54,7 +73,10 @@ public final class ErosionFieldCompiler {
|
||||
Long2DoubleOpenHashMap distance = new Long2DoubleOpenHashMap();
|
||||
for (int station = 0; station < count; station++) {
|
||||
double widest = Math.max(blendWidth[station][0], blendWidth[station][1]);
|
||||
int radius = (int) StrictMath.ceil(channel.halfWidth()[station] * (1D + roughness) + shoreWidth + widest) + 1;
|
||||
// The shore biome band a policy asks for can be wider than the geometric shore and the
|
||||
// eroded valley; the search radius has to reach it so those columns get a shore role.
|
||||
double stationBand = stationShoreBiomeBand(centerline, station, shoreWidth);
|
||||
int radius = (int) StrictMath.ceil(channel.halfWidth()[station] * (1D + roughness) + Math.max(shoreWidth + widest, stationBand)) + 1;
|
||||
int stationX = centerline.x()[station];
|
||||
int stationZ = centerline.z()[station];
|
||||
for (int deltaZ = -radius; deltaZ <= radius; deltaZ++) {
|
||||
@@ -107,42 +129,195 @@ public final class ErosionFieldCompiler {
|
||||
if (wet) {
|
||||
double normalized = Math.min(1D, cellDistance / Math.max(0.5D, outline));
|
||||
double depth = channel.depth()[station];
|
||||
double local = 1D + (depth - 1D) * bowl(normalized)
|
||||
+ 0.5D * roughness * SurfaceNoise.signed(courseSeed ^ BED_SALT, cellX, cellZ, banks.roughnessWavelength())
|
||||
double local = 1D + (depth - 1D) * bowl(normalized, erosion.thalwegFraction())
|
||||
+ erosion.bedNoise() * roughness * SurfaceNoise.signed(courseSeed ^ BED_SALT, cellX, cellZ, banks.roughnessWavelength())
|
||||
+ (basin[station] ? 1D : 0D);
|
||||
int bed = Math.min(natural, head - Math.max(1, (int) StrictMath.round(local)));
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.CHANNEL, bed, head, false));
|
||||
wetKeys.add(key);
|
||||
continue;
|
||||
}
|
||||
int bankTop = head + freeboard;
|
||||
int bankTop = head + sink;
|
||||
// The SHORE role is the shore biome band: the column's policy width, or the geometric shore
|
||||
// width where no policy sets one. It is independent of the flattened geometric shore and of
|
||||
// the eroded valley, so a wide band reaches over untouched ground and a zero band leaves the
|
||||
// geometric shore with the bank biome.
|
||||
boolean shoreBiome = cellDistance <= outline + terrain.shoreBiomeWidth(shoreWidth) + 0.25D;
|
||||
SurfaceRole dryRole = shoreBiome ? SurfaceRole.SHORE : SurfaceRole.BANK;
|
||||
if (cellDistance <= outline + shoreWidth + 0.25D) {
|
||||
int height = Math.min(natural, bankTop);
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, height, height, false));
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
|
||||
continue;
|
||||
}
|
||||
int cut = natural - bankTop;
|
||||
if (cut <= 0) {
|
||||
if (cut <= 0 || !erosion.enabled()) {
|
||||
if (shoreBiome) {
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
double side = side(centerline, station, cellX, cellZ);
|
||||
double width = blendWidth[station][side < 0D ? 0 : 1];
|
||||
double progress = (cellDistance - outline - shoreWidth) / width;
|
||||
if (progress >= 1D) {
|
||||
if (shoreBiome) {
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int height = Math.min(natural, (int) StrictMath.round(bankTop + cut * SurfaceNoise.smoothStep(progress)));
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.BANK, height, height, false));
|
||||
int height = Math.min(natural, (int) StrictMath.round(bankTop + cut * blend(progress, erosion.blendCurve())));
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
|
||||
}
|
||||
int uncontained = contain(columns, wetKeys, freeboard);
|
||||
if (count > 0) {
|
||||
pond(columns, wetKeys, courseSeed ^ SOURCE_POND_SALT, ponds.source(), centerline, 0, valley.head()[0]);
|
||||
if (terminal == SurfaceTerminal.SINKHOLE) {
|
||||
pond(columns, wetKeys, courseSeed ^ TERMINAL_POND_SALT, ponds.terminal(), centerline, count - 1, valley.head()[count - 1]);
|
||||
}
|
||||
}
|
||||
int uncontained = contain(columns, wetKeys, sink);
|
||||
connectSteps(columns, wetKeys);
|
||||
return new ErosionField(columns, uncontained);
|
||||
}
|
||||
|
||||
/**
|
||||
* A round bowl around an end station holding that station's head: wet inside its outline, a shore
|
||||
* ring and an eroded rim outside it like the channel's own. Channel water inside the bowl joins
|
||||
* the pond at the pond's level; dry channel columns give way to the bowl. The radius is chosen per
|
||||
* course within the configured range and shrinks where the ground around the rim falls below the
|
||||
* water, so the pond never sits above the bank that has to hold it; no radius fits, no pond.
|
||||
*/
|
||||
private void pond(
|
||||
Long2ObjectOpenHashMap<SurfaceColumn> columns,
|
||||
LongArrayList wetKeys,
|
||||
long seed,
|
||||
HydrologyPlannerSettings.Pond pond,
|
||||
SurfaceCenterline centerline,
|
||||
int station,
|
||||
int head
|
||||
) {
|
||||
if (!pond.enabled()) {
|
||||
return;
|
||||
}
|
||||
HydrologyPlannerSettings.Banks banks = surface.banks();
|
||||
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
|
||||
double roughness = banks.roughness();
|
||||
double shoreWidth = surface.shoreWidth();
|
||||
int sink = banks.sink();
|
||||
int centreX = centerline.x()[station];
|
||||
int centreZ = centerline.z()[station];
|
||||
int radius = pondRadius(seed, pond, centreX, centreZ, head + sink, roughness);
|
||||
if (radius < 1) {
|
||||
return;
|
||||
}
|
||||
int bankTop = head + sink;
|
||||
int reach = (int) StrictMath.ceil(radius * (1D + roughness) + shoreWidth + banks.maximumBlendWidth()) + 1;
|
||||
for (int deltaZ = -reach; deltaZ <= reach; deltaZ++) {
|
||||
for (int deltaX = -reach; deltaX <= reach; deltaX++) {
|
||||
double distance = Math.sqrt((double) deltaX * deltaX + (double) deltaZ * deltaZ);
|
||||
if (distance > reach) {
|
||||
continue;
|
||||
}
|
||||
int cellX = centreX + deltaX;
|
||||
int cellZ = centreZ + deltaZ;
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
|
||||
continue;
|
||||
}
|
||||
int natural = terrain.naturalHeight();
|
||||
long key = RiverFootprint.pack(cellX, cellZ);
|
||||
SurfaceColumn existing = columns.get(key);
|
||||
if (existing != null && existing.apron()) {
|
||||
continue;
|
||||
}
|
||||
double outline = radius * (1D + roughness * SurfaceNoise.signed(
|
||||
seed ^ OUTLINE_SALT, cellX, cellZ, banks.roughnessWavelength()));
|
||||
outline = Math.max(Math.max(0.5D, 0.6D * radius), Math.min(1.4D * radius, outline));
|
||||
if (distance <= outline + 0.25D) {
|
||||
double normalized = Math.min(1D, distance / Math.max(0.5D, outline));
|
||||
double local = 1D + (pond.depth() - 1D) * bowl(normalized, erosion.thalwegFraction())
|
||||
+ erosion.bedNoise() * roughness * SurfaceNoise.signed(seed ^ BED_SALT, cellX, cellZ, banks.roughnessWavelength());
|
||||
int bed = Math.min(natural, head - Math.max(1, (int) StrictMath.round(local)));
|
||||
boolean wasWet = existing != null && existing.role() == SurfaceRole.CHANNEL;
|
||||
if (wasWet) {
|
||||
bed = Math.min(bed, existing.height());
|
||||
}
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.CHANNEL, bed, head, false));
|
||||
if (!wasWet) {
|
||||
wetKeys.add(key);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (existing != null && existing.role() == SurfaceRole.CHANNEL) {
|
||||
continue;
|
||||
}
|
||||
if (distance <= outline + shoreWidth + 0.25D) {
|
||||
int height = Math.min(natural, bankTop);
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, height, height, false));
|
||||
continue;
|
||||
}
|
||||
int cut = natural - bankTop;
|
||||
if (cut <= 0 || !erosion.enabled()) {
|
||||
continue;
|
||||
}
|
||||
double width = Math.max(banks.minimumBlendWidth(), Math.min(banks.maximumBlendWidth(), cut * banks.blendSlope()));
|
||||
double progress = (distance - outline - shoreWidth) / width;
|
||||
if (progress >= 1D) {
|
||||
continue;
|
||||
}
|
||||
int height = Math.min(natural, (int) StrictMath.round(bankTop + cut * blend(progress, erosion.blendCurve())));
|
||||
if (existing != null && existing.height() <= height) {
|
||||
continue;
|
||||
}
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.BANK, height, height, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The largest radius in the configured range, chosen per course, whose rim ground everywhere
|
||||
* reaches the bank top; zero when even the smallest radius sits above the ground beside it.
|
||||
*/
|
||||
private int pondRadius(long seed, HydrologyPlannerSettings.Pond pond, int centreX, int centreZ, int bankTop, double roughness) {
|
||||
int chosen = HydrologyHash.between(seed, pond.minimumRadius(), pond.maximumRadius());
|
||||
for (int radius = chosen; radius >= pond.minimumRadius(); radius--) {
|
||||
if (rimHolds(centreX, centreZ, radius * (1D + roughness) + 1D, bankTop)) {
|
||||
return radius;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private boolean rimHolds(int centreX, int centreZ, double rim, int bankTop) {
|
||||
int samples = Math.max(8, (int) StrictMath.ceil(rim * POND_RIM_SAMPLES_PER_BLOCK));
|
||||
for (int sample = 0; sample < samples; sample++) {
|
||||
double angle = sample * (2D * Math.PI / samples);
|
||||
for (double ring = rim; ring <= rim + 1D; ring += 0.5D) {
|
||||
int cellX = (int) StrictMath.round(centreX + Math.cos(angle) * ring);
|
||||
int cellZ = (int) StrictMath.round(centreZ + Math.sin(angle) * ring);
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel) || terrain.naturalHeight() < bankTop) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Eased blend from the bank top (0) to natural terrain (1); the curve exponent reshapes the valley side. */
|
||||
static double blend(double progress, double curve) {
|
||||
return SurfaceNoise.smoothStep(Math.pow(progress, curve));
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the head steps down between neighbouring wet cells, the upper bed is lowered so its water
|
||||
* column reaches the lower head; the water stays one connected body across every step.
|
||||
*/
|
||||
/** The shore biome band the terrain at a station asks for, or the geometric shore width without a policy. */
|
||||
private double stationShoreBiomeBand(SurfaceCenterline centerline, int station, double shoreWidth) {
|
||||
HydrologyTerrainSample terrain = sampler.sample(centerline.x()[station], centerline.z()[station]);
|
||||
return terrain == null ? shoreWidth : terrain.shoreBiomeWidth(shoreWidth);
|
||||
}
|
||||
|
||||
private static void connectSteps(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys) {
|
||||
for (int index = 0; index < wetKeys.size(); index++) {
|
||||
long key = wetKeys.getLong(index);
|
||||
@@ -167,21 +342,29 @@ 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 freeboard) {
|
||||
private int contain(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys, int sink) {
|
||||
int uncontained = 0;
|
||||
for (int index = 0; index < wetKeys.size(); index++) {
|
||||
SurfaceColumn wet = columns.get(wetKeys.getLong(index));
|
||||
uncontained += lip(columns, wet, CARDINALS, freeboard, true);
|
||||
lip(columns, wet, DIAGONALS, freeboard, false);
|
||||
uncontained += lip(columns, wet, CARDINALS, sink, true);
|
||||
lip(columns, wet, DIAGONALS, sink, false);
|
||||
}
|
||||
return uncontained;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private int lip(
|
||||
Long2ObjectOpenHashMap<SurfaceColumn> columns,
|
||||
SurfaceColumn wet,
|
||||
int[][] offsets,
|
||||
int freeboard,
|
||||
int sink,
|
||||
boolean countSpills
|
||||
) {
|
||||
int uncontained = 0;
|
||||
@@ -189,7 +372,9 @@ public final class ErosionFieldCompiler {
|
||||
long key = RiverFootprint.pack(wet.x() + offset[0], wet.z() + offset[1]);
|
||||
SurfaceColumn neighbour = columns.get(key);
|
||||
if (neighbour == null) {
|
||||
if (countSpills && wet.headY() != seaLevel) {
|
||||
// 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)) {
|
||||
uncontained++;
|
||||
}
|
||||
continue;
|
||||
@@ -197,9 +382,10 @@ public final class ErosionFieldCompiler {
|
||||
if (neighbour.role() == SurfaceRole.CHANNEL) {
|
||||
continue;
|
||||
}
|
||||
int required = wet.headY() + freeboard;
|
||||
int required = wet.headY() + sink;
|
||||
if (neighbour.height() < required) {
|
||||
int raised = Math.min(neighbour.terrain().naturalHeight(), required);
|
||||
int ceiling = neighbour.terrain().naturalHeight() + (countSpills ? 1 : 0);
|
||||
int raised = Math.min(ceiling, required);
|
||||
neighbour = neighbour.withHeight(raised);
|
||||
columns.put(key, neighbour);
|
||||
}
|
||||
@@ -243,10 +429,13 @@ public final class ErosionFieldCompiler {
|
||||
private double[][] blendWidths(SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley, int count) {
|
||||
HydrologyPlannerSettings.Banks banks = surface.banks();
|
||||
double[][] widths = new double[count][2];
|
||||
if (!banks.erosion().enabled()) {
|
||||
return widths;
|
||||
}
|
||||
for (int station = 0; station < count; station++) {
|
||||
double halfWidth = channel.halfWidth()[station];
|
||||
double probe = halfWidth + surface.shoreWidth() + Math.max(2D, halfWidth);
|
||||
int bankTop = valley.head()[station] + banks.freeboard();
|
||||
int bankTop = valley.head()[station] + banks.sink();
|
||||
for (int side = 0; side < 2; side++) {
|
||||
double direction = side == 0 ? -1D : 1D;
|
||||
int probeX = (int) StrictMath.round(centerline.x()[station] + centerline.normalX(station) * probe * direction);
|
||||
@@ -257,7 +446,7 @@ public final class ErosionFieldCompiler {
|
||||
widths[station][side] = Math.max(banks.minimumBlendWidth(), Math.min(banks.maximumBlendWidth(), width));
|
||||
}
|
||||
}
|
||||
return smoothWidths(widths, BLEND_SMOOTHING_RADIUS);
|
||||
return smoothWidths(widths, banks.erosion().smoothingRadius());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,11 +482,11 @@ public final class ErosionFieldCompiler {
|
||||
* Bed depth factor across the channel: a level thalweg over the inner part of the width that eases
|
||||
* up to a one-block edge, so the channel reads as a broad bowl rather than a V-shaped trough.
|
||||
*/
|
||||
static double bowl(double normalized) {
|
||||
if (normalized <= THALWEG_FRACTION) {
|
||||
static double bowl(double normalized, double thalwegFraction) {
|
||||
if (normalized <= thalwegFraction) {
|
||||
return 1D;
|
||||
}
|
||||
double t = Math.min(1D, (normalized - THALWEG_FRACTION) / (1D - THALWEG_FRACTION));
|
||||
double t = Math.min(1D, (normalized - thalwegFraction) / (1D - thalwegFraction));
|
||||
return 1D - t * t * (3D - 2D * t);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -74,7 +74,8 @@ public final class SurfaceFootprintCompiler {
|
||||
channel,
|
||||
valley,
|
||||
terminal,
|
||||
settings.outlets().maximumOceanApron()
|
||||
settings.outlets().maximumOceanApron(),
|
||||
pool ? HydrologyPlannerSettings.Ponds.none() : settings.surface().banks().ponds()
|
||||
);
|
||||
ArrayList<SurfaceColumn> ordered = new ArrayList<>(field.columns().values());
|
||||
ordered.sort(Comparator
|
||||
|
||||
@@ -3,5 +3,7 @@ package art.arcane.iris.engine.hydrology.surface;
|
||||
public enum SurfaceTerminal {
|
||||
OCEAN_MOUTH,
|
||||
SINKHOLE,
|
||||
COASTAL_GROTTO
|
||||
COASTAL_GROTTO,
|
||||
/** The course ends on another surface course; the planner levels its tail to the stem's water at the junction. */
|
||||
TRIBUTARY
|
||||
}
|
||||
|
||||
+109
-8
@@ -8,6 +8,7 @@ import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ValleyProfileSolver {
|
||||
private static final int BANK_LOOKAHEAD = 2;
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
private final int seaLevel;
|
||||
@@ -84,11 +85,28 @@ public final class ValleyProfileSolver {
|
||||
if (exposed < count && terminal != SurfaceTerminal.OCEAN_MOUTH) {
|
||||
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_EXPOSURE, exposed);
|
||||
}
|
||||
int inset = surface.banks().inset();
|
||||
// The channel's rounded end caps reach past the first and last stations; the ground under
|
||||
// them bounds those stations' water like any other bank cell.
|
||||
crossMin[0] = Math.min(crossMin[0], capMinimum(centerline, channel, 0, -1D, roughness));
|
||||
if (exposed == count) {
|
||||
crossMin[exposed - 1] = Math.min(crossMin[exposed - 1], capMinimum(centerline, channel, exposed - 1, 1D, roughness));
|
||||
}
|
||||
int sink = surface.banks().sink();
|
||||
int[] head = new int[count];
|
||||
for (int station = 0; station < exposed; station++) {
|
||||
int ceiling = crossMin[station] - inset;
|
||||
head[station] = station == 0 ? ceiling : Math.min(head[station - 1], ceiling);
|
||||
// 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
|
||||
// a fall and keeps its edge where the ground breaks.
|
||||
int ceiling = crossMin[station] - sink;
|
||||
int lookahead = ceiling;
|
||||
for (int ahead = 1; ahead <= BANK_LOOKAHEAD && station + ahead < exposed; ahead++) {
|
||||
int next = crossMin[station + ahead] - sink;
|
||||
if (next < lookahead && next >= ceiling - 1) {
|
||||
lookahead = next;
|
||||
}
|
||||
}
|
||||
head[station] = station == 0 ? lookahead : Math.min(head[station - 1], lookahead);
|
||||
}
|
||||
if (terminal == SurfaceTerminal.OCEAN_MOUTH || terminal == SurfaceTerminal.COASTAL_GROTTO) {
|
||||
for (int station = 0; station < exposed; station++) {
|
||||
@@ -100,25 +118,108 @@ public final class ValleyProfileSolver {
|
||||
head[station] = seaLevel;
|
||||
}
|
||||
}
|
||||
// The inlet: the drowned reach runs inland from the coast, holding the water at sea level, as
|
||||
// far as the inlet length allows and the ground can be cut to sea level within the inlet cap;
|
||||
// a rise the cap cannot pass ends it there. Above it the approach grades down one block per
|
||||
// station into the estuary wherever that cut fits, instead of dropping in a wall at the coast.
|
||||
// The drowned reach is the end of a river, never most of one: it takes at most half the exposed
|
||||
// course and the approach half of that again, so the headwater keeps its natural head.
|
||||
// Every head set here is at or below the terrain-supported head, so the course stays non-rising.
|
||||
HydrologyPlannerSettings.Inlet inlet = surface.banks().inlet();
|
||||
int channelIncision = surface.maximumIncision();
|
||||
int inletIncision = Math.max(channelIncision, inlet.maximumIncision());
|
||||
int rampStart = exposed;
|
||||
if (terminal == SurfaceTerminal.OCEAN_MOUTH && inlet.length() > 0) {
|
||||
int reach = Math.min(inlet.length(), exposed / 2);
|
||||
int inletStart = exposed;
|
||||
while (inletStart > 0 && exposed - inletStart < reach
|
||||
&& cutFits(inletStart - 1, seaLevel, channel, centerNatural, incisionMultiplier, inletIncision)) {
|
||||
inletStart--;
|
||||
}
|
||||
for (int station = inletStart; station < exposed; station++) {
|
||||
head[station] = seaLevel;
|
||||
}
|
||||
rampStart = Math.max(0, inletStart - reach / 2);
|
||||
// The ramp is contiguous with the inlet: the first station the cap cannot lower ends it,
|
||||
// otherwise a station lowered further upstream would sit below one left at its natural head.
|
||||
for (int station = inletStart - 1; station >= rampStart; station--) {
|
||||
int target = seaLevel + (inletStart - station);
|
||||
if (head[station] <= target) {
|
||||
continue;
|
||||
}
|
||||
if (!cutFits(station, target, channel, centerNatural, incisionMultiplier, inletIncision)) {
|
||||
break;
|
||||
}
|
||||
head[station] = target;
|
||||
}
|
||||
}
|
||||
if (terminal == SurfaceTerminal.SINKHOLE && head[exposed - 1] < terminalHead) {
|
||||
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_SINKHOLE_CLEARANCE, terminalHead - head[exposed - 1]);
|
||||
}
|
||||
if (terminal == SurfaceTerminal.COASTAL_GROTTO && head[exposed - 1] < terminalHead) {
|
||||
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_HEAD_RANGE, terminalHead - head[exposed - 1]);
|
||||
}
|
||||
int maximumIncision = surface.maximumIncision();
|
||||
int deepestCut = 0;
|
||||
boolean rejected = false;
|
||||
for (int station = 0; station < exposed; station++) {
|
||||
int permitted = Math.min(maximumIncision, (int) StrictMath.floor(maximumIncision * incisionMultiplier[station]));
|
||||
int bed = head[station] - (int) StrictMath.round(channel.depth()[station]);
|
||||
int cut = centerNatural[station] - bed;
|
||||
int maximumIncision = station >= rampStart ? inletIncision : channelIncision;
|
||||
int cut = cut(station, head[station], channel, centerNatural);
|
||||
deepestCut = Math.max(deepestCut, cut);
|
||||
rejected |= cut > permitted;
|
||||
rejected |= cut > permitted(station, incisionMultiplier, maximumIncision);
|
||||
}
|
||||
if (rejected) {
|
||||
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, deepestCut);
|
||||
}
|
||||
return new ValleyProfile(head, crossMin, crossMax, centerNatural, exposed, null, 0);
|
||||
}
|
||||
|
||||
private static boolean cutFits(
|
||||
int station,
|
||||
int head,
|
||||
ChannelProfile channel,
|
||||
int[] centerNatural,
|
||||
double[] incisionMultiplier,
|
||||
int maximumIncision
|
||||
) {
|
||||
return cut(station, head, channel, centerNatural) <= permitted(station, incisionMultiplier, maximumIncision);
|
||||
}
|
||||
|
||||
/** 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]));
|
||||
}
|
||||
|
||||
private static int permitted(int station, double[] incisionMultiplier, int maximumIncision) {
|
||||
return Math.min(maximumIncision, (int) StrictMath.floor(maximumIncision * incisionMultiplier[station]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowest natural ground under the rounded cap the channel outline draws past an end station, in
|
||||
* the given direction along the tangent; cells the channel cannot write are ignored.
|
||||
*/
|
||||
private int capMinimum(SurfaceCenterline centerline, ChannelProfile channel, int station, double direction, double roughness) {
|
||||
double reach = channel.halfWidth()[station] * (1D + roughness) + 2D;
|
||||
double tangentX = centerline.tangentX()[station] * direction;
|
||||
double tangentZ = centerline.tangentZ()[station] * direction;
|
||||
double normalX = centerline.normalX(station);
|
||||
double normalZ = centerline.normalZ(station);
|
||||
int stationX = centerline.x()[station];
|
||||
int stationZ = centerline.z()[station];
|
||||
int minimum = Integer.MAX_VALUE;
|
||||
for (double along = 0.5D; along <= reach; along += 0.5D) {
|
||||
for (double across = -reach; across <= reach; across += 0.5D) {
|
||||
if (along * along + across * across > reach * reach) {
|
||||
continue;
|
||||
}
|
||||
int cellX = (int) StrictMath.round(stationX + tangentX * along + normalX * across);
|
||||
int cellZ = (int) StrictMath.round(stationZ + tangentZ * along + normalZ * across);
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
|
||||
continue;
|
||||
}
|
||||
minimum = Math.min(minimum, terrain.naturalHeight());
|
||||
}
|
||||
}
|
||||
return minimum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls direct, contained river-to-ocean grotto outlets.")
|
||||
@Desc("Controls coastal grottos: sea-level chambers whose only opening is their ocean face, admitted either as a river outlet or as a standalone sea cave.")
|
||||
@Data
|
||||
public class IrisCoastalRiverGrottoConfig {
|
||||
@Desc("Allow a coastal grotto to serve as an explicit accepted outlet.")
|
||||
@@ -37,4 +37,7 @@ public class IrisCoastalRiverGrottoConfig {
|
||||
@MaxNumber(1048576)
|
||||
@Desc("Maximum accepted coastal grotto volume in blocks.")
|
||||
private int maximumVolume = 8192;
|
||||
|
||||
@Desc("Standalone sea caves: coastal grottos opening from the ocean into the coast without a river.")
|
||||
private IrisSeaCaveConfig seaCaves = new IrisSeaCaveConfig();
|
||||
}
|
||||
|
||||
@@ -21,4 +21,19 @@ public class IrisRiverMouthConfig {
|
||||
@MaxNumber(32)
|
||||
@Desc("Maximum number of blocks the recorded mouth footprint may enter the ocean before terminating.")
|
||||
private int maximumOceanApron = 8;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(256)
|
||||
@Desc("Blocks of river before the coast held at sea level as a drowned inlet, widened toward flareRatio over that reach. Zero ends the river at the shoreline with no inlet.")
|
||||
private int inletLength = 64;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Extra bed depth reached at the shoreline over the inlet, so the estuary is deeper than the river above it.")
|
||||
private int inletDepth = 3;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(128)
|
||||
@Desc("Deepest cut allowed in the inlet and its approach ramp, replacing channel.maximumIncision there, so the coast may be cut down to sea level through a rise of this height.")
|
||||
private int maximumIncision = 32;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,14 @@ public class IrisRiverPolicy {
|
||||
@Desc("Multiplier applied to the eroded bank width outside the shore band.")
|
||||
private Double bankMultiplier = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(32)
|
||||
@Desc("Width in blocks of the shore biome band beside a surface river in this area. Null inherits, and without any value the band is the surface banks shore width. Zero gives the area no shore biome band; the geometric shore and erosion are unchanged.")
|
||||
private Double shoreBiomeWidth = null;
|
||||
|
||||
@Desc("Rivers that start in this area stay inside it: their whole course and their outlet lie in this region (when set on a region) or this biome (when set on a biome), and a river that flows into the area never leaves it. A source with no reachable outlet inside the area is rejected. Null inherits, false releases the area.")
|
||||
private Boolean confined = null;
|
||||
|
||||
public KSet<String> getAllBiomeIds() {
|
||||
KSet<String> biomeIds = new KSet<>();
|
||||
addAll(biomeIds, surfaceBiomes);
|
||||
|
||||
@@ -41,9 +41,14 @@ public class IrisRiverRoutingConfig {
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(256)
|
||||
@Desc("Maximum drainage roots selected per planning tile.")
|
||||
@Desc("Maximum inland drainage roots (sinkhole grottos) selected per planning tile. Unused when no inland outlet kind is enabled.")
|
||||
private int maximumOutletsPerTile = 4;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("Maximum sea outlets (river mouths and coastal grottos) selected per planning tile, chosen by type in turn: the best mouth, the best coastal grotto, the next mouth, and so on, each at least two sample spacings from the others. Budgeted separately from maximumOutletsPerTile.")
|
||||
private int maximumCoastalOutletsPerTile = 2;
|
||||
|
||||
@Desc("Allow rivers to terminate at an ocean reservoir.")
|
||||
private boolean oceanOutlets = true;
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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("Sea caves: coastal grottos that open from the ocean into the coast without a river. The chamber size, headroom and volume cap are the coastal grotto's.")
|
||||
@Data
|
||||
public class IrisSeaCaveConfig {
|
||||
@Desc("Plan sea caves along the coast. Requires the coastal grotto to be enabled.")
|
||||
private boolean enabled = true;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("Sea caves accepted per planning tile; the steepest owned coast is taken first.")
|
||||
private int maximumPerTile = 3;
|
||||
|
||||
@MinNumber(16)
|
||||
@MaxNumber(8192)
|
||||
@Desc("Least distance in blocks between two sea caves. Must be at least twice the coastal grotto horizontalRadius.")
|
||||
private int minimumSpacing = 160;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(128)
|
||||
@Desc("The coast must stand this many blocks above the sea both at the shoreline and at the back of the chamber.")
|
||||
private int minimumCoastHeight = 8;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(128)
|
||||
@Desc("How far inland from the shoreline the chamber is swept. 0 leaves a single chamber at the shore.")
|
||||
private int depth = 12;
|
||||
}
|
||||
@@ -9,14 +9,9 @@ import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls the lip, shore band and eroded valley around a surface river.")
|
||||
@Desc("Controls the shore band and the eroded valley around a surface river.")
|
||||
@Data
|
||||
public class IrisSurfaceRiverBankConfig {
|
||||
@MinNumber(0)
|
||||
@MaxNumber(4)
|
||||
@Desc("Blocks the bank top sits above the water surface at the channel edge.")
|
||||
private int freeboard = 1;
|
||||
|
||||
@MinNumber(0.5)
|
||||
@MaxNumber(6)
|
||||
@Desc("Width in blocks of the shore band that receives shore biome content.")
|
||||
|
||||
@@ -20,8 +20,8 @@ public class IrisSurfaceRiverChannelConfig {
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(3)
|
||||
@Desc("Blocks the water surface sits below the lowest natural bank beside the channel.")
|
||||
private int inset = 1;
|
||||
@Desc("Blocks the water surface sinks below the lowest natural ground beside the channel; 0 keeps the water flush with the bank, and the bank always meets the water at its own height.")
|
||||
private int sink = 0;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(32)
|
||||
|
||||
@@ -16,10 +16,10 @@ public class IrisSurfaceRiverConfig {
|
||||
@Desc("Surface-river source allocation.")
|
||||
private IrisSurfaceRiverSourceConfig sources = new IrisSurfaceRiverSourceConfig();
|
||||
|
||||
@Desc("Wet channel width, depth, inset and roughness.")
|
||||
@Desc("Wet channel width, depth, sink and roughness.")
|
||||
private IrisSurfaceRiverChannelConfig channel = new IrisSurfaceRiverChannelConfig();
|
||||
|
||||
@Desc("Lip, shore band and eroded valley around the channel.")
|
||||
@Desc("Shore band and eroded valley around the channel.")
|
||||
private IrisSurfaceRiverBankConfig banks = new IrisSurfaceRiverBankConfig();
|
||||
@Desc("Material under and beside the water: falling-block replacement and padding.")
|
||||
private IrisSurfaceRiverBedConfig bed = new IrisSurfaceRiverBedConfig();
|
||||
@@ -29,4 +29,8 @@ public class IrisSurfaceRiverConfig {
|
||||
|
||||
@Desc("Coastal mouth flare and ocean-apron limits.")
|
||||
private IrisRiverMouthConfig mouths = new IrisRiverMouthConfig();
|
||||
@Desc("How the ground around the channel is eroded into a valley.")
|
||||
private IrisSurfaceRiverErosionConfig erosion = new IrisSurfaceRiverErosionConfig();
|
||||
@Desc("Ponds at the source and at the inland end of every surface river.")
|
||||
private IrisSurfaceRiverPondsConfig ponds = new IrisSurfaceRiverPondsConfig();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls how the ground around a surface river is eroded into a valley. The valley's reach comes from banks.blendSlope and the blend widths; this section shapes it.")
|
||||
@Data
|
||||
public class IrisSurfaceRiverErosionConfig {
|
||||
@Desc("Erode the ground beyond the shore band into a valley; false keeps only the wet channel, the shore band and the lip that holds the water.")
|
||||
private boolean enabled = true;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("Stations along the course the valley width is averaged over, so it widens and narrows gradually; 0 follows the local cut exactly.")
|
||||
private int smoothingRadius = 12;
|
||||
|
||||
@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.")
|
||||
private double thalwegFraction = 0.45D;
|
||||
|
||||
@MinNumber(0.25)
|
||||
@MaxNumber(4)
|
||||
@Desc("Exponent on the blend from the bank top out to natural terrain; below 1 hollows the valley sides, above 1 keeps them steep near the shore.")
|
||||
private double blendCurve = 1D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(2)
|
||||
@Desc("Share of the channel roughness applied to the bed as depth variation; 0 leaves a smooth bed.")
|
||||
private double bedNoise = 0.5D;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("A round pond at one end of a surface river, holding the river's water level there.")
|
||||
@Data
|
||||
public class IrisSurfaceRiverPondConfig {
|
||||
@Desc("Dig the pond.")
|
||||
private boolean enabled = true;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(32)
|
||||
@Desc("Smallest pond radius in blocks; the radius is chosen per river between the minimum and the maximum, and shrinks where the ground falls away around the rim.")
|
||||
private int minimumRadius = 6;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(32)
|
||||
@Desc("Largest pond radius in blocks.")
|
||||
private int maximumRadius = 12;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@Desc("Depth of the pond bowl below the water surface at its center.")
|
||||
private int depth = 3;
|
||||
|
||||
public IrisSurfaceRiverPondConfig(int minimumRadius, int maximumRadius, int depth) {
|
||||
this.minimumRadius = minimumRadius;
|
||||
this.maximumRadius = maximumRadius;
|
||||
this.depth = depth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Ponds at the two ends of a surface river: the spring it rises from and, for a river that ends inland, the pool it drains into.")
|
||||
@Data
|
||||
public class IrisSurfaceRiverPondsConfig {
|
||||
@Desc("The spring pond every surface river rises from.")
|
||||
private IrisSurfaceRiverPondConfig source = new IrisSurfaceRiverPondConfig(6, 12, 3);
|
||||
|
||||
@Desc("The pond a river that ends inland drains into; rivers that reach the ocean get none.")
|
||||
private IrisSurfaceRiverPondConfig terminal = new IrisSurfaceRiverPondConfig(4, 7, 3);
|
||||
}
|
||||
@@ -38,6 +38,11 @@ public class IrisUndergroundRiverConfig {
|
||||
@Desc("Distance in blocks over which an ocean-bound underground river levels to sea level before its mouth.")
|
||||
private int mouthLevelingDistance = 64;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(4)
|
||||
@Desc("Extra underground courses an outlet may accept as tributaries joining its main passage; they are budgeted on top of the source density, and 0 keeps one passage per outlet.")
|
||||
private int tributaries = 1;
|
||||
|
||||
private static IrisStyledRange range(double min, double max, double zoom) {
|
||||
return new IrisStyledRange(min, max, new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(zoom));
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ public class PackHydrologyValidatorTest {
|
||||
"minimumSurfaceCourseLength": 0,
|
||||
"minimumUndergroundCourseLength": 0,
|
||||
"maximumOutletsPerTile": 1,
|
||||
"maximumCoastalOutletsPerTile": 0,
|
||||
"oceanOutlets": true,
|
||||
"valleyPreference": 0,
|
||||
"uphillPenalty": 0,
|
||||
@@ -88,13 +89,12 @@ public class PackHydrologyValidatorTest {
|
||||
"channel": {
|
||||
"width": {"min": 1, "max": 1},
|
||||
"depth": {"min": 1, "max": 1},
|
||||
"inset": 0,
|
||||
"sink": 0,
|
||||
"maximumIncision": 1,
|
||||
"roughness": 0,
|
||||
"roughnessWavelength": 4
|
||||
},
|
||||
"banks": {
|
||||
"freeboard": 0,
|
||||
"shoreWidth": 0.5,
|
||||
"blendSlope": 0.5,
|
||||
"minimumBlendWidth": 1,
|
||||
@@ -104,7 +104,15 @@ public class PackHydrologyValidatorTest {
|
||||
"flow": {"cascadeRun": 1, "waterfallMinimumDrop": 2},
|
||||
"mouths": {"flareRatio": 1, "maximumOceanApron": 0}
|
||||
},
|
||||
"underground": {"mouthLevelingDistance": 16}
|
||||
"underground": {"mouthLevelingDistance": 16},
|
||||
"grottos": {
|
||||
"coastal": {
|
||||
"horizontalRadius": 8,
|
||||
"verticalRadius": 1,
|
||||
"headroom": 1,
|
||||
"seaCaves": {"enabled": false, "maximumPerTile": 0, "minimumSpacing": 16, "minimumCoastHeight": 1, "depth": 0}
|
||||
}
|
||||
}
|
||||
}
|
||||
""", "\"bankMultiplier\": 0"));
|
||||
|
||||
@@ -247,6 +255,7 @@ public class PackHydrologyValidatorTest {
|
||||
"minimumSurfaceCourseLength": 32768,
|
||||
"minimumUndergroundCourseLength": 32768,
|
||||
"maximumOutletsPerTile": 256,
|
||||
"maximumCoastalOutletsPerTile": 64,
|
||||
"oceanOutlets": true,
|
||||
"valleyPreference": 8,
|
||||
"uphillPenalty": 128,
|
||||
@@ -257,13 +266,12 @@ public class PackHydrologyValidatorTest {
|
||||
"channel": {
|
||||
"width": {"min": 128, "max": 128},
|
||||
"depth": {"min": 64, "max": 64},
|
||||
"inset": 3,
|
||||
"sink": 3,
|
||||
"maximumIncision": 32,
|
||||
"roughness": 1,
|
||||
"roughnessWavelength": 64
|
||||
},
|
||||
"banks": {
|
||||
"freeboard": 4,
|
||||
"shoreWidth": 6,
|
||||
"blendSlope": 12,
|
||||
"minimumBlendWidth": 64,
|
||||
@@ -273,7 +281,13 @@ public class PackHydrologyValidatorTest {
|
||||
"flow": {"cascadeRun": 8, "waterfallMinimumDrop": 32},
|
||||
"mouths": {"flareRatio": 4, "maximumOceanApron": 32}
|
||||
},
|
||||
"underground": {"mouthLevelingDistance": 512}
|
||||
"underground": {"mouthLevelingDistance": 512},
|
||||
"grottos": {
|
||||
"coastal": {
|
||||
"horizontalRadius": 128,
|
||||
"seaCaves": {"enabled": true, "maximumPerTile": 64, "minimumSpacing": 8192, "minimumCoastHeight": 128, "depth": 128}
|
||||
}
|
||||
}
|
||||
}
|
||||
""", "\"bankMultiplier\": 4"));
|
||||
|
||||
@@ -294,6 +308,7 @@ public class PackHydrologyValidatorTest {
|
||||
"minimumSurfaceCourseLength": 512,
|
||||
"minimumUndergroundCourseLength": 512,
|
||||
"maximumOutletsPerTile": 0,
|
||||
"maximumCoastalOutletsPerTile": 65,
|
||||
"valleyPreference": 8.5,
|
||||
"uphillPenalty": 128.5,
|
||||
"slopePenalty": 16.5,
|
||||
@@ -310,6 +325,7 @@ public class PackHydrologyValidatorTest {
|
||||
"tileSize": 128,
|
||||
"sampleSpacing": 4,
|
||||
"maximumRouteLength": 128,
|
||||
"maximumCoastalOutletsPerTile": -1,
|
||||
"valleyPreference": -0.5,
|
||||
"uphillPenalty": -0.5,
|
||||
"slopePenalty": -0.5,
|
||||
@@ -326,6 +342,7 @@ public class PackHydrologyValidatorTest {
|
||||
assertContains(aboveResult.errors(), "minimumSurfaceCourseLength must not exceed maximumRouteLength");
|
||||
assertContains(aboveResult.errors(), "minimumUndergroundCourseLength must not exceed maximumRouteLength");
|
||||
assertContains(aboveResult.errors(), "maximumOutletsPerTile must be at least 1");
|
||||
assertContains(aboveResult.errors(), "maximumCoastalOutletsPerTile must be at most 64");
|
||||
assertContains(aboveResult.errors(), "valleyPreference must be at most 8.0");
|
||||
assertContains(aboveResult.errors(), "uphillPenalty must be at most 128.0");
|
||||
assertContains(aboveResult.errors(), "slopePenalty must be at most 16.0");
|
||||
@@ -334,6 +351,7 @@ public class PackHydrologyValidatorTest {
|
||||
assertContains(belowResult.errors(), "tileSize must be at least 256");
|
||||
assertContains(belowResult.errors(), "sampleSpacing must be at least 8");
|
||||
assertContains(belowResult.errors(), "maximumRouteLength must be at least 256");
|
||||
assertContains(belowResult.errors(), "maximumCoastalOutletsPerTile must be at least 0");
|
||||
assertContains(belowResult.errors(), "valleyPreference must be at least 0.0");
|
||||
assertContains(belowResult.errors(), "uphillPenalty must be at least 0.0");
|
||||
assertContains(belowResult.errors(), "slopePenalty must be at least 0.0");
|
||||
@@ -349,7 +367,7 @@ public class PackHydrologyValidatorTest {
|
||||
"channel": {
|
||||
"width": {"min": 20, "max": 2},
|
||||
"depth": {"min": 2, "max": 65},
|
||||
"inset": 4,
|
||||
"sink": 4,
|
||||
"maximumIncision": 33,
|
||||
"roughness": 1.5,
|
||||
"roughnessWavelength": 65,
|
||||
@@ -366,7 +384,7 @@ public class PackHydrologyValidatorTest {
|
||||
"channel": {
|
||||
"width": {"min": 0, "max": 8},
|
||||
"depth": {"min": 0, "max": 4},
|
||||
"inset": -1,
|
||||
"sink": -1,
|
||||
"maximumIncision": 0,
|
||||
"roughness": -0.1,
|
||||
"roughnessWavelength": 3,
|
||||
@@ -382,7 +400,7 @@ public class PackHydrologyValidatorTest {
|
||||
|
||||
assertContains(aboveResult.errors(), "width.min must not exceed max");
|
||||
assertContains(aboveResult.errors(), "depth.max must be at most 64.0");
|
||||
assertContains(aboveResult.errors(), "inset must be at most 3");
|
||||
assertContains(aboveResult.errors(), "sink must be at most 3");
|
||||
assertContains(aboveResult.errors(), "maximumIncision must be at most 32");
|
||||
assertContains(aboveResult.errors(), "roughness must be at most 1.0");
|
||||
assertContains(aboveResult.errors(), "roughnessWavelength must be at most 64");
|
||||
@@ -392,12 +410,52 @@ public class PackHydrologyValidatorTest {
|
||||
assertContains(belowResult.errors(), "springLength must be at least 4");
|
||||
assertContains(belowResult.errors(), "width.min must be at least 1.0");
|
||||
assertContains(belowResult.errors(), "depth.min must be at least 1.0");
|
||||
assertContains(belowResult.errors(), "inset must be at least 0");
|
||||
assertContains(belowResult.errors(), "sink must be at least 0");
|
||||
assertContains(belowResult.errors(), "maximumIncision must be at least 1");
|
||||
assertContains(belowResult.errors(), "roughness must be at least 0.0");
|
||||
assertContains(belowResult.errors(), "roughnessWavelength must be at least 4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsSurfaceErosionAndPondValuesOutsideTheirBounds() throws Exception {
|
||||
File above = pack(riversDimension("""
|
||||
{
|
||||
"enabled": true,
|
||||
"surface": {
|
||||
"erosion": {"enabled": "yes", "smoothingRadius": 65, "thalwegFraction": 0.96, "blendCurve": 4.5, "bedNoise": 2.5},
|
||||
"ponds": {"source": {"minimumRadius": 33, "maximumRadius": 33, "depth": 9}, "terminal": {"minimumRadius": 6, "maximumRadius": 5, "depth": 1}}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
File below = pack(riversDimension("""
|
||||
{
|
||||
"enabled": true,
|
||||
"surface": {
|
||||
"erosion": {"smoothingRadius": -1, "thalwegFraction": -0.1, "blendCurve": 0.1, "bedNoise": -1},
|
||||
"ponds": {"source": {"minimumRadius": 0, "maximumRadius": 0, "depth": 0}}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
|
||||
PackHydrologyValidator.Validation aboveResult = validate(above);
|
||||
PackHydrologyValidator.Validation belowResult = validate(below);
|
||||
|
||||
assertContains(aboveResult.errors(), "enabled must be a boolean");
|
||||
assertContains(aboveResult.errors(), "smoothingRadius must be at most 64");
|
||||
assertContains(aboveResult.errors(), "thalwegFraction must be at most 0.95");
|
||||
assertContains(aboveResult.errors(), "blendCurve must be at most 4.0");
|
||||
assertContains(aboveResult.errors(), "bedNoise must be at most 2.0");
|
||||
assertContains(aboveResult.errors(), "minimumRadius must be at most 32");
|
||||
assertContains(aboveResult.errors(), "depth must be at most 8");
|
||||
assertContains(aboveResult.errors(), "ponds.terminal.minimumRadius must not exceed maximumRadius");
|
||||
assertContains(belowResult.errors(), "smoothingRadius must be at least 0");
|
||||
assertContains(belowResult.errors(), "thalwegFraction must be at least 0.0");
|
||||
assertContains(belowResult.errors(), "blendCurve must be at least 0.25");
|
||||
assertContains(belowResult.errors(), "bedNoise must be at least 0.0");
|
||||
assertContains(belowResult.errors(), "minimumRadius must be at least 1");
|
||||
assertContains(belowResult.errors(), "depth must be at least 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsSurfaceBankValuesOutsideTheirBounds() throws Exception {
|
||||
File above = pack(riversDimension("""
|
||||
@@ -405,7 +463,6 @@ public class PackHydrologyValidatorTest {
|
||||
"enabled": true,
|
||||
"surface": {
|
||||
"banks": {
|
||||
"freeboard": 5,
|
||||
"shoreWidth": 6.5,
|
||||
"blendSlope": 12.5,
|
||||
"minimumBlendWidth": 40,
|
||||
@@ -420,7 +477,6 @@ public class PackHydrologyValidatorTest {
|
||||
"enabled": true,
|
||||
"surface": {
|
||||
"banks": {
|
||||
"freeboard": -1,
|
||||
"shoreWidth": 0.4,
|
||||
"blendSlope": 0.4,
|
||||
"minimumBlendWidth": 0,
|
||||
@@ -442,12 +498,10 @@ public class PackHydrologyValidatorTest {
|
||||
PackHydrologyValidator.Validation belowResult = validate(below);
|
||||
PackHydrologyValidator.Validation unorderedResult = validate(unordered);
|
||||
|
||||
assertContains(aboveResult.errors(), "freeboard must be at most 4");
|
||||
assertContains(aboveResult.errors(), "shoreWidth must be at most 6.0");
|
||||
assertContains(aboveResult.errors(), "blendSlope must be at most 12.0");
|
||||
assertContains(aboveResult.errors(), "maximumBlendWidth must be at most 64");
|
||||
assertContains(aboveResult.errors(), "exposeCutStrata must be a boolean");
|
||||
assertContains(belowResult.errors(), "freeboard must be at least 0");
|
||||
assertContains(belowResult.errors(), "shoreWidth must be at least 0.5");
|
||||
assertContains(belowResult.errors(), "blendSlope must be at least 0.5");
|
||||
assertContains(belowResult.errors(), "minimumBlendWidth must be at least 1");
|
||||
@@ -461,7 +515,7 @@ public class PackHydrologyValidatorTest {
|
||||
"enabled": true,
|
||||
"surface": {
|
||||
"flow": {"cascadeRun": 9, "waterfallMinimumDrop": 33},
|
||||
"mouths": {"flareRatio": 4.5, "maximumOceanApron": 33}
|
||||
"mouths": {"flareRatio": 4.5, "maximumOceanApron": 33, "inletLength": 257, "inletDepth": 17, "maximumIncision": 129}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
@@ -470,22 +524,51 @@ public class PackHydrologyValidatorTest {
|
||||
"enabled": true,
|
||||
"surface": {
|
||||
"flow": {"cascadeRun": 0, "waterfallMinimumDrop": 1},
|
||||
"mouths": {"flareRatio": 0.5, "maximumOceanApron": -1}
|
||||
"mouths": {"flareRatio": 0.5, "maximumOceanApron": -1, "inletLength": -1, "inletDepth": -1, "maximumIncision": -1}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
File longInlet = pack(riversDimension("""
|
||||
{
|
||||
"enabled": true,
|
||||
"routing": {"minimumSurfaceCourseLength": 128},
|
||||
"surface": {
|
||||
"mouths": {"inletLength": 200}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
File fittingInlet = pack(riversDimension("""
|
||||
{
|
||||
"enabled": true,
|
||||
"routing": {"minimumSurfaceCourseLength": 128},
|
||||
"surface": {
|
||||
"mouths": {"inletLength": 128, "inletDepth": 0, "maximumIncision": 0}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
|
||||
PackHydrologyValidator.Validation aboveResult = validate(above);
|
||||
PackHydrologyValidator.Validation belowResult = validate(below);
|
||||
PackHydrologyValidator.Validation longInletResult = validate(longInlet);
|
||||
PackHydrologyValidator.Validation fittingInletResult = validate(fittingInlet);
|
||||
|
||||
assertContains(aboveResult.errors(), "cascadeRun must be at most 8");
|
||||
assertContains(aboveResult.errors(), "waterfallMinimumDrop must be at most 32");
|
||||
assertContains(aboveResult.errors(), "flareRatio must be at most 4.0");
|
||||
assertContains(aboveResult.errors(), "maximumOceanApron must be at most 32");
|
||||
assertContains(aboveResult.errors(), "inletLength must be at most 256");
|
||||
assertContains(aboveResult.errors(), "inletDepth must be at most 16");
|
||||
assertContains(aboveResult.errors(), "mouths.maximumIncision must be at most 128");
|
||||
assertContains(belowResult.errors(), "cascadeRun must be at least 1");
|
||||
assertContains(belowResult.errors(), "waterfallMinimumDrop must be at least 2");
|
||||
assertContains(belowResult.errors(), "flareRatio must be at least 1.0");
|
||||
assertContains(belowResult.errors(), "maximumOceanApron must be at least 0");
|
||||
assertContains(belowResult.errors(), "inletLength must be at least 0");
|
||||
assertContains(belowResult.errors(), "inletDepth must be at least 0");
|
||||
assertContains(belowResult.errors(), "mouths.maximumIncision must be at least 0");
|
||||
assertContains(longInletResult.errors(), "mouths.inletLength must not exceed routing.minimumSurfaceCourseLength");
|
||||
assertTrue(fittingInletResult.errors().toString(),
|
||||
fittingInletResult.errors().stream().noneMatch(error -> error.contains("inletLength")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1153,6 +1236,58 @@ public class PackHydrologyValidatorTest {
|
||||
assertContains(result.getBlockingErrors(), "tileSize must be divisible by sampleSpacing");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsSeaCaveValuesOutsideTheirBoundsAndSpacingBelowTheChamberWidth() throws Exception {
|
||||
File above = pack(riversDimension("""
|
||||
{
|
||||
"enabled": true,
|
||||
"grottos": {
|
||||
"coastal": {
|
||||
"horizontalRadius": 12,
|
||||
"seaCaves": {"enabled": true, "maximumPerTile": 65, "minimumSpacing": 8193, "minimumCoastHeight": 129, "depth": 129}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
File below = pack(riversDimension("""
|
||||
{
|
||||
"enabled": true,
|
||||
"grottos": {
|
||||
"coastal": {
|
||||
"horizontalRadius": 12,
|
||||
"seaCaves": {"enabled": "yes", "maximumPerTile": -1, "minimumSpacing": 20, "minimumCoastHeight": 0, "depth": -1}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
File canonical = pack(riversDimension("""
|
||||
{
|
||||
"enabled": true,
|
||||
"grottos": {
|
||||
"coastal": {
|
||||
"horizontalRadius": 12,
|
||||
"seaCaves": {"enabled": true, "maximumPerTile": 3, "minimumSpacing": 160, "minimumCoastHeight": 8, "depth": 12}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""));
|
||||
|
||||
PackHydrologyValidator.Validation aboveResult = validate(above);
|
||||
PackHydrologyValidator.Validation belowResult = validate(below);
|
||||
PackHydrologyValidator.Validation canonicalResult = validate(canonical);
|
||||
|
||||
assertContains(aboveResult.errors(), "seaCaves.maximumPerTile must be at most 64");
|
||||
assertContains(aboveResult.errors(), "seaCaves.minimumSpacing must be at most 8192");
|
||||
assertContains(aboveResult.errors(), "seaCaves.minimumCoastHeight must be at most 128");
|
||||
assertContains(aboveResult.errors(), "seaCaves.depth must be at most 128");
|
||||
assertContains(belowResult.errors(), "seaCaves.enabled must be a boolean");
|
||||
assertContains(belowResult.errors(), "seaCaves.maximumPerTile must be at least 0");
|
||||
assertContains(belowResult.errors(), "seaCaves.minimumSpacing must be at least twice");
|
||||
assertContains(belowResult.errors(), "seaCaves.minimumCoastHeight must be at least 1");
|
||||
assertContains(belowResult.errors(), "seaCaves.depth must be at least 0");
|
||||
assertTrue(canonicalResult.errors().toString(), canonicalResult.errors().isEmpty());
|
||||
}
|
||||
|
||||
private PackHydrologyValidator.Validation validate(File pack) {
|
||||
File[] dimensions = new File(pack, "dimensions").listFiles(
|
||||
file -> file.isFile() && file.getName().endsWith(".json"));
|
||||
@@ -1258,7 +1393,6 @@ public class PackHydrologyValidatorTest {
|
||||
"roughnessWavelength": 16
|
||||
},
|
||||
"banks": {
|
||||
"freeboard": 1,
|
||||
"shoreWidth": 1.5,
|
||||
"blendSlope": 3,
|
||||
"minimumBlendWidth": 4,
|
||||
@@ -1291,7 +1425,14 @@ public class PackHydrologyValidatorTest {
|
||||
"horizontalRadius": 12,
|
||||
"verticalRadius": 7,
|
||||
"headroom": 4,
|
||||
"maximumVolume": 8192
|
||||
"maximumVolume": 8192,
|
||||
"seaCaves": {
|
||||
"enabled": true,
|
||||
"maximumPerTile": 3,
|
||||
"minimumSpacing": 160,
|
||||
"minimumCoastHeight": 8,
|
||||
"depth": 12
|
||||
}
|
||||
},
|
||||
"inland": {
|
||||
"enabled": true,
|
||||
|
||||
@@ -112,13 +112,25 @@ public class IrisRiverSchemaTest {
|
||||
assertEquals(32, drops.getJSONObject("maximumBasinDepth").getInt("maximum"));
|
||||
assertSnippetBackedObjectReference(channel.getJSONObject("width"));
|
||||
assertSnippetBackedObjectReference(channel.getJSONObject("depth"));
|
||||
assertEquals(0, channel.getJSONObject("inset").getInt("minimum"));
|
||||
assertEquals(3, channel.getJSONObject("inset").getInt("maximum"));
|
||||
assertEquals(0, channel.getJSONObject("sink").getInt("minimum"));
|
||||
assertEquals(3, channel.getJSONObject("sink").getInt("maximum"));
|
||||
assertFalse(channel.has("inset"));
|
||||
assertEquals(32, channel.getJSONObject("maximumIncision").getInt("maximum"));
|
||||
assertEquals(1D, channel.getJSONObject("roughness").getDouble("maximum"), 0D);
|
||||
assertEquals(64, channel.getJSONObject("roughnessWavelength").getInt("maximum"));
|
||||
assertEquals(0, banks.getJSONObject("freeboard").getInt("minimum"));
|
||||
assertEquals(4, banks.getJSONObject("freeboard").getInt("maximum"));
|
||||
assertFalse(banks.has("freeboard"));
|
||||
JSONObject erosion = referencedProperties(definitions, surface.getJSONObject("erosion"));
|
||||
assertEquals("boolean", erosion.getJSONObject("enabled").getString("type"));
|
||||
assertEquals(64, erosion.getJSONObject("smoothingRadius").getInt("maximum"));
|
||||
assertEquals(0.95D, erosion.getJSONObject("thalwegFraction").getDouble("maximum"), 0D);
|
||||
assertEquals(0.25D, erosion.getJSONObject("blendCurve").getDouble("minimum"), 0D);
|
||||
assertEquals(2D, erosion.getJSONObject("bedNoise").getDouble("maximum"), 0D);
|
||||
JSONObject ponds = referencedProperties(definitions, surface.getJSONObject("ponds"));
|
||||
JSONObject sourcePond = referencedProperties(definitions, ponds.getJSONObject("source"));
|
||||
JSONObject terminalPond = referencedProperties(definitions, ponds.getJSONObject("terminal"));
|
||||
assertEquals("boolean", sourcePond.getJSONObject("enabled").getString("type"));
|
||||
assertEquals(32, sourcePond.getJSONObject("maximumRadius").getInt("maximum"));
|
||||
assertEquals(8, terminalPond.getJSONObject("depth").getInt("maximum"));
|
||||
assertEquals(6D, banks.getJSONObject("shoreWidth").getDouble("maximum"), 0D);
|
||||
assertEquals(0.5D, banks.getJSONObject("blendSlope").getDouble("minimum"), 0D);
|
||||
assertEquals(12D, banks.getJSONObject("blendSlope").getDouble("maximum"), 0D);
|
||||
@@ -186,6 +198,9 @@ public class IrisRiverSchemaTest {
|
||||
assertEquals("array", properties.getJSONObject("bankBiomes").getString("type"));
|
||||
assertEquals("array", properties.getJSONObject("floodedCaveBiomes").getString("type"));
|
||||
assertEquals(16D, properties.getJSONObject("widthMultiplier").getDouble("maximum"), 0D);
|
||||
assertEquals(32D, properties.getJSONObject("shoreBiomeWidth").getDouble("maximum"), 0D);
|
||||
assertEquals(0D, properties.getJSONObject("shoreBiomeWidth").getDouble("minimum"), 0D);
|
||||
assertEquals("boolean", properties.getJSONObject("confined").getString("type"));
|
||||
assertEquals(0D, properties.getJSONObject("incisionMultiplier").getDouble("minimum"), 0D);
|
||||
assertEquals(64D, properties.getJSONObject("routingMultiplier").getDouble("maximum"), 0D);
|
||||
assertEquals("#/definitions/erzbiomes",
|
||||
|
||||
+153
@@ -6,6 +6,7 @@ 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.HydrologyCaveCandidate;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlannerSettings;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -1067,6 +1068,158 @@ public class HydrologyCaveCourseFilterTest {
|
||||
assertEquals(1, diagnostics.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaCaveIsContainedWithItsSeaFaceAsTheOnlyOpening() {
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> {
|
||||
if (x >= 112) {
|
||||
return HydrologyTerrainSample.ocean(48, "ocean");
|
||||
}
|
||||
if (z >= 9) {
|
||||
return HydrologyTerrainSample.openLand(64, 0D, "beach");
|
||||
}
|
||||
if (x >= 110) {
|
||||
return HydrologyTerrainSample.openLand(74, 0D, "face");
|
||||
}
|
||||
if (x >= 104) {
|
||||
return HydrologyTerrainSample.openLand(66, 0D, "low");
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(92, 0D, "cliff");
|
||||
};
|
||||
HydraulicSegment chamber = new HydraulicSegment(
|
||||
921L,
|
||||
920L,
|
||||
HydrologyFeatureType.COASTAL_GROTTO,
|
||||
63,
|
||||
63,
|
||||
4,
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
|
||||
);
|
||||
RiverCourse seaCave = new RiverCourse(
|
||||
920L,
|
||||
RiverCourseType.SEA_CAVE,
|
||||
OptionalLong.empty(),
|
||||
OptionalLong.empty(),
|
||||
"water",
|
||||
1,
|
||||
List.of(),
|
||||
List.of(chamber)
|
||||
);
|
||||
RiverFootprint footprint = new HydrologyFootprintCompiler(
|
||||
seaCaveSettings(),
|
||||
coast,
|
||||
request -> request.minimum()
|
||||
).compile(List.of(seaCave));
|
||||
HydrologyTerrainCaveVoxelView view = new HydrologyTerrainCaveVoxelView(coast, 63, -64, 320);
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
|
||||
HydrologyCaveCourseFilter terrainFilter = new HydrologyCaveCourseFilter(
|
||||
view,
|
||||
new HydrologyCaveCourseFilter.Options(true, 32768, 32768)
|
||||
);
|
||||
HydrologyCaveCourseFilter.Result result = terrainFilter.filter(
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(seaCave),
|
||||
footprint.columns().values(),
|
||||
diagnostics
|
||||
);
|
||||
|
||||
assertEquals("diagnostics=" + diagnostics, List.of(seaCave), result.courses());
|
||||
assertTrue(diagnostics.isEmpty());
|
||||
assertEquals(1, result.cavePlans().size());
|
||||
HydrologyCavePlan plan = result.cavePlans().getFirst();
|
||||
assertTrue(plan.accepted());
|
||||
assertEquals(920L, plan.source().sourceId());
|
||||
AtomicInteger seaOpenings = new AtomicInteger();
|
||||
plan.forEachPrecondition((CavePosition position, CaveVoxelPrecondition precondition) -> {
|
||||
if (position.x() >= 112
|
||||
&& position.y() > 48
|
||||
&& position.y() <= 63
|
||||
&& precondition.voxel() == CaveVoxel.COMPATIBLE_FLUID) {
|
||||
seaOpenings.incrementAndGet();
|
||||
}
|
||||
});
|
||||
assertTrue(seaOpenings.get() > 0);
|
||||
plan.forEachAction((CavePosition position, HydrologyCaveAction action) -> {
|
||||
int naturalHeight = coast.sample(position.x(), position.z()).naturalHeight();
|
||||
assertTrue(
|
||||
"carve above the surface at " + position + " " + action,
|
||||
action == HydrologyCaveAction.SEAL_GUARD ? position.y() <= naturalHeight : position.y() < naturalHeight
|
||||
);
|
||||
});
|
||||
|
||||
ArrayList<HydrologyColumnSample> sealed = new ArrayList<>();
|
||||
for (HydrologyColumnSample sample : footprint.columns().values()) {
|
||||
List<HydrologyColumnLayer> kept = sample.layers().stream()
|
||||
.filter((HydrologyColumnLayer layer) -> !layer.oceanApron())
|
||||
.toList();
|
||||
if (!kept.isEmpty()) {
|
||||
sealed.add(new HydrologyColumnSample(
|
||||
sample.x(),
|
||||
sample.z(),
|
||||
sample.naturalHeight(),
|
||||
sample.seaLevel(),
|
||||
sample.ocean(),
|
||||
sample.parentBiomeKey(),
|
||||
kept
|
||||
));
|
||||
}
|
||||
}
|
||||
ArrayList<HydrologyDiagnosticCandidate> sealedDiagnostics = new ArrayList<>();
|
||||
HydrologyCaveCourseFilter.Result control = terrainFilter.filter(
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(seaCave),
|
||||
sealed,
|
||||
sealedDiagnostics
|
||||
);
|
||||
|
||||
assertTrue(control.courses().isEmpty());
|
||||
assertTrue(control.cavePlans().isEmpty());
|
||||
assertEquals(1, sealedDiagnostics.size());
|
||||
assertEquals(HydrologyCandidateRejection.CAVE_CONTAINMENT, sealedDiagnostics.getFirst().rejection());
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings seaCaveSettings() {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 32768),
|
||||
base.outlets().inlandGrotto(),
|
||||
base.outlets().surfaceSinkholesEnabled(),
|
||||
base.outlets().coastalCliffMinimumHeight(),
|
||||
base.outlets().mouthLevelingDistance(),
|
||||
8,
|
||||
base.outlets().maximumPerTile(),
|
||||
base.outlets().maximumPerTile()
|
||||
);
|
||||
HydrologyPlannerSettings.Geometry geometry = new HydrologyPlannerSettings.Geometry(
|
||||
base.geometry().meanders(),
|
||||
base.geometry().surface(),
|
||||
base.geometry().underground(),
|
||||
new HydrologyPlannerSettings.ChannelShape(2.4D, 0D, 0D, 11),
|
||||
base.geometry().drops()
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
base.routing(),
|
||||
base.surface(),
|
||||
base.hydraulics(),
|
||||
base.underground(),
|
||||
outlets,
|
||||
geometry,
|
||||
base.deepFluids(),
|
||||
base.surfacePools(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
private RiverCourse course(HydraulicSegment segment) {
|
||||
return new RiverCourse(
|
||||
segment.courseId(),
|
||||
|
||||
+271
-13
@@ -20,10 +20,10 @@ public class HydrologyFootprintCompilerTest {
|
||||
|
||||
/**
|
||||
* Published dry cells cardinally adjacent to owned water whose final terrain sits below the
|
||||
* water head plus the configured freeboard.
|
||||
* water head plus the configured sink.
|
||||
*/
|
||||
private static int unsupportedBankCells(RiverFootprint footprint, long courseId) {
|
||||
int freeboard = HydrologyPlannerSettings.defaults().surface().banks().freeboard();
|
||||
int sink = HydrologyPlannerSettings.defaults().surface().banks().sink();
|
||||
int unsupported = 0;
|
||||
for (HydrologyColumnSample sample : footprint.columns().values()) {
|
||||
HydrologyColumnLayer fluid = sample.primarySurfaceFluidLayer().orElse(null);
|
||||
@@ -40,7 +40,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
if (neighbor == null || neighbor.primarySurfaceFluidLayer().isPresent()) {
|
||||
continue;
|
||||
}
|
||||
if (neighbor.terrainHeight() < fluid.fluidHeadY() + freeboard) {
|
||||
if (neighbor.terrainHeight() < fluid.fluidHeadY() + sink) {
|
||||
unsupported++;
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceChannelAtTerrainLevelHasNoContainingBank() {
|
||||
public void surfaceChannelAtTerrainLevelIsHeldByTheGroundBesideIt() {
|
||||
HydraulicSegment segment = new HydraulicSegment(
|
||||
15L,
|
||||
14L,
|
||||
@@ -231,7 +231,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
HydrologyFootprintCompiler compiler = compiler(terrain);
|
||||
RiverFootprint footprint = compiler.compile(List.of(course));
|
||||
|
||||
assertTrue(unsupportedBankCells(footprint, 14L) > 0);
|
||||
assertEquals(0, unsupportedBankCells(footprint, 14L));
|
||||
for (HydrologyColumnSample sample : footprint.columns().values()) {
|
||||
assertTrue(sample.terrainHeight() <= sample.naturalHeight());
|
||||
}
|
||||
@@ -268,7 +268,9 @@ public class HydrologyFootprintCompilerTest {
|
||||
base.underground(),
|
||||
base.outlets(),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(deepFluid), List.of()
|
||||
List.of(deepFluid), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
HydraulicSegment segment = new HydraulicSegment(
|
||||
15L,
|
||||
@@ -717,7 +719,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(32, 40, 0))
|
||||
List.of(new HydrologyPoint(96, 40, 0))
|
||||
)
|
||||
);
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
@@ -1434,13 +1436,13 @@ public class HydrologyFootprintCompilerTest {
|
||||
if (neighbor.primarySurfaceFluidLayer().isPresent()) {
|
||||
continue;
|
||||
}
|
||||
assertTrue(neighbor.terrainHeight() > fluid.fluidHeadY());
|
||||
assertTrue(neighbor.terrainHeight() >= fluid.fluidHeadY());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedWetEdgeIsReportedWithoutRaisingItsNaturalBank() {
|
||||
public void aOneBlockLowBankIsLippedAndADeeperOneIsReportedWithoutRaising() {
|
||||
HydraulicSegment channel = new HydraulicSegment(
|
||||
529L,
|
||||
528L,
|
||||
@@ -1464,21 +1466,33 @@ public class HydrologyFootprintCompilerTest {
|
||||
}
|
||||
int lowBankZ = bankZ;
|
||||
HydrologyTerrainSampler lowBankTerrain = (int x, int z) -> HydrologyTerrainSample.openLand(
|
||||
x == 12 && z == lowBankZ ? 70 : 82,
|
||||
x == 12 && z == lowBankZ ? 69 : 82,
|
||||
0D,
|
||||
"parent"
|
||||
);
|
||||
HydrologyFootprintCompiler lowBankCompiler = compiler(lowBankTerrain);
|
||||
RiverFootprint lowBankFootprint = lowBankCompiler.compile(List.of(course));
|
||||
HydrologyColumnSample lowBank = lowBankFootprint.sample(12, lowBankZ).orElseThrow();
|
||||
HydrologyTerrainSampler deepBankTerrain = (int x, int z) -> HydrologyTerrainSample.openLand(
|
||||
x == 12 && z == lowBankZ ? 68 : 82,
|
||||
0D,
|
||||
"parent"
|
||||
);
|
||||
RiverFootprint deepBankFootprint = compiler(deepBankTerrain).compile(List.of(course));
|
||||
HydrologyColumnSample deepBank = deepBankFootprint.sample(12, lowBankZ).orElseThrow();
|
||||
|
||||
assertEquals(0, unsupportedBankCells(highFootprint, 528L));
|
||||
assertEquals(70, lowBank.naturalHeight());
|
||||
// One block short of the water: a one-block lip holds it and nothing is reported.
|
||||
assertEquals(69, lowBank.naturalHeight());
|
||||
assertEquals(70, lowBank.terrainHeight());
|
||||
assertTrue(unsupportedBankCells(lowBankFootprint, 528L) > 0);
|
||||
assertEquals(0, unsupportedBankCells(lowBankFootprint, 528L));
|
||||
for (HydrologyColumnSample sample : lowBankFootprint.columns().values()) {
|
||||
assertTrue(sample.terrainHeight() <= sample.naturalHeight());
|
||||
assertTrue(sample.terrainHeight() <= sample.naturalHeight() + 1);
|
||||
}
|
||||
// Two blocks short: the bank keeps its natural height and the edge is reported.
|
||||
assertEquals(68, deepBank.naturalHeight());
|
||||
assertEquals(68, deepBank.terrainHeight());
|
||||
assertTrue(unsupportedBankCells(deepBankFootprint, 528L) > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1731,6 +1745,250 @@ public class HydrologyFootprintCompilerTest {
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void coastalGrottoApronReachesTheChamberRadius() {
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> x >= 112
|
||||
? HydrologyTerrainSample.ocean(48, "ocean")
|
||||
: HydrologyTerrainSample.openLand(80, 0D, "parent");
|
||||
RiverCourse seaCave = seaCaveCourse(900L);
|
||||
RiverFootprint footprint = new HydrologyFootprintCompiler(
|
||||
seaCaveSettings(8, 12),
|
||||
coast,
|
||||
request -> request.minimum()
|
||||
).compile(List.of(seaCave));
|
||||
RiverFootprint wideApron = new HydrologyFootprintCompiler(
|
||||
seaCaveSettings(12, 12),
|
||||
coast,
|
||||
request -> request.minimum()
|
||||
).compile(List.of(seaCave));
|
||||
|
||||
int chamberColumns = 0;
|
||||
int apronColumns = 0;
|
||||
double farthestApron = 0D;
|
||||
for (HydrologyColumnSample sample : footprint.columns().values()) {
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (layer.oceanApron()) {
|
||||
apronColumns++;
|
||||
assertTrue(sample.ocean());
|
||||
farthestApron = Math.max(farthestApron, StrictMath.hypot(sample.x() - 112, sample.z()));
|
||||
assertTrue(
|
||||
"apron beyond the chamber at " + sample.x() + "," + sample.z(),
|
||||
nearestCenterlineDistance(seaCave, sample.x(), sample.z()) <= 12.25D
|
||||
|| touchesChamber(footprint, sample.x(), sample.z())
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!chamberLayer(layer)) {
|
||||
continue;
|
||||
}
|
||||
chamberColumns++;
|
||||
assertFalse(sample.ocean());
|
||||
for (int[] cardinal : CARDINALS) {
|
||||
int neighborX = sample.x() + cardinal[0];
|
||||
int neighborZ = sample.z() + cardinal[1];
|
||||
if (!coast.sample(neighborX, neighborZ).ocean()) {
|
||||
continue;
|
||||
}
|
||||
assertTrue(
|
||||
"sea column " + neighborX + "," + neighborZ + " beside the chamber has no apron",
|
||||
apronAt(footprint, neighborX, neighborZ)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(chamberColumns > 0);
|
||||
assertTrue(apronColumns > 0);
|
||||
assertTrue("farthest apron " + farthestApron, farthestApron > 8.25D);
|
||||
assertEquals(wideApron, footprint);
|
||||
|
||||
HydraulicSegment mouth = new HydraulicSegment(
|
||||
902L,
|
||||
901L,
|
||||
HydrologyFeatureType.MOUTH,
|
||||
63,
|
||||
63,
|
||||
8,
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(96, 63, 0), new HydrologyPoint(112, 63, 0))
|
||||
);
|
||||
RiverFootprint mouthFootprint = new HydrologyFootprintCompiler(
|
||||
seaCaveSettings(8, 12),
|
||||
coast,
|
||||
request -> request.minimum()
|
||||
).compile(List.of(course(901L, RiverCourseType.SURFACE, mouth)));
|
||||
assertTrue(mouthFootprint.sample(112, 0).isPresent());
|
||||
assertTrue(mouthFootprint.sample(112 + 8, 0).isEmpty());
|
||||
for (HydrologyColumnSample sample : mouthFootprint.columns().values()) {
|
||||
if (sample.ocean()) {
|
||||
assertTrue(sample.x() - 112 < 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaCaveChamberStaysUnderTheNaturalSurface() {
|
||||
HydrologyTerrainSampler coast = this::cliffCoast;
|
||||
RiverCourse seaCave = seaCaveCourse(910L);
|
||||
RiverFootprint footprint = new HydrologyFootprintCompiler(
|
||||
seaCaveSettings(8, 12),
|
||||
coast,
|
||||
request -> request.minimum()
|
||||
).compile(List.of(seaCave));
|
||||
|
||||
int chamberColumns = 0;
|
||||
for (HydrologyColumnSample sample : footprint.columns().values()) {
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (!chamberLayer(layer)) {
|
||||
continue;
|
||||
}
|
||||
chamberColumns++;
|
||||
String where = "chamber at " + sample.x() + "," + sample.z() + " " + layer;
|
||||
assertFalse(where, sample.ocean());
|
||||
assertTrue(where, sample.naturalHeight() != 64);
|
||||
assertEquals(where, 63, layer.fluidHeadY());
|
||||
assertTrue(where, layer.bedY() < 63);
|
||||
assertTrue(where, layer.ceilingY() >= 64);
|
||||
assertTrue(where, layer.ceilingY() <= sample.naturalHeight() - 1);
|
||||
for (int[] cardinal : CARDINALS) {
|
||||
HydrologyTerrainSample neighbor = coast.sample(sample.x() + cardinal[0], sample.z() + cardinal[1]);
|
||||
if (neighbor.ocean()) {
|
||||
continue;
|
||||
}
|
||||
assertTrue(where, layer.ceilingY() <= neighbor.naturalHeight() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(chamberColumns > 0);
|
||||
HydrologyColumnLayer deepRoof = chamberLayerAt(footprint, 100, 0);
|
||||
assertTrue(deepRoof != null);
|
||||
assertEquals(73, deepRoof.ceilingY());
|
||||
HydrologyColumnLayer lowRoof = chamberLayerAt(footprint, 108, 0);
|
||||
assertTrue(lowRoof != null);
|
||||
assertEquals(65, lowRoof.ceilingY());
|
||||
HydrologyColumnLayer face = chamberLayerAt(footprint, 111, 0);
|
||||
assertTrue(face != null);
|
||||
assertTrue(face.ceilingY() >= 64 && face.ceilingY() <= 73);
|
||||
assertTrue(chamberLayerAt(footprint, 111, 9) == null);
|
||||
assertTrue(chamberLayerAt(footprint, 100, 9) == null);
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample cliffCoast(int x, int z) {
|
||||
if (x >= 112) {
|
||||
return HydrologyTerrainSample.ocean(48, "ocean");
|
||||
}
|
||||
if (z >= 9) {
|
||||
return HydrologyTerrainSample.openLand(64, 0D, "beach");
|
||||
}
|
||||
if (x >= 110) {
|
||||
return HydrologyTerrainSample.openLand(74, 0D, "face");
|
||||
}
|
||||
if (x >= 104) {
|
||||
return HydrologyTerrainSample.openLand(66, 0D, "low");
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(92, 0D, "cliff");
|
||||
}
|
||||
|
||||
private RiverCourse seaCaveCourse(long id) {
|
||||
HydraulicSegment chamber = new HydraulicSegment(
|
||||
id + 1L,
|
||||
id,
|
||||
HydrologyFeatureType.COASTAL_GROTTO,
|
||||
63,
|
||||
63,
|
||||
4,
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
|
||||
);
|
||||
return new RiverCourse(
|
||||
id,
|
||||
RiverCourseType.SEA_CAVE,
|
||||
OptionalLong.empty(),
|
||||
OptionalLong.empty(),
|
||||
"water",
|
||||
1,
|
||||
List.of(),
|
||||
List.of(chamber)
|
||||
);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings seaCaveSettings(int maximumOceanApron, int chamberRadius) {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Grotto(true, chamberRadius, 7, 10, 32768),
|
||||
base.outlets().inlandGrotto(),
|
||||
base.outlets().surfaceSinkholesEnabled(),
|
||||
base.outlets().coastalCliffMinimumHeight(),
|
||||
base.outlets().mouthLevelingDistance(),
|
||||
maximumOceanApron,
|
||||
base.outlets().maximumPerTile(),
|
||||
base.outlets().maximumPerTile()
|
||||
);
|
||||
HydrologyPlannerSettings.Geometry geometry = new HydrologyPlannerSettings.Geometry(
|
||||
base.geometry().meanders(),
|
||||
base.geometry().surface(),
|
||||
base.geometry().underground(),
|
||||
new HydrologyPlannerSettings.ChannelShape(2.4D, 0D, 0D, 11),
|
||||
base.geometry().drops()
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
base.routing(),
|
||||
base.surface(),
|
||||
base.hydraulics(),
|
||||
base.underground(),
|
||||
outlets,
|
||||
geometry,
|
||||
base.deepFluids(),
|
||||
base.surfacePools(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean chamberLayer(HydrologyColumnLayer layer) {
|
||||
return layer.feature().type() == HydrologyFeatureType.COASTAL_GROTTO
|
||||
&& layer.terrainOwned()
|
||||
&& !layer.oceanApron();
|
||||
}
|
||||
|
||||
private HydrologyColumnLayer chamberLayerAt(RiverFootprint footprint, int x, int z) {
|
||||
return footprint.sample(x, z)
|
||||
.flatMap((HydrologyColumnSample sample) -> sample.layers().stream()
|
||||
.filter(HydrologyFootprintCompilerTest::chamberLayer)
|
||||
.findFirst())
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private boolean apronAt(RiverFootprint footprint, int x, int z) {
|
||||
return footprint.sample(x, z)
|
||||
.map((HydrologyColumnSample sample) -> sample.layers().stream().anyMatch(HydrologyColumnLayer::oceanApron))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private boolean touchesChamber(RiverFootprint footprint, int x, int z) {
|
||||
for (int[] cardinal : CARDINALS) {
|
||||
if (chamberLayerAt(footprint, x + cardinal[0], z + cardinal[1]) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private double nearestCenterlineDistance(RiverCourse course, int x, int z) {
|
||||
double nearest = Double.MAX_VALUE;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
nearest = Math.min(nearest, StrictMath.hypot(x - point.x(), z - point.z()));
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
private RiverCourse course(long id, RiverCourseType type, HydraulicSegment segment) {
|
||||
return new RiverCourse(
|
||||
id,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyForkJoinTest {
|
||||
@Test
|
||||
public void nestedFanOutCompletesOnASaturatedForkJoinPool() throws Exception {
|
||||
ForkJoinPool pool = new ForkJoinPool(1);
|
||||
try {
|
||||
List<Integer> results = pool.submit(() -> {
|
||||
List<Callable<Integer>> outer = new ArrayList<>();
|
||||
for (int index = 0; index < 3; index++) {
|
||||
int value = index;
|
||||
outer.add(() -> {
|
||||
List<Callable<Integer>> inner = List.of(() -> value * 10, () -> value * 10 + 1);
|
||||
List<Integer> nested = HydrologyForkJoin.invokeAll(inner, Runnable::run);
|
||||
return nested.get(0) + nested.get(1);
|
||||
});
|
||||
}
|
||||
return HydrologyForkJoin.invokeAll(outer, Runnable::run);
|
||||
}).get(10, TimeUnit.SECONDS);
|
||||
|
||||
assertEquals(List.of(1, 21, 41), results);
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outsideAPoolTasksRunOnTheFallbackExecutorInOrder() throws Exception {
|
||||
List<String> threads = Collections.synchronizedList(new ArrayList<>());
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2, runnable -> new Thread(runnable, "fallback-executor"));
|
||||
try {
|
||||
List<Callable<Integer>> tasks = new ArrayList<>();
|
||||
for (int index = 0; index < 4; index++) {
|
||||
int value = index;
|
||||
tasks.add(() -> {
|
||||
threads.add(Thread.currentThread().getName());
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
List<Integer> results = HydrologyForkJoin.invokeAll(tasks, executor);
|
||||
|
||||
assertEquals(List.of(0, 1, 2, 3), results);
|
||||
assertEquals(4, threads.size());
|
||||
assertTrue(threads.stream().allMatch("fallback-executor"::equals));
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withoutAFallbackTasksRunOnTheCaller() {
|
||||
String caller = Thread.currentThread().getName();
|
||||
List<Callable<String>> tasks = List.of(
|
||||
() -> Thread.currentThread().getName(),
|
||||
() -> Thread.currentThread().getName()
|
||||
);
|
||||
|
||||
assertEquals(List.of(caller, caller), HydrologyForkJoin.invokeAll(tasks, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aFailingTaskRethrowsItsExceptionOnEveryPath() throws Exception {
|
||||
List<Callable<Integer>> tasks = List.of(() -> 1, () -> {
|
||||
throw new IllegalStateException("planning failed");
|
||||
});
|
||||
|
||||
IllegalStateException inline = assertThrows(IllegalStateException.class, () -> HydrologyForkJoin.invokeAll(tasks, null));
|
||||
assertEquals("planning failed", inline.getMessage());
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
IllegalStateException pooled = assertThrows(IllegalStateException.class, () -> HydrologyForkJoin.invokeAll(tasks, executor));
|
||||
assertEquals("planning failed", pooled.getMessage());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
ForkJoinPool pool = new ForkJoinPool(1);
|
||||
try {
|
||||
String message = pool.submit(() -> {
|
||||
try {
|
||||
HydrologyForkJoin.invokeAll(tasks, Runnable::run);
|
||||
return "no failure";
|
||||
} catch (IllegalStateException failure) {
|
||||
return failure.getMessage();
|
||||
}
|
||||
}).get(10, TimeUnit.SECONDS);
|
||||
assertEquals("planning failed", message);
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,68 @@ public class HydrologyModelTest {
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaCaveCoursesAreIndependent() {
|
||||
HydraulicSegment chamber = new HydraulicSegment(
|
||||
1L,
|
||||
21L,
|
||||
HydrologyFeatureType.COASTAL_GROTTO,
|
||||
63,
|
||||
63,
|
||||
4,
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
|
||||
);
|
||||
|
||||
RiverCourse seaCave = new RiverCourse(
|
||||
21L,
|
||||
RiverCourseType.SEA_CAVE,
|
||||
OptionalLong.empty(),
|
||||
OptionalLong.empty(),
|
||||
"water",
|
||||
1,
|
||||
List.of(),
|
||||
List.of(chamber)
|
||||
);
|
||||
|
||||
assertEquals(RiverCourseType.SEA_CAVE, seaCave.type());
|
||||
assertTrue(seaCave.sourceNodeId().isEmpty());
|
||||
assertTrue(seaCave.outletId().isEmpty());
|
||||
assertFalse(seaCave.surfaceSinkholeContinuation());
|
||||
assertThrows(IllegalArgumentException.class, () -> new RiverCourse(
|
||||
21L,
|
||||
RiverCourseType.SEA_CAVE,
|
||||
OptionalLong.of(4L),
|
||||
OptionalLong.empty(),
|
||||
"water",
|
||||
1,
|
||||
List.of(),
|
||||
List.of(chamber)
|
||||
));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RiverCourse(
|
||||
21L,
|
||||
RiverCourseType.SEA_CAVE,
|
||||
OptionalLong.empty(),
|
||||
OptionalLong.of(5L),
|
||||
"water",
|
||||
1,
|
||||
List.of(),
|
||||
List.of(chamber)
|
||||
));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RiverCourse(
|
||||
21L,
|
||||
RiverCourseType.SEA_CAVE,
|
||||
OptionalLong.empty(),
|
||||
OptionalLong.empty(),
|
||||
"water",
|
||||
1,
|
||||
List.of(),
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oceanColumnsRejectOwnedOrElevatedWrites() {
|
||||
HydrologyFeatureRef feature = feature(HydrologyFeatureType.MOUTH, 7L, 63);
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* A 256-block tile whose western half is one region and whose eastern half is another. The only sea is
|
||||
* along the south edge of the eastern half, plus an optional pocket of sea in the south-western corner;
|
||||
* terrain falls to the south and slightly to the east, so rivers born in the west naturally want to
|
||||
* cross into the east.
|
||||
*/
|
||||
public class HydrologyPlannerConfinementTest {
|
||||
private static final HydrologyTileKey TILE = new HydrologyTileKey(0, 0);
|
||||
private static final int BOUNDARY = 128;
|
||||
private static final String WEST = "region:west";
|
||||
|
||||
@Test
|
||||
public void unconfinedWestSourcesMayDrainAcrossTheRegionBoundary() {
|
||||
HydrologyTile tile = new HydrologyPlanner(91L, settings(), terrain(false, true)).plan(TILE);
|
||||
|
||||
List<RiverCourse> westCourses = westCourses(tile);
|
||||
assertFalse("diagnostics=" + tile.diagnosticCandidates(), westCourses.isEmpty());
|
||||
assertTrue("no western course crossed into the east: " + describe(westCourses),
|
||||
westCourses.stream().anyMatch(course -> maximumX(course) >= BOUNDARY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void confinedWestSourcesStayWestAllTheWayToTheirOutlet() {
|
||||
HydrologyTile tile = new HydrologyPlanner(91L, settings(), terrain(true, true)).plan(TILE);
|
||||
|
||||
List<RiverCourse> westCourses = westCourses(tile);
|
||||
assertFalse("diagnostics=" + tile.diagnosticCandidates(), westCourses.isEmpty());
|
||||
for (RiverCourse course : westCourses) {
|
||||
assertTrue("western course left its region: " + describe(List.of(course)), maximumX(course) < BOUNDARY);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void confinedWestSourcesWithoutAnOutletInsideAreRejectedAndSayWhy() {
|
||||
HydrologyTile tile = new HydrologyPlanner(91L, settings(), terrain(true, false)).plan(TILE);
|
||||
|
||||
assertTrue(describe(westCourses(tile)), westCourses(tile).isEmpty());
|
||||
assertTrue("diagnostics=" + tile.diagnosticCandidates(), tile.diagnosticCandidates().stream()
|
||||
.anyMatch(candidate -> candidate.rejection() == HydrologyCandidateRejection.CONFINED_NO_OUTLET));
|
||||
}
|
||||
|
||||
private static List<RiverCourse> westCourses(HydrologyTile tile) {
|
||||
ArrayList<RiverCourse> courses = new ArrayList<>();
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (course.type() == RiverCourseType.SURFACE && sourceX(course) < BOUNDARY) {
|
||||
courses.add(course);
|
||||
}
|
||||
}
|
||||
return courses;
|
||||
}
|
||||
|
||||
private static int sourceX(RiverCourse course) {
|
||||
return course.segments().getFirst().centerline().getFirst().x();
|
||||
}
|
||||
|
||||
private static int maximumX(RiverCourse course) {
|
||||
int maximum = Integer.MIN_VALUE;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
maximum = Math.max(maximum, point.x());
|
||||
}
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
private static String describe(List<RiverCourse> courses) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
for (RiverCourse course : courses) {
|
||||
out.append(" course ").append(course.id()).append(" source x=").append(sourceX(course))
|
||||
.append(" max x=").append(maximumX(course));
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler terrain(boolean confineWest, boolean westPocket) {
|
||||
return (int x, int z) -> {
|
||||
String confines = confineWest && x < BOUNDARY ? WEST : null;
|
||||
boolean eastSea = x >= BOUNDARY && z < 24;
|
||||
boolean pocket = westPocket && x < 24 && z < 24;
|
||||
if (eastSea || pocket) {
|
||||
return sample(60, true, confines);
|
||||
}
|
||||
int height = (int) StrictMath.round(66D + z * 0.3D - x * 0.05D);
|
||||
return sample(height, false, confines);
|
||||
};
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample sample(int height, boolean ocean, String confines) {
|
||||
return new HydrologyTerrainSample(
|
||||
height, 1D, ocean, false, height - 32, height - 30,
|
||||
!ocean, !ocean, !ocean, false, false, false,
|
||||
0D, ocean ? 0D : 1D, 0D, 1D, 1D, 1D, 1D, 1D,
|
||||
"land", "land", "land", "land", "land", "land",
|
||||
List.of("default"), List.of(), Double.NaN, confines
|
||||
);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings settings() {
|
||||
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(true, 4D, 80, 1, 6, 32);
|
||||
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(false, 0D, Integer.MIN_VALUE, 0, 1, 48);
|
||||
HydrologyPlannerSettings.ChannelShape stableChannel = new HydrologyPlannerSettings.ChannelShape(2D, 0D, 0D, 11);
|
||||
return new HydrologyPlannerSettings(
|
||||
63,
|
||||
new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D, 1D, 0),
|
||||
new HydrologyPlannerSettings.Surface(true, surfaceSources, 4, 12, 2, 4, 12, 1.5D, HydrologyPlannerSettings.Banks.defaults()),
|
||||
new HydrologyPlannerSettings.Hydraulics(4),
|
||||
new HydrologyPlannerSettings.Underground(false, undergroundSources, 68, 82, 4, 14, 2, 5, 6, 12, false, 0),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||
false, 12, 48, 2, 8, 8),
|
||||
new HydrologyPlannerSettings.Geometry(
|
||||
new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D),
|
||||
stableChannel, stableChannel, stableChannel,
|
||||
HydrologyPlannerSettings.Geometry.defaults().drops()),
|
||||
List.of(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
}
|
||||
+57
-9
@@ -5,7 +5,10 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelPrecondition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveConflictPolicy;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -17,6 +20,7 @@ import java.util.Map;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
private static final long SEED = 77L;
|
||||
@@ -65,16 +69,46 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
assertEquals(forward.get(pair.first()).diagnosticCandidates().toString(), 1, firstSources.size());
|
||||
assertEquals(forward.get(pair.second()).diagnosticCandidates().toString(), 1, secondSources.size());
|
||||
assertPointNear(new HydrologyPoint(32, 121, -272), firstSources.getFirst(), 8);
|
||||
assertPointNear(new HydrologyPoint(16, 126, -192), secondSources.getFirst(), 8);
|
||||
// The second tile keeps the mouth on its own coast, so its heaviest required source no longer
|
||||
// competes with the first tile's network and is accepted ahead of the lighter fallback source.
|
||||
assertPointNear(new HydrologyPoint(16, 126, -224), secondSources.getFirst(), 8);
|
||||
assertNoIncompatibleCaveOverlaps(pair, forward.get(pair.first()), forward.get(pair.second()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundPlatformResolvesNeighbourOwnersInParallelToTheSameTiles() {
|
||||
List<HydrologyTileKey> corner = List.of(
|
||||
new HydrologyTileKey(-1, -1),
|
||||
new HydrologyTileKey(0, -1),
|
||||
new HydrologyTileKey(-1, 0),
|
||||
new HydrologyTileKey(0, 0)
|
||||
);
|
||||
Map<HydrologyTileKey, HydrologyTile> lazy = plans(planner(), corner);
|
||||
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
IrisPlatforms.unbind();
|
||||
IrisPlatforms.bind(mock(IrisPlatform.class));
|
||||
Map<HydrologyTileKey, HydrologyTile> parallel;
|
||||
try {
|
||||
parallel = plans(planner(), corner);
|
||||
} finally {
|
||||
IrisPlatforms.unbind();
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
|
||||
assertEquals(lazy, parallel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void colorRankBoundsTheCompleteOwnerDependencyGeometry() {
|
||||
HydrologyPlanner planner = planner();
|
||||
|
||||
assertEquals(1, planner.maximumCrossTileDependencyOwners(new HydrologyTileKey(0, 0)));
|
||||
assertEquals(49, planner.maximumCrossTileDependencyOwners(new HydrologyTileKey(-1, -1)));
|
||||
// The mouth flare reaches maximumWidth * flareRatio / 2 beside the coast, which pushes these
|
||||
// 256-block tiles to a three-colour period: rank 8 at (-1, -1), two tiles of offset per rank.
|
||||
assertEquals(3, settings().crossTileColorPeriod());
|
||||
assertEquals(1089, planner.maximumCrossTileDependencyOwners(new HydrologyTileKey(-1, -1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -275,7 +309,8 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
5,
|
||||
6,
|
||||
12,
|
||||
true
|
||||
true,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
@@ -285,10 +320,13 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
12,
|
||||
48,
|
||||
2,
|
||||
8,
|
||||
8
|
||||
),
|
||||
stableGeometry(),
|
||||
List.of(deepFluid), List.of()
|
||||
List.of(deepFluid), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -349,7 +387,8 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
baseUnderground.maximumDepth(),
|
||||
baseUnderground.minimumHeadroom(),
|
||||
baseUnderground.maximumHeadroom(),
|
||||
baseUnderground.connectToExistingCaves()
|
||||
baseUnderground.connectToExistingCaves(),
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
base.outlets().oceanEnabled(),
|
||||
@@ -359,10 +398,13 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
base.outlets().coastalCliffMinimumHeight(),
|
||||
base.outlets().mouthLevelingDistance(),
|
||||
base.outlets().maximumOceanApron(),
|
||||
base.outlets().maximumPerTile(),
|
||||
base.outlets().maximumPerTile()
|
||||
),
|
||||
base.geometry(),
|
||||
base.deepFluids(), List.of()
|
||||
base.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -404,7 +446,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
List.of("water"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -445,7 +489,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
sample.shoreBiomeKey(),
|
||||
sample.bankBiomeKey(),
|
||||
sample.floodedCaveBiomeKey(),
|
||||
sample.preferredProfileKeys(), List.of()
|
||||
sample.preferredProfileKeys(), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -478,7 +524,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
List.of("water"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+24
-9
@@ -667,8 +667,10 @@ public class HydrologyPlannerGlobalInvariantTest {
|
||||
0.5D,
|
||||
12D,
|
||||
0.5D,
|
||||
0.1D
|
||||
, 1D, 0),
|
||||
0.1D,
|
||||
1D,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
surfaceEnabled,
|
||||
surfaceSources,
|
||||
@@ -691,7 +693,8 @@ public class HydrologyPlannerGlobalInvariantTest {
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
false
|
||||
false,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
@@ -701,10 +704,13 @@ public class HydrologyPlannerGlobalInvariantTest {
|
||||
32,
|
||||
16,
|
||||
1,
|
||||
8,
|
||||
8
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
deepFluids, List.of()
|
||||
deepFluids, List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -748,11 +754,14 @@ public class HydrologyPlannerGlobalInvariantTest {
|
||||
underground.maximumDepth(),
|
||||
underground.minimumHeadroom(),
|
||||
underground.maximumHeadroom(),
|
||||
underground.connectToExistingCaves()
|
||||
underground.connectToExistingCaves(),
|
||||
0
|
||||
),
|
||||
base.outlets(),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
base.deepFluids(), List.of()
|
||||
base.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -945,7 +954,9 @@ public class HydrologyPlannerGlobalInvariantTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("default"), List.of()
|
||||
List.of("default"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -996,7 +1007,9 @@ public class HydrologyPlannerGlobalInvariantTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("default"), List.of()
|
||||
List.of("default"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1045,7 +1058,9 @@ public class HydrologyPlannerGlobalInvariantTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("default"), List.of()
|
||||
List.of("default"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyPlannerSeaCaveTest {
|
||||
private static final HydrologyTileKey TILE = new HydrologyTileKey(0, 0);
|
||||
private static final int SEA = 63;
|
||||
private static final HydrologyPlannerSettings.Grotto CHAMBER = new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 32768);
|
||||
|
||||
@Test
|
||||
public void seaCavesOpenFromTheSeaIntoTheCliffWithoutARiver() {
|
||||
HydrologyPlannerSettings.SeaCaves seaCaves = new HydrologyPlannerSettings.SeaCaves(true, 2, 64, 8, 12);
|
||||
HydrologyPlannerSettings settings = settings(128, 0D, CHAMBER, 12, seaCaves);
|
||||
HydrologyTerrainSampler cliffCoast = cliffCoast(112, 92);
|
||||
|
||||
HydrologyTile tile = new HydrologyPlanner(994L, settings, cliffCoast).plan(TILE);
|
||||
List<RiverCourse> caves = courses(tile, RiverCourseType.SEA_CAVE);
|
||||
|
||||
assertFalse("diagnostics=" + tile.diagnosticCandidates(), caves.isEmpty());
|
||||
for (RiverCourse cave : caves) {
|
||||
assertEquals(1, cave.segments().size());
|
||||
HydraulicSegment chamber = cave.segments().getFirst();
|
||||
assertEquals(HydrologyFeatureType.COASTAL_GROTTO, chamber.type());
|
||||
assertEquals(SEA, chamber.upstreamHeadY());
|
||||
assertEquals(SEA, chamber.downstreamHeadY());
|
||||
HydrologyPoint inner = chamber.start();
|
||||
HydrologyPoint connection = chamber.end();
|
||||
assertTrue(cliffCoast.sample(connection.x(), connection.z()).ocean());
|
||||
assertFalse(cliffCoast.sample(inner.x(), inner.z()).ocean());
|
||||
assertTrue("inner=" + inner + " connection=" + connection,
|
||||
inner.distanceSquared2D(connection) >= (long) seaCaves.depth() * seaCaves.depth());
|
||||
HydrologyCavePlan plan = tile.cavePlan(cave.id()).orElse(null);
|
||||
assertTrue("cave plan missing or rejected for " + cave.id() + " diagnostics=" + tile.diagnosticCandidates(),
|
||||
plan != null && plan.accepted());
|
||||
|
||||
int chamberColumns = 0;
|
||||
int apronColumns = 0;
|
||||
for (HydrologyColumnSample column : tile.footprint().columns().values()) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (layer.feature().courseId() != cave.id()) {
|
||||
continue;
|
||||
}
|
||||
assertEquals(HydrologyFeatureType.COASTAL_GROTTO, layer.feature().type());
|
||||
if (layer.oceanApron()) {
|
||||
apronColumns++;
|
||||
assertTrue(cliffCoast.sample(column.x(), column.z()).ocean());
|
||||
assertTrue(StrictMath.hypot(column.x() - connection.x(), column.z() - connection.z()) <= 12.25D);
|
||||
continue;
|
||||
}
|
||||
if (!layer.terrainOwned() || !layer.channel()) {
|
||||
continue;
|
||||
}
|
||||
chamberColumns++;
|
||||
assertFalse(cliffCoast.sample(column.x(), column.z()).ocean());
|
||||
assertTrue(layer.bedY() <= SEA);
|
||||
assertEquals(SEA, layer.fluidHeadY());
|
||||
assertTrue(layer.ceilingY() >= SEA);
|
||||
assertTrue(layer.ceilingY() <= column.naturalHeight() - 1);
|
||||
}
|
||||
}
|
||||
assertTrue("chamber=" + chamberColumns, chamberColumns >= 100);
|
||||
assertTrue("apron=" + apronColumns, apronColumns > 0);
|
||||
HydrologyColumnLayer center = tile.columnAt(inner.x(), inner.z()).orElseThrow().layers().stream()
|
||||
.filter((HydrologyColumnLayer layer) -> layer.feature().courseId() == cave.id() && layer.channel())
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
// The chamber walls carry the grotto roughness, so the centre sits within a few blocks of the ellipsoid.
|
||||
assertTrue("bed=" + center.bedY(), center.bedY() <= SEA - CHAMBER.verticalRadius() + 3
|
||||
&& center.bedY() >= SEA - CHAMBER.verticalRadius() - 3);
|
||||
assertTrue("ceiling=" + center.ceilingY(), center.ceilingY() <= SEA + CHAMBER.headroom()
|
||||
&& center.ceilingY() >= SEA + CHAMBER.headroom() - 3);
|
||||
}
|
||||
boolean featured = false;
|
||||
for (HydrologyFeatureRef feature : tile.features()) {
|
||||
featured |= feature.type() == HydrologyFeatureType.COASTAL_GROTTO;
|
||||
}
|
||||
assertTrue(featured);
|
||||
|
||||
HydrologyTile withoutCaves = new HydrologyPlanner(
|
||||
994L,
|
||||
settings(128, 0D, CHAMBER, 12, HydrologyPlannerSettings.SeaCaves.disabled()),
|
||||
cliffCoast
|
||||
).plan(TILE);
|
||||
assertTrue(courses(withoutCaves, RiverCourseType.SEA_CAVE).isEmpty());
|
||||
assertEquals(withoutCaves.outlets(), tile.outlets());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaCavesAreDeterministicAndOwnedByExactlyOneTile() {
|
||||
HydrologyPlannerSettings settings = settings(128, 0D, CHAMBER, 12,
|
||||
new HydrologyPlannerSettings.SeaCaves(true, 2, 64, 8, 12));
|
||||
HydrologyTerrainSampler cliffCoast = cliffCoast(112, 92);
|
||||
List<HydrologyTileKey> keys = List.of(
|
||||
TILE,
|
||||
new HydrologyTileKey(0, -1),
|
||||
new HydrologyTileKey(0, 1),
|
||||
new HydrologyTileKey(-1, 0),
|
||||
new HydrologyTileKey(1, 0)
|
||||
);
|
||||
HydrologyPlanner forward = new HydrologyPlanner(41L, settings, cliffCoast);
|
||||
HydrologyPlanner reversed = new HydrologyPlanner(41L, settings, cliffCoast);
|
||||
List<HydrologyTile> forwardTiles = new ArrayList<>();
|
||||
for (HydrologyTileKey key : keys) {
|
||||
forwardTiles.add(forward.plan(key));
|
||||
}
|
||||
List<HydrologyTile> reversedTiles = new ArrayList<>();
|
||||
for (HydrologyTileKey key : keys.reversed()) {
|
||||
reversedTiles.add(reversed.plan(key));
|
||||
}
|
||||
|
||||
for (int index = 0; index < keys.size(); index++) {
|
||||
assertEquals(keys.get(index).toString(), forwardTiles.get(index), reversedTiles.get(keys.size() - 1 - index));
|
||||
}
|
||||
List<RiverCourse> caves = courses(forwardTiles.getFirst(), RiverCourseType.SEA_CAVE);
|
||||
assertFalse(caves.isEmpty());
|
||||
Set<Long> ownedIds = new HashSet<>();
|
||||
for (RiverCourse cave : caves) {
|
||||
ownedIds.add(cave.id());
|
||||
}
|
||||
for (int index = 1; index < keys.size(); index++) {
|
||||
for (RiverCourse course : forwardTiles.get(index).courses()) {
|
||||
assertFalse(keys.get(index) + " republished " + course.id(), ownedIds.contains(course.id()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaCavesPreferTheSteeperCoastAndKeepTheirSpacing() {
|
||||
// The high stretch is 128 blocks of coast; the spacing leaves room for a second cave wherever the first lands.
|
||||
HydrologyPlannerSettings.SeaCaves seaCaves = new HydrologyPlannerSettings.SeaCaves(true, 2, 32, 4, 12);
|
||||
HydrologyPlannerSettings settings = settings(256, 0D, CHAMBER, 12, seaCaves);
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> x >= 240
|
||||
? HydrologyTerrainSample.ocean(54, "ocean")
|
||||
: HydrologyTerrainSample.openLand(Math.floorDiv(z, 128) % 2 == 0 ? 70 : 90, 2D, "land");
|
||||
|
||||
HydrologyTile tile = new HydrologyPlanner(17L, settings, coast).plan(TILE);
|
||||
List<RiverCourse> caves = courses(tile, RiverCourseType.SEA_CAVE);
|
||||
|
||||
assertEquals("diagnostics=" + tile.diagnosticCandidates(), 2, caves.size());
|
||||
for (RiverCourse cave : caves) {
|
||||
HydrologyPoint connection = cave.segments().getFirst().end();
|
||||
assertTrue("connection=" + connection + " diagnostics=" + tile.diagnosticCandidates(),
|
||||
connection.z() >= 128 && connection.z() < 256);
|
||||
}
|
||||
HydrologyPoint first = caves.get(0).segments().getFirst().end();
|
||||
HydrologyPoint second = caves.get(1).segments().getFirst().end();
|
||||
long minimum = seaCaves.minimumSpacing() - 2L;
|
||||
assertTrue(first + " vs " + second, first.distanceSquared2D(second) >= minimum * minimum);
|
||||
int lowCoastRejections = 0;
|
||||
for (HydrologyDiagnosticCandidate candidate : tile.diagnosticCandidates()) {
|
||||
if (candidate.kind() != HydrologyCandidateKind.OUTLET
|
||||
|| candidate.projectedType() != HydrologyFeatureType.COASTAL_GROTTO
|
||||
|| candidate.point().z() < 0 || candidate.point().z() >= 128) {
|
||||
continue;
|
||||
}
|
||||
lowCoastRejections++;
|
||||
assertEquals(candidate.toString(), HydrologyCandidateRejection.SOURCE_QUOTA, candidate.rejection());
|
||||
}
|
||||
assertTrue(lowCoastRejections > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaCavesKeepClearOfRiverMouthsAndLowCoasts() {
|
||||
HydrologyPlannerSettings settings = settings(128, 2D, CHAMBER, 12,
|
||||
new HydrologyPlannerSettings.SeaCaves(true, 4, 32, 8, 12));
|
||||
HydrologyTerrainSampler riverCoast = (int x, int z) -> x >= 112
|
||||
? HydrologyTerrainSample.ocean(54, "ocean")
|
||||
: land(92, x <= 16);
|
||||
|
||||
HydrologyTile tile = new HydrologyPlanner(994L, settings, riverCoast).plan(TILE);
|
||||
List<HydrologyPoint> outletPoints = new ArrayList<>();
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (course.type() == RiverCourseType.SEA_CAVE) {
|
||||
continue;
|
||||
}
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (segment.type() == HydrologyFeatureType.MOUTH
|
||||
|| segment.type() == HydrologyFeatureType.COASTAL_GROTTO
|
||||
|| segment.type() == HydrologyFeatureType.INLAND_GROTTO) {
|
||||
outletPoints.addAll(segment.centerline());
|
||||
}
|
||||
}
|
||||
}
|
||||
List<RiverCourse> caves = courses(tile, RiverCourseType.SEA_CAVE);
|
||||
|
||||
assertFalse("diagnostics=" + tile.diagnosticCandidates(), outletPoints.isEmpty());
|
||||
assertFalse("diagnostics=" + tile.diagnosticCandidates(), caves.isEmpty());
|
||||
long clearance = 2L * CHAMBER.horizontalRadius() + 12L;
|
||||
for (RiverCourse cave : caves) {
|
||||
for (HydrologyPoint point : cave.segments().getFirst().centerline()) {
|
||||
for (HydrologyPoint outlet : outletPoints) {
|
||||
assertTrue(point + " within " + clearance + " of " + outlet,
|
||||
point.distanceSquared2D(outlet) >= clearance * clearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyTile lowCoast = new HydrologyPlanner(
|
||||
994L,
|
||||
settings(128, 0D, CHAMBER, 12, new HydrologyPlannerSettings.SeaCaves(true, 4, 32, 8, 12)),
|
||||
cliffCoast(112, 66)
|
||||
).plan(TILE);
|
||||
assertTrue(courses(lowCoast, RiverCourseType.SEA_CAVE).isEmpty());
|
||||
assertTrue(lowCoast.diagnosticCandidates().toString(),
|
||||
hasDiagnostic(lowCoast, HydrologyCandidateRejection.SURFACE_HEAD_RANGE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaCaveChamberFitsTheVolumeCap() {
|
||||
HydrologyPlannerSettings settings = settings(128, 0D,
|
||||
new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 1000), 12,
|
||||
new HydrologyPlannerSettings.SeaCaves(true, 2, 64, 8, 12));
|
||||
|
||||
HydrologyTile tile = new HydrologyPlanner(994L, settings, cliffCoast(112, 92)).plan(TILE);
|
||||
|
||||
assertTrue(courses(tile, RiverCourseType.SEA_CAVE).isEmpty());
|
||||
assertTrue(tile.diagnosticCandidates().toString(),
|
||||
hasDiagnostic(tile, HydrologyCandidateRejection.VOLUME_LIMIT));
|
||||
}
|
||||
|
||||
private static boolean hasDiagnostic(HydrologyTile tile, HydrologyCandidateRejection rejection) {
|
||||
for (HydrologyDiagnosticCandidate candidate : tile.diagnosticCandidates()) {
|
||||
if (candidate.kind() == HydrologyCandidateKind.OUTLET
|
||||
&& candidate.projectedType() == HydrologyFeatureType.COASTAL_GROTTO
|
||||
&& candidate.rejection() == rejection) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<RiverCourse> courses(HydrologyTile tile, RiverCourseType type) {
|
||||
ArrayList<RiverCourse> selected = new ArrayList<>();
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (course.type() == type) {
|
||||
selected.add(course);
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler cliffCoast(int coastX, int height) {
|
||||
return (int x, int z) -> x >= coastX
|
||||
? HydrologyTerrainSample.ocean(54, "ocean")
|
||||
: HydrologyTerrainSample.openLand(height, 2D, "land");
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample land(int height, boolean surfaceSource) {
|
||||
return new HydrologyTerrainSample(
|
||||
height,
|
||||
2D,
|
||||
false,
|
||||
true,
|
||||
72,
|
||||
74,
|
||||
true,
|
||||
true,
|
||||
surfaceSource,
|
||||
surfaceSource,
|
||||
false,
|
||||
false,
|
||||
0D,
|
||||
surfaceSource ? 1D : 0D,
|
||||
0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"parent",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("beta", "alpha"),
|
||||
List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings settings(
|
||||
int tileSize,
|
||||
double surfaceDensity,
|
||||
HydrologyPlannerSettings.Grotto coastal,
|
||||
int maximumOceanApron,
|
||||
HydrologyPlannerSettings.SeaCaves seaCaves
|
||||
) {
|
||||
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
|
||||
true, surfaceDensity, 80, 0, 6, 24);
|
||||
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(
|
||||
true, 0D, Integer.MIN_VALUE, 0, 4, 32);
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
true, surfaceSources, 4, 18, 2, 4, 10, 1.5D, HydrologyPlannerSettings.Banks.defaults());
|
||||
return new HydrologyPlannerSettings(
|
||||
SEA,
|
||||
new HydrologyPlannerSettings.Routing(tileSize, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
|
||||
surface,
|
||||
new HydrologyPlannerSettings.Hydraulics(4),
|
||||
new HydrologyPlannerSettings.Underground(
|
||||
false, undergroundSources, 68, 82, 4, 12, 2, 4, 5, 9, true, 0),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
coastal,
|
||||
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||
false,
|
||||
12,
|
||||
32,
|
||||
maximumOceanApron,
|
||||
4,
|
||||
4
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
surface.shoreWidth(),
|
||||
seaCaves
|
||||
);
|
||||
}
|
||||
}
|
||||
+174
-17
@@ -62,8 +62,10 @@ public class HydrologyPlannerSettingsTest {
|
||||
base.underground(),
|
||||
base.outlets(),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(overlongChannel), List.of()
|
||||
));
|
||||
List.of(overlongChannel), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled())
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -78,8 +80,10 @@ public class HydrologyPlannerSettingsTest {
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
0D
|
||||
, 1D, 0);
|
||||
0D,
|
||||
1D,
|
||||
0
|
||||
);
|
||||
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0);
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
@@ -101,6 +105,7 @@ public class HydrologyPlannerSettingsTest {
|
||||
0,
|
||||
0,
|
||||
64,
|
||||
1,
|
||||
1
|
||||
);
|
||||
HydrologyPlannerSettings.DeepFluid deepFluid = new HydrologyPlannerSettings.DeepFluid(
|
||||
@@ -137,25 +142,77 @@ public class HydrologyPlannerSettingsTest {
|
||||
public void bankSettingsRejectValuesOutsideTheirBounds() {
|
||||
HydrologyPlannerSettings.Banks banks = HydrologyPlannerSettings.Banks.defaults();
|
||||
|
||||
assertEquals(1, banks.inset());
|
||||
assertEquals(1, banks.freeboard());
|
||||
assertEquals(0, banks.sink());
|
||||
assertTrue(banks.erosion().enabled());
|
||||
assertEquals(12, banks.erosion().smoothingRadius());
|
||||
assertTrue(banks.ponds().source().enabled());
|
||||
assertTrue(banks.ponds().terminal().enabled());
|
||||
assertEquals(3D, banks.blendSlope(), 0D);
|
||||
assertEquals(4, banks.minimumBlendWidth());
|
||||
assertEquals(32, banks.maximumBlendWidth());
|
||||
assertEquals(6, banks.waterfallMinimumDrop());
|
||||
assertTrue(banks.exposeCutStrata());
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(-1, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, -1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 0D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 40, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0.25D, 16, 2, 0, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0.25D, 16, 0, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 1.5D, 16, 2, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(-1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 0D, 4, 32, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 3D, 40, 32, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 3D, 4, 32, 0.25D, 16, 2, 0, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 3D, 4, 32, 0.25D, 16, 0, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 3D, 4, 32, 1.5D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0),
|
||||
1, 1, 1, 1, 0, 1D,
|
||||
null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, -1, 0.45D, 1D, 0.5D));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 1D, 1D, 0.5D));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 0D, 0.5D));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, -0.1D));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Pond(true, 0, 4, 3));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Pond(true, 5, 4, 3));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Pond(true, 4, 6, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Ponds(null, new HydrologyPlannerSettings.Pond(true, 4, 6, 3)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inletSettingsRejectValuesOutsideTheirBounds() {
|
||||
HydrologyPlannerSettings.Inlet inlet = HydrologyPlannerSettings.defaults().surface().banks().inlet();
|
||||
|
||||
assertEquals(64, inlet.length());
|
||||
assertEquals(3, inlet.depth());
|
||||
assertEquals(32, inlet.maximumIncision());
|
||||
assertEquals(new HydrologyPlannerSettings.Inlet(0, 0, 0), HydrologyPlannerSettings.Inlet.none());
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(-1, 3, 32));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(1025, 3, 32));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, -1, 32));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 65, 32));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, -1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, 513));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(
|
||||
0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, null, 2.5D, 24, true,
|
||||
HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
|
||||
new HydrologyPlannerSettings.Inlet(1024, 64, 512);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publicationRadiusGrowsWithTheMouthFlare() {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings.Banks banks = base.surface().banks();
|
||||
HydrologyPlannerSettings.Banks flared = new HydrologyPlannerSettings.Banks(
|
||||
banks.sink(), banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(), banks.roughness(),
|
||||
banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(), 4D, banks.inlet(),
|
||||
banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(), banks.erosion(), banks.ponds());
|
||||
HydrologyPlannerSettings.Surface surface = base.surface();
|
||||
HydrologyPlannerSettings wide = new HydrologyPlannerSettings(
|
||||
base.seaLevel(), base.routing(),
|
||||
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(),
|
||||
surface.maximumWidth(), surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(),
|
||||
surface.shoreWidth(), flared),
|
||||
base.hydraulics(), base.underground(), base.outlets(), base.geometry(), base.deepFluids(), base.surfacePools(), 0D, HydrologyPlannerSettings.SeaCaves.disabled());
|
||||
|
||||
assertEquals(1.6D, banks.mouthFlareRatio(), 0D);
|
||||
int widened = (int) StrictMath.ceil(surface.maximumWidth() * (4D - banks.mouthFlareRatio()) / 2D);
|
||||
assertTrue(wide.publicationRadius() - base.publicationRadius() >= widened - 1);
|
||||
assertTrue(wide.publicationRadius() > base.publicationRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -178,8 +235,9 @@ public class HydrologyPlannerSettingsTest {
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1
|
||||
));
|
||||
1,
|
||||
1)
|
||||
);
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
accepted,
|
||||
@@ -188,8 +246,33 @@ public class HydrologyPlannerSettingsTest {
|
||||
0,
|
||||
0,
|
||||
65,
|
||||
1
|
||||
));
|
||||
1,
|
||||
1)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coastalOutletBudgetIsBounded() {
|
||||
HydrologyPlannerSettings.Grotto grotto = new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 1);
|
||||
HydrologyPlannerSettings.Outlets separate = new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
grotto,
|
||||
grotto,
|
||||
true,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
0
|
||||
);
|
||||
|
||||
assertEquals(0, separate.maximumCoastalPerTile());
|
||||
assertEquals(256, new HydrologyPlannerSettings.Outlets(true, grotto, grotto, true, 0, 0, 1, 3, 256)
|
||||
.maximumCoastalPerTile());
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
|
||||
true, grotto, grotto, true, 0, 0, 1, 3, -1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
|
||||
true, grotto, grotto, true, 0, 0, 1, 3, 257));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -259,4 +342,78 @@ public class HydrologyPlannerSettingsTest {
|
||||
0
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaCaveBoundsAreEnforcedAndTheDefaultsPlanNone() {
|
||||
HydrologyPlannerSettings.SeaCaves seaCaves = new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 12);
|
||||
assertEquals(3, seaCaves.maximumPerTile());
|
||||
assertFalse(HydrologyPlannerSettings.SeaCaves.disabled().enabled());
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, -1, 160, 8, 12));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 65, 160, 8, 12));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 15, 8, 12));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 8193, 8, 12));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 0, 12));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 129, 12));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, -1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 129));
|
||||
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
assertEquals(HydrologyPlannerSettings.SeaCaves.disabled(), base.seaCaves());
|
||||
HydrologyPlannerSettings withCaves = new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
base.routing(),
|
||||
base.surface(),
|
||||
base.hydraulics(),
|
||||
base.underground(),
|
||||
base.outlets(),
|
||||
base.geometry(),
|
||||
base.deepFluids(),
|
||||
base.surfacePools(),
|
||||
base.widestShoreBiomeWidth(),
|
||||
seaCaves
|
||||
);
|
||||
assertEquals(seaCaves, withCaves.seaCaves());
|
||||
assertTrue(base.fingerprint() != withCaves.fingerprint());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaCavesAloneReachTheChamberAndItsSweepBeyondTheHalo() {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings.Source noSources = new HydrologyPlannerSettings.Source(false, 0D, 0, 0, 0, 0);
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
false, noSources, 4, 8, 2, 4, 10, 1.5D, HydrologyPlannerSettings.Banks.defaults());
|
||||
HydrologyPlannerSettings.Underground underground = new HydrologyPlannerSettings.Underground(
|
||||
false, noSources, -48, 72, 3, 8, 1, 3, 6, 14, true, 0);
|
||||
HydrologyPlannerSettings quiet = new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
base.routing(),
|
||||
surface,
|
||||
base.hydraulics(),
|
||||
underground,
|
||||
base.outlets(),
|
||||
base.geometry(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
HydrologyPlannerSettings withCaves = new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
base.routing(),
|
||||
surface,
|
||||
base.hydraulics(),
|
||||
underground,
|
||||
base.outlets(),
|
||||
base.geometry(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
surface.shoreWidth(),
|
||||
new HydrologyPlannerSettings.SeaCaves(true, 1, 64, 8, 12)
|
||||
);
|
||||
|
||||
int alignedHalo = Math.min(base.routing().maximumRouteLength(), base.routing().sampleSpacing() * 2)
|
||||
/ base.routing().sampleSpacing() * base.routing().sampleSpacing();
|
||||
assertEquals(0, quiet.publicationRadius());
|
||||
assertEquals(alignedHalo + base.outlets().coastalGrotto().horizontalRadius() + 12 + 1, withCaves.publicationRadius());
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -31,7 +31,7 @@ public class HydrologyPlannerSurfacePoolTest {
|
||||
assertEquals(HydrologyFeatureType.STANDING_POOL, segment.type());
|
||||
assertTrue(segment.width() >= 8 && segment.width() <= 12);
|
||||
assertTrue(segment.start().x() < 512);
|
||||
assertEquals(79, segment.upstreamHeadY());
|
||||
assertEquals(80, segment.upstreamHeadY());
|
||||
assertEquals(segment.upstreamHeadY(), segment.downstreamHeadY());
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class HydrologyPlannerSurfacePoolTest {
|
||||
if (layer.channel()) {
|
||||
wet++;
|
||||
assertEquals("lava_pool", layer.profileKey());
|
||||
assertEquals(79, layer.fluidHeadY());
|
||||
assertEquals(80, layer.fluidHeadY());
|
||||
assertTrue(layer.bedY() < layer.fluidHeadY());
|
||||
assertTrue(center.distanceSquared2D(new HydrologyPoint(column.x(), 0, column.z())) <= 8 * 8);
|
||||
} else if (layer.shore()) {
|
||||
@@ -113,7 +113,9 @@ public class HydrologyPlannerSurfacePoolTest {
|
||||
base.geometry(),
|
||||
List.of(),
|
||||
List.of(new HydrologyPlannerSettings.SurfacePool(
|
||||
"lava_pool", true, density, spacing, minimumRadius, maximumRadius, depth, 64, null))
|
||||
"lava_pool", true, density, spacing, minimumRadius, maximumRadius, depth, 64, null)),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +125,9 @@ public class HydrologyPlannerTest {
|
||||
configured.coastalCliffMinimumHeight(),
|
||||
0,
|
||||
configured.maximumOceanApron(),
|
||||
configured.maximumPerTile()
|
||||
));
|
||||
configured.maximumPerTile(),
|
||||
configured.maximumPerTile())
|
||||
);
|
||||
|
||||
HydrologyTile tile = planner(78L, settings, rollingCoast(112)).plan(TILE);
|
||||
|
||||
@@ -438,10 +439,10 @@ public class HydrologyPlannerTest {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
assertTrue(point.y() <= previousHead);
|
||||
assertTrue(previousHead - point.y() <= 1);
|
||||
assertTrue(rollingTerrain.sample(point.x() - 1, point.z()).naturalHeight() > point.y());
|
||||
assertTrue(rollingTerrain.sample(point.x() + 1, point.z()).naturalHeight() > point.y());
|
||||
assertTrue(rollingTerrain.sample(point.x(), point.z() - 1).naturalHeight() > point.y());
|
||||
assertTrue(rollingTerrain.sample(point.x(), point.z() + 1).naturalHeight() > point.y());
|
||||
assertTrue(rollingTerrain.sample(point.x() - 1, point.z()).naturalHeight() >= point.y());
|
||||
assertTrue(rollingTerrain.sample(point.x() + 1, point.z()).naturalHeight() >= point.y());
|
||||
assertTrue(rollingTerrain.sample(point.x(), point.z() - 1).naturalHeight() >= point.y());
|
||||
assertTrue(rollingTerrain.sample(point.x(), point.z() + 1).naturalHeight() >= point.y());
|
||||
previousHead = point.y();
|
||||
}
|
||||
assertEquals(segment.downstreamHeadY(), previousHead);
|
||||
@@ -514,7 +515,9 @@ public class HydrologyPlannerTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("alpha"), List.of()
|
||||
List.of("alpha"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
};
|
||||
HydrologyTile tile = new HydrologyPlanner(812L, settings, terrain, solidCaveView()).plan(TILE);
|
||||
@@ -588,7 +591,9 @@ public class HydrologyPlannerTest {
|
||||
base.underground(),
|
||||
base.outlets(),
|
||||
base.geometry(),
|
||||
base.deepFluids(), List.of()
|
||||
base.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
HydrologyTerrainSampler repeatedRidges = (int x, int z) -> {
|
||||
if (x >= 112) {
|
||||
@@ -756,8 +761,9 @@ public class HydrologyPlannerTest {
|
||||
configured.coastalCliffMinimumHeight(),
|
||||
configured.mouthLevelingDistance(),
|
||||
configured.maximumOceanApron(),
|
||||
1
|
||||
));
|
||||
1,
|
||||
1)
|
||||
);
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> {
|
||||
if (x >= 112) {
|
||||
return oceanTerrain();
|
||||
@@ -1122,7 +1128,9 @@ public class HydrologyPlannerTest {
|
||||
base.underground(),
|
||||
base.outlets(),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
base.deepFluids(), List.of()
|
||||
base.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
HydrologyPlannerSettings settings = withOutlets(shaped, new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
@@ -1132,8 +1140,9 @@ public class HydrologyPlannerTest {
|
||||
base.outlets().coastalCliffMinimumHeight(),
|
||||
base.outlets().mouthLevelingDistance(),
|
||||
base.outlets().maximumOceanApron(),
|
||||
1
|
||||
));
|
||||
1,
|
||||
1)
|
||||
);
|
||||
HydrologyTile tile = new HydrologyPlanner(642L, settings, optionalRollingCoast(112)).plan(TILE);
|
||||
|
||||
List<RiverCourse> courses = surfaceCourses(tile);
|
||||
@@ -1236,8 +1245,9 @@ public class HydrologyPlannerTest {
|
||||
12,
|
||||
base.outlets().mouthLevelingDistance(),
|
||||
base.outlets().maximumOceanApron(),
|
||||
base.outlets().maximumPerTile()
|
||||
));
|
||||
base.outlets().maximumPerTile(),
|
||||
base.outlets().maximumPerTile())
|
||||
);
|
||||
HydrologyTerrainSampler cliffCoast = (int x, int z) -> {
|
||||
if (x >= 112) {
|
||||
return oceanTerrain();
|
||||
@@ -1285,6 +1295,192 @@ public class HydrologyPlannerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coastalAndInlandOutletsAreBudgetedSeparately() {
|
||||
HydrologyPlannerSettings base = standardSettings(2D, 0D, true, true, List.of());
|
||||
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
base.outlets().coastalGrotto(),
|
||||
base.outlets().inlandGrotto(),
|
||||
base.outlets().surfaceSinkholesEnabled(),
|
||||
base.outlets().coastalCliffMinimumHeight(),
|
||||
base.outlets().mouthLevelingDistance(),
|
||||
base.outlets().maximumOceanApron(),
|
||||
1,
|
||||
1
|
||||
));
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> {
|
||||
if (x >= 112) {
|
||||
return oceanTerrain();
|
||||
}
|
||||
if (x == 48) {
|
||||
return blockedTerrain();
|
||||
}
|
||||
boolean source = (x == 0 || x == 80) && z == 0;
|
||||
return terrain(90 - Math.floorDiv(x, 16), 1D, false, true, source, source, false, false);
|
||||
};
|
||||
|
||||
HydrologyTile tile = new HydrologyPlanner(70143L, settings, terrain, solidCaveView()).plan(TILE);
|
||||
|
||||
assertEquals(
|
||||
"outlets=" + tile.outlets() + " diagnostics=" + tile.diagnosticCandidates(),
|
||||
1,
|
||||
tile.outlets().stream().filter(RiverOutlet::directOcean).count()
|
||||
);
|
||||
assertEquals(
|
||||
"outlets=" + tile.outlets() + " diagnostics=" + tile.diagnosticCandidates(),
|
||||
1,
|
||||
tile.outlets().stream().filter((RiverOutlet outlet) -> !outlet.directOcean()).count()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aCliffAndABeachOnTheSameCoastSelectAMouthAndACoastalGrotto() {
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> {
|
||||
if (x >= 112) {
|
||||
return oceanTerrain();
|
||||
}
|
||||
if (z < 64) {
|
||||
return terrain(92, 2D, false, true, x <= 16, true, false, false);
|
||||
}
|
||||
return terrain(66 + Math.floorDiv((96 - x) * 3, 16), 1D, false, false, x <= 16, true, false, false);
|
||||
};
|
||||
|
||||
HydrologyTile mixed = new HydrologyPlanner(995L, mixedCoastSettings(2), coast).plan(TILE);
|
||||
HydrologyTile single = new HydrologyPlanner(995L, mixedCoastSettings(1), coast).plan(TILE);
|
||||
|
||||
assertTrue("outlets=" + mixed.outlets(), mixed.outlets().stream().allMatch(RiverOutlet::directOcean));
|
||||
assertTrue("outlets=" + mixed.outlets(), mixed.outlets().stream()
|
||||
.anyMatch((RiverOutlet outlet) -> outlet.type() == HydrologyFeatureType.MOUTH));
|
||||
assertTrue("outlets=" + mixed.outlets(), mixed.outlets().stream()
|
||||
.anyMatch((RiverOutlet outlet) -> outlet.type() == HydrologyFeatureType.COASTAL_GROTTO));
|
||||
assertEquals("outlets=" + single.outlets(), 1, single.outlets().size());
|
||||
assertEquals(HydrologyFeatureType.MOUTH, single.outlets().getFirst().type());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ownedCoastOutranksHaloCoast() {
|
||||
HydrologyPlannerSettings settings = standardSettings(2D, 0D, true, false, List.of());
|
||||
HydrologyTerrainSampler twoCoasts = (int x, int z) -> {
|
||||
if (x >= 112 || x <= -32) {
|
||||
return oceanTerrain();
|
||||
}
|
||||
boolean source = x >= 48 && x <= 96;
|
||||
return terrain(88 - Math.floorDiv(x, 16), 1D, false, true, source, source, false, false);
|
||||
};
|
||||
|
||||
HydrologyTile tile = new HydrologyPlanner(883L, settings, twoCoasts).plan(TILE);
|
||||
|
||||
assertFalse("diagnostics=" + tile.diagnosticCandidates(), tile.outlets().isEmpty());
|
||||
assertFalse(surfaceCourses(tile).isEmpty());
|
||||
for (RiverOutlet outlet : tile.outlets()) {
|
||||
assertTrue(
|
||||
"outlet=" + outlet,
|
||||
TILE.contains(outlet.landwardPoint().x(), outlet.landwardPoint().z(), tile.tileSize())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundSeaLevelOutletsAreReportedAsOutletLevel() {
|
||||
HydrologyPlannerSettings base = standardSettings(2D, 1D, true, true, List.of());
|
||||
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
base.routing(),
|
||||
base.surface(),
|
||||
base.hydraulics(),
|
||||
new HydrologyPlannerSettings.Underground(
|
||||
true,
|
||||
base.underground().sources(),
|
||||
40,
|
||||
50,
|
||||
4,
|
||||
12,
|
||||
2,
|
||||
4,
|
||||
5,
|
||||
9,
|
||||
true,
|
||||
0
|
||||
),
|
||||
base.outlets(),
|
||||
base.geometry(),
|
||||
base.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> {
|
||||
if (x >= 112) {
|
||||
return oceanTerrain();
|
||||
}
|
||||
boolean source = x <= 16;
|
||||
return terrain(88 - Math.floorDiv(x, 16), 1D, false, true, source, source, source, false);
|
||||
};
|
||||
|
||||
HydrologyTile tile = new HydrologyPlanner(884L, settings, coast, solidCaveView()).plan(TILE);
|
||||
|
||||
List<HydrologyDiagnosticCandidate> level = tile.diagnosticCandidates().stream()
|
||||
.filter((HydrologyDiagnosticCandidate candidate) ->
|
||||
candidate.kind() == HydrologyCandidateKind.OUTLET
|
||||
&& candidate.rejection() == HydrologyCandidateRejection.OUTLET_LEVEL)
|
||||
.toList();
|
||||
assertFalse("diagnostics=" + tile.diagnosticCandidates(), level.isEmpty());
|
||||
for (HydrologyDiagnosticCandidate candidate : level) {
|
||||
assertTrue(
|
||||
candidate.toString(),
|
||||
candidate.projectedType() == HydrologyFeatureType.MOUTH
|
||||
|| candidate.projectedType() == HydrologyFeatureType.COASTAL_GROTTO
|
||||
);
|
||||
assertEquals(settings.seaLevel(), candidate.point().y());
|
||||
assertFalse(coast.sample(candidate.point().x(), candidate.point().z()).ocean());
|
||||
}
|
||||
assertEquals(level.size(), level.stream().map(HydrologyDiagnosticCandidate::id).distinct().count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallbackAlternatesCoastalAndInlandTrials() {
|
||||
// An island whose coast is walled by a berm between the lattice columns: every mouth course
|
||||
// needs a cut deeper than the channel may make, so the surface routing falls back. The coast
|
||||
// offers more fallback mouths than the trial budget, so a sinkhole only gets its trial when
|
||||
// the fallback alternates coastal and inland candidates.
|
||||
HydrologyPlannerSettings settings = standardSettings(1D, 0D, true, true, List.of());
|
||||
HydrologyTerrainSampler island = (int x, int z) -> {
|
||||
if (x < -16 || x > 144 || z < -16 || z > 144) {
|
||||
return oceanTerrain();
|
||||
}
|
||||
boolean berm = (x > -13 && x < -3) || (x > 131 && x < 141)
|
||||
|| (z > -13 && z < -3) || (z > 131 && z < 141);
|
||||
boolean source = x >= 32 && x <= 96 && z >= 32 && z <= 96;
|
||||
return terrain(berm ? 170 : 90, 1D, false, true, source, source, false, false);
|
||||
};
|
||||
|
||||
HydrologyTile tile = new HydrologyPlanner(885L, settings, island, solidCaveView()).plan(TILE);
|
||||
|
||||
assertFalse("diagnostics=" + tile.diagnosticCandidates(), surfaceCourses(tile).isEmpty());
|
||||
assertTrue(tile.diagnosticCandidates().stream().anyMatch((HydrologyDiagnosticCandidate candidate) ->
|
||||
candidate.rejection() == HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED));
|
||||
for (RiverCourse course : surfaceCourses(tile)) {
|
||||
RiverOutlet outlet = tile.outlet(course.outletId().orElseThrow()).orElseThrow();
|
||||
assertFalse("outlet=" + outlet, outlet.directOcean());
|
||||
assertTrue(course.surfaceSinkholeContinuation());
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyPlannerSettings mixedCoastSettings(int maximumCoastalPerTile) {
|
||||
HydrologyPlannerSettings base = standardSettings(2D, 0D, true, false, List.of());
|
||||
return withOutlets(base, new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
|
||||
base.outlets().inlandGrotto(),
|
||||
base.outlets().surfaceSinkholesEnabled(),
|
||||
12,
|
||||
base.outlets().mouthLevelingDistance(),
|
||||
base.outlets().maximumOceanApron(),
|
||||
base.outlets().maximumPerTile(),
|
||||
maximumCoastalPerTile
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredSurfaceRiverContinuesThroughOneContainedSinkholeCourse() {
|
||||
HydrologyPlannerSettings settings = standardSettings(0D, 0D, false, true, List.of());
|
||||
@@ -1355,8 +1551,9 @@ public class HydrologyPlannerTest {
|
||||
configured.coastalCliffMinimumHeight(),
|
||||
configured.mouthLevelingDistance(),
|
||||
configured.maximumOceanApron(),
|
||||
configured.maximumPerTile()
|
||||
));
|
||||
configured.maximumPerTile(),
|
||||
configured.maximumPerTile())
|
||||
);
|
||||
|
||||
HydrologyTile tile = new HydrologyPlanner(
|
||||
7013L,
|
||||
@@ -1408,7 +1605,9 @@ public class HydrologyPlannerTest {
|
||||
base.underground(),
|
||||
base.outlets(),
|
||||
base.geometry(),
|
||||
base.deepFluids(), List.of()
|
||||
base.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
HydrologyTerrainSampler terrain = (int x, int z) -> {
|
||||
if (x >= 112) {
|
||||
@@ -1756,7 +1955,8 @@ public class HydrologyPlannerTest {
|
||||
4,
|
||||
5,
|
||||
9,
|
||||
true
|
||||
true,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
oceanEnabled,
|
||||
@@ -1766,10 +1966,13 @@ public class HydrologyPlannerTest {
|
||||
12,
|
||||
32,
|
||||
2,
|
||||
4,
|
||||
4
|
||||
),
|
||||
stableGeometry(),
|
||||
deepFluids, List.of()
|
||||
deepFluids, List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1799,8 +2002,10 @@ public class HydrologyPlannerTest {
|
||||
1.5D,
|
||||
24D,
|
||||
2D,
|
||||
0.2D
|
||||
, 1D, 0),
|
||||
0.2D,
|
||||
1D,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Source(true, 8D, 80, 0, 8, 128),
|
||||
@@ -1821,10 +2026,13 @@ public class HydrologyPlannerTest {
|
||||
outlets.coastalCliffMinimumHeight(),
|
||||
48,
|
||||
outlets.maximumOceanApron(),
|
||||
1,
|
||||
1
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(), List.of()
|
||||
List.of(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1893,7 +2101,9 @@ public class HydrologyPlannerTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("alpha"), List.of()
|
||||
List.of("alpha"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1958,7 +2168,9 @@ public class HydrologyPlannerTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("alpha", "beta"), List.of()
|
||||
List.of("alpha", "beta"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1990,7 +2202,9 @@ public class HydrologyPlannerTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("alpha"), List.of()
|
||||
List.of("alpha"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2031,7 +2245,9 @@ public class HydrologyPlannerTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("beta", "alpha"), List.of()
|
||||
List.of("beta", "alpha"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2063,7 +2279,9 @@ public class HydrologyPlannerTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("alpha"), List.of()
|
||||
List.of("alpha"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2095,7 +2313,9 @@ public class HydrologyPlannerTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("alpha", "beta"), List.of()
|
||||
List.of("alpha", "beta"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2121,7 +2341,9 @@ public class HydrologyPlannerTest {
|
||||
settings.underground(),
|
||||
settings.outlets(),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
settings.deepFluids(), List.of()
|
||||
settings.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2146,11 +2368,14 @@ public class HydrologyPlannerTest {
|
||||
underground.maximumDepth(),
|
||||
underground.minimumHeadroom(),
|
||||
underground.maximumHeadroom(),
|
||||
underground.connectToExistingCaves()
|
||||
underground.connectToExistingCaves(),
|
||||
0
|
||||
),
|
||||
settings.outlets(),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
settings.deepFluids(), List.of()
|
||||
settings.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2176,11 +2401,14 @@ public class HydrologyPlannerTest {
|
||||
underground.maximumDepth(),
|
||||
underground.minimumHeadroom(),
|
||||
underground.maximumHeadroom(),
|
||||
underground.connectToExistingCaves()
|
||||
underground.connectToExistingCaves(),
|
||||
0
|
||||
),
|
||||
settings.outlets(),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
settings.deepFluids(), List.of()
|
||||
settings.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2205,11 +2433,14 @@ public class HydrologyPlannerTest {
|
||||
underground.maximumDepth(),
|
||||
underground.minimumHeadroom(),
|
||||
underground.maximumHeadroom(),
|
||||
underground.connectToExistingCaves()
|
||||
underground.connectToExistingCaves(),
|
||||
0
|
||||
),
|
||||
settings.outlets(),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
settings.deepFluids(), List.of()
|
||||
settings.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2269,7 +2500,9 @@ public class HydrologyPlannerTest {
|
||||
settings.underground(),
|
||||
outlets,
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
settings.deepFluids(), List.of()
|
||||
settings.deepFluids(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+270
-4
@@ -6,6 +6,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -56,6 +57,110 @@ public class HydrologyPlannerTributaryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theApproachIntoTheStemNeverRisesWhereTheGroundHoldsAStationUp() {
|
||||
// A level tributary tail at 120 hangs twenty blocks above its stem. The ground beside the junction
|
||||
// falls to the stem, but a knoll a few stations back stands too high for the channel to cut it
|
||||
// down: the stations over the knoll keep their head, and every station upstream of them stays at
|
||||
// least as high, so the graded reach still descends into the stem instead of rising over the knoll.
|
||||
HydrologyTerrainSampler terrain = (x, z) -> land(x >= 14 && x <= 17 ? 130 : x >= 18 ? 100 + (20 - x) * 4 : 121);
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlanner planner = new HydrologyPlanner(19L, settings, terrain);
|
||||
ArrayList<HydrologyPoint> centerline = new ArrayList<>();
|
||||
for (int x = 0; x <= 20; x++) {
|
||||
centerline.add(new HydrologyPoint(x, 120, 0));
|
||||
}
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
|
||||
1L, 7L, HydrologyFeatureType.SURFACE_POOL, 120, 120, 4, 2, false, false, List.copyOf(centerline))));
|
||||
|
||||
planner.levelApproach(segments, 100);
|
||||
|
||||
List<HydrologyPoint> graded = new ArrayList<>();
|
||||
int previousHead = Integer.MAX_VALUE;
|
||||
for (HydraulicSegment segment : segments) {
|
||||
assertTrue(segment.upstreamHeadY() <= previousHead);
|
||||
assertTrue(segment.upstreamHeadY() >= segment.downstreamHeadY());
|
||||
previousHead = segment.downstreamHeadY();
|
||||
graded.addAll(segment.centerline());
|
||||
}
|
||||
assertEquals(21, graded.size());
|
||||
assertEquals(100, graded.getLast().y());
|
||||
int previous = Integer.MAX_VALUE;
|
||||
for (HydrologyPoint point : graded) {
|
||||
assertTrue("head rises downstream at x=" + point.x(), point.y() <= previous);
|
||||
assertTrue("cut below the incision floor at x=" + point.x(),
|
||||
point.y() >= terrain.sample(point.x(), point.z()).naturalHeight() - settings.surface().maximumIncision());
|
||||
previous = point.y();
|
||||
}
|
||||
assertEquals(120, graded.get(14).y());
|
||||
assertEquals(120, graded.get(17).y());
|
||||
assertTrue(graded.get(18).y() < 120);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aLevelReachWithABumpStaysGradedOnceItsTailIsCutDown() {
|
||||
// A pool reach may carry stations above its heads. Cutting its tail down to the stem turns it
|
||||
// into a graded transition, which must descend monotonically: the bump is held at the head.
|
||||
HydrologyTerrainSampler terrain = (x, z) -> land(121);
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlanner planner = new HydrologyPlanner(23L, settings, terrain);
|
||||
ArrayList<HydrologyPoint> centerline = new ArrayList<>();
|
||||
for (int x = 0; x <= 12; x++) {
|
||||
centerline.add(new HydrologyPoint(x, x >= 4 && x <= 6 ? 122 : 120, 0));
|
||||
}
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
|
||||
1L, 7L, HydrologyFeatureType.SURFACE_POOL, 120, 120, 4, 2, false, false, List.copyOf(centerline))));
|
||||
|
||||
planner.levelApproach(segments, 112);
|
||||
|
||||
List<HydrologyPoint> graded = segments.getFirst().centerline();
|
||||
assertEquals(13, graded.size());
|
||||
assertEquals(112, graded.getLast().y());
|
||||
int previous = Integer.MAX_VALUE;
|
||||
for (HydrologyPoint point : graded) {
|
||||
assertTrue("head rises downstream at x=" + point.x(), point.y() <= previous);
|
||||
previous = point.y();
|
||||
}
|
||||
assertTrue("bump above the head at x=5", graded.get(5).y() <= 120);
|
||||
assertTrue(graded.getFirst().y() <= 120);
|
||||
assertTrue(segments.getFirst().upstreamHeadY() >= segments.getFirst().downstreamHeadY());
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample land(int height) {
|
||||
return new HydrologyTerrainSample(
|
||||
height,
|
||||
0D,
|
||||
false,
|
||||
false,
|
||||
height - 40,
|
||||
height - 38,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"land",
|
||||
"land",
|
||||
"land",
|
||||
"land",
|
||||
"land",
|
||||
"land",
|
||||
List.of("default"),
|
||||
List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private static int stations(RiverCourse course) {
|
||||
int stations = 0;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
@@ -114,19 +219,25 @@ public class HydrologyPlannerTributaryTest {
|
||||
"land",
|
||||
"land",
|
||||
List.of("default"),
|
||||
List.of()
|
||||
List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings settings(int tributaries) {
|
||||
return settings(tributaries, 4D, 4);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings settings(int tributaries, double density, int maximumPerTile) {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings.Routing routing = new HydrologyPlannerSettings.Routing(
|
||||
1024, 64, base.routing().maximumRouteNodes(), 4096, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, tributaries);
|
||||
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 4D, 0, 0, 4, 128);
|
||||
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, density, 0, 0, maximumPerTile, 128);
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
true, sources, 4, 8, 2, 4, 40, 1.5D, HydrologyPlannerSettings.Banks.defaults());
|
||||
HydrologyPlannerSettings.Underground underground = new HydrologyPlannerSettings.Underground(
|
||||
false, base.underground().sources(), -48, 72, 3, 8, 1, 3, 6, 14, true);
|
||||
false, base.underground().sources(), -48, 72, 3, 8, 1, 3, 6, 14, true, 0);
|
||||
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
base.outlets().coastalGrotto(),
|
||||
@@ -135,6 +246,7 @@ public class HydrologyPlannerTributaryTest {
|
||||
base.outlets().coastalCliffMinimumHeight(),
|
||||
base.outlets().mouthLevelingDistance(),
|
||||
base.outlets().maximumOceanApron(),
|
||||
1,
|
||||
1
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
@@ -146,7 +258,161 @@ public class HydrologyPlannerTributaryTest {
|
||||
outlets,
|
||||
base.geometry(),
|
||||
List.of(),
|
||||
List.of()
|
||||
List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
@Test
|
||||
public void aTributaryOwnsOnlyTheDrainageUpstreamOfItsJunctionAndMeetsTheStemWater() {
|
||||
HydrologyTerrainSampler terrain = HydrologyPlannerTributaryTest::valley;
|
||||
|
||||
HydrologyTile split = new HydrologyPlanner(19L, settings(1), terrain).plan(TILE);
|
||||
|
||||
List<RiverCourse> courses = surface(split);
|
||||
assertEquals(split.diagnosticCandidates().toString(), 2, courses.size());
|
||||
RiverCourse stem = stations(courses.get(0)) >= stations(courses.get(1)) ? courses.get(0) : courses.get(1);
|
||||
RiverCourse tributary = stem == courses.get(0) ? courses.get(1) : courses.get(0);
|
||||
java.util.Set<Long> stemEdges = new java.util.HashSet<>();
|
||||
for (DrainageEdge edge : stem.drainageEdges()) {
|
||||
stemEdges.add(edge.id());
|
||||
}
|
||||
assertFalse(tributary.drainageEdges().isEmpty());
|
||||
for (DrainageEdge edge : tributary.drainageEdges()) {
|
||||
assertFalse("tributary shares stem drainage " + edge.id(), stemEdges.contains(edge.id()));
|
||||
}
|
||||
HydrologyPoint confluence = tributary.segments().getLast().end();
|
||||
HydrologyPoint stemEnd = stem.segments().getLast().end();
|
||||
assertTrue("junction sits at the stem's mouth", confluence.x() != stemEnd.x() || confluence.z() != stemEnd.z());
|
||||
Integer stemHead = null;
|
||||
for (HydraulicSegment segment : stem.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
if (point.x() == confluence.x() && point.z() == confluence.z()) {
|
||||
stemHead = point.y();
|
||||
}
|
||||
}
|
||||
}
|
||||
assertNotNull("tributary ends on the stem", stemHead);
|
||||
assertEquals(stemHead.intValue(), tributary.segments().getLast().downstreamHeadY());
|
||||
for (HydrologyDiagnosticCandidate candidate : split.diagnosticCandidates()) {
|
||||
assertTrue(candidate.toString(), candidate.kind() != HydrologyCandidateKind.TRIBUTARY);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tributariesAreBudgetedOnTopOfTheSourceDensity() {
|
||||
HydrologyTerrainSampler terrain = HydrologyPlannerTributaryTest::valley;
|
||||
|
||||
HydrologyTile single = new HydrologyPlanner(19L, settings(0, 1D, 1), terrain).plan(TILE);
|
||||
HydrologyTile joined = new HydrologyPlanner(19L, settings(1, 1D, 1), terrain).plan(TILE);
|
||||
|
||||
assertEquals(single.diagnosticCandidates().toString(), 1, surface(single).size());
|
||||
assertEquals(joined.diagnosticCandidates().toString(), 2, surface(joined).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundSourcesJoinTheirStemWhereTheirPassagesMeet() {
|
||||
HydrologyTerrainSampler terrain = HydrologyPlannerTributaryTest::cavernousCoast;
|
||||
|
||||
HydrologyTile single = new HydrologyPlanner(23L, undergroundSettings(0), terrain).plan(TILE);
|
||||
HydrologyTile joined = new HydrologyPlanner(23L, undergroundSettings(1), terrain).plan(TILE);
|
||||
|
||||
assertEquals(single.diagnosticCandidates().toString(), 1, underground(single).size());
|
||||
List<RiverCourse> courses = underground(joined);
|
||||
assertEquals(joined.diagnosticCandidates().toString(), 2, courses.size());
|
||||
RiverCourse stem = stations(courses.get(0)) >= stations(courses.get(1)) ? courses.get(0) : courses.get(1);
|
||||
RiverCourse tributary = stem == courses.get(0) ? courses.get(1) : courses.get(0);
|
||||
assertEquals(stem.outletId(), tributary.outletId());
|
||||
HydrologyPoint junction = tributary.segments().getLast().end();
|
||||
Integer stemHead = null;
|
||||
for (HydraulicSegment segment : stem.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
if (point.x() == junction.x() && point.z() == junction.z()) {
|
||||
stemHead = point.y();
|
||||
}
|
||||
}
|
||||
}
|
||||
assertNotNull("underground tributary ends on its stem", stemHead);
|
||||
assertTrue(tributary.segments().getLast().downstreamHeadY() >= stemHead);
|
||||
java.util.Set<Long> stemEdges = new java.util.HashSet<>();
|
||||
for (DrainageEdge edge : stem.drainageEdges()) {
|
||||
stemEdges.add(edge.id());
|
||||
}
|
||||
for (DrainageEdge edge : tributary.drainageEdges()) {
|
||||
assertFalse(stemEdges.contains(edge.id()));
|
||||
}
|
||||
for (HydraulicSegment segment : tributary.segments()) {
|
||||
assertTrue(segment.upstreamHeadY() >= segment.downstreamHeadY());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundTributariesAreBounded() {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
org.junit.Assert.assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Underground(
|
||||
true, base.underground().sources(), -48, 72, 3, 8, 1, 3, 6, 14, true, 5));
|
||||
}
|
||||
|
||||
private static List<RiverCourse> underground(HydrologyTile tile) {
|
||||
List<RiverCourse> courses = new java.util.ArrayList<>();
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (course.type() == RiverCourseType.UNDERGROUND) {
|
||||
courses.add(course);
|
||||
}
|
||||
}
|
||||
return courses;
|
||||
}
|
||||
|
||||
/** A small coast with caves: two underground source zones at the west edge drain east into one sea outlet. */
|
||||
private static HydrologyTerrainSample cavernousCoast(int x, int z) {
|
||||
if (x >= 112) {
|
||||
return new HydrologyTerrainSample(
|
||||
54, 0D, true, false, 30, 32, false, false, false, false, false, false,
|
||||
0D, 0D, 0D, 1D, 1D, 1D, 1D, 1D,
|
||||
"ocean_parent", "surface", "mouth", "shore", "dry", "flooded",
|
||||
List.of("default"), List.of(), Double.NaN, null);
|
||||
}
|
||||
int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D);
|
||||
boolean source = x >= 0 && x <= 24 && (z >= 8 && z <= 40 || z >= 72 && z <= 104);
|
||||
return new HydrologyTerrainSample(
|
||||
height, 1D, false, true, 70, 74, true, true, false, false, source, false,
|
||||
0D, 0D, source ? 1D : 0D, 1D, 1D, 1D, 1D, 1D,
|
||||
"parent", "surface", "mouth", "shore", "dry", "flooded",
|
||||
List.of("default"), List.of(), Double.NaN, null);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings undergroundSettings(int tributaries) {
|
||||
HydrologyPlannerSettings.Source none = new HydrologyPlannerSettings.Source(false, 0D, 80, 0, 0, 24);
|
||||
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, Integer.MIN_VALUE, 0, 1, 32);
|
||||
HydrologyPlannerSettings.ChannelShape stableChannel = new HydrologyPlannerSettings.ChannelShape(2D, 0D, 0D, 11);
|
||||
return new HydrologyPlannerSettings(
|
||||
63,
|
||||
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
|
||||
new HydrologyPlannerSettings.Surface(false, none, 4, 18, 2, 4, 10, 1.5D, HydrologyPlannerSettings.Banks.defaults()),
|
||||
new HydrologyPlannerSettings.Hydraulics(4),
|
||||
new HydrologyPlannerSettings.Underground(true, sources, 68, 82, 4, 12, 2, 4, 5, 9, true, tributaries),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||
false,
|
||||
12,
|
||||
32,
|
||||
2,
|
||||
1,
|
||||
1
|
||||
),
|
||||
new HydrologyPlannerSettings.Geometry(
|
||||
new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D),
|
||||
stableChannel,
|
||||
stableChannel,
|
||||
stableChannel,
|
||||
HydrologyPlannerSettings.Geometry.defaults().drops()
|
||||
),
|
||||
List.of(),
|
||||
List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-5
@@ -89,7 +89,7 @@ public class HydrologyStyledGeometryTest {
|
||||
+ terrain(surfaceSource.naturalPoint().x(), surfaceSource.naturalPoint().z()).naturalHeight(),
|
||||
surfaceStart.upstreamHeadY() <=
|
||||
terrain(surfaceSource.naturalPoint().x(), surfaceSource.naturalPoint().z()).naturalHeight()
|
||||
- surfaceSettings.surface().banks().inset()
|
||||
- surfaceSettings.surface().banks().sink()
|
||||
);
|
||||
int widestSurfaceWidth = 0;
|
||||
for (HydraulicSegment segment : surface.segments()) {
|
||||
@@ -218,7 +218,8 @@ public class HydrologyStyledGeometryTest {
|
||||
4,
|
||||
5,
|
||||
9,
|
||||
true
|
||||
true,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
@@ -228,10 +229,13 @@ public class HydrologyStyledGeometryTest {
|
||||
12,
|
||||
32,
|
||||
2,
|
||||
4,
|
||||
4
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
deepFluids, List.of()
|
||||
deepFluids, List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,7 +268,9 @@ public class HydrologyStyledGeometryTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
List.of("water"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D);
|
||||
@@ -296,7 +302,9 @@ public class HydrologyStyledGeometryTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
List.of("water"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+39
-1
@@ -6,6 +6,9 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
@@ -77,7 +80,42 @@ public final class HydrologyTerrainSampleTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
profiles, List.of()
|
||||
profiles, List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
@Test
|
||||
public void shoreBiomeWidthInheritsWhenUnsetAndRejectsNegatives() {
|
||||
HydrologyTerrainSample unset = HydrologyTerrainSample.openLand(80, 0D, "parent");
|
||||
assertTrue(Double.isNaN(unset.shoreBiomeWidth()));
|
||||
assertEquals(1.5D, unset.shoreBiomeWidth(1.5D), 0D);
|
||||
|
||||
HydrologyTerrainSample wide = sample(List.of("default"), 12D, null);
|
||||
assertEquals(12D, wide.shoreBiomeWidth(1.5D), 0D);
|
||||
assertThrows(IllegalArgumentException.class, () -> sample(List.of("default"), -1D, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> sample(List.of("default"), Double.POSITIVE_INFINITY, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void confinesKeyIsNormalizedAndDecidesWhereWaterMayDrain() {
|
||||
HydrologyTerrainSample open = sample(List.of("default"), Double.NaN, " ");
|
||||
HydrologyTerrainSample west = sample(List.of("default"), Double.NaN, "region:west");
|
||||
HydrologyTerrainSample east = sample(List.of("default"), Double.NaN, "region:east");
|
||||
|
||||
assertNull(open.confinesKey());
|
||||
assertTrue(open.drainsInto(west));
|
||||
assertTrue(west.drainsInto(west));
|
||||
assertFalse(west.drainsInto(east));
|
||||
assertFalse(west.drainsInto(open));
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample sample(List<String> profiles, double shoreBiomeWidth, String confinesKey) {
|
||||
return new HydrologyTerrainSample(
|
||||
80, 0D, false, false, 48, 50, true, true, true, false, false, false,
|
||||
0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
|
||||
"parent", "parent", "parent", "parent", "parent", "parent",
|
||||
profiles, List.of(), shoreBiomeWidth, confinesKey
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
@@ -247,7 +248,7 @@ public class HydrologyTileCacheTest {
|
||||
new HydrologyPlannerSettings.Surface(false, disabled, 2, 4, 1, 2, 4, 1D,
|
||||
HydrologyPlannerSettings.Banks.defaults()),
|
||||
new HydrologyPlannerSettings.Hydraulics(3),
|
||||
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false),
|
||||
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
false,
|
||||
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
|
||||
@@ -256,10 +257,13 @@ public class HydrologyTileCacheTest {
|
||||
8,
|
||||
16,
|
||||
1,
|
||||
2,
|
||||
2
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(), List.of()
|
||||
List.of(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -285,7 +289,7 @@ public class HydrologyTileCacheTest {
|
||||
new HydrologyPlannerSettings.Routing(64, 16, 128, 96, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
|
||||
new HydrologyPlannerSettings.Surface(true, surfaceSources, 4, 8, 2, 3, 20, 1.5D, tileBoundedBanks()),
|
||||
new HydrologyPlannerSettings.Hydraulics(3),
|
||||
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false),
|
||||
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
|
||||
@@ -294,16 +298,20 @@ public class HydrologyTileCacheTest {
|
||||
8,
|
||||
16,
|
||||
1,
|
||||
2,
|
||||
2
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(), List.of()
|
||||
List.of(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
// Small-tile settings need a blend envelope that fits inside the bounded cross-tile admission period.
|
||||
private static HydrologyPlannerSettings.Banks tileBoundedBanks() {
|
||||
return new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 4, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true);
|
||||
return new HydrologyPlannerSettings.Banks(0, 3D, 4, 4, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true,
|
||||
HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults());
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample featureTerrain(int x, int z) {
|
||||
@@ -338,7 +346,9 @@ public class HydrologyTileCacheTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("default"), List.of()
|
||||
List.of("default"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -419,4 +429,234 @@ public class HydrologyTileCacheTest {
|
||||
assertEquals(new HydrologyTileKey(1, 1), order.getFirst());
|
||||
assertTrue(order.subList(0, 4).containsAll(List.of(new HydrologyTileKey(0, 0), new HydrologyTileKey(1, 0), new HydrologyTileKey(0, 1), new HydrologyTileKey(1, 1))));
|
||||
}
|
||||
@Test
|
||||
public void tileBatchesPlanMissingTilesConcurrentlyOnThePrefetchExecutor() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
AtomicInteger inFlight = new AtomicInteger();
|
||||
AtomicInteger peak = new AtomicInteger();
|
||||
CountDownLatch allStarted = new CountDownLatch(4);
|
||||
doAnswer(invocation -> {
|
||||
peak.accumulateAndGet(inFlight.incrementAndGet(), Math::max);
|
||||
allStarted.countDown();
|
||||
assertTrue("every batch member should plan at the same time", allStarted.await(5, TimeUnit.SECONDS));
|
||||
inFlight.decrementAndGet();
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
ExecutorService executor = Executors.newFixedThreadPool(4);
|
||||
try {
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, executor);
|
||||
List<HydrologyTileKey> keys = List.of(
|
||||
new HydrologyTileKey(0, 0),
|
||||
new HydrologyTileKey(1, 0),
|
||||
new HydrologyTileKey(2, 0),
|
||||
new HydrologyTileKey(3, 0)
|
||||
);
|
||||
|
||||
List<HydrologyTile> tiles = cache.tiles(keys);
|
||||
|
||||
assertEquals(4, tiles.size());
|
||||
assertEquals(4, peak.get());
|
||||
verify(planner, times(4)).plan(any(HydrologyTileKey.class));
|
||||
assertEquals(4, cache.size());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tileBatchesNeverKeepMoreThanHalfTheCacheBoundInFlight() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
AtomicInteger inFlight = new AtomicInteger();
|
||||
AtomicInteger peak = new AtomicInteger();
|
||||
List<HydrologyTileKey> planned = java.util.Collections.synchronizedList(new ArrayList<>());
|
||||
doAnswer(invocation -> {
|
||||
peak.accumulateAndGet(inFlight.incrementAndGet(), Math::max);
|
||||
Thread.sleep(20L);
|
||||
planned.add(invocation.getArgument(0));
|
||||
inFlight.decrementAndGet();
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 4, executor);
|
||||
ArrayList<HydrologyTileKey> keys = new ArrayList<>();
|
||||
for (int tileX = 0; tileX < 6; tileX++) {
|
||||
keys.add(new HydrologyTileKey(tileX, 0));
|
||||
}
|
||||
|
||||
List<HydrologyTile> tiles = cache.tiles(keys);
|
||||
|
||||
assertEquals(6, tiles.size());
|
||||
assertEquals(6, planned.size());
|
||||
assertTrue("in flight " + peak.get(), peak.get() <= 2);
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tileBatchesWithoutAPrefetchExecutorPlanInlineInKeyOrder() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
List<HydrologyTileKey> planned = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
planned.add(invocation.getArgument(0));
|
||||
return mock(HydrologyTile.class);
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 8);
|
||||
List<HydrologyTileKey> keys = List.of(new HydrologyTileKey(2, 1), new HydrologyTileKey(0, 0), new HydrologyTileKey(1, 1));
|
||||
|
||||
List<HydrologyTile> tiles = cache.tiles(keys);
|
||||
|
||||
assertEquals(keys, planned);
|
||||
assertEquals(3, tiles.size());
|
||||
assertSame(cache.get(keys.get(1)), tiles.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tileBatchesJoinAPlanAlreadyInFlightInsteadOfQueueingAnother() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicInteger plans = new AtomicInteger();
|
||||
doAnswer(invocation -> {
|
||||
plans.incrementAndGet();
|
||||
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
ExecutorService executor = Executors.newFixedThreadPool(4);
|
||||
try {
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, executor);
|
||||
HydrologyTileKey key = new HydrologyTileKey(5, 5);
|
||||
ExecutorService callers = Executors.newFixedThreadPool(3);
|
||||
try {
|
||||
List<Future<List<HydrologyTile>>> results = new ArrayList<>();
|
||||
for (int caller = 0; caller < 3; caller++) {
|
||||
results.add(callers.submit(() -> cache.tiles(List.of(key))));
|
||||
}
|
||||
Thread.sleep(100L);
|
||||
release.countDown();
|
||||
for (Future<List<HydrologyTile>> result : results) {
|
||||
assertSame(tile, result.get(5, TimeUnit.SECONDS).getFirst());
|
||||
}
|
||||
} finally {
|
||||
callers.shutdownNow();
|
||||
}
|
||||
assertEquals(1, plans.get());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void interruptedPlanningIsNotPublishedAsAnEmptyTile() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
AtomicInteger plans = new AtomicInteger();
|
||||
doAnswer(invocation -> {
|
||||
if (plans.incrementAndGet() == 1) {
|
||||
throw new IllegalStateException("Interrupted", new InterruptedException());
|
||||
}
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 8);
|
||||
HydrologyTileKey key = new HydrologyTileKey(3, 4);
|
||||
|
||||
IllegalStateException interrupted = org.junit.Assert.assertThrows(IllegalStateException.class, () -> cache.get(key));
|
||||
|
||||
assertEquals("Interrupted", interrupted.getMessage());
|
||||
assertEquals(0, cache.size());
|
||||
assertSame(tile, cache.get(key));
|
||||
assertEquals(2, plans.get());
|
||||
verify(planner, org.mockito.Mockito.never()).emptyTile(any(HydrologyTileKey.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directTileRequestsPlanOnThePrefetchExecutorInsteadOfTheCaller() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
List<String> planningThreads = java.util.Collections.synchronizedList(new ArrayList<>());
|
||||
doAnswer(invocation -> {
|
||||
planningThreads.add(Thread.currentThread().getName());
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "hydrology-planning-executor"));
|
||||
try {
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 8, executor);
|
||||
|
||||
assertSame(tile, cache.get(new HydrologyTileKey(7, 7)));
|
||||
|
||||
assertEquals(List.of("hydrology-planning-executor"), planningThreads);
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void forkJoinWorkersPlanOnTheirOwnThreadInsteadOfTheExecutor() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
List<String> planningThreads = java.util.Collections.synchronizedList(new ArrayList<>());
|
||||
doAnswer(invocation -> {
|
||||
planningThreads.add(Thread.currentThread().getName());
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "hydrology-planning-executor"));
|
||||
java.util.concurrent.ForkJoinPool pool = new java.util.concurrent.ForkJoinPool(1);
|
||||
try {
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 8, executor);
|
||||
|
||||
HydrologyTile planned = pool.submit(() -> cache.get(new HydrologyTileKey(9, 9))).get(5, TimeUnit.SECONDS);
|
||||
List<HydrologyTile> batch = pool.submit(() -> cache.tiles(List.of(new HydrologyTileKey(10, 9), new HydrologyTileKey(11, 9)))).get(5, TimeUnit.SECONDS);
|
||||
|
||||
assertSame(tile, planned);
|
||||
assertEquals(2, batch.size());
|
||||
assertEquals(3, planningThreads.size());
|
||||
assertTrue(planningThreads.toString(), planningThreads.stream().allMatch(name -> name.startsWith("ForkJoinPool-")));
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void plannedCheckNeverPlansItselfButAsksThePrefetchExecutorForMissingTiles() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||
List<Runnable> queued = new ArrayList<>();
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add);
|
||||
|
||||
assertFalse(cache.isPlanned(40, 40));
|
||||
assertFalse(queued.isEmpty());
|
||||
verify(planner, org.mockito.Mockito.never()).plan(any(HydrologyTileKey.class));
|
||||
|
||||
for (Runnable task : List.copyOf(queued)) {
|
||||
task.run();
|
||||
}
|
||||
|
||||
assertTrue(cache.isPlanned(40, 40));
|
||||
assertTrue(cache.columnAt(40, 40).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannedCheckWithoutAnExecutorReportsTheCacheOnly() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64);
|
||||
|
||||
assertFalse(cache.isPlanned(40, 40));
|
||||
cache.columnAt(40, 40);
|
||||
assertTrue(cache.isPlanned(40, 40));
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -13,6 +13,7 @@ import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -150,4 +151,29 @@ public class RiverPolicyResolverTest {
|
||||
private EffectiveRiverPolicy resolvePlacement(IrisRiverPlacementMode placement) {
|
||||
return RiverPolicyResolver.resolve(new IrisRiverPolicy().setPlacement(placement), null, null);
|
||||
}
|
||||
@Test
|
||||
public void shoreBiomeWidthInheritsFromDimensionThroughRegionToBiome() {
|
||||
IrisRiverPolicy region = new IrisRiverPolicy().setShoreBiomeWidth(6D);
|
||||
IrisRiverPolicy biome = new IrisRiverPolicy().setShoreBiomeWidth(0D);
|
||||
|
||||
assertNull(RiverPolicyResolver.resolve((IrisRiverPolicy) null, null, null).shoreBiomeWidth());
|
||||
assertEquals(6D, RiverPolicyResolver.resolve(null, region, null).shoreBiomeWidth(), 0D);
|
||||
assertEquals(6D, RiverPolicyResolver.resolve(null, region, new IrisRiverPolicy()).shoreBiomeWidth(), 0D);
|
||||
assertEquals(0D, RiverPolicyResolver.resolve(null, region, biome).shoreBiomeWidth(), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void confinementScopeIsTheLevelThatConfinedTheRiver() {
|
||||
IrisRiverPolicy confined = new IrisRiverPolicy().setConfined(true);
|
||||
IrisRiverPolicy released = new IrisRiverPolicy().setConfined(false);
|
||||
|
||||
assertEquals(RiverConfinement.NONE, RiverPolicyResolver.resolve((IrisRiverPolicy) null, null, null).confinement());
|
||||
assertEquals(RiverConfinement.REGION, RiverPolicyResolver.resolve(confined, null, null).confinement());
|
||||
assertEquals(RiverConfinement.REGION, RiverPolicyResolver.resolve(null, confined, null).confinement());
|
||||
assertEquals(RiverConfinement.REGION, RiverPolicyResolver.resolve(null, confined, new IrisRiverPolicy()).confinement());
|
||||
assertEquals(RiverConfinement.BIOME, RiverPolicyResolver.resolve(null, null, confined).confinement());
|
||||
assertEquals(RiverConfinement.BIOME, RiverPolicyResolver.resolve(null, confined, confined).confinement());
|
||||
assertEquals(RiverConfinement.NONE, RiverPolicyResolver.resolve(null, confined, released).confinement());
|
||||
assertEquals(RiverConfinement.NONE, RiverPolicyResolver.resolve(confined, released, null).confinement());
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -440,7 +440,7 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void burstWorkerUsesAvailableParallelRows() throws Exception {
|
||||
public void burstWorkerForksRowsIntoItsOwnPoolInsteadOfTheExecutor() throws Exception {
|
||||
AtomicInteger submissions = new AtomicInteger();
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
MultiBurst burst = new MultiBurst("Iris Hydrology Test", Thread.NORM_PRIORITY, () -> 2);
|
||||
@@ -471,7 +471,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
|
||||
);
|
||||
completion.get(5L, TimeUnit.SECONDS);
|
||||
|
||||
assertTrue(submissions.get() > 1);
|
||||
// Handing rows to another pool from a pool worker is how saturated pools deadlock each other.
|
||||
assertEquals(0, submissions.get());
|
||||
assertEquals((width + 1) * (width + 1), heightCalls.get());
|
||||
} finally {
|
||||
burst.close();
|
||||
|
||||
+68
-16
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.engine.hydrology.runtime;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.engine.object.IrisCoastalRiverGrottoConfig;
|
||||
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRiverHydrology;
|
||||
@@ -43,12 +44,12 @@ public class IrisHydrologyRuntimeSettingsTest {
|
||||
rivers.getGrottos().getInland().setEnabled(true).setConnectSurfaceRivers(true);
|
||||
rivers.getRouting().getInlandOutlets().add(IrisRiverInlandOutlet.SINKHOLE_GROTTO);
|
||||
|
||||
assertTrue(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
assertTrue(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
|
||||
.outlets().surfaceSinkholesEnabled());
|
||||
|
||||
rivers.getRouting().getInlandOutlets().clear();
|
||||
|
||||
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
|
||||
.outlets().surfaceSinkholesEnabled());
|
||||
}
|
||||
|
||||
@@ -63,13 +64,13 @@ public class IrisHydrologyRuntimeSettingsTest {
|
||||
rivers.getUnderground().getSources().setDensity(4.5D);
|
||||
rivers.getRouting().setSampleSpacing(64);
|
||||
|
||||
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
|
||||
.surface().sources().maximumPerTile());
|
||||
assertEquals(384, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
assertEquals(384, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
|
||||
.surface().sources().minimumSpacing());
|
||||
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
|
||||
.underground().sources().maximumPerTile());
|
||||
assertEquals(512, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
assertEquals(512, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
|
||||
.underground().sources().minimumSpacing());
|
||||
}
|
||||
|
||||
@@ -77,16 +78,23 @@ public class IrisHydrologyRuntimeSettingsTest {
|
||||
public void surfaceBankAndFlowConfigurationMapsIntoPlannerSettings() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
IrisRiverHydrology rivers = dimension.getHydrology().getRivers();
|
||||
rivers.getSurface().getChannel().setInset(2).setMaximumIncision(12);
|
||||
rivers.getSurface().getChannel().setSink(2).setMaximumIncision(12);
|
||||
rivers.getSurface().getErosion()
|
||||
.setEnabled(false)
|
||||
.setSmoothingRadius(6)
|
||||
.setThalwegFraction(0.6D)
|
||||
.setBlendCurve(1.5D)
|
||||
.setBedNoise(0.25D);
|
||||
rivers.getSurface().getPonds().getSource().setEnabled(false).setMinimumRadius(3).setMaximumRadius(4).setDepth(2);
|
||||
rivers.getSurface().getPonds().getTerminal().setMinimumRadius(7).setMaximumRadius(11).setDepth(5);
|
||||
rivers.getSurface().getBanks()
|
||||
.setFreeboard(2)
|
||||
.setShoreWidth(2.5D)
|
||||
.setBlendSlope(4D)
|
||||
.setMinimumBlendWidth(6)
|
||||
.setMaximumBlendWidth(48)
|
||||
.setExposeCutStrata(false);
|
||||
rivers.getSurface().getFlow().setCascadeRun(3).setWaterfallMinimumDrop(8);
|
||||
rivers.getSurface().getMouths().setFlareRatio(2.2D);
|
||||
rivers.getSurface().getMouths().setFlareRatio(2.2D).setInletLength(96).setInletDepth(5).setMaximumIncision(40);
|
||||
rivers.getUnderground().setMouthLevelingDistance(96);
|
||||
rivers.getRouting()
|
||||
.setSampleSpacing(32)
|
||||
@@ -97,17 +105,30 @@ public class IrisHydrologyRuntimeSettingsTest {
|
||||
.setSlopePenalty(3D)
|
||||
.setConfluenceAttraction(0.4D);
|
||||
|
||||
HydrologyPlannerSettings settings = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology());
|
||||
HydrologyPlannerSettings settings = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null);
|
||||
HydrologyPlannerSettings.Banks banks = settings.surface().banks();
|
||||
|
||||
assertEquals(2, banks.inset());
|
||||
assertEquals(2, banks.freeboard());
|
||||
assertEquals(2, banks.sink());
|
||||
assertFalse(banks.erosion().enabled());
|
||||
assertEquals(6, banks.erosion().smoothingRadius());
|
||||
assertEquals(0.6D, banks.erosion().thalwegFraction(), 0D);
|
||||
assertEquals(1.5D, banks.erosion().blendCurve(), 0D);
|
||||
assertEquals(0.25D, banks.erosion().bedNoise(), 0D);
|
||||
assertFalse(banks.ponds().source().enabled());
|
||||
assertEquals(3, banks.ponds().source().minimumRadius());
|
||||
assertEquals(4, banks.ponds().source().maximumRadius());
|
||||
assertEquals(2, banks.ponds().source().depth());
|
||||
assertTrue(banks.ponds().terminal().enabled());
|
||||
assertEquals(7, banks.ponds().terminal().minimumRadius());
|
||||
assertEquals(11, banks.ponds().terminal().maximumRadius());
|
||||
assertEquals(5, banks.ponds().terminal().depth());
|
||||
assertEquals(4D, banks.blendSlope(), 0D);
|
||||
assertEquals(6, banks.minimumBlendWidth());
|
||||
assertEquals(48, banks.maximumBlendWidth());
|
||||
assertEquals(3, banks.cascadeRun());
|
||||
assertEquals(8, banks.waterfallMinimumDrop());
|
||||
assertEquals(2.2D, banks.mouthFlareRatio(), 0D);
|
||||
assertEquals(new HydrologyPlannerSettings.Inlet(96, 5, 40), banks.inlet());
|
||||
assertFalse(banks.exposeCutStrata());
|
||||
assertEquals(12, settings.surface().maximumIncision());
|
||||
assertEquals(2.5D, settings.surface().shoreWidth(), 0D);
|
||||
@@ -127,10 +148,16 @@ public class IrisHydrologyRuntimeSettingsTest {
|
||||
@Test
|
||||
public void routingOutletLimitControlsDrainageRootCount() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.getHydrology().getRivers().getRouting().setMaximumOutletsPerTile(2);
|
||||
dimension.getHydrology().getRivers().getRouting()
|
||||
.setMaximumOutletsPerTile(2)
|
||||
.setMaximumCoastalOutletsPerTile(5);
|
||||
|
||||
assertEquals(2, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
.outlets().maximumPerTile());
|
||||
HydrologyPlannerSettings.Outlets outlets = IrisHydrologyRuntime
|
||||
.createSettings(dimension, dimension.getHydrology(), () -> null)
|
||||
.outlets();
|
||||
|
||||
assertEquals(2, outlets.maximumPerTile());
|
||||
assertEquals(5, outlets.maximumCoastalPerTile());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -158,7 +185,7 @@ public class IrisHydrologyRuntimeSettingsTest {
|
||||
.setMaximumBasinDepth(9);
|
||||
|
||||
HydrologyPlannerSettings.Geometry geometry = IrisHydrologyRuntime
|
||||
.createSettings(dimension, dimension.getHydrology())
|
||||
.createSettings(dimension, dimension.getHydrology(), () -> null)
|
||||
.geometry();
|
||||
|
||||
assertEquals(47, geometry.meanders().primaryWavelength());
|
||||
@@ -179,4 +206,29 @@ public class IrisHydrologyRuntimeSettingsTest {
|
||||
assertEquals(2.1D, geometry.drops().basinWidthRatio(), 0D);
|
||||
assertEquals(9, geometry.drops().maximumBasinDepth());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaCaveConfigurationMapsIntoPlannerSettings() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
IrisRiverHydrology rivers = dimension.getHydrology().getRivers();
|
||||
rivers.setEnabled(true);
|
||||
IrisCoastalRiverGrottoConfig coastal = rivers.getGrottos().getCoastal();
|
||||
coastal.setEnabled(true);
|
||||
coastal.getSeaCaves().setEnabled(true).setMaximumPerTile(5).setMinimumSpacing(200).setMinimumCoastHeight(9).setDepth(20);
|
||||
|
||||
HydrologyPlannerSettings.SeaCaves mapped = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
|
||||
.seaCaves();
|
||||
assertEquals(new HydrologyPlannerSettings.SeaCaves(true, 5, 200, 9, 20), mapped);
|
||||
|
||||
coastal.setEnabled(false);
|
||||
HydrologyPlannerSettings.SeaCaves withoutCoastalGrottos = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
|
||||
.seaCaves();
|
||||
assertFalse(withoutCoastalGrottos.enabled());
|
||||
assertEquals(5, withoutCoastalGrottos.maximumPerTile());
|
||||
assertEquals(20, withoutCoastalGrottos.depth());
|
||||
|
||||
coastal.setEnabled(true);
|
||||
coastal.getSeaCaves().setEnabled(false);
|
||||
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null).seaCaves().enabled());
|
||||
}
|
||||
}
|
||||
|
||||
+49
-3
@@ -40,11 +40,47 @@ public class ChannelProfileBuilderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directOceanCoursesFlareAtTheMouth() {
|
||||
public void theInletWidensAndDeepensTowardTheCoastOverItsLength() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
|
||||
ChannelProfile profile = builder(1D, 1D).build(centerline, "water", true);
|
||||
ChannelProfile plain = builder(defaults.banks().withInlet(HydrologyPlannerSettings.Inlet.none()))
|
||||
.build(centerline, "water", true);
|
||||
|
||||
assertEquals(3D * 1.6D, profile.halfWidth()[399], 0.05D);
|
||||
assertEquals(64, inlet.length());
|
||||
int flareStart = 400 - inlet.length();
|
||||
assertEquals(3D * defaults.banks().mouthFlareRatio(), profile.halfWidth()[399], 1.0E-9D);
|
||||
assertEquals(3D + inlet.depth(), profile.depth()[399], 1.0E-9D);
|
||||
assertEquals(3D, profile.halfWidth()[flareStart], 1.0E-9D);
|
||||
assertEquals(3D, profile.depth()[flareStart], 1.0E-9D);
|
||||
assertTrue(profile.halfWidth()[flareStart + 1] > 3D);
|
||||
assertTrue(profile.depth()[flareStart + 1] > 3D);
|
||||
assertEquals(3D, profile.halfWidth()[300], 1.0E-9D);
|
||||
for (int station = flareStart + 1; station < 400; station++) {
|
||||
assertTrue(profile.halfWidth()[station] >= profile.halfWidth()[station - 1]);
|
||||
assertTrue(profile.depth()[station] >= profile.depth()[station - 1]);
|
||||
}
|
||||
assertEquals(3D, plain.halfWidth()[399], 1.0E-9D);
|
||||
assertEquals(3D, plain.depth()[399], 1.0E-9D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theFlareCompletesAtTheShorelineWhenTheCenterlineRunsIntoTheSea() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> x >= 380
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
ChannelProfile profile = new ChannelProfileBuilder(defaults, coast, CONSTANT_GEOMETRY).build(centerline, "water", true);
|
||||
|
||||
int flareStart = 380 - inlet.length();
|
||||
assertEquals(3D, profile.halfWidth()[flareStart], 1.0E-9D);
|
||||
assertTrue(profile.halfWidth()[flareStart + 1] > 3D);
|
||||
assertEquals(3D * defaults.banks().mouthFlareRatio(), profile.halfWidth()[379], 1.0E-9D);
|
||||
assertEquals(3D + inlet.depth(), profile.depth()[379], 1.0E-9D);
|
||||
assertEquals(3D * defaults.banks().mouthFlareRatio(), profile.halfWidth()[399], 1.0E-9D);
|
||||
assertEquals(3D, profile.halfWidth()[300], 1.0E-9D);
|
||||
}
|
||||
|
||||
@@ -76,6 +112,14 @@ public class ChannelProfileBuilderTest {
|
||||
return builder(widthMultiplier, depthMultiplier, (int x, int z) -> 80);
|
||||
}
|
||||
|
||||
private static ChannelProfileBuilder builder(HydrologyPlannerSettings.Banks banks) {
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
|
||||
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(), banks);
|
||||
return new ChannelProfileBuilder(surface, (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land"), CONSTANT_GEOMETRY);
|
||||
}
|
||||
|
||||
private static ChannelProfileBuilder builder(double widthMultiplier, double depthMultiplier, IntBinaryOperator height) {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> new HydrologyTerrainSample(
|
||||
height.applyAsInt(x, z),
|
||||
@@ -104,7 +148,9 @@ public class ChannelProfileBuilderTest {
|
||||
"parent",
|
||||
"parent",
|
||||
"parent",
|
||||
List.of("water"), List.of()
|
||||
List.of("water"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY);
|
||||
}
|
||||
|
||||
+335
-17
@@ -25,20 +25,20 @@ public class ErosionFieldCompilerTest {
|
||||
};
|
||||
|
||||
@Test
|
||||
public void flatTerrainProducesAContainedChannelWithLipAndNoWriteAboveNatural() {
|
||||
public void flatTerrainHoldsTheWaterFlushWithTheBankAndContainedByDefault() {
|
||||
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||
SurfaceColumn center = compiled.field().column(150, 0);
|
||||
assertNotNull(center);
|
||||
assertEquals(SurfaceRole.CHANNEL, center.role());
|
||||
assertEquals(79, center.headY());
|
||||
assertTrue(center.height() <= 76);
|
||||
assertEquals(80, center.headY());
|
||||
assertTrue(center.height() <= 77);
|
||||
SurfaceColumn edge = compiled.field().column(150, 3);
|
||||
assertNotNull(edge);
|
||||
assertEquals(SurfaceRole.CHANNEL, edge.role());
|
||||
assertTrue(edge.height() > center.height());
|
||||
assertTrue(edge.height() <= 78);
|
||||
assertTrue(edge.height() <= 79);
|
||||
SurfaceColumn shore = compiled.field().column(150, 4);
|
||||
assertNotNull(shore);
|
||||
assertEquals(SurfaceRole.SHORE, shore.role());
|
||||
@@ -47,6 +47,71 @@ public class ErosionFieldCompilerTest {
|
||||
assertChannelContained(compiled);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aConfiguredSinkLowersTheWaterBelowTheBankByThatMuch() {
|
||||
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
Compiled sunk = compile(zeroRoughnessSurface(1, HydrologyPlannerSettings.Erosion.defaults()), flat, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertEquals(0, sunk.field().uncontainedWetCells());
|
||||
assertEquals(79, sunk.field().column(150, 0).headY());
|
||||
SurfaceColumn shore = sunk.field().column(150, 4);
|
||||
assertEquals(SurfaceRole.SHORE, shore.role());
|
||||
assertEquals(80, shore.height());
|
||||
assertChannelContained(sunk);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledErosionKeepsTheChannelShoreAndLipButNoValley() {
|
||||
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z) / 2, 0D, "land");
|
||||
HydrologyPlannerSettings.Erosion off = new HydrologyPlannerSettings.Erosion(false, 12, 0.45D, 1D, 0.5D);
|
||||
Compiled compiled = compile(zeroRoughnessSurface(0, off), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||
assertChannelContained(compiled);
|
||||
assertNoWriteAboveNatural(compiled);
|
||||
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||
assertTrue(column.role() != SurfaceRole.BANK);
|
||||
}
|
||||
assertNotNull(compiled.field().column(150, 0));
|
||||
assertEquals(SurfaceRole.SHORE, compiled.field().column(150, 4).role());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void erosionDefaultsReproduceTheValleyBitForBitAndACurveReshapesIt() {
|
||||
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z) / 2, 0D, "land");
|
||||
Compiled defaults = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults()), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
Compiled explicit = compile(zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D)), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
Compiled steep = compile(zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 2D, 0.5D)), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertEquals(defaults.field().columns().size(), explicit.field().columns().size());
|
||||
for (SurfaceColumn column : defaults.field().columns().values()) {
|
||||
SurfaceColumn same = explicit.field().column(column.x(), column.z());
|
||||
assertNotNull(same);
|
||||
assertEquals(column.height(), same.height());
|
||||
assertEquals(column.headY(), same.headY());
|
||||
assertEquals(column.role(), same.role());
|
||||
}
|
||||
boolean differs = false;
|
||||
for (SurfaceColumn column : defaults.field().columns().values()) {
|
||||
SurfaceColumn other = steep.field().column(column.x(), column.z());
|
||||
if (column.role() == SurfaceRole.BANK && other != null && other.height() != column.height()) {
|
||||
differs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(differs);
|
||||
assertChannelContained(steep);
|
||||
assertNoWriteAboveNatural(steep);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blendCurveOneIsTheEasedBlendAndHigherCurvesHoldTheBankTopLonger() {
|
||||
assertEquals(SurfaceNoise.smoothStep(0.3D), ErosionFieldCompiler.blend(0.3D, 1D), 0D);
|
||||
assertTrue(ErosionFieldCompiler.blend(0.3D, 2D) < ErosionFieldCompiler.blend(0.3D, 1D));
|
||||
assertTrue(ErosionFieldCompiler.blend(0.3D, 0.5D) > ErosionFieldCompiler.blend(0.3D, 1D));
|
||||
assertEquals(1D, ErosionFieldCompiler.blend(1D, 3D), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hillsideCutGetsABankWhoseWidthGrowsWithTheCut() {
|
||||
Compiled compiled = compile(300, (x, z) -> 90 + Math.max(0, z) / 2, SurfaceTerminal.SINKHOLE, 40);
|
||||
@@ -115,6 +180,77 @@ public class ErosionFieldCompilerTest {
|
||||
assertNoWriteAboveNatural(compiled);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inletBanksAreCutToSeaLevelAndBlendBackToTheNaturalValley() {
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> {
|
||||
if (x >= 240) {
|
||||
return HydrologyTerrainSample.ocean(50, "ocean");
|
||||
}
|
||||
int height = x < 200 ? 100 - x / 6 : 66 + (x - 200) * 18 / 40;
|
||||
return HydrologyTerrainSample.openLand(height, 0D, "land");
|
||||
};
|
||||
HydrologyPlannerSettings.Inlet inlet = new HydrologyPlannerSettings.Inlet(32, 3, 32);
|
||||
HydrologyPlannerSettings.Surface surface = withBanks(zeroRoughnessSurface(), zeroRoughnessSurface().banks().withInlet(inlet));
|
||||
Compiled compiled = compile(surface, coast, 244, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
|
||||
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||
assertNoWriteAboveNatural(compiled);
|
||||
assertChannelContained(compiled);
|
||||
int oceanWrites = 0;
|
||||
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||
HydrologyTerrainSample terrain = coast.sample(column.x(), column.z());
|
||||
if ((terrain.ocean() || terrain.naturalHeight() <= SEA_LEVEL) && !(column.apron() && column.height() == terrain.naturalHeight())) {
|
||||
oceanWrites++;
|
||||
}
|
||||
}
|
||||
assertEquals(0, oceanWrites);
|
||||
for (int x = 240 - inlet.length(); x < 240; x++) {
|
||||
SurfaceColumn center = compiled.field().column(x, 0);
|
||||
assertNotNull(center);
|
||||
assertEquals(SurfaceRole.CHANNEL, center.role());
|
||||
assertEquals(SEA_LEVEL, center.headY());
|
||||
assertTrue(center.height() < SEA_LEVEL);
|
||||
}
|
||||
int pinned = 220;
|
||||
int shoreZ = -1;
|
||||
for (int z = 1; z < 12; z++) {
|
||||
SurfaceColumn column = compiled.field().column(pinned, z);
|
||||
assertNotNull(column);
|
||||
if (column.role() != SurfaceRole.CHANNEL) {
|
||||
shoreZ = z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(shoreZ > 0);
|
||||
SurfaceColumn shore = compiled.field().column(pinned, shoreZ);
|
||||
assertEquals(SurfaceRole.SHORE, shore.role());
|
||||
assertEquals(SEA_LEVEL, shore.height());
|
||||
assertTrue(coast.sample(pinned, shoreZ).naturalHeight() > SEA_LEVEL + 10);
|
||||
int previous = SEA_LEVEL;
|
||||
int banks = 0;
|
||||
int lastHeight = SEA_LEVEL;
|
||||
for (int z = shoreZ + 1; z < 60; z++) {
|
||||
SurfaceColumn column = compiled.field().column(pinned, z);
|
||||
if (column == null) {
|
||||
break;
|
||||
}
|
||||
assertTrue(column.role() == SurfaceRole.SHORE || column.role() == SurfaceRole.BANK);
|
||||
assertTrue(column.height() >= previous);
|
||||
assertTrue(column.height() - previous <= 1);
|
||||
previous = column.height();
|
||||
lastHeight = column.height();
|
||||
banks++;
|
||||
}
|
||||
assertTrue(banks > 8);
|
||||
assertTrue(lastHeight >= coast.sample(pinned, 0).naturalHeight() - 1);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings.Surface withBanks(HydrologyPlannerSettings.Surface surface, HydrologyPlannerSettings.Banks banks) {
|
||||
return new HydrologyPlannerSettings.Surface(
|
||||
surface.enabled(), surface.sources(), surface.minimumWidth(), surface.maximumWidth(),
|
||||
surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(), surface.shoreWidth(), banks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bankColumnsBeyondTheBlendAreNotWritten() {
|
||||
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
||||
@@ -154,7 +290,16 @@ public class ErosionFieldCompilerTest {
|
||||
}
|
||||
|
||||
private static Compiled compile(HydrologyTerrainSampler sampler, int stations, SurfaceTerminal terminal, int terminalHead) {
|
||||
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
|
||||
return compile(zeroRoughnessSurface(), sampler, stations, terminal, terminalHead);
|
||||
}
|
||||
|
||||
private static Compiled compile(
|
||||
HydrologyPlannerSettings.Surface surface,
|
||||
HydrologyTerrainSampler sampler,
|
||||
int stations,
|
||||
SurfaceTerminal terminal,
|
||||
int terminalHead
|
||||
) {
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(stations - 1, 0, 0));
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
|
||||
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, CONSTANT_GEOMETRY)
|
||||
@@ -168,12 +313,26 @@ public class ErosionFieldCompilerTest {
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings.Surface zeroRoughnessSurface() {
|
||||
return zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults());
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings.Surface zeroRoughnessSurface(int sink, HydrologyPlannerSettings.Erosion erosion) {
|
||||
return zeroRoughnessSurface(sink, erosion, new HydrologyPlannerSettings.Ponds(
|
||||
new HydrologyPlannerSettings.Pond(false, 5, 9, 3),
|
||||
new HydrologyPlannerSettings.Pond(false, 4, 7, 3)));
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings.Surface zeroRoughnessSurface(
|
||||
int sink,
|
||||
HydrologyPlannerSettings.Erosion erosion,
|
||||
HydrologyPlannerSettings.Ponds ponds
|
||||
) {
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
return new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
defaults.sources(),
|
||||
4, 8, 2, 4, 10, 1.5D,
|
||||
new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, 2.5D, 24, true)
|
||||
new HydrologyPlannerSettings.Banks(sink, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, erosion, ponds)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,14 +369,14 @@ public class ErosionFieldCompilerTest {
|
||||
|
||||
@Test
|
||||
public void bedBowlIsLevelAcrossTheThalwegAndOneBlockAtTheEdge() {
|
||||
assertEquals(1D, ErosionFieldCompiler.bowl(0D), 1e-9);
|
||||
assertEquals(1D, ErosionFieldCompiler.bowl(0.45D), 1e-9);
|
||||
assertTrue(ErosionFieldCompiler.bowl(0.6D) > 0.8D);
|
||||
assertTrue(ErosionFieldCompiler.bowl(0.8D) > 0.3D && ErosionFieldCompiler.bowl(0.8D) < 0.7D);
|
||||
assertEquals(0D, ErosionFieldCompiler.bowl(1D), 1e-9);
|
||||
assertEquals(1D, ErosionFieldCompiler.bowl(0D, 0.45D), 1e-9);
|
||||
assertEquals(1D, ErosionFieldCompiler.bowl(0.45D, 0.45D), 1e-9);
|
||||
assertTrue(ErosionFieldCompiler.bowl(0.6D, 0.45D) > 0.8D);
|
||||
assertTrue(ErosionFieldCompiler.bowl(0.8D, 0.45D) > 0.3D && ErosionFieldCompiler.bowl(0.8D, 0.45D) < 0.7D);
|
||||
assertEquals(0D, ErosionFieldCompiler.bowl(1D, 0.45D), 1e-9);
|
||||
double previous = 1D;
|
||||
for (double normalized = 0D; normalized <= 1D; normalized += 0.05D) {
|
||||
double value = ErosionFieldCompiler.bowl(normalized);
|
||||
double value = ErosionFieldCompiler.bowl(normalized, 0.45D);
|
||||
assertTrue(value <= previous + 1e-9);
|
||||
previous = value;
|
||||
}
|
||||
@@ -226,7 +385,7 @@ public class ErosionFieldCompilerTest {
|
||||
@Test
|
||||
public void everyCellTouchingWaterKeepsTheLipAcrossHeadSteps() {
|
||||
Compiled compiled = compile(400, (x, z) -> 120 - x / 8, SurfaceTerminal.SINKHOLE, 40);
|
||||
int freeboard = zeroRoughnessSurface().banks().freeboard();
|
||||
int sink = zeroRoughnessSurface().banks().sink();
|
||||
int checked = 0;
|
||||
|
||||
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||
@@ -239,7 +398,7 @@ public class ErosionFieldCompilerTest {
|
||||
if (neighbour == null || neighbour.role() == SurfaceRole.CHANNEL) {
|
||||
continue;
|
||||
}
|
||||
int required = Math.min(neighbour.terrain().naturalHeight(), column.headY() + freeboard);
|
||||
int required = Math.min(neighbour.terrain().naturalHeight(), column.headY() + sink);
|
||||
assertTrue(neighbour.x() + "," + neighbour.z() + " height " + neighbour.height() + " < " + required,
|
||||
neighbour.height() >= required);
|
||||
checked++;
|
||||
@@ -250,16 +409,34 @@ public class ErosionFieldCompilerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aLowCellBesideTheChannelLowersTheHeadSoTheLipSurvives() {
|
||||
public void aDipOneBlockBelowTheWaterAnywhereBesideTheChannelIsHeldByALip() {
|
||||
// Whatever the solver sampled, no water cell may border a lower dry cell: a one-block dip
|
||||
// beside the channel is either answered by a lower head or by a one-block lip on the dip.
|
||||
for (int dipZ = 2; dipZ <= 6; dipZ++) {
|
||||
for (int dipX = 148; dipX <= 152; dipX++) {
|
||||
int finalZ = dipZ;
|
||||
int finalX = dipX;
|
||||
Compiled compiled = compile(300, (x, z) -> x == finalX && z == finalZ ? 79 : 80, SurfaceTerminal.SINKHOLE, 40);
|
||||
assertEquals("dip at " + finalX + "," + finalZ, 0, compiled.field().uncontainedWetCells());
|
||||
SurfaceColumn dip = compiled.field().column(finalX, finalZ);
|
||||
if (dip != null) {
|
||||
assertTrue("lip taller than one block at " + finalX + "," + finalZ, dip.height() <= 80);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aLowCellBesideTheChannelLowersTheHeadSoTheBankStillHoldsIt() {
|
||||
Compiled compiled = compile(300, (x, z) -> x == 150 && z == 4 ? 79 : 80, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
SurfaceColumn center = compiled.field().column(150, 0);
|
||||
assertNotNull(center);
|
||||
assertTrue("head " + center.headY(), center.headY() <= 78);
|
||||
assertTrue("head " + center.headY(), center.headY() <= 79);
|
||||
SurfaceColumn low = compiled.field().column(150, 4);
|
||||
assertEquals(SurfaceRole.SHORE, low.role());
|
||||
assertNotNull(low);
|
||||
assertTrue(low.height() >= center.headY() + 1);
|
||||
assertTrue(low.height() >= center.headY());
|
||||
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||
}
|
||||
|
||||
@@ -292,4 +469,145 @@ public class ErosionFieldCompilerTest {
|
||||
assertEquals(200, firstApron);
|
||||
assertEquals(202, lastApron);
|
||||
}
|
||||
@Test
|
||||
public void policyShoreBiomeWidthWidensTheShoreRoleOverUntouchedGround() {
|
||||
Compiled compiled = compile((int x, int z) -> land(80, 12D), 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
SurfaceColumn geometricShore = compiled.field().column(150, 4);
|
||||
assertNotNull(geometricShore);
|
||||
assertEquals(SurfaceRole.SHORE, geometricShore.role());
|
||||
SurfaceColumn band = compiled.field().column(150, 12);
|
||||
assertNotNull(band);
|
||||
assertEquals(SurfaceRole.SHORE, band.role());
|
||||
assertEquals(80, band.height());
|
||||
assertNull(compiled.field().column(150, 17));
|
||||
assertNoWriteAboveNatural(compiled);
|
||||
assertChannelContained(compiled);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroShoreBiomeWidthLeavesTheGeometricShoreWithTheBankRole() {
|
||||
Compiled compiled = compile((int x, int z) -> land(80, 0D), 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
SurfaceColumn geometricShore = compiled.field().column(150, 4);
|
||||
assertNotNull(geometricShore);
|
||||
assertEquals(SurfaceRole.BANK, geometricShore.role());
|
||||
assertEquals(80, geometricShore.height());
|
||||
assertNull(compiled.field().column(150, 6));
|
||||
assertChannelContained(compiled);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample land(int height, double shoreBiomeWidth) {
|
||||
HydrologyTerrainSample open = HydrologyTerrainSample.openLand(height, 0D, "land");
|
||||
return new HydrologyTerrainSample(
|
||||
open.naturalHeight(), open.slope(), open.ocean(), open.caveAvailable(), open.caveFloorY(), open.caveFluidY(),
|
||||
open.transitAllowed(), open.outletAllowed(), open.surfaceSourceAllowed(), open.surfaceSourceRequired(),
|
||||
open.undergroundSourceAllowed(), open.undergroundSourceRequired(), open.routingCost(),
|
||||
open.surfaceSourceWeight(), open.undergroundSourceWeight(), open.widthMultiplier(), open.depthMultiplier(),
|
||||
open.incisionMultiplier(), open.routingMultiplier(), open.bankMultiplier(), open.parentBiomeKey(),
|
||||
open.surfaceBiomeKey(), open.mouthBiomeKey(), open.shoreBiomeKey(), open.bankBiomeKey(),
|
||||
open.floodedCaveBiomeKey(), open.preferredProfileKeys(), open.surfacePoolKeys(), shoreBiomeWidth, null
|
||||
);
|
||||
}
|
||||
@Test
|
||||
public void aSourcePondOpensAroundTheHeadwaterAtTheSourceHead() {
|
||||
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
HydrologyPlannerSettings.Ponds ponds = new HydrologyPlannerSettings.Ponds(
|
||||
new HydrologyPlannerSettings.Pond(true, 10, 10, 3),
|
||||
new HydrologyPlannerSettings.Pond(false, 4, 7, 3));
|
||||
Compiled compiled = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), ponds), flat, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
Compiled plain = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||
assertChannelContained(compiled);
|
||||
assertNoWriteAboveNatural(compiled);
|
||||
SurfaceColumn centre = compiled.field().column(0, 0);
|
||||
assertEquals(SurfaceRole.CHANNEL, centre.role());
|
||||
assertEquals(80, centre.headY());
|
||||
assertTrue(centre.height() <= 77);
|
||||
SurfaceColumn behind = compiled.field().column(-9, 0);
|
||||
assertNotNull(behind);
|
||||
assertEquals(SurfaceRole.CHANNEL, behind.role());
|
||||
assertEquals(80, behind.headY());
|
||||
SurfaceColumn beside = compiled.field().column(0, 9);
|
||||
assertEquals(SurfaceRole.CHANNEL, beside.role());
|
||||
assertEquals(0, behind.station());
|
||||
SurfaceColumn plainBehind = plain.field().column(-9, 0);
|
||||
assertTrue(plainBehind == null || plainBehind.role() != SurfaceRole.CHANNEL);
|
||||
assertEquals(SurfaceRole.SHORE, compiled.field().column(-11, 0).role());
|
||||
assertNotNull(compiled.field().column(150, 0));
|
||||
assertEquals(SurfaceRole.CHANNEL, compiled.field().column(150, 0).role());
|
||||
assertTrue(compiled.field().column(150, 4).role() == SurfaceRole.SHORE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aTerminalPondOpensAtAnInlandEndButNotAtAnOceanMouth() {
|
||||
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
HydrologyPlannerSettings.Ponds ponds = new HydrologyPlannerSettings.Ponds(
|
||||
new HydrologyPlannerSettings.Pond(false, 5, 9, 3),
|
||||
new HydrologyPlannerSettings.Pond(true, 5, 5, 2));
|
||||
Compiled inland = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), ponds), flat, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertEquals(0, inland.field().uncontainedWetCells());
|
||||
assertChannelContained(inland);
|
||||
SurfaceColumn end = inland.field().column(299, 0);
|
||||
assertEquals(SurfaceRole.CHANNEL, end.role());
|
||||
assertTrue(end.height() <= 78);
|
||||
SurfaceColumn beyond = inland.field().column(303, 0);
|
||||
assertNotNull(beyond);
|
||||
assertEquals(SurfaceRole.CHANNEL, beyond.role());
|
||||
assertEquals(299, beyond.station());
|
||||
SurfaceColumn plainBeyond = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40).field().column(303, 0);
|
||||
assertTrue(plainBeyond == null || plainBeyond.role() != SurfaceRole.CHANNEL);
|
||||
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> x >= 240
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(100 - x / 6, 0D, "land");
|
||||
Compiled mouth = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), ponds), coast, 300, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
int wetBeyondTheShore = 0;
|
||||
for (SurfaceColumn column : mouth.field().columns().values()) {
|
||||
if (column.role() == SurfaceRole.CHANNEL && !column.apron() && column.x() >= 236 && Math.abs(column.z()) > 4) {
|
||||
wetBeyondTheShore++;
|
||||
}
|
||||
}
|
||||
assertEquals(0, wetBeyondTheShore);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPondShrinksWhereTheGroundFallsAwayAroundItsRim() {
|
||||
HydrologyTerrainSampler ridge = (int x, int z) -> HydrologyTerrainSample.openLand(z > 7 || z < -7 ? 70 : 80, 0D, "land");
|
||||
HydrologyPlannerSettings.Ponds ponds = new HydrologyPlannerSettings.Ponds(
|
||||
new HydrologyPlannerSettings.Pond(true, 4, 12, 3),
|
||||
new HydrologyPlannerSettings.Pond(false, 4, 7, 3));
|
||||
Compiled compiled = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), ponds), ridge, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||
assertChannelContained(compiled);
|
||||
assertNoWriteAboveNatural(compiled);
|
||||
assertEquals(80, compiled.field().column(0, 0).headY());
|
||||
SurfaceColumn nearRim = compiled.field().column(0, 4);
|
||||
assertEquals(SurfaceRole.CHANNEL, nearRim.role());
|
||||
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||
if (column.role() == SurfaceRole.CHANNEL) {
|
||||
assertTrue(Math.abs(column.z()) <= 7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pondsAreDeterministicForACourseSeed() {
|
||||
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults());
|
||||
Compiled first = compile(surface, flat, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
Compiled second = compile(surface, flat, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertEquals(first.field().columns().size(), second.field().columns().size());
|
||||
for (SurfaceColumn column : first.field().columns().values()) {
|
||||
SurfaceColumn same = second.field().column(column.x(), column.z());
|
||||
assertNotNull(same);
|
||||
assertEquals(column.height(), same.height());
|
||||
assertEquals(column.role(), same.role());
|
||||
}
|
||||
assertEquals(0, first.field().uncontainedWetCells());
|
||||
}
|
||||
}
|
||||
|
||||
+36
-1
@@ -79,7 +79,7 @@ public class SurfaceCourseBuilderTest {
|
||||
assertNull(result.rejection());
|
||||
assertEquals(1, result.segments().size());
|
||||
assertEquals(HydrologyFeatureType.SURFACE_POOL, result.segments().getFirst().type());
|
||||
assertEquals(79, result.lastHead());
|
||||
assertEquals(80, result.lastHead());
|
||||
assertEquals(201, result.segments().getFirst().centerline().size());
|
||||
}
|
||||
|
||||
@@ -135,7 +135,42 @@ public class SurfaceCourseBuilderTest {
|
||||
assertTrue(result.segments().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aCourseWithAnInletEndsAtSeaLevelWithoutACoastalDropStation() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 240
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(100 - x / 6, 0D, "land");
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(241, 0, 0));
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
|
||||
SurfaceCourseResult result = builder(sampler).build(7L, COURSE_ID, "water", path, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
SurfaceCourseResult plain = builder(defaults.banks().withInlet(HydrologyPlannerSettings.Inlet.none()), sampler)
|
||||
.build(7L, COURSE_ID, "water", path, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
|
||||
assertNull(result.rejection());
|
||||
assertEquals(SEA_LEVEL, result.lastHead());
|
||||
assertEquals(239, result.pathEnd().x());
|
||||
HydraulicSegment last = result.segments().getLast();
|
||||
assertEquals(HydrologyFeatureType.SURFACE_POOL, last.type());
|
||||
assertEquals(SEA_LEVEL, last.upstreamHeadY());
|
||||
assertEquals(SEA_LEVEL, last.downstreamHeadY());
|
||||
assertEquals(inlet.length(), last.centerline().size());
|
||||
assertEquals(240 - inlet.length(), last.start().x());
|
||||
assertNull(plain.rejection());
|
||||
assertEquals(SEA_LEVEL, plain.lastHead());
|
||||
assertEquals(240, plain.pathEnd().x());
|
||||
assertTrue(plain.segments().getLast().upstreamHeadY() > SEA_LEVEL);
|
||||
}
|
||||
|
||||
private static SurfaceCourseBuilder builder(HydrologyTerrainSampler sampler) {
|
||||
return new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL, 64);
|
||||
}
|
||||
|
||||
private static SurfaceCourseBuilder builder(HydrologyPlannerSettings.Banks banks, HydrologyTerrainSampler sampler) {
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
|
||||
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(), banks);
|
||||
return new SurfaceCourseBuilder(surface, sampler, CONSTANT_GEOMETRY, SEA_LEVEL, 64);
|
||||
}
|
||||
}
|
||||
|
||||
+52
@@ -114,6 +114,58 @@ public class SurfaceFootprintCompilerTest {
|
||||
assertNull(column(footprint, 111, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inletColumnsPublishSeaLevelFluidOverALoweredBed() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 100
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
HydrologyPlannerSettings.Inlet inlet = HydrologyPlannerSettings.defaults().surface().banks().inlet();
|
||||
ArrayList<HydrologyPoint> ramp = new ArrayList<>();
|
||||
for (int x = 30; x <= 39; x++) {
|
||||
ramp.add(new HydrologyPoint(x, 69 - (x - 30), 0));
|
||||
}
|
||||
RiverCourse course = course(List.of(
|
||||
segment(1L, HydrologyFeatureType.SURFACE_POOL, 69, 69, points(0, 30, 69)),
|
||||
segment(2L, HydrologyFeatureType.CASCADE, 69, SEA_LEVEL, List.copyOf(ramp)),
|
||||
segment(3L, HydrologyFeatureType.SURFACE_POOL, SEA_LEVEL, SEA_LEVEL, points(39, 99, SEA_LEVEL)),
|
||||
segment(4L, HydrologyFeatureType.MOUTH, SEA_LEVEL, SEA_LEVEL, points(99, 101, SEA_LEVEL))
|
||||
));
|
||||
SurfaceFootprint footprint = compiler(sampler).compile(course);
|
||||
|
||||
assertEquals(0, footprint.uncontainedWetCells());
|
||||
assertEquals(3, inlet.depth());
|
||||
for (int x = 40; x < 100; x++) {
|
||||
SurfaceLayerColumn center = column(footprint, x, 0);
|
||||
assertNotNull("x=" + x, center);
|
||||
HydrologyColumnLayer layer = center.layer();
|
||||
assertTrue(layer.channel());
|
||||
assertTrue(layer.fluidOwned());
|
||||
assertEquals(SEA_LEVEL, layer.fluidHeadY());
|
||||
assertTrue("x=" + x + " bed " + layer.bedY(), layer.bedY() <= SEA_LEVEL - inlet.depth());
|
||||
assertEquals(sampler.sample(x, 0).mouthBiomeKey(), layer.mouthBiomeKey());
|
||||
}
|
||||
int shoreZ = -1;
|
||||
for (int z = 1; z < 16; z++) {
|
||||
SurfaceLayerColumn column = column(footprint, 90, z);
|
||||
assertNotNull(column);
|
||||
if (!column.layer().channel()) {
|
||||
shoreZ = z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(shoreZ > 3);
|
||||
SurfaceLayerColumn shore = column(footprint, 90, shoreZ);
|
||||
assertTrue(shore.layer().shore());
|
||||
assertEquals(SEA_LEVEL, shore.layer().bedY());
|
||||
assertEquals(SEA_LEVEL, shore.layer().fluidHeadY());
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (column.terrain().ocean()) {
|
||||
assertTrue(column.layer().oceanApron());
|
||||
assertFalse(column.layer().terrainOwned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SurfaceLayerColumn column(SurfaceFootprint footprint, int x, int z) {
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (column.x() == x && column.z() == z) {
|
||||
|
||||
+167
-10
@@ -24,18 +24,29 @@ public class ValleyProfileSolverTest {
|
||||
};
|
||||
|
||||
@Test
|
||||
public void flatTerrainHoldsTheHeadOneInsetBelowTheBanks() {
|
||||
public void flatTerrainHoldsTheHeadAtTheLowestBankByDefault() {
|
||||
ValleyProfile valley = solve(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertNull(valley.rejection());
|
||||
assertEquals(300, valley.exposedStations());
|
||||
for (int station = 0; station < valley.exposedStations(); station++) {
|
||||
assertEquals(79, valley.head()[station]);
|
||||
assertEquals(80, valley.head()[station]);
|
||||
assertEquals(80, valley.crossMin()[station]);
|
||||
assertEquals(80, valley.centerNatural()[station]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aConfiguredSinkHoldsTheHeadThatFarBelowTheLowestBank() {
|
||||
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
ValleyProfile valley = solver(flat, 2).solve(straight(300), channel(300, flat), SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertNull(valley.rejection());
|
||||
for (int station = 0; station < valley.exposedStations(); station++) {
|
||||
assertEquals(78, valley.head()[station]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gentleSlopeProducesOneBlockNonRisingSteps() {
|
||||
ValleyProfile valley = solve(400, (x, z) -> 120 - x / 8, SurfaceTerminal.SINKHOLE, 40);
|
||||
@@ -66,9 +77,9 @@ public class ValleyProfileSolverTest {
|
||||
ValleyProfile valley = solve(300, (x, z) -> x == 150 && z == 6 ? 74 : 80, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertNull(valley.rejection());
|
||||
assertEquals(79, valley.head()[100]);
|
||||
assertTrue(valley.head()[150] <= 73);
|
||||
assertTrue(valley.head()[299] <= 73);
|
||||
assertEquals(80, valley.head()[100]);
|
||||
assertTrue(valley.head()[150] <= 74);
|
||||
assertTrue(valley.head()[299] <= 74);
|
||||
assertContained(valley);
|
||||
}
|
||||
|
||||
@@ -87,7 +98,8 @@ public class ValleyProfileSolverTest {
|
||||
assertNull(valley.rejection());
|
||||
assertTrue(valley.exposedStations() > 200);
|
||||
assertTrue(valley.exposedStations() <= 240);
|
||||
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations() - 1]);
|
||||
assertTrue(valley.head()[valley.exposedStations() - 1] >= SEA_LEVEL);
|
||||
assertTrue(valley.head()[valley.exposedStations() - 1] <= SEA_LEVEL + 1);
|
||||
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations()]);
|
||||
assertEquals(SEA_LEVEL, valley.head()[299]);
|
||||
for (int station = 0; station < valley.exposedStations(); station++) {
|
||||
@@ -103,7 +115,8 @@ public class ValleyProfileSolverTest {
|
||||
assertNull(valley.rejection());
|
||||
assertTrue(valley.exposedStations() <= 200);
|
||||
assertTrue(valley.exposedStations() >= 190);
|
||||
assertEquals(89, valley.head()[valley.exposedStations() - 1]);
|
||||
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations() - 1]);
|
||||
assertEquals(90, valley.head()[valley.exposedStations() - 1 - HydrologyPlannerSettings.Inlet.defaults().length() * 3 / 2]);
|
||||
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations()]);
|
||||
}
|
||||
|
||||
@@ -117,8 +130,8 @@ public class ValleyProfileSolverTest {
|
||||
largest = Math.max(largest, valley.head()[station - 1] - valley.head()[station]);
|
||||
}
|
||||
assertEquals(10, largest);
|
||||
assertEquals(99, valley.head()[100]);
|
||||
assertEquals(89, valley.head()[200]);
|
||||
assertEquals(100, valley.head()[100]);
|
||||
assertEquals(90, valley.head()[200]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -142,9 +155,140 @@ public class ValleyProfileSolverTest {
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, valley.rejection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anInletHoldsTheLastStationsAtSeaLevelAndMayCutDeeperThanTheChannelCap() {
|
||||
HydrologyTerrainSampler coast = risingCoast();
|
||||
HydrologyPlannerSettings.Inlet inlet = new HydrologyPlannerSettings.Inlet(32, 3, 32);
|
||||
|
||||
ValleyProfile valley = solver(coast, inlet).solve(straight(300), channel(300, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
ValleyProfile plain = solver(coast, HydrologyPlannerSettings.Inlet.none())
|
||||
.solve(straight(300), channel(300, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
|
||||
assertNull(valley.rejection());
|
||||
assertEquals(240, valley.exposedStations());
|
||||
for (int station = 240 - inlet.length(); station < 240; station++) {
|
||||
assertEquals(SEA_LEVEL, valley.head()[station]);
|
||||
}
|
||||
int deepestCut = 0;
|
||||
for (int station = 0; station < valley.exposedStations(); station++) {
|
||||
deepestCut = Math.max(deepestCut, valley.centerNatural()[station] - valley.head()[station]);
|
||||
}
|
||||
assertTrue("deepest " + deepestCut, deepestCut > 10);
|
||||
assertContained(valley);
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, plain.rejection());
|
||||
assertTrue(plain.rejectionDetail() > 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theInletApproachGradesDownOneBlockPerStation() {
|
||||
HydrologyTerrainSampler plateau = (int x, int z) -> x >= 240
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(88, 0D, "land");
|
||||
|
||||
ValleyProfile valley = solver(plateau, new HydrologyPlannerSettings.Inlet(32, 3, 32))
|
||||
.solve(straight(300), channel(300, plateau), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
ValleyProfile shallow = solver(plateau, new HydrologyPlannerSettings.Inlet(32, 3, 24))
|
||||
.solve(straight(300), channel(300, plateau), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
|
||||
assertNull(valley.rejection());
|
||||
int inletStart = 240 - 32;
|
||||
assertEquals(88, valley.head()[inletStart - 17]);
|
||||
for (int station = inletStart - 16; station < inletStart; station++) {
|
||||
assertEquals(SEA_LEVEL + (inletStart - station), valley.head()[station]);
|
||||
}
|
||||
assertEquals(SEA_LEVEL, valley.head()[inletStart]);
|
||||
assertContained(valley);
|
||||
// A cap the plateau cannot be cut through keeps the plain crossing: no inlet, no ramp, the
|
||||
// river still reaches the sea and drops into it at the coast.
|
||||
assertNull(shallow.rejection());
|
||||
assertEquals(88, valley.head()[0]);
|
||||
for (int station = 0; station < shallow.exposedStations(); station++) {
|
||||
assertEquals(88, shallow.head()[station]);
|
||||
}
|
||||
assertEquals(SEA_LEVEL, shallow.head()[shallow.exposedStations()]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theInletStopsWhereTheCoastRisesBeyondItsCap() {
|
||||
HydrologyTerrainSampler bluff = (int x, int z) -> x >= 240
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(x >= 220 ? 66 + (x - 220) / 5 : 110, 0D, "land");
|
||||
|
||||
ValleyProfile valley = solver(bluff, new HydrologyPlannerSettings.Inlet(64, 3, 32))
|
||||
.solve(straight(300), channel(300, bluff), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
|
||||
assertNull(valley.rejection());
|
||||
assertEquals(240, valley.exposedStations());
|
||||
for (int station = 220; station < 240; station++) {
|
||||
assertEquals(SEA_LEVEL, valley.head()[station]);
|
||||
}
|
||||
assertEquals(110, valley.head()[219]);
|
||||
assertEquals(110, valley.head()[100]);
|
||||
assertContained(valley);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void upstreamStationsKeepTheChannelCap() {
|
||||
HydrologyTerrainSampler ridge = (int x, int z) -> x >= 240
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(x >= 30 && x < 50 ? 94 : 80, 0D, "land");
|
||||
|
||||
ValleyProfile valley = solver(ridge, new HydrologyPlannerSettings.Inlet(32, 3, 32))
|
||||
.solve(straight(300), channel(300, ridge), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, valley.rejection());
|
||||
assertTrue(valley.rejectionDetail() > 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theInletNeverTakesMoreThanHalfOfAShortCourse() {
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> x >= 80
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
|
||||
ValleyProfile valley = solver(coast, new HydrologyPlannerSettings.Inlet(64, 3, 32))
|
||||
.solve(straight(100), channel(100, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
|
||||
assertNull(valley.rejection());
|
||||
assertEquals(80, valley.exposedStations());
|
||||
for (int station = 40; station < 80; station++) {
|
||||
assertEquals(SEA_LEVEL, valley.head()[station]);
|
||||
}
|
||||
assertEquals(SEA_LEVEL + 1, valley.head()[39]);
|
||||
assertEquals(SEA_LEVEL + 10, valley.head()[30]);
|
||||
for (int station = 0; station < 20; station++) {
|
||||
assertEquals(70, valley.head()[station]);
|
||||
}
|
||||
assertContained(valley);
|
||||
}
|
||||
|
||||
/** Land falling gently to 66 at x=200, then a coast rising to 83 over the last forty blocks before the sea at x=240. */
|
||||
private static HydrologyTerrainSampler risingCoast() {
|
||||
return (int x, int z) -> {
|
||||
if (x >= 240) {
|
||||
return HydrologyTerrainSample.ocean(50, "ocean");
|
||||
}
|
||||
int height = x < 200 ? 100 - x / 6 : 66 + (x - 200) * 18 / 40;
|
||||
return HydrologyTerrainSample.openLand(height, 0D, "land");
|
||||
};
|
||||
}
|
||||
|
||||
private static ValleyProfileSolver solver(HydrologyTerrainSampler sampler, HydrologyPlannerSettings.Inlet inlet) {
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Banks banks = defaults.banks();
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
|
||||
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(),
|
||||
new HydrologyPlannerSettings.Banks(banks.sink(), banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(),
|
||||
banks.roughness(), banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(),
|
||||
banks.mouthFlareRatio(), inlet, banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(),
|
||||
banks.erosion(), banks.ponds()));
|
||||
return new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64);
|
||||
}
|
||||
|
||||
private static void assertContained(ValleyProfile valley) {
|
||||
for (int station = 0; station < valley.exposedStations(); station++) {
|
||||
assertTrue(valley.head()[station] <= valley.crossMin()[station] - 1);
|
||||
assertTrue(valley.head()[station] <= valley.crossMin()[station]);
|
||||
if (station > 0) {
|
||||
assertTrue(valley.head()[station] <= valley.head()[station - 1]);
|
||||
}
|
||||
@@ -160,6 +304,19 @@ public class ValleyProfileSolverTest {
|
||||
return new ValleyProfileSolver(HydrologyPlannerSettings.defaults().surface(), sampler, SEA_LEVEL, 64);
|
||||
}
|
||||
|
||||
private static ValleyProfileSolver solver(HydrologyTerrainSampler sampler, int sink) {
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Banks banks = defaults.banks();
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
|
||||
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(),
|
||||
new HydrologyPlannerSettings.Banks(sink, banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(),
|
||||
banks.roughness(), banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(),
|
||||
banks.mouthFlareRatio(), HydrologyPlannerSettings.Inlet.none(), banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(),
|
||||
banks.erosion(), banks.ponds()));
|
||||
return new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64);
|
||||
}
|
||||
|
||||
private static ChannelProfile channel(int stations, HydrologyTerrainSampler sampler) {
|
||||
return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY)
|
||||
.build(straight(stations), "water", false);
|
||||
|
||||
+9
-3
@@ -1553,7 +1553,8 @@ public class MantleHydrologyComponentTest {
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
false
|
||||
false,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
@@ -1563,10 +1564,13 @@ public class MantleHydrologyComponentTest {
|
||||
32,
|
||||
16,
|
||||
1,
|
||||
8,
|
||||
8
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(), List.of()
|
||||
List.of(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1603,7 +1607,9 @@ public class MantleHydrologyComponentTest {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("default"), List.of()
|
||||
List.of("default"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,11 +50,10 @@ public class IrisRiverConfigurationTest {
|
||||
assertEquals(8D, surface.getChannel().getWidth().getMax(), 0D);
|
||||
assertEquals(2D, surface.getChannel().getDepth().getMin(), 0D);
|
||||
assertEquals(4D, surface.getChannel().getDepth().getMax(), 0D);
|
||||
assertEquals(1, surface.getChannel().getInset());
|
||||
assertEquals(0, surface.getChannel().getSink());
|
||||
assertEquals(10, surface.getChannel().getMaximumIncision());
|
||||
assertEquals(0.25D, surface.getChannel().getRoughness(), 0D);
|
||||
assertEquals(16, surface.getChannel().getRoughnessWavelength());
|
||||
assertEquals(1, surface.getBanks().getFreeboard());
|
||||
assertEquals(1.5D, surface.getBanks().getShoreWidth(), 0D);
|
||||
assertEquals(3D, surface.getBanks().getBlendSlope(), 0D);
|
||||
assertEquals(4, surface.getBanks().getMinimumBlendWidth());
|
||||
@@ -154,13 +153,12 @@ public class IrisRiverConfigurationTest {
|
||||
"channel": {
|
||||
"width": {"min": 5, "max": 24},
|
||||
"depth": {"min": 2, "max": 6},
|
||||
"inset": 2,
|
||||
"sink": 2,
|
||||
"maximumIncision": 14,
|
||||
"roughness": 0.4,
|
||||
"roughnessWavelength": 24
|
||||
},
|
||||
"banks": {
|
||||
"freeboard": 2,
|
||||
"shoreWidth": 2.5,
|
||||
"blendSlope": 4,
|
||||
"minimumBlendWidth": 6,
|
||||
@@ -262,11 +260,10 @@ public class IrisRiverConfigurationTest {
|
||||
assertEquals(0.4D, rivers.getRouting().getConfluenceAttraction(), 0D);
|
||||
assertEquals(new KList<>(IrisRiverInlandOutlet.SINKHOLE_GROTTO), rivers.getRouting().getInlandOutlets());
|
||||
assertEquals(24D, rivers.getSurface().getChannel().getWidth().getMax(), 0D);
|
||||
assertEquals(2, rivers.getSurface().getChannel().getInset());
|
||||
assertEquals(2, rivers.getSurface().getChannel().getSink());
|
||||
assertEquals(14, rivers.getSurface().getChannel().getMaximumIncision());
|
||||
assertEquals(0.4D, rivers.getSurface().getChannel().getRoughness(), 0D);
|
||||
assertEquals(24, rivers.getSurface().getChannel().getRoughnessWavelength());
|
||||
assertEquals(2, rivers.getSurface().getBanks().getFreeboard());
|
||||
assertEquals(2.5D, rivers.getSurface().getBanks().getShoreWidth(), 0D);
|
||||
assertEquals(4D, rivers.getSurface().getBanks().getBlendSlope(), 0D);
|
||||
assertEquals(6, rivers.getSurface().getBanks().getMinimumBlendWidth());
|
||||
|
||||
@@ -1705,7 +1705,7 @@ public final class HydrologyPackProbe {
|
||||
List<ChannelIncision> incisions = channelIncisions(
|
||||
tile.footprint(),
|
||||
scopedCourseIds,
|
||||
settings.surface().banks().inset(),
|
||||
settings.surface().banks().sink(),
|
||||
settings.surface().maximumIncision()
|
||||
);
|
||||
for (ChannelIncision incision : incisions) {
|
||||
@@ -2698,7 +2698,9 @@ public final class HydrologyPackProbe {
|
||||
int deepCourses,
|
||||
int features,
|
||||
int coveragePairs,
|
||||
long engineReadyNanos
|
||||
long engineReadyNanos,
|
||||
int surfaceJunctions,
|
||||
int undergroundJunctions
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -2896,7 +2898,7 @@ public final class HydrologyPackProbe {
|
||||
Locale.ROOT,
|
||||
"IRIS_HYDROLOGY_PACK_SEED seed=%d hydrology_seed=%d tiles=%d courses=%d "
|
||||
+ "surface_courses=%d underground_courses=%d deep_courses=%d features=%d "
|
||||
+ "pairs=%d engine_ready_ms=%.3f%n",
|
||||
+ "pairs=%d engine_ready_ms=%.3f surface_junctions=%d underground_junctions=%d%n",
|
||||
summary.seed(),
|
||||
summary.hydrologySeed(),
|
||||
summary.tiles(),
|
||||
@@ -2906,7 +2908,9 @@ public final class HydrologyPackProbe {
|
||||
summary.deepCourses(),
|
||||
summary.features(),
|
||||
summary.coveragePairs(),
|
||||
summary.engineReadyNanos() / 1_000_000D
|
||||
summary.engineReadyNanos() / 1_000_000D,
|
||||
summary.surfaceJunctions(),
|
||||
summary.undergroundJunctions()
|
||||
);
|
||||
}
|
||||
shapeMetrics.requireOrganicSurfaceNetwork();
|
||||
@@ -2976,6 +2980,8 @@ public final class HydrologyPackProbe {
|
||||
int undergroundCourses = 0;
|
||||
int deepCourses = 0;
|
||||
int features = 0;
|
||||
int surfaceJunctions = 0;
|
||||
int undergroundJunctions = 0;
|
||||
long hydrologySeed;
|
||||
long engineReadyNanos;
|
||||
Set<CoverageKey> seedCoverage = new HashSet<>();
|
||||
@@ -3008,10 +3014,57 @@ public final class HydrologyPackProbe {
|
||||
shapeMetrics.observe(tile, runtime.settings());
|
||||
List<HydrologyColumnSample> orderedColumns = null;
|
||||
courses += tile.courses().size();
|
||||
List<Junction> tileJunctions = junctions(tile);
|
||||
int tileSurfaceCourses = 0;
|
||||
int tileUndergroundCourses = 0;
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (course.type() == RiverCourseType.SURFACE) {
|
||||
tileSurfaceCourses++;
|
||||
} else if (course.type() == RiverCourseType.UNDERGROUND) {
|
||||
tileUndergroundCourses++;
|
||||
}
|
||||
}
|
||||
System.out.printf(
|
||||
Locale.ROOT,
|
||||
"IRIS_HYDROLOGY_PACK_TILE seed=%d tile_x=%d tile_z=%d courses=%d surface_courses=%d "
|
||||
+ "underground_courses=%d junctions=%d%n",
|
||||
seed,
|
||||
tileKey.tileX(),
|
||||
tileKey.tileZ(),
|
||||
tile.courses().size(),
|
||||
tileSurfaceCourses,
|
||||
tileUndergroundCourses,
|
||||
tileJunctions.size()
|
||||
);
|
||||
for (Junction junction : tileJunctions) {
|
||||
if (junction.type() == RiverCourseType.SURFACE) {
|
||||
surfaceJunctions++;
|
||||
} else {
|
||||
undergroundJunctions++;
|
||||
}
|
||||
System.out.printf(
|
||||
Locale.ROOT,
|
||||
"IRIS_HYDROLOGY_PACK_JUNCTION seed=%d tile_x=%d tile_z=%d type=%s x=%d y=%d z=%d "
|
||||
+ "tributary=%016x stem=%016x tributary_stations=%d%n",
|
||||
seed,
|
||||
tileKey.tileX(),
|
||||
tileKey.tileZ(),
|
||||
junction.type().name(),
|
||||
junction.point().x(),
|
||||
junction.point().y(),
|
||||
junction.point().z(),
|
||||
junction.tributaryId(),
|
||||
junction.stemId(),
|
||||
junction.tributaryStations()
|
||||
);
|
||||
}
|
||||
HashMap<Long, String> profilesByCourse = new HashMap<>(tile.courses().size());
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
profilesByCourse.put(course.id(), course.profileKey());
|
||||
observedConfiguredCoverage.add(courseCoverage(course));
|
||||
ConfiguredCoverage courseCoverage = courseCoverage(course);
|
||||
if (courseCoverage != null) {
|
||||
observedConfiguredCoverage.add(courseCoverage);
|
||||
}
|
||||
if (course.type() == RiverCourseType.SURFACE) {
|
||||
surfaceCourses++;
|
||||
} else if (course.type() == RiverCourseType.UNDERGROUND) {
|
||||
@@ -3140,10 +3193,61 @@ public final class HydrologyPackProbe {
|
||||
deepCourses,
|
||||
features,
|
||||
seedCoverage.size(),
|
||||
engineReadyNanos
|
||||
engineReadyNanos,
|
||||
surfaceJunctions,
|
||||
undergroundJunctions
|
||||
));
|
||||
}
|
||||
|
||||
record Junction(
|
||||
RiverCourseType type,
|
||||
HydrologyPoint point,
|
||||
long tributaryId,
|
||||
long stemId,
|
||||
int tributaryStations
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A junction is a course whose last centerline point lies on, or within a channel width of, a
|
||||
* station of another course of the same type: the tributary ends on its stem.
|
||||
*/
|
||||
static List<Junction> junctions(HydrologyTile tile) {
|
||||
ArrayList<Junction> junctions = new ArrayList<>();
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (course.segments().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
HydrologyPoint end = course.segments().getLast().end();
|
||||
int stations = 0;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
stations += segment.centerline().size();
|
||||
}
|
||||
Long stemId = null;
|
||||
double stemDistance = Double.POSITIVE_INFINITY;
|
||||
for (RiverCourse stem : tile.courses()) {
|
||||
if (stem.id() == course.id() || stem.type() != course.type()) {
|
||||
continue;
|
||||
}
|
||||
for (HydraulicSegment segment : stem.segments()) {
|
||||
double reach = segment.width() / 2D + 1D;
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
double distance = point.distanceSquared2D(end);
|
||||
if (distance <= reach * reach && distance < stemDistance) {
|
||||
stemId = stem.id();
|
||||
stemDistance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stemId != null) {
|
||||
junctions.add(new Junction(course.type(), end, course.id(), stemId, stations));
|
||||
}
|
||||
}
|
||||
junctions.sort(Comparator.comparingLong(Junction::tributaryId));
|
||||
return List.copyOf(junctions);
|
||||
}
|
||||
|
||||
// No public production equivalent: HydrologyPlanner.routeTurnDegrees is private.
|
||||
static double turnDegrees(HydrologyPoint previous, HydrologyPoint point, HydrologyPoint next) {
|
||||
double incomingX = point.x() - previous.x();
|
||||
@@ -4082,8 +4186,10 @@ public final class HydrologyPackProbe {
|
||||
case UNDERGROUND -> CoverageFamily.UNDERGROUND;
|
||||
case DEEP_FLUID -> CoverageFamily.DEEP;
|
||||
case SURFACE_POOL -> CoverageFamily.POOL;
|
||||
// A sea cave is not a source-budgeted course family; its coverage is the coastal grotto feature.
|
||||
case SEA_CAVE -> null;
|
||||
};
|
||||
return new ConfiguredCoverage(family, course.profileKey());
|
||||
return family == null ? null : new ConfiguredCoverage(family, course.profileKey());
|
||||
}
|
||||
|
||||
private static void validateTileBlockBounds(
|
||||
|
||||
@@ -228,8 +228,10 @@ public final class HydrologyPlannerProbe {
|
||||
0.5D,
|
||||
12D,
|
||||
0.5D,
|
||||
0.1D
|
||||
, 1D, 0),
|
||||
0.1D,
|
||||
1D,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
surfaceSources,
|
||||
@@ -253,7 +255,8 @@ public final class HydrologyPlannerProbe {
|
||||
4,
|
||||
5,
|
||||
9,
|
||||
true
|
||||
true,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
false,
|
||||
@@ -263,10 +266,13 @@ public final class HydrologyPlannerProbe {
|
||||
12,
|
||||
32,
|
||||
2,
|
||||
4,
|
||||
4
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(), List.of()
|
||||
List.of(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -306,7 +312,9 @@ public final class HydrologyPlannerProbe {
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
);
|
||||
,
|
||||
Double.NaN,
|
||||
null);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -357,7 +365,9 @@ public final class HydrologyPlannerProbe {
|
||||
"bank",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
);
|
||||
,
|
||||
Double.NaN,
|
||||
null);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -439,8 +449,10 @@ public final class HydrologyPlannerProbe {
|
||||
0.5D,
|
||||
12D,
|
||||
0.5D,
|
||||
0.2D
|
||||
, 1D, 0),
|
||||
0.2D,
|
||||
1D,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
surfaceSources,
|
||||
@@ -464,7 +476,8 @@ public final class HydrologyPlannerProbe {
|
||||
5,
|
||||
6,
|
||||
12,
|
||||
true
|
||||
true,
|
||||
0
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
@@ -474,10 +487,13 @@ public final class HydrologyPlannerProbe {
|
||||
12,
|
||||
48,
|
||||
2,
|
||||
8,
|
||||
8
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(deepFluid), List.of()
|
||||
List.of(deepFluid), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -522,7 +538,9 @@ public final class HydrologyPlannerProbe {
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
List.of("water"), List.of(),
|
||||
Double.NaN,
|
||||
null
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -556,7 +574,9 @@ public final class HydrologyPlannerProbe {
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
);
|
||||
,
|
||||
Double.NaN,
|
||||
null);
|
||||
}
|
||||
|
||||
private static long[] parseSeeds(String[] arguments) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -179,6 +180,18 @@ public final class RiverTransectProbe {
|
||||
System.out.println(String.format(Locale.ROOT,
|
||||
"%s tile=%d,%d seed=%d courses=%d planMs=%.1f",
|
||||
PREFIX, key.tileX(), key.tileZ(), configuration.seed(), tile.courses().size(), planMillis));
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (course.type() != RiverCourseType.SEA_CAVE) {
|
||||
continue;
|
||||
}
|
||||
HydraulicSegment chamber = course.segments().getFirst();
|
||||
System.out.println(String.format(Locale.ROOT,
|
||||
"%s seaCave id=%016x inner=%d,%d,%d mouth=%d,%d,%d cavePlan=%s",
|
||||
PREFIX, course.id(),
|
||||
chamber.start().x(), chamber.start().y(), chamber.start().z(),
|
||||
chamber.end().x(), chamber.end().y(), chamber.end().z(),
|
||||
tile.cavePlan(course.id()).map((plan) -> plan.accepted() ? "accepted" : "rejected").orElse("none")));
|
||||
}
|
||||
|
||||
for (String line : rejectionLines(tile)) {
|
||||
System.out.println(PREFIX + " " + line);
|
||||
@@ -457,8 +470,28 @@ public final class RiverTransectProbe {
|
||||
line(image, (from.naturalPoint().x() - minimumX) / scale, (from.naturalPoint().z() - minimumZ) / scale,
|
||||
(to.naturalPoint().x() - minimumX) / scale, (to.naturalPoint().z() - minimumZ) / scale, 0x20C020);
|
||||
}
|
||||
// Sea-cave chambers: every column the chamber owns in dark blue, so the cave shows on the coast with
|
||||
// its open side to the sea; the centerline on top in white like a surface course.
|
||||
HashSet<Long> seaCaveIds = new HashSet<>();
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
int rgb = course.type() == RiverCourseType.SURFACE ? 0xFFFFFF : 0xFF00FF;
|
||||
if (course.type() == RiverCourseType.SEA_CAVE) {
|
||||
seaCaveIds.add(course.id());
|
||||
}
|
||||
}
|
||||
if (!seaCaveIds.isEmpty()) {
|
||||
for (HydrologyColumnSample column : tile.footprint().columns().values()) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (seaCaveIds.contains(layer.feature().courseId()) && layer.terrainOwned() && layer.channel()) {
|
||||
plot(image, (column.x() - minimumX) / scale, (column.z() - minimumZ) / scale, 0x000080, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
int rgb = course.type() == RiverCourseType.SURFACE || course.type() == RiverCourseType.SEA_CAVE
|
||||
? 0xFFFFFF
|
||||
: 0xFF00FF;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
plot(image, (point.x() - minimumX) / scale, (point.z() - minimumZ) / scale, rgb, 1);
|
||||
|
||||
Reference in New Issue
Block a user