From 4b497a7d20fa56e4763f7fba3034eeac509e1d3b Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 1 Sep 2026 23:33:15 -0400 Subject: [PATCH] Prefer longer routes when ranking river sources routing.lengthPreference adds route length to the source score so outlets take their longest legal course. --- .../art/arcane/iris/core/pack/PackHydrologyValidator.java | 1 + .../art/arcane/iris/engine/hydrology/HydrologyPlanner.java | 3 ++- .../iris/engine/hydrology/HydrologyPlannerSettings.java | 6 ++++-- .../iris/engine/hydrology/runtime/IrisHydrologyRuntime.java | 3 ++- .../arcane/iris/engine/object/IrisRiverRoutingConfig.java | 5 +++++ ...drologyPlannerCrossTileCaveAdmissionIntegrationTest.java | 2 +- .../hydrology/HydrologyPlannerGlobalInvariantTest.java | 2 +- .../iris/engine/hydrology/HydrologyPlannerSettingsTest.java | 2 +- .../arcane/iris/engine/hydrology/HydrologyPlannerTest.java | 6 +++--- .../iris/engine/hydrology/HydrologyStyledGeometryTest.java | 5 +++-- .../iris/engine/hydrology/HydrologyTileCacheTest.java | 4 ++-- .../mantle/components/MantleHydrologyComponentTest.java | 2 +- .../java/art/arcane/iris/probe/HydrologyPlannerProbe.java | 4 ++-- 13 files changed, 28 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java index 371f60af3..9e8715371 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java @@ -278,6 +278,7 @@ final class PackHydrologyValidator { PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "uphillPenalty", 0D, 128D, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "slopePenalty", 0D, 16D, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "confluenceAttraction", 0D, 1D, errors); + PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "lengthPreference", 0D, 8D, errors); int tileSize = integerValue(routing, "tileSize", 2048); int sampleSpacing = integerValue(routing, "sampleSpacing", 64); diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java index 7be078701..1607353e6 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java @@ -2113,9 +2113,10 @@ public final class HydrologyPlanner { addSourceDiagnostic(node, surface, stable, HydrologyCandidateRejection.COURSE_TOO_SHORT, diagnostics); continue; } + // Elevation and route length share the same scale: 1000 route blocks count as 100 blocks of height. double score = (required ? 1.0E15D : 0D) + weight * 1.0E9D - + terrain.naturalHeight() * 1.0E5D + + (terrain.naturalHeight() + routeLength * 0.1D * settings.routing().lengthPreference()) * 1.0E5D + routing.potential()[node.index()] + HydrologyHash.unit(stable); candidates.add(new SourceCandidate(node.index(), stable, score, required)); diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java index b288bec9d..a0bc9c701 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java @@ -35,7 +35,7 @@ public record HydrologyPlannerSettings( Source undergroundSources = new Source(true, 0.25D, Integer.MIN_VALUE, 0, 1, 512); return new HydrologyPlannerSettings( 63, - new Routing(2048, 64, 8192, 8192, 384, 192, 1.5D, 24D, 2D, 0.2D), + new Routing(2048, 64, 8192, 8192, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D), new Surface(true, surfaceSources, 4, 8, 2, 4, 10, 1.5D, Banks.defaults()), new Hydraulics(8), new Underground(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true), @@ -151,7 +151,8 @@ public record HydrologyPlannerSettings( double valleyPreference, double uphillPenalty, double slopePenalty, - double confluenceAttraction + double confluenceAttraction, + double lengthPreference ) { public Routing { if (tileSize < 32 || sampleSpacing < 4 || tileSize % sampleSpacing != 0) { @@ -177,6 +178,7 @@ public record HydrologyPlannerSettings( requireFiniteNonNegative(uphillPenalty, "uphillPenalty"); requireFiniteNonNegative(slopePenalty, "slopePenalty"); requireFiniteNonNegative(confluenceAttraction, "confluenceAttraction"); + requireFiniteNonNegative(lengthPreference, "lengthPreference"); } // Route refinement is derived from the lattice, never authored. diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java b/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java index 6dfbcb5c9..733f8f217 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java @@ -671,7 +671,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable { routing.getValleyPreference(), routing.getUphillPenalty(), routing.getSlopePenalty(), - routing.getConfluenceAttraction() + routing.getConfluenceAttraction(), + routing.getLengthPreference() ); HydrologyPlannerSettings.Source plannerSurfaceSources = new HydrologyPlannerSettings.Source( surfaceEnabled, diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverRoutingConfig.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverRoutingConfig.java index f19adf4c8..f1456ac47 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverRoutingConfig.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverRoutingConfig.java @@ -70,4 +70,9 @@ public class IrisRiverRoutingConfig { @MaxNumber(1) @Desc("How strongly routes are drawn toward existing drainage.") private double confluenceAttraction = 0.2D; + + @MinNumber(0) + @MaxNumber(8) + @Desc("How strongly longer source-to-outlet routes are preferred when choosing river sources; 0 ranks by elevation alone.") + private double lengthPreference = 1D; } diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerCrossTileCaveAdmissionIntegrationTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerCrossTileCaveAdmissionIntegrationTest.java index b8518f301..c2ac2671f 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerCrossTileCaveAdmissionIntegrationTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerCrossTileCaveAdmissionIntegrationTest.java @@ -252,7 +252,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest { ); return new HydrologyPlannerSettings( 63, - new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D), + new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D, 1D), new HydrologyPlannerSettings.Surface( true, surfaceSources, diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerGlobalInvariantTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerGlobalInvariantTest.java index 90fb3a53a..835adb226 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerGlobalInvariantTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerGlobalInvariantTest.java @@ -668,7 +668,7 @@ public class HydrologyPlannerGlobalInvariantTest { 12D, 0.5D, 0.1D - ), + , 1D), new HydrologyPlannerSettings.Surface( surfaceEnabled, surfaceSources, diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java index eb15696de..fe6bb08d3 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java @@ -79,7 +79,7 @@ public class HydrologyPlannerSettingsTest { 0D, 0D, 0D - ); + , 1D); HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0); HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface( true, diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerTest.java index 07b110800..de0ae6303 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerTest.java @@ -581,7 +581,7 @@ public class HydrologyPlannerTest { routing.valleyPreference(), routing.uphillPenalty(), routing.slopePenalty(), - routing.confluenceAttraction() + routing.confluenceAttraction(), 1D ), base.surface(), base.hydraulics(), @@ -1733,7 +1733,7 @@ public class HydrologyPlannerTest { ); return new HydrologyPlannerSettings( 63, - new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D), + new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D), new HydrologyPlannerSettings.Surface( surfaceDensity > 0D || surfaceSources.maximumPerTile() > 0, surfaceSources, @@ -1800,7 +1800,7 @@ public class HydrologyPlannerTest { 24D, 2D, 0.2D - ), + , 1D), new HydrologyPlannerSettings.Surface( true, new HydrologyPlannerSettings.Source(true, 8D, 80, 0, 8, 128), diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyStyledGeometryTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyStyledGeometryTest.java index ff7155356..d9c00d093 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyStyledGeometryTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyStyledGeometryTest.java @@ -96,7 +96,8 @@ public class HydrologyStyledGeometryTest { widestSurfaceWidth = Math.max(widestSurfaceWidth, segment.width()); } assertTrue(widestSurfaceWidth >= surfaceStart.width()); - assertTrue(widestSurfaceWidth <= surfaceSettings.surface().maximumWidth() * 2); + assertTrue(widestSurfaceWidth <= (int) Math.ceil(surfaceSettings.surface().maximumWidth() + * Math.max(2D, surfaceSettings.surface().banks().springWidthRatio()))); RiverCourse underground = courses(undergroundFirst, RiverCourseType.UNDERGROUND).getFirst(); DrainageNode undergroundSource = undergroundFirst.node(underground.sourceNodeId().orElseThrow()).orElseThrow(); @@ -194,7 +195,7 @@ public class HydrologyStyledGeometryTest { ); return new HydrologyPlannerSettings( 63, - new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D), + new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D), new HydrologyPlannerSettings.Surface( surfaceDensity > 0D, surfaceSources, diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java index 33a28735c..72258e553 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java @@ -243,7 +243,7 @@ public class HydrologyTileCacheTest { ); return new HydrologyPlannerSettings( 63, - new HydrologyPlannerSettings.Routing(64, 16, 128, 64, 0, 0, 0D, 0D, 0D, 0D), + new HydrologyPlannerSettings.Routing(64, 16, 128, 64, 0, 0, 0D, 0D, 0D, 0D, 1D), new HydrologyPlannerSettings.Surface(false, disabled, 2, 4, 1, 2, 4, 1D, HydrologyPlannerSettings.Banks.defaults()), new HydrologyPlannerSettings.Hydraulics(3), @@ -282,7 +282,7 @@ public class HydrologyTileCacheTest { ); return new HydrologyPlannerSettings( 63, - new HydrologyPlannerSettings.Routing(64, 16, 128, 96, 16, 8, 0.5D, 12D, 0.5D, 0.1D), + new HydrologyPlannerSettings.Routing(64, 16, 128, 96, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D), new HydrologyPlannerSettings.Surface(true, surfaceSources, 4, 8, 2, 3, 20, 1.5D, tileBoundedBanks()), new HydrologyPlannerSettings.Hydraulics(3), new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false), diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java index d279bbbae..9716a4925 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java @@ -1530,7 +1530,7 @@ public class MantleHydrologyComponentTest { ); return new HydrologyPlannerSettings( 63, - new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D), + new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D), new HydrologyPlannerSettings.Surface( true, surfaceSources, diff --git a/probe/src/main/java/art/arcane/iris/probe/HydrologyPlannerProbe.java b/probe/src/main/java/art/arcane/iris/probe/HydrologyPlannerProbe.java index 75323ad1b..4ab40b4cf 100644 --- a/probe/src/main/java/art/arcane/iris/probe/HydrologyPlannerProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/HydrologyPlannerProbe.java @@ -228,7 +228,7 @@ public final class HydrologyPlannerProbe { 12D, 0.5D, 0.1D - ), + , 1D), new HydrologyPlannerSettings.Surface( true, surfaceSources, @@ -439,7 +439,7 @@ public final class HydrologyPlannerProbe { 12D, 0.5D, 0.2D - ), + , 1D), new HydrologyPlannerSettings.Surface( true, surfaceSources,