Disregard.

This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-03 14:01:45 -04:00
1 parent e4ca7d4e70
commit 0510c108be
57 files changed
+4420 -344

No files matched your search

@@ -45,6 +45,8 @@ final class PackHydrologyValidator {
"REQUIRED_HEADWATER" "REQUIRED_HEADWATER"
); );
private static final Set<String> ROUTING_MODES = Set.of("BLOCK", "AVOID", "ALLOW", "PREFER"); 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 Set<String> INLAND_OUTLETS = Set.of("SINKHOLE_GROTTO");
private static final List<String> POLICY_BIOME_FIELDS = List.of( private static final List<String> POLICY_BIOME_FIELDS = List.of(
"surfaceBiomes", "surfaceBiomes",
@@ -208,12 +210,17 @@ final class PackHydrologyValidator {
private static void validateGeometry(String path, JSONObject geometry, List<String> errors) { private static void validateGeometry(String path, JSONObject geometry, List<String> errors) {
JSONObject meanders = nestedObject(geometry, "meanders", path, errors); JSONObject meanders = nestedObject(geometry, "meanders", path, errors);
JSONObject surface = nestedObject(geometry, "surface", path, errors);
JSONObject underground = nestedObject(geometry, "underground", path, errors); JSONObject underground = nestedObject(geometry, "underground", path, errors);
JSONObject grottos = nestedObject(geometry, "grottos", path, errors); JSONObject grottos = nestedObject(geometry, "grottos", path, errors);
JSONObject drops = nestedObject(geometry, "drops", path, errors); JSONObject drops = nestedObject(geometry, "drops", path, errors);
if (meanders != null) { if (meanders != null) {
validateMeanders(path + ".meanders", meanders, errors); 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) { if (underground != null) {
validateChannelShape(path + ".underground", underground, errors); validateChannelShape(path + ".underground", underground, errors);
} }
@@ -244,6 +251,14 @@ final class PackHydrologyValidator {
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "bedRoughness", 0D, 1D, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "bedRoughness", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "wallRoughness", 0D, 1D, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, shape, "wallRoughness", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, shape, "roughnessWavelength", 3, 128, 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) { 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.validateOptionalIntegerRange(path, drops, "maximumFlowDepth", 1, 16, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, drops, "basinWidthRatio", 1D, 4D, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, drops, "basinWidthRatio", 1D, 4D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, drops, "maximumBasinDepth", 1, 32, 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) { 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.validateOptionalIntegerRange(path, channel, "springLength", 4, 96, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "roughness", 0D, 1D, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "roughness", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "roughnessWavelength", 4, 64, 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) { 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, "thalwegFraction", 0D, 0.95D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, erosion, "blendCurve", 0.25D, 4D, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, erosion, "blendCurve", 0.25D, 4D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, erosion, "bedNoise", 0D, 2D, 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) { 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) { 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.validateOptionalDoubleRange(path, banks, "blendSlope", 0.5D, 12D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "minimumBlendWidth", 1, 64, errors); PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "minimumBlendWidth", 1, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "maximumBlendWidth", 1, 64, errors); PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "maximumBlendWidth", 1, 64, errors);
PackJsonFieldChecks.validateOptionalBoolean(path, banks, "exposeCutStrata", 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)) { if (integerValue(banks, "minimumBlendWidth", 4) > integerValue(banks, "maximumBlendWidth", 32)) {
errors.add(path + ".minimumBlendWidth must not exceed maximumBlendWidth."); 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) { private static void validateSurfaceFlow(String path, JSONObject flow, List<String> errors) {
PackJsonFieldChecks.validateOptionalIntegerRange(path, flow, "cascadeRun", 1, 8, errors); PackJsonFieldChecks.validateOptionalIntegerRange(path, flow, "cascadeRun", 1, 8, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, flow, "waterfallMinimumDrop", 2, 32, 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) { private static void validateSurfaceBed(String path, JSONObject bed, List<String> errors) {
PackJsonFieldChecks.validateOptionalBoolean(path, bed, "allowGravityBlocks", errors); PackJsonFieldChecks.validateOptionalBoolean(path, bed, "allowGravityBlocks", errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, bed, "padding", 0, 8, 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; 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)) { if (!(rawPalette instanceof JSONObject palette)) {
errors.add(path + ".paddingPalette must be an object."); errors.add(path + " must be an object.");
return; return;
} }
Object rawEntries = palette.opt("palette"); Object rawEntries = palette.opt("palette");
if (!(rawEntries instanceof JSONArray entries) || entries.length() == 0) { 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; return;
} }
for (int index = 0; index < entries.length(); index++) { for (int index = 0; index < entries.length(); index++) {
String entryPath = path + ".paddingPalette.palette[" + index + "]"; String entryPath = path + ".palette[" + index + "]";
JSONObject entry = entries.optJSONObject(index); JSONObject entry = entries.optJSONObject(index);
if (entry == null) { if (entry == null) {
errors.add(entryPath + " must be an object."); 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, "inletLength", 0, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "inletDepth", 0, 16, errors); PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "inletDepth", 0, 16, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "maximumIncision", 0, 128, 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. // The drowned reach is the end of a river, never the whole of one.
if (mouths.has("inletLength") && integerValue(mouths, "inletLength", 64) > minimumSurfaceCourseLength) { if (mouths.has("inletLength") && integerValue(mouths, "inletLength", 64) > minimumSurfaceCourseLength) {
errors.add(path + ".inletLength must not exceed routing.minimumSurfaceCourseLength."); errors.add(path + ".inletLength must not exceed routing.minimumSurfaceCourseLength.");
@@ -508,6 +559,10 @@ final class PackHydrologyValidator {
512, 512,
errors 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); NumericRange height = dimensionHeight(dimension);
if (fluidLevel.minimum() - depth.maximum() <= height.minimum()) { if (fluidLevel.minimum() - depth.maximum() <= height.minimum()) {
@@ -598,6 +653,9 @@ final class PackHydrologyValidator {
} }
} }
if (coastal) { 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); JSONObject seaCaves = nestedObject(grotto, "seaCaves", path, errors);
if (seaCaves != null) { if (seaCaves != null) {
String seaCavePath = path + ".seaCaves"; String seaCavePath = path + ".seaCaves";
@@ -606,6 +664,7 @@ final class PackHydrologyValidator {
PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "minimumSpacing", 16, 8192, errors); PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "minimumSpacing", 16, 8192, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "minimumCoastHeight", 1, 128, errors); PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "minimumCoastHeight", 1, 128, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(seaCavePath, seaCaves, "depth", 0, 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 horizontalRadius = integerValue(grotto, "horizontalRadius", DEFAULT_COASTAL_GROTTO.getHorizontalRadius());
int minimumSpacing = integerValue(seaCaves, "minimumSpacing", DEFAULT_COASTAL_GROTTO.getSeaCaves().getMinimumSpacing()); int minimumSpacing = integerValue(seaCaves, "minimumSpacing", DEFAULT_COASTAL_GROTTO.getSeaCaves().getMinimumSpacing());
if (minimumSpacing < horizontalRadius * 2) { if (minimumSpacing < horizontalRadius * 2) {
@@ -962,6 +1021,8 @@ final class PackHydrologyValidator {
validateNullableDouble(path, policy, "routingMultiplier", 0D, 64D, errors); validateNullableDouble(path, policy, "routingMultiplier", 0D, 64D, errors);
validateNullableDouble(path, policy, "bankMultiplier", 0D, 4D, errors); validateNullableDouble(path, policy, "bankMultiplier", 0D, 4D, errors);
validateNullableDouble(path, policy, "shoreBiomeWidth", 0D, 32D, 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); validateNullableBoolean(path, policy, "confined", errors);
return policy; 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.IrisOreGenerator;
import art.arcane.iris.engine.object.IrisOreGeneratorBounds; import art.arcane.iris.engine.object.IrisOreGeneratorBounds;
import art.arcane.iris.engine.object.IrisRegion; 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.volmlib.util.collection.KList;
import art.arcane.iris.util.project.context.ChunkedDataCache; import art.arcane.iris.util.project.context.ChunkedDataCache;
import art.arcane.iris.util.project.context.ChunkContext; 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(); KList<IrisOreGenerator> dimensionUndergroundOres = hideOres ? null : dimension.getUndergroundOreGenerators();
IrisOreGeneratorBounds dimensionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getSurfaceOreGeneratorBounds(); IrisOreGeneratorBounds dimensionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getSurfaceOreGeneratorBounds();
IrisOreGeneratorBounds dimensionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getUndergroundOreGeneratorBounds(); IrisOreGeneratorBounds dimensionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getUndergroundOreGeneratorBounds();
boolean exposeCutStrata = dimension.getHydrology() != null IrisSurfaceRiverBankConfig riverBanks = dimension.getHydrology() == null
&& dimension.getHydrology().getRivers().getSurface().getBanks().isExposeCutStrata(); ? null
: dimension.getHydrology().getRivers().getSurface().getBanks();
boolean exposeCutStrata = riverBanks != null && riverBanks.isExposeCutStrata();
IrisSurfaceRiverBedConfig riverBed = dimension.getHydrology() == null IrisSurfaceRiverBedConfig riverBed = dimension.getHydrology() == null
? null ? null
: dimension.getHydrology().getRivers().getSurface().getBed(); : dimension.getHydrology().getRivers().getSurface().getBed();
boolean padRiverBed = riverBed != null && !riverBed.isAllowGravityBlocks(); 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++) { for (int zf = 0; zf < chunkDepth; zf++) {
int realZ = zf + z; int realZ = zf + z;
@@ -143,6 +150,8 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
? Math.max(0, hydrology.naturalHeight() - he) ? Math.max(0, hydrology.naturalHeight() - he)
: 0; : 0;
boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned(); boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned();
IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial(
hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial);
PlatformBlockState fluid = hydrologyFluid == null PlatformBlockState fluid = hydrologyFluid == null
? complex.resolveSurfaceFluid(realX, realZ) ? complex.resolveSurfaceFluid(realX, realZ)
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ); : complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
@@ -211,6 +220,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
if (blocks.hasIndex(depth + cut)) { if (blocks.hasIndex(depth + cut)) {
PlatformBlockState layerBlock = blocks.get(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)) { if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
layerBlock = riverBed.getPaddingPalette().get(localRng, realX, i, realZ, data); 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) { private PlatformBlockState generateOres(KList<IrisOreGenerator> oreGenerators, int x, int y, int z, RNG rng, IrisData data) {
if (oreGenerators == null || oreGenerators.isEmpty()) { if (oreGenerators == null || oreGenerators.isEmpty()) {
return null; return null;
@@ -24,10 +24,10 @@ final class HydrologyFootprintCompiler {
private static final int[][] HORIZONTAL_NEIGHBORS = { private static final int[][] HORIZONTAL_NEIGHBORS = {
{1, 0}, {-1, 0}, {0, 1}, {0, -1} {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_FIRST_PHASE_SALT = 0x4f5247414e31L;
private static final long ORGANIC_SHAPE_SECOND_PHASE_SALT = 0x4f5247414e32L; private static final long ORGANIC_SHAPE_SECOND_PHASE_SALT = 0x4f5247414e32L;
private static final long ORGANIC_BED_VARIATION_SALT = 0x424544564152L; private static final long ORGANIC_BED_VARIATION_SALT = 0x424544564152L;
private static final long ORGANIC_CEILING_VARIATION_SALT = 0x4345494c564152L;
private final HydrologyPlannerSettings settings; private final HydrologyPlannerSettings settings;
private final HydrologyTerrainSampler sampler; private final HydrologyTerrainSampler sampler;
@@ -1169,7 +1169,7 @@ final class HydrologyFootprintCompiler {
ellipsoidScale ellipsoidScale
); );
int fluidHead = channel ? shape.fluidHead() : bed; 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)) { if (channel && terrainRoofedLayer(course, segment)) {
ceiling = Math.max(fluidHead, Math.min(ceiling, terrain.naturalHeight() - 1)); 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); resolved = channel ? shape.bed() : gradedBed(shape, terrain, distance);
} else if (channel) { } else if (channel) {
double normalized = Math.min(1D, distance / Math.max(1D, shape.channelRadius())); double normalized = Math.min(1D, distance / Math.max(1D, shape.channelRadius()));
double thalwegFraction = settings.surface().banks().flow().waterfallThalwegFraction();
double shoulder = Math.max( double shoulder = Math.max(
0D, 0D,
(normalized - SURFACE_THALWEG_RATIO) / (1D - SURFACE_THALWEG_RATIO) (normalized - thalwegFraction) / (1D - thalwegFraction)
); );
int maximumDepth = Math.max(1, shape.fluidHead() - shape.bed()); int maximumDepth = Math.max(1, shape.fluidHead() - shape.bed());
double roughness = signedOrganicNoise( double roughness = signedOrganicNoise(
@@ -1407,12 +1408,29 @@ final class HydrologyFootprintCompiler {
return shape.fluidHead() - localExtent; 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()) { if (!shape.ellipsoid() && !shape.archedChannel()) {
return shape.ceiling(); return shape.ceiling();
} }
int upperExtent = shape.ceiling() - shape.fluidHead(); 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()) { if (shape.archedChannel()) {
localExtent = Math.max(1, localExtent); localExtent = Math.max(1, localExtent);
} }
@@ -1473,6 +1491,7 @@ final class HydrologyFootprintCompiler {
if (!shape.organicBoundary()) { if (!shape.organicBoundary()) {
return distance; return distance;
} }
HydrologyPlannerSettings.ChannelShape channelShape = channelShape(segment.type());
if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO
|| segment.type() == HydrologyFeatureType.INLAND_GROTTO) { || segment.type() == HydrologyFeatureType.INLAND_GROTTO) {
double orientation = HydrologyHash.unit(HydrologyHash.mix( double orientation = HydrologyHash.unit(HydrologyHash.mix(
@@ -1483,10 +1502,10 @@ final class HydrologyFootprintCompiler {
double sine = StrictMath.sin(orientation); double sine = StrictMath.sin(orientation);
double rotatedX = deltaX * cosine + deltaZ * sine; double rotatedX = deltaX * cosine + deltaZ * sine;
double rotatedZ = -deltaX * sine + deltaZ * cosine; double rotatedZ = -deltaX * sine + deltaZ * cosine;
double aspect = 0.62D + HydrologyHash.unit(HydrologyHash.mix( double aspect = channelShape.aspectMinimum() + HydrologyHash.unit(HydrologyHash.mix(
segment.courseId(), segment.courseId(),
ORGANIC_SHAPE_SECOND_PHASE_SALT ORGANIC_SHAPE_SECOND_PHASE_SALT
)) * 0.2D; )) * channelShape.aspectRange();
distance = StrictMath.hypot(rotatedX, rotatedZ / aspect); distance = StrictMath.hypot(rotatedX, rotatedZ / aspect);
} }
double angle = StrictMath.atan2(deltaZ, deltaX); double angle = StrictMath.atan2(deltaZ, deltaX);
@@ -1500,7 +1519,6 @@ final class HydrologyFootprintCompiler {
)) * StrictMath.PI * 2D; )) * StrictMath.PI * 2D;
double firstLobe = 0.5D + 0.5D * StrictMath.sin(angle * 3D + firstPhase); double firstLobe = 0.5D + 0.5D * StrictMath.sin(angle * 3D + firstPhase);
double secondLobe = 0.5D + 0.5D * StrictMath.sin(angle * 5D + secondPhase); double secondLobe = 0.5D + 0.5D * StrictMath.sin(angle * 5D + secondPhase);
HydrologyPlannerSettings.ChannelShape channelShape = channelShape(segment.type());
double coherent = signedOrganicNoise( double coherent = signedOrganicNoise(
segment.courseId(), segment.courseId(),
worldX, worldX,
@@ -1513,12 +1531,12 @@ final class HydrologyFootprintCompiler {
worldZ, worldZ,
Math.max(3, channelShape.roughnessWavelength() / 2) Math.max(3, channelShape.roughnessWavelength() / 2)
); );
double radialScale = 0.86D double radialScale = channelShape.radialBase()
+ (firstLobe - 0.5D) * 0.08D + (firstLobe - 0.5D) * channelShape.primaryLobeStrength()
+ (secondLobe - 0.5D) * 0.06D + (secondLobe - 0.5D) * channelShape.detailLobeStrength()
+ coherent * channelShape.wallRoughness() * 0.7D + coherent * channelShape.wallRoughness() * 0.7D
+ detail * channelShape.wallRoughness() * 0.3D; + 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; return distance / radialScale;
} }
@@ -62,7 +62,6 @@ public final class HydrologyPlanner {
private static final long DEEP_FLUID_SALT = 0x44454550464cL; private static final long DEEP_FLUID_SALT = 0x44454550464cL;
private static final long SURFACE_POOL_SALT = 0x504f4f4cL; private static final long SURFACE_POOL_SALT = 0x504f4f4cL;
private static final long SEA_CAVE_SALT = 0x534541434156L; 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_X_OFFSET_SALT = 0x44454550584fL;
private static final long DEEP_FLUID_Z_OFFSET_SALT = 0x444545505a4fL; private static final long DEEP_FLUID_Z_OFFSET_SALT = 0x444545505a4fL;
private static final long DEEP_CHANNEL_HEADING_SALT = 0x44454550484447L; private static final long DEEP_CHANNEL_HEADING_SALT = 0x44454550484447L;
@@ -2262,9 +2261,11 @@ public final class HydrologyPlanner {
private HydrologyFeatureType coastalOutletType(HydrologyTerrainSample land) { private HydrologyFeatureType coastalOutletType(HydrologyTerrainSample land) {
HydrologyPlannerSettings.Outlets outlets = settings.outlets(); 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) boolean cliff = land.naturalHeight() - settingsSeaLevel(land)
>= outlets.coastalCliffMinimumHeight() >= outlets.coastalCliffMinimumHeight()
|| land.slope() >= outlets.coastalCliffMinimumHeight() * 0.5D; || outlets.coastalCliffSlopeFactor() > 0D
&& land.slope() >= outlets.coastalCliffMinimumHeight() * outlets.coastalCliffSlopeFactor();
if (cliff && outlets.coastalGrotto().enabled()) { if (cliff && outlets.coastalGrotto().enabled()) {
return HydrologyFeatureType.COASTAL_GROTTO; return HydrologyFeatureType.COASTAL_GROTTO;
} }
@@ -5374,7 +5375,7 @@ public final class HydrologyPlanner {
HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z()); HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z());
return terrain == null || terrain.ocean() return terrain == null || terrain.ocean()
? Integer.MAX_VALUE ? Integer.MAX_VALUE
: terrain.naturalHeight() - minimumHeadroom - 1; : terrain.naturalHeight() - minimumHeadroom - settings.underground().minimumRockCover();
} }
private int undergroundSegmentCap( private int undergroundSegmentCap(
@@ -5418,7 +5419,7 @@ public final class HydrologyPlanner {
); );
int minimumLandHeight = minimumLandHeightWithinRadius(point.x(), point.z(), radius); int minimumLandHeight = minimumLandHeightWithinRadius(point.x(), point.z(), radius);
if (minimumLandHeight != Integer.MAX_VALUE) { if (minimumLandHeight != Integer.MAX_VALUE) {
segmentCap = Math.min(segmentCap, minimumLandHeight - headroom - 1); segmentCap = Math.min(segmentCap, minimumLandHeight - headroom - settings.underground().minimumRockCover());
} }
} }
if (samples != null) { if (samples != null) {
@@ -5484,7 +5485,7 @@ public final class HydrologyPlanner {
? depths[0] ? depths[0]
: index == depths.length ? depths[depths.length - 1] : Math.max(depths[index - 1], depths[index]); : index == depths.length ? depths[depths.length - 1] : Math.max(depths[index - 1], depths[index]);
depth = Math.max(depth, settings.geometry().drops().maximumBasinDepth()); 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 int boundedWorldFloor = worldFloorHead < Integer.MIN_VALUE
? Integer.MIN_VALUE ? Integer.MIN_VALUE
: worldFloorHead > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) worldFloorHead; : worldFloorHead > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) worldFloorHead;
@@ -5707,8 +5708,14 @@ public final class HydrologyPlanner {
return false; return false;
} }
int preferredRun = Math.addExact(Math.multiplyExact(drop, drops.cascadeRunPerBlock()), 1); int preferredRun = Math.addExact(Math.multiplyExact(drop, drops.cascadeRunPerBlock()), 1);
int undergroundRun = drops.undergroundCascadeRunPerBlock();
int desiredRun = type.isUnderground() int desiredRun = type.isUnderground()
? minimumRun ? (undergroundRun == 0
? minimumRun
: Math.min(
Math.max(minimumRun, Math.addExact(Math.multiplyExact(drop, undergroundRun), 1)),
maximumGeneratedRun
))
: Math.min(preferredRun, maximumGeneratedRun); : Math.min(preferredRun, maximumGeneratedRun);
List<HydrologyPoint> raster = continuousSurfaceBore List<HydrologyPoint> raster = continuousSurfaceBore
? directRaster ? 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 // 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. // face the same way along a straight shore.
private static HydrologyPoint seaCaveInnerPoint( private HydrologyPoint seaCaveInnerPoint(
GridNode land, GridNode land,
GridNode ocean, GridNode ocean,
HydrologyPoint landward, HydrologyPoint landward,
@@ -7136,7 +7143,8 @@ public final class HydrologyPlanner {
double normalZ = land.z() - ocean.z(); double normalZ = land.z() - ocean.z();
double length = StrictMath.hypot(normalX, normalZ); double length = StrictMath.hypot(normalX, normalZ);
double jitter = StrictMath.toRadians( 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 cos = StrictMath.cos(jitter);
double sin = StrictMath.sin(jitter); double sin = StrictMath.sin(jitter);
@@ -7605,7 +7613,10 @@ public final class HydrologyPlanner {
int discharge, int discharge,
double multiplier 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); int base = styledBase + (int) StrictMath.round((maximum - styledBase) * flowScale);
return clamp((int) StrictMath.round(base * multiplier), minimum, maximum); return clamp((int) StrictMath.round(base * multiplier), minimum, maximum);
} }
@@ -1,6 +1,10 @@
package art.arcane.iris.engine.hydrology; 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.List;
import java.util.Objects;
public record HydrologyPlannerSettings( public record HydrologyPlannerSettings(
int seaLevel, 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 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 Surface(true, surfaceSources, 4, 8, 2, 4, 10, 1.5D, Banks.defaults()),
new Hydraulics(8), new Hydraulics(8),
new Underground(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1), Underground.of(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1),
new Outlets( Outlets.of(
true, true,
new Grotto(true, 18, 8, 8, 32768), new Grotto(true, 18, 8, 8, 32768),
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 * {@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. * 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( public record Banks(
int sink, int sink,
@@ -302,12 +307,14 @@ public record HydrologyPlannerSettings(
int springLength, int springLength,
boolean exposeCutStrata, boolean exposeCutStrata,
Erosion erosion, Erosion erosion,
Ponds ponds Ponds ponds,
Channel channel,
Flow flow
) { ) {
// Structural invariants only; authoring bounds live in the pack validator. // Structural invariants only; authoring bounds live in the pack validator.
public Banks { public Banks {
if (erosion == null || ponds == null || inlet == null) { if (erosion == null || ponds == null || inlet == null || channel == null || flow == null) {
throw new IllegalArgumentException("Surface erosion, inlet and pond settings are required."); throw new IllegalArgumentException("Surface erosion, inlet, pond, channel and flow settings are required.");
} }
if (sink < 0 if (sink < 0
|| !Double.isFinite(blendSlope) || blendSlope <= 0D || !Double.isFinite(blendSlope) || blendSlope <= 0D
@@ -324,42 +331,78 @@ public record HydrologyPlannerSettings(
} }
public static Banks defaults() { 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) { public Banks withInlet(Inlet inlet) {
return new Banks(sink, blendSlope, minimumBlendWidth, maximumBlendWidth, roughness, roughnessWavelength, return new Banks(sink, blendSlope, minimumBlendWidth, maximumBlendWidth, roughness, roughnessWavelength,
cascadeRun, waterfallMinimumDrop, mouthFlareRatio, inlet, springWidthRatio, springLength, 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 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 * 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. * deepens by {@code depth}; the stations above it grade down {@code rampSlope} blocks per station into
* The inlet and its approach may be cut up to {@code maximumIncision} deep instead of the channel * the inlet. The inlet and its approach may be cut up to {@code maximumIncision} deep instead of the
* cap, so a coastal rise no longer rejects the course; a rise the cap cannot pass ends the inlet * channel cap, so a coastal rise no longer rejects the course; a rise the cap cannot pass ends the
* there, and the inlet never takes more than half the exposed course. A zero length is the plain * inlet there, and the inlet never takes more than {@code courseFraction} of the exposed course. A
* crossing: the head drops to sea level at the coast, the channel neither widens nor deepens. * zero length is the plain crossing: the head drops to sea level at the coast, the channel neither
* widens nor deepens.
*/ */
public record Inlet( public record Inlet(
int length, int length,
int depth, int depth,
int maximumIncision int maximumIncision,
double courseFraction,
double rampSlope
) { ) {
public Inlet { public Inlet {
if (length < 0 || length > 1024 || depth < 0 || depth > 64 || maximumIncision < 0 || maximumIncision > 512) { if (length < 0 || length > 1024 || depth < 0 || depth > 64 || maximumIncision < 0 || maximumIncision > 512) {
throw new IllegalArgumentException("Surface inlet settings are invalid."); 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() { public static Inlet none() {
return new Inlet(0, 0, 0); return new Inlet(0, 0, 0, 0.5D, 1D);
} }
public static Inlet defaults() { 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 * 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 * (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 * {@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( public record Erosion(
boolean enabled, boolean enabled,
int smoothingRadius, int smoothingRadius,
double thalwegFraction, double thalwegFraction,
double blendCurve, double blendCurve,
double bedNoise double bedNoise,
IrisRiverBlendStyle style,
int terraceSteps,
double cliffFraction,
IrisRiverBedProfile bedProfile,
double shoreRise,
double blendBaseWidth
) { ) {
public Erosion { public Erosion {
if (smoothingRadius < 0 if (smoothingRadius < 0
@@ -385,10 +438,82 @@ public record HydrologyPlannerSettings(
|| !Double.isFinite(bedNoise) || bedNoise < 0D) { || !Double.isFinite(bedNoise) || bedNoise < 0D) {
throw new IllegalArgumentException("Surface erosion settings are invalid."); 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() { 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 * 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} * 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 * 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( public record SeaCaves(
boolean enabled, boolean enabled,
int maximumPerTile, int maximumPerTile,
int minimumSpacing, int minimumSpacing,
int minimumCoastHeight, int minimumCoastHeight,
int depth int depth,
double sweepJitterDegrees
) { ) {
public SeaCaves { public SeaCaves {
if (maximumPerTile < 0 || maximumPerTile > 64 if (maximumPerTile < 0 || maximumPerTile > 64
|| minimumSpacing < 16 || minimumSpacing > 8192 || minimumSpacing < 16 || minimumSpacing > 8192
|| minimumCoastHeight < 1 || minimumCoastHeight > 128 || 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."); throw new IllegalArgumentException("Sea cave bounds are invalid.");
} }
} }
public static SeaCaves disabled() { 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 minimumHeadroom,
int maximumHeadroom, int maximumHeadroom,
boolean connectToExistingCaves, boolean connectToExistingCaves,
int tributaries int tributaries,
int minimumRockCover,
int minimumFloorCover,
int wideningSources
) { ) {
public Underground { public Underground {
if (sources == null || minimumFluidY > maximumFluidY) { if (sources == null || minimumFluidY > maximumFluidY) {
@@ -504,6 +640,30 @@ public record HydrologyPlannerSettings(
if (tributaries < 0 || tributaries > 4) { if (tributaries < 0 || tributaries > 4) {
throw new IllegalArgumentException("underground tributaries must be between 0 and 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 mouthLevelingDistance,
int maximumOceanApron, int maximumOceanApron,
int maximumPerTile, int maximumPerTile,
int maximumCoastalPerTile int maximumCoastalPerTile,
double coastalCliffSlopeFactor
) { ) {
public Outlets { public Outlets {
if (coastalGrotto == null || inlandGrotto == null) { if (coastalGrotto == null || inlandGrotto == null) {
@@ -530,6 +691,23 @@ public record HydrologyPlannerSettings(
|| maximumCoastalPerTile < 0 || maximumCoastalPerTile > 256) { || maximumCoastalPerTile < 0 || maximumCoastalPerTile > 256) {
throw new IllegalArgumentException("Outlet bounds are invalid."); 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() { 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( return new Geometry(
new Meanders(64, 12, 0.34D, 0.42D, 0.48D, 1, 82D), new Meanders(64, 12, 0.34D, 0.42D, 0.48D, 1, 82D),
channel, channel,
channel, 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( public record ChannelShape(
double bedRoundness, double bedRoundness,
double bedRoughness, double bedRoughness,
double wallRoughness, double wallRoughness,
int roughnessWavelength int roughnessWavelength,
double radialBase,
double radialMinimum,
double radialMaximum,
double primaryLobeStrength,
double detailLobeStrength,
double ceilingRoughness,
double aspectMinimum,
double aspectRange
) { ) {
public ChannelShape { public ChannelShape {
if (!Double.isFinite(bedRoundness) || bedRoundness < 1D || bedRoundness > 6D if (!Double.isFinite(bedRoundness) || bedRoundness < 1D || bedRoundness > 6D
@@ -610,6 +802,22 @@ public record HydrologyPlannerSettings(
|| roughnessWavelength < 3 || roughnessWavelength > 128) { || roughnessWavelength < 3 || roughnessWavelength > 128) {
throw new IllegalArgumentException("Hydrology channel shape is invalid."); 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, double flowWidthRatio,
int maximumFlowDepth, int maximumFlowDepth,
double basinWidthRatio, double basinWidthRatio,
int maximumBasinDepth int maximumBasinDepth,
int undergroundCascadeRunPerBlock
) { ) {
public Drops { public Drops {
if (cascadeRunPerBlock < 1 || cascadeRunPerBlock > 16 if (cascadeRunPerBlock < 1 || cascadeRunPerBlock > 16
@@ -632,6 +841,23 @@ public record HydrologyPlannerSettings(
|| maximumBasinDepth < maximumFlowDepth || maximumBasinDepth > 32) { || maximumBasinDepth < maximumFlowDepth || maximumBasinDepth > 32) {
throw new IllegalArgumentException("Hydrology drop geometry is invalid."); 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) { public int flowWidth(int channelWidth) {
@@ -32,13 +32,17 @@ public record HydrologyTerrainSample(
List<String> preferredProfileKeys, List<String> preferredProfileKeys,
List<String> surfacePoolKeys, List<String> surfacePoolKeys,
double shoreBiomeWidth, 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 * {@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. * 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 * {@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 { public HydrologyTerrainSample {
requireFiniteNonNegative(slope, "slope"); requireFiniteNonNegative(slope, "slope");
@@ -64,6 +68,9 @@ public record HydrologyTerrainSample(
if (!Double.isNaN(shoreBiomeWidth) && (!Double.isFinite(shoreBiomeWidth) || shoreBiomeWidth < 0D)) { if (!Double.isNaN(shoreBiomeWidth) && (!Double.isFinite(shoreBiomeWidth) || shoreBiomeWidth < 0D)) {
throw new IllegalArgumentException("shoreBiomeWidth must be NaN, or finite and non-negative."); 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; confinesKey = confinesKey == null || confinesKey.isBlank() ? null : confinesKey;
} }
@@ -72,6 +79,11 @@ public record HydrologyTerrainSample(
return Double.isNaN(shoreBiomeWidth) ? fallback : shoreBiomeWidth; 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. */ /** Whether a river at {@code upstream} may drain into this column without leaving its confines. */
public boolean drainsInto(HydrologyTerrainSample downstream) { public boolean drainsInto(HydrologyTerrainSample downstream) {
return confinesKey == null || confinesKey.equals(downstream.confinesKey()); return confinesKey == null || confinesKey.equals(downstream.confinesKey());
@@ -108,7 +120,9 @@ public record HydrologyTerrainSample(
List.of("default"), List.of("default"),
List.of(), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -143,7 +157,9 @@ public record HydrologyTerrainSample(
List.of("default"), List.of("default"),
List.of(), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -178,7 +194,9 @@ public record HydrologyTerrainSample(
preferredProfileKeys, preferredProfileKeys,
replacementPoolKeys, replacementPoolKeys,
shoreBiomeWidth, shoreBiomeWidth,
confinesKey confinesKey,
shoreWidth,
erosion
); );
} }
@@ -213,7 +231,83 @@ public record HydrologyTerrainSample(
preferredProfileKeys, preferredProfileKeys,
surfacePoolKeys, surfacePoolKeys,
shoreBiomeWidth, 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 routingMultiplier,
double bankMultiplier, double bankMultiplier,
Double shoreBiomeWidth, Double shoreBiomeWidth,
RiverConfinement confinement RiverConfinement confinement,
Double shoreWidth,
Boolean erosion
) { ) {
public EffectiveRiverPolicy { public EffectiveRiverPolicy {
placement = Objects.requireNonNull(placement); placement = Objects.requireNonNull(placement);
@@ -32,6 +34,9 @@ public record EffectiveRiverPolicy(
if (shoreBiomeWidth != null && (!Double.isFinite(shoreBiomeWidth) || shoreBiomeWidth < 0D)) { if (shoreBiomeWidth != null && (!Double.isFinite(shoreBiomeWidth) || shoreBiomeWidth < 0D)) {
throw new IllegalArgumentException("shoreBiomeWidth must be finite and non-negative."); 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); profiles = List.copyOf(profiles);
surfaceBiomes = List.copyOf(surfaceBiomes); surfaceBiomes = List.copyOf(surfaceBiomes);
mouthBiomes = List.copyOf(mouthBiomes); mouthBiomes = List.copyOf(mouthBiomes);
@@ -77,6 +77,8 @@ public final class RiverPolicyResolver {
private double bankMultiplier = 1D; private double bankMultiplier = 1D;
private Double shoreBiomeWidth = null; private Double shoreBiomeWidth = null;
private RiverConfinement confinement = RiverConfinement.NONE; 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 * {@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) { if (policy.getShoreBiomeWidth() != null) {
shoreBiomeWidth = policy.getShoreBiomeWidth(); shoreBiomeWidth = policy.getShoreBiomeWidth();
} }
if (policy.getShoreWidth() != null) {
shoreWidth = policy.getShoreWidth();
}
if (policy.getErosion() != null) {
erosion = policy.getErosion();
}
if (policy.getConfined() != null) { if (policy.getConfined() != null) {
confinement = policy.getConfined() ? scope : RiverConfinement.NONE; confinement = policy.getConfined() ? scope : RiverConfinement.NONE;
} }
@@ -157,7 +165,9 @@ public final class RiverPolicyResolver {
routingMultiplier, routingMultiplier,
bankMultiplier, bankMultiplier,
shoreBiomeWidth, 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.IrisRiverHydrology;
import art.arcane.iris.engine.object.IrisRiverInlandOutlet; import art.arcane.iris.engine.object.IrisRiverInlandOutlet;
import art.arcane.iris.engine.object.IrisRiverMeanderConfig; 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.IrisRiverPolicy;
import art.arcane.iris.engine.object.IrisRiverProfile; import art.arcane.iris.engine.object.IrisRiverProfile;
import art.arcane.iris.engine.object.IrisRiverRoutingConfig; 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.IrisSurfaceRiverBankConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverChannelConfig; import art.arcane.iris.engine.object.IrisSurfaceRiverChannelConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverConfig; 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.IrisSurfaceRiverFlowConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverPondConfig; 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.IrisSurfaceRiverSourceConfig;
import art.arcane.iris.engine.object.IrisUndergroundRiverConfig; import art.arcane.iris.engine.object.IrisUndergroundRiverConfig;
import art.arcane.iris.engine.object.IrisUndergroundRiverSourceConfig; import art.arcane.iris.engine.object.IrisUndergroundRiverSourceConfig;
@@ -510,11 +513,12 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
if (!rivers.getUnderground().getFluidLevel().isFlat()) { if (!rivers.getUnderground().getFluidLevel().isFlat()) {
int lowestFluidY = Math.max( int lowestFluidY = Math.max(
settings.underground().minimumFluidY(), settings.underground().minimumFluidY(),
settings.underground().maximumDepth() + 1 settings.underground().maximumDepth() + settings.underground().minimumFloorCover()
); );
int highestFluidY = Math.min( int highestFluidY = Math.min(
settings.underground().maximumFluidY(), settings.underground().maximumFluidY(),
naturalHeight - settings.underground().minimumHeadroom() - 1 naturalHeight - settings.underground().minimumHeadroom()
- settings.underground().minimumRockCover()
); );
if (lowestFluidY <= highestFluidY) { if (lowestFluidY <= highestFluidY) {
configuredFluidY = lowestFluidY + (highestFluidY - lowestFluidY) / 2; configuredFluidY = lowestFluidY + (highestFluidY - lowestFluidY) / 2;
@@ -522,7 +526,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
} }
int caveFloorY = configuredFluidY - settings.underground().maximumDepth(); int caveFloorY = configuredFluidY - settings.underground().maximumDepth();
boolean caveAvailable = caveFloorY > 0 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 routingAllowed = policy.allowsTransit() && policy.allowsRouting() && !ocean;
boolean sourceAllowed = routingAllowed && policy.allowsSources(); boolean sourceAllowed = routingAllowed && policy.allowsSources();
double sourceWeight = switch (policy.placement()) { double sourceWeight = switch (policy.placement()) {
@@ -564,7 +569,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
profiles, profiles,
policy.surfacePools(), policy.surfacePools(),
policy.shoreBiomeWidth() == null ? Double.NaN : policy.shoreBiomeWidth(), 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); return new IrisHydrologyRoutingTerrainSampler.TerrainBasis(resolvedNaturalHeight, terrain);
} }
@@ -740,7 +747,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
IrisSurfaceRiverSourceConfig surfaceSources = surface.getSources(); IrisSurfaceRiverSourceConfig surfaceSources = surface.getSources();
IrisSurfaceRiverChannelConfig channel = surface.getChannel(); IrisSurfaceRiverChannelConfig channel = surface.getChannel();
IrisSurfaceRiverBankConfig banks = surface.getBanks(); IrisSurfaceRiverBankConfig banks = surface.getBanks();
IrisSurfaceRiverErosionConfig erosion = surface.getErosion();
IrisSurfaceRiverFlowConfig flow = surface.getFlow(); IrisSurfaceRiverFlowConfig flow = surface.getFlow();
IrisRiverMouthConfig mouths = surface.getMouths();
IrisUndergroundRiverConfig underground = rivers.getUnderground(); IrisUndergroundRiverConfig underground = rivers.getUnderground();
IrisUndergroundRiverSourceConfig undergroundSources = underground.getSources(); IrisUndergroundRiverSourceConfig undergroundSources = underground.getSources();
int routeNodes = maximumRouteNodes(routing); int routeNodes = maximumRouteNodes(routing);
@@ -788,25 +797,45 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
channel.getRoughnessWavelength(), channel.getRoughnessWavelength(),
flow.getCascadeRun(), flow.getCascadeRun(),
flow.getWaterfallMinimumDrop(), flow.getWaterfallMinimumDrop(),
surface.getMouths().getFlareRatio(), mouths.getFlareRatio(),
new HydrologyPlannerSettings.Inlet( new HydrologyPlannerSettings.Inlet(
surface.getMouths().getInletLength(), mouths.getInletLength(),
surface.getMouths().getInletDepth(), mouths.getInletDepth(),
surface.getMouths().getMaximumIncision() mouths.getMaximumIncision(),
mouths.getInletCourseFraction(),
mouths.getInletRampSlope()
), ),
channel.getSpringWidthRatio(), channel.getSpringWidthRatio(),
channel.getSpringLength(), channel.getSpringLength(),
banks.isExposeCutStrata(), banks.isExposeCutStrata(),
new HydrologyPlannerSettings.Erosion( new HydrologyPlannerSettings.Erosion(
surface.getErosion().isEnabled(), erosion.isEnabled(),
surface.getErosion().getSmoothingRadius(), erosion.getSmoothingRadius(),
surface.getErosion().getThalwegFraction(), erosion.getThalwegFraction(),
surface.getErosion().getBlendCurve(), erosion.getBlendCurve(),
surface.getErosion().getBedNoise() erosion.getBedNoise(),
erosion.getStyle(),
erosion.getTerraceSteps(),
erosion.getCliffFraction(),
erosion.getBedProfile(),
banks.getShoreRise(),
banks.getBlendBaseWidth()
), ),
new HydrologyPlannerSettings.Ponds( new HydrologyPlannerSettings.Ponds(
pond(surface.getPonds().getSource()), pond(surface.getPonds().getSource()),
pond(surface.getPonds().getTerminal()) 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()), minimumInt(underground.getHeadroom()),
maximumInt(underground.getHeadroom()), maximumInt(underground.getHeadroom()),
underground.isConnectToExistingCaves(), underground.isConnectToExistingCaves(),
underground.getTributaries() underground.getTributaries(),
underground.getMinimumRockCover(),
underground.getMinimumFloorCover(),
underground.getWideningSources()
); );
IrisRiverGrottoConfig grottos = rivers.getGrottos(); IrisRiverGrottoConfig grottos = rivers.getGrottos();
IrisCoastalRiverGrottoConfig coastal = grottos.getCoastal(); IrisCoastalRiverGrottoConfig coastal = grottos.getCoastal();
@@ -846,20 +878,18 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
grotto(inlandEnabled, inland.getHorizontalRadius(), inland.getVerticalRadius(), grotto(inlandEnabled, inland.getHorizontalRadius(), inland.getVerticalRadius(),
inland.getHeadroom(), inland.getMaximumVolume()), inland.getHeadroom(), inland.getMaximumVolume()),
inlandEnabled && inland.isConnectSurfaceRivers(), inlandEnabled && inland.isConnectSurfaceRivers(),
Math.max(4, coastal.getVerticalRadius()), coastal.getCliffMinimumHeight() == null
? Math.max(4, coastal.getVerticalRadius())
: coastal.getCliffMinimumHeight(),
underground.getMouthLevelingDistance(), underground.getMouthLevelingDistance(),
surface.getMouths().getMaximumOceanApron(), mouths.getMaximumOceanApron(),
routing.getMaximumOutletsPerTile(), routing.getMaximumOutletsPerTile(),
routing.getMaximumCoastalOutletsPerTile() routing.getMaximumCoastalOutletsPerTile(),
coastal.getCliffSlopeFactor()
); );
HydrologyPlannerSettings.Geometry plannerGeometry = geometry( HydrologyPlannerSettings.Geometry plannerGeometry = geometry(
rivers.getGeometry(), rivers.getGeometry(),
new HydrologyPlannerSettings.ChannelShape( surfaceShape(rivers.getGeometry().getSurface(), channel)
2D,
channel.getRoughness(),
channel.getRoughness(),
channel.getRoughnessWavelength()
)
); );
return new HydrologyPlannerSettings( return new HydrologyPlannerSettings(
dimension.getFluidHeight(), 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 * The widest shore band any policy of the dimension asks for, whether as a shore biome band or as a
* width, so the publication envelope reaches every column a river can give shore content to. * 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) { static double widestShoreBiomeWidth(IrisDimension dimension, DataProvider data, double shoreWidth) {
double widest = Math.max(0D, 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)) { 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)) { for (IrisBiome biome : dimension.getAllBiomes(data)) {
widest = Math.max(widest, policyShoreBiomeWidth(biome.getRiverPolicy())); widest = Math.max(widest, policyShoreReach(biome.getRiverPolicy()));
} }
return widest; return widest;
} }
private static double policyShoreBiomeWidth(IrisRiverPolicy policy) { private static double policyShoreReach(IrisRiverPolicy policy) {
return policy == null || policy.getShoreBiomeWidth() == null ? 0D : policy.getShoreBiomeWidth(); 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) { private static List<HydrologyPlannerSettings.SurfacePool> surfacePools(List<IrisSurfacePoolConfig> configurations) {
@@ -940,7 +976,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
drops.getFlowWidthRatio(), drops.getFlowWidthRatio(),
drops.getMaximumFlowDepth(), drops.getMaximumFlowDepth(),
drops.getBasinWidthRatio(), drops.getBasinWidthRatio(),
drops.getMaximumBasinDepth() drops.getMaximumBasinDepth(),
drops.getUndergroundCascadeRunPerBlock()
) )
); );
} }
@@ -950,7 +987,36 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
shape.getBedRoundness(), shape.getBedRoundness(),
shape.getBedRoughness(), shape.getBedRoughness(),
shape.getWallRoughness(), 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.getMaximumPerTile(),
seaCaves.getMinimumSpacing(), seaCaves.getMinimumSpacing(),
seaCaves.getMinimumCoastHeight(), seaCaves.getMinimumCoastHeight(),
seaCaves.getDepth() seaCaves.getDepth(),
seaCaves.getSweepJitterDegrees()
); );
} }
@@ -8,8 +8,6 @@ import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import java.util.Objects; import java.util.Objects;
public final class ChannelProfileBuilder { public final class ChannelProfileBuilder {
private static final int SMOOTHING_RADIUS = 16;
private final HydrologyPlannerSettings.Surface surface; private final HydrologyPlannerSettings.Surface surface;
private final HydrologyTerrainSampler sampler; private final HydrologyTerrainSampler sampler;
private final HydrologyGeometrySampler geometry; private final HydrologyGeometrySampler geometry;
@@ -65,17 +63,19 @@ public final class ChannelProfileBuilder {
width[station] = clamp(sampledWidth * widthMultiplier, surface.minimumWidth(), surface.maximumWidth() * 2D); width[station] = clamp(sampledWidth * widthMultiplier, surface.minimumWidth(), surface.maximumWidth() * 2D);
depth[station] = clamp(sampledDepth * depthMultiplier, 1D, surface.maximumDepth() * 2D); depth[station] = clamp(sampledDepth * depthMultiplier, 1D, surface.maximumDepth() * 2D);
} }
double[] smoothWidth = smooth(width); HydrologyPlannerSettings.Channel channel = surface.banks().channel();
double[] smoothDepth = smooth(depth); double[] smoothWidth = smooth(width, channel.smoothingRadius());
// The headwater opens as a spring pool: wider and one block deeper, narrowing to the cruise width. double[] smoothDepth = smooth(depth, channel.smoothingRadius());
// Ground that falls away across the pool shrinks it so it never demands a cut the valley solver rejects. // 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); int spring = Math.min(surface.banks().springLength(), count / 2);
double springRatio = surface.banks().springWidthRatio(); double springRatio = surface.banks().springWidthRatio();
for (int station = 0; station < spring; station++) { for (int station = 0; station < spring; station++) {
double remaining = 1D - SurfaceNoise.smoothStep(station / (double) spring); double remaining = 1D - SurfaceNoise.smoothStep(station / (double) spring);
double localRatio = 1D + (springRatio - 1D) * springRoom(centerline, station, smoothWidth[station] * springRatio / 2D); double localRatio = 1D + (springRatio - 1D) * springRoom(centerline, station, smoothWidth[station] * springRatio / 2D);
smoothWidth[station] *= 1D + (localRatio - 1D) * remaining; 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 // 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 // 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)); 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; int count = values.length;
if (radius <= 0) {
return values.clone();
}
double[] smoothed = new double[count]; double[] smoothed = new double[count];
for (int station = 0; station < count; station++) { for (int station = 0; station < count; station++) {
double total = 0D; double total = 0D;
double weight = 0D; double weight = 0D;
for (int offset = -SMOOTHING_RADIUS; offset <= SMOOTHING_RADIUS; offset++) { for (int offset = -radius; offset <= radius; offset++) {
int index = station + offset; int index = station + offset;
if (index < 0 || index >= count) { if (index < 0 || index >= count) {
continue; continue;
} }
double factor = SMOOTHING_RADIUS + 1 - Math.abs(offset); double factor = radius + 1 - Math.abs(offset);
total += values[index] * factor; total += values[index] * factor;
weight += 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.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler; import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import art.arcane.iris.engine.hydrology.RiverFootprint; 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.Long2DoubleOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
@@ -62,9 +64,12 @@ public final class ErosionFieldCompiler {
) { ) {
HydrologyPlannerSettings.Banks banks = surface.banks(); HydrologyPlannerSettings.Banks banks = surface.banks();
HydrologyPlannerSettings.Erosion erosion = banks.erosion(); 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(); int count = terminal == SurfaceTerminal.OCEAN_MOUTH ? centerline.size() : valley.exposedStations();
double roughness = banks.roughness(); double roughness = banks.roughness();
double shoreWidth = surface.shoreWidth(); double shoreWidth = surface.shoreWidth();
double shoreRise = erosion.shoreRise();
int sink = banks.sink(); int sink = banks.sink();
boolean[] basin = basins(valley, channel, count); boolean[] basin = basins(valley, channel, count);
double[][] blendWidth = blendWidths(centerline, channel, valley, 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 // 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. // eroded valley; the search radius has to reach it so those columns get a shore role.
double stationBand = stationShoreBiomeBand(centerline, station, shoreWidth); 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 stationX = centerline.x()[station];
int stationZ = centerline.z()[station]; int stationZ = centerline.z()[station];
for (int deltaZ = -radius; deltaZ <= radius; deltaZ++) { for (int deltaZ = -radius; deltaZ <= radius; deltaZ++) {
@@ -113,7 +119,9 @@ public final class ErosionFieldCompiler {
double halfWidth = channel.halfWidth()[station]; double halfWidth = channel.halfWidth()[station];
double outline = halfWidth * (1D + roughness * SurfaceNoise.signed( double outline = halfWidth * (1D + roughness * SurfaceNoise.signed(
courseSeed ^ OUTLINE_SALT, cellX, cellZ, banks.roughnessWavelength())); 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; boolean wet = cellDistance <= outline + 0.25D;
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) { if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
if (terrain != null && wet && terminal == SurfaceTerminal.OCEAN_MOUTH if (terrain != null && wet && terminal == SurfaceTerminal.OCEAN_MOUTH
@@ -129,28 +137,32 @@ public final class ErosionFieldCompiler {
if (wet) { if (wet) {
double normalized = Math.min(1D, cellDistance / Math.max(0.5D, outline)); double normalized = Math.min(1D, cellDistance / Math.max(0.5D, outline));
double depth = channel.depth()[station]; 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()) + 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))); 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)); columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.CHANNEL, bed, head, false));
wetKeys.add(key); wetKeys.add(key);
continue; continue;
} }
int bankTop = head + sink; 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 // 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 // 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 // the eroded valley, so a wide band reaches over untouched ground and a zero band leaves the
// geometric shore with the bank biome. // geometric shore with the bank biome.
boolean shoreBiome = cellDistance <= outline + terrain.shoreBiomeWidth(shoreWidth) + 0.25D; boolean shoreBiome = cellDistance <= outline + terrain.shoreBiomeWidth(shoreWidth) + 0.25D;
SurfaceRole dryRole = shoreBiome ? SurfaceRole.SHORE : SurfaceRole.BANK; SurfaceRole dryRole = shoreBiome ? SurfaceRole.SHORE : SurfaceRole.BANK;
if (cellDistance <= outline + shoreWidth + 0.25D) { if (cellDistance <= outline + shore + 0.25D) {
int height = Math.min(natural, bankTop); 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)); columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
continue; continue;
} }
int cut = natural - bankTop; int shoreTop = bankTop + (int) StrictMath.round(shoreRise);
if (cut <= 0 || !erosion.enabled()) { int cut = natural - shoreTop;
if (cut <= 0 || !erosion.enabled() || !terrain.erosion()) {
if (shoreBiome) { if (shoreBiome) {
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false)); 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 side = side(centerline, station, cellX, cellZ);
double width = blendWidth[station][side < 0D ? 0 : 1]; double width = blendWidth[station][side < 0D ? 0 : 1];
double progress = (cellDistance - outline - shoreWidth) / width; double progress = (cellDistance - outline - shore) / width;
if (progress >= 1D) { if (progress >= 1D) {
if (shoreBiome) { if (shoreBiome) {
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false)); columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
} }
continue; 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)); columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
} }
if (count > 0) { if (count > 0) {
@@ -200,17 +212,24 @@ public final class ErosionFieldCompiler {
} }
HydrologyPlannerSettings.Banks banks = surface.banks(); HydrologyPlannerSettings.Banks banks = surface.banks();
HydrologyPlannerSettings.Erosion erosion = banks.erosion(); HydrologyPlannerSettings.Erosion erosion = banks.erosion();
HydrologyPlannerSettings.Channel channelSettings = banks.channel();
double roughness = banks.roughness(); double roughness = banks.roughness();
double shoreWidth = surface.shoreWidth(); double shoreWidth = surface.shoreWidth();
double shoreRise = erosion.shoreRise();
int sink = banks.sink(); int sink = banks.sink();
int centreX = centerline.x()[station]; int centreX = centerline.x()[station];
int centreZ = centerline.z()[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); int radius = pondRadius(seed, pond, centreX, centreZ, head + sink, roughness);
if (radius < 1) { if (radius < 1) {
return; return;
} }
int bankTop = head + sink; 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 deltaZ = -reach; deltaZ <= reach; deltaZ++) {
for (int deltaX = -reach; deltaX <= reach; deltaX++) { for (int deltaX = -reach; deltaX <= reach; deltaX++) {
double distance = Math.sqrt((double) deltaX * deltaX + (double) deltaZ * deltaZ); 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( double outline = radius * (1D + roughness * SurfaceNoise.signed(
seed ^ OUTLINE_SALT, cellX, cellZ, banks.roughnessWavelength())); 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) { if (distance <= outline + 0.25D) {
double normalized = Math.min(1D, distance / Math.max(0.5D, outline)); 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()); + 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))); int bed = Math.min(natural, head - Math.max(1, (int) StrictMath.round(local)));
boolean wasWet = existing != null && existing.role() == SurfaceRole.CHANNEL; boolean wasWet = existing != null && existing.role() == SurfaceRole.CHANNEL;
@@ -250,21 +271,22 @@ public final class ErosionFieldCompiler {
if (existing != null && existing.role() == SurfaceRole.CHANNEL) { if (existing != null && existing.role() == SurfaceRole.CHANNEL) {
continue; continue;
} }
if (distance <= outline + shoreWidth + 0.25D) { if (distance <= outline + shore + 0.25D) {
int height = Math.min(natural, bankTop); 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)); columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, height, height, false));
continue; continue;
} }
int cut = natural - bankTop; int cut = natural - shoreTop;
if (cut <= 0 || !erosion.enabled()) { if (cut <= 0 || !erosion.enabled() || !erode) {
continue; continue;
} }
double width = Math.max(banks.minimumBlendWidth(), Math.min(banks.maximumBlendWidth(), cut * banks.blendSlope())); double width = Math.max(banks.minimumBlendWidth(),
double progress = (distance - outline - shoreWidth) / width; Math.min(banks.maximumBlendWidth(), cut * banks.blendSlope() + erosion.blendBaseWidth()));
double progress = (distance - outline - shore) / width;
if (progress >= 1D) { if (progress >= 1D) {
continue; 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) { if (existing != null && existing.height() <= height) {
continue; continue;
} }
@@ -308,6 +330,60 @@ public final class ErosionFieldCompiler {
return SurfaceNoise.smoothStep(Math.pow(progress, curve)); 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 * 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. * 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); 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) { private static void connectSteps(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys) {
for (int index = 0; index < wetKeys.size(); index++) { for (int index = 0; index < wetKeys.size(); index++) {
long key = wetKeys.getLong(index); long key = wetKeys.getLong(index);
@@ -412,13 +494,14 @@ public final class ErosionFieldCompiler {
} }
private boolean[] basins(ValleyProfile valley, ChannelProfile channel, int count) { private boolean[] basins(ValleyProfile valley, ChannelProfile channel, int count) {
HydrologyPlannerSettings.Flow flow = surface.banks().flow();
boolean[] basin = new boolean[count]; boolean[] basin = new boolean[count];
for (int station = 1; station < count; station++) { for (int station = 1; station < count; station++) {
int drop = valley.head()[station - 1] - valley.head()[station]; int drop = valley.head()[station - 1] - valley.head()[station];
if (drop < 2) { if (drop < flow.plungeBasinMinimumDrop()) {
continue; 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++) { for (int following = station; following < Math.min(count, station + length); following++) {
basin[following] = true; basin[following] = true;
} }
@@ -428,13 +511,14 @@ public final class ErosionFieldCompiler {
private double[][] blendWidths(SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley, int count) { private double[][] blendWidths(SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley, int count) {
HydrologyPlannerSettings.Banks banks = surface.banks(); HydrologyPlannerSettings.Banks banks = surface.banks();
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
double[][] widths = new double[count][2]; double[][] widths = new double[count][2];
if (!banks.erosion().enabled()) { if (!erosion.enabled()) {
return widths; return widths;
} }
for (int station = 0; station < count; station++) { for (int station = 0; station < count; station++) {
double halfWidth = channel.halfWidth()[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(); int bankTop = valley.head()[station] + banks.sink();
for (int side = 0; side < 2; side++) { for (int side = 0; side < 2; side++) {
double direction = side == 0 ? -1D : 1D; 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); int probeZ = (int) StrictMath.round(centerline.z()[station] + centerline.normalZ(station) * probe * direction);
HydrologyTerrainSample terrain = sampler.sample(probeX, probeZ); HydrologyTerrainSample terrain = sampler.sample(probeX, probeZ);
double cut = terrain == null ? 0D : Math.max(0D, terrain.naturalHeight() - bankTop); 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)); 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 // 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; // 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 // a rise the cap cannot pass ends it there. Above it the approach grades down the inlet ramp
// station into the estuary wherever that cut fits, instead of dropping in a wall at the coast. // slope per station into the estuary wherever that cut fits, instead of dropping in a wall at
// The drowned reach is the end of a river, never most of one: it takes at most half the exposed // the coast. The drowned reach is the end of a river, never most of one: it takes at most the
// course and the approach half of that again, so the headwater keeps its natural head. // 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. // Every head set here is at or below the terrain-supported head, so the course stays non-rising.
HydrologyPlannerSettings.Inlet inlet = surface.banks().inlet(); HydrologyPlannerSettings.Inlet inlet = surface.banks().inlet();
int channelIncision = surface.maximumIncision(); int channelIncision = surface.maximumIncision();
int inletIncision = Math.max(channelIncision, inlet.maximumIncision()); int inletIncision = Math.max(channelIncision, inlet.maximumIncision());
int rampStart = exposed; int rampStart = exposed;
if (terminal == SurfaceTerminal.OCEAN_MOUTH && inlet.length() > 0) { 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; int inletStart = exposed;
while (inletStart > 0 && exposed - inletStart < reach while (inletStart > 0 && exposed - inletStart < reach
&& cutFits(inletStart - 1, seaLevel, channel, centerNatural, incisionMultiplier, inletIncision)) { && 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, // 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. // otherwise a station lowered further upstream would sit below one left at its natural head.
for (int station = inletStart - 1; station >= rampStart; station--) { 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) { if (head[station] <= target) {
continue; continue;
} }
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.modifier; package art.arcane.iris.engine.modifier;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.UpperDimensionContext; import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.actuator.IrisDecorantActuator; import art.arcane.iris.engine.actuator.IrisDecorantActuator;
import art.arcane.iris.engine.framework.Engine; 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.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; 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.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisRiverMaterialConfig;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.context.ChunkContext;
@@ -690,6 +693,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (floorBiome != null) { if (floorBiome != null) {
HydrologyCaveCell floorHydrology = dataIfPresent( HydrologyCaveCell floorHydrology = dataIfPresent(
mantleChunk, rx, zoneFloor, rz, HydrologyCaveCell.class); mantleChunk, rx, zoneFloor, rz, HydrologyCaveCell.class);
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial();
KList<PlatformBlockState> floorLayers = floorBiome.generateLayers( KList<PlatformBlockState> floorLayers = floorBiome.generateLayers(
getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex()); getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex());
for (int i = 0; i < zoneFloor - 1; i++) { 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 existing = output.getRaw(rx, floorY, rz);
PlatformBlockState layer = resolveSubmergedCaveFloorLayer( 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) if (!B.isSolid(existing)
|| !canReplaceHydrologyGuard(hydrology, layer, false) || !canReplaceHydrologyGuard(hydrology, layer, false)
|| !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) { || !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
@@ -804,6 +812,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (floorBiome != null) { if (floorBiome != null) {
HydrologyCaveCell floorHydrology = dataIfPresent( HydrologyCaveCell floorHydrology = dataIfPresent(
mc, rx, zone.floor, rz, HydrologyCaveCell.class); mc, rx, zone.floor, rz, HydrologyCaveCell.class);
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial();
KList<PlatformBlockState> floorBlocks = floorBiome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex()); KList<PlatformBlockState> floorBlocks = floorBiome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex());
for (int i = 0; i < zone.floor - 1; i++) { for (int i = 0; i < zone.floor - 1; i++) {
if (!floorBlocks.hasIndex(i)) { if (!floorBlocks.hasIndex(i)) {
@@ -817,7 +826,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
continue; continue;
} }
PlatformBlockState block = resolveSubmergedCaveFloorLayer( 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); PlatformBlockState existing = output.getRaw(rx, y, rz);
if (!B.isSolid(existing) if (!B.isSolid(existing)
|| !canReplaceHydrologyGuard(hydrology, block, false) || !canReplaceHydrologyGuard(hydrology, block, false)
@@ -925,6 +938,44 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState); 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( static boolean canReplaceHydrologyGuard(
HydrologyCaveCell hydrology, HydrologyCaveCell hydrology,
PlatformBlockState layer, PlatformBlockState layer,
@@ -229,9 +229,21 @@ public class IrisBlockData extends IrisRegistrant {
return data.getBlockLoader() != null && data.getBlockLoader().load(getBlock(), false) != null; 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) { 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(); return resolution == null ? null : resolution.state();
} }
@@ -38,6 +38,16 @@ public class IrisCoastalRiverGrottoConfig {
@Desc("Maximum accepted coastal grotto volume in blocks.") @Desc("Maximum accepted coastal grotto volume in blocks.")
private int maximumVolume = 8192; 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.") @Desc("Standalone sea caves: coastal grottos opening from the ocean into the coast without a river.")
private IrisSeaCaveConfig seaCaves = new IrisSeaCaveConfig(); 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) @MaxNumber(128)
@Desc("Wavelength in blocks of bed and wall roughness.") @Desc("Wavelength in blocks of bed and wall roughness.")
private int roughnessWavelength = 11; 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) @MaxNumber(32)
@Desc("Maximum receiving-basin depth after drop-scaled erosion.") @Desc("Maximum receiving-basin depth after drop-scaled erosion.")
private int maximumBasinDepth = 8; 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.") @Desc("Surface and underground centerline meanders.")
private IrisRiverMeanderConfig meanders = new IrisRiverMeanderConfig(); 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.") @Desc("Underground channel bed and wall shape.")
private IrisRiverChannelShapeConfig underground = new IrisRiverChannelShapeConfig(); 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) @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.") @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; 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.") @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; 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.") @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; private Boolean confined = null;
@@ -34,4 +34,9 @@ public class IrisSeaCaveConfig {
@MaxNumber(128) @MaxNumber(128)
@Desc("How far inland from the shoreline the chamber is swept. 0 leaves a single chamber at the shore.") @Desc("How far inland from the shoreline the chamber is swept. 0 leaves a single chamber at the shore.")
private int depth = 12; 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.") @Desc("Controls the shore band and the eroded valley around a surface river.")
@Data @Data
public class IrisSurfaceRiverBankConfig { public class IrisSurfaceRiverBankConfig {
@MinNumber(0.5) @MinNumber(0)
@MaxNumber(6) @MaxNumber(16)
@Desc("Width in blocks of the shore band that receives shore biome content.") @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; private double shoreWidth = 1.5D;
@MinNumber(0.5) @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.") @Desc("Show the biome's deeper layers on eroded banks instead of the surface layer.")
private boolean exposeCutStrata = true; 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.") @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"); 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.") @Desc("Blocks over which the spring pool narrows back to the channel width.")
private int springLength = 24; 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) { private static IrisStyledRange range(double min, double max, double zoom) {
IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(zoom); IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(zoom);
return new IrisStyledRange(min, max, style); return new IrisStyledRange(min, max, style);
@@ -34,4 +34,20 @@ public class IrisSurfaceRiverErosionConfig {
@MaxNumber(2) @MaxNumber(2)
@Desc("Share of the channel roughness applied to the bed as depth variation; 0 leaves a smooth bed.") @Desc("Share of the channel roughness applied to the bed as depth variation; 0 leaves a smooth bed.")
private double bedNoise = 0.5D; 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) @MaxNumber(32)
@Desc("Smallest natural cliff in blocks that becomes a waterfall instead of a cascade.") @Desc("Smallest natural cliff in blocks that becomes a waterfall instead of a cascade.")
private int waterfallMinimumDrop = 6; 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.") @Desc("Extra underground courses an outlet may accept as tributaries joining its main passage; they are budgeted on top of the source density, and 0 keeps one passage per outlet.")
private int tributaries = 1; private 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) { private static IrisStyledRange range(double min, double max, double zoom) {
return new IrisStyledRange(min, max, new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(zoom)); return new IrisStyledRange(min, max, new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(zoom));
} }
@@ -40,7 +40,9 @@ public class PackHydrologyValidatorTest {
"riverPolicy": { "riverPolicy": {
"routing": "PREFER", "routing": "PREFER",
"outletAdmission": null, "outletAdmission": null,
"shoreBiomes": ["river_shore"] "shoreBiomes": ["river_shore"],
"shoreWidth": null,
"erosion": null
} }
} }
"""); """);
@@ -85,6 +87,60 @@ public class PackHydrologyValidatorTest {
"slopePenalty": 0, "slopePenalty": 0,
"confluenceAttraction": 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": { "surface": {
"channel": { "channel": {
"width": {"min": 1, "max": 1}, "width": {"min": 1, "max": 1},
@@ -92,29 +148,78 @@ public class PackHydrologyValidatorTest {
"sink": 0, "sink": 0,
"maximumIncision": 1, "maximumIncision": 1,
"roughness": 0, "roughness": 0,
"roughnessWavelength": 4 "roughnessWavelength": 4,
"springWidthRatio": 1,
"springLength": 4,
"smoothingRadius": 0,
"outlineMinimumRatio": 0.2,
"outlineMaximumRatio": 1,
"springExtraDepth": 0
}, },
"banks": { "banks": {
"shoreWidth": 0.5, "shoreWidth": 0,
"blendSlope": 0.5, "blendSlope": 0.5,
"minimumBlendWidth": 1, "minimumBlendWidth": 1,
"maximumBlendWidth": 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}, "bed": {
"mouths": {"flareRatio": 1, "maximumOceanApron": 0} "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": { "grottos": {
"coastal": { "coastal": {
"horizontalRadius": 8, "horizontalRadius": 8,
"verticalRadius": 1, "verticalRadius": 1,
"headroom": 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); PackHydrologyValidator.Validation result = validate(pack);
@@ -131,16 +236,34 @@ public class PackHydrologyValidatorTest {
"bed": { "bed": {
"allowGravityBlocks": false, "allowGravityBlocks": false,
"padding": 9, "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 result = validate(pack);
PackHydrologyValidator.Validation shallowResult = validate(shallow);
assertContains(result.errors(), "padding must be at most 8"); assertContains(result.errors(), "padding must be at most 8");
assertContains(result.errors(), "paddingPalette.palette[0].block must not be a fluid"); 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 @Test
@@ -262,6 +385,60 @@ public class PackHydrologyValidatorTest {
"slopePenalty": 16, "slopePenalty": 16,
"confluenceAttraction": 1 "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": { "surface": {
"channel": { "channel": {
"width": {"min": 128, "max": 128}, "width": {"min": 128, "max": 128},
@@ -269,27 +446,76 @@ public class PackHydrologyValidatorTest {
"sink": 3, "sink": 3,
"maximumIncision": 32, "maximumIncision": 32,
"roughness": 1, "roughness": 1,
"roughnessWavelength": 64 "roughnessWavelength": 64,
"springWidthRatio": 4,
"springLength": 96,
"smoothingRadius": 64,
"outlineMinimumRatio": 1,
"outlineMaximumRatio": 3,
"springExtraDepth": 8
}, },
"banks": { "banks": {
"shoreWidth": 6, "shoreWidth": 16,
"blendSlope": 12, "blendSlope": 12,
"minimumBlendWidth": 64, "minimumBlendWidth": 64,
"maximumBlendWidth": 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}, "bed": {
"mouths": {"flareRatio": 4, "maximumOceanApron": 32} "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": { "grottos": {
"coastal": { "coastal": {
"horizontalRadius": 128, "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); PackHydrologyValidator.Validation result = validate(pack);
@@ -372,7 +598,11 @@ public class PackHydrologyValidatorTest {
"roughness": 1.5, "roughness": 1.5,
"roughnessWavelength": 65, "roughnessWavelength": 65,
"springWidthRatio": 5, "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, "roughness": -0.1,
"roughnessWavelength": 3, "roughnessWavelength": 3,
"springWidthRatio": 0.5, "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(), "roughnessWavelength must be at most 64");
assertContains(aboveResult.errors(), "springWidthRatio must be at most 4.0"); assertContains(aboveResult.errors(), "springWidthRatio must be at most 4.0");
assertContains(aboveResult.errors(), "springLength must be at most 96"); 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(), "springWidthRatio must be at least 1.0");
assertContains(belowResult.errors(), "springLength must be at least 4"); assertContains(belowResult.errors(), "springLength must be at least 4");
assertContains(belowResult.errors(), "width.min must be at least 1.0"); assertContains(belowResult.errors(), "width.min must be at least 1.0");
@@ -422,7 +664,7 @@ public class PackHydrologyValidatorTest {
{ {
"enabled": true, "enabled": true,
"surface": { "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}} "ponds": {"source": {"minimumRadius": 33, "maximumRadius": 33, "depth": 9}, "terminal": {"minimumRadius": 6, "maximumRadius": 5, "depth": 1}}
} }
} }
@@ -431,20 +673,33 @@ public class PackHydrologyValidatorTest {
{ {
"enabled": true, "enabled": true,
"surface": { "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}} "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 aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below); PackHydrologyValidator.Validation belowResult = validate(below);
PackHydrologyValidator.Validation everyStyleResult = validate(everyStyle);
assertContains(aboveResult.errors(), "enabled must be a boolean"); assertContains(aboveResult.errors(), "enabled must be a boolean");
assertContains(aboveResult.errors(), "smoothingRadius must be at most 64"); assertContains(aboveResult.errors(), "smoothingRadius must be at most 64");
assertContains(aboveResult.errors(), "thalwegFraction must be at most 0.95"); assertContains(aboveResult.errors(), "thalwegFraction must be at most 0.95");
assertContains(aboveResult.errors(), "blendCurve must be at most 4.0"); assertContains(aboveResult.errors(), "blendCurve must be at most 4.0");
assertContains(aboveResult.errors(), "bedNoise must be at most 2.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(), "minimumRadius must be at most 32");
assertContains(aboveResult.errors(), "depth must be at most 8"); assertContains(aboveResult.errors(), "depth must be at most 8");
assertContains(aboveResult.errors(), "ponds.terminal.minimumRadius must not exceed maximumRadius"); 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(), "thalwegFraction must be at least 0.0");
assertContains(belowResult.errors(), "blendCurve must be at least 0.25"); assertContains(belowResult.errors(), "blendCurve must be at least 0.25");
assertContains(belowResult.errors(), "bedNoise must be at least 0.0"); 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(), "minimumRadius must be at least 1");
assertContains(belowResult.errors(), "depth 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 @Test
@@ -463,11 +724,15 @@ public class PackHydrologyValidatorTest {
"enabled": true, "enabled": true,
"surface": { "surface": {
"banks": { "banks": {
"shoreWidth": 6.5, "shoreWidth": 16.5,
"blendSlope": 12.5, "blendSlope": 12.5,
"minimumBlendWidth": 40, "minimumBlendWidth": 40,
"maximumBlendWidth": 65, "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, "enabled": true,
"surface": { "surface": {
"banks": { "banks": {
"shoreWidth": 0.4, "shoreWidth": -0.1,
"blendSlope": 0.4, "blendSlope": 0.4,
"minimumBlendWidth": 0, "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 aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below); PackHydrologyValidator.Validation belowResult = validate(below);
PackHydrologyValidator.Validation unorderedResult = validate(unordered); 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(), "blendSlope must be at most 12.0");
assertContains(aboveResult.errors(), "maximumBlendWidth must be at most 64"); assertContains(aboveResult.errors(), "maximumBlendWidth must be at most 64");
assertContains(aboveResult.errors(), "exposeCutStrata must be a boolean"); 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(), "blendSlope must be at least 0.5");
assertContains(belowResult.errors(), "minimumBlendWidth must be at least 1"); 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"); assertContains(unorderedResult.errors(), "minimumBlendWidth must not exceed maximumBlendWidth");
assertTrue(widenedResult.errors().toString(),
widenedResult.errors().stream().noneMatch(error -> error.contains(".banks")));
} }
@Test @Test
@@ -514,8 +806,8 @@ public class PackHydrologyValidatorTest {
{ {
"enabled": true, "enabled": true,
"surface": { "surface": {
"flow": {"cascadeRun": 9, "waterfallMinimumDrop": 33}, "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} "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, "enabled": true,
"surface": { "surface": {
"flow": {"cascadeRun": 0, "waterfallMinimumDrop": 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} "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(), "inletLength must be at most 256");
assertContains(aboveResult.errors(), "inletDepth must be at most 16"); assertContains(aboveResult.errors(), "inletDepth must be at most 16");
assertContains(aboveResult.errors(), "mouths.maximumIncision must be at most 128"); 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(), "cascadeRun must be at least 1");
assertContains(belowResult.errors(), "waterfallMinimumDrop must be at least 2"); assertContains(belowResult.errors(), "waterfallMinimumDrop must be at least 2");
assertContains(belowResult.errors(), "flareRatio must be at least 1.0"); 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(), "inletLength must be at least 0");
assertContains(belowResult.errors(), "inletDepth 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(), "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"); assertContains(longInletResult.errors(), "mouths.inletLength must not exceed routing.minimumSurfaceCourseLength");
assertTrue(fittingInletResult.errors().toString(), assertTrue(fittingInletResult.errors().toString(),
fittingInletResult.errors().stream().noneMatch(error -> error.contains("inletLength"))); fittingInletResult.errors().stream().noneMatch(error -> error.contains("inletLength")));
} }
@Test @Test
public void rejectsUndergroundMouthLevelingDistanceOutsideItsBounds() throws Exception { public void rejectsUndergroundValuesOutsideTheirBounds() throws Exception {
File above = pack(riversDimension(""" File above = pack(riversDimension("""
{ {
"enabled": true, "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(""" File below = pack(riversDimension("""
{ {
"enabled": true, "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); PackHydrologyValidator.Validation belowResult = validate(below);
assertContains(aboveResult.errors(), "mouthLevelingDistance must be at most 512"); 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(), "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 @Test
@@ -762,13 +1185,17 @@ public class PackHydrologyValidatorTest {
"depthMultiplier": 17, "depthMultiplier": 17,
"incisionMultiplier": -1, "incisionMultiplier": -1,
"routingMultiplier": -1, "routingMultiplier": -1,
"bankMultiplier": 4.5 "bankMultiplier": 4.5,
"shoreWidth": 16.5,
"erosion": "no"
} }
} }
"""); """);
PackHydrologyValidator.Validation result = validate(pack); 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(), ".placement must be one of");
assertContains(result.errors(), ".routing must be one of"); assertContains(result.errors(), ".routing must be one of");
assertContains(result.errors(), ".outletAdmission must be a boolean or null"); assertContains(result.errors(), ".outletAdmission must be a boolean or null");
@@ -795,7 +1222,8 @@ public class PackHydrologyValidatorTest {
}, },
"riverPolicy": { "riverPolicy": {
"bankBiomes": [17], "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(), "bankBiomes[0] must name a biome resource");
assertContains(result.errors(), "bankMultiplier must be at least 0.0"); assertContains(result.errors(), "bankMultiplier must be at least 0.0");
assertContains(result.errors(), "riverPolicy.shoreWidth must be at least 0.0");
} }
@Test @Test
@@ -1244,7 +1673,9 @@ public class PackHydrologyValidatorTest {
"grottos": { "grottos": {
"coastal": { "coastal": {
"horizontalRadius": 12, "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": { "grottos": {
"coastal": { "coastal": {
"horizontalRadius": 12, "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": { "grottos": {
"coastal": { "coastal": {
"horizontalRadius": 12, "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 belowResult = validate(below);
PackHydrologyValidator.Validation canonicalResult = validate(canonical); 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.maximumPerTile must be at most 64");
assertContains(aboveResult.errors(), "seaCaves.minimumSpacing must be at most 8192"); assertContains(aboveResult.errors(), "seaCaves.minimumSpacing must be at most 8192");
assertContains(aboveResult.errors(), "seaCaves.minimumCoastHeight must be at most 128"); assertContains(aboveResult.errors(), "seaCaves.minimumCoastHeight must be at most 128");
assertContains(aboveResult.errors(), "seaCaves.depth 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.enabled must be a boolean");
assertContains(belowResult.errors(), "seaCaves.maximumPerTile must be at least 0"); assertContains(belowResult.errors(), "seaCaves.maximumPerTile must be at least 0");
assertContains(belowResult.errors(), "seaCaves.minimumSpacing must be at least twice"); assertContains(belowResult.errors(), "seaCaves.minimumSpacing must be at least twice");
assertContains(belowResult.errors(), "seaCaves.minimumCoastHeight must be at least 1"); assertContains(belowResult.errors(), "seaCaves.minimumCoastHeight must be at least 1");
assertContains(belowResult.errors(), "seaCaves.depth must be at least 0"); 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()); assertTrue(canonicalResult.errors().toString(), canonicalResult.errors().isEmpty());
} }
@@ -85,7 +85,27 @@ public class IrisRiverSchemaTest {
assertFalse(channel.has("surfaceInset")); assertFalse(channel.has("surfaceInset"));
assertFalse(channel.has("terrainBlendWidth")); assertFalse(channel.has("terrainBlendWidth"));
assertFalse(mouths.has("levelingDistance")); 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(8192, routing.getJSONObject("tileSize").getInt("maximum"));
assertEquals(32768, routing.getJSONObject("maximumRouteLength").getInt("maximum")); assertEquals(32768, routing.getJSONObject("maximumRouteLength").getInt("maximum"));
@@ -118,6 +138,14 @@ public class IrisRiverSchemaTest {
assertEquals(32, channel.getJSONObject("maximumIncision").getInt("maximum")); assertEquals(32, channel.getJSONObject("maximumIncision").getInt("maximum"));
assertEquals(1D, channel.getJSONObject("roughness").getDouble("maximum"), 0D); assertEquals(1D, channel.getJSONObject("roughness").getDouble("maximum"), 0D);
assertEquals(64, channel.getJSONObject("roughnessWavelength").getInt("maximum")); 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")); assertFalse(banks.has("freeboard"));
JSONObject erosion = referencedProperties(definitions, surface.getJSONObject("erosion")); JSONObject erosion = referencedProperties(definitions, surface.getJSONObject("erosion"));
assertEquals("boolean", erosion.getJSONObject("enabled").getString("type")); 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.95D, erosion.getJSONObject("thalwegFraction").getDouble("maximum"), 0D);
assertEquals(0.25D, erosion.getJSONObject("blendCurve").getDouble("minimum"), 0D); assertEquals(0.25D, erosion.getJSONObject("blendCurve").getDouble("minimum"), 0D);
assertEquals(2D, erosion.getJSONObject("bedNoise").getDouble("maximum"), 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 ponds = referencedProperties(definitions, surface.getJSONObject("ponds"));
JSONObject sourcePond = referencedProperties(definitions, ponds.getJSONObject("source")); JSONObject sourcePond = referencedProperties(definitions, ponds.getJSONObject("source"));
JSONObject terminalPond = referencedProperties(definitions, ponds.getJSONObject("terminal")); JSONObject terminalPond = referencedProperties(definitions, ponds.getJSONObject("terminal"));
assertEquals("boolean", sourcePond.getJSONObject("enabled").getString("type")); assertEquals("boolean", sourcePond.getJSONObject("enabled").getString("type"));
assertEquals(32, sourcePond.getJSONObject("maximumRadius").getInt("maximum")); assertEquals(32, sourcePond.getJSONObject("maximumRadius").getInt("maximum"));
assertEquals(8, terminalPond.getJSONObject("depth").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(0.5D, banks.getJSONObject("blendSlope").getDouble("minimum"), 0D);
assertEquals(12D, banks.getJSONObject("blendSlope").getDouble("maximum"), 0D); assertEquals(12D, banks.getJSONObject("blendSlope").getDouble("maximum"), 0D);
assertEquals(64, banks.getJSONObject("minimumBlendWidth").getInt("maximum")); assertEquals(64, banks.getJSONObject("minimumBlendWidth").getInt("maximum"));
assertEquals(64, banks.getJSONObject("maximumBlendWidth").getInt("maximum")); assertEquals(64, banks.getJSONObject("maximumBlendWidth").getInt("maximum"));
assertEquals("boolean", banks.getJSONObject("exposeCutStrata").getString("type")); 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(1, flow.getJSONObject("cascadeRun").getInt("minimum"));
assertEquals(8, flow.getJSONObject("cascadeRun").getInt("maximum")); assertEquals(8, flow.getJSONObject("cascadeRun").getInt("maximum"));
assertEquals(2, flow.getJSONObject("waterfallMinimumDrop").getInt("minimum")); assertEquals(2, flow.getJSONObject("waterfallMinimumDrop").getInt("minimum"));
assertEquals(32, flow.getJSONObject("waterfallMinimumDrop").getInt("maximum")); 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(1D, mouths.getJSONObject("flareRatio").getDouble("minimum"), 0D);
assertEquals(32, mouths.getJSONObject("maximumOceanApron").getInt("maximum")); 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(16, underground.getJSONObject("mouthLevelingDistance").getInt("minimum"));
assertEquals(512, underground.getJSONObject("mouthLevelingDistance").getInt("maximum")); assertEquals(512, underground.getJSONObject("mouthLevelingDistance").getInt("maximum"));
assertSnippetBackedObjectReference(underground.getJSONObject("fluidLevel")); assertSnippetBackedObjectReference(underground.getJSONObject("fluidLevel"));
assertSnippetBackedObjectReference(underground.getJSONObject("channelWidth")); assertSnippetBackedObjectReference(underground.getJSONObject("channelWidth"));
assertSnippetBackedObjectReference(underground.getJSONObject("headroom")); 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(coastal);
assertGrottoBounds(inland); 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("boolean", inland.getJSONObject("connectSurfaceRivers").getString("type"));
assertEquals("array", profiles.getString("type")); assertEquals("array", profiles.getString("type"));
assertEquals(1, profiles.getInt("minItems")); assertEquals(1, profiles.getInt("minItems"));
@@ -200,6 +275,10 @@ public class IrisRiverSchemaTest {
assertEquals(16D, properties.getJSONObject("widthMultiplier").getDouble("maximum"), 0D); assertEquals(16D, properties.getJSONObject("widthMultiplier").getDouble("maximum"), 0D);
assertEquals(32D, properties.getJSONObject("shoreBiomeWidth").getDouble("maximum"), 0D); assertEquals(32D, properties.getJSONObject("shoreBiomeWidth").getDouble("maximum"), 0D);
assertEquals(0D, properties.getJSONObject("shoreBiomeWidth").getDouble("minimum"), 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("boolean", properties.getJSONObject("confined").getString("type"));
assertEquals(0D, properties.getJSONObject("incisionMultiplier").getDouble("minimum"), 0D); assertEquals(0D, properties.getJSONObject("incisionMultiplier").getDouble("minimum"), 0D);
assertEquals(64D, properties.getJSONObject("routingMultiplier").getDouble("maximum"), 0D); assertEquals(64D, properties.getJSONObject("routingMultiplier").getDouble("maximum"), 0D);
@@ -207,6 +286,32 @@ public class IrisRiverSchemaTest {
properties.getJSONObject("shoreBiomes").getJSONObject("items").getString("$ref")); 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) { private static void assertGrottoBounds(JSONObject grotto) {
assertEquals(1, grotto.getJSONObject("horizontalRadius").getInt("minimum")); assertEquals(1, grotto.getJSONObject("horizontalRadius").getInt("minimum"));
assertEquals(1, grotto.getJSONObject("verticalRadius").getInt("minimum")); assertEquals(1, grotto.getJSONObject("verticalRadius").getInt("minimum"));
@@ -1,12 +1,16 @@
package art.arcane.iris.engine.actuator; 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.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample; import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef; import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType; import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.object.IrisDecorationStep; 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.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
@@ -15,8 +19,15 @@ import java.util.List;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; 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.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class IrisTerrainNormalActuatorHydrologyTest { public class IrisTerrainNormalActuatorHydrologyTest {
@@ -112,4 +123,171 @@ public class IrisTerrainNormalActuatorHydrologyTest {
assertEquals("minecraft:ice", state.key()); assertEquals("minecraft:ice", state.key());
assertFalse(state.isWater()); 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() { private static HydrologyPlannerSettings seaCaveSettings() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults(); HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets outlets = HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 32768), new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 32768),
base.outlets().inlandGrotto(), base.outlets().inlandGrotto(),
@@ -1202,7 +1202,7 @@ public class HydrologyCaveCourseFilterTest {
base.geometry().meanders(), base.geometry().meanders(),
base.geometry().surface(), base.geometry().surface(),
base.geometry().underground(), base.geometry().underground(),
new HydrologyPlannerSettings.ChannelShape(2.4D, 0D, 0D, 11), HydrologyPlannerSettings.ChannelShape.of(2.4D, 0D, 0D, 11),
base.geometry().drops() base.geometry().drops()
); );
return new HydrologyPlannerSettings( 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) { private static HydrologyPlannerSettings seaCaveSettings(int maximumOceanApron, int chamberRadius) {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults(); HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets outlets = HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(true, chamberRadius, 7, 10, 32768), new HydrologyPlannerSettings.Grotto(true, chamberRadius, 7, 10, 32768),
base.outlets().inlandGrotto(), base.outlets().inlandGrotto(),
@@ -1932,7 +2149,7 @@ public class HydrologyFootprintCompilerTest {
base.geometry().meanders(), base.geometry().meanders(),
base.geometry().surface(), base.geometry().surface(),
base.geometry().underground(), base.geometry().underground(),
new HydrologyPlannerSettings.ChannelShape(2.4D, 0D, 0D, 11), HydrologyPlannerSettings.ChannelShape.of(2.4D, 0D, 0D, 11),
base.geometry().drops() base.geometry().drops()
); );
return new HydrologyPlannerSettings( return new HydrologyPlannerSettings(
@@ -101,21 +101,21 @@ public class HydrologyPlannerConfinementTest {
!ocean, !ocean, !ocean, false, false, false, !ocean, !ocean, !ocean, false, false, false,
0D, ocean ? 0D : 1D, 0D, 1D, 1D, 1D, 1D, 1D, 0D, ocean ? 0D : 1D, 0D, 1D, 1D, 1D, 1D, 1D,
"land", "land", "land", "land", "land", "land", "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() { private static HydrologyPlannerSettings settings() {
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(true, 4D, 80, 1, 6, 32); 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.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( return new HydrologyPlannerSettings(
63, 63,
new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D, 1D, 0), 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.Surface(true, surfaceSources, 4, 12, 2, 4, 12, 1.5D, HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4), new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(false, undergroundSources, 68, 82, 4, 14, 2, 5, 6, 12, false, 0), HydrologyPlannerSettings.Underground.of(false, undergroundSources, 68, 82, 4, 14, 2, 5, 6, 12, false, 0),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
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, 1.5D,
HydrologyPlannerSettings.Banks.defaults()), HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4), new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
true, true,
undergroundSources, undergroundSources,
68, 68,
@@ -312,7 +312,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
true, true,
0 0
), ),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
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() { private HydrologyPlannerSettings.Geometry stableGeometry() {
HydrologyPlannerSettings.ChannelShape stableChannel = HydrologyPlannerSettings.ChannelShape stableChannel =
new HydrologyPlannerSettings.ChannelShape(2D, 0D, 0D, 11); HydrologyPlannerSettings.ChannelShape.of(2D, 0D, 0D, 11);
return new HydrologyPlannerSettings.Geometry( return new HydrologyPlannerSettings.Geometry(
new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D), new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D),
stableChannel, stableChannel,
@@ -376,7 +376,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
baseSurface.shoreWidth(), baseSurface.shoreWidth(),
HydrologyPlannerSettings.Banks.defaults()), HydrologyPlannerSettings.Banks.defaults()),
base.hydraulics(), base.hydraulics(),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
false, false,
disabledSources, disabledSources,
baseUnderground.minimumFluidY(), baseUnderground.minimumFluidY(),
@@ -390,7 +390,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
baseUnderground.connectToExistingCaves(), baseUnderground.connectToExistingCaves(),
0 0
), ),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
base.outlets().oceanEnabled(), base.outlets().oceanEnabled(),
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
base.outlets().inlandGrotto(), base.outlets().inlandGrotto(),
@@ -448,7 +448,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
"flooded", "flooded",
List.of("water"), List.of(), List.of("water"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
}; };
} }
@@ -491,7 +493,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
sample.floodedCaveBiomeKey(), sample.floodedCaveBiomeKey(),
sample.preferredProfileKeys(), List.of(), sample.preferredProfileKeys(), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
}; };
} }
@@ -526,7 +530,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
"flooded", "flooded",
List.of("water"), List.of(), List.of("water"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -682,7 +682,7 @@ public class HydrologyPlannerGlobalInvariantTest {
1D, 1D,
HydrologyPlannerSettings.Banks.defaults()), HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4), new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
undergroundEnabled, undergroundEnabled,
undergroundSources, undergroundSources,
64, 64,
@@ -696,7 +696,7 @@ public class HydrologyPlannerGlobalInvariantTest {
false, false,
0 0
), ),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512), new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
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(), surface.shoreWidth(),
HydrologyPlannerSettings.Banks.defaults()), HydrologyPlannerSettings.Banks.defaults()),
base.hydraulics(), base.hydraulics(),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
underground.enabled(), underground.enabled(),
underground.sources(), underground.sources(),
underground.minimumFluidY(), underground.minimumFluidY(),
@@ -956,7 +956,9 @@ public class HydrologyPlannerGlobalInvariantTest {
"flooded", "flooded",
List.of("default"), List.of(), List.of("default"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -1009,7 +1011,9 @@ public class HydrologyPlannerGlobalInvariantTest {
"flooded", "flooded",
List.of("default"), List.of(), List.of("default"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -1060,7 +1064,9 @@ public class HydrologyPlannerGlobalInvariantTest {
"flooded", "flooded",
List.of("default"), List.of(), List.of("default"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -19,7 +19,7 @@ public class HydrologyPlannerSeaCaveTest {
@Test @Test
public void seaCavesOpenFromTheSeaIntoTheCliffWithoutARiver() { 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); HydrologyPlannerSettings settings = settings(128, 0D, CHAMBER, 12, seaCaves);
HydrologyTerrainSampler cliffCoast = cliffCoast(112, 92); HydrologyTerrainSampler cliffCoast = cliffCoast(112, 92);
@@ -98,7 +98,7 @@ public class HydrologyPlannerSeaCaveTest {
@Test @Test
public void seaCavesAreDeterministicAndOwnedByExactlyOneTile() { public void seaCavesAreDeterministicAndOwnedByExactlyOneTile() {
HydrologyPlannerSettings settings = settings(128, 0D, CHAMBER, 12, 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); HydrologyTerrainSampler cliffCoast = cliffCoast(112, 92);
List<HydrologyTileKey> keys = List.of( List<HydrologyTileKey> keys = List.of(
TILE, TILE,
@@ -137,7 +137,7 @@ public class HydrologyPlannerSeaCaveTest {
@Test @Test
public void seaCavesPreferTheSteeperCoastAndKeepTheirSpacing() { public void seaCavesPreferTheSteeperCoastAndKeepTheirSpacing() {
// The high stretch is 128 blocks of coast; the spacing leaves room for a second cave wherever the first lands. // 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); HydrologyPlannerSettings settings = settings(256, 0D, CHAMBER, 12, seaCaves);
HydrologyTerrainSampler coast = (int x, int z) -> x >= 240 HydrologyTerrainSampler coast = (int x, int z) -> x >= 240
? HydrologyTerrainSample.ocean(54, "ocean") ? HydrologyTerrainSample.ocean(54, "ocean")
@@ -172,7 +172,7 @@ public class HydrologyPlannerSeaCaveTest {
@Test @Test
public void seaCavesKeepClearOfRiverMouthsAndLowCoasts() { public void seaCavesKeepClearOfRiverMouthsAndLowCoasts() {
HydrologyPlannerSettings settings = settings(128, 2D, CHAMBER, 12, 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 HydrologyTerrainSampler riverCoast = (int x, int z) -> x >= 112
? HydrologyTerrainSample.ocean(54, "ocean") ? HydrologyTerrainSample.ocean(54, "ocean")
: land(92, x <= 16); : land(92, x <= 16);
@@ -207,7 +207,7 @@ public class HydrologyPlannerSeaCaveTest {
HydrologyTile lowCoast = new HydrologyPlanner( HydrologyTile lowCoast = new HydrologyPlanner(
994L, 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) cliffCoast(112, 66)
).plan(TILE); ).plan(TILE);
assertTrue(courses(lowCoast, RiverCourseType.SEA_CAVE).isEmpty()); assertTrue(courses(lowCoast, RiverCourseType.SEA_CAVE).isEmpty());
@@ -219,7 +219,7 @@ public class HydrologyPlannerSeaCaveTest {
public void seaCaveChamberFitsTheVolumeCap() { public void seaCaveChamberFitsTheVolumeCap() {
HydrologyPlannerSettings settings = settings(128, 0D, HydrologyPlannerSettings settings = settings(128, 0D,
new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 1000), 12, 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); HydrologyTile tile = new HydrologyPlanner(994L, settings, cliffCoast(112, 92)).plan(TILE);
@@ -286,7 +286,9 @@ public class HydrologyPlannerSeaCaveTest {
List.of("beta", "alpha"), List.of("beta", "alpha"),
List.of(), List.of(),
Double.NaN, 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), new HydrologyPlannerSettings.Routing(tileSize, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
surface, surface,
new HydrologyPlannerSettings.Hydraulics(4), new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
false, undergroundSources, 68, 82, 4, 12, 2, 4, 5, 9, true, 0), false, undergroundSources, 68, 82, 4, 12, 2, 4, 5, 9, true, 0),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
true, true,
coastal, coastal,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
@@ -1,5 +1,7 @@
package art.arcane.iris.engine.hydrology; 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 org.junit.Test;
import java.util.List; import java.util.List;
@@ -97,7 +99,7 @@ public class HydrologyPlannerSettingsTest {
HydrologyPlannerSettings.Banks.defaults()); HydrologyPlannerSettings.Banks.defaults());
HydrologyPlannerSettings.Hydraulics hydraulics = new HydrologyPlannerSettings.Hydraulics(1); HydrologyPlannerSettings.Hydraulics hydraulics = new HydrologyPlannerSettings.Hydraulics(1);
HydrologyPlannerSettings.Grotto grotto = new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 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, true,
grotto, grotto,
grotto, grotto,
@@ -152,21 +154,21 @@ public class HydrologyPlannerSettingsTest {
assertEquals(32, banks.maximumBlendWidth()); assertEquals(32, banks.maximumBlendWidth());
assertEquals(6, banks.waterfallMinimumDrop()); assertEquals(6, banks.waterfallMinimumDrop());
assertTrue(banks.exposeCutStrata()); 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, () -> 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, () -> 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, () -> 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, () -> 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, () -> 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, () -> 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, () -> 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, () -> 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, () -> 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, () -> 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, 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( assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Surface(
true, true,
new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0), new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0),
1, 1, 1, 1, 0, 1D, 1, 1, 1, 1, 0, 1D,
null)); null));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, -1, 0.45D, 1D, 0.5D)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Erosion.of(true, -1, 0.45D, 1D, 0.5D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 1D, 1D, 0.5D)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Erosion.of(true, 12, 1D, 1D, 0.5D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 0D, 0.5D)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Erosion.of(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, 12, 0.45D, 1D, -0.1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Pond(true, 0, 4, 3)); 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, 5, 4, 3));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Pond(true, 4, 6, 0)); assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Pond(true, 4, 6, 0));
@@ -180,24 +182,24 @@ public class HydrologyPlannerSettingsTest {
assertEquals(64, inlet.length()); assertEquals(64, inlet.length());
assertEquals(3, inlet.depth()); assertEquals(3, inlet.depth());
assertEquals(32, inlet.maximumIncision()); assertEquals(32, inlet.maximumIncision());
assertEquals(new HydrologyPlannerSettings.Inlet(0, 0, 0), HydrologyPlannerSettings.Inlet.none()); assertEquals(HydrologyPlannerSettings.Inlet.of(0, 0, 0), HydrologyPlannerSettings.Inlet.none());
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(-1, 3, 32)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(-1, 3, 32));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(1025, 3, 32)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(1025, 3, 32));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, -1, 32)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(64, -1, 32));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 65, 32)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(64, 65, 32));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, -1)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(64, 3, -1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, 513)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Inlet.of(64, 3, 513));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks( assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Banks.of(
0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, null, 2.5D, 24, true, 0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, null, 2.5D, 24, true,
HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults())); HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
new HydrologyPlannerSettings.Inlet(1024, 64, 512); HydrologyPlannerSettings.Inlet.of(1024, 64, 512);
} }
@Test @Test
public void publicationRadiusGrowsWithTheMouthFlare() { public void publicationRadiusGrowsWithTheMouthFlare() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults(); HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Banks banks = base.surface().banks(); 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.sink(), banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(), banks.roughness(),
banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(), 4D, banks.inlet(), banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(), 4D, banks.inlet(),
banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(), banks.erosion(), banks.ponds()); banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(), banks.erosion(), banks.ponds());
@@ -227,7 +229,7 @@ public class HydrologyPlannerSettingsTest {
assertThrows(IllegalArgumentException.class, assertThrows(IllegalArgumentException.class,
() -> new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 0)); () -> new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 0));
HydrologyPlannerSettings.Grotto disabled = new HydrologyPlannerSettings.Grotto(false, 1, 1, 1, 1); HydrologyPlannerSettings.Grotto disabled = new HydrologyPlannerSettings.Grotto(false, 1, 1, 1, 1);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets( assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Outlets.of(
true, true,
accepted, accepted,
disabled, disabled,
@@ -238,7 +240,7 @@ public class HydrologyPlannerSettingsTest {
1, 1,
1) 1)
); );
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets( assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Outlets.of(
true, true,
accepted, accepted,
accepted, accepted,
@@ -254,7 +256,7 @@ public class HydrologyPlannerSettingsTest {
@Test @Test
public void coastalOutletBudgetIsBounded() { public void coastalOutletBudgetIsBounded() {
HydrologyPlannerSettings.Grotto grotto = new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 1); HydrologyPlannerSettings.Grotto grotto = new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 1);
HydrologyPlannerSettings.Outlets separate = new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets separate = HydrologyPlannerSettings.Outlets.of(
true, true,
grotto, grotto,
grotto, grotto,
@@ -267,17 +269,17 @@ public class HydrologyPlannerSettingsTest {
); );
assertEquals(0, separate.maximumCoastalPerTile()); 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()); .maximumCoastalPerTile());
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets( assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Outlets.of(
true, grotto, grotto, true, 0, 0, 1, 3, -1)); 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)); true, grotto, grotto, true, 0, 0, 1, 3, 257));
} }
@Test @Test
public void dropGeometryNarrowsFlowAndExpandsItsReceivingBasin() { public void dropGeometryNarrowsFlowAndExpandsItsReceivingBasin() {
HydrologyPlannerSettings.Drops drops = new HydrologyPlannerSettings.Drops( HydrologyPlannerSettings.Drops drops = HydrologyPlannerSettings.Drops.of(
2, 2,
1.4D, 1.4D,
2, 2,
@@ -293,7 +295,7 @@ public class HydrologyPlannerSettingsTest {
assertEquals(2, drops.flowDepth(6)); assertEquals(2, drops.flowDepth(6));
assertEquals(9, drops.basinWidth(5)); assertEquals(9, drops.basinWidth(5));
assertEquals(5, drops.basinDepth(2, 8)); assertEquals(5, drops.basinDepth(2, 8));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Drops( assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.Drops.of(
2, 2,
1.4D, 1.4D,
2, 2,
@@ -345,17 +347,17 @@ public class HydrologyPlannerSettingsTest {
@Test @Test
public void seaCaveBoundsAreEnforcedAndTheDefaultsPlanNone() { 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()); assertEquals(3, seaCaves.maximumPerTile());
assertFalse(HydrologyPlannerSettings.SeaCaves.disabled().enabled()); assertFalse(HydrologyPlannerSettings.SeaCaves.disabled().enabled());
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, -1, 160, 8, 12)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, -1, 160, 8, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 65, 160, 8, 12)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 65, 160, 8, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 15, 8, 12)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 15, 8, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 8193, 8, 12)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 8193, 8, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 0, 12)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 160, 0, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 129, 12)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 160, 129, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, -1)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 160, 8, -1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 129)); assertThrows(IllegalArgumentException.class, () -> HydrologyPlannerSettings.SeaCaves.of(true, 3, 160, 8, 129));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults(); HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
assertEquals(HydrologyPlannerSettings.SeaCaves.disabled(), base.seaCaves()); 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.Source noSources = new HydrologyPlannerSettings.Source(false, 0D, 0, 0, 0, 0);
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface( HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
false, noSources, 4, 8, 2, 4, 10, 1.5D, HydrologyPlannerSettings.Banks.defaults()); 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); false, noSources, -48, 72, 3, 8, 1, 3, 6, 14, true, 0);
HydrologyPlannerSettings quiet = new HydrologyPlannerSettings( HydrologyPlannerSettings quiet = new HydrologyPlannerSettings(
base.seaLevel(), base.seaLevel(),
@@ -408,7 +410,7 @@ public class HydrologyPlannerSettingsTest {
List.of(), List.of(),
List.of(), List.of(),
surface.shoreWidth(), 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) int alignedHalo = Math.min(base.routing().maximumRouteLength(), base.routing().sampleSpacing() * 2)
@@ -416,4 +418,319 @@ public class HydrologyPlannerSettingsTest {
assertEquals(0, quiet.publicationRadius()); assertEquals(0, quiet.publicationRadius());
assertEquals(alignedHalo + base.outlets().coastalGrotto().horizontalRadius() + 12 + 1, withCaves.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() { public void zeroMouthLevelingDistanceKeepsAcceptedCoursesHydraulicallyNonRising() {
HydrologyPlannerSettings base = standardSettings(4D, 0D, true, false, List.of()); HydrologyPlannerSettings base = standardSettings(4D, 0D, true, false, List.of());
HydrologyPlannerSettings.Outlets configured = base.outlets(); HydrologyPlannerSettings.Outlets configured = base.outlets();
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings settings = withOutlets(base, HydrologyPlannerSettings.Outlets.of(
configured.oceanEnabled(), configured.oceanEnabled(),
configured.coastalGrotto(), configured.coastalGrotto(),
configured.inlandGrotto(), configured.inlandGrotto(),
@@ -517,7 +517,9 @@ public class HydrologyPlannerTest {
"flooded", "flooded",
List.of("alpha"), List.of(), List.of("alpha"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
}; };
HydrologyTile tile = new HydrologyPlanner(812L, settings, terrain, solidCaveView()).plan(TILE); HydrologyTile tile = new HydrologyPlanner(812L, settings, terrain, solidCaveView()).plan(TILE);
@@ -753,7 +755,7 @@ public class HydrologyPlannerTest {
62 62
); );
HydrologyPlannerSettings.Outlets configured = base.outlets(); HydrologyPlannerSettings.Outlets configured = base.outlets();
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings settings = withOutlets(base, HydrologyPlannerSettings.Outlets.of(
configured.oceanEnabled(), configured.oceanEnabled(),
configured.coastalGrotto(), configured.coastalGrotto(),
configured.inlandGrotto(), configured.inlandGrotto(),
@@ -1132,7 +1134,7 @@ public class HydrologyPlannerTest {
0D, 0D,
HydrologyPlannerSettings.SeaCaves.disabled() HydrologyPlannerSettings.SeaCaves.disabled()
); );
HydrologyPlannerSettings settings = withOutlets(shaped, new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings settings = withOutlets(shaped, HydrologyPlannerSettings.Outlets.of(
true, true,
base.outlets().coastalGrotto(), base.outlets().coastalGrotto(),
base.outlets().inlandGrotto(), base.outlets().inlandGrotto(),
@@ -1237,7 +1239,7 @@ public class HydrologyPlannerTest {
@Test @Test
public void coastalGrottoIsASeaLevelEllipsoidWithADirectOceanConnection() { public void coastalGrottoIsASeaLevelEllipsoidWithADirectOceanConnection() {
HydrologyPlannerSettings base = standardSettings(2D, 0D, true, true, List.of()); HydrologyPlannerSettings base = standardSettings(2D, 0D, true, true, List.of());
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings settings = withOutlets(base, HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
base.outlets().inlandGrotto(), base.outlets().inlandGrotto(),
@@ -1298,7 +1300,7 @@ public class HydrologyPlannerTest {
@Test @Test
public void coastalAndInlandOutletsAreBudgetedSeparately() { public void coastalAndInlandOutletsAreBudgetedSeparately() {
HydrologyPlannerSettings base = standardSettings(2D, 0D, true, true, List.of()); HydrologyPlannerSettings base = standardSettings(2D, 0D, true, true, List.of());
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings settings = withOutlets(base, HydrologyPlannerSettings.Outlets.of(
true, true,
base.outlets().coastalGrotto(), base.outlets().coastalGrotto(),
base.outlets().inlandGrotto(), base.outlets().inlandGrotto(),
@@ -1389,7 +1391,7 @@ public class HydrologyPlannerTest {
base.routing(), base.routing(),
base.surface(), base.surface(),
base.hydraulics(), base.hydraulics(),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
true, true,
base.underground().sources(), base.underground().sources(),
40, 40,
@@ -1468,7 +1470,7 @@ public class HydrologyPlannerTest {
private HydrologyPlannerSettings mixedCoastSettings(int maximumCoastalPerTile) { private HydrologyPlannerSettings mixedCoastSettings(int maximumCoastalPerTile) {
HydrologyPlannerSettings base = standardSettings(2D, 0D, true, false, List.of()); HydrologyPlannerSettings base = standardSettings(2D, 0D, true, false, List.of());
return withOutlets(base, new HydrologyPlannerSettings.Outlets( return withOutlets(base, HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
base.outlets().inlandGrotto(), base.outlets().inlandGrotto(),
@@ -1543,7 +1545,7 @@ public class HydrologyPlannerTest {
public void disabledSurfaceSinkholesRejectSurfaceSourcesButKeepUndergroundSourcesEligible() { public void disabledSurfaceSinkholesRejectSurfaceSourcesButKeepUndergroundSourcesEligible() {
HydrologyPlannerSettings base = standardSettings(0D, 1D, false, true, List.of()); HydrologyPlannerSettings base = standardSettings(0D, 1D, false, true, List.of());
HydrologyPlannerSettings.Outlets configured = base.outlets(); HydrologyPlannerSettings.Outlets configured = base.outlets();
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings settings = withOutlets(base, HydrologyPlannerSettings.Outlets.of(
configured.oceanEnabled(), configured.oceanEnabled(),
configured.coastalGrotto(), configured.coastalGrotto(),
configured.inlandGrotto(), configured.inlandGrotto(),
@@ -1944,7 +1946,7 @@ public class HydrologyPlannerTest {
1.5D, 1.5D,
HydrologyPlannerSettings.Banks.defaults()), HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4), new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
undergroundDensity > 0D, undergroundDensity > 0D,
undergroundSources, undergroundSources,
68, 68,
@@ -1958,7 +1960,7 @@ public class HydrologyPlannerTest {
true, true,
0 0
), ),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
oceanEnabled, oceanEnabled,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(inlandEnabled, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(inlandEnabled, 4, 3, 3, 4096),
@@ -1978,7 +1980,7 @@ public class HydrologyPlannerTest {
private HydrologyPlannerSettings.Geometry stableGeometry() { private HydrologyPlannerSettings.Geometry stableGeometry() {
HydrologyPlannerSettings.ChannelShape stableChannel = HydrologyPlannerSettings.ChannelShape stableChannel =
new HydrologyPlannerSettings.ChannelShape(2D, 0D, 0D, 11); HydrologyPlannerSettings.ChannelShape.of(2D, 0D, 0D, 11);
return new HydrologyPlannerSettings.Geometry( return new HydrologyPlannerSettings.Geometry(
new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D), new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D),
stableChannel, stableChannel,
@@ -2018,7 +2020,7 @@ public class HydrologyPlannerTest {
HydrologyPlannerSettings.Banks.defaults()), HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(5), new HydrologyPlannerSettings.Hydraulics(5),
base.underground(), base.underground(),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
true, true,
outlets.coastalGrotto(), outlets.coastalGrotto(),
outlets.inlandGrotto(), outlets.inlandGrotto(),
@@ -2103,7 +2105,9 @@ public class HydrologyPlannerTest {
"flooded", "flooded",
List.of("alpha"), List.of(), List.of("alpha"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
}; };
} }
@@ -2170,7 +2174,9 @@ public class HydrologyPlannerTest {
"flooded", "flooded",
List.of("alpha", "beta"), List.of(), List.of("alpha", "beta"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -2204,7 +2210,9 @@ public class HydrologyPlannerTest {
"flooded", "flooded",
List.of("alpha"), List.of(), List.of("alpha"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -2247,7 +2255,9 @@ public class HydrologyPlannerTest {
"flooded", "flooded",
List.of("beta", "alpha"), List.of(), List.of("beta", "alpha"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -2281,7 +2291,9 @@ public class HydrologyPlannerTest {
"flooded", "flooded",
List.of("alpha"), List.of(), List.of("alpha"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -2315,7 +2327,9 @@ public class HydrologyPlannerTest {
"flooded", "flooded",
List.of("alpha", "beta"), List.of(), List.of("alpha", "beta"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -2357,7 +2371,7 @@ public class HydrologyPlannerTest {
settings.routing(), settings.routing(),
settings.surface(), settings.surface(),
settings.hydraulics(), settings.hydraulics(),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
true, true,
sources, sources,
underground.minimumFluidY(), underground.minimumFluidY(),
@@ -2390,7 +2404,7 @@ public class HydrologyPlannerTest {
settings.routing(), settings.routing(),
settings.surface(), settings.surface(),
settings.hydraulics(), settings.hydraulics(),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
underground.enabled(), underground.enabled(),
underground.sources(), underground.sources(),
minimumFluidY, minimumFluidY,
@@ -2422,7 +2436,7 @@ public class HydrologyPlannerTest {
settings.routing(), settings.routing(),
settings.surface(), settings.surface(),
settings.hydraulics(), settings.hydraulics(),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
underground.enabled(), underground.enabled(),
underground.sources(), underground.sources(),
underground.minimumFluidY(), underground.minimumFluidY(),
@@ -157,7 +157,9 @@ public class HydrologyPlannerTributaryTest {
List.of("default"), List.of("default"),
List.of(), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -221,7 +223,9 @@ public class HydrologyPlannerTributaryTest {
List.of("default"), List.of("default"),
List.of(), List.of(),
Double.NaN, 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.Source sources = new HydrologyPlannerSettings.Source(true, density, 0, 0, maximumPerTile, 128);
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface( HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
true, sources, 4, 8, 2, 4, 40, 1.5D, HydrologyPlannerSettings.Banks.defaults()); 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); 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, true,
base.outlets().coastalGrotto(), base.outlets().coastalGrotto(),
base.outlets().inlandGrotto(), base.outlets().inlandGrotto(),
@@ -349,7 +353,7 @@ public class HydrologyPlannerTributaryTest {
@Test @Test
public void undergroundTributariesAreBounded() { public void undergroundTributariesAreBounded() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults(); 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)); 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, 54, 0D, true, false, 30, 32, false, false, false, false, false, false,
0D, 0D, 0D, 1D, 1D, 1D, 1D, 1D, 0D, 0D, 0D, 1D, 1D, 1D, 1D, 1D,
"ocean_parent", "surface", "mouth", "shore", "dry", "flooded", "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); 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); 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, height, 1D, false, true, 70, 74, true, true, false, false, source, false,
0D, 0D, source ? 1D : 0D, 1D, 1D, 1D, 1D, 1D, 0D, 0D, source ? 1D : 0D, 1D, 1D, 1D, 1D, 1D,
"parent", "surface", "mouth", "shore", "dry", "flooded", "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) { private static HydrologyPlannerSettings undergroundSettings(int tributaries) {
HydrologyPlannerSettings.Source none = new HydrologyPlannerSettings.Source(false, 0D, 80, 0, 0, 24); 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.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( return new HydrologyPlannerSettings(
63, 63,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0), 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.Surface(false, none, 4, 18, 2, 4, 10, 1.5D, HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4), new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(true, sources, 68, 82, 4, 12, 2, 4, 5, 9, true, tributaries), HydrologyPlannerSettings.Underground.of(true, sources, 68, 82, 4, 12, 2, 4, 5, 9, true, tributaries),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
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, 1.5D,
HydrologyPlannerSettings.Banks.defaults()), HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4), new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
undergroundDensity > 0D, undergroundDensity > 0D,
undergroundSources, undergroundSources,
68, 68,
@@ -221,7 +221,7 @@ public class HydrologyStyledGeometryTest {
true, true,
0 0
), ),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
@@ -270,7 +270,9 @@ public class HydrologyStyledGeometryTest {
"flooded", "flooded",
List.of("water"), List.of(), List.of("water"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D); int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D);
@@ -304,7 +306,9 @@ public class HydrologyStyledGeometryTest {
"flooded", "flooded",
List.of("water"), List.of(), List.of("water"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@@ -82,7 +82,9 @@ public final class HydrologyTerrainSampleTest {
"flooded", "flooded",
profiles, List.of(), profiles, List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
} }
@Test @Test
@@ -111,11 +113,59 @@ public final class HydrologyTerrainSampleTest {
} }
private static HydrologyTerrainSample sample(List<String> profiles, double shoreBiomeWidth, String confinesKey) { 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( return new HydrologyTerrainSample(
80, 0D, false, false, 48, 50, true, true, true, false, false, false, 80, 0D, false, false, 48, 50, true, true, true, false, false, false,
0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D, 0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
"parent", "parent", "parent", "parent", "parent", "parent", "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, new HydrologyPlannerSettings.Surface(false, disabled, 2, 4, 1, 2, 4, 1D,
HydrologyPlannerSettings.Banks.defaults()), HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(3), new HydrologyPlannerSettings.Hydraulics(3),
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0), HydrologyPlannerSettings.Underground.of(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
false, false,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512), new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
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.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.Surface(true, surfaceSources, 4, 8, 2, 3, 20, 1.5D, tileBoundedBanks()),
new HydrologyPlannerSettings.Hydraulics(3), new HydrologyPlannerSettings.Hydraulics(3),
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0), HydrologyPlannerSettings.Underground.of(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512), new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
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. // Small-tile settings need a blend envelope that fits inside the bounded cross-tile admission period.
private static HydrologyPlannerSettings.Banks tileBoundedBanks() { 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()); HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults());
} }
@@ -348,7 +348,9 @@ public class HydrologyTileCacheTest {
"flooded", "flooded",
List.of("default"), List.of(), List.of("default"), List.of(),
Double.NaN, 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(null, confined, released).confinement());
assertEquals(RiverConfinement.NONE, RiverPolicyResolver.resolve(confined, released, null).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; 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.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisCoastalRiverGrottoConfig; import art.arcane.iris.engine.object.IrisCoastalRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisDeepFluidConfig; import art.arcane.iris.engine.object.IrisDeepFluidConfig;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.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.IrisRiverHydrology;
import art.arcane.iris.engine.object.IrisRiverInlandOutlet; 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.IrisRiverRoutingConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverShapeConfig;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisHydrologyRuntimeSettingsTest { public class IrisHydrologyRuntimeSettingsTest {
@Test @Test
@@ -128,7 +141,7 @@ public class IrisHydrologyRuntimeSettingsTest {
assertEquals(3, banks.cascadeRun()); assertEquals(3, banks.cascadeRun());
assertEquals(8, banks.waterfallMinimumDrop()); assertEquals(8, banks.waterfallMinimumDrop());
assertEquals(2.2D, banks.mouthFlareRatio(), 0D); 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()); assertFalse(banks.exposeCutStrata());
assertEquals(12, settings.surface().maximumIncision()); assertEquals(12, settings.surface().maximumIncision());
assertEquals(2.5D, settings.surface().shoreWidth(), 0D); assertEquals(2.5D, settings.surface().shoreWidth(), 0D);
@@ -218,7 +231,7 @@ public class IrisHydrologyRuntimeSettingsTest {
HydrologyPlannerSettings.SeaCaves mapped = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null) HydrologyPlannerSettings.SeaCaves mapped = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
.seaCaves(); .seaCaves();
assertEquals(new HydrologyPlannerSettings.SeaCaves(true, 5, 200, 9, 20), mapped); assertEquals(HydrologyPlannerSettings.SeaCaves.of(true, 5, 200, 9, 20), mapped);
coastal.setEnabled(false); coastal.setEnabled(false);
HydrologyPlannerSettings.SeaCaves withoutCoastalGrottos = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null) HydrologyPlannerSettings.SeaCaves withoutCoastalGrottos = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
@@ -231,4 +244,244 @@ public class IrisHydrologyRuntimeSettingsTest {
coastal.getSeaCaves().setEnabled(false); coastal.getSeaCaves().setEnabled(false);
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null).seaCaves().enabled()); 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); 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) { private static ChannelProfileBuilder builder(double widthMultiplier, double depthMultiplier) {
return builder(widthMultiplier, depthMultiplier, (int x, int z) -> 80); return builder(widthMultiplier, depthMultiplier, (int x, int z) -> 80);
} }
@@ -150,7 +204,9 @@ public class ChannelProfileBuilderTest {
"parent", "parent",
List.of("water"), List.of(), List.of("water"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY); 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.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler; import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import art.arcane.iris.engine.hydrology.RiverFootprint; 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 org.junit.Test;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.IntBinaryOperator; import java.util.function.IntBinaryOperator;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -63,7 +67,7 @@ public class ErosionFieldCompilerTest {
@Test @Test
public void disabledErosionKeepsTheChannelShoreAndLipButNoValley() { public void disabledErosionKeepsTheChannelShoreAndLipButNoValley() {
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z) / 2, 0D, "land"); 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); Compiled compiled = compile(zeroRoughnessSurface(0, off), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
assertEquals(0, compiled.field().uncontainedWetCells()); assertEquals(0, compiled.field().uncontainedWetCells());
@@ -80,8 +84,8 @@ public class ErosionFieldCompilerTest {
public void erosionDefaultsReproduceTheValleyBitForBitAndACurveReshapesIt() { public void erosionDefaultsReproduceTheValleyBitForBitAndACurveReshapesIt() {
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z) / 2, 0D, "land"); 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 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 explicit = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.of(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 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()); assertEquals(defaults.field().columns().size(), explicit.field().columns().size());
for (SurfaceColumn column : defaults.field().columns().values()) { 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; int height = x < 200 ? 100 - x / 6 : 66 + (x - 200) * 18 / 40;
return HydrologyTerrainSample.openLand(height, 0D, "land"); 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)); HydrologyPlannerSettings.Surface surface = withBanks(zeroRoughnessSurface(), zeroRoughnessSurface().banks().withInlet(inlet));
Compiled compiled = compile(surface, coast, 244, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL); Compiled compiled = compile(surface, coast, 244, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
@@ -332,10 +336,111 @@ public class ErosionFieldCompilerTest {
true, true,
defaults.sources(), defaults.sources(),
4, 8, 2, 4, 10, 1.5D, 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) { private record Compiled(ErosionField field, ValleyProfile valley) {
} }
@@ -506,7 +611,7 @@ public class ErosionFieldCompilerTest {
open.surfaceSourceWeight(), open.undergroundSourceWeight(), open.widthMultiplier(), open.depthMultiplier(), open.surfaceSourceWeight(), open.undergroundSourceWeight(), open.widthMultiplier(), open.depthMultiplier(),
open.incisionMultiplier(), open.routingMultiplier(), open.bankMultiplier(), open.parentBiomeKey(), open.incisionMultiplier(), open.routingMultiplier(), open.bankMultiplier(), open.parentBiomeKey(),
open.surfaceBiomeKey(), open.mouthBiomeKey(), open.shoreBiomeKey(), open.bankBiomeKey(), 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 @Test
@@ -610,4 +715,206 @@ public class ErosionFieldCompilerTest {
} }
assertEquals(0, first.field().uncontainedWetCells()); 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 @Test
public void anInletHoldsTheLastStationsAtSeaLevelAndMayCutDeeperThanTheChannelCap() { public void anInletHoldsTheLastStationsAtSeaLevelAndMayCutDeeperThanTheChannelCap() {
HydrologyTerrainSampler coast = risingCoast(); 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 valley = solver(coast, inlet).solve(straight(300), channel(300, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
ValleyProfile plain = solver(coast, HydrologyPlannerSettings.Inlet.none()) ValleyProfile plain = solver(coast, HydrologyPlannerSettings.Inlet.none())
@@ -185,9 +185,9 @@ public class ValleyProfileSolverTest {
? HydrologyTerrainSample.ocean(50, "ocean") ? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(88, 0D, "land"); : 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); .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); .solve(straight(300), channel(300, plateau), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection()); assertNull(valley.rejection());
@@ -214,7 +214,7 @@ public class ValleyProfileSolverTest {
? HydrologyTerrainSample.ocean(50, "ocean") ? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(x >= 220 ? 66 + (x - 220) / 5 : 110, 0D, "land"); : 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); .solve(straight(300), channel(300, bluff), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection()); assertNull(valley.rejection());
@@ -233,7 +233,7 @@ public class ValleyProfileSolverTest {
? HydrologyTerrainSample.ocean(50, "ocean") ? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(x >= 30 && x < 50 ? 94 : 80, 0D, "land"); : 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); .solve(straight(300), channel(300, ridge), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, valley.rejection()); assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, valley.rejection());
@@ -246,7 +246,7 @@ public class ValleyProfileSolverTest {
? HydrologyTerrainSample.ocean(50, "ocean") ? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(70, 0D, "land"); : 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); .solve(straight(100), channel(100, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection()); assertNull(valley.rejection());
@@ -262,6 +262,62 @@ public class ValleyProfileSolverTest {
assertContained(valley); 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. */ /** 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() { private static HydrologyTerrainSampler risingCoast() {
return (int x, int z) -> { return (int x, int z) -> {
@@ -279,7 +335,7 @@ public class ValleyProfileSolverTest {
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface( HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(), defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(), 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.roughness(), banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(),
banks.mouthFlareRatio(), inlet, banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(), banks.mouthFlareRatio(), inlet, banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(),
banks.erosion(), banks.ponds())); banks.erosion(), banks.ponds()));
@@ -310,7 +366,7 @@ public class ValleyProfileSolverTest {
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface( HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(), defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(), 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.roughness(), banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(),
banks.mouthFlareRatio(), HydrologyPlannerSettings.Inlet.none(), banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(), banks.mouthFlareRatio(), HydrologyPlannerSettings.Inlet.none(), banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(),
banks.erosion(), banks.ponds())); banks.erosion(), banks.ponds()));
@@ -1542,7 +1542,7 @@ public class MantleHydrologyComponentTest {
0D, 0D,
HydrologyPlannerSettings.Banks.defaults()), HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4), new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
false, false,
disabledSources, disabledSources,
64, 64,
@@ -1556,7 +1556,7 @@ public class MantleHydrologyComponentTest {
false, false,
0 0
), ),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512), new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512), new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
@@ -1609,7 +1609,9 @@ public class MantleHydrologyComponentTest {
"flooded", "flooded",
List.of("default"), List.of(), List.of("default"), List.of(),
Double.NaN, 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( private static final List<Class<? extends Enum<?>>> HYDROLOGY_ENUMS = List.of(
IrisRiverPlacementMode.class, IrisRiverPlacementMode.class,
IrisRiverRoutingMode.class, IrisRiverRoutingMode.class,
IrisGrottoPoolLevel.class IrisGrottoPoolLevel.class,
IrisRiverBlendStyle.class,
IrisRiverBedProfile.class
); );
@Test @Test
@@ -54,16 +54,47 @@ public class IrisRiverConfigurationTest {
assertEquals(10, surface.getChannel().getMaximumIncision()); assertEquals(10, surface.getChannel().getMaximumIncision());
assertEquals(0.25D, surface.getChannel().getRoughness(), 0D); assertEquals(0.25D, surface.getChannel().getRoughness(), 0D);
assertEquals(16, surface.getChannel().getRoughnessWavelength()); 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(1.5D, surface.getBanks().getShoreWidth(), 0D);
assertEquals(3D, surface.getBanks().getBlendSlope(), 0D); assertEquals(3D, surface.getBanks().getBlendSlope(), 0D);
assertEquals(4, surface.getBanks().getMinimumBlendWidth()); assertEquals(4, surface.getBanks().getMinimumBlendWidth());
assertEquals(32, surface.getBanks().getMaximumBlendWidth()); assertEquals(32, surface.getBanks().getMaximumBlendWidth());
assertTrue(surface.getBanks().isExposeCutStrata()); 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(2, surface.getFlow().getCascadeRun());
assertEquals(6, surface.getFlow().getWaterfallMinimumDrop()); 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(1.6D, surface.getMouths().getFlareRatio(), 0D);
assertEquals(8, surface.getMouths().getMaximumOceanApron()); 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(2, drops.getCascadeRunPerBlock());
assertEquals(1.4D, drops.getCascadeExponent(), 0D); assertEquals(1.4D, drops.getCascadeExponent(), 0D);
assertEquals(2, drops.getMaximumCascadeStep()); assertEquals(2, drops.getMaximumCascadeStep());
@@ -71,6 +102,7 @@ public class IrisRiverConfigurationTest {
assertEquals(2, drops.getMaximumFlowDepth()); assertEquals(2, drops.getMaximumFlowDepth());
assertEquals(1.8D, drops.getBasinWidthRatio(), 0D); assertEquals(1.8D, drops.getBasinWidthRatio(), 0D);
assertEquals(8, drops.getMaximumBasinDepth()); assertEquals(8, drops.getMaximumBasinDepth());
assertEquals(0, drops.getUndergroundCascadeRunPerBlock());
IrisUndergroundRiverConfig underground = dimension.getHydrology().getRivers().getUnderground(); IrisUndergroundRiverConfig underground = dimension.getHydrology().getRivers().getUnderground();
assertTrue(underground.isEnabled()); assertTrue(underground.isEnabled());
@@ -83,6 +115,10 @@ public class IrisRiverConfigurationTest {
assertEquals(8D, underground.getChannelWidth().getMax(), 0D); assertEquals(8D, underground.getChannelWidth().getMax(), 0D);
assertTrue(underground.isConnectToExistingCaves()); assertTrue(underground.isConnectToExistingCaves());
assertEquals(64, underground.getMouthLevelingDistance()); 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(); IrisCoastalRiverGrottoConfig coastal = dimension.getHydrology().getRivers().getGrottos().getCoastal();
IrisInlandRiverGrottoConfig inland = dimension.getHydrology().getRivers().getGrottos().getInland(); IrisInlandRiverGrottoConfig inland = dimension.getHydrology().getRivers().getGrottos().getInland();
@@ -90,6 +126,9 @@ public class IrisRiverConfigurationTest {
assertEquals(7, coastal.getVerticalRadius()); assertEquals(7, coastal.getVerticalRadius());
assertEquals(10, coastal.getHeadroom()); assertEquals(10, coastal.getHeadroom());
assertEquals(8192, coastal.getMaximumVolume()); assertEquals(8192, coastal.getMaximumVolume());
assertNull(coastal.getCliffMinimumHeight());
assertEquals(0.5D, coastal.getCliffSlopeFactor(), 0D);
assertEquals(25D, coastal.getSeaCaves().getSweepJitterDegrees(), 0D);
assertEquals(10, inland.getHorizontalRadius()); assertEquals(10, inland.getHorizontalRadius());
assertEquals(6, inland.getVerticalRadius()); assertEquals(6, inland.getVerticalRadius());
assertEquals(10, inland.getHeadroom()); assertEquals(10, inland.getHeadroom());
@@ -110,10 +149,38 @@ public class IrisRiverConfigurationTest {
assertNull(dimension.getRiverPolicy().getRouting()); assertNull(dimension.getRiverPolicy().getRouting());
assertNull(dimension.getRiverPolicy().getOutletAdmission()); assertNull(dimension.getRiverPolicy().getOutletAdmission());
assertNull(dimension.getRiverPolicy().getBankMultiplier()); assertNull(dimension.getRiverPolicy().getBankMultiplier());
assertNull(dimension.getRiverPolicy().getShoreWidth());
assertNull(dimension.getRiverPolicy().getErosion());
assertNull(new IrisRegion().getRiverPolicy()); assertNull(new IrisRegion().getRiverPolicy());
assertNull(new IrisBiome().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 @Test
public void deserializesCanonicalDimensionRegionAndBiomeConfiguration() { public void deserializesCanonicalDimensionRegionAndBiomeConfiguration() {
Gson gson = new Gson(); Gson gson = new Gson();
@@ -137,6 +204,32 @@ public class IrisRiverConfigurationTest {
"confluenceAttraction": 0.4 "confluenceAttraction": 0.4
}, },
"geometry": { "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": { "drops": {
"cascadeRunPerBlock": 5, "cascadeRunPerBlock": 5,
"cascadeExponent": 2.4, "cascadeExponent": 2.4,
@@ -144,7 +237,8 @@ public class IrisRiverConfigurationTest {
"flowWidthRatio": 0.6, "flowWidthRatio": 0.6,
"maximumFlowDepth": 4, "maximumFlowDepth": 4,
"basinWidthRatio": 2.2, "basinWidthRatio": 2.2,
"maximumBasinDepth": 11 "maximumBasinDepth": 11,
"undergroundCascadeRunPerBlock": 3
} }
}, },
"surface": { "surface": {
@@ -156,17 +250,36 @@ public class IrisRiverConfigurationTest {
"sink": 2, "sink": 2,
"maximumIncision": 14, "maximumIncision": 14,
"roughness": 0.4, "roughness": 0.4,
"roughnessWavelength": 24 "roughnessWavelength": 24,
"smoothingRadius": 8,
"outlineMinimumRatio": 0.8,
"outlineMaximumRatio": 2,
"springExtraDepth": 3
}, },
"banks": { "banks": {
"shoreWidth": 2.5, "shoreWidth": 2.5,
"blendSlope": 4, "blendSlope": 4,
"minimumBlendWidth": 6, "minimumBlendWidth": 6,
"maximumBlendWidth": 48, "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}, "bed": {
"mouths": {"flareRatio": 2.2, "maximumOceanApron": 1} "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": { "underground": {
"sources": {"density": 0.4, "minimumPerTile": 3, "minimumSpacing": 896}, "sources": {"density": 0.4, "minimumPerTile": 3, "minimumSpacing": 896},
@@ -175,7 +288,11 @@ public class IrisRiverConfigurationTest {
"depth": {"min": 2, "max": 4}, "depth": {"min": 2, "max": 4},
"headroom": {"min": 7, "max": 15}, "headroom": {"min": 7, "max": 15},
"connectToExistingCaves": false, "connectToExistingCaves": false,
"mouthLevelingDistance": 80 "mouthLevelingDistance": 80,
"minimumRockCover": 4,
"minimumFloorCover": 2,
"wideningSources": 16,
"bedMaterial": {"enabled": true, "palette": {"palette": [{"block": "minecraft:mud"}]}, "depth": 2}
}, },
"grottos": { "grottos": {
"coastal": { "coastal": {
@@ -184,7 +301,10 @@ public class IrisRiverConfigurationTest {
"horizontalRadius": 40, "horizontalRadius": 40,
"verticalRadius": 18, "verticalRadius": 18,
"headroom": 9, "headroom": 9,
"maximumVolume": 70000 "maximumVolume": 70000,
"cliffMinimumHeight": 9,
"cliffSlopeFactor": 1.25,
"seaCaves": {"sweepJitterDegrees": 40}
}, },
"inland": { "inland": {
"enabled": true, "enabled": true,
@@ -225,7 +345,9 @@ public class IrisRiverConfigurationTest {
"bankBiomes": ["river/bank"], "bankBiomes": ["river/bank"],
"widthMultiplier": 1.5, "widthMultiplier": 1.5,
"incisionMultiplier": 0.75, "incisionMultiplier": 0.75,
"bankMultiplier": 2 "bankMultiplier": 2,
"shoreWidth": 3,
"erosion": false
} }
} }
""", IrisDimension.class); """, IrisDimension.class);
@@ -234,7 +356,8 @@ public class IrisRiverConfigurationTest {
"placement": "TRANSIT_ONLY", "placement": "TRANSIT_ONLY",
"routing": "AVOID", "routing": "AVOID",
"mouthBiomes": ["river/mouth"], "mouthBiomes": ["river/mouth"],
"routingMultiplier": 2.5 "routingMultiplier": 2.5,
"erosion": true
}} }}
""", IrisRegion.class); """, IrisRegion.class);
IrisBiome biome = gson.fromJson(""" IrisBiome biome = gson.fromJson("""
@@ -242,7 +365,8 @@ public class IrisRiverConfigurationTest {
"placement": "REQUIRED_HEADWATER", "placement": "REQUIRED_HEADWATER",
"routing": "BLOCK", "routing": "BLOCK",
"floodedCaveBiomes": [], "floodedCaveBiomes": [],
"depthMultiplier": 0.8 "depthMultiplier": 0.8,
"shoreWidth": 0
}} }}
""", IrisBiome.class); """, IrisBiome.class);
@@ -264,16 +388,62 @@ public class IrisRiverConfigurationTest {
assertEquals(14, rivers.getSurface().getChannel().getMaximumIncision()); assertEquals(14, rivers.getSurface().getChannel().getMaximumIncision());
assertEquals(0.4D, rivers.getSurface().getChannel().getRoughness(), 0D); assertEquals(0.4D, rivers.getSurface().getChannel().getRoughness(), 0D);
assertEquals(24, rivers.getSurface().getChannel().getRoughnessWavelength()); 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(2.5D, rivers.getSurface().getBanks().getShoreWidth(), 0D);
assertEquals(4D, rivers.getSurface().getBanks().getBlendSlope(), 0D); assertEquals(4D, rivers.getSurface().getBanks().getBlendSlope(), 0D);
assertEquals(6, rivers.getSurface().getBanks().getMinimumBlendWidth()); assertEquals(6, rivers.getSurface().getBanks().getMinimumBlendWidth());
assertEquals(48, rivers.getSurface().getBanks().getMaximumBlendWidth()); assertEquals(48, rivers.getSurface().getBanks().getMaximumBlendWidth());
assertFalse(rivers.getSurface().getBanks().isExposeCutStrata()); 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(3, rivers.getSurface().getFlow().getCascadeRun());
assertEquals(8, rivers.getSurface().getFlow().getWaterfallMinimumDrop()); 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(2.2D, rivers.getSurface().getMouths().getFlareRatio(), 0D);
assertEquals(1, rivers.getSurface().getMouths().getMaximumOceanApron()); 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()); 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(5, rivers.getGeometry().getDrops().getCascadeRunPerBlock());
assertEquals(2.4D, rivers.getGeometry().getDrops().getCascadeExponent(), 0D); assertEquals(2.4D, rivers.getGeometry().getDrops().getCascadeExponent(), 0D);
assertEquals(3, rivers.getGeometry().getDrops().getMaximumCascadeStep()); assertEquals(3, rivers.getGeometry().getDrops().getMaximumCascadeStep());
@@ -281,11 +451,19 @@ public class IrisRiverConfigurationTest {
assertEquals(4, rivers.getGeometry().getDrops().getMaximumFlowDepth()); assertEquals(4, rivers.getGeometry().getDrops().getMaximumFlowDepth());
assertEquals(2.2D, rivers.getGeometry().getDrops().getBasinWidthRatio(), 0D); assertEquals(2.2D, rivers.getGeometry().getDrops().getBasinWidthRatio(), 0D);
assertEquals(11, rivers.getGeometry().getDrops().getMaximumBasinDepth()); assertEquals(11, rivers.getGeometry().getDrops().getMaximumBasinDepth());
assertEquals(3, rivers.getGeometry().getDrops().getUndergroundCascadeRunPerBlock());
assertEquals(-96D, rivers.getUnderground().getFluidLevel().getMin(), 0D); assertEquals(-96D, rivers.getUnderground().getFluidLevel().getMin(), 0D);
assertEquals(896, rivers.getUnderground().getSources().getMinimumSpacing()); assertEquals(896, rivers.getUnderground().getSources().getMinimumSpacing());
assertFalse(rivers.getUnderground().isConnectToExistingCaves()); assertFalse(rivers.getUnderground().isConnectToExistingCaves());
assertEquals(80, rivers.getUnderground().getMouthLevelingDistance()); 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(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().isEnabled());
assertTrue(rivers.getGrottos().getInland().isConnectSurfaceRivers()); assertTrue(rivers.getGrottos().getInland().isConnectSurfaceRivers());
assertEquals("underworld", rivers.getProfiles().getFirst().getId()); assertEquals("underworld", rivers.getProfiles().getFirst().getId());
@@ -303,10 +481,23 @@ public class IrisRiverConfigurationTest {
assertEquals(IrisRiverRoutingMode.PREFER, dimension.getRiverPolicy().getRouting()); assertEquals(IrisRiverRoutingMode.PREFER, dimension.getRiverPolicy().getRouting());
assertFalse(dimension.getRiverPolicy().getOutletAdmission()); assertFalse(dimension.getRiverPolicy().getOutletAdmission());
assertEquals(2D, dimension.getRiverPolicy().getBankMultiplier(), 0D); 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(Set.of("river/surface", "river/shore", "river/bank"), dimension.getRiverPolicy().getAllBiomeIds());
assertEquals(IrisRiverPlacementMode.TRANSIT_ONLY, region.getRiverPolicy().getPlacement()); assertEquals(IrisRiverPlacementMode.TRANSIT_ONLY, region.getRiverPolicy().getPlacement());
assertEquals(new KList<>("river/mouth"), region.getRiverPolicy().getMouthBiomes()); assertEquals(new KList<>("river/mouth"), region.getRiverPolicy().getMouthBiomes());
assertNull(region.getRiverPolicy().getShoreWidth());
assertTrue(region.getRiverPolicy().getErosion());
assertEquals(IrisRiverPlacementMode.REQUIRED_HEADWATER, biome.getRiverPolicy().getPlacement()); assertEquals(IrisRiverPlacementMode.REQUIRED_HEADWATER, biome.getRiverPolicy().getPlacement());
assertTrue(biome.getRiverPolicy().getFloodedCaveBiomes().isEmpty()); 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() HydrologyPlannerSettings.Banks.defaults()
), ),
new HydrologyPlannerSettings.Hydraulics(4), new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
undergroundDensity > 0D, undergroundDensity > 0D,
undergroundSources, undergroundSources,
68, 68,
@@ -258,7 +258,7 @@ public final class HydrologyPlannerProbe {
true, true,
0 0
), ),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
false, false,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(true, 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() List.of("water"), List.of()
, ,
Double.NaN, Double.NaN,
null); null, Double.NaN, true);
}; };
} }
@@ -367,7 +367,7 @@ public final class HydrologyPlannerProbe {
List.of("water"), List.of() List.of("water"), List.of()
, ,
Double.NaN, Double.NaN,
null); null, Double.NaN, true);
}; };
} }
@@ -465,7 +465,7 @@ public final class HydrologyPlannerProbe {
HydrologyPlannerSettings.Banks.defaults() HydrologyPlannerSettings.Banks.defaults()
), ),
new HydrologyPlannerSettings.Hydraulics(4), new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground( HydrologyPlannerSettings.Underground.of(
true, true,
undergroundSources, undergroundSources,
68, 68,
@@ -479,7 +479,7 @@ public final class HydrologyPlannerProbe {
true, true,
0 0
), ),
new HydrologyPlannerSettings.Outlets( HydrologyPlannerSettings.Outlets.of(
true, true,
new HydrologyPlannerSettings.Grotto(coastalGrottosEnabled, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(coastalGrottosEnabled, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096), new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
@@ -540,7 +540,9 @@ public final class HydrologyPlannerProbe {
"flooded", "flooded",
List.of("water"), List.of(), List.of("water"), List.of(),
Double.NaN, Double.NaN,
null null,
Double.NaN,
true
); );
}; };
} }
@@ -576,7 +578,7 @@ public final class HydrologyPlannerProbe {
List.of("water"), List.of() List.of("water"), List.of()
, ,
Double.NaN, Double.NaN,
null); null, Double.NaN, true);
} }
private static long[] parseSeeds(String[] arguments) { private static long[] parseSeeds(String[] arguments) {