Disregard.

This commit is contained in:
Brian Neumann-Fopiano
2026-09-03 14:01:45 -04:00
parent e4ca7d4e70
commit 0510c108be
57 changed files with 4420 additions and 344 deletions
@@ -45,6 +45,8 @@ final class PackHydrologyValidator {
"REQUIRED_HEADWATER"
);
private static final Set<String> ROUTING_MODES = Set.of("BLOCK", "AVOID", "ALLOW", "PREFER");
private static final Set<String> BLEND_STYLES = Set.of("SMOOTH", "LINEAR", "CONCAVE", "TERRACED", "CLIFF");
private static final Set<String> BED_PROFILES = Set.of("BOWL", "FLAT", "V", "U");
private static final Set<String> INLAND_OUTLETS = Set.of("SINKHOLE_GROTTO");
private static final List<String> POLICY_BIOME_FIELDS = List.of(
"surfaceBiomes",
@@ -208,12 +210,17 @@ final class PackHydrologyValidator {
private static void validateGeometry(String path, JSONObject geometry, List<String> errors) {
JSONObject meanders = nestedObject(geometry, "meanders", path, errors);
JSONObject surface = nestedObject(geometry, "surface", path, errors);
JSONObject underground = nestedObject(geometry, "underground", path, errors);
JSONObject grottos = nestedObject(geometry, "grottos", path, errors);
JSONObject drops = nestedObject(geometry, "drops", path, errors);
if (meanders != null) {
validateMeanders(path + ".meanders", meanders, errors);
}
if (surface != null) {
// The three nullable roughness fields fall back to the surface channel; a JSON null counts as absent.
validateChannelShape(path + ".surface", surface, errors);
}
if (underground != null) {
validateChannelShape(path + ".underground", underground, errors);
}
@@ -244,6 +251,14 @@ final class PackHydrologyValidator {
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "bedRoughness", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "wallRoughness", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, shape, "roughnessWavelength", 3, 128, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "radialBase", 0.4D, 1.2D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "radialMinimum", 0.2D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "radialMaximum", 1D, 2D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "primaryLobeStrength", 0D, 0.5D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "detailLobeStrength", 0D, 0.5D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "ceilingRoughness", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "aspectMinimum", 0.2D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "aspectRange", 0D, 0.8D, errors);
}
private static void validateDropShape(String path, JSONObject drops, List<String> errors) {
@@ -254,6 +269,7 @@ final class PackHydrologyValidator {
PackJsonFieldChecks.validateOptionalIntegerRange(path, drops, "maximumFlowDepth", 1, 16, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, drops, "basinWidthRatio", 1D, 4D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, drops, "maximumBasinDepth", 1, 32, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, drops, "undergroundCascadeRunPerBlock", 0, 16, errors);
}
private static RoutingValues validateRouting(String path, JSONObject routing, List<String> errors) {
@@ -385,6 +401,10 @@ final class PackHydrologyValidator {
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "springLength", 4, 96, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "roughness", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "roughnessWavelength", 4, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "smoothingRadius", 0, 64, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "outlineMinimumRatio", 0.2D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "outlineMaximumRatio", 1D, 3D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "springExtraDepth", 0D, 8D, errors);
}
private static void validateSurfaceErosion(String path, JSONObject erosion, List<String> errors) {
@@ -393,6 +413,11 @@ final class PackHydrologyValidator {
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);
PackJsonFieldChecks.validateOptionalEnum(path, erosion, "style", BLEND_STYLES, errors);
// terraceSteps and cliffFraction only matter to their own style; the other styles ignore them, so no cross-field rule.
PackJsonFieldChecks.validateOptionalIntegerRange(path, erosion, "terraceSteps", 2, 16, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, erosion, "cliffFraction", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalEnum(path, erosion, "bedProfile", BED_PROFILES, errors);
}
private static void validateSurfacePonds(String path, JSONObject ponds, List<String> errors) {
@@ -414,11 +439,15 @@ final class PackHydrologyValidator {
}
private static void validateSurfaceBanks(String path, JSONObject banks, List<String> errors) {
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "shoreWidth", 0.5D, 6D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "shoreWidth", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "blendSlope", 0.5D, 12D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "minimumBlendWidth", 1, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "maximumBlendWidth", 1, 64, errors);
PackJsonFieldChecks.validateOptionalBoolean(path, banks, "exposeCutStrata", errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "shoreRise", 0D, 4D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "blendBaseWidth", 0D, 32D, errors);
validateMaterial(path, banks, "shoreMaterial", errors);
validateMaterial(path, banks, "bankMaterial", errors);
if (integerValue(banks, "minimumBlendWidth", 4) > integerValue(banks, "maximumBlendWidth", 32)) {
errors.add(path + ".minimumBlendWidth must not exceed maximumBlendWidth.");
}
@@ -427,26 +456,46 @@ final class PackHydrologyValidator {
private static void validateSurfaceFlow(String path, JSONObject flow, List<String> errors) {
PackJsonFieldChecks.validateOptionalIntegerRange(path, flow, "cascadeRun", 1, 8, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, flow, "waterfallMinimumDrop", 2, 32, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, flow, "waterfallThalwegFraction", 0D, 0.95D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, flow, "plungeBasinMinimumDrop", 1, 8, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, flow, "plungeBasinLengthRatio", 0D, 8D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, flow, "plungeBasinDepth", 0, 8, errors);
}
private static void validateSurfaceBed(String path, JSONObject bed, List<String> errors) {
PackJsonFieldChecks.validateOptionalBoolean(path, bed, "allowGravityBlocks", errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, bed, "padding", 0, 8, errors);
if (!bed.has("paddingPalette")) {
if (bed.has("paddingPalette")) {
validateSolidPalette(path + ".paddingPalette", bed.opt("paddingPalette"), errors);
}
validateMaterial(path, bed, "material", errors);
}
private static void validateMaterial(String path, JSONObject owner, String field, List<String> errors) {
JSONObject material = nestedObject(owner, field, path, errors);
if (material == null) {
return;
}
Object rawPalette = bed.opt("paddingPalette");
String materialPath = path + "." + field;
PackJsonFieldChecks.validateOptionalBoolean(materialPath, material, "enabled", errors);
PackJsonFieldChecks.validateOptionalIntegerRange(materialPath, material, "depth", 1, 8, errors);
if (material.has("palette")) {
validateSolidPalette(materialPath + ".palette", material.opt("palette"), errors);
}
}
private static void validateSolidPalette(String path, Object rawPalette, List<String> errors) {
if (!(rawPalette instanceof JSONObject palette)) {
errors.add(path + ".paddingPalette must be an object.");
errors.add(path + " must be an object.");
return;
}
Object rawEntries = palette.opt("palette");
if (!(rawEntries instanceof JSONArray entries) || entries.length() == 0) {
errors.add(path + ".paddingPalette.palette must contain at least one solid block.");
errors.add(path + ".palette must contain at least one solid block.");
return;
}
for (int index = 0; index < entries.length(); index++) {
String entryPath = path + ".paddingPalette.palette[" + index + "]";
String entryPath = path + ".palette[" + index + "]";
JSONObject entry = entries.optJSONObject(index);
if (entry == null) {
errors.add(entryPath + " must be an object.");
@@ -468,6 +517,8 @@ final class PackHydrologyValidator {
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "inletLength", 0, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "inletDepth", 0, 16, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "maximumIncision", 0, 128, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, mouths, "inletCourseFraction", 0.05D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, mouths, "inletRampSlope", 0.1D, 4D, 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.");
@@ -508,6 +559,10 @@ final class PackHydrologyValidator {
512,
errors
);
PackJsonFieldChecks.validateOptionalIntegerRange(path, underground, "minimumRockCover", 1, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, underground, "minimumFloorCover", 1, 32, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, underground, "wideningSources", 1, 64, errors);
validateMaterial(path, underground, "bedMaterial", errors);
NumericRange height = dimensionHeight(dimension);
if (fluidLevel.minimum() - depth.maximum() <= height.minimum()) {
@@ -598,6 +653,9 @@ final class PackHydrologyValidator {
}
}
if (coastal) {
// cliffMinimumHeight is nullable; a JSON null falls back to max(4, verticalRadius) at runtime.
PackJsonFieldChecks.validateOptionalIntegerRange(path, grotto, "cliffMinimumHeight", 0, 128, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, grotto, "cliffSlopeFactor", 0D, 4D, errors);
JSONObject seaCaves = nestedObject(grotto, "seaCaves", path, errors);
if (seaCaves != null) {
String seaCavePath = path + ".seaCaves";
@@ -606,6 +664,7 @@ final class PackHydrologyValidator {
PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "minimumSpacing", 16, 8192, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "minimumCoastHeight", 1, 128, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "depth", 0, 128, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(seaCavePath, seaCaves, "sweepJitterDegrees", 0D, 90D, errors);
int horizontalRadius = integerValue(grotto, "horizontalRadius", DEFAULT_COASTAL_GROTTO.getHorizontalRadius());
int minimumSpacing = integerValue(seaCaves, "minimumSpacing", DEFAULT_COASTAL_GROTTO.getSeaCaves().getMinimumSpacing());
if (minimumSpacing < horizontalRadius * 2) {
@@ -962,6 +1021,8 @@ final class PackHydrologyValidator {
validateNullableDouble(path, policy, "routingMultiplier", 0D, 64D, errors);
validateNullableDouble(path, policy, "bankMultiplier", 0D, 4D, errors);
validateNullableDouble(path, policy, "shoreBiomeWidth", 0D, 32D, errors);
validateNullableDouble(path, policy, "shoreWidth", 0D, 16D, errors);
validateNullableBoolean(path, policy, "erosion", errors);
validateNullableBoolean(path, policy, "confined", errors);
return policy;
}
@@ -32,6 +32,8 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisOreGenerator;
import art.arcane.iris.engine.object.IrisOreGeneratorBounds;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverMaterialConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverBankConfig;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.project.context.ChunkedDataCache;
import art.arcane.iris.util.project.context.ChunkContext;
@@ -105,12 +107,17 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
KList<IrisOreGenerator> dimensionUndergroundOres = hideOres ? null : dimension.getUndergroundOreGenerators();
IrisOreGeneratorBounds dimensionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getSurfaceOreGeneratorBounds();
IrisOreGeneratorBounds dimensionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getUndergroundOreGeneratorBounds();
boolean exposeCutStrata = dimension.getHydrology() != null
&& dimension.getHydrology().getRivers().getSurface().getBanks().isExposeCutStrata();
IrisSurfaceRiverBankConfig riverBanks = dimension.getHydrology() == null
? null
: dimension.getHydrology().getRivers().getSurface().getBanks();
boolean exposeCutStrata = riverBanks != null && riverBanks.isExposeCutStrata();
IrisSurfaceRiverBedConfig riverBed = dimension.getHydrology() == null
? null
: dimension.getHydrology().getRivers().getSurface().getBed();
boolean padRiverBed = riverBed != null && !riverBed.isAllowGravityBlocks();
IrisRiverMaterialConfig bedMaterial = riverBed == null ? null : riverBed.getMaterial();
IrisRiverMaterialConfig shoreMaterial = riverBanks == null ? null : riverBanks.getShoreMaterial();
IrisRiverMaterialConfig bankMaterial = riverBanks == null ? null : riverBanks.getBankMaterial();
for (int zf = 0; zf < chunkDepth; zf++) {
int realZ = zf + z;
@@ -143,6 +150,8 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
? Math.max(0, hydrology.naturalHeight() - he)
: 0;
boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned();
IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial(
hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial);
PlatformBlockState fluid = hydrologyFluid == null
? complex.resolveSurfaceFluid(realX, realZ)
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
@@ -211,6 +220,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
if (blocks.hasIndex(depth + cut)) {
PlatformBlockState layerBlock = blocks.get(depth + cut);
if (roleMaterial != null) {
layerBlock = paintHydrologyMaterial(
layerBlock, roleMaterial, depth, localRng, realX, i, realZ, data);
}
if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
layerBlock = riverBed.getPaddingPalette().get(localRng, realX, i, realZ, data);
}
@@ -275,6 +288,56 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
}
}
/**
* The river material that owns this column's top layers, or null when the biome layers stand.
* Roles are exclusive: a channel bed, then the shore strip, then the eroded bank.
*/
static IrisRiverMaterialConfig hydrologyRoleMaterial(
HydrologyColumnLayer terrainLayer,
IrisRiverMaterialConfig bedMaterial,
IrisRiverMaterialConfig shoreMaterial,
IrisRiverMaterialConfig bankMaterial
) {
if (terrainLayer == null || !terrainLayer.terrainOwned()) {
return null;
}
IrisRiverMaterialConfig material;
if (terrainLayer.channel()) {
material = bedMaterial;
} else if (terrainLayer.shore()) {
material = shoreMaterial;
} else if (terrainLayer.grading()) {
material = bankMaterial;
} else {
material = null;
}
return material != null && material.isEnabled() ? material : null;
}
/**
* Replaces a biome layer with the river material for the top {@code depth} blocks. A painted
* gravity block still goes through the bed padding swap after this.
*/
static PlatformBlockState paintHydrologyMaterial(
PlatformBlockState layerBlock,
IrisRiverMaterialConfig material,
int depth,
RNG rng,
int x,
int y,
int z,
IrisData data
) {
if (material == null || !material.isEnabled() || depth >= material.getDepth()) {
return layerBlock;
}
PlatformBlockState painted = material.getPalette().get(rng, x, y, z, data);
return painted == null ? layerBlock : painted;
}
private PlatformBlockState generateOres(KList<IrisOreGenerator> oreGenerators, int x, int y, int z, RNG rng, IrisData data) {
if (oreGenerators == null || oreGenerators.isEmpty()) {
return null;
@@ -24,10 +24,10 @@ final class HydrologyFootprintCompiler {
private static final int[][] HORIZONTAL_NEIGHBORS = {
{1, 0}, {-1, 0}, {0, 1}, {0, -1}
};
private static final double SURFACE_THALWEG_RATIO = 0.65D;
private static final long ORGANIC_SHAPE_FIRST_PHASE_SALT = 0x4f5247414e31L;
private static final long ORGANIC_SHAPE_SECOND_PHASE_SALT = 0x4f5247414e32L;
private static final long ORGANIC_BED_VARIATION_SALT = 0x424544564152L;
private static final long ORGANIC_CEILING_VARIATION_SALT = 0x4345494c564152L;
private final HydrologyPlannerSettings settings;
private final HydrologyTerrainSampler sampler;
@@ -1169,7 +1169,7 @@ final class HydrologyFootprintCompiler {
ellipsoidScale
);
int fluidHead = channel ? shape.fluidHead() : bed;
int ceiling = channel ? localCeiling(shape, ellipsoidScale) : fluidHead;
int ceiling = channel ? localCeiling(shape, ellipsoidScale, segment, channelShape, worldX, worldZ) : fluidHead;
if (channel && terrainRoofedLayer(course, segment)) {
ceiling = Math.max(fluidHead, Math.min(ceiling, terrain.naturalHeight() - 1));
}
@@ -1346,9 +1346,10 @@ final class HydrologyFootprintCompiler {
resolved = channel ? shape.bed() : gradedBed(shape, terrain, distance);
} else if (channel) {
double normalized = Math.min(1D, distance / Math.max(1D, shape.channelRadius()));
double thalwegFraction = settings.surface().banks().flow().waterfallThalwegFraction();
double shoulder = Math.max(
0D,
(normalized - SURFACE_THALWEG_RATIO) / (1D - SURFACE_THALWEG_RATIO)
(normalized - thalwegFraction) / (1D - thalwegFraction)
);
int maximumDepth = Math.max(1, shape.fluidHead() - shape.bed());
double roughness = signedOrganicNoise(
@@ -1407,12 +1408,29 @@ final class HydrologyFootprintCompiler {
return shape.fluidHead() - localExtent;
}
private int localCeiling(LayerShape shape, double scale) {
private int localCeiling(
LayerShape shape,
double scale,
HydraulicSegment segment,
HydrologyPlannerSettings.ChannelShape channelShape,
int worldX,
int worldZ
) {
if (!shape.ellipsoid() && !shape.archedChannel()) {
return shape.ceiling();
}
int upperExtent = shape.ceiling() - shape.fluidHead();
int localExtent = (int) StrictMath.floor(upperExtent * scale);
double extent = upperExtent * scale;
if (channelShape.ceilingRoughness() > 0D) {
double roughness = signedOrganicNoise(
HydrologyHash.mix(segment.id(), ORGANIC_CEILING_VARIATION_SALT),
worldX,
worldZ,
channelShape.roughnessWavelength()
) * channelShape.ceilingRoughness();
extent *= Math.max(0.55D, 1D + roughness);
}
int localExtent = (int) StrictMath.floor(extent);
if (shape.archedChannel()) {
localExtent = Math.max(1, localExtent);
}
@@ -1473,6 +1491,7 @@ final class HydrologyFootprintCompiler {
if (!shape.organicBoundary()) {
return distance;
}
HydrologyPlannerSettings.ChannelShape channelShape = channelShape(segment.type());
if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO
|| segment.type() == HydrologyFeatureType.INLAND_GROTTO) {
double orientation = HydrologyHash.unit(HydrologyHash.mix(
@@ -1483,10 +1502,10 @@ final class HydrologyFootprintCompiler {
double sine = StrictMath.sin(orientation);
double rotatedX = deltaX * cosine + deltaZ * sine;
double rotatedZ = -deltaX * sine + deltaZ * cosine;
double aspect = 0.62D + HydrologyHash.unit(HydrologyHash.mix(
double aspect = channelShape.aspectMinimum() + HydrologyHash.unit(HydrologyHash.mix(
segment.courseId(),
ORGANIC_SHAPE_SECOND_PHASE_SALT
)) * 0.2D;
)) * channelShape.aspectRange();
distance = StrictMath.hypot(rotatedX, rotatedZ / aspect);
}
double angle = StrictMath.atan2(deltaZ, deltaX);
@@ -1500,7 +1519,6 @@ final class HydrologyFootprintCompiler {
)) * StrictMath.PI * 2D;
double firstLobe = 0.5D + 0.5D * StrictMath.sin(angle * 3D + firstPhase);
double secondLobe = 0.5D + 0.5D * StrictMath.sin(angle * 5D + secondPhase);
HydrologyPlannerSettings.ChannelShape channelShape = channelShape(segment.type());
double coherent = signedOrganicNoise(
segment.courseId(),
worldX,
@@ -1513,12 +1531,12 @@ final class HydrologyFootprintCompiler {
worldZ,
Math.max(3, channelShape.roughnessWavelength() / 2)
);
double radialScale = 0.86D
+ (firstLobe - 0.5D) * 0.08D
+ (secondLobe - 0.5D) * 0.06D
double radialScale = channelShape.radialBase()
+ (firstLobe - 0.5D) * channelShape.primaryLobeStrength()
+ (secondLobe - 0.5D) * channelShape.detailLobeStrength()
+ coherent * channelShape.wallRoughness() * 0.7D
+ detail * channelShape.wallRoughness() * 0.3D;
radialScale = Math.max(0.58D, Math.min(1.18D, radialScale));
radialScale = Math.max(channelShape.radialMinimum(), Math.min(channelShape.radialMaximum(), radialScale));
return distance / radialScale;
}
@@ -62,7 +62,6 @@ public final class HydrologyPlanner {
private static final long DEEP_FLUID_SALT = 0x44454550464cL;
private static final long SURFACE_POOL_SALT = 0x504f4f4cL;
private static final long SEA_CAVE_SALT = 0x534541434156L;
private static final double SEA_CAVE_JITTER_DEGREES = 25D;
private static final long DEEP_FLUID_X_OFFSET_SALT = 0x44454550584fL;
private static final long DEEP_FLUID_Z_OFFSET_SALT = 0x444545505a4fL;
private static final long DEEP_CHANNEL_HEADING_SALT = 0x44454550484447L;
@@ -2262,9 +2261,11 @@ public final class HydrologyPlanner {
private HydrologyFeatureType coastalOutletType(HydrologyTerrainSample land) {
HydrologyPlannerSettings.Outlets outlets = settings.outlets();
// A zero slope factor turns the slope rule off, so only the coast's height over the sea decides.
boolean cliff = land.naturalHeight() - settingsSeaLevel(land)
>= outlets.coastalCliffMinimumHeight()
|| land.slope() >= outlets.coastalCliffMinimumHeight() * 0.5D;
|| outlets.coastalCliffSlopeFactor() > 0D
&& land.slope() >= outlets.coastalCliffMinimumHeight() * outlets.coastalCliffSlopeFactor();
if (cliff && outlets.coastalGrotto().enabled()) {
return HydrologyFeatureType.COASTAL_GROTTO;
}
@@ -5374,7 +5375,7 @@ public final class HydrologyPlanner {
HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z());
return terrain == null || terrain.ocean()
? Integer.MAX_VALUE
: terrain.naturalHeight() - minimumHeadroom - 1;
: terrain.naturalHeight() - minimumHeadroom - settings.underground().minimumRockCover();
}
private int undergroundSegmentCap(
@@ -5418,7 +5419,7 @@ public final class HydrologyPlanner {
);
int minimumLandHeight = minimumLandHeightWithinRadius(point.x(), point.z(), radius);
if (minimumLandHeight != Integer.MAX_VALUE) {
segmentCap = Math.min(segmentCap, minimumLandHeight - headroom - 1);
segmentCap = Math.min(segmentCap, minimumLandHeight - headroom - settings.underground().minimumRockCover());
}
}
if (samples != null) {
@@ -5484,7 +5485,7 @@ public final class HydrologyPlanner {
? depths[0]
: index == depths.length ? depths[depths.length - 1] : Math.max(depths[index - 1], depths[index]);
depth = Math.max(depth, settings.geometry().drops().maximumBasinDepth());
long worldFloorHead = (long) minimumY + depth + 1L;
long worldFloorHead = (long) minimumY + depth + settings.underground().minimumFloorCover();
int boundedWorldFloor = worldFloorHead < Integer.MIN_VALUE
? Integer.MIN_VALUE
: worldFloorHead > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) worldFloorHead;
@@ -5707,8 +5708,14 @@ public final class HydrologyPlanner {
return false;
}
int preferredRun = Math.addExact(Math.multiplyExact(drop, drops.cascadeRunPerBlock()), 1);
int undergroundRun = drops.undergroundCascadeRunPerBlock();
int desiredRun = type.isUnderground()
? (undergroundRun == 0
? minimumRun
: Math.min(
Math.max(minimumRun, Math.addExact(Math.multiplyExact(drop, undergroundRun), 1)),
maximumGeneratedRun
))
: Math.min(preferredRun, maximumGeneratedRun);
List<HydrologyPoint> raster = continuousSurfaceBore
? directRaster
@@ -7125,7 +7132,7 @@ public final class HydrologyPlanner {
// The chamber runs inland along the coast normal, turned by a stable jitter so caves do not all
// face the same way along a straight shore.
private static HydrologyPoint seaCaveInnerPoint(
private HydrologyPoint seaCaveInnerPoint(
GridNode land,
GridNode ocean,
HydrologyPoint landward,
@@ -7136,7 +7143,8 @@ public final class HydrologyPlanner {
double normalZ = land.z() - ocean.z();
double length = StrictMath.hypot(normalX, normalZ);
double jitter = StrictMath.toRadians(
(HydrologyHash.unit(HydrologyHash.mix(stableId, 7)) * 2D - 1D) * SEA_CAVE_JITTER_DEGREES
(HydrologyHash.unit(HydrologyHash.mix(stableId, 7)) * 2D - 1D)
* settings.seaCaves().sweepJitterDegrees()
);
double cos = StrictMath.cos(jitter);
double sin = StrictMath.sin(jitter);
@@ -7605,7 +7613,10 @@ public final class HydrologyPlanner {
int discharge,
double multiplier
) {
double flowScale = Math.min(1D, StrictMath.log(discharge + 1D) / StrictMath.log(9D));
double flowScale = Math.min(
1D,
StrictMath.log(discharge + 1D) / StrictMath.log(settings.underground().wideningSources() + 1D)
);
int base = styledBase + (int) StrictMath.round((maximum - styledBase) * flowScale);
return clamp((int) StrictMath.round(base * multiplier), minimum, maximum);
}
@@ -1,6 +1,10 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.object.IrisRiverBedProfile;
import art.arcane.iris.engine.object.IrisRiverBlendStyle;
import java.util.List;
import java.util.Objects;
public record HydrologyPlannerSettings(
int seaLevel,
@@ -48,8 +52,8 @@ 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, 1),
new Outlets(
Underground.of(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1),
Outlets.of(
true,
new Grotto(true, 18, 8, 8, 32768),
new Grotto(true, 18, 8, 8, 32768),
@@ -286,6 +290,7 @@ 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.
* {@code channel} shapes the wet outline and {@code flow} the waterfalls and plunge basins.
*/
public record Banks(
int sink,
@@ -302,12 +307,14 @@ public record HydrologyPlannerSettings(
int springLength,
boolean exposeCutStrata,
Erosion erosion,
Ponds ponds
Ponds ponds,
Channel channel,
Flow flow
) {
// Structural invariants only; authoring bounds live in the pack validator.
public Banks {
if (erosion == null || ponds == null || inlet == null) {
throw new IllegalArgumentException("Surface erosion, inlet and pond settings are required.");
if (erosion == null || ponds == null || inlet == null || channel == null || flow == null) {
throw new IllegalArgumentException("Surface erosion, inlet, pond, channel and flow settings are required.");
}
if (sink < 0
|| !Double.isFinite(blendSlope) || blendSlope <= 0D
@@ -324,42 +331,78 @@ public record HydrologyPlannerSettings(
}
public static Banks defaults() {
return new Banks(0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, Inlet.defaults(), 2.5D, 24, true, Erosion.defaults(), Ponds.defaults());
return new Banks(0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, Inlet.defaults(), 2.5D, 24, true, Erosion.defaults(),
Ponds.defaults(), Channel.defaults(), Flow.defaults());
}
/** The bank settings before channel and flow shaping existed; both take their defaults. */
public static Banks of(
int sink,
double blendSlope,
int minimumBlendWidth,
int maximumBlendWidth,
double roughness,
int roughnessWavelength,
int cascadeRun,
int waterfallMinimumDrop,
double mouthFlareRatio,
Inlet inlet,
double springWidthRatio,
int springLength,
boolean exposeCutStrata,
Erosion erosion,
Ponds ponds
) {
return new Banks(sink, blendSlope, minimumBlendWidth, maximumBlendWidth, roughness, roughnessWavelength,
cascadeRun, waterfallMinimumDrop, mouthFlareRatio, inlet, springWidthRatio, springLength,
exposeCutStrata, erosion, ponds, Channel.defaults(), Flow.defaults());
}
public Banks withInlet(Inlet inlet) {
return new Banks(sink, blendSlope, minimumBlendWidth, maximumBlendWidth, roughness, roughnessWavelength,
cascadeRun, waterfallMinimumDrop, mouthFlareRatio, inlet, springWidthRatio, springLength,
exposeCutStrata, erosion, ponds);
exposeCutStrata, erosion, ponds, channel, flow);
}
}
/**
* 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.
* deepens by {@code depth}; the stations above it grade down {@code rampSlope} blocks 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 {@code courseFraction} of 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
int maximumIncision,
double courseFraction,
double rampSlope
) {
public Inlet {
if (length < 0 || length > 1024 || depth < 0 || depth > 64 || maximumIncision < 0 || maximumIncision > 512) {
throw new IllegalArgumentException("Surface inlet settings are invalid.");
}
if (!Double.isFinite(courseFraction) || courseFraction <= 0D || courseFraction > 1D
|| !Double.isFinite(rampSlope) || rampSlope <= 0D) {
throw new IllegalArgumentException("Surface inlet course fraction and ramp slope are invalid.");
}
}
public static Inlet none() {
return new Inlet(0, 0, 0);
return new Inlet(0, 0, 0, 0.5D, 1D);
}
public static Inlet defaults() {
return new Inlet(64, 3, 32);
return new Inlet(64, 3, 32, 0.5D, 1D);
}
/** The inlet reach with the default course fraction and ramp slope. */
public static Inlet of(int length, int depth, int maximumIncision) {
return new Inlet(length, depth, maximumIncision, 0.5D, 1D);
}
}
@@ -369,14 +412,24 @@ public record HydrologyPlannerSettings(
* 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.
* only the wet channel, the shore band and the containing lip. {@code style} is the curve of the
* valley side ({@code terraceSteps} and {@code cliffFraction} shape only the terraced and cliff
* styles), {@code bedProfile} the wet bed cross-section, {@code shoreRise} how many blocks the shore
* bench climbs from the water to the valley foot, and {@code blendBaseWidth} a width added to every
* valley blend before its bounds apply.
*/
public record Erosion(
boolean enabled,
int smoothingRadius,
double thalwegFraction,
double blendCurve,
double bedNoise
double bedNoise,
IrisRiverBlendStyle style,
int terraceSteps,
double cliffFraction,
IrisRiverBedProfile bedProfile,
double shoreRise,
double blendBaseWidth
) {
public Erosion {
if (smoothingRadius < 0
@@ -385,10 +438,82 @@ public record HydrologyPlannerSettings(
|| !Double.isFinite(bedNoise) || bedNoise < 0D) {
throw new IllegalArgumentException("Surface erosion settings are invalid.");
}
if (style == null || bedProfile == null
|| terraceSteps < 2
|| !Double.isFinite(cliffFraction) || cliffFraction < 0D || cliffFraction > 1D
|| !Double.isFinite(shoreRise) || shoreRise < 0D
|| !Double.isFinite(blendBaseWidth) || blendBaseWidth < 0D) {
throw new IllegalArgumentException("Surface erosion style settings are invalid.");
}
}
public static Erosion defaults() {
return new Erosion(true, 12, 0.45D, 1D, 0.5D);
return new Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D);
}
/** The erosion amounts with the smooth valley side, bowl bed and flat shore bench they always had. */
public static Erosion of(boolean enabled, int smoothingRadius, double thalwegFraction, double blendCurve, double bedNoise) {
return new Erosion(enabled, smoothingRadius, thalwegFraction, blendCurve, bedNoise,
IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D);
}
// Enum hash codes are identity based and change between runs; the plan fingerprint mixes this hash in.
@Override
public int hashCode() {
return Objects.hash(enabled, smoothingRadius, thalwegFraction, blendCurve, bedNoise, style.name(),
terraceSteps, cliffFraction, bedProfile.name(), shoreRise, blendBaseWidth);
}
}
/**
* The wet outline of a surface channel. {@code smoothingRadius} is the run of stations the profile is
* averaged over, {@code outlineMinimumRatio} and {@code outlineMaximumRatio} bound the outline against
* the planned half-width, and {@code springExtraDepth} is how much of the remaining spring taper is
* added to the bed depth at a spring.
*/
public record Channel(
int smoothingRadius,
double outlineMinimumRatio,
double outlineMaximumRatio,
double springExtraDepth
) {
public Channel {
if (smoothingRadius < 0
|| !Double.isFinite(outlineMinimumRatio) || outlineMinimumRatio <= 0D || outlineMinimumRatio > 1D
|| !Double.isFinite(outlineMaximumRatio) || outlineMaximumRatio < 1D
|| !Double.isFinite(springExtraDepth) || springExtraDepth < 0D) {
throw new IllegalArgumentException("Surface channel settings are invalid.");
}
}
public static Channel defaults() {
return new Channel(16, 0.6D, 1.4D, 1D);
}
}
/**
* Waterfalls and plunge basins on a surface river. {@code waterfallThalwegFraction} is the share of the
* half-width kept at full depth over a fall; a drop of at least {@code plungeBasinMinimumDrop} blocks
* scours a basin {@code plungeBasinLengthRatio} half-widths long and {@code plungeBasinDepth} blocks
* deeper than the bed.
*/
public record Flow(
double waterfallThalwegFraction,
int plungeBasinMinimumDrop,
double plungeBasinLengthRatio,
int plungeBasinDepth
) {
public Flow {
if (!Double.isFinite(waterfallThalwegFraction) || waterfallThalwegFraction < 0D || waterfallThalwegFraction >= 1D
|| plungeBasinMinimumDrop < 1
|| !Double.isFinite(plungeBasinLengthRatio) || plungeBasinLengthRatio < 0D
|| plungeBasinDepth < 0) {
throw new IllegalArgumentException("Surface flow settings are invalid.");
}
}
public static Flow defaults() {
return new Flow(0.65D, 2, 2D, 1);
}
}
@@ -448,26 +573,34 @@ 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.
* {@code depth} blocks inland from the shoreline, its sweep turned up to {@code sweepJitterDegrees}
* either side of the inland normal. Chamber size and volume come from the coastal grotto.
*/
public record SeaCaves(
boolean enabled,
int maximumPerTile,
int minimumSpacing,
int minimumCoastHeight,
int depth
int depth,
double sweepJitterDegrees
) {
public SeaCaves {
if (maximumPerTile < 0 || maximumPerTile > 64
|| minimumSpacing < 16 || minimumSpacing > 8192
|| minimumCoastHeight < 1 || minimumCoastHeight > 128
|| depth < 0 || depth > 128) {
|| depth < 0 || depth > 128
|| !Double.isFinite(sweepJitterDegrees) || sweepJitterDegrees < 0D || sweepJitterDegrees > 180D) {
throw new IllegalArgumentException("Sea cave bounds are invalid.");
}
}
public static SeaCaves disabled() {
return new SeaCaves(false, 0, 16, 1, 0);
return new SeaCaves(false, 0, 16, 1, 0, 25D);
}
/** The chamber budget with the default sweep jitter. */
public static SeaCaves of(boolean enabled, int maximumPerTile, int minimumSpacing, int minimumCoastHeight, int depth) {
return new SeaCaves(enabled, maximumPerTile, minimumSpacing, minimumCoastHeight, depth, 25D);
}
}
@@ -492,7 +625,10 @@ public record HydrologyPlannerSettings(
int minimumHeadroom,
int maximumHeadroom,
boolean connectToExistingCaves,
int tributaries
int tributaries,
int minimumRockCover,
int minimumFloorCover,
int wideningSources
) {
public Underground {
if (sources == null || minimumFluidY > maximumFluidY) {
@@ -504,6 +640,30 @@ public record HydrologyPlannerSettings(
if (tributaries < 0 || tributaries > 4) {
throw new IllegalArgumentException("underground tributaries must be between 0 and 4.");
}
if (minimumRockCover < 1 || minimumRockCover > 256
|| minimumFloorCover < 1 || minimumFloorCover > 256
|| wideningSources < 1 || wideningSources > 1024) {
throw new IllegalArgumentException("Underground rock cover and widening settings are invalid.");
}
}
/** One block of rock above and below a tunnel, and widening that saturates at eight sources. */
public static Underground of(
boolean enabled,
Source sources,
int minimumFluidY,
int maximumFluidY,
int minimumWidth,
int maximumWidth,
int minimumDepth,
int maximumDepth,
int minimumHeadroom,
int maximumHeadroom,
boolean connectToExistingCaves,
int tributaries
) {
return new Underground(enabled, sources, minimumFluidY, maximumFluidY, minimumWidth, maximumWidth, minimumDepth,
maximumDepth, minimumHeadroom, maximumHeadroom, connectToExistingCaves, tributaries, 1, 1, 8);
}
}
@@ -516,7 +676,8 @@ public record HydrologyPlannerSettings(
int mouthLevelingDistance,
int maximumOceanApron,
int maximumPerTile,
int maximumCoastalPerTile
int maximumCoastalPerTile,
double coastalCliffSlopeFactor
) {
public Outlets {
if (coastalGrotto == null || inlandGrotto == null) {
@@ -530,6 +691,23 @@ public record HydrologyPlannerSettings(
|| maximumCoastalPerTile < 0 || maximumCoastalPerTile > 256) {
throw new IllegalArgumentException("Outlet bounds are invalid.");
}
requireFiniteNonNegative(coastalCliffSlopeFactor, "coastalCliffSlopeFactor");
}
/** The outlet budget with the default coastal cliff slope factor. */
public static Outlets of(
boolean oceanEnabled,
Grotto coastalGrotto,
Grotto inlandGrotto,
boolean surfaceSinkholesEnabled,
int coastalCliffMinimumHeight,
int mouthLevelingDistance,
int maximumOceanApron,
int maximumPerTile,
int maximumCoastalPerTile
) {
return new Outlets(oceanEnabled, coastalGrotto, inlandGrotto, surfaceSinkholesEnabled, coastalCliffMinimumHeight,
mouthLevelingDistance, maximumOceanApron, maximumPerTile, maximumCoastalPerTile, 0.5D);
}
}
@@ -562,13 +740,13 @@ public record HydrologyPlannerSettings(
}
public static Geometry defaults() {
ChannelShape channel = new ChannelShape(2.4D, 0.28D, 0.24D, 11);
ChannelShape channel = ChannelShape.of(2.4D, 0.28D, 0.24D, 11);
return new Geometry(
new Meanders(64, 12, 0.34D, 0.42D, 0.48D, 1, 82D),
channel,
channel,
channel,
new Drops(2, 1.4D, 2, 0.45D, 2, 1.8D, 8)
Drops.of(2, 1.4D, 2, 0.45D, 2, 1.8D, 8)
);
}
}
@@ -597,11 +775,25 @@ public record HydrologyPlannerSettings(
}
}
/**
* The cross-section of a carved channel. The bed and wall roughness shape every channel; the radial
* and lobe terms shape the organic wall outline of an underground channel or grotto around
* {@code radialBase}, clamped to {@code [radialMinimum, radialMaximum]}; {@code ceilingRoughness}
* roughens a tunnel ceiling; {@code aspectMinimum} and {@code aspectRange} bound a grotto's plan aspect.
*/
public record ChannelShape(
double bedRoundness,
double bedRoughness,
double wallRoughness,
int roughnessWavelength
int roughnessWavelength,
double radialBase,
double radialMinimum,
double radialMaximum,
double primaryLobeStrength,
double detailLobeStrength,
double ceilingRoughness,
double aspectMinimum,
double aspectRange
) {
public ChannelShape {
if (!Double.isFinite(bedRoundness) || bedRoundness < 1D || bedRoundness > 6D
@@ -610,6 +802,22 @@ public record HydrologyPlannerSettings(
|| roughnessWavelength < 3 || roughnessWavelength > 128) {
throw new IllegalArgumentException("Hydrology channel shape is invalid.");
}
if (!Double.isFinite(radialBase) || radialBase < 0.1D || radialBase > 4D
|| !Double.isFinite(radialMinimum) || radialMinimum <= 0D
|| !Double.isFinite(radialMaximum) || radialMaximum < radialMinimum || radialMaximum > 4D
|| !Double.isFinite(primaryLobeStrength) || primaryLobeStrength < 0D || primaryLobeStrength > 1D
|| !Double.isFinite(detailLobeStrength) || detailLobeStrength < 0D || detailLobeStrength > 1D
|| !Double.isFinite(ceilingRoughness) || ceilingRoughness < 0D || ceilingRoughness > 1D
|| !Double.isFinite(aspectMinimum) || aspectMinimum <= 0D || aspectMinimum > 1D
|| !Double.isFinite(aspectRange) || aspectRange < 0D || aspectRange > 1D) {
throw new IllegalArgumentException("Hydrology channel wall shape is invalid.");
}
}
/** The bed and wall roughness with the organic wall outline every channel had before it was configurable. */
public static ChannelShape of(double bedRoundness, double bedRoughness, double wallRoughness, int roughnessWavelength) {
return new ChannelShape(bedRoundness, bedRoughness, wallRoughness, roughnessWavelength,
0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D);
}
}
@@ -620,7 +828,8 @@ public record HydrologyPlannerSettings(
double flowWidthRatio,
int maximumFlowDepth,
double basinWidthRatio,
int maximumBasinDepth
int maximumBasinDepth,
int undergroundCascadeRunPerBlock
) {
public Drops {
if (cascadeRunPerBlock < 1 || cascadeRunPerBlock > 16
@@ -632,6 +841,23 @@ public record HydrologyPlannerSettings(
|| maximumBasinDepth < maximumFlowDepth || maximumBasinDepth > 32) {
throw new IllegalArgumentException("Hydrology drop geometry is invalid.");
}
if (undergroundCascadeRunPerBlock < 0 || undergroundCascadeRunPerBlock > 64) {
throw new IllegalArgumentException("undergroundCascadeRunPerBlock must be between 0 and 64.");
}
}
/** The drop geometry with the underground cascade run left at the routing minimum. */
public static Drops of(
int cascadeRunPerBlock,
double cascadeExponent,
int maximumCascadeStep,
double flowWidthRatio,
int maximumFlowDepth,
double basinWidthRatio,
int maximumBasinDepth
) {
return new Drops(cascadeRunPerBlock, cascadeExponent, maximumCascadeStep, flowWidthRatio, maximumFlowDepth,
basinWidthRatio, maximumBasinDepth, 0);
}
public int flowWidth(int channelWidth) {
@@ -32,13 +32,17 @@ public record HydrologyTerrainSample(
List<String> preferredProfileKeys,
List<String> surfacePoolKeys,
double shoreBiomeWidth,
String confinesKey
String confinesKey,
double shoreWidth,
boolean erosion
) {
/**
* {@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.
* null where rivers may flow anywhere. {@code shoreWidth} is the width in blocks of the level shore
* bench beside the water at this column, {@code NaN} where no policy set one and the dimension's bank
* shore width applies; {@code erosion} is whether the ground beyond that bench is eroded into a valley here.
*/
public HydrologyTerrainSample {
requireFiniteNonNegative(slope, "slope");
@@ -64,6 +68,9 @@ public record HydrologyTerrainSample(
if (!Double.isNaN(shoreBiomeWidth) && (!Double.isFinite(shoreBiomeWidth) || shoreBiomeWidth < 0D)) {
throw new IllegalArgumentException("shoreBiomeWidth must be NaN, or finite and non-negative.");
}
if (!Double.isNaN(shoreWidth) && (!Double.isFinite(shoreWidth) || shoreWidth < 0D)) {
throw new IllegalArgumentException("shoreWidth must be NaN, or finite and non-negative.");
}
confinesKey = confinesKey == null || confinesKey.isBlank() ? null : confinesKey;
}
@@ -72,6 +79,11 @@ public record HydrologyTerrainSample(
return Double.isNaN(shoreBiomeWidth) ? fallback : shoreBiomeWidth;
}
/** The level shore bench width at this column, or {@code fallback} when no policy set one. */
public double shoreWidth(double fallback) {
return Double.isNaN(shoreWidth) ? fallback : shoreWidth;
}
/** 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());
@@ -108,7 +120,9 @@ public record HydrologyTerrainSample(
List.of("default"),
List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -143,7 +157,9 @@ public record HydrologyTerrainSample(
List.of("default"),
List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -178,7 +194,9 @@ public record HydrologyTerrainSample(
preferredProfileKeys,
replacementPoolKeys,
shoreBiomeWidth,
confinesKey
confinesKey,
shoreWidth,
erosion
);
}
@@ -213,7 +231,83 @@ public record HydrologyTerrainSample(
preferredProfileKeys,
surfacePoolKeys,
shoreBiomeWidth,
confinesKey
confinesKey,
shoreWidth,
erosion
);
}
public HydrologyTerrainSample withShoreWidth(double replacementShoreWidth) {
return new HydrologyTerrainSample(
naturalHeight,
slope,
ocean,
caveAvailable,
caveFloorY,
caveFluidY,
transitAllowed,
outletAllowed,
surfaceSourceAllowed,
surfaceSourceRequired,
undergroundSourceAllowed,
undergroundSourceRequired,
routingCost,
surfaceSourceWeight,
undergroundSourceWeight,
widthMultiplier,
depthMultiplier,
incisionMultiplier,
routingMultiplier,
bankMultiplier,
parentBiomeKey,
surfaceBiomeKey,
mouthBiomeKey,
shoreBiomeKey,
bankBiomeKey,
floodedCaveBiomeKey,
preferredProfileKeys,
surfacePoolKeys,
shoreBiomeWidth,
confinesKey,
replacementShoreWidth,
erosion
);
}
public HydrologyTerrainSample withErosion(boolean replacementErosion) {
return new HydrologyTerrainSample(
naturalHeight,
slope,
ocean,
caveAvailable,
caveFloorY,
caveFluidY,
transitAllowed,
outletAllowed,
surfaceSourceAllowed,
surfaceSourceRequired,
undergroundSourceAllowed,
undergroundSourceRequired,
routingCost,
surfaceSourceWeight,
undergroundSourceWeight,
widthMultiplier,
depthMultiplier,
incisionMultiplier,
routingMultiplier,
bankMultiplier,
parentBiomeKey,
surfaceBiomeKey,
mouthBiomeKey,
shoreBiomeKey,
bankBiomeKey,
floodedCaveBiomeKey,
preferredProfileKeys,
surfacePoolKeys,
shoreBiomeWidth,
confinesKey,
shoreWidth,
replacementErosion
);
}
@@ -23,7 +23,9 @@ public record EffectiveRiverPolicy(
double routingMultiplier,
double bankMultiplier,
Double shoreBiomeWidth,
RiverConfinement confinement
RiverConfinement confinement,
Double shoreWidth,
Boolean erosion
) {
public EffectiveRiverPolicy {
placement = Objects.requireNonNull(placement);
@@ -32,6 +34,9 @@ public record EffectiveRiverPolicy(
if (shoreBiomeWidth != null && (!Double.isFinite(shoreBiomeWidth) || shoreBiomeWidth < 0D)) {
throw new IllegalArgumentException("shoreBiomeWidth must be finite and non-negative.");
}
if (shoreWidth != null && (!Double.isFinite(shoreWidth) || shoreWidth < 0D)) {
throw new IllegalArgumentException("shoreWidth must be finite and non-negative.");
}
profiles = List.copyOf(profiles);
surfaceBiomes = List.copyOf(surfaceBiomes);
mouthBiomes = List.copyOf(mouthBiomes);
@@ -77,6 +77,8 @@ public final class RiverPolicyResolver {
private double bankMultiplier = 1D;
private Double shoreBiomeWidth = null;
private RiverConfinement confinement = RiverConfinement.NONE;
private Double shoreWidth = null;
private Boolean erosion = null;
/**
* {@code scope} is the confinement a {@code confined: true} at this level binds courses to; {@code data} is the
@@ -89,6 +91,12 @@ public final class RiverPolicyResolver {
if (policy.getShoreBiomeWidth() != null) {
shoreBiomeWidth = policy.getShoreBiomeWidth();
}
if (policy.getShoreWidth() != null) {
shoreWidth = policy.getShoreWidth();
}
if (policy.getErosion() != null) {
erosion = policy.getErosion();
}
if (policy.getConfined() != null) {
confinement = policy.getConfined() ? scope : RiverConfinement.NONE;
}
@@ -157,7 +165,9 @@ public final class RiverPolicyResolver {
routingMultiplier,
bankMultiplier,
shoreBiomeWidth,
confinement
confinement,
shoreWidth,
erosion
);
}
}
@@ -34,6 +34,7 @@ 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.IrisRiverMouthConfig;
import art.arcane.iris.engine.object.IrisRiverPolicy;
import art.arcane.iris.engine.object.IrisRiverProfile;
import art.arcane.iris.engine.object.IrisRiverRoutingConfig;
@@ -44,8 +45,10 @@ 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.IrisSurfaceRiverConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverErosionConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverFlowConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverPondConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverShapeConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverSourceConfig;
import art.arcane.iris.engine.object.IrisUndergroundRiverConfig;
import art.arcane.iris.engine.object.IrisUndergroundRiverSourceConfig;
@@ -510,11 +513,12 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
if (!rivers.getUnderground().getFluidLevel().isFlat()) {
int lowestFluidY = Math.max(
settings.underground().minimumFluidY(),
settings.underground().maximumDepth() + 1
settings.underground().maximumDepth() + settings.underground().minimumFloorCover()
);
int highestFluidY = Math.min(
settings.underground().maximumFluidY(),
naturalHeight - settings.underground().minimumHeadroom() - 1
naturalHeight - settings.underground().minimumHeadroom()
- settings.underground().minimumRockCover()
);
if (lowestFluidY <= highestFluidY) {
configuredFluidY = lowestFluidY + (highestFluidY - lowestFluidY) / 2;
@@ -522,7 +526,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
}
int caveFloorY = configuredFluidY - settings.underground().maximumDepth();
boolean caveAvailable = caveFloorY > 0
&& configuredFluidY + settings.underground().maximumHeadroom() < naturalHeight;
&& configuredFluidY + settings.underground().maximumHeadroom()
+ settings.underground().minimumRockCover() <= naturalHeight;
boolean routingAllowed = policy.allowsTransit() && policy.allowsRouting() && !ocean;
boolean sourceAllowed = routingAllowed && policy.allowsSources();
double sourceWeight = switch (policy.placement()) {
@@ -564,7 +569,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
profiles,
policy.surfacePools(),
policy.shoreBiomeWidth() == null ? Double.NaN : policy.shoreBiomeWidth(),
confinesKey(policy.confinement(), region, biome)
confinesKey(policy.confinement(), region, biome),
policy.shoreWidth() == null ? Double.NaN : policy.shoreWidth(),
policy.erosion() == null || policy.erosion()
);
return new IrisHydrologyRoutingTerrainSampler.TerrainBasis(resolvedNaturalHeight, terrain);
}
@@ -740,7 +747,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
IrisSurfaceRiverSourceConfig surfaceSources = surface.getSources();
IrisSurfaceRiverChannelConfig channel = surface.getChannel();
IrisSurfaceRiverBankConfig banks = surface.getBanks();
IrisSurfaceRiverErosionConfig erosion = surface.getErosion();
IrisSurfaceRiverFlowConfig flow = surface.getFlow();
IrisRiverMouthConfig mouths = surface.getMouths();
IrisUndergroundRiverConfig underground = rivers.getUnderground();
IrisUndergroundRiverSourceConfig undergroundSources = underground.getSources();
int routeNodes = maximumRouteNodes(routing);
@@ -788,25 +797,45 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
channel.getRoughnessWavelength(),
flow.getCascadeRun(),
flow.getWaterfallMinimumDrop(),
surface.getMouths().getFlareRatio(),
mouths.getFlareRatio(),
new HydrologyPlannerSettings.Inlet(
surface.getMouths().getInletLength(),
surface.getMouths().getInletDepth(),
surface.getMouths().getMaximumIncision()
mouths.getInletLength(),
mouths.getInletDepth(),
mouths.getMaximumIncision(),
mouths.getInletCourseFraction(),
mouths.getInletRampSlope()
),
channel.getSpringWidthRatio(),
channel.getSpringLength(),
banks.isExposeCutStrata(),
new HydrologyPlannerSettings.Erosion(
surface.getErosion().isEnabled(),
surface.getErosion().getSmoothingRadius(),
surface.getErosion().getThalwegFraction(),
surface.getErosion().getBlendCurve(),
surface.getErosion().getBedNoise()
erosion.isEnabled(),
erosion.getSmoothingRadius(),
erosion.getThalwegFraction(),
erosion.getBlendCurve(),
erosion.getBedNoise(),
erosion.getStyle(),
erosion.getTerraceSteps(),
erosion.getCliffFraction(),
erosion.getBedProfile(),
banks.getShoreRise(),
banks.getBlendBaseWidth()
),
new HydrologyPlannerSettings.Ponds(
pond(surface.getPonds().getSource()),
pond(surface.getPonds().getTerminal())
),
new HydrologyPlannerSettings.Channel(
channel.getSmoothingRadius(),
channel.getOutlineMinimumRatio(),
channel.getOutlineMaximumRatio(),
channel.getSpringExtraDepth()
),
new HydrologyPlannerSettings.Flow(
flow.getWaterfallThalwegFraction(),
flow.getPlungeBasinMinimumDrop(),
flow.getPlungeBasinLengthRatio(),
flow.getPlungeBasinDepth()
)
)
);
@@ -832,7 +861,10 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
minimumInt(underground.getHeadroom()),
maximumInt(underground.getHeadroom()),
underground.isConnectToExistingCaves(),
underground.getTributaries()
underground.getTributaries(),
underground.getMinimumRockCover(),
underground.getMinimumFloorCover(),
underground.getWideningSources()
);
IrisRiverGrottoConfig grottos = rivers.getGrottos();
IrisCoastalRiverGrottoConfig coastal = grottos.getCoastal();
@@ -846,20 +878,18 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
grotto(inlandEnabled, inland.getHorizontalRadius(), inland.getVerticalRadius(),
inland.getHeadroom(), inland.getMaximumVolume()),
inlandEnabled && inland.isConnectSurfaceRivers(),
Math.max(4, coastal.getVerticalRadius()),
coastal.getCliffMinimumHeight() == null
? Math.max(4, coastal.getVerticalRadius())
: coastal.getCliffMinimumHeight(),
underground.getMouthLevelingDistance(),
surface.getMouths().getMaximumOceanApron(),
mouths.getMaximumOceanApron(),
routing.getMaximumOutletsPerTile(),
routing.getMaximumCoastalOutletsPerTile()
routing.getMaximumCoastalOutletsPerTile(),
coastal.getCliffSlopeFactor()
);
HydrologyPlannerSettings.Geometry plannerGeometry = geometry(
rivers.getGeometry(),
new HydrologyPlannerSettings.ChannelShape(
2D,
channel.getRoughness(),
channel.getRoughness(),
channel.getRoughnessWavelength()
)
surfaceShape(rivers.getGeometry().getSurface(), channel)
);
return new HydrologyPlannerSettings(
dimension.getFluidHeight(),
@@ -877,23 +907,29 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
}
/**
* 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.
* The widest shore band any policy of the dimension asks for, whether as a shore biome band or as a
* geometric shore width, never below the dimension's 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()));
widest = Math.max(widest, policyShoreReach(dimension.getRiverPolicy()));
for (IrisRegion region : dimension.getAllRegions(data)) {
widest = Math.max(widest, policyShoreBiomeWidth(region.getRiverPolicy()));
widest = Math.max(widest, policyShoreReach(region.getRiverPolicy()));
}
for (IrisBiome biome : dimension.getAllBiomes(data)) {
widest = Math.max(widest, policyShoreBiomeWidth(biome.getRiverPolicy()));
widest = Math.max(widest, policyShoreReach(biome.getRiverPolicy()));
}
return widest;
}
private static double policyShoreBiomeWidth(IrisRiverPolicy policy) {
return policy == null || policy.getShoreBiomeWidth() == null ? 0D : policy.getShoreBiomeWidth();
private static double policyShoreReach(IrisRiverPolicy policy) {
if (policy == null) {
return 0D;
}
double shoreBiomeWidth = policy.getShoreBiomeWidth() == null ? 0D : policy.getShoreBiomeWidth();
double shoreWidth = policy.getShoreWidth() == null ? 0D : policy.getShoreWidth();
return Math.max(shoreBiomeWidth, shoreWidth);
}
private static List<HydrologyPlannerSettings.SurfacePool> surfacePools(List<IrisSurfacePoolConfig> configurations) {
@@ -940,7 +976,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
drops.getFlowWidthRatio(),
drops.getMaximumFlowDepth(),
drops.getBasinWidthRatio(),
drops.getMaximumBasinDepth()
drops.getMaximumBasinDepth(),
drops.getUndergroundCascadeRunPerBlock()
)
);
}
@@ -950,7 +987,36 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
shape.getBedRoundness(),
shape.getBedRoughness(),
shape.getWallRoughness(),
shape.getRoughnessWavelength()
shape.getRoughnessWavelength(),
shape.getRadialBase(),
shape.getRadialMinimum(),
shape.getRadialMaximum(),
shape.getPrimaryLobeStrength(),
shape.getDetailLobeStrength(),
shape.getCeilingRoughness(),
shape.getAspectMinimum(),
shape.getAspectRange()
);
}
// The surface shape's roughness fields are optional and fall back to the channel roughness.
private static HydrologyPlannerSettings.ChannelShape surfaceShape(
IrisSurfaceRiverShapeConfig shape,
IrisSurfaceRiverChannelConfig channel
) {
return new HydrologyPlannerSettings.ChannelShape(
shape.getBedRoundness(),
shape.getBedRoughness() == null ? channel.getRoughness() : shape.getBedRoughness(),
shape.getWallRoughness() == null ? channel.getRoughness() : shape.getWallRoughness(),
shape.getRoughnessWavelength() == null ? channel.getRoughnessWavelength() : shape.getRoughnessWavelength(),
shape.getRadialBase(),
shape.getRadialMinimum(),
shape.getRadialMaximum(),
shape.getPrimaryLobeStrength(),
shape.getDetailLobeStrength(),
shape.getCeilingRoughness(),
shape.getAspectMinimum(),
shape.getAspectRange()
);
}
@@ -978,7 +1044,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
seaCaves.getMaximumPerTile(),
seaCaves.getMinimumSpacing(),
seaCaves.getMinimumCoastHeight(),
seaCaves.getDepth()
seaCaves.getDepth(),
seaCaves.getSweepJitterDegrees()
);
}
@@ -8,8 +8,6 @@ import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import java.util.Objects;
public final class ChannelProfileBuilder {
private static final int SMOOTHING_RADIUS = 16;
private final HydrologyPlannerSettings.Surface surface;
private final HydrologyTerrainSampler sampler;
private final HydrologyGeometrySampler geometry;
@@ -65,17 +63,19 @@ public final class ChannelProfileBuilder {
width[station] = clamp(sampledWidth * widthMultiplier, surface.minimumWidth(), surface.maximumWidth() * 2D);
depth[station] = clamp(sampledDepth * depthMultiplier, 1D, surface.maximumDepth() * 2D);
}
double[] smoothWidth = smooth(width);
double[] smoothDepth = smooth(depth);
// The headwater opens as a spring pool: wider and one block deeper, narrowing to the cruise width.
// Ground that falls away across the pool shrinks it so it never demands a cut the valley solver rejects.
HydrologyPlannerSettings.Channel channel = surface.banks().channel();
double[] smoothWidth = smooth(width, channel.smoothingRadius());
double[] smoothDepth = smooth(depth, channel.smoothingRadius());
// The headwater opens as a spring pool: wider and the spring extra depth deeper, narrowing to the
// cruise width. Ground that falls away across the pool shrinks it so it never demands a cut the
// valley solver rejects.
int spring = Math.min(surface.banks().springLength(), count / 2);
double springRatio = surface.banks().springWidthRatio();
for (int station = 0; station < spring; station++) {
double remaining = 1D - SurfaceNoise.smoothStep(station / (double) spring);
double localRatio = 1D + (springRatio - 1D) * springRoom(centerline, station, smoothWidth[station] * springRatio / 2D);
smoothWidth[station] *= 1D + (localRatio - 1D) * remaining;
smoothDepth[station] += remaining;
smoothDepth[station] += remaining * channel.springExtraDepth();
}
// 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
@@ -122,18 +122,22 @@ public final class ChannelProfileBuilder {
return Math.max(0D, Math.min(1D, 1D - drop / allowance));
}
static double[] smooth(double[] values) {
/** Triangle-weighted running mean over {@code radius} stations either side; a zero radius keeps every sample. */
static double[] smooth(double[] values, int radius) {
int count = values.length;
if (radius <= 0) {
return values.clone();
}
double[] smoothed = new double[count];
for (int station = 0; station < count; station++) {
double total = 0D;
double weight = 0D;
for (int offset = -SMOOTHING_RADIUS; offset <= SMOOTHING_RADIUS; offset++) {
for (int offset = -radius; offset <= radius; offset++) {
int index = station + offset;
if (index < 0 || index >= count) {
continue;
}
double factor = SMOOTHING_RADIUS + 1 - Math.abs(offset);
double factor = radius + 1 - Math.abs(offset);
total += values[index] * factor;
weight += factor;
}
@@ -5,6 +5,8 @@ import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import art.arcane.iris.engine.object.IrisRiverBedProfile;
import art.arcane.iris.engine.object.IrisRiverBlendStyle;
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
@@ -62,9 +64,12 @@ public final class ErosionFieldCompiler {
) {
HydrologyPlannerSettings.Banks banks = surface.banks();
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
HydrologyPlannerSettings.Channel channelSettings = banks.channel();
HydrologyPlannerSettings.Flow flow = banks.flow();
int count = terminal == SurfaceTerminal.OCEAN_MOUTH ? centerline.size() : valley.exposedStations();
double roughness = banks.roughness();
double shoreWidth = surface.shoreWidth();
double shoreRise = erosion.shoreRise();
int sink = banks.sink();
boolean[] basin = basins(valley, channel, count);
double[][] blendWidth = blendWidths(centerline, channel, valley, count);
@@ -76,7 +81,8 @@ public final class ErosionFieldCompiler {
// 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;
double stationShore = stationShoreWidth(centerline, station, shoreWidth);
int radius = (int) StrictMath.ceil(channel.halfWidth()[station] * (1D + roughness) + Math.max(stationShore + widest, stationBand)) + 1;
int stationX = centerline.x()[station];
int stationZ = centerline.z()[station];
for (int deltaZ = -radius; deltaZ <= radius; deltaZ++) {
@@ -113,7 +119,9 @@ public final class ErosionFieldCompiler {
double halfWidth = channel.halfWidth()[station];
double outline = halfWidth * (1D + roughness * SurfaceNoise.signed(
courseSeed ^ OUTLINE_SALT, cellX, cellZ, banks.roughnessWavelength()));
outline = Math.max(Math.max(0.5D, 0.6D * halfWidth), Math.min(1.4D * halfWidth, outline));
outline = Math.max(
Math.max(0.5D, channelSettings.outlineMinimumRatio() * halfWidth),
Math.min(channelSettings.outlineMaximumRatio() * halfWidth, outline));
boolean wet = cellDistance <= outline + 0.25D;
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
if (terrain != null && wet && terminal == SurfaceTerminal.OCEAN_MOUTH
@@ -129,28 +137,32 @@ 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, erosion.thalwegFraction())
double local = 1D + (depth - 1D) * bedFactor(normalized, erosion)
+ erosion.bedNoise() * roughness * SurfaceNoise.signed(courseSeed ^ BED_SALT, cellX, cellZ, banks.roughnessWavelength())
+ (basin[station] ? 1D : 0D);
+ (basin[station] ? flow.plungeBasinDepth() : 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 + sink;
// The geometric shore is the column's own policy width where it sets one, so a beach can be
// wide on one bank and narrow on the other.
double shore = terrain.shoreWidth(shoreWidth);
// 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);
if (cellDistance <= outline + shore + 0.25D) {
int height = Math.min(natural, bankTop + (int) StrictMath.round(shoreRise * benchProgress(cellDistance, outline, shore)));
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
continue;
}
int cut = natural - bankTop;
if (cut <= 0 || !erosion.enabled()) {
int shoreTop = bankTop + (int) StrictMath.round(shoreRise);
int cut = natural - shoreTop;
if (cut <= 0 || !erosion.enabled() || !terrain.erosion()) {
if (shoreBiome) {
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
}
@@ -158,14 +170,14 @@ public final class ErosionFieldCompiler {
}
double side = side(centerline, station, cellX, cellZ);
double width = blendWidth[station][side < 0D ? 0 : 1];
double progress = (cellDistance - outline - shoreWidth) / width;
double progress = (cellDistance - outline - shore) / 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 * blend(progress, erosion.blendCurve())));
int height = Math.min(natural, (int) StrictMath.round(shoreTop + cut * blend(progress, erosion)));
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
}
if (count > 0) {
@@ -200,17 +212,24 @@ public final class ErosionFieldCompiler {
}
HydrologyPlannerSettings.Banks banks = surface.banks();
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
HydrologyPlannerSettings.Channel channelSettings = banks.channel();
double roughness = banks.roughness();
double shoreWidth = surface.shoreWidth();
double shoreRise = erosion.shoreRise();
int sink = banks.sink();
int centreX = centerline.x()[station];
int centreZ = centerline.z()[station];
// One bowl, one beach: the pond takes the shore width and the erosion switch its centre asks for.
HydrologyTerrainSample centre = sampler.sample(centreX, centreZ);
double shore = centre == null ? shoreWidth : centre.shoreWidth(shoreWidth);
boolean erode = centre == null || centre.erosion();
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;
int shoreTop = bankTop + (int) StrictMath.round(shoreRise);
int reach = (int) StrictMath.ceil(radius * (1D + roughness) + shore + 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);
@@ -231,10 +250,12 @@ public final class ErosionFieldCompiler {
}
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));
outline = Math.max(
Math.max(0.5D, channelSettings.outlineMinimumRatio() * radius),
Math.min(channelSettings.outlineMaximumRatio() * 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())
double local = 1D + (pond.depth() - 1D) * bedFactor(normalized, erosion)
+ 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;
@@ -250,21 +271,22 @@ public final class ErosionFieldCompiler {
if (existing != null && existing.role() == SurfaceRole.CHANNEL) {
continue;
}
if (distance <= outline + shoreWidth + 0.25D) {
int height = Math.min(natural, bankTop);
if (distance <= outline + shore + 0.25D) {
int height = Math.min(natural, bankTop + (int) StrictMath.round(shoreRise * benchProgress(distance, outline, shore)));
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, height, height, false));
continue;
}
int cut = natural - bankTop;
if (cut <= 0 || !erosion.enabled()) {
int cut = natural - shoreTop;
if (cut <= 0 || !erosion.enabled() || !erode) {
continue;
}
double width = Math.max(banks.minimumBlendWidth(), Math.min(banks.maximumBlendWidth(), cut * banks.blendSlope()));
double progress = (distance - outline - shoreWidth) / width;
double width = Math.max(banks.minimumBlendWidth(),
Math.min(banks.maximumBlendWidth(), cut * banks.blendSlope() + erosion.blendBaseWidth()));
double progress = (distance - outline - shore) / width;
if (progress >= 1D) {
continue;
}
int height = Math.min(natural, (int) StrictMath.round(bankTop + cut * blend(progress, erosion.blendCurve())));
int height = Math.min(natural, (int) StrictMath.round(shoreTop + cut * blend(progress, erosion)));
if (existing != null && existing.height() <= height) {
continue;
}
@@ -308,6 +330,60 @@ public final class ErosionFieldCompiler {
return SurfaceNoise.smoothStep(Math.pow(progress, curve));
}
/**
* The configured valley side, from the shore top (0) to natural terrain (1). The smooth style is the
* eased blend the valley always had; the others read the same progress through a different curve.
*/
static double blend(double progress, HydrologyPlannerSettings.Erosion erosion) {
double curve = erosion.blendCurve();
IrisRiverBlendStyle style = erosion.style();
if (style == IrisRiverBlendStyle.SMOOTH) {
return blend(progress, curve);
}
return switch (style) {
case LINEAR -> Math.pow(progress, curve);
case CONCAVE -> 1D - Math.pow(1D - progress, curve);
case TERRACED -> {
int steps = erosion.terraceSteps();
yield Math.floor(blend(progress, curve) * steps) / steps;
}
case CLIFF -> progress < erosion.cliffFraction() ? 0D : 1D;
default -> blend(progress, curve);
};
}
/** How far across the shore bench a cell sits, 0 at the waterline and 1 at the valley foot. */
private static double benchProgress(double cellDistance, double outline, double shore) {
if (shore <= 0D) {
return 0D;
}
return Math.max(0D, Math.min(1D, (cellDistance - outline) / shore));
}
/**
* The configured bed cross-section factor, 1 at full depth and 0 at the one-block channel edge;
* the bowl is the broad thalweg the bed always had.
*/
static double bedFactor(double normalized, HydrologyPlannerSettings.Erosion erosion) {
double thalweg = erosion.thalwegFraction();
IrisRiverBedProfile profile = erosion.bedProfile();
if (profile == IrisRiverBedProfile.BOWL) {
return bowl(normalized, thalweg);
}
return switch (profile) {
case FLAT -> 1D;
case V -> 1D - normalized;
case U -> {
if (normalized <= thalweg) {
yield 1D;
}
double u = Math.min(1D, (normalized - thalweg) / (1D - thalweg));
yield 1D - u * u * u * u;
}
default -> bowl(normalized, thalweg);
};
}
/**
* 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.
@@ -318,6 +394,12 @@ public final class ErosionFieldCompiler {
return terrain == null ? shoreWidth : terrain.shoreBiomeWidth(shoreWidth);
}
/** The geometric shore the terrain under a station asks for, or the dimension width without a policy. */
private double stationShoreWidth(SurfaceCenterline centerline, int station, double shoreWidth) {
HydrologyTerrainSample terrain = sampler.sample(centerline.x()[station], centerline.z()[station]);
return terrain == null ? shoreWidth : terrain.shoreWidth(shoreWidth);
}
private static void connectSteps(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys) {
for (int index = 0; index < wetKeys.size(); index++) {
long key = wetKeys.getLong(index);
@@ -412,13 +494,14 @@ public final class ErosionFieldCompiler {
}
private boolean[] basins(ValleyProfile valley, ChannelProfile channel, int count) {
HydrologyPlannerSettings.Flow flow = surface.banks().flow();
boolean[] basin = new boolean[count];
for (int station = 1; station < count; station++) {
int drop = valley.head()[station - 1] - valley.head()[station];
if (drop < 2) {
if (drop < flow.plungeBasinMinimumDrop()) {
continue;
}
int length = Math.max(2, (int) StrictMath.round(2D * channel.halfWidth()[station]));
int length = Math.max(2, (int) StrictMath.round(flow.plungeBasinLengthRatio() * channel.halfWidth()[station]));
for (int following = station; following < Math.min(count, station + length); following++) {
basin[following] = true;
}
@@ -428,13 +511,14 @@ public final class ErosionFieldCompiler {
private double[][] blendWidths(SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley, int count) {
HydrologyPlannerSettings.Banks banks = surface.banks();
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
double[][] widths = new double[count][2];
if (!banks.erosion().enabled()) {
if (!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);
double probe = halfWidth + stationShoreWidth(centerline, station, surface.shoreWidth()) + Math.max(2D, halfWidth);
int bankTop = valley.head()[station] + banks.sink();
for (int side = 0; side < 2; side++) {
double direction = side == 0 ? -1D : 1D;
@@ -442,11 +526,21 @@ public final class ErosionFieldCompiler {
int probeZ = (int) StrictMath.round(centerline.z()[station] + centerline.normalZ(station) * probe * direction);
HydrologyTerrainSample terrain = sampler.sample(probeX, probeZ);
double cut = terrain == null ? 0D : Math.max(0D, terrain.naturalHeight() - bankTop);
double width = cut * banks.blendSlope() * channel.bankMultiplier()[station];
double width = cut * banks.blendSlope() * channel.bankMultiplier()[station] + erosion.blendBaseWidth();
widths[station][side] = Math.max(banks.minimumBlendWidth(), Math.min(banks.maximumBlendWidth(), width));
}
}
return smoothWidths(widths, banks.erosion().smoothingRadius());
double[][] smoothed = smoothWidths(widths, erosion.smoothingRadius());
// A station whose ground asks for no erosion keeps its channel, its bench and its lip; the
// averaging above may not carry a neighbour's valley into it.
for (int station = 0; station < count; station++) {
HydrologyTerrainSample centre = sampler.sample(centerline.x()[station], centerline.z()[station]);
if (centre != null && !centre.erosion()) {
smoothed[station][0] = 0D;
smoothed[station][1] = 0D;
}
}
return smoothed;
}
/**
@@ -120,17 +120,18 @@ public final class ValleyProfileSolver {
}
// 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.
// a rise the cap cannot pass ends it there. Above it the approach grades down the inlet ramp
// slope 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 the
// inlet course fraction of 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 reach = Math.min(inlet.length(), (int) StrictMath.floor(exposed * inlet.courseFraction()));
int inletStart = exposed;
while (inletStart > 0 && exposed - inletStart < reach
&& cutFits(inletStart - 1, seaLevel, channel, centerNatural, incisionMultiplier, inletIncision)) {
@@ -143,7 +144,7 @@ public final class ValleyProfileSolver {
// 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);
int target = seaLevel + (int) StrictMath.round((inletStart - station) * inlet.rampSlope());
if (head[station] <= target) {
continue;
}
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
import art.arcane.iris.engine.framework.Engine;
@@ -27,7 +28,9 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisHydrology;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisRiverMaterialConfig;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.util.project.context.ChunkContext;
@@ -690,6 +693,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (floorBiome != null) {
HydrologyCaveCell floorHydrology = dataIfPresent(
mantleChunk, rx, zoneFloor, rz, HydrologyCaveCell.class);
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial();
KList<PlatformBlockState> floorLayers = floorBiome.generateLayers(
getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex());
for (int i = 0; i < zoneFloor - 1; i++) {
@@ -709,7 +713,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
PlatformBlockState existing = output.getRaw(rx, floorY, rz);
PlatformBlockState layer = resolveSubmergedCaveFloorLayer(
output, rx, floorY, rz, floorLayers.get(i), floorHydrology);
output, rx, floorY, rz,
paintUndergroundBedMaterial(
floorLayers.get(i), bedMaterial, floorHydrology, i,
rng, worldX, floorY, worldZ, getData()),
floorHydrology);
if (!B.isSolid(existing)
|| !canReplaceHydrologyGuard(hydrology, layer, false)
|| !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
@@ -804,6 +812,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (floorBiome != null) {
HydrologyCaveCell floorHydrology = dataIfPresent(
mc, rx, zone.floor, rz, HydrologyCaveCell.class);
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial();
KList<PlatformBlockState> floorBlocks = floorBiome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex());
for (int i = 0; i < zone.floor - 1; i++) {
if (!floorBlocks.hasIndex(i)) {
@@ -817,7 +826,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
continue;
}
PlatformBlockState block = resolveSubmergedCaveFloorLayer(
output, rx, y, rz, floorBlocks.get(i), floorHydrology);
output, rx, y, rz,
paintUndergroundBedMaterial(
floorBlocks.get(i), bedMaterial, floorHydrology, i,
rng, xx, y, zz, getData()),
floorHydrology);
PlatformBlockState existing = output.getRaw(rx, y, rz);
if (!B.isSolid(existing)
|| !canReplaceHydrologyGuard(hydrology, block, false)
@@ -925,6 +938,44 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState);
}
/** The dimension's underground river bed palette, or null when nothing paints. */
private IrisRiverMaterialConfig undergroundBedMaterial() {
IrisHydrology hydrology = getDimension().getHydrology();
if (hydrology == null) {
return null;
}
IrisRiverMaterialConfig material = hydrology.getRivers().getUnderground().getBedMaterial();
return material != null && material.isEnabled() ? material : null;
}
/**
* Replaces a cave-floor biome layer with the underground bed palette for the top
* {@code index} layers under a hydrology cell. Seal guards keep their biome layers.
*/
static PlatformBlockState paintUndergroundBedMaterial(
PlatformBlockState layer,
IrisRiverMaterialConfig material,
HydrologyCaveCell floorHydrology,
int index,
RNG rng,
int x,
int y,
int z,
IrisData data
) {
if (material == null
|| !material.isEnabled()
|| floorHydrology == null
|| floorHydrology.action() == HydrologyCaveAction.SEAL_GUARD
|| index >= material.getDepth()) {
return layer;
}
PlatformBlockState painted = material.getPalette().get(rng, x, y, z, data);
return painted == null ? layer : painted;
}
static boolean canReplaceHydrologyGuard(
HydrologyCaveCell hydrology,
PlatformBlockState layer,
@@ -229,9 +229,21 @@ public class IrisBlockData extends IrisRegistrant {
return data.getBlockLoader() != null && data.getBlockLoader().load(getBlock(), false) != null;
}
/** Registry, legacy rename table, then dimension blockFallbacks; null when the state is missing on this server. */
/**
* Registry, legacy rename table, then dimension blockFallbacks; null when the state is missing on this server.
* <p>
* A gate that is not ready (its registry cannot be enumerated, so every status is
* {@link art.arcane.iris.core.compat.KeyStatus#UNKNOWN}) gates nothing and answers null for every key, so this
* keeps the plain registry lookup in that case - otherwise every block in the pack would resolve to air.
*/
private static PlatformBlockState resolve(IrisData data, String state) {
ContentGate.BlockResolution resolution = data.getContentGate().resolveBlock(state);
ContentGate gate = data.getContentGate();
if (gate == null || !gate.ready()) {
return B.getStateOrNull(state, false);
}
ContentGate.BlockResolution resolution = gate.resolveBlock(state);
return resolution == null ? null : resolution.state();
}
@@ -38,6 +38,16 @@ public class IrisCoastalRiverGrottoConfig {
@Desc("Maximum accepted coastal grotto volume in blocks.")
private int maximumVolume = 8192;
@MinNumber(0)
@MaxNumber(128)
@Desc("Blocks the coast must stand above the sea at the outlet for a river to end in a coastal grotto instead of an open mouth. Null uses the larger of 4 and verticalRadius.")
private Integer cliffMinimumHeight = null;
@MinNumber(0)
@MaxNumber(4)
@Desc("Share of cliffMinimumHeight the coast's slope must reach at the outlet before a grotto is chosen over an open mouth; 0 ignores the slope and decides on height alone.")
private double cliffSlopeFactor = 0.5D;
@Desc("Standalone sea caves: coastal grottos opening from the ocean into the coast without a river.")
private IrisSeaCaveConfig seaCaves = new IrisSeaCaveConfig();
}
@@ -0,0 +1,15 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("The cross-section of the wet channel bed from the centerline out to the waterline.")
public enum IrisRiverBedProfile {
@Desc("A level thalweg over thalwegFraction of the half-width that eases up to a one-block edge; a broad bowl.")
BOWL,
@Desc("Full depth across the whole width, so the channel edge drops straight to the bed.")
FLAT,
@Desc("A straight slope from full depth at the centerline to one block at the edge; thalwegFraction is ignored.")
V,
@Desc("A level thalweg over thalwegFraction of the half-width that stays deep almost to the edge, then rises steeply; a trough with steep sides.")
U
}
@@ -0,0 +1,17 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("The shape of the eroded valley side between the shore band and natural terrain.")
public enum IrisRiverBlendStyle {
@Desc("An eased S-curve from the bank top out to natural terrain, flat at both ends and steepest across the middle of the band; blendCurve skews it.")
SMOOTH,
@Desc("A straight slope from the bank top to natural terrain with a sharp shoulder at the shore and a sharp lip at the top; blendCurve bends it.")
LINEAR,
@Desc("A hollowed valley side that climbs fast beside the shore and flattens out toward natural terrain; blendCurve deepens the hollow.")
CONCAVE,
@Desc("The eased curve cut into level steps, terraceSteps of them between the bank top and natural terrain.")
TERRACED,
@Desc("A level bench at the bank top for cliffFraction of the band, then a vertical wall up to natural terrain.")
CLIFF
}
@@ -31,4 +31,44 @@ public class IrisRiverChannelShapeConfig {
@MaxNumber(128)
@Desc("Wavelength in blocks of bed and wall roughness.")
private int roughnessWavelength = 11;
@MinNumber(0.4)
@MaxNumber(1.2)
@Desc("Base radial scale of the organic passage outline before the lobes are added; 1 keeps the nominal radius, lower values carve a narrower passage.")
private double radialBase = 0.86D;
@MinNumber(0.2)
@MaxNumber(1)
@Desc("Smallest radial scale the organic outline may shrink to after the lobes are applied, as a fraction of the nominal radius.")
private double radialMinimum = 0.58D;
@MinNumber(1)
@MaxNumber(2)
@Desc("Largest radial scale the organic outline may grow to after the lobes are applied, as a fraction of the nominal radius.")
private double radialMaximum = 1.18D;
@MinNumber(0)
@MaxNumber(0.5)
@Desc("Strength of the broad lobes that bulge and pinch the passage outline along its length; 0 leaves the outline circular.")
private double primaryLobeStrength = 0.08D;
@MinNumber(0)
@MaxNumber(0.5)
@Desc("Strength of the fine lobes layered over the broad ones for small-scale wall detail; 0 leaves only the broad lobes.")
private double detailLobeStrength = 0.06D;
@MinNumber(0)
@MaxNumber(1)
@Desc("Coherent variation of the carved ceiling height as a fraction of the headroom; 0 keeps the ceiling smooth and evaluates no extra noise.")
private double ceilingRoughness = 0D;
@MinNumber(0.2)
@MaxNumber(1)
@Desc("Narrowest plan aspect of a chamber, the short axis as a fraction of the long axis; 1 makes every chamber circular in plan. Applies to grottos only.")
private double aspectMinimum = 0.62D;
@MinNumber(0)
@MaxNumber(0.8)
@Desc("How much the plan aspect may vary above aspectMinimum from one chamber to the next; 0 gives every chamber the same aspect. Applies to grottos only.")
private double aspectRange = 0.2D;
}
@@ -46,4 +46,9 @@ public class IrisRiverDropShapeConfig {
@MaxNumber(32)
@Desc("Maximum receiving-basin depth after drop-scaled erosion.")
private int maximumBasinDepth = 8;
@MinNumber(0)
@MaxNumber(16)
@Desc("Horizontal run in blocks an underground drop spreads over per block of head loss, so tall drops become long cascades; 0 keeps every underground drop at its shortest run.")
private int undergroundCascadeRunPerBlock = 0;
}
@@ -13,6 +13,9 @@ public class IrisRiverGeometryConfig {
@Desc("Surface and underground centerline meanders.")
private IrisRiverMeanderConfig meanders = new IrisRiverMeanderConfig();
@Desc("Surface river bed shape for waterfall and cascade throats, and the wall shape shared with the erosion compiler; roughness fields default to the channel roughness.")
private IrisSurfaceRiverShapeConfig surface = new IrisSurfaceRiverShapeConfig();
@Desc("Underground channel bed and wall shape.")
private IrisRiverChannelShapeConfig underground = new IrisRiverChannelShapeConfig();
@@ -0,0 +1,25 @@
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("An optional block palette painted over the top layers of one part of a river instead of the biome's own layers.")
@Data
public class IrisRiverMaterialConfig {
@Desc("Paint this palette. When false the biome layers are used as before.")
private boolean enabled = false;
@Desc("Blocks to paint. Any solid palette.")
private IrisMaterialPalette palette = new IrisMaterialPalette();
@MinNumber(1)
@MaxNumber(8)
@Desc("How many layers down from the surface the palette replaces, in blocks.")
private int depth = 1;
}
@@ -36,4 +36,14 @@ public class IrisRiverMouthConfig {
@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;
@MinNumber(0.05)
@MaxNumber(1)
@Desc("Largest share of the river's exposed course the drowned inlet may occupy, so a short river keeps most of its length above sea level; the inlet is the shorter of inletLength and this share.")
private double inletCourseFraction = 0.5D;
@MinNumber(0.1)
@MaxNumber(4)
@Desc("Blocks of head gained per station on the approach ramp climbing from the inlet back to the river's natural profile; 1 climbs one block per station, higher values grade the approach faster.")
private double inletRampSlope = 1D;
}
@@ -89,6 +89,14 @@ public class IrisRiverPolicy {
@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;
@MinNumber(0)
@MaxNumber(16)
@Desc("Width in blocks of the flattened shore bench beside the water in this area, the geometric beach that is cut level with the bank top. Null inherits, and without any value the bench is the surface banks shoreWidth. Zero gives the area no bench, so the eroded valley side begins at the waterline.")
private Double shoreWidth = null;
@Desc("Whether surface rivers erode the ground beyond the shore bench into a valley in this area. Null inherits, and without any value erosion follows surface.erosion.enabled. False keeps only the wet channel and the shore bench here, so a river crosses the area in a bare cut.")
private Boolean erosion = 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;
@@ -34,4 +34,9 @@ public class IrisSeaCaveConfig {
@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;
@MinNumber(0)
@MaxNumber(90)
@Desc("Largest angle in degrees the inland sweep may turn away from straight inland, chosen per cave; 0 sweeps every sea cave straight in from the shore.")
private double sweepJitterDegrees = 25D;
}
@@ -12,9 +12,9 @@ import lombok.experimental.Accessors;
@Desc("Controls the shore band and the eroded valley around a surface river.")
@Data
public class IrisSurfaceRiverBankConfig {
@MinNumber(0.5)
@MaxNumber(6)
@Desc("Width in blocks of the shore band that receives shore biome content.")
@MinNumber(0)
@MaxNumber(16)
@Desc("Width in blocks of the flattened shore bench beside the water, the geometric beach cut level with the bank top, and the default width of the shore biome band. 0 removes the bench so the eroded valley side begins at the waterline; riverPolicy.shoreWidth overrides it per area.")
private double shoreWidth = 1.5D;
@MinNumber(0.5)
@@ -34,4 +34,20 @@ public class IrisSurfaceRiverBankConfig {
@Desc("Show the biome's deeper layers on eroded banks instead of the surface layer.")
private boolean exposeCutStrata = true;
@MinNumber(0)
@MaxNumber(4)
@Desc("Blocks the shore bench rises from the waterline to its landward edge, so the beach climbs instead of lying level; 0 keeps a flat bench. The eroded valley side starts from the raised edge.")
private double shoreRise = 0D;
@MinNumber(0)
@MaxNumber(32)
@Desc("Blocks added to every eroded valley width before the blend width limits apply, so even a shallow cut erodes at least this far beyond the shore; 0 leaves the width proportional to the cut alone.")
private double blendBaseWidth = 0D;
@Desc("Optional palette painted over the shore bench columns instead of the biome's own layers. Disabled by default, which keeps the shore biome's layers.")
private IrisRiverMaterialConfig shoreMaterial = new IrisRiverMaterialConfig();
@Desc("Optional palette painted over the eroded bank columns outside the shore bench instead of the biome's own layers. Disabled by default, which keeps the bank biome's layers.")
private IrisRiverMaterialConfig bankMaterial = new IrisRiverMaterialConfig();
}
@@ -22,4 +22,7 @@ public class IrisSurfaceRiverBedConfig {
@Desc("Blocks used in place of falling blocks in the bed, padding, shore and banks.")
private IrisMaterialPalette paddingPalette = new IrisMaterialPalette().qclear().qadd("clay").qadd("dirt");
@Desc("Optional palette painted over the wet channel bed under the water instead of the biome's own layers. Disabled by default, which keeps the surface biome's layers.")
private IrisRiverMaterialConfig material = new IrisRiverMaterialConfig();
}
@@ -48,6 +48,26 @@ public class IrisSurfaceRiverChannelConfig {
@Desc("Blocks over which the spring pool narrows back to the channel width.")
private int springLength = 24;
@MinNumber(0)
@MaxNumber(64)
@Desc("Stations along the course the sampled width and depth are averaged over, so the channel changes size gradually; 0 follows the sampled values exactly at every station.")
private int smoothingRadius = 16;
@MinNumber(0.2)
@MaxNumber(1)
@Desc("Narrowest the roughened waterline may pinch, as a fraction of the channel half-width; 1 stops the outline from ever narrowing below the nominal width.")
private double outlineMinimumRatio = 0.6D;
@MinNumber(1)
@MaxNumber(3)
@Desc("Widest the roughened waterline may bulge, as a fraction of the channel half-width; 1 stops the outline from ever widening beyond the nominal width.")
private double outlineMaximumRatio = 1.4D;
@MinNumber(0)
@MaxNumber(8)
@Desc("Extra bed depth in blocks at the headwater spring, fading to nothing over springLength; 0 keeps the spring pool as deep as the channel.")
private double springExtraDepth = 1D;
private static IrisStyledRange range(double min, double max, double zoom) {
IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(zoom);
return new IrisStyledRange(min, max, style);
@@ -34,4 +34,20 @@ public class IrisSurfaceRiverErosionConfig {
@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;
@Desc("Shape of the valley side between the shore bench and natural terrain. SMOOTH is the eased curve used before this field existed.")
private IrisRiverBlendStyle style = IrisRiverBlendStyle.SMOOTH;
@MinNumber(2)
@MaxNumber(16)
@Desc("Number of level steps the valley side is cut into when style is TERRACED; ignored by every other style.")
private int terraceSteps = 4;
@MinNumber(0)
@MaxNumber(1)
@Desc("Share of the eroded band kept level at the bank top before the vertical wall when style is CLIFF; ignored by every other style.")
private double cliffFraction = 0.5D;
@Desc("Cross-section of the wet channel bed from the centerline out to the waterline. BOWL is the profile used before this field existed.")
private IrisRiverBedProfile bedProfile = IrisRiverBedProfile.BOWL;
}
@@ -21,4 +21,24 @@ public class IrisSurfaceRiverFlowConfig {
@MaxNumber(32)
@Desc("Smallest natural cliff in blocks that becomes a waterfall instead of a cascade.")
private int waterfallMinimumDrop = 6;
@MinNumber(0)
@MaxNumber(0.95)
@Desc("Share of the half-width that stays at full bed depth in the carved throat of a waterfall or cascade before the bed rises to the edge; higher is a flatter, broader throat.")
private double waterfallThalwegFraction = 0.65D;
@MinNumber(1)
@MaxNumber(8)
@Desc("Smallest drop in blocks that scours a plunge basin into the bed below it; drops shorter than this leave the bed untouched.")
private int plungeBasinMinimumDrop = 2;
@MinNumber(0)
@MaxNumber(8)
@Desc("Length of the plunge basin downstream of a drop as a multiple of the channel half-width, never shorter than two stations; 0 gives the shortest basin.")
private double plungeBasinLengthRatio = 2D;
@MinNumber(0)
@MaxNumber(8)
@Desc("Extra bed depth in blocks scoured inside a plunge basin; 0 marks the basin without deepening it.")
private int plungeBasinDepth = 1;
}
@@ -0,0 +1,74 @@
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 the bed shape of surface river waterfall and cascade throats and the wall shape shared with the erosion compiler. The roughness fields fall back to the surface channel roughness when left null.")
@Data
public class IrisSurfaceRiverShapeConfig {
@MinNumber(1)
@MaxNumber(6)
@Desc("Cross-section exponent of the throat bed. Larger values broaden the rounded U-shaped bed.")
private double bedRoundness = 2D;
@MinNumber(0)
@MaxNumber(1)
@Desc("Coherent vertical variation applied to the throat bed as a fraction of channel depth. Null uses surface.channel.roughness.")
private Double bedRoughness = null;
@MinNumber(0)
@MaxNumber(1)
@Desc("Coherent radial variation applied to the throat walls. Null uses surface.channel.roughness.")
private Double wallRoughness = null;
@MinNumber(3)
@MaxNumber(128)
@Desc("Wavelength in blocks of the throat bed and wall roughness. Null uses surface.channel.roughnessWavelength.")
private Integer roughnessWavelength = null;
@MinNumber(0.4)
@MaxNumber(1.2)
@Desc("Base radial scale of the organic passage outline before the lobes are added; 1 keeps the nominal radius, lower values carve a narrower passage.")
private double radialBase = 0.86D;
@MinNumber(0.2)
@MaxNumber(1)
@Desc("Smallest radial scale the organic outline may shrink to after the lobes are applied, as a fraction of the nominal radius.")
private double radialMinimum = 0.58D;
@MinNumber(1)
@MaxNumber(2)
@Desc("Largest radial scale the organic outline may grow to after the lobes are applied, as a fraction of the nominal radius.")
private double radialMaximum = 1.18D;
@MinNumber(0)
@MaxNumber(0.5)
@Desc("Strength of the broad lobes that bulge and pinch the passage outline along its length; 0 leaves the outline circular.")
private double primaryLobeStrength = 0.08D;
@MinNumber(0)
@MaxNumber(0.5)
@Desc("Strength of the fine lobes layered over the broad ones for small-scale wall detail; 0 leaves only the broad lobes.")
private double detailLobeStrength = 0.06D;
@MinNumber(0)
@MaxNumber(1)
@Desc("Coherent variation of the carved ceiling height as a fraction of the headroom; 0 keeps the ceiling smooth and evaluates no extra noise. Only the carved segments of a surface course read it; an exposed channel has no ceiling.")
private double ceilingRoughness = 0D;
@MinNumber(0.2)
@MaxNumber(1)
@Desc("Narrowest plan aspect of a chamber, the short axis as a fraction of the long axis; 1 makes every chamber circular in plan. The surface section carries this field so all three shape sections share one shape record; only grotto chambers read it.")
private double aspectMinimum = 0.62D;
@MinNumber(0)
@MaxNumber(0.8)
@Desc("How much the plan aspect may vary above aspectMinimum from one chamber to the next; 0 gives every chamber the same aspect. The surface section carries this field so all three shape sections share one shape record; only grotto chambers read it.")
private double aspectRange = 0.2D;
}
@@ -43,6 +43,24 @@ public class IrisUndergroundRiverConfig {
@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;
@MinNumber(1)
@MaxNumber(64)
@Desc("Blocks of solid rock kept between the top of a passage's headroom and the surface above it; a passage that cannot keep this much cover is lowered or rejected.")
private int minimumRockCover = 1;
@MinNumber(1)
@MaxNumber(32)
@Desc("Blocks of solid rock kept between the bottom of a passage's bed and the world floor below it.")
private int minimumFloorCover = 1;
@MinNumber(1)
@MaxNumber(64)
@Desc("Number of joined tributary sources at which a passage reaches its full sampled width; fewer contributing sources carve a proportionally narrower passage, and 1 gives every passage its full width.")
private int wideningSources = 8;
@Desc("Optional palette painted over the floor layers under an underground river instead of the cave biome's own layers. Disabled by default, which keeps the biome's layers.")
private IrisRiverMaterialConfig bedMaterial = new IrisRiverMaterialConfig();
private static IrisStyledRange range(double min, double max, double zoom) {
return new IrisStyledRange(min, max, new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(zoom));
}
@@ -40,7 +40,9 @@ public class PackHydrologyValidatorTest {
"riverPolicy": {
"routing": "PREFER",
"outletAdmission": null,
"shoreBiomes": ["river_shore"]
"shoreBiomes": ["river_shore"],
"shoreWidth": null,
"erosion": null
}
}
""");
@@ -85,6 +87,60 @@ public class PackHydrologyValidatorTest {
"slopePenalty": 0,
"confluenceAttraction": 0
},
"geometry": {
"surface": {
"bedRoundness": 1,
"bedRoughness": null,
"wallRoughness": null,
"roughnessWavelength": null,
"radialBase": 0.4,
"radialMinimum": 0.2,
"radialMaximum": 1,
"primaryLobeStrength": 0,
"detailLobeStrength": 0,
"ceilingRoughness": 0,
"aspectMinimum": 0.2,
"aspectRange": 0
},
"underground": {
"bedRoundness": 1,
"bedRoughness": 0,
"wallRoughness": 0,
"roughnessWavelength": 3,
"radialBase": 0.4,
"radialMinimum": 0.2,
"radialMaximum": 1,
"primaryLobeStrength": 0,
"detailLobeStrength": 0,
"ceilingRoughness": 0,
"aspectMinimum": 0.2,
"aspectRange": 0
},
"grottos": {
"bedRoundness": 1,
"bedRoughness": 0,
"wallRoughness": 0,
"roughnessWavelength": 3,
"radialBase": 0.4,
"radialMinimum": 0.2,
"radialMaximum": 1,
"primaryLobeStrength": 0,
"detailLobeStrength": 0,
"ceilingRoughness": 0,
"aspectMinimum": 0.2,
"aspectRange": 0
},
"drops": {
"cascadeRunPerBlock": 1,
"cascadeExponent": 0.25,
"maximumCascadeStep": 1,
"flowWidthRatio": 0.25,
"maximumFlowDepth": 1,
"basinWidthRatio": 1,
"maximumBasinDepth": 1,
"undergroundCascadeRunPerBlock": 0
}
},
"surface": {
"channel": {
"width": {"min": 1, "max": 1},
@@ -92,29 +148,78 @@ public class PackHydrologyValidatorTest {
"sink": 0,
"maximumIncision": 1,
"roughness": 0,
"roughnessWavelength": 4
"roughnessWavelength": 4,
"springWidthRatio": 1,
"springLength": 4,
"smoothingRadius": 0,
"outlineMinimumRatio": 0.2,
"outlineMaximumRatio": 1,
"springExtraDepth": 0
},
"banks": {
"shoreWidth": 0.5,
"shoreWidth": 0,
"blendSlope": 0.5,
"minimumBlendWidth": 1,
"maximumBlendWidth": 1,
"exposeCutStrata": false
"exposeCutStrata": false,
"shoreRise": 0,
"blendBaseWidth": 0,
"shoreMaterial": {"enabled": false, "palette": {"palette": [{"block": "minecraft:stone"}]}, "depth": 1},
"bankMaterial": {"enabled": false, "palette": {"palette": [{"block": "minecraft:stone"}]}, "depth": 1}
},
"flow": {"cascadeRun": 1, "waterfallMinimumDrop": 2},
"mouths": {"flareRatio": 1, "maximumOceanApron": 0}
"bed": {
"allowGravityBlocks": false,
"padding": 0,
"material": {"enabled": false, "palette": {"palette": [{"block": "minecraft:stone"}]}, "depth": 1}
},
"flow": {
"cascadeRun": 1,
"waterfallMinimumDrop": 2,
"waterfallThalwegFraction": 0,
"plungeBasinMinimumDrop": 1,
"plungeBasinLengthRatio": 0,
"plungeBasinDepth": 0
},
"mouths": {
"flareRatio": 1,
"maximumOceanApron": 0,
"inletLength": 0,
"inletDepth": 0,
"maximumIncision": 0,
"inletCourseFraction": 0.05,
"inletRampSlope": 0.1
},
"erosion": {
"enabled": false,
"smoothingRadius": 0,
"thalwegFraction": 0,
"blendCurve": 0.25,
"bedNoise": 0,
"style": "SMOOTH",
"terraceSteps": 2,
"cliffFraction": 0,
"bedProfile": "BOWL"
}
},
"underground": {
"mouthLevelingDistance": 16,
"minimumRockCover": 1,
"minimumFloorCover": 1,
"wideningSources": 1,
"bedMaterial": {"enabled": false, "palette": {"palette": [{"block": "minecraft:stone"}]}, "depth": 1}
},
"underground": {"mouthLevelingDistance": 16},
"grottos": {
"coastal": {
"horizontalRadius": 8,
"verticalRadius": 1,
"headroom": 1,
"seaCaves": {"enabled": false, "maximumPerTile": 0, "minimumSpacing": 16, "minimumCoastHeight": 1, "depth": 0}
"cliffMinimumHeight": 0,
"cliffSlopeFactor": 0,
"seaCaves": {"enabled": false, "maximumPerTile": 0, "minimumSpacing": 16, "minimumCoastHeight": 1, "depth": 0, "sweepJitterDegrees": 0}
}
}
}
""", "\"bankMultiplier\": 0"));
""", "\"bankMultiplier\": 0, \"shoreWidth\": 0, \"erosion\": false"));
PackHydrologyValidator.Validation result = validate(pack);
@@ -131,16 +236,34 @@ public class PackHydrologyValidatorTest {
"bed": {
"allowGravityBlocks": false,
"padding": 9,
"paddingPalette": {"palette": [{"block": "minecraft:water"}]}
"paddingPalette": {"palette": [{"block": "minecraft:water"}]},
"material": {"enabled": 1, "depth": 9, "palette": {"palette": [{"block": "minecraft:lava"}]}}
}
}
}
""", ""));
File shallow = pack(riversDimension("""
{
"enabled": true,
"routing": {"tileSize": 1024, "sampleSpacing": 64, "oceanOutlets": true},
"surface": {
"bed": {
"material": {"enabled": true, "depth": 0, "palette": "stone"}
}
}
}
""", ""));
PackHydrologyValidator.Validation result = validate(pack);
PackHydrologyValidator.Validation shallowResult = validate(shallow);
assertContains(result.errors(), "padding must be at most 8");
assertContains(result.errors(), "paddingPalette.palette[0].block must not be a fluid");
assertContains(result.errors(), "bed.material.enabled must be a boolean");
assertContains(result.errors(), "bed.material.depth must be at most 8");
assertContains(result.errors(), "bed.material.palette.palette[0].block must not be a fluid");
assertContains(shallowResult.errors(), "bed.material.depth must be at least 1");
assertContains(shallowResult.errors(), "bed.material.palette must be an object");
}
@Test
@@ -262,6 +385,60 @@ public class PackHydrologyValidatorTest {
"slopePenalty": 16,
"confluenceAttraction": 1
},
"geometry": {
"surface": {
"bedRoundness": 6,
"bedRoughness": 1,
"wallRoughness": 1,
"roughnessWavelength": 128,
"radialBase": 1.2,
"radialMinimum": 1,
"radialMaximum": 2,
"primaryLobeStrength": 0.5,
"detailLobeStrength": 0.5,
"ceilingRoughness": 1,
"aspectMinimum": 1,
"aspectRange": 0.8
},
"underground": {
"bedRoundness": 6,
"bedRoughness": 1,
"wallRoughness": 1,
"roughnessWavelength": 128,
"radialBase": 1.2,
"radialMinimum": 1,
"radialMaximum": 2,
"primaryLobeStrength": 0.5,
"detailLobeStrength": 0.5,
"ceilingRoughness": 1,
"aspectMinimum": 1,
"aspectRange": 0.8
},
"grottos": {
"bedRoundness": 6,
"bedRoughness": 1,
"wallRoughness": 1,
"roughnessWavelength": 128,
"radialBase": 1.2,
"radialMinimum": 1,
"radialMaximum": 2,
"primaryLobeStrength": 0.5,
"detailLobeStrength": 0.5,
"ceilingRoughness": 1,
"aspectMinimum": 1,
"aspectRange": 0.8
},
"drops": {
"cascadeRunPerBlock": 16,
"cascadeExponent": 6,
"maximumCascadeStep": 4,
"flowWidthRatio": 1,
"maximumFlowDepth": 16,
"basinWidthRatio": 4,
"maximumBasinDepth": 32,
"undergroundCascadeRunPerBlock": 16
}
},
"surface": {
"channel": {
"width": {"min": 128, "max": 128},
@@ -269,27 +446,76 @@ public class PackHydrologyValidatorTest {
"sink": 3,
"maximumIncision": 32,
"roughness": 1,
"roughnessWavelength": 64
"roughnessWavelength": 64,
"springWidthRatio": 4,
"springLength": 96,
"smoothingRadius": 64,
"outlineMinimumRatio": 1,
"outlineMaximumRatio": 3,
"springExtraDepth": 8
},
"banks": {
"shoreWidth": 6,
"shoreWidth": 16,
"blendSlope": 12,
"minimumBlendWidth": 64,
"maximumBlendWidth": 64,
"exposeCutStrata": true
"exposeCutStrata": true,
"shoreRise": 4,
"blendBaseWidth": 32,
"shoreMaterial": {"enabled": true, "palette": {"palette": [{"block": "minecraft:sand"}]}, "depth": 8},
"bankMaterial": {"enabled": true, "palette": {"palette": [{"block": "minecraft:coarse_dirt"}]}, "depth": 8}
},
"flow": {"cascadeRun": 8, "waterfallMinimumDrop": 32},
"mouths": {"flareRatio": 4, "maximumOceanApron": 32}
"bed": {
"allowGravityBlocks": true,
"padding": 8,
"material": {"enabled": true, "palette": {"palette": [{"block": "minecraft:gravel"}]}, "depth": 8}
},
"flow": {
"cascadeRun": 8,
"waterfallMinimumDrop": 32,
"waterfallThalwegFraction": 0.95,
"plungeBasinMinimumDrop": 8,
"plungeBasinLengthRatio": 8,
"plungeBasinDepth": 8
},
"mouths": {
"flareRatio": 4,
"maximumOceanApron": 32,
"inletLength": 256,
"inletDepth": 16,
"maximumIncision": 128,
"inletCourseFraction": 1,
"inletRampSlope": 4
},
"erosion": {
"enabled": true,
"smoothingRadius": 64,
"thalwegFraction": 0.95,
"blendCurve": 4,
"bedNoise": 2,
"style": "CLIFF",
"terraceSteps": 16,
"cliffFraction": 1,
"bedProfile": "U"
}
},
"underground": {
"mouthLevelingDistance": 512,
"minimumRockCover": 64,
"minimumFloorCover": 32,
"wideningSources": 64,
"bedMaterial": {"enabled": true, "palette": {"palette": [{"block": "minecraft:mud"}]}, "depth": 8}
},
"underground": {"mouthLevelingDistance": 512},
"grottos": {
"coastal": {
"horizontalRadius": 128,
"seaCaves": {"enabled": true, "maximumPerTile": 64, "minimumSpacing": 8192, "minimumCoastHeight": 128, "depth": 128}
"cliffMinimumHeight": 128,
"cliffSlopeFactor": 4,
"seaCaves": {"enabled": true, "maximumPerTile": 64, "minimumSpacing": 8192, "minimumCoastHeight": 128, "depth": 128, "sweepJitterDegrees": 90}
}
}
}
""", "\"bankMultiplier\": 4"));
""", "\"bankMultiplier\": 4, \"shoreWidth\": 16, \"erosion\": true"));
PackHydrologyValidator.Validation result = validate(pack);
@@ -372,7 +598,11 @@ public class PackHydrologyValidatorTest {
"roughness": 1.5,
"roughnessWavelength": 65,
"springWidthRatio": 5,
"springLength": 97
"springLength": 97,
"smoothingRadius": 65,
"outlineMinimumRatio": 1.5,
"outlineMaximumRatio": 3.5,
"springExtraDepth": 8.5
}
}
}
@@ -389,7 +619,11 @@ public class PackHydrologyValidatorTest {
"roughness": -0.1,
"roughnessWavelength": 3,
"springWidthRatio": 0.5,
"springLength": 3
"springLength": 3,
"smoothingRadius": -1,
"outlineMinimumRatio": 0.1,
"outlineMaximumRatio": 0.5,
"springExtraDepth": -0.5
}
}
}
@@ -406,6 +640,14 @@ public class PackHydrologyValidatorTest {
assertContains(aboveResult.errors(), "roughnessWavelength must be at most 64");
assertContains(aboveResult.errors(), "springWidthRatio must be at most 4.0");
assertContains(aboveResult.errors(), "springLength must be at most 96");
assertContains(aboveResult.errors(), "channel.smoothingRadius must be at most 64");
assertContains(aboveResult.errors(), "outlineMinimumRatio must be at most 1.0");
assertContains(aboveResult.errors(), "outlineMaximumRatio must be at most 3.0");
assertContains(aboveResult.errors(), "springExtraDepth must be at most 8.0");
assertContains(belowResult.errors(), "channel.smoothingRadius must be at least 0");
assertContains(belowResult.errors(), "outlineMinimumRatio must be at least 0.2");
assertContains(belowResult.errors(), "outlineMaximumRatio must be at least 1.0");
assertContains(belowResult.errors(), "springExtraDepth must be at least 0.0");
assertContains(belowResult.errors(), "springWidthRatio must be at least 1.0");
assertContains(belowResult.errors(), "springLength must be at least 4");
assertContains(belowResult.errors(), "width.min must be at least 1.0");
@@ -422,7 +664,7 @@ public class PackHydrologyValidatorTest {
{
"enabled": true,
"surface": {
"erosion": {"enabled": "yes", "smoothingRadius": 65, "thalwegFraction": 0.96, "blendCurve": 4.5, "bedNoise": 2.5},
"erosion": {"enabled": "yes", "smoothingRadius": 65, "thalwegFraction": 0.96, "blendCurve": 4.5, "bedNoise": 2.5, "style": "WAVY", "terraceSteps": 17, "cliffFraction": 1.5, "bedProfile": "W"},
"ponds": {"source": {"minimumRadius": 33, "maximumRadius": 33, "depth": 9}, "terminal": {"minimumRadius": 6, "maximumRadius": 5, "depth": 1}}
}
}
@@ -431,20 +673,33 @@ public class PackHydrologyValidatorTest {
{
"enabled": true,
"surface": {
"erosion": {"smoothingRadius": -1, "thalwegFraction": -0.1, "blendCurve": 0.1, "bedNoise": -1},
"erosion": {"smoothingRadius": -1, "thalwegFraction": -0.1, "blendCurve": 0.1, "bedNoise": -1, "style": null, "terraceSteps": 1, "cliffFraction": -0.1, "bedProfile": 3},
"ponds": {"source": {"minimumRadius": 0, "maximumRadius": 0, "depth": 0}}
}
}
"""));
File everyStyle = pack(riversDimension("""
{
"enabled": true,
"surface": {
"erosion": {"style": "TERRACED", "terraceSteps": 2, "bedProfile": "V"}
}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below);
PackHydrologyValidator.Validation everyStyleResult = validate(everyStyle);
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(), "erosion.style must be one of");
assertContains(aboveResult.errors(), "terraceSteps must be at most 16");
assertContains(aboveResult.errors(), "cliffFraction must be at most 1.0");
assertContains(aboveResult.errors(), "erosion.bedProfile must be one of");
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");
@@ -452,8 +707,14 @@ public class PackHydrologyValidatorTest {
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(), "erosion.style must be one of");
assertContains(belowResult.errors(), "terraceSteps must be at least 2");
assertContains(belowResult.errors(), "cliffFraction must be at least 0.0");
assertContains(belowResult.errors(), "erosion.bedProfile must be one of");
assertContains(belowResult.errors(), "minimumRadius must be at least 1");
assertContains(belowResult.errors(), "depth must be at least 1");
assertTrue(everyStyleResult.errors().toString(),
everyStyleResult.errors().stream().noneMatch(error -> error.contains(".erosion")));
}
@Test
@@ -463,11 +724,15 @@ public class PackHydrologyValidatorTest {
"enabled": true,
"surface": {
"banks": {
"shoreWidth": 6.5,
"shoreWidth": 16.5,
"blendSlope": 12.5,
"minimumBlendWidth": 40,
"maximumBlendWidth": 65,
"exposeCutStrata": "yes"
"exposeCutStrata": "yes",
"shoreRise": 4.5,
"blendBaseWidth": 32.5,
"shoreMaterial": {"enabled": "yes", "depth": 9, "palette": "stone"},
"bankMaterial": {"enabled": true, "depth": 9, "palette": {"palette": []}}
}
}
}
@@ -477,10 +742,14 @@ public class PackHydrologyValidatorTest {
"enabled": true,
"surface": {
"banks": {
"shoreWidth": 0.4,
"shoreWidth": -0.1,
"blendSlope": 0.4,
"minimumBlendWidth": 0,
"maximumBlendWidth": 8
"maximumBlendWidth": 8,
"shoreRise": -0.5,
"blendBaseWidth": -1,
"shoreMaterial": {"depth": 0},
"bankMaterial": {"depth": 0, "palette": {"palette": [{"block": "minecraft:water"}]}}
}
}
}
@@ -493,19 +762,42 @@ public class PackHydrologyValidatorTest {
}
}
"""));
File widened = pack(riversDimension("""
{
"enabled": true,
"surface": {
"banks": {"shoreWidth": 0, "shoreRise": 2, "blendBaseWidth": 12}
}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below);
PackHydrologyValidator.Validation unorderedResult = validate(unordered);
PackHydrologyValidator.Validation widenedResult = validate(widened);
assertContains(aboveResult.errors(), "shoreWidth must be at most 6.0");
assertContains(aboveResult.errors(), "shoreWidth must be at most 16.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(), "shoreWidth must be at least 0.5");
assertContains(aboveResult.errors(), "shoreRise must be at most 4.0");
assertContains(aboveResult.errors(), "blendBaseWidth must be at most 32.0");
assertContains(aboveResult.errors(), "shoreMaterial.enabled must be a boolean");
assertContains(aboveResult.errors(), "shoreMaterial.depth must be at most 8");
assertContains(aboveResult.errors(), "shoreMaterial.palette must be an object");
assertContains(aboveResult.errors(), "bankMaterial.depth must be at most 8");
assertContains(aboveResult.errors(), "bankMaterial.palette.palette must contain at least one solid block");
assertContains(belowResult.errors(), "shoreWidth must be at least 0.0");
assertContains(belowResult.errors(), "blendSlope must be at least 0.5");
assertContains(belowResult.errors(), "minimumBlendWidth must be at least 1");
assertContains(belowResult.errors(), "shoreRise must be at least 0.0");
assertContains(belowResult.errors(), "blendBaseWidth must be at least 0.0");
assertContains(belowResult.errors(), "shoreMaterial.depth must be at least 1");
assertContains(belowResult.errors(), "bankMaterial.depth must be at least 1");
assertContains(belowResult.errors(), "bankMaterial.palette.palette[0].block must not be a fluid");
assertContains(unorderedResult.errors(), "minimumBlendWidth must not exceed maximumBlendWidth");
assertTrue(widenedResult.errors().toString(),
widenedResult.errors().stream().noneMatch(error -> error.contains(".banks")));
}
@Test
@@ -514,8 +806,8 @@ public class PackHydrologyValidatorTest {
{
"enabled": true,
"surface": {
"flow": {"cascadeRun": 9, "waterfallMinimumDrop": 33},
"mouths": {"flareRatio": 4.5, "maximumOceanApron": 33, "inletLength": 257, "inletDepth": 17, "maximumIncision": 129}
"flow": {"cascadeRun": 9, "waterfallMinimumDrop": 33, "waterfallThalwegFraction": 0.96, "plungeBasinMinimumDrop": 9, "plungeBasinLengthRatio": 8.5, "plungeBasinDepth": 9},
"mouths": {"flareRatio": 4.5, "maximumOceanApron": 33, "inletLength": 257, "inletDepth": 17, "maximumIncision": 129, "inletCourseFraction": 1.5, "inletRampSlope": 4.5}
}
}
"""));
@@ -523,8 +815,8 @@ public class PackHydrologyValidatorTest {
{
"enabled": true,
"surface": {
"flow": {"cascadeRun": 0, "waterfallMinimumDrop": 1},
"mouths": {"flareRatio": 0.5, "maximumOceanApron": -1, "inletLength": -1, "inletDepth": -1, "maximumIncision": -1}
"flow": {"cascadeRun": 0, "waterfallMinimumDrop": 1, "waterfallThalwegFraction": -0.1, "plungeBasinMinimumDrop": 0, "plungeBasinLengthRatio": -0.5, "plungeBasinDepth": -1},
"mouths": {"flareRatio": 0.5, "maximumOceanApron": -1, "inletLength": -1, "inletDepth": -1, "maximumIncision": -1, "inletCourseFraction": 0.01, "inletRampSlope": 0.05}
}
}
"""));
@@ -559,6 +851,12 @@ public class PackHydrologyValidatorTest {
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(aboveResult.errors(), "waterfallThalwegFraction must be at most 0.95");
assertContains(aboveResult.errors(), "plungeBasinMinimumDrop must be at most 8");
assertContains(aboveResult.errors(), "plungeBasinLengthRatio must be at most 8.0");
assertContains(aboveResult.errors(), "plungeBasinDepth must be at most 8");
assertContains(aboveResult.errors(), "inletCourseFraction must be at most 1.0");
assertContains(aboveResult.errors(), "inletRampSlope must be at most 4.0");
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");
@@ -566,23 +864,41 @@ public class PackHydrologyValidatorTest {
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(belowResult.errors(), "waterfallThalwegFraction must be at least 0.0");
assertContains(belowResult.errors(), "plungeBasinMinimumDrop must be at least 1");
assertContains(belowResult.errors(), "plungeBasinLengthRatio must be at least 0.0");
assertContains(belowResult.errors(), "plungeBasinDepth must be at least 0");
assertContains(belowResult.errors(), "inletCourseFraction must be at least 0.05");
assertContains(belowResult.errors(), "inletRampSlope must be at least 0.1");
assertContains(longInletResult.errors(), "mouths.inletLength must not exceed routing.minimumSurfaceCourseLength");
assertTrue(fittingInletResult.errors().toString(),
fittingInletResult.errors().stream().noneMatch(error -> error.contains("inletLength")));
}
@Test
public void rejectsUndergroundMouthLevelingDistanceOutsideItsBounds() throws Exception {
public void rejectsUndergroundValuesOutsideTheirBounds() throws Exception {
File above = pack(riversDimension("""
{
"enabled": true,
"underground": {"mouthLevelingDistance": 513}
"underground": {
"mouthLevelingDistance": 513,
"minimumRockCover": 65,
"minimumFloorCover": 33,
"wideningSources": 65,
"bedMaterial": {"enabled": "yes", "depth": 9, "palette": {"palette": [{"block": "minecraft:lava"}]}}
}
}
"""));
File below = pack(riversDimension("""
{
"enabled": true,
"underground": {"mouthLevelingDistance": 15}
"underground": {
"mouthLevelingDistance": 15,
"minimumRockCover": 0,
"minimumFloorCover": 0,
"wideningSources": 0,
"bedMaterial": {"depth": 0, "palette": []}
}
}
"""));
@@ -590,7 +906,114 @@ public class PackHydrologyValidatorTest {
PackHydrologyValidator.Validation belowResult = validate(below);
assertContains(aboveResult.errors(), "mouthLevelingDistance must be at most 512");
assertContains(aboveResult.errors(), "minimumRockCover must be at most 64");
assertContains(aboveResult.errors(), "minimumFloorCover must be at most 32");
assertContains(aboveResult.errors(), "wideningSources must be at most 64");
assertContains(aboveResult.errors(), "bedMaterial.enabled must be a boolean");
assertContains(aboveResult.errors(), "bedMaterial.depth must be at most 8");
assertContains(aboveResult.errors(), "bedMaterial.palette.palette[0].block must not be a fluid");
assertContains(belowResult.errors(), "mouthLevelingDistance must be at least 16");
assertContains(belowResult.errors(), "minimumRockCover must be at least 1");
assertContains(belowResult.errors(), "minimumFloorCover must be at least 1");
assertContains(belowResult.errors(), "wideningSources must be at least 1");
assertContains(belowResult.errors(), "bedMaterial.depth must be at least 1");
assertContains(belowResult.errors(), "bedMaterial.palette must be an object");
}
@Test
public void rejectsChannelShapeValuesOutsideTheirBoundsAndAcceptsNullSurfaceFallbacks() throws Exception {
File above = pack(riversDimension("""
{
"enabled": true,
"geometry": {
"surface": {
"bedRoundness": 6.5,
"bedRoughness": 1.5,
"wallRoughness": 1.5,
"roughnessWavelength": 129,
"radialBase": 1.25,
"radialMinimum": 1.5,
"radialMaximum": 2.5,
"primaryLobeStrength": 0.6,
"detailLobeStrength": 0.6,
"ceilingRoughness": 1.5,
"aspectMinimum": 1.5,
"aspectRange": 0.9
},
"grottos": {"radialBase": 1.25, "aspectMinimum": 1.5, "aspectRange": 0.9},
"drops": {"undergroundCascadeRunPerBlock": 17}
}
}
"""));
File below = pack(riversDimension("""
{
"enabled": true,
"geometry": {
"surface": {
"bedRoundness": 0.5,
"bedRoughness": -0.1,
"wallRoughness": -0.1,
"roughnessWavelength": 2,
"radialBase": 0.3,
"radialMinimum": 0.1,
"radialMaximum": 0.5,
"primaryLobeStrength": -0.1,
"detailLobeStrength": -0.1,
"ceilingRoughness": -0.1,
"aspectMinimum": 0.1,
"aspectRange": -0.1
},
"underground": {"radialMinimum": 0.1, "ceilingRoughness": -0.1},
"drops": {"undergroundCascadeRunPerBlock": -1}
}
}
"""));
File fallbacks = pack(riversDimension("""
{
"enabled": true,
"geometry": {
"surface": {"bedRoughness": null, "wallRoughness": null, "roughnessWavelength": null}
}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below);
PackHydrologyValidator.Validation fallbacksResult = validate(fallbacks);
assertContains(aboveResult.errors(), "geometry.surface.bedRoundness must be at most 6.0");
assertContains(aboveResult.errors(), "geometry.surface.bedRoughness must be at most 1.0");
assertContains(aboveResult.errors(), "geometry.surface.wallRoughness must be at most 1.0");
assertContains(aboveResult.errors(), "geometry.surface.roughnessWavelength must be at most 128");
assertContains(aboveResult.errors(), "geometry.surface.radialBase must be at most 1.2");
assertContains(aboveResult.errors(), "geometry.surface.radialMinimum must be at most 1.0");
assertContains(aboveResult.errors(), "geometry.surface.radialMaximum must be at most 2.0");
assertContains(aboveResult.errors(), "geometry.surface.primaryLobeStrength must be at most 0.5");
assertContains(aboveResult.errors(), "geometry.surface.detailLobeStrength must be at most 0.5");
assertContains(aboveResult.errors(), "geometry.surface.ceilingRoughness must be at most 1.0");
assertContains(aboveResult.errors(), "geometry.surface.aspectMinimum must be at most 1.0");
assertContains(aboveResult.errors(), "geometry.surface.aspectRange must be at most 0.8");
assertContains(aboveResult.errors(), "geometry.grottos.radialBase must be at most 1.2");
assertContains(aboveResult.errors(), "geometry.grottos.aspectMinimum must be at most 1.0");
assertContains(aboveResult.errors(), "geometry.grottos.aspectRange must be at most 0.8");
assertContains(aboveResult.errors(), "undergroundCascadeRunPerBlock must be at most 16");
assertContains(belowResult.errors(), "geometry.surface.bedRoundness must be at least 1.0");
assertContains(belowResult.errors(), "geometry.surface.bedRoughness must be at least 0.0");
assertContains(belowResult.errors(), "geometry.surface.wallRoughness must be at least 0.0");
assertContains(belowResult.errors(), "geometry.surface.roughnessWavelength must be at least 3");
assertContains(belowResult.errors(), "geometry.surface.radialBase must be at least 0.4");
assertContains(belowResult.errors(), "geometry.surface.radialMinimum must be at least 0.2");
assertContains(belowResult.errors(), "geometry.surface.radialMaximum must be at least 1.0");
assertContains(belowResult.errors(), "geometry.surface.primaryLobeStrength must be at least 0.0");
assertContains(belowResult.errors(), "geometry.surface.detailLobeStrength must be at least 0.0");
assertContains(belowResult.errors(), "geometry.surface.ceilingRoughness must be at least 0.0");
assertContains(belowResult.errors(), "geometry.surface.aspectMinimum must be at least 0.2");
assertContains(belowResult.errors(), "geometry.surface.aspectRange must be at least 0.0");
assertContains(belowResult.errors(), "geometry.underground.radialMinimum must be at least 0.2");
assertContains(belowResult.errors(), "geometry.underground.ceilingRoughness must be at least 0.0");
assertContains(belowResult.errors(), "undergroundCascadeRunPerBlock must be at least 0");
assertTrue(fallbacksResult.errors().toString(),
fallbacksResult.errors().stream().noneMatch(error -> error.contains(".geometry")));
}
@Test
@@ -762,13 +1185,17 @@ public class PackHydrologyValidatorTest {
"depthMultiplier": 17,
"incisionMultiplier": -1,
"routingMultiplier": -1,
"bankMultiplier": 4.5
"bankMultiplier": 4.5,
"shoreWidth": 16.5,
"erosion": "no"
}
}
""");
PackHydrologyValidator.Validation result = validate(pack);
assertContains(result.errors(), ".shoreWidth must be at most 16.0");
assertContains(result.errors(), ".erosion must be a boolean or null");
assertContains(result.errors(), ".placement must be one of");
assertContains(result.errors(), ".routing must be one of");
assertContains(result.errors(), ".outletAdmission must be a boolean or null");
@@ -795,7 +1222,8 @@ public class PackHydrologyValidatorTest {
},
"riverPolicy": {
"bankBiomes": [17],
"bankMultiplier": -0.5
"bankMultiplier": -0.5,
"shoreWidth": -0.5
}
}
""");
@@ -804,6 +1232,7 @@ public class PackHydrologyValidatorTest {
assertContains(result.errors(), "bankBiomes[0] must name a biome resource");
assertContains(result.errors(), "bankMultiplier must be at least 0.0");
assertContains(result.errors(), "riverPolicy.shoreWidth must be at least 0.0");
}
@Test
@@ -1244,7 +1673,9 @@ public class PackHydrologyValidatorTest {
"grottos": {
"coastal": {
"horizontalRadius": 12,
"seaCaves": {"enabled": true, "maximumPerTile": 65, "minimumSpacing": 8193, "minimumCoastHeight": 129, "depth": 129}
"cliffMinimumHeight": 129,
"cliffSlopeFactor": 4.5,
"seaCaves": {"enabled": true, "maximumPerTile": 65, "minimumSpacing": 8193, "minimumCoastHeight": 129, "depth": 129, "sweepJitterDegrees": 90.5}
}
}
}
@@ -1255,7 +1686,9 @@ public class PackHydrologyValidatorTest {
"grottos": {
"coastal": {
"horizontalRadius": 12,
"seaCaves": {"enabled": "yes", "maximumPerTile": -1, "minimumSpacing": 20, "minimumCoastHeight": 0, "depth": -1}
"cliffMinimumHeight": -1,
"cliffSlopeFactor": -0.5,
"seaCaves": {"enabled": "yes", "maximumPerTile": -1, "minimumSpacing": 20, "minimumCoastHeight": 0, "depth": -1, "sweepJitterDegrees": -1}
}
}
}
@@ -1266,7 +1699,9 @@ public class PackHydrologyValidatorTest {
"grottos": {
"coastal": {
"horizontalRadius": 12,
"seaCaves": {"enabled": true, "maximumPerTile": 3, "minimumSpacing": 160, "minimumCoastHeight": 8, "depth": 12}
"cliffMinimumHeight": null,
"cliffSlopeFactor": 0.5,
"seaCaves": {"enabled": true, "maximumPerTile": 3, "minimumSpacing": 160, "minimumCoastHeight": 8, "depth": 12, "sweepJitterDegrees": 25}
}
}
}
@@ -1276,15 +1711,21 @@ public class PackHydrologyValidatorTest {
PackHydrologyValidator.Validation belowResult = validate(below);
PackHydrologyValidator.Validation canonicalResult = validate(canonical);
assertContains(aboveResult.errors(), "coastal.cliffMinimumHeight must be at most 128");
assertContains(aboveResult.errors(), "coastal.cliffSlopeFactor must be at most 4.0");
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(aboveResult.errors(), "seaCaves.sweepJitterDegrees must be at most 90.0");
assertContains(belowResult.errors(), "coastal.cliffMinimumHeight must be at least 0");
assertContains(belowResult.errors(), "coastal.cliffSlopeFactor must be at least 0.0");
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");
assertContains(belowResult.errors(), "seaCaves.sweepJitterDegrees must be at least 0.0");
assertTrue(canonicalResult.errors().toString(), canonicalResult.errors().isEmpty());
}
@@ -85,7 +85,27 @@ public class IrisRiverSchemaTest {
assertFalse(channel.has("surfaceInset"));
assertFalse(channel.has("terrainBlendWidth"));
assertFalse(mouths.has("levelingDistance"));
assertFalse(geometry.has("surface"));
assertTrue(geometry.has("surface"));
JSONObject surfaceShape = referencedProperties(definitions, geometry.getJSONObject("surface"));
JSONObject undergroundShape = referencedProperties(definitions, geometry.getJSONObject("underground"));
JSONObject grottoShape = referencedProperties(definitions, geometry.getJSONObject("grottos"));
assertEquals("number", surfaceShape.getJSONObject("bedRoundness").getString("type"));
assertEquals(1D, surfaceShape.getJSONObject("bedRoundness").getDouble("minimum"), 0D);
assertEquals(6D, surfaceShape.getJSONObject("bedRoundness").getDouble("maximum"), 0D);
assertEquals("number", surfaceShape.getJSONObject("bedRoughness").getString("type"));
assertEquals(0D, surfaceShape.getJSONObject("bedRoughness").getDouble("minimum"), 0D);
assertEquals(1D, surfaceShape.getJSONObject("bedRoughness").getDouble("maximum"), 0D);
assertEquals("number", surfaceShape.getJSONObject("wallRoughness").getString("type"));
assertEquals(0D, surfaceShape.getJSONObject("wallRoughness").getDouble("minimum"), 0D);
assertEquals(1D, surfaceShape.getJSONObject("wallRoughness").getDouble("maximum"), 0D);
assertEquals("integer", surfaceShape.getJSONObject("roughnessWavelength").getString("type"));
assertEquals(3, surfaceShape.getJSONObject("roughnessWavelength").getInt("minimum"));
assertEquals(128, surfaceShape.getJSONObject("roughnessWavelength").getInt("maximum"));
assertSharedShapeBounds(surfaceShape);
assertSharedShapeBounds(undergroundShape);
assertSharedShapeBounds(grottoShape);
assertEquals(0, drops.getJSONObject("undergroundCascadeRunPerBlock").getInt("minimum"));
assertEquals(16, drops.getJSONObject("undergroundCascadeRunPerBlock").getInt("maximum"));
assertEquals(8192, routing.getJSONObject("tileSize").getInt("maximum"));
assertEquals(32768, routing.getJSONObject("maximumRouteLength").getInt("maximum"));
@@ -118,6 +138,14 @@ public class IrisRiverSchemaTest {
assertEquals(32, channel.getJSONObject("maximumIncision").getInt("maximum"));
assertEquals(1D, channel.getJSONObject("roughness").getDouble("maximum"), 0D);
assertEquals(64, channel.getJSONObject("roughnessWavelength").getInt("maximum"));
assertEquals(0, channel.getJSONObject("smoothingRadius").getInt("minimum"));
assertEquals(64, channel.getJSONObject("smoothingRadius").getInt("maximum"));
assertEquals(0.2D, channel.getJSONObject("outlineMinimumRatio").getDouble("minimum"), 0D);
assertEquals(1D, channel.getJSONObject("outlineMinimumRatio").getDouble("maximum"), 0D);
assertEquals(1D, channel.getJSONObject("outlineMaximumRatio").getDouble("minimum"), 0D);
assertEquals(3D, channel.getJSONObject("outlineMaximumRatio").getDouble("maximum"), 0D);
assertEquals(0D, channel.getJSONObject("springExtraDepth").getDouble("minimum"), 0D);
assertEquals(8D, channel.getJSONObject("springExtraDepth").getDouble("maximum"), 0D);
assertFalse(banks.has("freeboard"));
JSONObject erosion = referencedProperties(definitions, surface.getJSONObject("erosion"));
assertEquals("boolean", erosion.getJSONObject("enabled").getString("type"));
@@ -125,32 +153,79 @@ public class IrisRiverSchemaTest {
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);
assertEquals(List.of("SMOOTH", "LINEAR", "CONCAVE", "TERRACED", "CLIFF"),
enumValues(definitions, erosion.getJSONObject("style")));
assertEquals(2, erosion.getJSONObject("terraceSteps").getInt("minimum"));
assertEquals(16, erosion.getJSONObject("terraceSteps").getInt("maximum"));
assertEquals(0D, erosion.getJSONObject("cliffFraction").getDouble("minimum"), 0D);
assertEquals(1D, erosion.getJSONObject("cliffFraction").getDouble("maximum"), 0D);
assertEquals(List.of("BOWL", "FLAT", "V", "U"),
enumValues(definitions, erosion.getJSONObject("bedProfile")));
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(0D, banks.getJSONObject("shoreWidth").getDouble("minimum"), 0D);
assertEquals(16D, banks.getJSONObject("shoreWidth").getDouble("maximum"), 0D);
assertEquals(0.5D, banks.getJSONObject("blendSlope").getDouble("minimum"), 0D);
assertEquals(12D, banks.getJSONObject("blendSlope").getDouble("maximum"), 0D);
assertEquals(64, banks.getJSONObject("minimumBlendWidth").getInt("maximum"));
assertEquals(64, banks.getJSONObject("maximumBlendWidth").getInt("maximum"));
assertEquals("boolean", banks.getJSONObject("exposeCutStrata").getString("type"));
assertEquals(0D, banks.getJSONObject("shoreRise").getDouble("minimum"), 0D);
assertEquals(4D, banks.getJSONObject("shoreRise").getDouble("maximum"), 0D);
assertEquals(0D, banks.getJSONObject("blendBaseWidth").getDouble("minimum"), 0D);
assertEquals(32D, banks.getJSONObject("blendBaseWidth").getDouble("maximum"), 0D);
assertMaterialBounds(referencedProperties(definitions, banks.getJSONObject("shoreMaterial")));
assertMaterialBounds(referencedProperties(definitions, banks.getJSONObject("bankMaterial")));
JSONObject bed = referencedProperties(definitions, surface.getJSONObject("bed"));
assertEquals("boolean", bed.getJSONObject("allowGravityBlocks").getString("type"));
assertEquals(8, bed.getJSONObject("padding").getInt("maximum"));
assertMaterialBounds(referencedProperties(definitions, bed.getJSONObject("material")));
assertEquals(1, flow.getJSONObject("cascadeRun").getInt("minimum"));
assertEquals(8, flow.getJSONObject("cascadeRun").getInt("maximum"));
assertEquals(2, flow.getJSONObject("waterfallMinimumDrop").getInt("minimum"));
assertEquals(32, flow.getJSONObject("waterfallMinimumDrop").getInt("maximum"));
assertEquals(0D, flow.getJSONObject("waterfallThalwegFraction").getDouble("minimum"), 0D);
assertEquals(0.95D, flow.getJSONObject("waterfallThalwegFraction").getDouble("maximum"), 0D);
assertEquals(1, flow.getJSONObject("plungeBasinMinimumDrop").getInt("minimum"));
assertEquals(8, flow.getJSONObject("plungeBasinMinimumDrop").getInt("maximum"));
assertEquals(0D, flow.getJSONObject("plungeBasinLengthRatio").getDouble("minimum"), 0D);
assertEquals(8D, flow.getJSONObject("plungeBasinLengthRatio").getDouble("maximum"), 0D);
assertEquals(0, flow.getJSONObject("plungeBasinDepth").getInt("minimum"));
assertEquals(8, flow.getJSONObject("plungeBasinDepth").getInt("maximum"));
assertEquals(1D, mouths.getJSONObject("flareRatio").getDouble("minimum"), 0D);
assertEquals(32, mouths.getJSONObject("maximumOceanApron").getInt("maximum"));
assertEquals(0.05D, mouths.getJSONObject("inletCourseFraction").getDouble("minimum"), 0D);
assertEquals(1D, mouths.getJSONObject("inletCourseFraction").getDouble("maximum"), 0D);
assertEquals(0.1D, mouths.getJSONObject("inletRampSlope").getDouble("minimum"), 0D);
assertEquals(4D, mouths.getJSONObject("inletRampSlope").getDouble("maximum"), 0D);
assertEquals(16, underground.getJSONObject("mouthLevelingDistance").getInt("minimum"));
assertEquals(512, underground.getJSONObject("mouthLevelingDistance").getInt("maximum"));
assertSnippetBackedObjectReference(underground.getJSONObject("fluidLevel"));
assertSnippetBackedObjectReference(underground.getJSONObject("channelWidth"));
assertSnippetBackedObjectReference(underground.getJSONObject("headroom"));
assertEquals(1, underground.getJSONObject("minimumRockCover").getInt("minimum"));
assertEquals(64, underground.getJSONObject("minimumRockCover").getInt("maximum"));
assertEquals(1, underground.getJSONObject("minimumFloorCover").getInt("minimum"));
assertEquals(32, underground.getJSONObject("minimumFloorCover").getInt("maximum"));
assertEquals(1, underground.getJSONObject("wideningSources").getInt("minimum"));
assertEquals(64, underground.getJSONObject("wideningSources").getInt("maximum"));
assertMaterialBounds(referencedProperties(definitions, underground.getJSONObject("bedMaterial")));
assertGrottoBounds(coastal);
assertGrottoBounds(inland);
assertEquals("integer", coastal.getJSONObject("cliffMinimumHeight").getString("type"));
assertEquals(0, coastal.getJSONObject("cliffMinimumHeight").getInt("minimum"));
assertEquals(128, coastal.getJSONObject("cliffMinimumHeight").getInt("maximum"));
assertEquals(0D, coastal.getJSONObject("cliffSlopeFactor").getDouble("minimum"), 0D);
assertEquals(4D, coastal.getJSONObject("cliffSlopeFactor").getDouble("maximum"), 0D);
JSONObject seaCaves = referencedProperties(definitions, coastal.getJSONObject("seaCaves"));
assertEquals(128, seaCaves.getJSONObject("depth").getInt("maximum"));
assertEquals(0D, seaCaves.getJSONObject("sweepJitterDegrees").getDouble("minimum"), 0D);
assertEquals(90D, seaCaves.getJSONObject("sweepJitterDegrees").getDouble("maximum"), 0D);
assertEquals("boolean", inland.getJSONObject("connectSurfaceRivers").getString("type"));
assertEquals("array", profiles.getString("type"));
assertEquals(1, profiles.getInt("minItems"));
@@ -200,6 +275,10 @@ public class IrisRiverSchemaTest {
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("number", properties.getJSONObject("shoreWidth").getString("type"));
assertEquals(0D, properties.getJSONObject("shoreWidth").getDouble("minimum"), 0D);
assertEquals(16D, properties.getJSONObject("shoreWidth").getDouble("maximum"), 0D);
assertEquals("boolean", properties.getJSONObject("erosion").getString("type"));
assertEquals("boolean", properties.getJSONObject("confined").getString("type"));
assertEquals(0D, properties.getJSONObject("incisionMultiplier").getDouble("minimum"), 0D);
assertEquals(64D, properties.getJSONObject("routingMultiplier").getDouble("maximum"), 0D);
@@ -207,6 +286,32 @@ public class IrisRiverSchemaTest {
properties.getJSONObject("shoreBiomes").getJSONObject("items").getString("$ref"));
}
private static void assertSharedShapeBounds(JSONObject shape) {
assertEquals(0.4D, shape.getJSONObject("radialBase").getDouble("minimum"), 0D);
assertEquals(1.2D, shape.getJSONObject("radialBase").getDouble("maximum"), 0D);
assertEquals(0.2D, shape.getJSONObject("radialMinimum").getDouble("minimum"), 0D);
assertEquals(1D, shape.getJSONObject("radialMinimum").getDouble("maximum"), 0D);
assertEquals(1D, shape.getJSONObject("radialMaximum").getDouble("minimum"), 0D);
assertEquals(2D, shape.getJSONObject("radialMaximum").getDouble("maximum"), 0D);
assertEquals(0D, shape.getJSONObject("primaryLobeStrength").getDouble("minimum"), 0D);
assertEquals(0.5D, shape.getJSONObject("primaryLobeStrength").getDouble("maximum"), 0D);
assertEquals(0D, shape.getJSONObject("detailLobeStrength").getDouble("minimum"), 0D);
assertEquals(0.5D, shape.getJSONObject("detailLobeStrength").getDouble("maximum"), 0D);
assertEquals(0D, shape.getJSONObject("ceilingRoughness").getDouble("minimum"), 0D);
assertEquals(1D, shape.getJSONObject("ceilingRoughness").getDouble("maximum"), 0D);
assertEquals(0.2D, shape.getJSONObject("aspectMinimum").getDouble("minimum"), 0D);
assertEquals(1D, shape.getJSONObject("aspectMinimum").getDouble("maximum"), 0D);
assertEquals(0D, shape.getJSONObject("aspectRange").getDouble("minimum"), 0D);
assertEquals(0.8D, shape.getJSONObject("aspectRange").getDouble("maximum"), 0D);
}
private static void assertMaterialBounds(JSONObject material) {
assertEquals("boolean", material.getJSONObject("enabled").getString("type"));
assertSnippetBackedObjectReference(material.getJSONObject("palette"));
assertEquals(1, material.getJSONObject("depth").getInt("minimum"));
assertEquals(8, material.getJSONObject("depth").getInt("maximum"));
}
private static void assertGrottoBounds(JSONObject grotto) {
assertEquals(1, grotto.getJSONObject("horizontalRadius").getInt("minimum"));
assertEquals(1, grotto.getJSONObject("verticalRadius").getInt("minimum"));
@@ -1,12 +1,16 @@
package art.arcane.iris.engine.actuator;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.object.IrisDecorationStep;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisRiverMaterialConfig;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.ArrayList;
@@ -15,8 +19,15 @@ import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisTerrainNormalActuatorHydrologyTest {
@@ -112,4 +123,171 @@ public class IrisTerrainNormalActuatorHydrologyTest {
assertEquals("minecraft:ice", state.key());
assertFalse(state.isWater());
}
@Test
public void bedMaterialPaintsTheChannelBedToItsDepth() {
Fixture fixture = new Fixture();
IrisRiverMaterialConfig bedMaterial = fixture.enabled(2);
IrisRiverMaterialConfig shoreMaterial = fixture.disabled();
IrisRiverMaterialConfig bankMaterial = fixture.disabled();
HydrologyColumnLayer channel = channelLayer();
IrisRiverMaterialConfig role = IrisTerrainNormalActuator.hydrologyRoleMaterial(
channel, bedMaterial, shoreMaterial, bankMaterial);
assertSame(bedMaterial, role);
assertSame(fixture.painted, fixture.paint(role, 0));
assertSame(fixture.painted, fixture.paint(role, 1));
assertSame(fixture.biomeLayer, fixture.paint(role, 2));
assertNull(IrisTerrainNormalActuator.hydrologyRoleMaterial(
apronLayer(), bedMaterial, shoreMaterial, bankMaterial));
assertNull(IrisTerrainNormalActuator.hydrologyRoleMaterial(
null, bedMaterial, shoreMaterial, bankMaterial));
}
@Test
public void shoreMaterialPaintsOnlyShoreColumns() {
Fixture fixture = new Fixture();
IrisRiverMaterialConfig bedMaterial = fixture.disabled();
IrisRiverMaterialConfig shoreMaterial = fixture.enabled(1);
IrisRiverMaterialConfig bankMaterial = fixture.disabled();
assertSame(shoreMaterial, IrisTerrainNormalActuator.hydrologyRoleMaterial(
shoreLayer(), bedMaterial, shoreMaterial, bankMaterial));
assertNull(IrisTerrainNormalActuator.hydrologyRoleMaterial(
channelLayer(), bedMaterial, shoreMaterial, bankMaterial));
assertNull(IrisTerrainNormalActuator.hydrologyRoleMaterial(
bankLayer(), bedMaterial, shoreMaterial, bankMaterial));
assertSame(fixture.painted, fixture.paint(shoreMaterial, 0));
assertSame(fixture.biomeLayer, fixture.paint(shoreMaterial, 1));
}
@Test
public void bankMaterialPaintsOnlyBankColumns() {
Fixture fixture = new Fixture();
IrisRiverMaterialConfig bedMaterial = fixture.disabled();
IrisRiverMaterialConfig shoreMaterial = fixture.disabled();
IrisRiverMaterialConfig bankMaterial = fixture.enabled(3);
assertSame(bankMaterial, IrisTerrainNormalActuator.hydrologyRoleMaterial(
bankLayer(), bedMaterial, shoreMaterial, bankMaterial));
assertNull(IrisTerrainNormalActuator.hydrologyRoleMaterial(
shoreLayer(), bedMaterial, shoreMaterial, bankMaterial));
assertNull(IrisTerrainNormalActuator.hydrologyRoleMaterial(
channelLayer(), bedMaterial, shoreMaterial, bankMaterial));
assertSame(fixture.painted, fixture.paint(bankMaterial, 2));
assertSame(fixture.biomeLayer, fixture.paint(bankMaterial, 3));
}
@Test
public void disabledMaterialsKeepTheBiomeLayers() {
Fixture fixture = new Fixture();
IrisRiverMaterialConfig bedMaterial = fixture.disabled();
IrisRiverMaterialConfig shoreMaterial = fixture.disabled();
IrisRiverMaterialConfig bankMaterial = fixture.disabled();
for (HydrologyColumnLayer layer : List.of(channelLayer(), shoreLayer(), bankLayer())) {
IrisRiverMaterialConfig disabledRole = IrisTerrainNormalActuator.hydrologyRoleMaterial(
layer, bedMaterial, shoreMaterial, bankMaterial);
IrisRiverMaterialConfig absentRole = IrisTerrainNormalActuator.hydrologyRoleMaterial(
layer, null, null, null);
assertNull(disabledRole);
assertNull(absentRole);
for (int depth = 0; depth < 4; depth++) {
assertSame(fixture.biomeLayer, fixture.paint(disabledRole, depth));
assertSame(fixture.biomeLayer, fixture.paint(absentRole, depth));
}
}
verify(fixture.palette, never()).get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any());
}
private static HydrologyColumnLayer channelLayer() {
return surfaceLayer(true, false);
}
private static HydrologyColumnLayer shoreLayer() {
return surfaceLayer(false, true);
}
private static HydrologyColumnLayer bankLayer() {
return surfaceLayer(false, false);
}
private static HydrologyColumnLayer surfaceLayer(boolean channel, boolean shore) {
return new HydrologyColumnLayer(
surfaceFeature(),
60,
63,
63,
channel,
shore,
!channel,
channel,
false,
false,
true,
channel,
false,
"default",
"river",
"mouth",
"shore",
"bank",
"cave"
);
}
private static HydrologyColumnLayer apronLayer() {
return new HydrologyColumnLayer(
surfaceFeature(),
63,
63,
63,
true,
false,
false,
true,
false,
false,
false,
false,
true,
"default",
"river",
"mouth",
"shore",
"bank",
"cave"
);
}
private static HydrologyFeatureRef surfaceFeature() {
return new HydrologyFeatureRef(21L, HydrologyFeatureType.RIFFLE, 22L, 23L, 11, 63, -4, 1, 0, false);
}
private static final class Fixture {
private final PlatformBlockState biomeLayer = mock(PlatformBlockState.class);
private final PlatformBlockState painted = mock(PlatformBlockState.class);
private final IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
private final IrisData data = mock(IrisData.class);
private final RNG rng = new RNG(7L);
private Fixture() {
when(palette.get(same(rng), eq(11.0), eq(63.0), eq(-4.0), same(data))).thenReturn(painted);
}
private IrisRiverMaterialConfig enabled(int depth) {
return new IrisRiverMaterialConfig().setEnabled(true).setPalette(palette).setDepth(depth);
}
private IrisRiverMaterialConfig disabled() {
return new IrisRiverMaterialConfig().setPalette(palette).setDepth(4);
}
private PlatformBlockState paint(IrisRiverMaterialConfig material, int depth) {
return IrisTerrainNormalActuator.paintHydrologyMaterial(
biomeLayer, material, depth, rng, 11, 63, -4, data);
}
}
}
@@ -1187,7 +1187,7 @@ public class HydrologyCaveCourseFilterTest {
private static HydrologyPlannerSettings seaCaveSettings() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets outlets = HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 32768),
base.outlets().inlandGrotto(),
@@ -1202,7 +1202,7 @@ public class HydrologyCaveCourseFilterTest {
base.geometry().meanders(),
base.geometry().surface(),
base.geometry().underground(),
new HydrologyPlannerSettings.ChannelShape(2.4D, 0D, 0D, 11),
HydrologyPlannerSettings.ChannelShape.of(2.4D, 0D, 0D, 11),
base.geometry().drops()
);
return new HydrologyPlannerSettings(
@@ -1915,9 +1915,226 @@ public class HydrologyFootprintCompilerTest {
);
}
@Test
public void waterfallThalwegFractionDefaultsReproduceTheThroatBed() {
HydraulicSegment segment = new HydraulicSegment(
121L,
120L,
HydrologyFeatureType.WATERFALL,
90,
70,
16,
12,
true,
false,
List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(32, 70, 0))
);
RiverCourse course = course(120L, RiverCourseType.SURFACE, segment);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(96, 1D, "parent");
RiverFootprint defaults = compile(HydrologyPlannerSettings.defaults(), terrain, course);
RiverFootprint explicit = compile(withFlow(new HydrologyPlannerSettings.Flow(0.65D, 2, 2D, 1)), terrain, course);
RiverFootprint broad = compile(withFlow(new HydrologyPlannerSettings.Flow(0.2D, 2, 2D, 1)), terrain, course);
assertEquals(defaults, explicit);
int shallower = 0;
for (HydrologyColumnSample sample : defaults.columns().values()) {
HydrologyColumnLayer deepThalweg = surfaceFluidLayer(defaults, sample.x(), sample.z());
HydrologyColumnLayer broadThalweg = surfaceFluidLayer(broad, sample.x(), sample.z());
if (deepThalweg == null || broadThalweg == null || !deepThalweg.channel() || deepThalweg.fallingFluid()) {
continue;
}
String where = "bed at " + sample.x() + "," + sample.z();
assertTrue(where, broadThalweg.bedY() >= deepThalweg.bedY());
if (broadThalweg.bedY() > deepThalweg.bedY()) {
shallower++;
}
}
assertTrue("no column changed depth", shallower > 0);
}
@Test
public void radialKnobsDefaultsReproduceThePassageAndANarrowerBaseShrinksIt() {
RiverCourse course = undergroundCourse(130L, 24);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(84, 1D, "parent");
HydrologyPlannerSettings.ChannelShape shorthand = HydrologyPlannerSettings.ChannelShape.of(2.4D, 0.28D, 0.24D, 11);
HydrologyPlannerSettings.ChannelShape explicit = new HydrologyPlannerSettings.ChannelShape(
2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D);
HydrologyPlannerSettings.ChannelShape narrow = new HydrologyPlannerSettings.ChannelShape(
2.4D, 0.28D, 0.24D, 11, 0.6D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D);
RiverFootprint defaults = compile(HydrologyPlannerSettings.defaults(), terrain, course);
RiverFootprint shorthandFootprint = compile(withUndergroundShape(shorthand), terrain, course);
RiverFootprint explicitFootprint = compile(withUndergroundShape(explicit), terrain, course);
RiverFootprint narrowFootprint = compile(withUndergroundShape(narrow), terrain, course);
assertEquals(defaults, shorthandFootprint);
assertEquals(defaults, explicitFootprint);
assertTrue(
"narrow " + narrowFootprint.columns().size() + " vs " + defaults.columns().size(),
narrowFootprint.columns().size() < defaults.columns().size()
);
}
@Test
public void ceilingRoughnessZeroKeepsTheRoofSmoothAndAPositiveValueVariesIt() {
RiverCourse course = undergroundCourse(135L, 0);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(84, 1D, "parent");
HydrologyPlannerSettings.ChannelShape smooth = new HydrologyPlannerSettings.ChannelShape(
2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D);
HydrologyPlannerSettings.ChannelShape rough = new HydrologyPlannerSettings.ChannelShape(
2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0.5D, 0.62D, 0.2D);
RiverFootprint defaults = compile(HydrologyPlannerSettings.defaults(), terrain, course);
RiverFootprint smoothFootprint = compile(withUndergroundShape(smooth), terrain, course);
RiverFootprint roughFootprint = compile(withUndergroundShape(rough), terrain, course);
assertEquals(defaults, smoothFootprint);
assertTrue(axisCeilings(roughFootprint).size() > 1);
assertEquals(1, axisCeilings(smoothFootprint).size());
}
@Test
public void grottoAspectRangeZeroGivesACircularPlanAtAspectOne() {
HydraulicSegment chamber = new HydraulicSegment(
141L,
140L,
HydrologyFeatureType.INLAND_GROTTO,
48,
48,
4,
2,
false,
false,
List.of(new HydrologyPoint(0, 48, 0))
);
RiverCourse course = course(140L, RiverCourseType.UNDERGROUND, chamber);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(84, 1D, "parent");
HydrologyPlannerSettings.ChannelShape circle = new HydrologyPlannerSettings.ChannelShape(
2.4D, 0D, 0D, 11, 0.86D, 0.58D, 1.18D, 0D, 0D, 0D, 1D, 0D);
HydrologyPlannerSettings.ChannelShape ellipse = new HydrologyPlannerSettings.ChannelShape(
2.4D, 0D, 0D, 11, 0.86D, 0.58D, 1.18D, 0D, 0D, 0D, 0.62D, 0D);
RiverFootprint circular = compile(withGrottoShape(circle), terrain, course);
RiverFootprint elliptical = compile(withGrottoShape(ellipse), terrain, course);
int east = planExtent(circular, 1, 0);
assertTrue(east > 0);
assertEquals(east, planExtent(circular, -1, 0));
assertEquals(east, planExtent(circular, 0, 1));
assertEquals(east, planExtent(circular, 0, -1));
assertTrue(
"elliptical " + elliptical.columns().size() + " vs " + circular.columns().size(),
elliptical.columns().size() < circular.columns().size()
);
}
/** Distinct ceilings over the centreline axis of a straight underground passage. */
private static HashSet<Integer> axisCeilings(RiverFootprint footprint) {
HashSet<Integer> ceilings = new HashSet<>();
for (int x = 0; x <= 48; x++) {
HydrologyColumnSample sample = footprint.sample(x, 0).orElse(null);
if (sample == null) {
continue;
}
for (HydrologyColumnLayer layer : sample.layers()) {
if (layer.channel()) {
ceilings.add(layer.ceilingY());
}
}
}
return ceilings;
}
/** How far the published plan reaches from the origin along one cardinal. */
private static int planExtent(RiverFootprint footprint, int stepX, int stepZ) {
int reach = 0;
while (footprint.sample(stepX * (reach + 1), stepZ * (reach + 1)).isPresent()) {
reach++;
}
return reach;
}
private static RiverCourse undergroundCourse(long id, int bend) {
HydraulicSegment segment = new HydraulicSegment(
id + 1L,
id,
HydrologyFeatureType.UNDERGROUND_POOL,
48,
48,
12,
2,
false,
false,
bend == 0
? List.of(new HydrologyPoint(0, 48, 0), new HydrologyPoint(48, 48, 0))
: List.of(
new HydrologyPoint(0, 48, 0),
new HydrologyPoint(bend / 2, 48, 5),
new HydrologyPoint(bend, 48, 0)
)
);
return new RiverCourse(
id,
RiverCourseType.UNDERGROUND,
OptionalLong.of(id + 2L),
OptionalLong.of(id + 3L),
"water",
1,
List.of(),
List.of(segment)
);
}
private static RiverFootprint compile(
HydrologyPlannerSettings settings,
HydrologyTerrainSampler terrain,
RiverCourse course
) {
return new HydrologyFootprintCompiler(settings, terrain, request -> request.minimum()).compile(List.of(course));
}
private static HydrologyPlannerSettings withGeometry(HydrologyPlannerSettings.Geometry geometry) {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
return new HydrologyPlannerSettings(
base.seaLevel(), base.routing(), base.surface(), base.hydraulics(), base.underground(),
base.outlets(), geometry, base.deepFluids(), base.surfacePools(), base.widestShoreBiomeWidth(),
base.seaCaves());
}
private static HydrologyPlannerSettings withUndergroundShape(HydrologyPlannerSettings.ChannelShape shape) {
HydrologyPlannerSettings.Geometry base = HydrologyPlannerSettings.defaults().geometry();
return withGeometry(new HydrologyPlannerSettings.Geometry(
base.meanders(), base.surface(), shape, base.grottos(), base.drops()));
}
private static HydrologyPlannerSettings withGrottoShape(HydrologyPlannerSettings.ChannelShape shape) {
HydrologyPlannerSettings.Geometry base = HydrologyPlannerSettings.defaults().geometry();
return withGeometry(new HydrologyPlannerSettings.Geometry(
base.meanders(), base.surface(), base.underground(), shape, base.drops()));
}
private static HydrologyPlannerSettings withFlow(HydrologyPlannerSettings.Flow flow) {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Banks banks = base.surface().banks();
HydrologyPlannerSettings.Banks tuned = new HydrologyPlannerSettings.Banks(
banks.sink(), banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(),
banks.roughness(), banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(),
banks.mouthFlareRatio(), banks.inlet(), banks.springWidthRatio(), banks.springLength(),
banks.exposeCutStrata(), banks.erosion(), banks.ponds(), banks.channel(), flow);
HydrologyPlannerSettings.Surface surface = base.surface();
HydrologyPlannerSettings.Surface tunedSurface = new HydrologyPlannerSettings.Surface(
surface.enabled(), surface.sources(), surface.minimumWidth(), surface.maximumWidth(),
surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(), surface.shoreWidth(), tuned);
return new HydrologyPlannerSettings(
base.seaLevel(), base.routing(), tunedSurface, base.hydraulics(), base.underground(),
base.outlets(), base.geometry(), base.deepFluids(), base.surfacePools(), base.widestShoreBiomeWidth(),
base.seaCaves());
}
private static HydrologyPlannerSettings seaCaveSettings(int maximumOceanApron, int chamberRadius) {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets outlets = HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(true, chamberRadius, 7, 10, 32768),
base.outlets().inlandGrotto(),
@@ -1932,7 +2149,7 @@ public class HydrologyFootprintCompilerTest {
base.geometry().meanders(),
base.geometry().surface(),
base.geometry().underground(),
new HydrologyPlannerSettings.ChannelShape(2.4D, 0D, 0D, 11),
HydrologyPlannerSettings.ChannelShape.of(2.4D, 0D, 0D, 11),
base.geometry().drops()
);
return new HydrologyPlannerSettings(
@@ -101,21 +101,21 @@ public class HydrologyPlannerConfinementTest {
!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
List.of("default"), List.of(), Double.NaN, confines, Double.NaN, true
);
}
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);
HydrologyPlannerSettings.ChannelShape stableChannel = HydrologyPlannerSettings.ChannelShape.of(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(
HydrologyPlannerSettings.Underground.of(false, undergroundSources, 68, 82, 4, 14, 2, 5, 6, 12, false, 0),
HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
@@ -298,7 +298,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
1.5D,
HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
true,
undergroundSources,
68,
@@ -312,7 +312,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
true,
0
),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
@@ -332,7 +332,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
private HydrologyPlannerSettings.Geometry stableGeometry() {
HydrologyPlannerSettings.ChannelShape stableChannel =
new HydrologyPlannerSettings.ChannelShape(2D, 0D, 0D, 11);
HydrologyPlannerSettings.ChannelShape.of(2D, 0D, 0D, 11);
return new HydrologyPlannerSettings.Geometry(
new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D),
stableChannel,
@@ -376,7 +376,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
baseSurface.shoreWidth(),
HydrologyPlannerSettings.Banks.defaults()),
base.hydraulics(),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
false,
disabledSources,
baseUnderground.minimumFluidY(),
@@ -390,7 +390,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
baseUnderground.connectToExistingCaves(),
0
),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets.of(
base.outlets().oceanEnabled(),
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
base.outlets().inlandGrotto(),
@@ -448,7 +448,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
"flooded",
List.of("water"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
};
}
@@ -491,7 +493,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
sample.floodedCaveBiomeKey(),
sample.preferredProfileKeys(), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
};
}
@@ -526,7 +530,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
"flooded",
List.of("water"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -682,7 +682,7 @@ public class HydrologyPlannerGlobalInvariantTest {
1D,
HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
undergroundEnabled,
undergroundSources,
64,
@@ -696,7 +696,7 @@ public class HydrologyPlannerGlobalInvariantTest {
false,
0
),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
@@ -743,7 +743,7 @@ public class HydrologyPlannerGlobalInvariantTest {
surface.shoreWidth(),
HydrologyPlannerSettings.Banks.defaults()),
base.hydraulics(),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
underground.enabled(),
underground.sources(),
underground.minimumFluidY(),
@@ -956,7 +956,9 @@ public class HydrologyPlannerGlobalInvariantTest {
"flooded",
List.of("default"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -1009,7 +1011,9 @@ public class HydrologyPlannerGlobalInvariantTest {
"flooded",
List.of("default"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -1060,7 +1064,9 @@ public class HydrologyPlannerGlobalInvariantTest {
"flooded",
List.of("default"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -19,7 +19,7 @@ public class HydrologyPlannerSeaCaveTest {
@Test
public void seaCavesOpenFromTheSeaIntoTheCliffWithoutARiver() {
HydrologyPlannerSettings.SeaCaves seaCaves = new HydrologyPlannerSettings.SeaCaves(true, 2, 64, 8, 12);
HydrologyPlannerSettings.SeaCaves seaCaves = HydrologyPlannerSettings.SeaCaves.of(true, 2, 64, 8, 12);
HydrologyPlannerSettings settings = settings(128, 0D, CHAMBER, 12, seaCaves);
HydrologyTerrainSampler cliffCoast = cliffCoast(112, 92);
@@ -98,7 +98,7 @@ public class HydrologyPlannerSeaCaveTest {
@Test
public void seaCavesAreDeterministicAndOwnedByExactlyOneTile() {
HydrologyPlannerSettings settings = settings(128, 0D, CHAMBER, 12,
new HydrologyPlannerSettings.SeaCaves(true, 2, 64, 8, 12));
HydrologyPlannerSettings.SeaCaves.of(true, 2, 64, 8, 12));
HydrologyTerrainSampler cliffCoast = cliffCoast(112, 92);
List<HydrologyTileKey> keys = List.of(
TILE,
@@ -137,7 +137,7 @@ public class HydrologyPlannerSeaCaveTest {
@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.SeaCaves seaCaves = HydrologyPlannerSettings.SeaCaves.of(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")
@@ -172,7 +172,7 @@ public class HydrologyPlannerSeaCaveTest {
@Test
public void seaCavesKeepClearOfRiverMouthsAndLowCoasts() {
HydrologyPlannerSettings settings = settings(128, 2D, CHAMBER, 12,
new HydrologyPlannerSettings.SeaCaves(true, 4, 32, 8, 12));
HydrologyPlannerSettings.SeaCaves.of(true, 4, 32, 8, 12));
HydrologyTerrainSampler riverCoast = (int x, int z) -> x >= 112
? HydrologyTerrainSample.ocean(54, "ocean")
: land(92, x <= 16);
@@ -207,7 +207,7 @@ public class HydrologyPlannerSeaCaveTest {
HydrologyTile lowCoast = new HydrologyPlanner(
994L,
settings(128, 0D, CHAMBER, 12, new HydrologyPlannerSettings.SeaCaves(true, 4, 32, 8, 12)),
settings(128, 0D, CHAMBER, 12, HydrologyPlannerSettings.SeaCaves.of(true, 4, 32, 8, 12)),
cliffCoast(112, 66)
).plan(TILE);
assertTrue(courses(lowCoast, RiverCourseType.SEA_CAVE).isEmpty());
@@ -219,7 +219,7 @@ public class HydrologyPlannerSeaCaveTest {
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));
HydrologyPlannerSettings.SeaCaves.of(true, 2, 64, 8, 12));
HydrologyTile tile = new HydrologyPlanner(994L, settings, cliffCoast(112, 92)).plan(TILE);
@@ -286,7 +286,9 @@ public class HydrologyPlannerSeaCaveTest {
List.of("beta", "alpha"),
List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -308,9 +310,9 @@ public class HydrologyPlannerSeaCaveTest {
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(
HydrologyPlannerSettings.Underground.of(
false, undergroundSources, 68, 82, 4, 12, 2, 4, 5, 9, true, 0),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets.of(
true,
coastal,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
@@ -1,5 +1,7 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.object.IrisRiverBedProfile;
import art.arcane.iris.engine.object.IrisRiverBlendStyle;
import org.junit.Test;
import java.util.List;
@@ -97,7 +99,7 @@ public class HydrologyPlannerSettingsTest {
HydrologyPlannerSettings.Banks.defaults());
HydrologyPlannerSettings.Hydraulics hydraulics = new HydrologyPlannerSettings.Hydraulics(1);
HydrologyPlannerSettings.Grotto grotto = new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 1);
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets outlets = HydrologyPlannerSettings.Outlets.of(
true,
grotto,
grotto,
@@ -152,21 +154,21 @@ public class HydrologyPlannerSettingsTest {
assertEquals(32, banks.maximumBlendWidth());
assertEquals(6, banks.waterfallMinimumDrop());
assertTrue(banks.exposeCutStrata());
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, () -> HydrologyPlannerSettings.Banks.of(-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, () -> HydrologyPlannerSettings.Banks.of(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, () -> HydrologyPlannerSettings.Banks.of(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, () -> HydrologyPlannerSettings.Banks.of(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, () -> HydrologyPlannerSettings.Banks.of(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, () -> HydrologyPlannerSettings.Banks.of(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, () -> HydrologyPlannerSettings.Erosion.of(true, -1, 0.45D, 1D, 0.5D));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Erosion.of(true, 12, 1D, 1D, 0.5D));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Erosion.of(true, 12, 0.45D, 0D, 0.5D));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Erosion.of(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));
@@ -180,24 +182,24 @@ public class HydrologyPlannerSettingsTest {
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(
assertEquals(HydrologyPlannerSettings.Inlet.of(0, 0, 0), HydrologyPlannerSettings.Inlet.none());
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(-1, 3, 32));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(1025, 3, 32));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(64, -1, 32));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(64, 65, 32));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(64, 3, -1));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(64, 3, 513));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Banks.of(
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);
HydrologyPlannerSettings.Inlet.of(1024, 64, 512);
}
@Test
public void publicationRadiusGrowsWithTheMouthFlare() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Banks banks = base.surface().banks();
HydrologyPlannerSettings.Banks flared = new HydrologyPlannerSettings.Banks(
HydrologyPlannerSettings.Banks flared = HydrologyPlannerSettings.Banks.of(
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());
@@ -227,7 +229,7 @@ public class HydrologyPlannerSettingsTest {
assertThrows(IllegalArgumentException.class,
() -> new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 0));
HydrologyPlannerSettings.Grotto disabled = new HydrologyPlannerSettings.Grotto(false, 1, 1, 1, 1);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Outlets.of(
true,
accepted,
disabled,
@@ -238,7 +240,7 @@ public class HydrologyPlannerSettingsTest {
1,
1)
);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Outlets.of(
true,
accepted,
accepted,
@@ -254,7 +256,7 @@ public class HydrologyPlannerSettingsTest {
@Test
public void coastalOutletBudgetIsBounded() {
HydrologyPlannerSettings.Grotto grotto = new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 1);
HydrologyPlannerSettings.Outlets separate = new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets separate = HydrologyPlannerSettings.Outlets.of(
true,
grotto,
grotto,
@@ -267,17 +269,17 @@ public class HydrologyPlannerSettingsTest {
);
assertEquals(0, separate.maximumCoastalPerTile());
assertEquals(256, new HydrologyPlannerSettings.Outlets(true, grotto, grotto, true, 0, 0, 1, 3, 256)
assertEquals(256, HydrologyPlannerSettings.Outlets.of(true, grotto, grotto, true, 0, 0, 1, 3, 256)
.maximumCoastalPerTile());
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Outlets.of(
true, grotto, grotto, true, 0, 0, 1, 3, -1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Outlets.of(
true, grotto, grotto, true, 0, 0, 1, 3, 257));
}
@Test
public void dropGeometryNarrowsFlowAndExpandsItsReceivingBasin() {
HydrologyPlannerSettings.Drops drops = new HydrologyPlannerSettings.Drops(
HydrologyPlannerSettings.Drops drops = HydrologyPlannerSettings.Drops.of(
2,
1.4D,
2,
@@ -293,7 +295,7 @@ public class HydrologyPlannerSettingsTest {
assertEquals(2, drops.flowDepth(6));
assertEquals(9, drops.basinWidth(5));
assertEquals(5, drops.basinDepth(2, 8));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Drops(
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Drops.of(
2,
1.4D,
2,
@@ -345,17 +347,17 @@ public class HydrologyPlannerSettingsTest {
@Test
public void seaCaveBoundsAreEnforcedAndTheDefaultsPlanNone() {
HydrologyPlannerSettings.SeaCaves seaCaves = new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 12);
HydrologyPlannerSettings.SeaCaves seaCaves = HydrologyPlannerSettings.SeaCaves.of(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));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, -1, 160, 8, 12));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 65, 160, 8, 12));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 15, 8, 12));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 8193, 8, 12));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 160, 0, 12));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 160, 129, 12));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 160, 8, -1));
assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 160, 8, 129));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
assertEquals(HydrologyPlannerSettings.SeaCaves.disabled(), base.seaCaves());
@@ -382,7 +384,7 @@ public class HydrologyPlannerSettingsTest {
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(
HydrologyPlannerSettings.Underground underground = HydrologyPlannerSettings.Underground.of(
false, noSources, -48, 72, 3, 8, 1, 3, 6, 14, true, 0);
HydrologyPlannerSettings quiet = new HydrologyPlannerSettings(
base.seaLevel(),
@@ -408,7 +410,7 @@ public class HydrologyPlannerSettingsTest {
List.of(),
List.of(),
surface.shoreWidth(),
new HydrologyPlannerSettings.SeaCaves(true, 1, 64, 8, 12)
HydrologyPlannerSettings.SeaCaves.of(true, 1, 64, 8, 12)
);
int alignedHalo = Math.min(base.routing().maximumRouteLength(), base.routing().sampleSpacing() * 2)
@@ -416,4 +418,319 @@ public class HydrologyPlannerSettingsTest {
assertEquals(0, quiet.publicationRadius());
assertEquals(alignedHalo + base.outlets().coastalGrotto().horizontalRadius() + 12 + 1, withCaves.publicationRadius());
}
@Test
public void channelSettingsPinTheirDefaultsAndRejectValuesOutsideTheirBounds() {
HydrologyPlannerSettings.Channel channel = HydrologyPlannerSettings.Channel.defaults();
assertEquals(new HydrologyPlannerSettings.Channel(16, 0.6D, 1.4D, 1D), channel);
assertEquals(channel, HydrologyPlannerSettings.Banks.defaults().channel());
assertEquals(channel, HydrologyPlannerSettings.defaults().surface().banks().channel());
new HydrologyPlannerSettings.Channel(0, 1D, 1D, 0D);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Channel(-1, 0.6D, 1.4D, 1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Channel(16, 0D, 1.4D, 1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Channel(16, 1.01D, 1.4D, 1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Channel(16, Double.NaN, 1.4D, 1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Channel(16, 0.6D, 0.99D, 1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Channel(16, 0.6D, Double.POSITIVE_INFINITY, 1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Channel(16, 0.6D, 1.4D, -0.1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Channel(16, 0.6D, 1.4D, Double.NaN));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Banks banks = base.surface().banks();
HydrologyPlannerSettings smoothed = withBanks(base, banksWith(banks, banks.erosion(),
new HydrologyPlannerSettings.Channel(8, 0.6D, 1.4D, 1D), banks.flow()));
assertTrue(base.fingerprint() != smoothed.fingerprint());
assertEquals(base.publicationRadius(), smoothed.publicationRadius());
}
@Test
public void flowSettingsPinTheirDefaultsAndRejectValuesOutsideTheirBounds() {
HydrologyPlannerSettings.Flow flow = HydrologyPlannerSettings.Flow.defaults();
assertEquals(new HydrologyPlannerSettings.Flow(0.65D, 2, 2D, 1), flow);
assertEquals(flow, HydrologyPlannerSettings.Banks.defaults().flow());
assertEquals(flow, HydrologyPlannerSettings.defaults().surface().banks().flow());
new HydrologyPlannerSettings.Flow(0D, 1, 0D, 0);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Flow(-0.01D, 2, 2D, 1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Flow(1D, 2, 2D, 1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Flow(Double.NaN, 2, 2D, 1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Flow(0.65D, 0, 2D, 1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Flow(0.65D, 2, -0.1D, 1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Flow(0.65D, 2, Double.POSITIVE_INFINITY, 1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Flow(0.65D, 2, 2D, -1));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Banks banks = base.surface().banks();
HydrologyPlannerSettings plunging = withBanks(base, banksWith(banks, banks.erosion(), banks.channel(),
new HydrologyPlannerSettings.Flow(0.65D, 3, 2D, 1)));
assertTrue(base.fingerprint() != plunging.fingerprint());
assertEquals(base.publicationRadius(), plunging.publicationRadius());
}
@Test
public void inletSettingsCarryTheEstuaryRampAndRejectValuesOutsideTheirBounds() {
assertEquals(new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.5D, 1D), HydrologyPlannerSettings.Inlet.defaults());
assertEquals(new HydrologyPlannerSettings.Inlet(0, 0, 0, 0.5D, 1D), HydrologyPlannerSettings.Inlet.none());
assertEquals(HydrologyPlannerSettings.Inlet.defaults(), HydrologyPlannerSettings.Inlet.of(64, 3, 32));
assertEquals(0.5D, HydrologyPlannerSettings.defaults().surface().banks().inlet().courseFraction(), 0D);
assertEquals(1D, HydrologyPlannerSettings.defaults().surface().banks().inlet().rampSlope(), 0D);
new HydrologyPlannerSettings.Inlet(64, 3, 32, 1D, 0.01D);
new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.01D, 4D);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, 32, 0D, 1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, 32, 1.01D, 1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, 32, Double.NaN, 1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.5D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.5D, -1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.5D, Double.POSITIVE_INFINITY));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings ramped = withBanks(base, base.surface().banks()
.withInlet(new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.25D, 1D)));
assertTrue(base.fingerprint() != ramped.fingerprint());
assertEquals(base.publicationRadius(), ramped.publicationRadius());
}
@Test
public void erosionSettingsPinTheirDefaultsAndRejectValuesOutsideTheirBounds() {
HydrologyPlannerSettings.Erosion erosion = HydrologyPlannerSettings.Erosion.defaults();
assertEquals(new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D), erosion);
assertEquals(erosion, HydrologyPlannerSettings.Erosion.of(true, 12, 0.45D, 1D, 0.5D));
assertEquals(IrisRiverBlendStyle.SMOOTH, erosion.style());
assertEquals(4, erosion.terraceSteps());
assertEquals(0.5D, erosion.cliffFraction(), 0D);
assertEquals(IrisRiverBedProfile.BOWL, erosion.bedProfile());
assertEquals(0D, erosion.shoreRise(), 0D);
assertEquals(0D, erosion.blendBaseWidth(), 0D);
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.TERRACED, 2, 0D, IrisRiverBedProfile.V, 0D, 0D);
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.CLIFF, 16, 1D, IrisRiverBedProfile.U, 4D, 32D);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, null, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 1, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, -0.01D, IrisRiverBedProfile.BOWL, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 1.01D, IrisRiverBedProfile.BOWL, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, Double.NaN, IrisRiverBedProfile.BOWL, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, null, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, -0.1D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, Double.POSITIVE_INFINITY, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, -0.1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, Double.NaN));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Banks banks = base.surface().banks();
HydrologyPlannerSettings linear = withBanks(base, banksWith(banks,
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.LINEAR, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D),
banks.channel(), banks.flow()));
assertTrue(base.fingerprint() != linear.fingerprint());
assertEquals(base.publicationRadius(), linear.publicationRadius());
}
@Test
public void channelShapePinsTheOrganicWallDefaultsAndRejectsValuesOutsideTheirBounds() {
HydrologyPlannerSettings.ChannelShape shape = HydrologyPlannerSettings.ChannelShape.of(2.4D, 0.28D, 0.24D, 11);
assertEquals(new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D), shape);
assertEquals(shape, HydrologyPlannerSettings.Geometry.defaults().surface());
assertEquals(shape, HydrologyPlannerSettings.Geometry.defaults().underground());
assertEquals(shape, HydrologyPlannerSettings.Geometry.defaults().grottos());
new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.1D, 0.01D, 0.01D, 0D, 0D, 0D, 0.01D, 0D);
new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 4D, 4D, 4D, 1D, 1D, 1D, 1D, 1D);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.09D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 4.01D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, Double.NaN, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.7D, 0.6D, 0.08D, 0.06D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 4.01D, 0.08D, 0.06D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, Double.POSITIVE_INFINITY, 0.08D, 0.06D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, -0.01D, 0.06D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 1.01D, 0.06D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, -0.01D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 1.01D, 0D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, -0.01D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 1.01D, 0.62D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 1.01D, 0.2D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, -0.01D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.ChannelShape(2.4D, 0.28D, 0.24D, 11, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 1.01D));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Geometry geometry = base.geometry();
HydrologyPlannerSettings rounder = rebuild(base, base.surface(), base.underground(), base.outlets(),
new HydrologyPlannerSettings.Geometry(geometry.meanders(), geometry.surface(),
shapeWithRadialBase(geometry.underground(), 0.9D), geometry.grottos(), geometry.drops()),
base.seaCaves());
assertTrue(base.fingerprint() != rounder.fingerprint());
assertEquals(base.publicationRadius(), rounder.publicationRadius());
}
@Test
public void undergroundSettingsPinRockCoverAndRejectValuesOutsideTheirBounds() {
HydrologyPlannerSettings.Source sources = HydrologyPlannerSettings.defaults().underground().sources();
HydrologyPlannerSettings.Underground underground = HydrologyPlannerSettings.Underground.of(
true, sources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1);
assertEquals(new HydrologyPlannerSettings.Underground(true, sources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1, 1, 1, 8), underground);
assertEquals(underground, HydrologyPlannerSettings.defaults().underground());
assertEquals(1, underground.minimumRockCover());
assertEquals(1, underground.minimumFloorCover());
assertEquals(8, underground.wideningSources());
new HydrologyPlannerSettings.Underground(true, sources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1, 256, 256, 1024);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Underground(true, sources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1, 0, 1, 8));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Underground(true, sources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1, 257, 1, 8));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Underground(true, sources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1, 1, 0, 8));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Underground(true, sources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1, 1, 257, 8));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Underground(true, sources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1, 1, 1, 0));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Underground(true, sources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1, 1, 1, 1025));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings covered = rebuild(base, base.surface(),
new HydrologyPlannerSettings.Underground(true, sources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1, 2, 1, 8),
base.outlets(), base.geometry(), base.seaCaves());
assertTrue(base.fingerprint() != covered.fingerprint());
assertEquals(base.publicationRadius(), covered.publicationRadius());
}
@Test
public void outletSettingsPinTheCliffSlopeFactorAndRejectValuesOutsideTheirBounds() {
HydrologyPlannerSettings.Grotto grotto = new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 1);
HydrologyPlannerSettings.Outlets outlets = HydrologyPlannerSettings.Outlets.of(true, grotto, grotto, true, 0, 0, 1, 3, 0);
assertEquals(new HydrologyPlannerSettings.Outlets(true, grotto, grotto, true, 0, 0, 1, 3, 0, 0.5D), outlets);
assertEquals(0.5D, HydrologyPlannerSettings.defaults().outlets().coastalCliffSlopeFactor(), 0D);
new HydrologyPlannerSettings.Outlets(true, grotto, grotto, true, 0, 0, 1, 3, 0, 0D);
new HydrologyPlannerSettings.Outlets(true, grotto, grotto, true, 0, 0, 1, 3, 0, 4D);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(true, grotto, grotto, true, 0, 0, 1, 3, 0, -0.1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(true, grotto, grotto, true, 0, 0, 1, 3, 0, Double.NaN));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(true, grotto, grotto, true, 0, 0, 1, 3, 0, Double.POSITIVE_INFINITY));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Outlets o = base.outlets();
HydrologyPlannerSettings steeper = rebuild(base, base.surface(), base.underground(),
new HydrologyPlannerSettings.Outlets(o.oceanEnabled(), o.coastalGrotto(), o.inlandGrotto(), o.surfaceSinkholesEnabled(),
o.coastalCliffMinimumHeight(), o.mouthLevelingDistance(), o.maximumOceanApron(), o.maximumPerTile(),
o.maximumCoastalPerTile(), 1D),
base.geometry(), base.seaCaves());
assertTrue(base.fingerprint() != steeper.fingerprint());
assertEquals(base.publicationRadius(), steeper.publicationRadius());
}
@Test
public void seaCaveSettingsPinTheSweepJitterAndRejectValuesOutsideTheirBounds() {
assertEquals(new HydrologyPlannerSettings.SeaCaves(false, 0, 16, 1, 0, 25D), HydrologyPlannerSettings.SeaCaves.disabled());
assertEquals(new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 12, 25D), HydrologyPlannerSettings.SeaCaves.of(true, 3, 160, 8, 12));
assertEquals(25D, HydrologyPlannerSettings.defaults().seaCaves().sweepJitterDegrees(), 0D);
new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 12, 0D);
new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 12, 180D);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 12, -0.1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 12, 180.1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 12, Double.NaN));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings straight = rebuild(base, base.surface(), base.underground(), base.outlets(), base.geometry(),
new HydrologyPlannerSettings.SeaCaves(true, 1, 64, 8, 12, 25D));
HydrologyPlannerSettings jittered = rebuild(base, base.surface(), base.underground(), base.outlets(), base.geometry(),
new HydrologyPlannerSettings.SeaCaves(true, 1, 64, 8, 12, 40D));
assertTrue(straight.fingerprint() != jittered.fingerprint());
assertEquals(straight.publicationRadius(), jittered.publicationRadius());
}
@Test
public void dropSettingsPinTheUndergroundCascadeRunAndRejectValuesOutsideTheirBounds() {
HydrologyPlannerSettings.Drops drops = HydrologyPlannerSettings.Drops.of(2, 1.4D, 2, 0.45D, 2, 1.8D, 8);
assertEquals(new HydrologyPlannerSettings.Drops(2, 1.4D, 2, 0.45D, 2, 1.8D, 8, 0), drops);
assertEquals(drops, HydrologyPlannerSettings.Geometry.defaults().drops());
assertEquals(0, drops.undergroundCascadeRunPerBlock());
new HydrologyPlannerSettings.Drops(2, 1.4D, 2, 0.45D, 2, 1.8D, 8, 64);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Drops(2, 1.4D, 2, 0.45D, 2, 1.8D, 8, -1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Drops(2, 1.4D, 2, 0.45D, 2, 1.8D, 8, 65));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Geometry geometry = base.geometry();
HydrologyPlannerSettings longer = rebuild(base, base.surface(), base.underground(), base.outlets(),
new HydrologyPlannerSettings.Geometry(geometry.meanders(), geometry.surface(), geometry.underground(), geometry.grottos(),
new HydrologyPlannerSettings.Drops(2, 1.4D, 2, 0.45D, 2, 1.8D, 8, 3)),
base.seaCaves());
assertTrue(base.fingerprint() != longer.fingerprint());
assertEquals(base.publicationRadius(), longer.publicationRadius());
}
@Test
public void bankSettingsCarryChannelAndFlowAndTheNewKnobsLeaveTheEnvelopeAlone() {
HydrologyPlannerSettings.Banks banks = HydrologyPlannerSettings.Banks.defaults();
assertEquals(HydrologyPlannerSettings.Banks.of(0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.defaults(),
2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()), banks);
assertEquals(HydrologyPlannerSettings.Channel.defaults(), banks.channel());
assertEquals(HydrologyPlannerSettings.Flow.defaults(), banks.flow());
assertEquals(HydrologyPlannerSettings.Channel.defaults(), banks.withInlet(HydrologyPlannerSettings.Inlet.none()).channel());
assertEquals(HydrologyPlannerSettings.Flow.defaults(), banks.withInlet(HydrologyPlannerSettings.Inlet.none()).flow());
assertEquals(HydrologyPlannerSettings.Inlet.none(), banks.withInlet(HydrologyPlannerSettings.Inlet.none()).inlet());
assertThrows(IllegalArgumentException.class, () -> banksWith(banks, banks.erosion(), null, banks.flow()));
assertThrows(IllegalArgumentException.class, () -> banksWith(banks, banks.erosion(), banks.channel(), null));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Underground u = base.underground();
HydrologyPlannerSettings.Outlets o = base.outlets();
HydrologyPlannerSettings.Geometry geometry = base.geometry();
HydrologyPlannerSettings reshaped = rebuild(base,
surfaceWith(base.surface(), banksWith(banks,
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.TERRACED, 8, 0.25D, IrisRiverBedProfile.U, 1D, 4D),
new HydrologyPlannerSettings.Channel(8, 0.5D, 2D, 2D),
new HydrologyPlannerSettings.Flow(0.5D, 3, 3D, 2))
.withInlet(new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.25D, 2D))),
new HydrologyPlannerSettings.Underground(u.enabled(), u.sources(), u.minimumFluidY(), u.maximumFluidY(), u.minimumWidth(),
u.maximumWidth(), u.minimumDepth(), u.maximumDepth(), u.minimumHeadroom(), u.maximumHeadroom(),
u.connectToExistingCaves(), u.tributaries(), 4, 3, 16),
new HydrologyPlannerSettings.Outlets(o.oceanEnabled(), o.coastalGrotto(), o.inlandGrotto(), o.surfaceSinkholesEnabled(),
o.coastalCliffMinimumHeight(), o.mouthLevelingDistance(), o.maximumOceanApron(), o.maximumPerTile(),
o.maximumCoastalPerTile(), 1D),
new HydrologyPlannerSettings.Geometry(geometry.meanders(), shapeWithRadialBase(geometry.surface(), 0.9D),
shapeWithRadialBase(geometry.underground(), 0.7D), shapeWithRadialBase(geometry.grottos(), 1.1D),
new HydrologyPlannerSettings.Drops(2, 1.4D, 2, 0.45D, 2, 1.8D, 8, 3)),
new HydrologyPlannerSettings.SeaCaves(false, 0, 16, 1, 0, 40D));
assertEquals(base.publicationRadius(), reshaped.publicationRadius());
assertEquals(base.crossTileColorPeriod(), reshaped.crossTileColorPeriod());
assertTrue(base.fingerprint() != reshaped.fingerprint());
}
private static HydrologyPlannerSettings withBanks(HydrologyPlannerSettings base, HydrologyPlannerSettings.Banks banks) {
return rebuild(base, surfaceWith(base.surface(), banks), base.underground(), base.outlets(), base.geometry(), base.seaCaves());
}
private static HydrologyPlannerSettings rebuild(
HydrologyPlannerSettings base,
HydrologyPlannerSettings.Surface surface,
HydrologyPlannerSettings.Underground underground,
HydrologyPlannerSettings.Outlets outlets,
HydrologyPlannerSettings.Geometry geometry,
HydrologyPlannerSettings.SeaCaves seaCaves
) {
return new HydrologyPlannerSettings(base.seaLevel(), base.routing(), surface, base.hydraulics(), underground, outlets,
geometry, base.deepFluids(), base.surfacePools(), base.widestShoreBiomeWidth(), seaCaves);
}
private static HydrologyPlannerSettings.Surface surfaceWith(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);
}
private static HydrologyPlannerSettings.Banks banksWith(
HydrologyPlannerSettings.Banks banks,
HydrologyPlannerSettings.Erosion erosion,
HydrologyPlannerSettings.Channel channel,
HydrologyPlannerSettings.Flow flow
) {
return new HydrologyPlannerSettings.Banks(banks.sink(), banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(),
banks.roughness(), banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(), banks.mouthFlareRatio(),
banks.inlet(), banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(), erosion, banks.ponds(), channel, flow);
}
private static HydrologyPlannerSettings.ChannelShape shapeWithRadialBase(HydrologyPlannerSettings.ChannelShape shape, double radialBase) {
return new HydrologyPlannerSettings.ChannelShape(shape.bedRoundness(), shape.bedRoughness(), shape.wallRoughness(), shape.roughnessWavelength(),
radialBase, shape.radialMinimum(), shape.radialMaximum(), shape.primaryLobeStrength(), shape.detailLobeStrength(),
shape.ceilingRoughness(), shape.aspectMinimum(), shape.aspectRange());
}
}
@@ -117,7 +117,7 @@ public class HydrologyPlannerTest {
public void zeroMouthLevelingDistanceKeepsAcceptedCoursesHydraulicallyNonRising() {
HydrologyPlannerSettings base = standardSettings(4D, 0D, true, false, List.of());
HydrologyPlannerSettings.Outlets configured = base.outlets();
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings settings = withOutlets(base, HydrologyPlannerSettings.Outlets.of(
configured.oceanEnabled(),
configured.coastalGrotto(),
configured.inlandGrotto(),
@@ -517,7 +517,9 @@ public class HydrologyPlannerTest {
"flooded",
List.of("alpha"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
};
HydrologyTile tile = new HydrologyPlanner(812L, settings, terrain, solidCaveView()).plan(TILE);
@@ -753,7 +755,7 @@ public class HydrologyPlannerTest {
62
);
HydrologyPlannerSettings.Outlets configured = base.outlets();
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings settings = withOutlets(base, HydrologyPlannerSettings.Outlets.of(
configured.oceanEnabled(),
configured.coastalGrotto(),
configured.inlandGrotto(),
@@ -1132,7 +1134,7 @@ public class HydrologyPlannerTest {
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
HydrologyPlannerSettings settings = withOutlets(shaped, new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings settings = withOutlets(shaped, HydrologyPlannerSettings.Outlets.of(
true,
base.outlets().coastalGrotto(),
base.outlets().inlandGrotto(),
@@ -1237,7 +1239,7 @@ public class HydrologyPlannerTest {
@Test
public void coastalGrottoIsASeaLevelEllipsoidWithADirectOceanConnection() {
HydrologyPlannerSettings base = standardSettings(2D, 0D, true, true, List.of());
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings settings = withOutlets(base, HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
base.outlets().inlandGrotto(),
@@ -1298,7 +1300,7 @@ public class HydrologyPlannerTest {
@Test
public void coastalAndInlandOutletsAreBudgetedSeparately() {
HydrologyPlannerSettings base = standardSettings(2D, 0D, true, true, List.of());
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings settings = withOutlets(base, HydrologyPlannerSettings.Outlets.of(
true,
base.outlets().coastalGrotto(),
base.outlets().inlandGrotto(),
@@ -1389,7 +1391,7 @@ public class HydrologyPlannerTest {
base.routing(),
base.surface(),
base.hydraulics(),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
true,
base.underground().sources(),
40,
@@ -1468,7 +1470,7 @@ public class HydrologyPlannerTest {
private HydrologyPlannerSettings mixedCoastSettings(int maximumCoastalPerTile) {
HydrologyPlannerSettings base = standardSettings(2D, 0D, true, false, List.of());
return withOutlets(base, new HydrologyPlannerSettings.Outlets(
return withOutlets(base, HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
base.outlets().inlandGrotto(),
@@ -1543,7 +1545,7 @@ public class HydrologyPlannerTest {
public void disabledSurfaceSinkholesRejectSurfaceSourcesButKeepUndergroundSourcesEligible() {
HydrologyPlannerSettings base = standardSettings(0D, 1D, false, true, List.of());
HydrologyPlannerSettings.Outlets configured = base.outlets();
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings settings = withOutlets(base, HydrologyPlannerSettings.Outlets.of(
configured.oceanEnabled(),
configured.coastalGrotto(),
configured.inlandGrotto(),
@@ -1944,7 +1946,7 @@ public class HydrologyPlannerTest {
1.5D,
HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
undergroundDensity > 0D,
undergroundSources,
68,
@@ -1958,7 +1960,7 @@ public class HydrologyPlannerTest {
true,
0
),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets.of(
oceanEnabled,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(inlandEnabled, 4, 3, 3, 4096),
@@ -1978,7 +1980,7 @@ public class HydrologyPlannerTest {
private HydrologyPlannerSettings.Geometry stableGeometry() {
HydrologyPlannerSettings.ChannelShape stableChannel =
new HydrologyPlannerSettings.ChannelShape(2D, 0D, 0D, 11);
HydrologyPlannerSettings.ChannelShape.of(2D, 0D, 0D, 11);
return new HydrologyPlannerSettings.Geometry(
new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D),
stableChannel,
@@ -2018,7 +2020,7 @@ public class HydrologyPlannerTest {
HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(5),
base.underground(),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets.of(
true,
outlets.coastalGrotto(),
outlets.inlandGrotto(),
@@ -2103,7 +2105,9 @@ public class HydrologyPlannerTest {
"flooded",
List.of("alpha"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
};
}
@@ -2170,7 +2174,9 @@ public class HydrologyPlannerTest {
"flooded",
List.of("alpha", "beta"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -2204,7 +2210,9 @@ public class HydrologyPlannerTest {
"flooded",
List.of("alpha"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -2247,7 +2255,9 @@ public class HydrologyPlannerTest {
"flooded",
List.of("beta", "alpha"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -2281,7 +2291,9 @@ public class HydrologyPlannerTest {
"flooded",
List.of("alpha"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -2315,7 +2327,9 @@ public class HydrologyPlannerTest {
"flooded",
List.of("alpha", "beta"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -2357,7 +2371,7 @@ public class HydrologyPlannerTest {
settings.routing(),
settings.surface(),
settings.hydraulics(),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
true,
sources,
underground.minimumFluidY(),
@@ -2390,7 +2404,7 @@ public class HydrologyPlannerTest {
settings.routing(),
settings.surface(),
settings.hydraulics(),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
underground.enabled(),
underground.sources(),
minimumFluidY,
@@ -2422,7 +2436,7 @@ public class HydrologyPlannerTest {
settings.routing(),
settings.surface(),
settings.hydraulics(),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
underground.enabled(),
underground.sources(),
underground.minimumFluidY(),
@@ -157,7 +157,9 @@ public class HydrologyPlannerTributaryTest {
List.of("default"),
List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -221,7 +223,9 @@ public class HydrologyPlannerTributaryTest {
List.of("default"),
List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -236,9 +240,9 @@ public class HydrologyPlannerTributaryTest {
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(
HydrologyPlannerSettings.Underground underground = HydrologyPlannerSettings.Underground.of(
false, base.underground().sources(), -48, 72, 3, 8, 1, 3, 6, 14, true, 0);
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets outlets = HydrologyPlannerSettings.Outlets.of(
true,
base.outlets().coastalGrotto(),
base.outlets().inlandGrotto(),
@@ -349,7 +353,7 @@ public class HydrologyPlannerTributaryTest {
@Test
public void undergroundTributariesAreBounded() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
org.junit.Assert.assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Underground(
org.junit.Assert.assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Underground.of(
true, base.underground().sources(), -48, 72, 3, 8, 1, 3, 6, 14, true, 5));
}
@@ -370,7 +374,7 @@ public class HydrologyPlannerTributaryTest {
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);
List.of("default"), List.of(), Double.NaN, null, Double.NaN, true);
}
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);
@@ -378,20 +382,20 @@ public class HydrologyPlannerTributaryTest {
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);
List.of("default"), List.of(), Double.NaN, null, Double.NaN, true);
}
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);
HydrologyPlannerSettings.ChannelShape stableChannel = HydrologyPlannerSettings.ChannelShape.of(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(
HydrologyPlannerSettings.Underground.of(true, sources, 68, 82, 4, 12, 2, 4, 5, 9, true, tributaries),
HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
@@ -0,0 +1,701 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
import org.junit.Test;
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.assertNotEquals;
import static org.junit.Assert.assertTrue;
/**
* The dimension-scoped underground shaping knobs: rock cover over a passage, floor cover under it,
* the source count the tributary widening saturates at, the underground cascade run, the coastal
* cliff slope factor and the sea cave sweep jitter. Every default reproduces the previous arithmetic.
*/
public class HydrologyPlannerUndergroundKnobsTest {
private static final HydrologyTileKey TILE = new HydrologyTileKey(0, 0);
private static final int SEA_LEVEL = 63;
private static final CaveVoxelView SOLID_CAVE_VIEW = new SolidCaveVoxelView();
private static final HydrologyPlannerSettings.Grotto CHAMBER =
new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 32768);
@Test
public void rockCoverDefaultsReproduceThePlanAndAThickerCoverLowersOrRejectsPassages() {
HydrologyPlannerSettings base = undergroundSettings();
HydrologyTerrainSampler coast = undergroundCoast();
HydrologyTile fromFactory = new HydrologyPlanner(331L, base, coast, SOLID_CAVE_VIEW).plan(TILE);
HydrologyTile explicit = new HydrologyPlanner(
331L,
withCover(base, 1, 1, base.underground().wideningSources()),
coast,
SOLID_CAVE_VIEW
).plan(TILE);
List<RiverCourse> planned = courses(fromFactory, RiverCourseType.UNDERGROUND);
assertFalse("diagnostics=" + fromFactory.diagnosticCandidates(), planned.isEmpty());
assertEquals(fromFactory.courses(), explicit.courses());
assertCoveredByRock(coast, planned, base.underground().minimumHeadroom(), 1);
HydrologyTile covered = new HydrologyPlanner(
331L,
withCover(base, 16, 1, base.underground().wideningSources()),
coast,
SOLID_CAVE_VIEW
).plan(TILE);
assertCoveredByRock(coast, courses(covered, RiverCourseType.UNDERGROUND), base.underground().minimumHeadroom(), 16);
// No column on this coast stands 48 blocks over the configured fluid range, so every passage is refused.
HydrologyTile buried = new HydrologyPlanner(
331L,
withCover(base, 48, 1, base.underground().wideningSources()),
coast,
SOLID_CAVE_VIEW
).plan(TILE);
assertTrue(
"buried=" + courses(buried, RiverCourseType.UNDERGROUND).size()
+ " planned=" + planned.size(),
courses(buried, RiverCourseType.UNDERGROUND).size() < planned.size()
);
assertCoveredByRock(coast, courses(buried, RiverCourseType.UNDERGROUND), base.underground().minimumHeadroom(), 48);
}
@Test
public void floorCoverKeepsBedsAboveTheWorldFloor() {
HydrologyPlannerSettings base = inlandUndergroundSettings();
HydrologyTerrainSampler inland = inlandTerrain();
int worldFloor = 0;
// The head solver reserves the deepest possible basin under every node, then the floor cover.
int reserved = Math.max(base.underground().maximumDepth(), base.geometry().drops().maximumBasinDepth());
int floorCover = 48;
int bound = worldFloor + reserved + floorCover;
HydrologyTile shallow = new HydrologyPlanner(
8821L,
base,
inland,
worldFloor,
plannedSurface -> SOLID_CAVE_VIEW
).plan(TILE);
HydrologyTile raised = new HydrologyPlanner(
8821L,
withCover(base, base.underground().minimumRockCover(), floorCover, base.underground().wideningSources()),
inland,
worldFloor,
plannedSurface -> SOLID_CAVE_VIEW
).plan(TILE);
List<RiverCourse> shallowCourses = courses(shallow, RiverCourseType.UNDERGROUND);
assertFalse("diagnostics=" + shallow.diagnosticCandidates(), shallowCourses.isEmpty());
assertTrue(
"heads=" + heads(shallowCourses) + " bound=" + bound,
heads(shallowCourses).stream().anyMatch((Integer head) -> head < bound)
);
for (int head : heads(courses(raised, RiverCourseType.UNDERGROUND))) {
assertTrue("head=" + head + " bound=" + bound, head >= bound);
}
assertNotEquals(shallowCourses, courses(raised, RiverCourseType.UNDERGROUND));
}
@Test
public void wideningSourcesChangesTheDischargeWidening() {
HydrologyPlannerSettings base = undergroundSettings();
HydrologyTerrainSampler coast = undergroundCoast();
HydrologyTile saturatingEarly = new HydrologyPlanner(331L, base, coast, SOLID_CAVE_VIEW).plan(TILE);
HydrologyTile saturatingLate = new HydrologyPlanner(
331L,
withCover(base, base.underground().minimumRockCover(), base.underground().minimumFloorCover(), 64),
coast,
SOLID_CAVE_VIEW
).plan(TILE);
int wide = widestUndergroundSegment(saturatingEarly);
int narrow = widestUndergroundSegment(saturatingLate);
assertTrue("wide=" + wide, wide > 0);
assertTrue("narrow=" + narrow, narrow > 0);
assertTrue("wide=" + wide + " narrow=" + narrow, narrow < wide);
}
@Test
public void undergroundCascadeRunZeroKeepsTheShortestRunAndAPositiveValueLengthensIt() {
HydrologyPlannerSettings base = terracedUndergroundSettings();
HydrologyTerrainSampler terraced = terracedTerrain();
HydrologyTile shortest = new HydrologyPlanner(1442L, base, terraced, SOLID_CAVE_VIEW).plan(TILE);
HydrologyTile lengthened = new HydrologyPlanner(
1442L,
withUndergroundCascadeRun(base, 4),
terraced,
SOLID_CAVE_VIEW
).plan(TILE);
int shortRun = cascadePoints(shortest);
int longRun = cascadePoints(lengthened);
assertTrue("shortRun=" + shortRun, shortRun > 0);
assertTrue("shortRun=" + shortRun + " longRun=" + longRun, longRun > shortRun);
}
@Test
public void cliffSlopeFactorScalesTheSlopeRuleAndZeroTurnsItOff() {
// A coast five blocks over the sea, well under the twelve-block cliff height, sloping at one.
HydrologyPlannerSettings base = coastalSettings();
HydrologyTerrainSampler lowCoast = lowCoast();
HydrologyTile atTheDefault = new HydrologyPlanner(4711L, base, lowCoast, SOLID_CAVE_VIEW).plan(TILE);
HydrologyTile sensitive = new HydrologyPlanner(
4711L,
withCliffSlopeFactor(base, 0.05D),
lowCoast,
SOLID_CAVE_VIEW
).plan(TILE);
HydrologyTile slopeRuleOff = new HydrologyPlanner(
4711L,
withCliffSlopeFactor(base, 0D),
lowCoast,
SOLID_CAVE_VIEW
).plan(TILE);
assertFalse("diagnostics=" + atTheDefault.diagnosticCandidates(), atTheDefault.outlets().isEmpty());
for (RiverOutlet outlet : atTheDefault.outlets()) {
assertNotEquals(HydrologyFeatureType.COASTAL_GROTTO, outlet.type());
}
assertFalse("diagnostics=" + sensitive.diagnosticCandidates(), sensitive.outlets().isEmpty());
boolean grotto = false;
for (RiverOutlet outlet : sensitive.outlets()) {
grotto |= outlet.type() == HydrologyFeatureType.COASTAL_GROTTO;
}
assertTrue("outlets=" + sensitive.outlets(), grotto);
assertFalse("diagnostics=" + slopeRuleOff.diagnosticCandidates(), slopeRuleOff.outlets().isEmpty());
for (RiverOutlet outlet : slopeRuleOff.outlets()) {
assertNotEquals(HydrologyFeatureType.COASTAL_GROTTO, outlet.type());
}
}
@Test
public void sweepJitterZeroSweepsSeaCavesStraightInland() {
HydrologyPlannerSettings.SeaCaves jittered = HydrologyPlannerSettings.SeaCaves.of(true, 4, 32, 8, 12);
HydrologyPlannerSettings base = seaCaveSettings(jittered);
HydrologyTerrainSampler cliffCoast = cliffCoast();
HydrologyTile turned = new HydrologyPlanner(994L, base, cliffCoast).plan(TILE);
HydrologyTile straight = new HydrologyPlanner(
994L,
seaCaveSettings(new HydrologyPlannerSettings.SeaCaves(
jittered.enabled(),
jittered.maximumPerTile(),
jittered.minimumSpacing(),
jittered.minimumCoastHeight(),
jittered.depth(),
0D
)),
cliffCoast
).plan(TILE);
List<RiverCourse> turnedCaves = courses(turned, RiverCourseType.SEA_CAVE);
List<RiverCourse> straightCaves = courses(straight, RiverCourseType.SEA_CAVE);
assertFalse("diagnostics=" + turned.diagnosticCandidates(), turnedCaves.isEmpty());
assertFalse("diagnostics=" + straight.diagnosticCandidates(), straightCaves.isEmpty());
boolean offNormal = false;
for (RiverCourse cave : turnedCaves) {
HydraulicSegment chamber = cave.segments().getFirst();
offNormal |= chamber.start().z() != chamber.end().z();
}
assertTrue("caves=" + turnedCaves, offNormal);
for (RiverCourse cave : straightCaves) {
HydraulicSegment chamber = cave.segments().getFirst();
assertEquals("chamber=" + chamber, chamber.end().z(), chamber.start().z());
}
}
private void assertCoveredByRock(
HydrologyTerrainSampler sampler,
List<RiverCourse> courses,
int minimumHeadroom,
int rockCover
) {
for (RiverCourse course : courses) {
for (HydraulicSegment segment : course.segments()) {
for (HydrologyPoint point : List.of(segment.start(), segment.end())) {
HydrologyTerrainSample terrain = sampler.sample(point.x(), point.z());
if (terrain.ocean()) {
continue;
}
int head = Math.max(segment.upstreamHeadY(), segment.downstreamHeadY());
assertTrue(
"head=" + head + " cover=" + rockCover + " natural=" + terrain.naturalHeight(),
head + minimumHeadroom + rockCover <= terrain.naturalHeight()
);
}
}
}
}
private List<Integer> heads(List<RiverCourse> courses) {
ArrayList<Integer> heads = new ArrayList<>();
for (RiverCourse course : courses) {
for (HydraulicSegment segment : course.segments()) {
heads.add(segment.upstreamHeadY());
heads.add(segment.downstreamHeadY());
}
}
return List.copyOf(heads);
}
private int widestUndergroundSegment(HydrologyTile tile) {
int widest = 0;
for (RiverCourse course : courses(tile, RiverCourseType.UNDERGROUND)) {
for (HydraulicSegment segment : course.segments()) {
widest = Math.max(widest, segment.width());
}
}
return widest;
}
private int cascadePoints(HydrologyTile tile) {
int points = 0;
for (RiverCourse course : courses(tile, RiverCourseType.UNDERGROUND)) {
for (HydraulicSegment segment : course.segments()) {
if (segment.type() == HydrologyFeatureType.UNDERGROUND_DROP && segment.drop() > 0) {
points += segment.centerline().size();
}
}
}
return points;
}
private 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 List.copyOf(selected);
}
private HydrologyPlannerSettings withCover(
HydrologyPlannerSettings settings,
int minimumRockCover,
int minimumFloorCover,
int wideningSources
) {
HydrologyPlannerSettings.Underground underground = settings.underground();
return replaceUnderground(settings, new HydrologyPlannerSettings.Underground(
underground.enabled(),
underground.sources(),
underground.minimumFluidY(),
underground.maximumFluidY(),
underground.minimumWidth(),
underground.maximumWidth(),
underground.minimumDepth(),
underground.maximumDepth(),
underground.minimumHeadroom(),
underground.maximumHeadroom(),
underground.connectToExistingCaves(),
underground.tributaries(),
minimumRockCover,
minimumFloorCover,
wideningSources
));
}
private HydrologyPlannerSettings replaceUnderground(
HydrologyPlannerSettings settings,
HydrologyPlannerSettings.Underground underground
) {
return new HydrologyPlannerSettings(
settings.seaLevel(),
settings.routing(),
settings.surface(),
settings.hydraulics(),
underground,
settings.outlets(),
settings.geometry(),
settings.deepFluids(),
settings.surfacePools(),
settings.widestShoreBiomeWidth(),
settings.seaCaves()
);
}
private HydrologyPlannerSettings withUndergroundCascadeRun(HydrologyPlannerSettings settings, int runPerBlock) {
HydrologyPlannerSettings.Drops drops = settings.geometry().drops();
HydrologyPlannerSettings.Geometry geometry = settings.geometry();
return new HydrologyPlannerSettings(
settings.seaLevel(),
settings.routing(),
settings.surface(),
settings.hydraulics(),
settings.underground(),
settings.outlets(),
new HydrologyPlannerSettings.Geometry(
geometry.meanders(),
geometry.surface(),
geometry.underground(),
geometry.grottos(),
new HydrologyPlannerSettings.Drops(
drops.cascadeRunPerBlock(),
drops.cascadeExponent(),
drops.maximumCascadeStep(),
drops.flowWidthRatio(),
drops.maximumFlowDepth(),
drops.basinWidthRatio(),
drops.maximumBasinDepth(),
runPerBlock
)
),
settings.deepFluids(),
settings.surfacePools(),
settings.widestShoreBiomeWidth(),
settings.seaCaves()
);
}
private HydrologyPlannerSettings withCliffSlopeFactor(HydrologyPlannerSettings settings, double factor) {
HydrologyPlannerSettings.Outlets outlets = settings.outlets();
return new HydrologyPlannerSettings(
settings.seaLevel(),
settings.routing(),
settings.surface(),
settings.hydraulics(),
settings.underground(),
new HydrologyPlannerSettings.Outlets(
outlets.oceanEnabled(),
outlets.coastalGrotto(),
outlets.inlandGrotto(),
outlets.surfaceSinkholesEnabled(),
outlets.coastalCliffMinimumHeight(),
outlets.mouthLevelingDistance(),
outlets.maximumOceanApron(),
outlets.maximumPerTile(),
outlets.maximumCoastalPerTile(),
factor
),
settings.geometry(),
settings.deepFluids(),
settings.surfacePools(),
settings.widestShoreBiomeWidth(),
settings.seaCaves()
);
}
private HydrologyTerrainSampler undergroundCoast() {
return (int x, int z) -> {
if (x >= 112) {
return oceanColumn();
}
int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D);
return undergroundColumn(height, 72, 74, x >= 0 && x <= 24, true);
};
}
private HydrologyTerrainSampler inlandTerrain() {
return (int x, int z) -> undergroundColumn(118 - Math.floorDiv(x, 12), 44, 48, x >= 0 && x <= 24, true);
}
private HydrologyTerrainSampler terracedTerrain() {
return (int x, int z) -> {
if (x >= 112) {
return oceanColumn();
}
int fluidY = 84 - Math.max(0, Math.floorDiv(x, 32)) * 2;
return undergroundColumn(120, fluidY - 4, fluidY, x == 0 && z == 0, z == 0);
};
}
private HydrologyTerrainSampler lowCoast() {
return (int x, int z) -> x >= 112
? oceanColumn()
: HydrologyTerrainSample.openLand(70 - Math.floorDiv(x, 40), 1D, "land");
}
private HydrologyTerrainSampler cliffCoast() {
return (int x, int z) -> x >= 112
? HydrologyTerrainSample.ocean(54, "ocean")
: HydrologyTerrainSample.openLand(92, 2D, "land");
}
private HydrologyTerrainSample oceanColumn() {
return HydrologyTerrainSample.ocean(54, "ocean");
}
private HydrologyTerrainSample undergroundColumn(
int height,
int caveFloorY,
int caveFluidY,
boolean source,
boolean transit
) {
return new HydrologyTerrainSample(
height,
1D,
false,
true,
caveFloorY,
caveFluidY,
transit,
transit,
false,
false,
source,
source,
0D,
0D,
source ? 1D : 0D,
1D,
1D,
1D,
1D,
1D,
"land",
"surface",
"mouth",
"shore",
"dry",
"flooded",
List.of("default"),
List.of(),
Double.NaN,
null,
Double.NaN,
true
);
}
private HydrologyPlannerSettings undergroundSettings() {
return plannerSettings(
disabledSource(),
new HydrologyPlannerSettings.Source(true, 3D, Integer.MIN_VALUE, 0, 4, 32),
false,
true,
68,
82,
4,
12,
2,
4,
5,
9,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
true,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
private HydrologyPlannerSettings inlandUndergroundSettings() {
return plannerSettings(
disabledSource(),
new HydrologyPlannerSettings.Source(true, 3D, Integer.MIN_VALUE, 0, 4, 32),
false,
true,
40,
60,
4,
12,
2,
4,
5,
9,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
false,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
private HydrologyPlannerSettings terracedUndergroundSettings() {
return plannerSettings(
disabledSource(),
new HydrologyPlannerSettings.Source(true, 1D, Integer.MIN_VALUE, 1, 1, 16),
false,
true,
64,
84,
2,
2,
1,
2,
2,
4,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
true,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
private HydrologyPlannerSettings coastalSettings() {
return plannerSettings(
new HydrologyPlannerSettings.Source(true, 4D, 0, 0, 6, 24),
disabledSource(),
true,
false,
68,
82,
4,
12,
2,
4,
5,
9,
CHAMBER,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
true,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
// A riverless cliff coast: the drainage lattice still runs, so the sea cave pass has a grid to walk.
private HydrologyPlannerSettings seaCaveSettings(HydrologyPlannerSettings.SeaCaves seaCaves) {
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
true,
new HydrologyPlannerSettings.Source(true, 0D, 80, 0, 6, 24),
4,
18,
2,
4,
10,
1.5D,
HydrologyPlannerSettings.Banks.defaults()
);
return new HydrologyPlannerSettings(
SEA_LEVEL,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
surface,
new HydrologyPlannerSettings.Hydraulics(4),
HydrologyPlannerSettings.Underground.of(
false,
new HydrologyPlannerSettings.Source(true, 0D, Integer.MIN_VALUE, 0, 4, 32),
68,
82,
4,
12,
2,
4,
5,
9,
true,
0
),
HydrologyPlannerSettings.Outlets.of(
true,
CHAMBER,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
false,
12,
32,
12,
4,
4
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(),
List.of(),
surface.shoreWidth(),
seaCaves
);
}
private HydrologyPlannerSettings.Source disabledSource() {
return new HydrologyPlannerSettings.Source(false, 0D, Integer.MIN_VALUE, 0, 0, 0);
}
private HydrologyPlannerSettings plannerSettings(
HydrologyPlannerSettings.Source surfaceSources,
HydrologyPlannerSettings.Source undergroundSources,
boolean surfaceEnabled,
boolean undergroundEnabled,
int minimumFluidY,
int maximumFluidY,
int minimumWidth,
int maximumWidth,
int minimumDepth,
int maximumDepth,
int minimumHeadroom,
int maximumHeadroom,
HydrologyPlannerSettings.Grotto coastalGrotto,
HydrologyPlannerSettings.Grotto inlandGrotto,
boolean oceanEnabled,
HydrologyPlannerSettings.SeaCaves seaCaves
) {
return new HydrologyPlannerSettings(
SEA_LEVEL,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Surface(
surfaceEnabled,
surfaceSources,
4,
18,
2,
4,
10,
1.5D,
HydrologyPlannerSettings.Banks.defaults()
),
new HydrologyPlannerSettings.Hydraulics(4),
HydrologyPlannerSettings.Underground.of(
undergroundEnabled,
undergroundSources,
minimumFluidY,
maximumFluidY,
minimumWidth,
maximumWidth,
minimumDepth,
maximumDepth,
minimumHeadroom,
maximumHeadroom,
false,
0
),
HydrologyPlannerSettings.Outlets.of(
oceanEnabled,
coastalGrotto,
inlandGrotto,
false,
12,
32,
8,
4,
4
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(),
List.of(),
0D,
seaCaves
);
}
private static final class SolidCaveVoxelView implements CaveVoxelView {
@Override
public boolean isInWorld(CavePosition position) {
return position.y() > -2048 && position.y() < 2048;
}
@Override
public CaveVoxel voxelAt(CavePosition position) {
return CaveVoxel.SOLID;
}
@Override
public boolean isOpenToSurface(CavePosition position) {
return false;
}
@Override
public boolean isAboveTerrainSurface(CavePosition position) {
return false;
}
}
}
@@ -207,7 +207,7 @@ public class HydrologyStyledGeometryTest {
1.5D,
HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
undergroundDensity > 0D,
undergroundSources,
68,
@@ -221,7 +221,7 @@ public class HydrologyStyledGeometryTest {
true,
0
),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
@@ -270,7 +270,9 @@ public class HydrologyStyledGeometryTest {
"flooded",
List.of("water"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D);
@@ -304,7 +306,9 @@ public class HydrologyStyledGeometryTest {
"flooded",
List.of("water"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -82,7 +82,9 @@ public final class HydrologyTerrainSampleTest {
"flooded",
profiles, List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@Test
@@ -111,11 +113,59 @@ public final class HydrologyTerrainSampleTest {
}
private static HydrologyTerrainSample sample(List<String> profiles, double shoreBiomeWidth, String confinesKey) {
return sample(profiles, shoreBiomeWidth, confinesKey, Double.NaN, true);
}
private static HydrologyTerrainSample sample(
List<String> profiles, double shoreBiomeWidth, String confinesKey, double shoreWidth, boolean erosion) {
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
profiles, List.of(), shoreBiomeWidth, confinesKey, shoreWidth, erosion
);
}
@Test
public void shoreWidthInheritsWhenUnsetAndRejectsNegatives() {
HydrologyTerrainSample unset = HydrologyTerrainSample.openLand(80, 0D, "parent");
HydrologyTerrainSample ocean = HydrologyTerrainSample.ocean(50, "ocean");
assertTrue(Double.isNaN(unset.shoreWidth()));
assertTrue(unset.erosion());
assertTrue(Double.isNaN(ocean.shoreWidth()));
assertTrue(ocean.erosion());
assertEquals(1.5D, unset.shoreWidth(1.5D), 0D);
HydrologyTerrainSample beach = sample(List.of("default"), Double.NaN, null, 3D, true);
assertEquals(3D, beach.shoreWidth(1.5D), 0D);
assertEquals(0D, sample(List.of("default"), Double.NaN, null, 0D, true).shoreWidth(1.5D), 0D);
assertThrows(IllegalArgumentException.class, () -> sample(List.of("default"), Double.NaN, null, -1D, true));
assertThrows(IllegalArgumentException.class, () -> sample(List.of("default"), Double.NaN, null, Double.POSITIVE_INFINITY, true));
assertThrows(IllegalArgumentException.class, () -> sample(List.of("default"), Double.NaN, null, Double.NEGATIVE_INFINITY, true));
}
@Test
public void shoreWidthAndErosionCopiersReplaceOnlyTheirOwnComponent() {
HydrologyTerrainSample open = HydrologyTerrainSample.openLand(80, 0D, "parent");
HydrologyTerrainSample beach = open.withShoreWidth(3D);
HydrologyTerrainSample bare = open.withErosion(false);
assertEquals(3D, beach.shoreWidth(1.5D), 0D);
assertTrue(beach.erosion());
assertEquals(open.withShoreWidth(Double.NaN), open);
assertFalse(bare.erosion());
assertTrue(Double.isNaN(bare.shoreWidth()));
assertEquals(open.withErosion(true), open);
assertEquals(open.naturalHeight(), beach.naturalHeight());
assertEquals(open.preferredProfileKeys(), bare.preferredProfileKeys());
assertThrows(IllegalArgumentException.class, () -> open.withShoreWidth(-0.5D));
assertEquals(3D, beach.withSlope(2D).shoreWidth(1.5D), 0D);
assertEquals(3D, beach.withSurfacePoolKeys(List.of("pool")).shoreWidth(1.5D), 0D);
assertFalse(bare.withSlope(2D).erosion());
assertFalse(bare.withSurfacePoolKeys(List.of("pool")).erosion());
assertFalse(beach.withErosion(false).erosion());
assertEquals(3D, beach.withErosion(false).shoreWidth(1.5D), 0D);
}
}
@@ -248,8 +248,8 @@ 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, 0),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Underground.of(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0),
HydrologyPlannerSettings.Outlets.of(
false,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
@@ -289,8 +289,8 @@ 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, 0),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Underground.of(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0),
HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
@@ -310,7 +310,7 @@ public class HydrologyTileCacheTest {
// 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(0, 3D, 4, 4, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true,
return HydrologyPlannerSettings.Banks.of(0, 3D, 4, 4, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true,
HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults());
}
@@ -348,7 +348,9 @@ public class HydrologyTileCacheTest {
"flooded",
List.of("default"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -176,4 +176,68 @@ public class RiverPolicyResolverTest {
assertEquals(RiverConfinement.NONE, RiverPolicyResolver.resolve(null, confined, released).confinement());
assertEquals(RiverConfinement.NONE, RiverPolicyResolver.resolve(confined, released, null).confinement());
}
@Test
public void shoreWidthInheritsFromDimensionThroughRegionToBiome() {
IrisRiverPolicy dimension = new IrisRiverPolicy().setShoreWidth(2D);
IrisRiverPolicy biome = new IrisRiverPolicy().setShoreWidth(0D);
assertNull(RiverPolicyResolver.resolve((IrisRiverPolicy) null, null, null).shoreWidth());
assertNull(RiverPolicyResolver.resolve(new IrisRiverPolicy(), new IrisRiverPolicy(), new IrisRiverPolicy()).shoreWidth());
assertEquals(2D, RiverPolicyResolver.resolve(dimension, null, null).shoreWidth(), 0D);
assertEquals(2D, RiverPolicyResolver.resolve(dimension, new IrisRiverPolicy(), null).shoreWidth(), 0D);
assertEquals(2D, RiverPolicyResolver.resolve(dimension, new IrisRiverPolicy(), new IrisRiverPolicy()).shoreWidth(), 0D);
assertEquals(0D, RiverPolicyResolver.resolve(dimension, new IrisRiverPolicy(), biome).shoreWidth(), 0D);
assertEquals(0D, RiverPolicyResolver.resolve(dimension, biome, null).shoreWidth(), 0D);
}
@Test
public void erosionInheritsUntilAnAreaDecidesIt() {
IrisRiverPolicy eroding = new IrisRiverPolicy().setErosion(true);
IrisRiverPolicy bare = new IrisRiverPolicy().setErosion(false);
assertNull(RiverPolicyResolver.resolve((IrisRiverPolicy) null, null, null).erosion());
assertNull(RiverPolicyResolver.resolve(new IrisRiverPolicy(), new IrisRiverPolicy(), new IrisRiverPolicy()).erosion());
assertEquals(Boolean.TRUE, RiverPolicyResolver.resolve(eroding, null, null).erosion());
assertEquals(Boolean.TRUE, RiverPolicyResolver.resolve(eroding, new IrisRiverPolicy(), new IrisRiverPolicy()).erosion());
assertEquals(Boolean.FALSE, RiverPolicyResolver.resolve(eroding, new IrisRiverPolicy(), bare).erosion());
assertEquals(Boolean.FALSE, RiverPolicyResolver.resolve(null, bare, new IrisRiverPolicy()).erosion());
assertEquals(Boolean.TRUE, RiverPolicyResolver.resolve(null, bare, eroding).erosion());
}
@Test
public void effectivePolicyRejectsANegativeOrInfiniteShoreWidth() {
EffectiveRiverPolicy base = RiverPolicyResolver.resolve((IrisRiverPolicy) null, null, null);
assertEquals(0D, withShoreWidth(base, 0D).shoreWidth(), 0D);
assertNull(withShoreWidth(base, null).shoreWidth());
assertThrows(IllegalArgumentException.class, () -> withShoreWidth(base, -1D));
assertThrows(IllegalArgumentException.class, () -> withShoreWidth(base, Double.POSITIVE_INFINITY));
assertThrows(IllegalArgumentException.class, () -> withShoreWidth(base, Double.NaN));
}
private static EffectiveRiverPolicy withShoreWidth(EffectiveRiverPolicy policy, Double shoreWidth) {
return new EffectiveRiverPolicy(
policy.placement(),
policy.routing(),
policy.outletAdmission(),
policy.profiles(),
policy.surfaceBiomes(),
policy.mouthBiomes(),
policy.shoreBiomes(),
policy.bankBiomes(),
policy.floodedCaveBiomes(),
policy.surfacePools(),
policy.widthMultiplier(),
policy.depthMultiplier(),
policy.incisionMultiplier(),
policy.routingMultiplier(),
policy.bankMultiplier(),
policy.shoreBiomeWidth(),
policy.confinement(),
shoreWidth,
policy.erosion()
);
}
}
@@ -1,17 +1,30 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisCoastalRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverBedProfile;
import art.arcane.iris.engine.object.IrisRiverBlendStyle;
import art.arcane.iris.engine.object.IrisRiverGeometryConfig;
import art.arcane.iris.engine.object.IrisRiverHydrology;
import art.arcane.iris.engine.object.IrisRiverInlandOutlet;
import art.arcane.iris.engine.object.IrisRiverPolicy;
import art.arcane.iris.engine.object.IrisRiverRoutingConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverShapeConfig;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisHydrologyRuntimeSettingsTest {
@Test
@@ -128,7 +141,7 @@ public class IrisHydrologyRuntimeSettingsTest {
assertEquals(3, banks.cascadeRun());
assertEquals(8, banks.waterfallMinimumDrop());
assertEquals(2.2D, banks.mouthFlareRatio(), 0D);
assertEquals(new HydrologyPlannerSettings.Inlet(96, 5, 40), banks.inlet());
assertEquals(HydrologyPlannerSettings.Inlet.of(96, 5, 40), banks.inlet());
assertFalse(banks.exposeCutStrata());
assertEquals(12, settings.surface().maximumIncision());
assertEquals(2.5D, settings.surface().shoreWidth(), 0D);
@@ -218,7 +231,7 @@ public class IrisHydrologyRuntimeSettingsTest {
HydrologyPlannerSettings.SeaCaves mapped = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
.seaCaves();
assertEquals(new HydrologyPlannerSettings.SeaCaves(true, 5, 200, 9, 20), mapped);
assertEquals(HydrologyPlannerSettings.SeaCaves.of(true, 5, 200, 9, 20), mapped);
coastal.setEnabled(false);
HydrologyPlannerSettings.SeaCaves withoutCoastalGrottos = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
@@ -231,4 +244,244 @@ public class IrisHydrologyRuntimeSettingsTest {
coastal.getSeaCaves().setEnabled(false);
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null).seaCaves().enabled());
}
@Test
public void surfaceShapingKnobsMapIntoErosionChannelFlowAndInlet() {
IrisDimension dimension = new IrisDimension();
IrisRiverHydrology rivers = dimension.getHydrology().getRivers();
rivers.getSurface().getBanks().setShoreRise(1.5D).setBlendBaseWidth(3D);
rivers.getSurface().getErosion()
.setEnabled(true)
.setSmoothingRadius(7)
.setThalwegFraction(0.35D)
.setBlendCurve(1.25D)
.setBedNoise(0.4D)
.setStyle(IrisRiverBlendStyle.TERRACED)
.setTerraceSteps(6)
.setCliffFraction(0.3D)
.setBedProfile(IrisRiverBedProfile.V);
rivers.getSurface().getChannel()
.setSmoothingRadius(9)
.setOutlineMinimumRatio(0.5D)
.setOutlineMaximumRatio(1.9D)
.setSpringExtraDepth(2.5D);
rivers.getSurface().getFlow()
.setWaterfallThalwegFraction(0.4D)
.setPlungeBasinMinimumDrop(3)
.setPlungeBasinLengthRatio(1.5D)
.setPlungeBasinDepth(2);
rivers.getSurface().getMouths()
.setInletLength(80)
.setInletDepth(4)
.setMaximumIncision(36)
.setInletCourseFraction(0.3D)
.setInletRampSlope(1.75D);
HydrologyPlannerSettings.Banks banks = settings(dimension).surface().banks();
assertEquals(new HydrologyPlannerSettings.Erosion(true, 7, 0.35D, 1.25D, 0.4D,
IrisRiverBlendStyle.TERRACED, 6, 0.3D, IrisRiverBedProfile.V, 1.5D, 3D), banks.erosion());
assertEquals(new HydrologyPlannerSettings.Channel(9, 0.5D, 1.9D, 2.5D), banks.channel());
assertEquals(new HydrologyPlannerSettings.Flow(0.4D, 3, 1.5D, 2), banks.flow());
assertEquals(new HydrologyPlannerSettings.Inlet(80, 4, 36, 0.3D, 1.75D), banks.inlet());
}
@Test
public void surfaceShapeRoughnessFallsBackToTheChannelRoughnessWhenUnset() {
IrisDimension dimension = new IrisDimension();
IrisRiverHydrology rivers = dimension.getHydrology().getRivers();
rivers.getSurface().getChannel().setRoughness(0.37D).setRoughnessWavelength(13);
IrisSurfaceRiverShapeConfig shape = rivers.getGeometry().getSurface();
shape.setBedRoughness(null).setWallRoughness(null).setRoughnessWavelength(null);
assertEquals(
new HydrologyPlannerSettings.ChannelShape(2D, 0.37D, 0.37D, 13, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D),
settings(dimension).geometry().surface());
shape.setBedRoughness(0.11D);
assertEquals(
new HydrologyPlannerSettings.ChannelShape(2D, 0.11D, 0.37D, 13, 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D),
settings(dimension).geometry().surface());
shape.setBedRoundness(3.5D)
.setWallRoughness(0.22D)
.setRoughnessWavelength(21)
.setRadialBase(0.9D)
.setRadialMinimum(0.5D)
.setRadialMaximum(1.3D)
.setPrimaryLobeStrength(0.12D)
.setDetailLobeStrength(0.04D)
.setCeilingRoughness(0.3D)
.setAspectMinimum(0.7D)
.setAspectRange(0.25D);
assertEquals(
new HydrologyPlannerSettings.ChannelShape(3.5D, 0.11D, 0.22D, 21, 0.9D, 0.5D, 1.3D, 0.12D, 0.04D, 0.3D, 0.7D, 0.25D),
settings(dimension).geometry().surface());
}
@Test
public void undergroundAndGrottoShapeKnobsMapIntoTheirChannelShapes() {
IrisDimension dimension = new IrisDimension();
IrisRiverGeometryConfig geometry = dimension.getHydrology().getRivers().getGeometry();
geometry.getUnderground()
.setBedRoundness(3.1D)
.setBedRoughness(0.31D)
.setWallRoughness(0.29D)
.setRoughnessWavelength(17)
.setRadialBase(0.7D)
.setRadialMinimum(0.4D)
.setRadialMaximum(1.5D)
.setPrimaryLobeStrength(0.2D)
.setDetailLobeStrength(0.1D)
.setCeilingRoughness(0.45D)
.setAspectMinimum(0.8D)
.setAspectRange(0.1D);
geometry.getGrottos()
.setBedRoundness(1.5D)
.setBedRoughness(0.15D)
.setWallRoughness(0.35D)
.setRoughnessWavelength(7)
.setRadialBase(1.1D)
.setRadialMinimum(0.9D)
.setRadialMaximum(1.9D)
.setPrimaryLobeStrength(0.3D)
.setDetailLobeStrength(0.25D)
.setCeilingRoughness(0.6D)
.setAspectMinimum(0.5D)
.setAspectRange(0.4D);
HydrologyPlannerSettings.Geometry mapped = settings(dimension).geometry();
assertEquals(new HydrologyPlannerSettings.ChannelShape(3.1D, 0.31D, 0.29D, 17, 0.7D, 0.4D, 1.5D, 0.2D, 0.1D, 0.45D, 0.8D, 0.1D),
mapped.underground());
assertEquals(new HydrologyPlannerSettings.ChannelShape(1.5D, 0.15D, 0.35D, 7, 1.1D, 0.9D, 1.9D, 0.3D, 0.25D, 0.6D, 0.5D, 0.4D),
mapped.grottos());
}
@Test
public void undergroundRockCoverFloorCoverAndWideningSourcesMap() {
IrisDimension dimension = new IrisDimension();
dimension.getHydrology().getRivers().getUnderground()
.setMinimumRockCover(3)
.setMinimumFloorCover(2)
.setWideningSources(20);
HydrologyPlannerSettings.Underground mapped = settings(dimension).underground();
assertEquals(3, mapped.minimumRockCover());
assertEquals(2, mapped.minimumFloorCover());
assertEquals(20, mapped.wideningSources());
}
@Test
public void coastalCliffKnobsMapWithTheVerticalRadiusFallback() {
IrisDimension dimension = new IrisDimension();
IrisCoastalRiverGrottoConfig coastal = dimension.getHydrology().getRivers().getGrottos().getCoastal();
coastal.setVerticalRadius(7).setCliffMinimumHeight(null).setCliffSlopeFactor(1.25D);
HydrologyPlannerSettings.Outlets fallback = settings(dimension).outlets();
assertEquals(7, fallback.coastalCliffMinimumHeight());
assertEquals(1.25D, fallback.coastalCliffSlopeFactor(), 0D);
coastal.setVerticalRadius(2);
assertEquals(4, settings(dimension).outlets().coastalCliffMinimumHeight());
coastal.setCliffMinimumHeight(2);
assertEquals(2, settings(dimension).outlets().coastalCliffMinimumHeight());
}
@Test
public void seaCaveSweepJitterMapsIntoPlannerSettings() {
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)
.setSweepJitterDegrees(40D);
assertEquals(new HydrologyPlannerSettings.SeaCaves(true, 5, 200, 9, 20, 40D), settings(dimension).seaCaves());
coastal.setEnabled(false);
assertEquals(new HydrologyPlannerSettings.SeaCaves(false, 5, 200, 9, 20, 40D), settings(dimension).seaCaves());
}
@Test
public void undergroundCascadeRunPerBlockMapsIntoDrops() {
IrisDimension dimension = new IrisDimension();
dimension.getHydrology().getRivers().getGeometry().getDrops()
.setCascadeRunPerBlock(4)
.setCascadeExponent(2.2D)
.setMaximumCascadeStep(1)
.setFlowWidthRatio(0.81D)
.setMaximumFlowDepth(3)
.setBasinWidthRatio(2.1D)
.setMaximumBasinDepth(9)
.setUndergroundCascadeRunPerBlock(3);
assertEquals(new HydrologyPlannerSettings.Drops(4, 2.2D, 1, 0.81D, 3, 2.1D, 9, 3), settings(dimension).geometry().drops());
}
@Test
@SuppressWarnings("unchecked")
public void widestShoreBiomeWidthCoversPolicyShoreWidthAcrossDimensionRegionsAndBiomes() {
IrisDimension dimension = new IrisDimension().setRegions(new KList<>("valley"));
dimension.getRiverPolicy().setShoreWidth(4D);
assertEquals(4D, IrisHydrologyRuntime.widestShoreBiomeWidth(dimension, () -> null, 1.5D), 0D);
assertEquals(4D, settings(dimension).widestShoreBiomeWidth(), 0D);
IrisRegion valley = new IrisRegion().setRiverPolicy(new IrisRiverPolicy().setShoreBiomeWidth(6D));
IrisBiome beach = new IrisBiome().setRiverPolicy(new IrisRiverPolicy().setShoreWidth(9D));
IrisBiome plain = new IrisBiome();
IrisData data = mock(IrisData.class);
ResourceLoader<IrisRegion> regionLoader = mock(ResourceLoader.class);
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
when(data.getRegionLoader()).thenReturn(regionLoader);
when(data.getBiomeLoader()).thenReturn(biomeLoader);
when(regionLoader.load("valley")).thenReturn(valley);
when(biomeLoader.getPossibleKeys()).thenReturn(new String[]{"beach", "plain"});
when(biomeLoader.loadAll(any(String[].class))).thenReturn(new KList<>(beach, plain));
assertEquals(9D, IrisHydrologyRuntime.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
beach.getRiverPolicy().setShoreWidth(null).setShoreBiomeWidth(11D);
assertEquals(11D, IrisHydrologyRuntime.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
valley.getRiverPolicy().setShoreWidth(12D);
assertEquals(12D, IrisHydrologyRuntime.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
dimension.getRiverPolicy().setShoreWidth(null);
valley.getRiverPolicy().setShoreWidth(null).setShoreBiomeWidth(null);
beach.getRiverPolicy().setShoreBiomeWidth(null);
assertEquals(1.5D, IrisHydrologyRuntime.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
}
@Test
public void defaultConfigurationMapsToTheDefaultShapingRecords() {
IrisDimension dimension = new IrisDimension();
HydrologyPlannerSettings settings = settings(dimension);
assertEquals(HydrologyPlannerSettings.Banks.defaults(), settings.surface().banks());
assertEquals(HydrologyPlannerSettings.ChannelShape.of(2D, 0.25D, 0.25D, 16), settings.geometry().surface());
assertEquals(HydrologyPlannerSettings.ChannelShape.of(2.4D, 0.28D, 0.24D, 11), settings.geometry().underground());
assertEquals(HydrologyPlannerSettings.ChannelShape.of(2.4D, 0.28D, 0.24D, 11), settings.geometry().grottos());
assertEquals(HydrologyPlannerSettings.Drops.of(2, 1.4D, 2, 0.45D, 2, 1.8D, 8), settings.geometry().drops());
assertEquals(1, settings.underground().minimumRockCover());
assertEquals(1, settings.underground().minimumFloorCover());
assertEquals(8, settings.underground().wideningSources());
assertEquals(7, settings.outlets().coastalCliffMinimumHeight());
assertEquals(0.5D, settings.outlets().coastalCliffSlopeFactor(), 0D);
assertEquals(25D, settings.seaCaves().sweepJitterDegrees(), 0D);
assertEquals(1.5D, settings.widestShoreBiomeWidth(), 0D);
}
private static HydrologyPlannerSettings settings(IrisDimension dimension) {
return IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null);
}
}
@@ -108,6 +108,60 @@ public class ChannelProfileBuilderTest {
assertEquals(3D, sloped.halfWidth()[24], 1.0E-9D);
}
@Test
public void smoothingRadiusZeroFollowsTheSampledWidthExactly() {
HydrologyGeometrySampler stepped = request -> switch (request.field()) {
case SURFACE_WIDTH -> request.x() < 200 ? 4 : 8;
case SURFACE_DEPTH -> 3;
default -> request.minimum();
};
SurfaceCenterline centerline = straight(400);
ChannelProfile sharp = builder(new HydrologyPlannerSettings.Channel(0, 0.6D, 1.4D, 1D), stepped)
.build(centerline, "water", false);
ChannelProfile smoothed = builder(HydrologyPlannerSettings.Channel.defaults(), stepped)
.build(centerline, "water", false);
assertEquals(2D, sharp.halfWidth()[199], 1.0E-9D);
assertEquals(4D, sharp.halfWidth()[200], 1.0E-9D);
assertEquals(2D, sharp.halfWidth()[100], 1.0E-9D);
assertEquals(4D, sharp.halfWidth()[300], 1.0E-9D);
assertTrue(smoothed.halfWidth()[199] > 2D);
assertTrue(smoothed.halfWidth()[200] < 4D);
assertEquals(2D, smoothed.halfWidth()[100], 1.0E-9D);
}
@Test
public void springExtraDepthDeepensTheSpringPool() {
SurfaceCenterline centerline = straight(400);
ChannelProfile flat = builder(new HydrologyPlannerSettings.Channel(16, 0.6D, 1.4D, 0D), CONSTANT_GEOMETRY)
.build(centerline, "water", false);
ChannelProfile deep = builder(new HydrologyPlannerSettings.Channel(16, 0.6D, 1.4D, 3D), CONSTANT_GEOMETRY)
.build(centerline, "water", false);
assertEquals(3D, flat.depth()[0], 1.0E-9D);
assertEquals(3D, flat.depth()[200], 1.0E-9D);
assertEquals(6D, deep.depth()[0], 1.0E-9D);
assertEquals(3D, deep.depth()[24], 1.0E-9D);
assertEquals(3D, deep.depth()[200], 1.0E-9D);
}
private static ChannelProfileBuilder builder(
HydrologyPlannerSettings.Channel channel,
HydrologyGeometrySampler geometry
) {
HydrologyPlannerSettings.Banks banks = HydrologyPlannerSettings.defaults().surface().banks();
HydrologyPlannerSettings.Banks tuned = new HydrologyPlannerSettings.Banks(
banks.sink(), banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(),
banks.roughness(), banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(),
banks.mouthFlareRatio(), banks.inlet(), banks.springWidthRatio(), banks.springLength(),
banks.exposeCutStrata(), banks.erosion(), banks.ponds(), channel, banks.flow());
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(), tuned);
return new ChannelProfileBuilder(surface, (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land"), geometry);
}
private static ChannelProfileBuilder builder(double widthMultiplier, double depthMultiplier) {
return builder(widthMultiplier, depthMultiplier, (int x, int z) -> 80);
}
@@ -150,7 +204,9 @@ public class ChannelProfileBuilderTest {
"parent",
List.of("water"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY);
}
@@ -6,9 +6,13 @@ import art.arcane.iris.engine.hydrology.HydrologyPoint;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import art.arcane.iris.engine.object.IrisRiverBedProfile;
import art.arcane.iris.engine.object.IrisRiverBlendStyle;
import org.junit.Test;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.IntBinaryOperator;
import static org.junit.Assert.assertEquals;
@@ -63,7 +67,7 @@ public class ErosionFieldCompilerTest {
@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);
HydrologyPlannerSettings.Erosion off = HydrologyPlannerSettings.Erosion.of(false, 12, 0.45D, 1D, 0.5D);
Compiled compiled = compile(zeroRoughnessSurface(0, off), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
assertEquals(0, compiled.field().uncontainedWetCells());
@@ -80,8 +84,8 @@ public class ErosionFieldCompilerTest {
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);
Compiled explicit = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.of(true, 12, 0.45D, 1D, 0.5D)), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled steep = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.of(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()) {
@@ -189,7 +193,7 @@ public class ErosionFieldCompilerTest {
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.Inlet inlet = HydrologyPlannerSettings.Inlet.of(32, 3, 32);
HydrologyPlannerSettings.Surface surface = withBanks(zeroRoughnessSurface(), zeroRoughnessSurface().banks().withInlet(inlet));
Compiled compiled = compile(surface, coast, 244, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
@@ -332,10 +336,111 @@ public class ErosionFieldCompilerTest {
true,
defaults.sources(),
4, 8, 2, 4, 10, 1.5D,
new HydrologyPlannerSettings.Banks(sink, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, erosion, ponds)
HydrologyPlannerSettings.Banks.of(sink, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, erosion, ponds)
);
}
/** The test bank settings with an explicit roughness, shore width, channel and flow. */
private static HydrologyPlannerSettings.Surface surfaceWith(
double roughness,
int sink,
double shoreWidth,
HydrologyPlannerSettings.Erosion erosion,
HydrologyPlannerSettings.Ponds ponds,
HydrologyPlannerSettings.Channel channel,
HydrologyPlannerSettings.Flow flow
) {
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
return new HydrologyPlannerSettings.Surface(
true,
defaults.sources(),
4, 8, 2, 4, 10, shoreWidth,
new HydrologyPlannerSettings.Banks(sink, 3D, 4, 32, roughness, 16, 2, 6, 1.6D,
HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, erosion, ponds, channel, flow)
);
}
private static HydrologyPlannerSettings.Ponds noPonds() {
return new HydrologyPlannerSettings.Ponds(
new HydrologyPlannerSettings.Pond(false, 5, 9, 3),
new HydrologyPlannerSettings.Pond(false, 4, 7, 3));
}
private static HydrologyPlannerSettings.Erosion shapedErosion(
IrisRiverBlendStyle style,
int terraceSteps,
double cliffFraction,
IrisRiverBedProfile bedProfile,
double shoreRise,
double blendBaseWidth
) {
return new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, style, terraceSteps, cliffFraction,
bedProfile, shoreRise, blendBaseWidth);
}
private static void assertSameField(Compiled expected, Compiled actual) {
assertEquals(expected.field().columns().size(), actual.field().columns().size());
for (SurfaceColumn column : expected.field().columns().values()) {
SurfaceColumn same = actual.field().column(column.x(), column.z());
assertNotNull(same);
assertEquals(column.height(), same.height());
assertEquals(column.headY(), same.headY());
assertEquals(column.role(), same.role());
}
}
private static boolean differsOn(Compiled base, Compiled other, SurfaceRole role) {
for (SurfaceColumn column : base.field().columns().values()) {
if (column.role() != role) {
continue;
}
SurfaceColumn same = other.field().column(column.x(), column.z());
if (same == null || same.height() != column.height()) {
return true;
}
}
return false;
}
private static void assertHolds(Compiled compiled) {
assertEquals(0, compiled.field().uncontainedWetCells());
assertChannelContained(compiled);
assertNoWriteAboveNatural(compiled);
}
private static Set<Integer> bankHeights(Compiled compiled, int x, int fromZ, int toZ) {
Set<Integer> heights = new HashSet<>();
for (int z = fromZ; z <= toZ; z++) {
SurfaceColumn column = compiled.field().column(x, z);
if (column != null && column.role() == SurfaceRole.BANK) {
heights.add(column.height());
}
}
return heights;
}
private static int outermostBank(Compiled compiled, int x) {
int outermost = -1;
for (int z = 1; z < 80; z++) {
SurfaceColumn column = compiled.field().column(x, z);
if (column != null && column.role() == SurfaceRole.BANK) {
outermost = z;
}
}
return outermost;
}
private static double widestWetOffset(Compiled compiled) {
double widest = 0D;
for (SurfaceColumn column : compiled.field().columns().values()) {
if (column.role() != SurfaceRole.CHANNEL || column.apron() || column.x() < 40 || column.x() > 280) {
continue;
}
widest = Math.max(widest, Math.abs(column.z()));
}
return widest;
}
private record Compiled(ErosionField field, ValleyProfile valley) {
}
@@ -506,7 +611,7 @@ public class ErosionFieldCompilerTest {
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
open.floodedCaveBiomeKey(), open.preferredProfileKeys(), open.surfacePoolKeys(), shoreBiomeWidth, null, Double.NaN, true
);
}
@Test
@@ -610,4 +715,206 @@ public class ErosionFieldCompilerTest {
}
assertEquals(0, first.field().uncontainedWetCells());
}
@Test
public void blendStyleDefaultsReproduceTheValleyAndEachStyleReshapesIt() {
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 smooth = compile(
zeroRoughnessSurface(0, shapedErosion(IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D)),
hillside, 300, SurfaceTerminal.SINKHOLE, 40);
assertSameField(defaults, smooth);
for (IrisRiverBlendStyle style : new IrisRiverBlendStyle[]{
IrisRiverBlendStyle.LINEAR, IrisRiverBlendStyle.CONCAVE, IrisRiverBlendStyle.TERRACED, IrisRiverBlendStyle.CLIFF}) {
Compiled shaped = compile(
zeroRoughnessSurface(0, shapedErosion(style, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D)),
hillside, 300, SurfaceTerminal.SINKHOLE, 40);
assertTrue(style.name(), differsOn(defaults, shaped, SurfaceRole.BANK));
assertHolds(shaped);
}
// A shelf whose ground is level from z = 8 outward, so one cross-section shows the step pattern
// of a style directly: every bank column out there is cut from the same 16-block rise.
HydrologyTerrainSampler shelf = (int x, int z) ->
HydrologyTerrainSample.openLand(80 + Math.min(16, Math.max(0, Math.abs(z) - 4) * 4), 0D, "land");
Compiled terraced = compile(
zeroRoughnessSurface(0, shapedErosion(IrisRiverBlendStyle.TERRACED, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D)),
shelf, 300, SurfaceTerminal.SINKHOLE, 40);
Set<Integer> steps = bankHeights(terraced, 150, 9, 40);
assertTrue(steps.toString(), steps.size() >= 2 && steps.size() <= 4);
assertHolds(terraced);
Compiled cliff = compile(
zeroRoughnessSurface(0, shapedErosion(IrisRiverBlendStyle.CLIFF, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D)),
shelf, 300, SurfaceTerminal.SINKHOLE, 40);
Set<Integer> bench = bankHeights(cliff, 150, 9, 40);
assertEquals(bench.toString(), 2, bench.size());
assertTrue(bench.contains(80));
assertTrue(bench.contains(96));
assertHolds(cliff);
}
@Test
public void bedProfileDefaultsReproduceTheBedAndFlatVAndUDiffer() {
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
Compiled defaults = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults()), flat, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled bowl = compile(
zeroRoughnessSurface(0, shapedErosion(IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D)),
flat, 300, SurfaceTerminal.SINKHOLE, 40);
assertSameField(defaults, bowl);
for (IrisRiverBedProfile profile : new IrisRiverBedProfile[]{
IrisRiverBedProfile.FLAT, IrisRiverBedProfile.V, IrisRiverBedProfile.U}) {
Compiled shaped = compile(
zeroRoughnessSurface(0, shapedErosion(IrisRiverBlendStyle.SMOOTH, 4, 0.5D, profile, 0D, 0D)),
flat, 300, SurfaceTerminal.SINKHOLE, 40);
assertTrue(profile.name(), differsOn(defaults, shaped, SurfaceRole.CHANNEL));
assertHolds(shaped);
}
}
@Test
public void shoreRiseLiftsTheBenchTowardTheLandAndZeroKeepsItFlat() {
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z) / 2, 0D, "land");
Compiled level = compile(
surfaceWith(0D, 0, 4D, HydrologyPlannerSettings.Erosion.defaults(), noPonds(),
HydrologyPlannerSettings.Channel.defaults(), HydrologyPlannerSettings.Flow.defaults()),
hillside, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled explicit = compile(
surfaceWith(0D, 0, 4D, shapedErosion(IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D), noPonds(),
HydrologyPlannerSettings.Channel.defaults(), HydrologyPlannerSettings.Flow.defaults()),
hillside, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled rising = compile(
surfaceWith(0D, 0, 4D, shapedErosion(IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 2D, 0D), noPonds(),
HydrologyPlannerSettings.Channel.defaults(), HydrologyPlannerSettings.Flow.defaults()),
hillside, 300, SurfaceTerminal.SINKHOLE, 40);
assertSameField(level, explicit);
int water = level.field().column(150, 0).headY();
assertEquals(water, level.field().column(150, 7).height());
assertEquals(water + 2, rising.field().column(150, 7).height());
int previous = water;
for (int z = 4; z <= 7; z++) {
SurfaceColumn column = rising.field().column(150, z);
assertNotNull("bench column at z " + z, column);
assertTrue(column.height() >= previous);
assertTrue(column.height() <= water + 2);
assertTrue(column.height() <= hillside.sample(150, z).naturalHeight());
previous = column.height();
}
assertHolds(rising);
}
@Test
public void blendBaseWidthWidensEveryValley() {
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, shapedErosion(IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D)),
hillside, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled wide = compile(
zeroRoughnessSurface(0, shapedErosion(IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 12D)),
hillside, 300, SurfaceTerminal.SINKHOLE, 40);
assertSameField(defaults, explicit);
assertTrue(differsOn(defaults, wide, SurfaceRole.BANK));
assertTrue(outermostBank(wide, 150) > outermostBank(defaults, 150));
assertHolds(wide);
}
@Test
public void outlineRatiosBoundTheWaterlineWobble() {
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
Compiled wobbly = compile(
surfaceWith(1D, 0, 1.5D, HydrologyPlannerSettings.Erosion.defaults(), noPonds(),
HydrologyPlannerSettings.Channel.defaults(), HydrologyPlannerSettings.Flow.defaults()),
flat, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled explicit = compile(
surfaceWith(1D, 0, 1.5D, HydrologyPlannerSettings.Erosion.defaults(), noPonds(),
new HydrologyPlannerSettings.Channel(16, 0.6D, 1.4D, 1D), HydrologyPlannerSettings.Flow.defaults()),
flat, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled bounded = compile(
surfaceWith(1D, 0, 1.5D, HydrologyPlannerSettings.Erosion.defaults(), noPonds(),
new HydrologyPlannerSettings.Channel(16, 1D, 1D, 1D), HydrologyPlannerSettings.Flow.defaults()),
flat, 300, SurfaceTerminal.SINKHOLE, 40);
assertSameField(wobbly, explicit);
assertTrue("wobble " + widestWetOffset(wobbly), widestWetOffset(wobbly) > 3.25D);
assertTrue("bounded " + widestWetOffset(bounded), widestWetOffset(bounded) <= 3.25D);
assertHolds(bounded);
}
@Test
public void plungeBasinKnobsControlTheScourAfterADrop() {
// Level ground broken by a two-block step every eight stations: every step is a drop the
// default plunge basin scours after.
HydrologyTerrainSampler stair = (int x, int z) ->
HydrologyTerrainSample.openLand(240 - 2 * (Math.max(0, x) / 8), 0D, "land");
Compiled defaults = compile(
surfaceWith(0D, 0, 1.5D, HydrologyPlannerSettings.Erosion.defaults(), noPonds(),
HydrologyPlannerSettings.Channel.defaults(), HydrologyPlannerSettings.Flow.defaults()),
stair, 70, SurfaceTerminal.SINKHOLE, 40);
Compiled explicit = compile(
surfaceWith(0D, 0, 1.5D, HydrologyPlannerSettings.Erosion.defaults(), noPonds(),
HydrologyPlannerSettings.Channel.defaults(), new HydrologyPlannerSettings.Flow(0.65D, 2, 2D, 1)),
stair, 70, SurfaceTerminal.SINKHOLE, 40);
Compiled none = compile(
surfaceWith(0D, 0, 1.5D, HydrologyPlannerSettings.Erosion.defaults(), noPonds(),
HydrologyPlannerSettings.Channel.defaults(), new HydrologyPlannerSettings.Flow(0.65D, 99, 2D, 1)),
stair, 70, SurfaceTerminal.SINKHOLE, 40);
Compiled deep = compile(
surfaceWith(0D, 0, 1.5D, HydrologyPlannerSettings.Erosion.defaults(), noPonds(),
HydrologyPlannerSettings.Channel.defaults(), new HydrologyPlannerSettings.Flow(0.65D, 2, 2D, 3)),
stair, 70, SurfaceTerminal.SINKHOLE, 40);
assertSameField(defaults, explicit);
assertTrue(none.field().column(35, 0).height() > defaults.field().column(35, 0).height());
assertTrue(deep.field().column(35, 0).height() < defaults.field().column(35, 0).height());
assertHolds(defaults);
assertHolds(none);
assertHolds(deep);
}
@Test
public void policyShoreWidthReplacesTheBenchPerColumn() {
HydrologyTerrainSampler split = (int x, int z) -> {
HydrologyTerrainSample open = HydrologyTerrainSample.openLand(80, 0D, "land");
return z > 0 ? open.withShoreWidth(4D) : open;
};
Compiled compiled = compile(zeroRoughnessSurface(), split, 300, SurfaceTerminal.SINKHOLE, 40);
for (int z = 4; z <= 7; z++) {
SurfaceColumn bench = compiled.field().column(150, z);
assertNotNull("wide bench column at z " + z, bench);
assertTrue(bench.role() != SurfaceRole.CHANNEL);
assertEquals(80, bench.height());
}
assertNull(compiled.field().column(150, 8));
assertNotNull(compiled.field().column(150, -4));
assertNull(compiled.field().column(150, -5));
assertHolds(compiled);
}
@Test
public void policyErosionFalseKeepsOnlyTheChannelAndBench() {
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z) / 2, 0D, "land");
HydrologyTerrainSampler quiet = (int x, int z) -> hillside.sample(x, z).withErosion(false);
Compiled eroded = compile(zeroRoughnessSurface(), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled bare = compile(zeroRoughnessSurface(), quiet, 300, SurfaceTerminal.SINKHOLE, 40);
boolean anyBank = false;
for (SurfaceColumn column : eroded.field().columns().values()) {
if (column.role() == SurfaceRole.BANK) {
anyBank = true;
break;
}
}
assertTrue(anyBank);
for (SurfaceColumn column : bare.field().columns().values()) {
assertTrue(column.x() + "," + column.z(), column.role() != SurfaceRole.BANK);
}
assertEquals(SurfaceRole.SHORE, bare.field().column(150, 4).role());
assertHolds(bare);
}
}
@@ -158,7 +158,7 @@ public class ValleyProfileSolverTest {
@Test
public void anInletHoldsTheLastStationsAtSeaLevelAndMayCutDeeperThanTheChannelCap() {
HydrologyTerrainSampler coast = risingCoast();
HydrologyPlannerSettings.Inlet inlet = new HydrologyPlannerSettings.Inlet(32, 3, 32);
HydrologyPlannerSettings.Inlet inlet = HydrologyPlannerSettings.Inlet.of(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())
@@ -185,9 +185,9 @@ public class ValleyProfileSolverTest {
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(88, 0D, "land");
ValleyProfile valley = solver(plateau, new HydrologyPlannerSettings.Inlet(32, 3, 32))
ValleyProfile valley = solver(plateau, HydrologyPlannerSettings.Inlet.of(32, 3, 32))
.solve(straight(300), channel(300, plateau), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
ValleyProfile shallow = solver(plateau, new HydrologyPlannerSettings.Inlet(32, 3, 24))
ValleyProfile shallow = solver(plateau, HydrologyPlannerSettings.Inlet.of(32, 3, 24))
.solve(straight(300), channel(300, plateau), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection());
@@ -214,7 +214,7 @@ public class ValleyProfileSolverTest {
? 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))
ValleyProfile valley = solver(bluff, HydrologyPlannerSettings.Inlet.of(64, 3, 32))
.solve(straight(300), channel(300, bluff), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection());
@@ -233,7 +233,7 @@ public class ValleyProfileSolverTest {
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(x >= 30 && x < 50 ? 94 : 80, 0D, "land");
ValleyProfile valley = solver(ridge, new HydrologyPlannerSettings.Inlet(32, 3, 32))
ValleyProfile valley = solver(ridge, HydrologyPlannerSettings.Inlet.of(32, 3, 32))
.solve(straight(300), channel(300, ridge), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, valley.rejection());
@@ -246,7 +246,7 @@ public class ValleyProfileSolverTest {
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(70, 0D, "land");
ValleyProfile valley = solver(coast, new HydrologyPlannerSettings.Inlet(64, 3, 32))
ValleyProfile valley = solver(coast, HydrologyPlannerSettings.Inlet.of(64, 3, 32))
.solve(straight(100), channel(100, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection());
@@ -262,6 +262,62 @@ public class ValleyProfileSolverTest {
assertContained(valley);
}
@Test
public void inletCourseFractionBoundsTheDrownedReach() {
HydrologyTerrainSampler coast = longCoast();
ValleyProfile tenth = solver(coast, new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.1D, 1D))
.solve(straight(340), channel(340, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
ValleyProfile half = solver(coast, new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.5D, 1D))
.solve(straight(340), channel(340, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(tenth.rejection());
assertNull(half.rejection());
assertEquals(300, tenth.exposedStations());
assertEquals(30, drownedStations(tenth));
assertEquals(64, drownedStations(half));
assertEquals(SEA_LEVEL, tenth.head()[270]);
assertTrue(tenth.head()[269] > SEA_LEVEL);
assertContained(tenth);
}
@Test
public void inletRampSlopeGradesTheApproachFaster() {
HydrologyTerrainSampler coast = longCoast();
ValleyProfile gentle = solver(coast, new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.5D, 1D))
.solve(straight(340), channel(340, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
ValleyProfile steep = solver(coast, new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.5D, 2D))
.solve(straight(340), channel(340, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(gentle.rejection());
assertNull(steep.rejection());
assertEquals(SEA_LEVEL, gentle.head()[236]);
assertEquals(SEA_LEVEL, steep.head()[236]);
assertEquals(SEA_LEVEL + 1, gentle.head()[235]);
assertEquals(SEA_LEVEL + 2, steep.head()[235]);
assertEquals(SEA_LEVEL + 5, gentle.head()[231]);
assertEquals(70, steep.head()[231]);
assertEquals(70, gentle.head()[226]);
assertContained(gentle);
assertContained(steep);
}
/** Flat land at 70 out to the last land station, x=299, with open sea from x=300. */
private static HydrologyTerrainSampler longCoast() {
return (int x, int z) -> x >= 300
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(70, 0D, "land");
}
private static int drownedStations(ValleyProfile valley) {
int drowned = 0;
for (int station = 0; station < valley.exposedStations(); station++) {
if (valley.head()[station] == SEA_LEVEL) {
drowned++;
}
}
return drowned;
}
/** 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) -> {
@@ -279,7 +335,7 @@ public class ValleyProfileSolverTest {
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(),
HydrologyPlannerSettings.Banks.of(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()));
@@ -310,7 +366,7 @@ public class ValleyProfileSolverTest {
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(),
HydrologyPlannerSettings.Banks.of(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()));
@@ -1542,7 +1542,7 @@ public class MantleHydrologyComponentTest {
0D,
HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
false,
disabledSources,
64,
@@ -1556,7 +1556,7 @@ public class MantleHydrologyComponentTest {
false,
0
),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
@@ -1609,7 +1609,9 @@ public class MantleHydrologyComponentTest {
"flooded",
List.of("default"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
}
@@ -0,0 +1,74 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisRiverMaterialConfig;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisCarveModifierHydrologyMaterialTest {
private final PlatformBlockState biomeLayer = mock(PlatformBlockState.class);
private final PlatformBlockState painted = mock(PlatformBlockState.class);
private final IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
private final IrisData data = mock(IrisData.class);
private final RNG rng = new RNG(13L);
public IrisCarveModifierHydrologyMaterialTest() {
when(palette.get(same(rng), eq(48.0), eq(31.0), eq(-9.0), same(data))).thenReturn(painted);
}
@Test
public void undergroundBedMaterialPaintsTheFloorUnderAHydrologyCell() {
IrisRiverMaterialConfig material = enabled(2);
HydrologyCaveCell wet = HydrologyCaveCell.of(HydrologyCaveAction.WET_SOURCE);
assertSame(painted, paint(material, wet, 0));
assertSame(painted, paint(material, wet, 1));
assertSame(biomeLayer, paint(material, wet, 2));
assertSame(painted, paint(material, HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR), 0));
}
@Test
public void undergroundBedMaterialSkipsSealGuardsAndNonHydrologyFloors() {
IrisRiverMaterialConfig material = enabled(3);
assertSame(biomeLayer, paint(material, HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD), 0));
assertSame(biomeLayer, paint(material, null, 0));
verify(palette, never()).get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any());
}
@Test
public void disabledUndergroundBedMaterialKeepsTheBiomeLayers() {
HydrologyCaveCell wet = HydrologyCaveCell.of(HydrologyCaveAction.WET_SOURCE);
IrisRiverMaterialConfig disabled = new IrisRiverMaterialConfig().setPalette(palette).setDepth(3);
for (int index = 0; index < 4; index++) {
assertSame(biomeLayer, paint(disabled, wet, index));
assertSame(biomeLayer, paint(null, wet, index));
}
verify(palette, never()).get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any());
}
private IrisRiverMaterialConfig enabled(int depth) {
return new IrisRiverMaterialConfig().setEnabled(true).setPalette(palette).setDepth(depth);
}
private PlatformBlockState paint(IrisRiverMaterialConfig material, HydrologyCaveCell floorHydrology, int index) {
return IrisCarveModifier.paintUndergroundBedMaterial(
biomeLayer, material, floorHydrology, index, rng, 48, 31, -9, data);
}
}
@@ -13,7 +13,9 @@ public class HydrologyEnumDescriptionsTest {
private static final List<Class<? extends Enum<?>>> HYDROLOGY_ENUMS = List.of(
IrisRiverPlacementMode.class,
IrisRiverRoutingMode.class,
IrisGrottoPoolLevel.class
IrisGrottoPoolLevel.class,
IrisRiverBlendStyle.class,
IrisRiverBedProfile.class
);
@Test
@@ -54,16 +54,47 @@ public class IrisRiverConfigurationTest {
assertEquals(10, surface.getChannel().getMaximumIncision());
assertEquals(0.25D, surface.getChannel().getRoughness(), 0D);
assertEquals(16, surface.getChannel().getRoughnessWavelength());
assertEquals(16, surface.getChannel().getSmoothingRadius());
assertEquals(0.6D, surface.getChannel().getOutlineMinimumRatio(), 0D);
assertEquals(1.4D, surface.getChannel().getOutlineMaximumRatio(), 0D);
assertEquals(1D, surface.getChannel().getSpringExtraDepth(), 0D);
assertEquals(1.5D, surface.getBanks().getShoreWidth(), 0D);
assertEquals(3D, surface.getBanks().getBlendSlope(), 0D);
assertEquals(4, surface.getBanks().getMinimumBlendWidth());
assertEquals(32, surface.getBanks().getMaximumBlendWidth());
assertTrue(surface.getBanks().isExposeCutStrata());
assertEquals(0D, surface.getBanks().getShoreRise(), 0D);
assertEquals(0D, surface.getBanks().getBlendBaseWidth(), 0D);
assertDisabledMaterial(surface.getBanks().getShoreMaterial());
assertDisabledMaterial(surface.getBanks().getBankMaterial());
assertDisabledMaterial(surface.getBed().getMaterial());
assertEquals(2, surface.getFlow().getCascadeRun());
assertEquals(6, surface.getFlow().getWaterfallMinimumDrop());
assertEquals(0.65D, surface.getFlow().getWaterfallThalwegFraction(), 0D);
assertEquals(2, surface.getFlow().getPlungeBasinMinimumDrop());
assertEquals(2D, surface.getFlow().getPlungeBasinLengthRatio(), 0D);
assertEquals(1, surface.getFlow().getPlungeBasinDepth());
assertEquals(1.6D, surface.getMouths().getFlareRatio(), 0D);
assertEquals(8, surface.getMouths().getMaximumOceanApron());
IrisRiverDropShapeConfig drops = dimension.getHydrology().getRivers().getGeometry().getDrops();
assertEquals(0.5D, surface.getMouths().getInletCourseFraction(), 0D);
assertEquals(1D, surface.getMouths().getInletRampSlope(), 0D);
assertEquals(IrisRiverBlendStyle.SMOOTH, surface.getErosion().getStyle());
assertEquals(4, surface.getErosion().getTerraceSteps());
assertEquals(0.5D, surface.getErosion().getCliffFraction(), 0D);
assertEquals(IrisRiverBedProfile.BOWL, surface.getErosion().getBedProfile());
IrisRiverGeometryConfig geometry = dimension.getHydrology().getRivers().getGeometry();
IrisSurfaceRiverShapeConfig surfaceShape = geometry.getSurface();
assertEquals(2D, surfaceShape.getBedRoundness(), 0D);
assertNull(surfaceShape.getBedRoughness());
assertNull(surfaceShape.getWallRoughness());
assertNull(surfaceShape.getRoughnessWavelength());
assertSharedShapeDefaults(surfaceShape.getRadialBase(), surfaceShape.getRadialMinimum(),
surfaceShape.getRadialMaximum(), surfaceShape.getPrimaryLobeStrength(),
surfaceShape.getDetailLobeStrength(), surfaceShape.getCeilingRoughness(),
surfaceShape.getAspectMinimum(), surfaceShape.getAspectRange());
assertSharedShapeDefaults(geometry.getUnderground());
assertSharedShapeDefaults(geometry.getGrottos());
IrisRiverDropShapeConfig drops = geometry.getDrops();
assertEquals(2, drops.getCascadeRunPerBlock());
assertEquals(1.4D, drops.getCascadeExponent(), 0D);
assertEquals(2, drops.getMaximumCascadeStep());
@@ -71,6 +102,7 @@ public class IrisRiverConfigurationTest {
assertEquals(2, drops.getMaximumFlowDepth());
assertEquals(1.8D, drops.getBasinWidthRatio(), 0D);
assertEquals(8, drops.getMaximumBasinDepth());
assertEquals(0, drops.getUndergroundCascadeRunPerBlock());
IrisUndergroundRiverConfig underground = dimension.getHydrology().getRivers().getUnderground();
assertTrue(underground.isEnabled());
@@ -83,6 +115,10 @@ public class IrisRiverConfigurationTest {
assertEquals(8D, underground.getChannelWidth().getMax(), 0D);
assertTrue(underground.isConnectToExistingCaves());
assertEquals(64, underground.getMouthLevelingDistance());
assertEquals(1, underground.getMinimumRockCover());
assertEquals(1, underground.getMinimumFloorCover());
assertEquals(8, underground.getWideningSources());
assertDisabledMaterial(underground.getBedMaterial());
IrisCoastalRiverGrottoConfig coastal = dimension.getHydrology().getRivers().getGrottos().getCoastal();
IrisInlandRiverGrottoConfig inland = dimension.getHydrology().getRivers().getGrottos().getInland();
@@ -90,6 +126,9 @@ public class IrisRiverConfigurationTest {
assertEquals(7, coastal.getVerticalRadius());
assertEquals(10, coastal.getHeadroom());
assertEquals(8192, coastal.getMaximumVolume());
assertNull(coastal.getCliffMinimumHeight());
assertEquals(0.5D, coastal.getCliffSlopeFactor(), 0D);
assertEquals(25D, coastal.getSeaCaves().getSweepJitterDegrees(), 0D);
assertEquals(10, inland.getHorizontalRadius());
assertEquals(6, inland.getVerticalRadius());
assertEquals(10, inland.getHeadroom());
@@ -110,10 +149,38 @@ public class IrisRiverConfigurationTest {
assertNull(dimension.getRiverPolicy().getRouting());
assertNull(dimension.getRiverPolicy().getOutletAdmission());
assertNull(dimension.getRiverPolicy().getBankMultiplier());
assertNull(dimension.getRiverPolicy().getShoreWidth());
assertNull(dimension.getRiverPolicy().getErosion());
assertNull(new IrisRegion().getRiverPolicy());
assertNull(new IrisBiome().getRiverPolicy());
}
private static void assertDisabledMaterial(IrisRiverMaterialConfig material) {
assertNotNull(material);
assertFalse(material.isEnabled());
assertEquals(1, material.getDepth());
assertEquals(1, material.getPalette().getPalette().size());
}
private static void assertSharedShapeDefaults(IrisRiverChannelShapeConfig shape) {
assertSharedShapeDefaults(shape.getRadialBase(), shape.getRadialMinimum(), shape.getRadialMaximum(),
shape.getPrimaryLobeStrength(), shape.getDetailLobeStrength(), shape.getCeilingRoughness(),
shape.getAspectMinimum(), shape.getAspectRange());
}
private static void assertSharedShapeDefaults(double radialBase, double radialMinimum, double radialMaximum,
double primaryLobeStrength, double detailLobeStrength,
double ceilingRoughness, double aspectMinimum, double aspectRange) {
assertEquals(0.86D, radialBase, 0D);
assertEquals(0.58D, radialMinimum, 0D);
assertEquals(1.18D, radialMaximum, 0D);
assertEquals(0.08D, primaryLobeStrength, 0D);
assertEquals(0.06D, detailLobeStrength, 0D);
assertEquals(0D, ceilingRoughness, 0D);
assertEquals(0.62D, aspectMinimum, 0D);
assertEquals(0.2D, aspectRange, 0D);
}
@Test
public void deserializesCanonicalDimensionRegionAndBiomeConfiguration() {
Gson gson = new Gson();
@@ -137,6 +204,32 @@ public class IrisRiverConfigurationTest {
"confluenceAttraction": 0.4
},
"geometry": {
"surface": {
"bedRoundness": 3.5,
"bedRoughness": 0.3,
"wallRoughness": 0.35,
"roughnessWavelength": 20,
"radialBase": 0.9,
"radialMinimum": 0.5,
"radialMaximum": 1.5,
"primaryLobeStrength": 0.1,
"detailLobeStrength": 0.05,
"ceilingRoughness": 0.2,
"aspectMinimum": 0.7,
"aspectRange": 0.3
},
"underground": {
"bedRoundness": 3,
"radialBase": 1.0,
"radialMinimum": 0.6,
"radialMaximum": 1.3,
"primaryLobeStrength": 0.12,
"detailLobeStrength": 0.04,
"ceilingRoughness": 0.4,
"aspectMinimum": 0.8,
"aspectRange": 0.1
},
"grottos": {"aspectMinimum": 0.5, "aspectRange": 0.6, "ceilingRoughness": 0.9},
"drops": {
"cascadeRunPerBlock": 5,
"cascadeExponent": 2.4,
@@ -144,7 +237,8 @@ public class IrisRiverConfigurationTest {
"flowWidthRatio": 0.6,
"maximumFlowDepth": 4,
"basinWidthRatio": 2.2,
"maximumBasinDepth": 11
"maximumBasinDepth": 11,
"undergroundCascadeRunPerBlock": 3
}
},
"surface": {
@@ -156,17 +250,36 @@ public class IrisRiverConfigurationTest {
"sink": 2,
"maximumIncision": 14,
"roughness": 0.4,
"roughnessWavelength": 24
"roughnessWavelength": 24,
"smoothingRadius": 8,
"outlineMinimumRatio": 0.8,
"outlineMaximumRatio": 2,
"springExtraDepth": 3
},
"banks": {
"shoreWidth": 2.5,
"blendSlope": 4,
"minimumBlendWidth": 6,
"maximumBlendWidth": 48,
"exposeCutStrata": false
"exposeCutStrata": false,
"shoreRise": 1.5,
"blendBaseWidth": 6,
"shoreMaterial": {"enabled": true, "palette": {"palette": [{"block": "minecraft:sand"}]}, "depth": 2},
"bankMaterial": {"enabled": true, "palette": {"palette": [{"block": "minecraft:coarse_dirt"}]}, "depth": 3}
},
"flow": {"cascadeRun": 3, "waterfallMinimumDrop": 8},
"mouths": {"flareRatio": 2.2, "maximumOceanApron": 1}
"bed": {
"material": {"enabled": true, "palette": {"palette": [{"block": "minecraft:gravel"}]}, "depth": 4}
},
"flow": {
"cascadeRun": 3,
"waterfallMinimumDrop": 8,
"waterfallThalwegFraction": 0.4,
"plungeBasinMinimumDrop": 3,
"plungeBasinLengthRatio": 1.5,
"plungeBasinDepth": 2
},
"mouths": {"flareRatio": 2.2, "maximumOceanApron": 1, "inletCourseFraction": 0.25, "inletRampSlope": 2},
"erosion": {"style": "TERRACED", "terraceSteps": 6, "cliffFraction": 0.3, "bedProfile": "V"}
},
"underground": {
"sources": {"density": 0.4, "minimumPerTile": 3, "minimumSpacing": 896},
@@ -175,7 +288,11 @@ public class IrisRiverConfigurationTest {
"depth": {"min": 2, "max": 4},
"headroom": {"min": 7, "max": 15},
"connectToExistingCaves": false,
"mouthLevelingDistance": 80
"mouthLevelingDistance": 80,
"minimumRockCover": 4,
"minimumFloorCover": 2,
"wideningSources": 16,
"bedMaterial": {"enabled": true, "palette": {"palette": [{"block": "minecraft:mud"}]}, "depth": 2}
},
"grottos": {
"coastal": {
@@ -184,7 +301,10 @@ public class IrisRiverConfigurationTest {
"horizontalRadius": 40,
"verticalRadius": 18,
"headroom": 9,
"maximumVolume": 70000
"maximumVolume": 70000,
"cliffMinimumHeight": 9,
"cliffSlopeFactor": 1.25,
"seaCaves": {"sweepJitterDegrees": 40}
},
"inland": {
"enabled": true,
@@ -225,7 +345,9 @@ public class IrisRiverConfigurationTest {
"bankBiomes": ["river/bank"],
"widthMultiplier": 1.5,
"incisionMultiplier": 0.75,
"bankMultiplier": 2
"bankMultiplier": 2,
"shoreWidth": 3,
"erosion": false
}
}
""", IrisDimension.class);
@@ -234,7 +356,8 @@ public class IrisRiverConfigurationTest {
"placement": "TRANSIT_ONLY",
"routing": "AVOID",
"mouthBiomes": ["river/mouth"],
"routingMultiplier": 2.5
"routingMultiplier": 2.5,
"erosion": true
}}
""", IrisRegion.class);
IrisBiome biome = gson.fromJson("""
@@ -242,7 +365,8 @@ public class IrisRiverConfigurationTest {
"placement": "REQUIRED_HEADWATER",
"routing": "BLOCK",
"floodedCaveBiomes": [],
"depthMultiplier": 0.8
"depthMultiplier": 0.8,
"shoreWidth": 0
}}
""", IrisBiome.class);
@@ -264,16 +388,62 @@ public class IrisRiverConfigurationTest {
assertEquals(14, rivers.getSurface().getChannel().getMaximumIncision());
assertEquals(0.4D, rivers.getSurface().getChannel().getRoughness(), 0D);
assertEquals(24, rivers.getSurface().getChannel().getRoughnessWavelength());
assertEquals(8, rivers.getSurface().getChannel().getSmoothingRadius());
assertEquals(0.8D, rivers.getSurface().getChannel().getOutlineMinimumRatio(), 0D);
assertEquals(2D, rivers.getSurface().getChannel().getOutlineMaximumRatio(), 0D);
assertEquals(3D, rivers.getSurface().getChannel().getSpringExtraDepth(), 0D);
assertEquals(2.5D, rivers.getSurface().getBanks().getShoreWidth(), 0D);
assertEquals(4D, rivers.getSurface().getBanks().getBlendSlope(), 0D);
assertEquals(6, rivers.getSurface().getBanks().getMinimumBlendWidth());
assertEquals(48, rivers.getSurface().getBanks().getMaximumBlendWidth());
assertFalse(rivers.getSurface().getBanks().isExposeCutStrata());
assertEquals(1.5D, rivers.getSurface().getBanks().getShoreRise(), 0D);
assertEquals(6D, rivers.getSurface().getBanks().getBlendBaseWidth(), 0D);
assertMaterial(rivers.getSurface().getBanks().getShoreMaterial(), "minecraft:sand", 2);
assertMaterial(rivers.getSurface().getBanks().getBankMaterial(), "minecraft:coarse_dirt", 3);
assertMaterial(rivers.getSurface().getBed().getMaterial(), "minecraft:gravel", 4);
assertEquals(3, rivers.getSurface().getFlow().getCascadeRun());
assertEquals(8, rivers.getSurface().getFlow().getWaterfallMinimumDrop());
assertEquals(0.4D, rivers.getSurface().getFlow().getWaterfallThalwegFraction(), 0D);
assertEquals(3, rivers.getSurface().getFlow().getPlungeBasinMinimumDrop());
assertEquals(1.5D, rivers.getSurface().getFlow().getPlungeBasinLengthRatio(), 0D);
assertEquals(2, rivers.getSurface().getFlow().getPlungeBasinDepth());
assertEquals(2.2D, rivers.getSurface().getMouths().getFlareRatio(), 0D);
assertEquals(1, rivers.getSurface().getMouths().getMaximumOceanApron());
assertEquals(0.25D, rivers.getSurface().getMouths().getInletCourseFraction(), 0D);
assertEquals(2D, rivers.getSurface().getMouths().getInletRampSlope(), 0D);
assertEquals(IrisRiverBlendStyle.TERRACED, rivers.getSurface().getErosion().getStyle());
assertEquals(6, rivers.getSurface().getErosion().getTerraceSteps());
assertEquals(0.3D, rivers.getSurface().getErosion().getCliffFraction(), 0D);
assertEquals(IrisRiverBedProfile.V, rivers.getSurface().getErosion().getBedProfile());
assertEquals(768, rivers.getSurface().getSources().getMinimumSpacing());
IrisSurfaceRiverShapeConfig surfaceShape = rivers.getGeometry().getSurface();
assertEquals(3.5D, surfaceShape.getBedRoundness(), 0D);
assertEquals(0.3D, surfaceShape.getBedRoughness(), 0D);
assertEquals(0.35D, surfaceShape.getWallRoughness(), 0D);
assertEquals(Integer.valueOf(20), surfaceShape.getRoughnessWavelength());
assertEquals(0.9D, surfaceShape.getRadialBase(), 0D);
assertEquals(0.5D, surfaceShape.getRadialMinimum(), 0D);
assertEquals(1.5D, surfaceShape.getRadialMaximum(), 0D);
assertEquals(0.1D, surfaceShape.getPrimaryLobeStrength(), 0D);
assertEquals(0.05D, surfaceShape.getDetailLobeStrength(), 0D);
assertEquals(0.2D, surfaceShape.getCeilingRoughness(), 0D);
assertEquals(0.7D, surfaceShape.getAspectMinimum(), 0D);
assertEquals(0.3D, surfaceShape.getAspectRange(), 0D);
IrisRiverChannelShapeConfig undergroundShape = rivers.getGeometry().getUnderground();
assertEquals(3D, undergroundShape.getBedRoundness(), 0D);
assertEquals(1D, undergroundShape.getRadialBase(), 0D);
assertEquals(0.6D, undergroundShape.getRadialMinimum(), 0D);
assertEquals(1.3D, undergroundShape.getRadialMaximum(), 0D);
assertEquals(0.12D, undergroundShape.getPrimaryLobeStrength(), 0D);
assertEquals(0.04D, undergroundShape.getDetailLobeStrength(), 0D);
assertEquals(0.4D, undergroundShape.getCeilingRoughness(), 0D);
assertEquals(0.8D, undergroundShape.getAspectMinimum(), 0D);
assertEquals(0.1D, undergroundShape.getAspectRange(), 0D);
assertEquals(0.5D, rivers.getGeometry().getGrottos().getAspectMinimum(), 0D);
assertEquals(0.6D, rivers.getGeometry().getGrottos().getAspectRange(), 0D);
assertEquals(0.9D, rivers.getGeometry().getGrottos().getCeilingRoughness(), 0D);
assertEquals(0.86D, rivers.getGeometry().getGrottos().getRadialBase(), 0D);
assertEquals(5, rivers.getGeometry().getDrops().getCascadeRunPerBlock());
assertEquals(2.4D, rivers.getGeometry().getDrops().getCascadeExponent(), 0D);
assertEquals(3, rivers.getGeometry().getDrops().getMaximumCascadeStep());
@@ -281,11 +451,19 @@ public class IrisRiverConfigurationTest {
assertEquals(4, rivers.getGeometry().getDrops().getMaximumFlowDepth());
assertEquals(2.2D, rivers.getGeometry().getDrops().getBasinWidthRatio(), 0D);
assertEquals(11, rivers.getGeometry().getDrops().getMaximumBasinDepth());
assertEquals(3, rivers.getGeometry().getDrops().getUndergroundCascadeRunPerBlock());
assertEquals(-96D, rivers.getUnderground().getFluidLevel().getMin(), 0D);
assertEquals(896, rivers.getUnderground().getSources().getMinimumSpacing());
assertFalse(rivers.getUnderground().isConnectToExistingCaves());
assertEquals(80, rivers.getUnderground().getMouthLevelingDistance());
assertEquals(4, rivers.getUnderground().getMinimumRockCover());
assertEquals(2, rivers.getUnderground().getMinimumFloorCover());
assertEquals(16, rivers.getUnderground().getWideningSources());
assertMaterial(rivers.getUnderground().getBedMaterial(), "minecraft:mud", 2);
assertEquals(40, rivers.getGrottos().getCoastal().getHorizontalRadius());
assertEquals(Integer.valueOf(9), rivers.getGrottos().getCoastal().getCliffMinimumHeight());
assertEquals(1.25D, rivers.getGrottos().getCoastal().getCliffSlopeFactor(), 0D);
assertEquals(40D, rivers.getGrottos().getCoastal().getSeaCaves().getSweepJitterDegrees(), 0D);
assertTrue(rivers.getGrottos().getInland().isEnabled());
assertTrue(rivers.getGrottos().getInland().isConnectSurfaceRivers());
assertEquals("underworld", rivers.getProfiles().getFirst().getId());
@@ -303,10 +481,23 @@ public class IrisRiverConfigurationTest {
assertEquals(IrisRiverRoutingMode.PREFER, dimension.getRiverPolicy().getRouting());
assertFalse(dimension.getRiverPolicy().getOutletAdmission());
assertEquals(2D, dimension.getRiverPolicy().getBankMultiplier(), 0D);
assertEquals(3D, dimension.getRiverPolicy().getShoreWidth(), 0D);
assertFalse(dimension.getRiverPolicy().getErosion());
assertEquals(Set.of("river/surface", "river/shore", "river/bank"), dimension.getRiverPolicy().getAllBiomeIds());
assertEquals(IrisRiverPlacementMode.TRANSIT_ONLY, region.getRiverPolicy().getPlacement());
assertEquals(new KList<>("river/mouth"), region.getRiverPolicy().getMouthBiomes());
assertNull(region.getRiverPolicy().getShoreWidth());
assertTrue(region.getRiverPolicy().getErosion());
assertEquals(IrisRiverPlacementMode.REQUIRED_HEADWATER, biome.getRiverPolicy().getPlacement());
assertTrue(biome.getRiverPolicy().getFloodedCaveBiomes().isEmpty());
assertEquals(0D, biome.getRiverPolicy().getShoreWidth(), 0D);
assertNull(biome.getRiverPolicy().getErosion());
}
private static void assertMaterial(IrisRiverMaterialConfig material, String block, int depth) {
assertTrue(material.isEnabled());
assertEquals(depth, material.getDepth());
assertEquals(1, material.getPalette().getPalette().size());
assertEquals(block, material.getPalette().getPalette().getFirst().getBlock());
}
}
@@ -244,7 +244,7 @@ public final class HydrologyPlannerProbe {
HydrologyPlannerSettings.Banks.defaults()
),
new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
undergroundDensity > 0D,
undergroundSources,
68,
@@ -258,7 +258,7 @@ public final class HydrologyPlannerProbe {
true,
0
),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets.of(
false,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
@@ -314,7 +314,7 @@ public final class HydrologyPlannerProbe {
List.of("water"), List.of()
,
Double.NaN,
null);
null, Double.NaN, true);
};
}
@@ -367,7 +367,7 @@ public final class HydrologyPlannerProbe {
List.of("water"), List.of()
,
Double.NaN,
null);
null, Double.NaN, true);
};
}
@@ -465,7 +465,7 @@ public final class HydrologyPlannerProbe {
HydrologyPlannerSettings.Banks.defaults()
),
new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(
HydrologyPlannerSettings.Underground.of(
true,
undergroundSources,
68,
@@ -479,7 +479,7 @@ public final class HydrologyPlannerProbe {
true,
0
),
new HydrologyPlannerSettings.Outlets(
HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(coastalGrottosEnabled, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
@@ -540,7 +540,9 @@ public final class HydrologyPlannerProbe {
"flooded",
List.of("water"), List.of(),
Double.NaN,
null
null,
Double.NaN,
true
);
};
}
@@ -576,7 +578,7 @@ public final class HydrologyPlannerProbe {
List.of("water"), List.of()
,
Double.NaN,
null);
null, Double.NaN, true);
}
private static long[] parseSeeds(String[] arguments) {