Tunabelly

This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-22 17:04:56 -04:00
1 parent 197d2c2461
commit 30c533feaf
114 files changed
+4071 -3994

No files matched your search

@@ -24,7 +24,7 @@ public class HydrologyCacheBudgetTest {
HydrologyCacheBudget budget = HydrologyCacheBudget.forHeap(heap, regionalEnabled);
long total = budget.tileBytes() + budget.columnBytes() + budget.diagnosticBytes()
+ budget.routingBytes() + budget.ownerBytes() + budget.edgeBytes() + budget.regionalDraftBytes()
+ budget.regionalCoarseBytes() + budget.regionalTerrainBytes() + budget.regionalReachBytes();
+ budget.regionalCoarseBytes() + budget.regionalTerrainBytes();
assertTrue(total <= Math.clamp(heap / 8L, 8L * MEBIBYTE, 256L * MEBIBYTE));
assertTrue(total >= 8L * MEBIBYTE);
assertEquals(budget.tileBytes() / 6L, budget.columnBytes());
@@ -40,7 +40,7 @@ public class HydrologyCacheBudgetTest {
assertEquals(24L * MEBIBYTE, localOnly.routingBytes());
assertEquals(8L * MEBIBYTE, localOnly.edgeBytes());
assertEquals(0L, localOnly.regionalDraftBytes() + localOnly.regionalCoarseBytes()
+ localOnly.regionalTerrainBytes() + localOnly.regionalReachBytes());
+ localOnly.regionalTerrainBytes());
assertThrows(IllegalArgumentException.class, () -> HydrologyCacheBudget.forHeap(0L, false));
}
@@ -112,7 +112,7 @@ public class HydrologyCacheBudgetTest {
HydrologyRegionalGraph.Label second = new HydrologyRegionalGraph.Label(
new HydrologyRegionalGraph.LabelState(3, 2, 63, 1D, 1D, 0), downstream);
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(
new HydrologyRegionalGraph.Label[]{first, second, null}, new int[4], List.of());
new HydrologyRegionalGraph.Label[]{first, second, null}, new int[4], List.of(), new int[4]);
assertEquals(3, tree.retainedLabelCount());
}
}
@@ -0,0 +1,66 @@
package art.arcane.iris.generation.hydrology;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class HydrologyCoastalFallAdmissionTest {
private static final HydrologyPlannerSettings SETTINGS = HydrologyPlannerSettings.defaults();
private static final HydrologyTerrainSampler TERRAIN = (x, z) -> x >= 80
? HydrologyTerrainSample.ocean(54, "ocean") : HydrologyTerrainSample.openLand(95, 0D, "land");
@Test
public void aConnectedMouthAdmitsTheCoastalFallAtTheNaturalSeaSurface() {
RiverCourse course = course(HydrologyFeatureType.MOUTH, 80);
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN);
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
assertTrue(compiler.surfaceFootprint(course).toString(), compiler.surfaceFootprint(course).accepted());
HydrologyCaveCourseFilter.Result result = filter().filter(List.of(), List.of(), List.of(), List.of(course),
compiler.compileValidation(List.of(course)), diagnostics);
assertEquals(diagnostics.toString(), List.of(course), result.courses());
assertEquals(1, result.cavePlans().size());
assertTrue(result.cavePlans().getFirst().accepted());
assertTrue(diagnostics.isEmpty());
}
@Test
public void anInlandTerminalOrDisconnectedMouthCannotOpenTheFallAtSeaLevel() {
for (RiverCourse course : List.of(course(HydrologyFeatureType.SURFACE_POOL, 80),
course(HydrologyFeatureType.MOUTH, 81))) {
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN);
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
HydrologyCaveCourseFilter.Result result = filter().filter(List.of(), List.of(), List.of(), List.of(course),
compiler.compileValidation(List.of(course)), diagnostics);
assertTrue(result.courses().isEmpty());
assertFalse(diagnostics.isEmpty());
assertEquals(HydrologyCandidateRejection.CAVE_CONTAINMENT, diagnostics.getFirst().rejection());
}
}
private static HydrologyCaveCourseFilter filter() {
return new HydrologyCaveCourseFilter(new HydrologyTerrainCaveVoxelView(TERRAIN, SETTINGS.seaLevel(), -64, 256),
new HydrologyCaveCourseFilter.Options(false, 8192, 8192));
}
private static RiverCourse course(HydrologyFeatureType terminal, int receiverX) {
int sea = SETTINGS.seaLevel();
HydraulicSegment approach = new HydraulicSegment(1L, 4L, HydrologyFeatureType.SURFACE_POOL,
93, 93, 4, 2, false, false, List.of(new HydrologyPoint(0, 93, 0), new HydrologyPoint(79, 93, 0)),
HydraulicChannelProfile.uniform(4, 2));
HydraulicSegment fall = new HydraulicSegment(2L, 4L, HydrologyFeatureType.WATERFALL,
93, sea, 4, 2, true, true, List.of(new HydrologyPoint(79, 93, 0), new HydrologyPoint(80, sea, 0)),
HydraulicChannelProfile.uniform(4, 2));
HydraulicSegment receiving = new HydraulicSegment(3L, 4L, terminal, sea, sea, 4, 2, false, false,
List.of(new HydrologyPoint(receiverX, sea, 0)), HydraulicChannelProfile.uniform(4, 2));
return new RiverCourse(4L, RiverCourseType.SURFACE, OptionalLong.of(5L), OptionalLong.of(6L),
"default", 1, List.of(), List.of(approach, fall, receiving));
}
}
@@ -0,0 +1,191 @@
package art.arcane.iris.generation.hydrology;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.doReturn;
public class HydrologyDrainageGeometryTest {
@Test
public void sourceBranchesShareExactlyTheSameDescendingTrunk() {
HydrologyTerrainSampler terrain = (x, z) -> land(120 - x / 8);
Fixture fixture = fixture(terrain);
HydrologyDrainageGeometry geometry = fixture.geometry();
List<HydrologyPoint> first = geometry.edge(0);
List<HydrologyPoint> tributary = geometry.edge(3);
List<HydrologyPoint> trunk = geometry.edge(1);
assertFalse(first.isEmpty());
assertFalse(tributary.isEmpty());
assertFalse(trunk.isEmpty());
assertEquals(first.getLast(), trunk.getFirst());
assertEquals(tributary.getLast(), trunk.getFirst());
for (List<HydrologyPoint> edge : List.of(first, tributary, trunk)) {
for (int index = 1; index < edge.size(); index++) {
assertTrue(edge.get(index - 1).y() >= edge.get(index).y());
}
}
assertEquals(geometry.head(1), trunk.getFirst().y());
}
@Test
public void repeatedAndConcurrentSourcesReuseOneGeometryComputation() {
AtomicInteger sampled = new AtomicInteger();
Fixture fixture = fixture((x, z) -> {
sampled.incrementAndGet();
return land(120 - x / 8);
});
ArrayList<CompletableFuture<List<HydrologyPoint>>> tasks = new ArrayList<>();
for (int index = 0; index < 16; index++) {
tasks.add(CompletableFuture.supplyAsync(() -> fixture.geometry().edge(0)));
}
List<HydrologyPoint> expected = tasks.getFirst().join();
assertFalse(expected.isEmpty());
for (CompletableFuture<List<HydrologyPoint>> task : tasks) {
assertSame(expected, task.join());
}
int samples = sampled.get();
assertSame(expected, fixture.geometry().edge(0));
assertEquals(samples, sampled.get());
}
@Test
public void aFailedRequiredSampleCanBeRetriedWithoutCachingItsFailure() {
AtomicInteger attempts = new AtomicInteger();
IllegalStateException unavailable = new IllegalStateException("Terrain unavailable");
Fixture fixture = fixture((x, z) -> {
if (x == 31 && attempts.incrementAndGet() == 1) {
throw unavailable;
}
return land(120 - x / 8);
});
assertSame(unavailable, assertThrows(IllegalStateException.class, () -> fixture.geometry().edge(0)));
assertFalse(fixture.geometry().edge(0).isEmpty());
}
@Test
public void detailedReliefDoesNotSendTheSharedRiverSearchingForAnotherRoute() {
Fixture flat = fixture((x, z) -> land(120 - x / 8));
Fixture broken = fixture((x, z) -> land(x > 24 && x < 40 ? 90 : 120 - x / 8));
assertEquals(flat.geometry().edge(0), broken.geometry().edge(0));
assertFalse(broken.geometry().edge(0).isEmpty());
}
@Test
public void aThinExcludedPolicyBandStillBlocksAnEdge() {
HydrologyTerrainSample blocked = spy(land(116));
doReturn(false).when(blocked).transitAllowed();
Fixture fixture = fixture((x, z) -> x == 31 ? blocked : land(120 - x / 8));
assertTrue(fixture.geometry().edge(0).isEmpty());
}
@Test
public void coastalRootKeepsItsLandwardPointAndDescendingHeadAboveTheSea() {
Fixture fixture = fixture((x, z) -> land(120 - x / 8));
HydrologyPoint root = fixture.geometry().anchor(2);
assertEquals(128, root.x());
assertEquals(0, root.z());
assertTrue(root.y() >= fixture.planner().settings.seaLevel());
assertEquals(root, fixture.geometry().edge(1).getLast());
}
@Test
public void everyOutletTrialReusesTheSameGridCostsAndPolicyEdges() {
Fixture fixture = fixture((x, z) -> land(120 - x / 8));
HydrologySourcePlanner sources = fixture.planner().sourcePlanner;
HydrologyDrainageGraph graph = fixture.grid().drainage(sources);
sources.buildRouting(fixture.grid(), List.of(fixture.outlet()), true);
sources.buildRouting(fixture.grid(), List.of(fixture.outlet()), false);
assertSame(graph, fixture.grid().drainage(sources));
for (HydrologyGridNode downstream : fixture.grid().nodes()) {
for (int direction = 0; direction < HydrologySourcePlanner.ROUTING_OFFSETS.size(); direction++) {
HydrologyGridOffset offset = HydrologySourcePlanner.ROUTING_OFFSETS.get(direction);
HydrologyGridNode upstream = fixture.grid().nodeAt(downstream.gridX() + offset.x(), downstream.gridZ() + offset.z());
if (upstream == null) {
assertEquals(-1, graph.upstream(downstream.index(), direction, false));
} else {
assertEquals(upstream.index(), graph.upstream(downstream.index(), direction, false));
assertEquals(sources.routeCost(upstream, downstream, offset,
sources.minimumNeighborHeight(fixture.grid(), downstream)), graph.cost(downstream.index(), direction), 0D);
}
}
}
}
@Test
public void aCuttableRidgeDoesNotLiftTheSharedUpstreamRiverAboveItsSpring() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Surface surface = base.surface();
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(base.seaLevel(), base.routing(),
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(),
surface.maximumWidth(), surface.minimumDepth(), surface.maximumDepth(), 64,
surface.shoreWidth(), surface.banks()), base.hydraulics(), base.underground(),
base.outlets(), base.geometry(), base.deepFluids(), base.surfacePools(),
base.widestShoreBiomeWidth(), base.seaCaves(), base.surfacePolicyBounds());
HydrologyTerrainSampler terrain = (x, z) -> land(x == 64 ? 154 : 120 - x / 8);
Fixture fixture = fixture(terrain, settings, terrain);
HydrologyDrainageGeometry geometry = fixture.geometry();
int depth = (surface.minimumDepth() + surface.maximumDepth()) / 2;
assertEquals(120 - surface.banks().sink() - depth, geometry.head(0));
assertTrue(geometry.head(0) >= geometry.head(1));
assertTrue(geometry.head(1) >= geometry.head(2));
assertTrue(154 - geometry.head(1) + depth <= 64);
assertEquals(geometry.edge(0).getLast(), geometry.edge(1).getFirst());
}
@Test
public void aSourceExcludedDepressionDoesNotLowerTheSharedOutletHead() {
HydrologyTerrainSample excluded = spy(land(90));
doReturn(false).when(excluded).surfaceSourceAllowed();
HydrologyTerrainSampler terrain = (x, z) -> z == 0 ? land(120 - x / 8) : excluded;
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
Fixture fixture = fixture(terrain, settings, terrain);
HydrologyPlannerSettings.Surface surface = settings.surface();
int depth = (surface.minimumDepth() + surface.maximumDepth()) / 2;
assertEquals(104 - surface.banks().sink() - depth, fixture.geometry().head(2));
assertTrue(fixture.geometry().head(0) >= fixture.geometry().head(1));
assertTrue(fixture.geometry().head(1) >= fixture.geometry().head(2));
assertEquals(fixture.geometry().edge(0).getLast(), fixture.geometry().edge(1).getFirst());
}
private Fixture fixture(HydrologyTerrainSampler terrain) {
return fixture(terrain, HydrologyPlannerSettings.defaults(), (x, z) -> land(120 - x / 8));
}
private Fixture fixture(HydrologyTerrainSampler terrain, HydrologyPlannerSettings settings,
HydrologyTerrainSampler coarseTerrain) {
HydrologyPlanner planner = new HydrologyPlanner(91L, settings, terrain);
ArrayList<HydrologyGridNode> nodes = new ArrayList<>();
for (int index = 0; index < 9; index++) {
int x = index % 3 * 64;
int z = index / 3 * 64;
nodes.add(new HydrologyGridNode(index, index % 3, index / 3, x, z, index + 1L, coarseTerrain.sample(x, z)));
}
HydrologySampledGrid grid = new HydrologySampledGrid(0, 0, 0, 0, 192, 3, 64, nodes);
OutletCandidate outlet = new OutletCandidate(2, -1, new RiverOutlet(10L, HydrologyFeatureType.MOUTH,
3L, new HydrologyPoint(128, 104, 0), new HydrologyPoint(129, 63, 0), 63, true));
int[] parent = {1, 2, -1, 1, 1, 2, 3, 4, 5};
HydrologyDrainageGeometry geometry = new HydrologyDrainageGeometry(planner,
new HydrologyDrainageGeometry.Basin(grid, parent, new int[9], List.of(outlet)));
return new Fixture(planner, grid, outlet, geometry);
}
private static HydrologyTerrainSample land(int height) {
return HydrologyTerrainSample.openLand(height, 0D, "land");
}
private record Fixture(HydrologyPlanner planner, HydrologySampledGrid grid,
OutletCandidate outlet, HydrologyDrainageGeometry geometry) {
}
}
@@ -154,7 +154,7 @@ public class HydrologyFallbackDiagnosticsTest {
}
private static HydrologyRoutingPlan routing(List<OutletCandidate> outlets) {
return new HydrologyRoutingPlan(new double[]{0D}, new int[]{-1}, new int[]{0}, new int[]{0}, outlets, true);
return new HydrologyRoutingPlan(new double[]{0D}, new int[]{-1}, new int[]{0}, new int[]{0}, outlets, true, null);
}
private static OutletCandidate outlet(int index) {
@@ -247,7 +247,7 @@ public class HydrologyFallbackParallelismTest {
}
private static HydrologyRoutingPlan routing(List<OutletCandidate> outlets) {
return new HydrologyRoutingPlan(new double[]{0D}, new int[]{-1}, new int[]{0}, new int[]{0}, outlets, true);
return new HydrologyRoutingPlan(new double[]{0D}, new int[]{-1}, new int[]{0}, new int[]{0}, outlets, true, null);
}
private static OutletCandidate outlet(int index) {
@@ -1,5 +1,6 @@
package art.arcane.iris.generation.hydrology;
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
import org.junit.Test;
import java.util.ArrayDeque;
@@ -1508,7 +1509,7 @@ public class HydrologyFootprintCompilerTest {
}
@Test
public void lowBanksKeepTheirNaturalHeightAndReportUncontainedWater() {
public void lowBanksFillWithinTheirPolicyAndReportRemainingUncontainedWater() {
HydraulicSegment channel = new HydraulicSegment(
529L,
528L,
@@ -1539,25 +1540,25 @@ public class HydrologyFootprintCompilerTest {
HydrologyFootprintCompiler lowBankCompiler = compiler(lowBankTerrain);
RiverFootprint lowBankFootprint = lowBankCompiler.compile(List.of(course));
HydrologyColumnSample lowBank = lowBankFootprint.sample(12, lowBankZ).orElseThrow();
HydrologyTerrainSampler deepBankTerrain = (int x, int z) -> HydrologyTerrainSample.openLand(
x == 12 && z == lowBankZ ? 68 : 82,
0D,
"parent"
);
SurfaceRiverPolicy oneBlockFill = new SurfaceRiverPolicy("restricted-bank",
null, null, null, null, null, null, 1);
HydrologyTerrainSampler deepBankTerrain = (int x, int z) -> x == 12 && z == lowBankZ
? HydrologyTerrainSample.openLand(68, 0D, "parent").withSurfacePolicy(oneBlockFill)
: HydrologyTerrainSample.openLand(82, 0D, "parent");
RiverFootprint deepBankFootprint = compiler(deepBankTerrain).compile(List.of(course));
HydrologyColumnSample deepBank = deepBankFootprint.sample(12, lowBankZ).orElseThrow();
assertEquals(0, unsupportedBankCells(highFootprint, 528L));
// A low bank remains below the water and is reported without raising terrain.
assertEquals(69, lowBank.naturalHeight());
assertEquals(69, lowBank.terrainHeight());
assertTrue(unsupportedBankCells(lowBankFootprint, 528L) > 0);
assertEquals(70, lowBank.terrainHeight());
assertEquals(0, unsupportedBankCells(lowBankFootprint, 528L));
for (HydrologyColumnSample sample : lowBankFootprint.columns().values()) {
assertTrue(sample.terrainHeight() <= sample.naturalHeight());
HydrologyColumnLayer layer = sample.primarySurfaceLayerOrNull();
int fillLimit = layer != null && layer.channel() ? 0 : 8;
assertTrue(sample.terrainHeight() - sample.naturalHeight() <= fillLimit);
}
// Two blocks short: the bank keeps its natural height and the edge is reported.
assertEquals(68, deepBank.naturalHeight());
assertEquals(68, deepBank.terrainHeight());
assertEquals(69, deepBank.terrainHeight());
assertTrue(unsupportedBankCells(deepBankFootprint, 528L) > 0);
}
@@ -394,6 +394,30 @@ public class HydrologyModelTest {
assertEquals(72, sample.terrainHeight());
}
@Test
public void overlappingRaisedBanksRetainOwnershipWithoutAccumulatingFill() {
for (boolean sameFeature : List.of(true, false)) {
for (boolean reverse : List.of(true, false)) {
FootprintMutableColumn column = new FootprintMutableColumn(0, 0,
HydrologyTerrainSample.openLand(77, 0D, "parent"), 63);
int[] beds = reverse ? new int[]{81, 80, 77, 75} : new int[]{75, 77, 80, 81};
for (int index = 0; index < beds.length; index++) {
HydrologyFeatureRef feature = feature(HydrologyFeatureType.SURFACE_POOL,
sameFeature ? 30L : 30L + index, 80);
column.add(surfaceLayer(feature, beds[index], beds[index],
false, false, true, beds[index] != 77, false));
}
HydrologyColumnSample merged = column.build();
assertEquals(77, merged.naturalHeight());
assertEquals(81, merged.terrainHeight());
assertEquals(4, merged.terrainHeight() - merged.naturalHeight());
assertTrue(merged.primarySurfaceLayerOrNull().terrainOwned());
assertEquals(sameFeature ? 1 : 4, merged.layers().size());
}
}
}
@Test
public void uncutShoreRetainsItsBiomeWithoutOverridingAnOwnedBank() {
HydrologyColumnLayer shore = surfaceLayer(feature(HydrologyFeatureType.SURFACE_POOL, 30L, 75),
@@ -17,7 +17,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class HydrologyPlanHashTest {
private static final String EXPECTED_PLAN_DIGEST = "9ec4c9cf7dbdad16976041a70853b909816655170cd694058bf359092c2d5a10";
private static final String EXPECTED_PLAN_DIGEST = "e54461c339854842f7fc7b2e85bd700b57942a2c691f5a249d8e498729378296";
private static final List<HydrologyTileKey> TILES = List.of(
new HydrologyTileKey(0, 0),
@@ -17,7 +17,6 @@ import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -35,7 +34,6 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
@@ -669,25 +667,18 @@ public class HydrologyPlannerTest {
}
@Test
public void surfaceRouteMemoPreservesCompleteOwnerOutputAcrossPublicationPasses() throws Exception {
public void sharedDrainagePreservesCompleteOwnerOutputAcrossPublicationPasses() {
for (boolean organic : new boolean[]{false, true}) {
HydrologyPlannerSettings settings = organic ? organicShapeSettings()
: standardSettings(4D, 2D, true, false, List.of());
HydrologyTerrainSampler terrain = organic ? organicShapeTerrain() : rollingCoast(112);
SurfaceRouteMemos uncached = new SurfaceRouteMemos(false);
SurfaceRouteMemos cached = new SurfaceRouteMemos(true);
HydrologyTile expected = planWithSurfaceRouteMemos(new HydrologyPlanner(642L, settings, terrain), uncached);
HydrologyTile actual = planWithSurfaceRouteMemos(new HydrologyPlanner(642L, settings, terrain), cached);
HydrologyPlanner planner = new HydrologyPlanner(642L, settings, terrain);
HydrologyTile expected = planner.plan(TILE);
planner.clearOwnerDrafts();
HydrologyTile actual = planner.plan(TILE);
assertEquals(expected, actual);
assertEquals(expected.courses(), actual.courses());
assertEquals(expected.localDiagnosticCandidates(), actual.localDiagnosticCandidates());
assertTrue(cached.computations <= uncached.computations);
if (organic) {
assertTrue("hits=" + cached.hits, cached.hits > 0);
assertTrue("uncached=" + uncached.computations + " cached=" + cached.computations,
cached.computations < uncached.computations);
}
}
}
@@ -717,7 +708,10 @@ public class HydrologyPlannerTest {
for (HydraulicSegment segment : course.segments()) {
if (segment.drop() > 0) {
assertTrue(segment.type().isDrop());
assertFalse(segment.fallingFluid());
if (segment.fallingFluid()) {
assertEquals(HydrologyFeatureType.WATERFALL, segment.type());
assertTrue(tile.cavePlan(course.id()).orElseThrow().accepted());
}
}
}
}
@@ -1124,7 +1118,9 @@ public class HydrologyPlannerTest {
);
for (HydraulicSegment segment : segments(cliff, HydrologyFeatureType.WATERFALL)) {
assertTrue(segment.drop() > 0);
assertFalse(segment.fallingFluid());
assertTrue(segment.fallingFluid());
assertTrue(segment.receivingPool());
assertTrue(cliff.cavePlan(segment.courseId()).orElseThrow().accepted());
assertTrue(segment.centerline().size() >= 2);
assertTrue(segment.width() >= 1);
}
@@ -1139,7 +1135,8 @@ public class HydrologyPlannerTest {
HydraulicSegment receiver = course.segments().get(index + 1);
assertTrue(approach.type().isSurface());
assertTrue(receiver.type().isSurface());
assertEquals(approach.end(), waterfall.start());
assertEquals(approach.downstreamHeadY(), waterfall.upstreamHeadY());
assertTrue(approach.end().distanceSquared2D(waterfall.start()) <= 2L);
assertEquals(waterfall.end(), receiver.start());
sawBlendedWaterfall = true;
}
@@ -1206,6 +1203,13 @@ public class HydrologyPlannerTest {
}
}
assertTrue("courses=" + surfaceCourses(tile), aggregateTransition);
for (HydrologyColumnSample column : tile.footprint().columns().values()) {
for (HydrologyColumnLayer layer : column.layers()) {
if (layer.feature().type().isSurface() && layer.channel() && layer.terrainOwned()) {
assertTrue(column.naturalHeight() - layer.bedY() <= settings.surface().maximumIncision());
}
}
}
}
@Test
@@ -1879,7 +1883,10 @@ public class HydrologyPlannerTest {
}
assertTrue(bed);
assertEquals(drop.fallingFluid(), falling);
assertFalse(drop.fallingFluid());
if (drop.fallingFluid()) {
assertEquals(HydrologyFeatureType.WATERFALL, drop.type());
assertTrue(tile.cavePlan(drop.courseId()).orElseThrow().accepted());
}
if (!receiving) {
HydrologyColumnSample endColumn = tile.columnAt(drop.end().x(), drop.end().z()).orElseThrow();
receiving = endColumn.layers().stream().anyMatch((HydrologyColumnLayer layer) ->
@@ -2210,6 +2217,15 @@ public class HydrologyPlannerTest {
).orElseThrow();
assertEquals(course.id(), located.courseId());
assertEquals(sinkhole.id(), located.segmentId());
for (HydrologyColumnSample column : first.footprint().columns().values()) {
for (HydrologyColumnLayer layer : column.layers()) {
if (layer.feature().courseId() == course.id() && layer.feature().type().isSurface()
&& layer.channel()) {
assertTrue("Surface pond exceeded the channel incision limit at " + column.x() + "," + column.z(),
column.naturalHeight() - layer.bedY() <= settings.surface().maximumIncision());
}
}
}
}
@Test
@@ -3446,57 +3462,6 @@ public class HydrologyPlannerTest {
}
return false;
}
@SuppressWarnings("unchecked")
private HydrologyTile planWithSurfaceRouteMemos(HydrologyPlanner planner, SurfaceRouteMemos memos) throws Exception {
Class<?> samplesClass = Class.forName(HydrologyPlanner.class.getName() + "$PlanningSamples");
Constructor<?> constructor = samplesClass.getDeclaredConstructor();
constructor.setAccessible(true);
Object samples = constructor.newInstance();
Field routes = samplesClass.getDeclaredField("surfaceRoutes");
routes.setAccessible(true);
routes.set(samples, memos);
Field scope = HydrologyPlanner.class.getDeclaredField("planningSamples");
scope.setAccessible(true);
ThreadLocal<Object> local = (ThreadLocal<Object>) scope.get(planner);
local.set(samples);
try {
return planner.plan(TILE);
} finally {
local.remove();
}
}
private static final class SurfaceRouteMemos extends IdentityHashMap<Object, HashMap<Object, List<HydrologyPoint>>> {
private final boolean retain;
private int hits;
private int computations;
private SurfaceRouteMemos(boolean retain) {
this.retain = retain;
}
@Override
public HashMap<Object, List<HydrologyPoint>> computeIfAbsent(Object key,
Function<? super Object, ? extends HashMap<Object, List<HydrologyPoint>>> ignored) {
return super.computeIfAbsent(key, unused -> new HashMap<>() {
@Override
public List<HydrologyPoint> computeIfAbsent(Object route,
Function<? super Object, ? extends List<HydrologyPoint>> computation) {
if (!retain) {
clear();
}
if (containsKey(route)) {
hits++;
}
return super.computeIfAbsent(route, missing -> {
computations++;
return computation.apply(missing);
});
}
});
}
}
private HydrologyPlanner countedPlanner(CountingNaturalSampler sampler) {
return new HydrologyPlanner(91L, standardSettings(4D, 0D, true, false, List.of()),
sampler.delegate, sampler, HydrologyGeometrySampler.deterministic(sampler.delegate),
@@ -31,9 +31,7 @@ public class HydrologyRegionalBankSupportTest {
HydrologyTerrainSampler terrain = depression(false);
List<HydrologyPoint> path = path();
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(path, "default", false, terrain);
assertEquals(path, refined);
assertNull(validate(planner, path, terrain).rejection());
SurfaceCourseResult built = build(path, terrain);
assertTrue(built.accepted());
@@ -44,7 +42,7 @@ public class HydrologyRegionalBankSupportTest {
assertEquals(0, footprint.uncontainedWetCells());
assertTrue(course.hydraulicallyNonRising());
for (SurfaceLayerColumn column : footprint.columns()) {
assertTrue(column.layer().bedY() <= column.terrain().naturalHeight());
assertTrue(column.layer().bedY() - column.terrain().naturalHeight() <= SETTINGS.surface().maximumIncision());
}
SurfaceLayerColumn center = footprint.columns().stream()
.filter(column -> column.x() == 132 && column.z() == 0).findFirst().orElseThrow();
@@ -58,67 +56,16 @@ public class HydrologyRegionalBankSupportTest {
HydrologyTerrainSampler terrain = depression(true);
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
validate(planner, path(), terrain).rejection());
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(path(), "default", false, terrain);
if (!refined.isEmpty()) {
assertFalse(build(refined, terrain).accepted());
}
assertNull(validate(planner, path(), terrain).rejection());
SurfaceCourseResult built = build(path(), terrain);
assertFalse(built.accepted());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, built.rejection());
assertEquals(17, built.rejectionDetail());
}
@Test
public void levelCentersNeedNoBankSamplesAndSupportedDipsCannotRaiseIncomingHead() {
AtomicInteger samples = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
assertTrue(Math.abs(x - 132) <= 5 && Math.abs(z) <= 5);
samples.incrementAndGet();
return depression(false).sample(x, z);
};
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(SETTINGS);
assertEquals(79, hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
132, 0, 1D, 0D, HydrologyTerrainSample.openLand(80, 0D, "land"), 79), terrain));
assertEquals(0, samples.get());
assertEquals(78, hydraulics.supportedHead(station(78), terrain));
assertTrue(samples.get() > 0 && samples.get() < 128);
}
@Test
public void unavailableCrossSectionOrPerimeterCannotCertifyAContainedDip() {
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(SETTINGS);
HydrologyTerrainSampler crossMissing = (x, z) -> x == 132 && z == 2 ? null : depression(false).sample(x, z);
HydrologyTerrainSampler perimeterMissing = (x, z) -> x == 133 ? null : depression(false).sample(x, z);
assertEquals(65, hydraulics.supportedHead(station(80), crossMissing));
assertEquals(65, hydraulics.supportedHead(station(80), perimeterMissing));
}
@Test
public void bankSupportCannotSamplePastTheExistingSearchBudget() throws Exception {
AtomicInteger requests = new AtomicInteger();
HydrologyTerrainSample ground = HydrologyTerrainSample.openLand(80, 0D, "land");
HydrologyTerrainSampler terrain = (x, z) -> {
requests.incrementAndGet();
return ground;
};
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(new HydrologyPlanner(71L, SETTINGS, terrain));
HydrologyRegionalTerrainRefiner.Samples sampled = refiner.new Samples();
for (int index = 0; index < 65536; index++) {
sampled.sample(index, 100000);
if (built.accepted()) {
RiverCourse course = new RiverCourse(91L, RiverCourseType.SURFACE, OptionalLong.of(1L), OptionalLong.of(2L),
"default", 1, List.of(), built.segments());
SurfaceFootprint footprint = new SurfaceFootprintCompiler(SETTINGS, terrain, GEOMETRY).compile(course);
assertFalse(footprint.accepted());
} else {
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, built.rejection());
}
requests.set(0);
assertEquals(65, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(80), sampled));
assertEquals(0, requests.get());
}
private static HydrologyRegionalHydraulics.HeadStation station(int incomingHead) {
return new HydrologyRegionalHydraulics.HeadStation(132, 0, 1D, 0D,
HydrologyTerrainSample.openLand(65, 0D, "land"), incomingHead);
}
private static HydrologyTerrainSampler depression(boolean acrossBanks) {
@@ -14,34 +14,6 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalCacheTest {
@Test
public void searchBudgetDoesNotLimitSubsequentSearchesOrTerrainResults() {
AtomicInteger reads = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
reads.incrementAndGet();
return HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
HydrologyRegionalTerrainRefiner.Samples first = refiner.new Samples();
for (int x = 0; x < 65536; x++) {
assertNotNull(first.sample(x, 0));
}
assertNull(first.sample(65536, 0));
HydrologyRegionalTerrainRefiner.Samples second = refiner.new Samples();
for (int x = 65536; x < 70000; x++) {
assertNotNull(second.sample(x, 0));
}
assertEquals(70000, reads.get());
for (int x = 0; x < 70000; x++) {
assertEquals(70, refiner.sample(x, 0).naturalHeight());
}
int readsBeforeClear = reads.get();
refiner.clear();
assertEquals(70, refiner.sample(0, 0).naturalHeight());
assertEquals(readsBeforeClear + 1, reads.get());
}
@Test
public void clearingTilesRefreshesChangedRegionalTerrain() throws Exception {
AtomicInteger height = new AtomicInteger(70);
@@ -51,7 +23,7 @@ public class HydrologyRegionalCacheTest {
return HydrologyTerrainSample.openLand(height.get(), 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
HydrologyRegionalTerrain refiner = refiner(planner);
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
assertEquals(70, refiner.sample(0, 0).naturalHeight());
height.set(85);
@@ -71,7 +43,7 @@ public class HydrologyRegionalCacheTest {
return available.get() ? HydrologyTerrainSample.openLand(70, 0D, "land") : null;
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
HydrologyRegionalTerrain refiner = refiner(planner);
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
assertNull(refiner.sample(0, 0));
available.set(true);
@@ -82,32 +54,11 @@ public class HydrologyRegionalCacheTest {
}
}
@Test
public void clearingTilesReplansRegionalReachesAgainstCurrentTerrain() throws Exception {
AtomicBoolean flooded = new AtomicBoolean();
HydrologyTerrainSampler terrain = (x, z) -> flooded.get() && x >= 96 && x <= 160 && Math.abs(z) < 32
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
assertEquals(guide, refiner.refine(guide, "default", false, terrain));
flooded.set(true);
cache.clear();
List<HydrologyPoint> actual = refiner.refine(guide, "default", false, terrain);
List<HydrologyPoint> fresh = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain);
assertFalse(fresh.isEmpty());
assertTrue(fresh.stream().anyMatch(point -> Math.abs(point.z()) >= 32));
assertEquals(fresh, actual);
}
}
private static HydrologyRegionalTerrainRefiner refiner(HydrologyPlanner planner) throws Exception {
private static HydrologyRegionalTerrain refiner(HydrologyPlanner planner) throws Exception {
Field routes = HydrologyRegionalPlanner.class.getDeclaredField("routes");
routes.setAccessible(true);
Field refiner = HydrologyRegionalRoute.class.getDeclaredField("terrainRefiner");
Field refiner = HydrologyRegionalRoute.class.getDeclaredField("terrain");
refiner.setAccessible(true);
return (HydrologyRegionalTerrainRefiner) refiner.get(routes.get(planner.regional));
return (HydrologyRegionalTerrain) refiner.get(routes.get(planner.regional));
}
}
@@ -19,7 +19,7 @@ import static org.junit.Assert.assertTrue;
public class HydrologyRegionalCandidateSelectionTest {
@Test
public void lowerMeanderCandidatePassesTheFullPlannerAfterTheFirstBankCutFails() throws Exception {
public void plannedHeadCrossesAChannelDepressionWithoutLoweringTheWholeCourse() throws Exception {
HydrologyTerrainSampler terrain = coast(true);
HydrologyPlanner planner = planner(terrain, 16, 2);
List<HydrologyPoint> guide = guide(terrain);
@@ -33,9 +33,8 @@ public class HydrologyRegionalCandidateSelectionTest {
});
assertTrue(selected.toString(), selected.accepted());
assertEquals(2, attempted.size());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, attempted.getFirst().rejection().rejection());
assertEquals(18, attempted.getFirst().rejection().detail());
assertEquals(1, attempted.size());
assertNull(attempted.getFirst().rejection());
assertEquals(1, selected.network().courses().size());
assertEquals(selected, attempted.getLast());
RiverCourse course = selected.network().courses().getFirst();
@@ -124,8 +123,10 @@ public class HydrologyRegionalCandidateSelectionTest {
}
int[] contributions = new int[9];
Arrays.fill(contributions, 1);
int[] heads = new int[9];
Arrays.fill(heads, 80);
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(selected, contributions,
List.of(new OutletCandidate(8, 8, outlet())));
List.of(new OutletCandidate(8, 8, outlet())), heads);
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
Method build = HydrologyRegionalPlanner.class.getDeclaredMethod("build", HydrologySampledGrid.class,
HydrologyRegionalGraph.Tree.class, int.class, OutletCandidate.class, long.class, List.class);
@@ -51,7 +51,7 @@ public final class HydrologyRegionalFallsTest {
HydrologyTile original = new HydrologyTile(new HydrologyTileKey(0, 0), 1L, SETTINGS.fingerprint(),
SETTINGS.routing().tileSize(), network.nodes(), network.edges(), network.outlets(),
List.of(regional, local), Set.of(regional.id()), clipped.cavePlans(), List.of(), materialized.footprint());
StudioHydrologyTileStore store = new StudioHydrologyTileStore(temporaryFolder.newFolder().toPath(),
PreparedHydrologyTileStore store = new PreparedHydrologyTileStore(temporaryFolder.newFolder().toPath(),
new HydrologyTileCache.SharedCacheScope("regional-ownership", 1L, 128, "overworld", SETTINGS.fingerprint()),
SETTINGS.routing().tileSize());
store.save(original);
@@ -28,8 +28,8 @@ public class HydrologyRegionalMouthRefinementTest {
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forOutlet(settings, terrain, outlet);
HydrologyPlanner planner = new HydrologyPlanner(1L, settings, terrain);
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(767, 65, 0),
new HydrologyPoint(768, 80, 0), landward);
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(767, 63, 0),
new HydrologyPoint(768, 63, 0), landward);
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validate", List.class, String.class,
boolean.class, HydrologyTerrainSampler.class);
validate.setAccessible(true);
@@ -40,7 +40,7 @@ public class HydrologyRegionalMouthRefinementTest {
assertNull(accepted.toString(), accepted.rejection());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, unproved.rejection());
assertTrue(new HydrologyRegionalTerrainRefiner(planner).receivingTerminal(landward, "default", false, receiver));
assertTrue(new HydrologyRegionalTerrain(planner).receivingTerminal(landward, "default", false, receiver));
assertFalse(terrain.sample(800, 0).ocean());
assertTrue(receiver.receivingWater(800, 0, 63));
}
@@ -171,8 +171,8 @@ public class HydrologyRegionalMouthRefinementTest {
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyTerrainSampler terrain = (x, z) -> x >= 800 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= 768 ? 80 : 65, 0D, "land");
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(767, 65, 0),
new HydrologyPoint(768, 80, 0), new HydrologyPoint(799, 80, 0), new HydrologyPoint(800, 63, 0));
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(767, 63, 0),
new HydrologyPoint(768, 63, 0), new HydrologyPoint(799, 63, 0), new HydrologyPoint(800, 63, 0));
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
case SURFACE_WIDTH -> 6;
case SURFACE_DEPTH -> 2;
@@ -38,7 +38,7 @@ public class HydrologyRegionalSamplingTest {
RecordingSampler serial = new RecordingSampler(terrain);
SurfaceBankSupport.CrossSection cross = support.crossSection(serial, station, true);
SurfaceBankSupport.Perimeter perimeter = support.perimeter(serial, station, 80);
RecordingSampler parallel = new RecordingSampler(refiner(terrain).new Samples());
RecordingSampler parallel = new RecordingSampler(refiner(terrain));
pool.submit(() -> {
assertEquals(cross, support.crossSection(parallel, station, true));
assertEquals(perimeter, support.perimeter(parallel, station, 80));
@@ -49,48 +49,6 @@ public class HydrologyRegionalSamplingTest {
}
}
@Test
public void searchLimitCountsNullsAndRetainsAlreadyAdmittedCoordinates() {
AtomicInteger reads = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
reads.incrementAndGet();
return x == 0 ? null : HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyRegionalTerrainRefiner refiner = refiner(terrain);
refiner.sample(100000, 0);
HydrologyRegionalTerrainRefiner.Samples samples = refiner.new Samples();
assertNull(samples.sample(0, 0));
for (int x = 1; x < 65536; x++) {
assertEquals(70, samples.sample(x, 0).naturalHeight());
}
assertNull(samples.sample(0, 0));
assertEquals(70, samples.sample(1, 0).naturalHeight());
assertNull(samples.sample(65536, 0));
assertNull(samples.sample(100000, 0));
assertEquals(65537, reads.get());
}
@Test
public void failedRequiredSampleRetainsItsExceptionAndDoesNotConsumeAdmission() {
AtomicInteger failedReads = new AtomicInteger();
IllegalStateException failure = new IllegalStateException("Required terrain unavailable");
HydrologyTerrainSampler terrain = (x, z) -> {
if (x == -1 && failedReads.incrementAndGet() == 1) {
throw failure;
}
return HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyRegionalTerrainRefiner.Samples samples = refiner(terrain).new Samples();
for (int x = 0; x < 65535; x++) {
samples.sample(x, 0);
}
assertSame(failure, assertThrows(IllegalStateException.class, () -> samples.sample(-1, 0)));
assertEquals(70, samples.sample(-1, 0).naturalHeight());
assertEquals(70, samples.sample(0, 0).naturalHeight());
assertNull(samples.sample(-2, 0));
assertEquals(2, failedReads.get());
}
@Test
public void unavailableBankStopsBeforeALaterProviderFailure() throws Exception {
IllegalStateException failure = new IllegalStateException("Later bank sample");
@@ -103,7 +61,7 @@ public class HydrologyRegionalSamplingTest {
SurfaceBankSupport support = new SurfaceBankSupport(SETTINGS.surface(), SETTINGS.seaLevel());
SurfaceBankSupport.Station station = new SurfaceBankSupport.Station(0, 0, 1D, 0D, 4D);
SurfaceBankSupport.CrossSection expected = support.crossSection(terrain, station, true);
HydrologyRegionalTerrainRefiner.Samples samples = refiner(terrain).new Samples();
HydrologyRegionalTerrain samples = refiner(terrain);
try (ForkJoinPool pool = new ForkJoinPool(4)) {
assertEquals(expected, pool.submit(() -> support.crossSection(samples, station, true)).get(10, TimeUnit.SECONDS));
}
@@ -111,137 +69,8 @@ public class HydrologyRegionalSamplingTest {
assertSame(failure, assertThrows(IllegalStateException.class, () -> samples.sample(0, 0)));
}
@Test
public void hydraulicBudgetAndLazyWidthsPreserveConsumptionOrder() throws Exception {
HydrologyPlannerSettings.Surface surface = SETTINGS.surface();
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(SETTINGS.seaLevel(), SETTINGS.routing(),
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(), 256,
surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(), surface.shoreWidth(), surface.banks()),
SETTINGS.hydraulics(), SETTINGS.underground(), SETTINGS.outlets(), SETTINGS.geometry(), SETTINGS.deepFluids(),
SETTINGS.surfacePools(), SETTINGS.widestShoreBiomeWidth(), SETTINGS.seaCaves(), SETTINGS.surfacePolicyBounds());
HydrologyTerrainSample ground = new HydrologyTerrainSample(65, 0D, false, false, 0, 0,
true, true, true, false, true, false, 0D, 1D, 1D, 0.75D, 1D, 1D, 1D, 1D,
"land", "land", "land", "land", "land", "land", List.of("default"), List.of(),
Double.NaN, null, Double.NaN, true, SurfaceRiverPolicy.INHERIT);
HydrologyTerrainSampler terrain = (x, z) -> x == 133 ? null : x == 132 && z == 0 ? ground
: HydrologyTerrainSample.openLand(80, 0D, "land");
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(settings);
HydrologyRegionalHydraulics.HeadStation station = new HydrologyRegionalHydraulics.HeadStation(
132, 0, 1D, 0D, terrain.sample(132, 0), 80);
RecordingSampler serial = new RecordingSampler(terrain);
int expected = hydraulics.supportedHead(station, serial);
RecordingSampler parallel = new RecordingSampler(refiner(terrain).new Samples());
try (ForkJoinPool pool = new ForkJoinPool(4)) {
assertEquals(expected, pool.submit(() -> hydraulics.supportedHead(station, parallel)).get(10, TimeUnit.SECONDS).intValue());
}
assertEquals(serial.requests, parallel.requests);
assertTrue(serial.requests.size() > 2048);
}
@Test
public void routeRefinementMatchesSerialTerrainAndHydraulics() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 95 : 70, 0D, "land");
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> expected = refiner(terrain).refine(guide, "default", false, terrain);
try (ForkJoinPool pool = new ForkJoinPool(4)) {
List<HydrologyPoint> actual = pool.submit(() -> refiner(terrain).refine(guide, "default", false, terrain))
.get(15, TimeUnit.SECONDS);
assertFalse(expected.isEmpty());
assertEquals(expected, actual);
}
}
@Test
public void coastalCenterlineRetainsNegativeAndFractionalTraversalOrder() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(64, 0D, "land");
List<HydrologyPoint> guide = List.of(new HydrologyPoint(-173, 64, -117), new HydrologyPoint(174, 64, 138));
List<HydrologyPoint> expected = refiner(terrain).refine(guide, "default", true, terrain);
try (ForkJoinPool pool = new ForkJoinPool(4)) {
assertFalse(expected.isEmpty());
assertEquals(expected, pool.submit(() -> refiner(terrain).refine(guide, "default", true, terrain))
.get(15, TimeUnit.SECONDS));
}
}
@Test
public void rejectedTraversalDoesNotReadALaterProviderFailure() throws Exception {
IllegalStateException failure = new IllegalStateException("Later centerline sample");
AtomicInteger failures = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
if (x == 32 && z == 0) {
failures.incrementAndGet();
throw failure;
}
return z == 0 && (x == 0 || x == 128)
? HydrologyTerrainSample.openLand(70, 0D, "land") : null;
};
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0));
assertTrue(refiner(terrain).refine(guide, "default", false, terrain).isEmpty());
assertEquals(0, failures.get());
try (ForkJoinPool pool = new ForkJoinPool(4)) {
assertTrue(pool.submit(() -> refiner(terrain).refine(guide, "default", false, terrain))
.get(15, TimeUnit.SECONDS).isEmpty());
assertEquals(0, failures.get());
}
}
@Test
public void clearingAnActiveReachCannotPublishIntoTheNewCache() throws Exception {
AtomicBoolean changed = new AtomicBoolean();
AtomicBoolean blocked = new AtomicBoolean();
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
HydrologyTerrainSampler terrain = (x, z) -> {
boolean elevated = changed.get();
if (x == 64 && z == 0 && blocked.compareAndSet(false, true)) {
started.countDown();
await(release);
}
return HydrologyTerrainSample.openLand(elevated && x == 64 && z == 0 ? 100 : 70, 0D, "land");
};
HydrologyRegionalTerrainRefiner refiner = refiner(terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
Future<List<HydrologyPoint>> previous = executor.submit(() -> refiner.refine(guide, "default", false, terrain));
try {
assertTrue(started.await(5, TimeUnit.SECONDS));
changed.set(true);
refiner.clear();
assertEquals(100, refiner.sample(64, 0).naturalHeight());
} finally {
release.countDown();
}
previous.get(15, TimeUnit.SECONDS);
assertEquals(refiner(terrain).refine(guide, "default", false, terrain),
refiner.refine(guide, "default", false, terrain));
assertEquals(100, refiner.sample(64, 0).naturalHeight());
}
}
@Test
public void signedCoordinateTiesRemainStableInEveryDirection() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(8)) {
for (int[] direction : new int[][]{{1, 0}, {0, 1}, {-1, 0}, {0, -1}}) {
HydrologyTerrainSampler terrain = (x, z) -> {
int along = x * direction[0] + z * direction[1];
int across = -x * direction[1] + z * direction[0];
return HydrologyTerrainSample.openLand(
along >= 96 && along <= 160 && Math.abs(across) < 32 ? 95 : 70, 0D, "land");
};
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0),
new HydrologyPoint(256 * direction[0], 70, 256 * direction[1]));
List<HydrologyPoint> expected = refiner(terrain).refine(guide, "default", false, terrain);
List<HydrologyPoint> actual = pool.submit(() -> refiner(terrain).refine(guide, "default", false, terrain))
.get(15, TimeUnit.SECONDS);
assertFalse(expected.isEmpty());
assertEquals(expected, actual);
}
}
}
private static HydrologyRegionalTerrainRefiner refiner(HydrologyTerrainSampler terrain) {
return new HydrologyRegionalTerrainRefiner(new HydrologyPlanner(71L, SETTINGS, terrain));
private static HydrologyRegionalTerrain refiner(HydrologyTerrainSampler terrain) {
return new HydrologyRegionalTerrain(new HydrologyPlanner(71L, SETTINGS, terrain));
}
private static void await(CountDownLatch latch) {
@@ -1,169 +0,0 @@
package art.arcane.iris.generation.hydrology;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalSkippedWaypointTest {
@Test
public void aSkippableHillDoesNotBlockAnEarlierValleyAcceptedByTheFullPlanner() throws Exception {
Set<Integer> hills = Set.of(4);
HydrologyTerrainSampler terrain = terrain(hills, true);
HydrologyPlanner planner = planner(terrain);
List<HydrologyPoint> guide = guide(terrain, hills);
assertCoarseFeasible(planner, terrain, guide);
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain);
assertFalse(refined.isEmpty());
assertFalse(refined.contains(guide.get(4)));
Object context = context(guide, terrain);
HydrologyRegionalRoute.Attempt selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain,
candidate -> attempt(planner, context, candidate));
assertTrue(selected.toString(), selected.accepted());
assertEquals(1, selected.network().courses().size());
assertTrue(selected.network().courses().getFirst().hydraulicallyNonRising());
assertEquals(16, planner.settings.surface().maximumIncision());
}
@Test
public void theTerminalHeadRemainsMandatory() {
HydrologyTerrainSampler highTerminal = terrain(Set.of(8), false);
HydrologyPlanner planner = planner(highTerminal);
List<HydrologyPoint> guide = guide(highTerminal, Set.of(8));
assertCoarseFeasible(planner, highTerminal, guide);
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, highTerminal).isEmpty());
HydrologyTerrainSampler lowTerminal = terrain(Set.of(), false);
assertFalse(new HydrologyRegionalTerrainRefiner(planner(lowTerminal))
.refine(guide(lowTerminal, Set.of()), "default", false, lowTerminal).isEmpty());
}
@Test
public void threeConsecutiveHighWaypointsCannotBeSkipped() {
Set<Integer> hills = Set.of(4, 5, 6);
HydrologyTerrainSampler terrain = terrain(hills, true);
HydrologyPlanner planner = planner(terrain);
List<HydrologyPoint> guide = guide(terrain, hills);
assertCoarseFeasible(planner, terrain, guide);
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain).isEmpty());
Set<Integer> twoHills = Set.of(4, 5);
HydrologyTerrainSampler skippable = terrain(twoHills, true);
assertFalse(new HydrologyRegionalTerrainRefiner(planner(skippable))
.refine(guide(skippable, twoHills), "default", false, skippable).isEmpty());
}
@Test
public void aSampledRidgeCannotRaiseTheHeadAfterAnEarlierValley() {
HydrologyTerrainSampler base = terrain(Set.of(), true);
HydrologyTerrainSampler ridge = (x, z) -> x >= 200 && x <= 224
? HydrologyTerrainSample.openLand(95, 0D, "land") : base.sample(x, z);
HydrologyPlanner planner = planner(ridge);
List<HydrologyPoint> guide = guide(ridge, Set.of());
assertCoarseFeasible(planner, ridge, guide);
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, ridge).isEmpty());
AtomicInteger admitted = new AtomicInteger();
HydrologyRegionalRoute.Attempt selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, ridge,
candidate -> {
admitted.incrementAndGet();
return new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null);
});
assertFalse(selected.accepted());
assertEquals(0, admitted.get());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, selected.refinement().rejection());
}
private static void assertCoarseFeasible(HydrologyPlanner planner, HydrologyTerrainSampler terrain, List<HydrologyPoint> guide) {
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
int available = Integer.MAX_VALUE;
for (HydrologyPoint point : guide) {
HydrologyTerrainSample sample = terrain.sample(point.x(), point.z());
available = Math.min(available, hydraulics.maximumHead(sample));
assertTrue(point.toString(), available >= hydraulics.minimumHead(sample));
}
}
private static HydrologyRegionalRoute.Attempt attempt(HydrologyPlanner planner, Object context,
HydrologyRegionalRoute.Refinement candidate) {
try {
Method attempt = HydrologyRegionalPlanner.class.getDeclaredMethod("attempt", context.getClass(), HydrologyRegionalRoute.Refinement.class);
attempt.setAccessible(true);
return (HydrologyRegionalRoute.Attempt) attempt.invoke(planner.regional, context, candidate);
} catch (ReflectiveOperationException failure) {
throw new AssertionError(failure);
}
}
private static Object context(List<HydrologyPoint> guide, HydrologyTerrainSampler terrain) throws Exception {
Class<?> type = Class.forName(HydrologyRegionalPlanner.class.getName() + "$BuildContext");
Constructor<?> constructor = type.getDeclaredConstructors()[0];
constructor.setAccessible(true);
int[] contributions = new int[guide.size()];
Arrays.fill(contributions, 1);
RiverOutlet outlet = new RiverOutlet(2L, HydrologyFeatureType.MOUTH, 8L, new HydrologyPoint(1024, 63, 0),
new HydrologyPoint(1025, 63, 0), 63, true);
return constructor.newInstance(new HydrologyGridNode(0, 0, 0, 0, 0, 100L, terrain.sample(0, 0)),
null, 123L, outlet, "default", new HydrologyRegionalFlow(guide, contributions),
HydrologyRegionalMorphology.sample(guide, terrain, HydrologyRegionalPlannerTest.settings(false, 1)));
}
private static List<HydrologyPoint> guide(HydrologyTerrainSampler terrain, Set<Integer> hills) {
ArrayList<HydrologyPoint> guide = new ArrayList<>();
for (int index = 0; index < 9; index++) {
int x = index * 128;
int z = index != 8 && hills.contains(index) ? 128 : 0;
guide.add(new HydrologyPoint(x, terrain.sample(x, z).naturalHeight(), z));
}
return List.copyOf(guide);
}
private static HydrologyTerrainSampler terrain(Set<Integer> hills, boolean coast) {
return (x, z) -> {
if (coast && x >= 1025) {
return HydrologyTerrainSample.ocean(60, "ocean");
}
int height = coast ? 80 - Math.max(0, x - 752) / 16 : 80;
if (x >= 32 && x <= 96) {
height = 75;
}
for (int hill : hills) {
if (Math.abs(x - hill * 128) <= 24 && Math.abs(z - (hill == 8 ? 0 : 128)) <= 24) {
height = 93;
}
}
return HydrologyTerrainSample.openLand(height, 0D, "land");
};
}
private static HydrologyPlanner planner(HydrologyTerrainSampler terrain) {
HydrologyPlannerSettings base = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlannerSettings.Surface surface = base.surface();
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(base.seaLevel(), base.routing(),
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(), surface.maximumWidth(),
surface.minimumDepth(), surface.maximumDepth(), 16, surface.shoreWidth(), surface.banks()),
base.hydraulics(), base.underground(), base.outlets(), base.geometry(), base.deepFluids(), base.surfacePools(),
base.widestShoreBiomeWidth(), base.seaCaves(), base.surfacePolicyBounds());
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
case SURFACE_WIDTH -> 8;
case SURFACE_DEPTH -> 2;
default -> request.minimum();
};
return new HydrologyPlanner(71L, settings, terrain, geometry, -4096,
footprint -> new HydrologyTerrainCaveVoxelView(terrain, 63, -4096, 4096));
}
}
@@ -209,7 +209,7 @@ public class HydrologyRegionalSourceParallelismTest {
sources.put(RiverFootprint.pack(node.x(), node.z()), ordinal++);
}
assertEquals(secondCatchmentSize + 4, ordinal);
return new HydrologyRegionalGraph.Tree(labels, contributions, outlets);
return new HydrologyRegionalGraph.Tree(labels, contributions, outlets, new int[contributions.length]);
});
when(graph.path(any(), anyInt())).thenAnswer(invocation -> List.of(invocation.<Integer>getArgument(1), 0));
replace(planner.regional, "graph", graph);
@@ -92,7 +92,7 @@ public class HydrologyRegionalSourceTrialsTest {
new RiverOutlet(100L + root, HydrologyFeatureType.MOUTH, root, point, point, 63, true)));
}
HydrologySampledGrid grid = new HydrologySampledGrid(0, 0, 0, 0, 16384, sources.size(), 1, nodes);
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(selected, new int[sources.size()], outlets);
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(selected, new int[sources.size()], outlets, new int[sources.size()]);
return new Fixture(grid, tree, indices);
}
@@ -249,7 +249,7 @@ public class HydrologyRegionalSurfacePolicyTest {
candidates.add(new OutletCandidate(node.index(), -1, new RiverOutlet(node.index() + 1000L,
HydrologyFeatureType.MOUTH, node.id(), point, point, 63, true)));
}
return new HydrologyRoutingPlan(potential, parent, outlets, lengths, List.copyOf(candidates), false);
return new HydrologyRoutingPlan(potential, parent, outlets, lengths, List.copyOf(candidates), false, null);
}
private static HydrologyPlannerSettings withSources(HydrologyPlannerSettings.Source sources) {
@@ -1,541 +0,0 @@
package art.arcane.iris.generation.hydrology;
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
import org.junit.Test;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalTerrainRefinerTest {
@Test(timeout = 10000)
public void clearingDuringSamplingCannotPublishOldTerrain() throws Exception {
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger calls = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
int call = calls.incrementAndGet();
if (call == 1) {
started.countDown();
try {
assertTrue(release.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException(interrupted);
}
}
return HydrologyTerrainSample.openLand(call == 1 ? 70 : 80, 0D, "land");
};
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(
new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain));
try (ExecutorService workers = Executors.newSingleThreadExecutor()) {
Future<HydrologyTerrainSample> first = workers.submit(() -> refiner.sample(0, 0));
try {
assertTrue(started.await(3, TimeUnit.SECONDS));
refiner.clear();
assertEquals(80, refiner.sample(0, 0).naturalHeight());
} finally {
release.countDown();
}
assertEquals(70, first.get(5, TimeUnit.SECONDS).naturalHeight());
assertEquals(80, refiner.sample(0, 0).naturalHeight());
assertEquals(2, calls.get());
}
}
@Test(timeout = 10000)
public void independentDiagonalSamplesDoNotWaitForEachOthersTerrain() throws Exception {
CountDownLatch sampling = new CountDownLatch(2);
AtomicInteger calls = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
calls.incrementAndGet();
sampling.countDown();
try {
assertTrue("Independent terrain samples were serialized", sampling.await(3, TimeUnit.SECONDS));
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException(interrupted);
}
return HydrologyTerrainSample.openLand(70 + x, 0D, "land");
};
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(
new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain));
try (ExecutorService workers = Executors.newFixedThreadPool(2)) {
Future<HydrologyTerrainSample> first = workers.submit(() -> refiner.sample(0, 0));
Future<HydrologyTerrainSample> second = workers.submit(() -> refiner.sample(1, 1));
assertEquals(70, first.get(5, TimeUnit.SECONDS).naturalHeight());
assertEquals(71, second.get(5, TimeUnit.SECONDS).naturalHeight());
assertEquals(first.get(), refiner.sample(0, 0));
assertEquals(second.get(), refiner.sample(1, 1));
assertEquals(2, calls.get());
}
}
@Test
public void aHigherHeadDetourSurvivesACheaperInfeasibleArrival() {
int[][] heights = {
{86, 94, 77, 91, 75, 85, 82, 88, 74},
{84, 94, 83, 85, 72, 76, 73, 80, 72},
{73, 85, 78, 77, 73, 82, 84, 81, 79},
{83, 77, 92, 81, 84, 80, 93, 94, 96},
{90, 73, 76, 84, 74, 73, 91, 80, 85},
{91, 96, 90, 75, 91, 84, 94, 96, 95},
{84, 76, 80, 92, 72, 73, 96, 75, 86},
{94, 93, 86, 81, 80, 89, 75, 93, 77},
{85, 95, 85, 73, 88, 82, 81, 94, 92}
};
HydrologyTerrainSampler terrain = (x, z) -> {
int gridX = Math.floorDiv(x, 16);
int gridZ = Math.floorDiv(z, 16) + 4;
return gridX < 0 || gridX >= 9 || gridZ < 0 || gridZ >= 9
? HydrologyTerrainSample.ocean(60, "outside")
: HydrologyTerrainSample.openLand(heights[gridZ][gridX], 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(1L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(128, 85, 0));
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
List<HydrologyPoint> path = refiner.refine(guide, "default", false, terrain);
assertFalse(path.isEmpty());
assertEquals(guide.getFirst(), path.getFirst());
assertEquals(guide.getLast(), path.getLast());
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
int available = 90;
for (int station = 0; station < centerline.size(); station++) {
HydrologyTerrainSample sample = terrain.sample(centerline.x()[station], centerline.z()[station]);
assertFalse(sample.ocean());
int supported = hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
centerline.x()[station], centerline.z()[station], centerline.tangentX()[station],
centerline.tangentZ()[station], sample, available), terrain);
assertTrue(supported <= available);
available = supported;
assertTrue("station=" + station, available >= hydraulics.minimumHead(sample));
}
assertEquals(path, refiner.refine(guide, "default", false, terrain));
assertEquals(path, new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain));
}
@Test
public void shortRepairedPrefixDoesNotHideALongEnoughGuide() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 406 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= 400 ? 60 : 70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(0, 70, 96),
new HydrologyPoint(128, 70, 96), new HydrologyPoint(256, 70, 96), new HydrologyPoint(256, 70, 0),
new HydrologyPoint(512, 70, 0));
HydrologyRegionalRoute.Refinement selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain,
candidate -> {
double length = 0D;
for (int index = 1; index < candidate.points().size(); index++) {
length += StrictMath.sqrt(candidate.points().get(index - 1).distanceSquared2D(candidate.points().get(index)));
}
assertTrue(length >= planner.settings.routing().regional().minimumLength());
return new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null);
}).refinement();
assertNull(selected.toString(), selected.rejection());
assertEquals(new HydrologyPoint(399, 63, 0), selected.oceanEntry().landward());
assertEquals(new HydrologyPoint(406, 63, 0), selected.oceanEntry().receiving());
}
@Test
public void onlyTheRetainedDryPrefixNeedsTerrainRepair() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 806 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= 800 ? 60 : x >= 96 && x <= 160 && Math.abs(z) < 32 ? 86 : 70,
0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0),
new HydrologyPoint(512, 70, 0), new HydrologyPoint(1024, 70, 0));
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain).isEmpty());
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
assertEquals(new HydrologyPoint(799, 63, 0), repaired.oceanEntry().landward());
assertEquals(new HydrologyPoint(806, 63, 0), repaired.oceanEntry().receiving());
assertTrue(repaired.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
assertTrue(repaired.points().stream().allMatch(point -> point.x() < 800));
}
@Test
public void aCoarseOceanHitBeforeMinimumLengthStillAllowsADryTerrainDetour() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 800 || x >= 96 && x <= 160 && Math.abs(z) < 32
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0),
new HydrologyPoint(512, 70, 0), new HydrologyPoint(799, 70, 0));
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
assertTrue(repaired.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
assertEquals(guide.getLast(), repaired.points().getLast());
}
@Test
public void naturallyFloodedLandCannotBeUsedAsAnOwnedDryReach() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 60 : 70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain);
assertFalse(repaired.isEmpty());
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
for (int station = 0; station < centerline.size(); station++) {
HydrologyTerrainSample sampled = terrain.sample(centerline.x()[station], centerline.z()[station]);
assertFalse(sampled.ocean());
assertTrue(sampled.naturalHeight() >= planner.settings.seaLevel());
}
}
@Test
public void repeatedCoarseAnchorsCannotCreateRetracedWaterways() {
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(128, 70, 128), new HydrologyPoint(128, 70, 0), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
assertEquals(List.of(guide.getFirst(), guide.getLast()), refined);
}
@Test
public void redundantCoarseAnchorHairpinsUseTheClearTerrainShortcut() {
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(160, 70, 32), new HydrologyPoint(144, 70, 48), new HydrologyPoint(160, 70, 48),
new HydrologyPoint(192, 70, 16), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
assertEquals(guide.getFirst(), refined.getFirst());
assertEquals(guide.getLast(), refined.getLast());
assertTrue(refined.size() <= 3);
for (int index = 1; index < refined.size(); index++) {
assertTrue(refined.get(index).x() > refined.get(index - 1).x());
}
}
@Test
public void repairsUnsampledOceanPocketsAtPositiveAndNegativeCoordinates() {
for (int origin : new int[]{0, -512}) {
HydrologyTerrainSampler terrain = (x, z) -> x >= origin + 96 && x <= origin + 160 && Math.abs(z) < 32
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
List<HydrologyPoint> guide = List.of(new HydrologyPoint(origin, 70, 0), new HydrologyPoint(origin + 256, 70, 0));
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
List<HydrologyPoint> first = refiner.refine(guide, "default", false, planner.sampler);
assertFalse(first.isEmpty());
assertEquals(guide.getFirst(), first.getFirst());
assertEquals(guide.getLast(), first.getLast());
SurfaceCenterline centerline = SurfaceCenterline.densify(first);
for (int station = 0; station < centerline.size(); station++) {
assertFalse(terrain.sample(centerline.x()[station], centerline.z()[station]).ocean());
}
assertEquals(first, refiner.refine(guide, "default", false, planner.sampler));
assertEquals(first, new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler));
}
}
@Test
public void aWaterCourseDetoursAroundALavaOnlyBiome() {
HydrologyTerrainSampler terrain = (x, z) -> land(x >= 96 && x <= 160 && Math.abs(z) < 32 ? "lava" : "water");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "water", false, planner.sampler);
assertFalse(refined.isEmpty());
SurfaceCenterline centerline = SurfaceCenterline.densify(refined);
for (int station = 0; station < centerline.size(); station++) {
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
}
}
@Test
public void repairedTerrainRouteStillPassesOrganicShapeValidation() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 96 && x <= 160 && Math.abs(z) < 32
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalRoute.Refinement refined = new HydrologyRegionalRoute(planner).select(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(refined.toString(), refined.rejection());
assertTrue(refined.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
}
@Test
public void anUnavoidableOceanBarrierRejectsWithinTheTerrainSampleBudget() {
AtomicInteger calls = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
calls.incrementAndGet();
return x >= 96 && x <= 160 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler);
assertTrue(refined.isEmpty());
assertTrue("terrain samples=" + calls.get(), calls.get() <= 65536);
}
@Test
public void unavailableTerrainIsSampledOnceAndCountsTowardTheBudget() {
Map<Long, Integer> unavailable = new HashMap<>();
AtomicInteger calls = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
calls.incrementAndGet();
if (x >= 96 && x <= 160 && Math.abs(z) < 32) {
unavailable.merge(RiverFootprint.pack(x, z), 1, Integer::sum);
return null;
}
return HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler);
assertFalse(repaired.isEmpty());
assertFalse(unavailable.isEmpty());
assertTrue(unavailable.values().stream().allMatch(count -> count == 1));
assertTrue(calls.get() <= 65536);
}
@Test
public void anUnsampledRidgeUsesTheLowerValleyWithoutIncreasingIncision() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 94 : 70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
assertFalse(repaired.isEmpty());
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
for (int station = 0; station < centerline.size(); station++) {
assertEquals(70, terrain.sample(centerline.x()[station], centerline.z()[station]).naturalHeight());
}
}
@Test
public void anImpossibleIntermediateAnchorCanBeSkippedWithinTheSameValley() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x < 48 || x >= 120 && x <= 136 && Math.abs(z) < 8 ? 90 : 65, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(128, 90, 0),
new HydrologyPoint(256, 65, 0));
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
List<HydrologyPoint> repaired = refiner.refine(guide, "default", false, planner.sampler);
assertFalse(repaired.isEmpty());
assertEquals(guide.getFirst(), repaired.getFirst());
assertEquals(guide.getLast(), repaired.getLast());
assertFalse(repaired.contains(guide.get(1)));
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
int head = Integer.MAX_VALUE;
for (int station = 0; station < centerline.size(); station++) {
HydrologyTerrainSample sample = terrain.sample(centerline.x()[station], centerline.z()[station]);
head = Math.min(head, hydraulics.maximumHead(sample));
assertTrue(hydraulics.minimumHead(sample) <= head);
}
assertEquals(repaired, refiner.refine(guide, "default", false, planner.sampler));
}
@Test
public void aLowFineDetourCannotResetTheHeadBeforeTheNextRidge() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 65 : x >= 384 ? 92 : 80, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(256, 80, 0),
new HydrologyPoint(512, 92, 0));
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
assertFalse(repaired.isEmpty());
assertTrue(repaired.stream().anyMatch(point -> Math.abs(point.z()) >= 32));
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
int head = Integer.MAX_VALUE;
for (int station = 0; station < centerline.size(); station++) {
HydrologyTerrainSample sample = terrain.sample(centerline.x()[station], centerline.z()[station]);
head = Math.min(head, hydraulics.maximumHead(sample));
assertTrue(hydraulics.minimumHead(sample) <= head);
}
}
@Test
public void ridgeRepairRespectsTheLocalIncisionPolicy() {
SurfaceRiverPolicy limited = new SurfaceRiverPolicy("valley", null, null, null, null, null, null, 4);
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 76 : 70, 0D, "land").withSurfacePolicy(limited);
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler);
assertFalse(repaired.isEmpty());
assertTrue(repaired.stream().anyMatch(point -> Math.abs(point.z()) >= 32));
}
@Test
public void detourLengthRejectsBeforeDetailedSamplingOfTheRemainingCourse() {
AtomicInteger maximumX = new AtomicInteger(Integer.MIN_VALUE);
HydrologyTerrainSampler terrain = (x, z) -> {
boolean coarseAnchor = Math.floorMod(x + 1, 256) <= 2 && Math.abs(z) <= 1;
if (!coarseAnchor) {
maximumX.accumulateAndGet(x, Math::max);
}
return x >= 96 && x <= 160 && Math.abs(z) < 32 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
ArrayList<HydrologyPoint> guide = new ArrayList<>();
for (int x = 0; x <= planner.settings.routing().maximumRouteLength(); x += 256) {
guide.add(new HydrologyPoint(x, 70, 0));
}
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler).isEmpty());
assertTrue("A rejected first detour must not sample later reaches", maximumX.get() < 512);
}
@Test
public void anAlreadyOverlongGuideDoesNotSampleTerrain() {
AtomicInteger calls = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
calls.incrementAndGet();
return HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
int before = calls.get();
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(4096, 70, 0)), "default", false, planner.sampler).isEmpty());
assertEquals(before, calls.get());
}
@Test
public void clearancePreferenceDoesNotExcludeTheOnlyNarrowCorridor() {
HydrologyTerrainSampler terrain = (x, z) -> {
boolean horizontal = Math.abs(z) <= 3 && (x <= 80 || x >= 176);
boolean detour = Math.abs(z - 16) <= 3 && x >= 61 && x <= 195;
boolean vertical = (Math.abs(x - 64) <= 3 || Math.abs(x - 192) <= 3) && z >= -3 && z <= 19;
return land(horizontal || detour || vertical ? "water" : "lava");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "water", false, planner.sampler);
assertFalse(repaired.isEmpty());
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
for (int station = 0; station < centerline.size(); station++) {
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
}
}
@Test
public void oceanDetourPreservesTheShippingTwentyDegreeTurnLimit() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 96 && x <= 160 && Math.abs(z) < 32
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0.34D, 0.42D, 0.48D, 1, 20D)), terrain);
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
assertTrue(repaired.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
}
@Test
public void shortRepairReversalsReceiveEnoughReachToMeetTwentyDegrees() {
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 1, 20D)),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(160, 70, 32), new HydrologyPoint(160, 70, 16),
new HydrologyPoint(192, 70, 16), new HydrologyPoint(192, 70, 128), new HydrologyPoint(512, 70, 128));
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
assertEquals(guide.getFirst(), repaired.points().getFirst());
assertEquals(guide.getLast(), repaired.points().getLast());
}
@Test
public void roundedCurveTurnLimitIsMeasuredBeforeBlockQuantization() throws Exception {
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 20D)),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(128, 70, 128), new HydrologyPoint(256, 70, 128));
Method interpolate = HydrologyRegionalRoute.class.getDeclaredMethod("interpolateLinear", List.class);
Method round = HydrologyRegionalRoute.class.getDeclaredMethod("roundBends", List.class, double.class);
Method resample = HydrologyRegionalRoute.class.getDeclaredMethod("resample", List.class);
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateRounded", List.class, String.class, boolean.class, HydrologyTerrainSampler.class);
interpolate.setAccessible(true);
round.setAccessible(true);
resample.setAccessible(true);
validate.setAccessible(true);
Object linear = interpolate.invoke(route, guide);
Object rounded = round.invoke(route, linear, 128D);
Object curve = resample.invoke(route, rounded);
HydrologyRegionalRoute.Refinement accepted = (HydrologyRegionalRoute.Refinement) validate.invoke(route, curve, "default", false, planner.sampler);
assertNull(accepted.toString(), accepted.rejection());
assertEquals(guide.getFirst(), accepted.points().getFirst());
assertEquals(guide.getLast(), accepted.points().getLast());
Object sharper = resample.invoke(route, round.invoke(route, linear, 64D));
HydrologyRegionalRoute.Refinement rejected = (HydrologyRegionalRoute.Refinement) validate.invoke(route, sharper, "default", false, planner.sampler);
assertEquals(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, rejected.rejection());
}
@Test
public void splineOvershootCannotDiscardAValidNarrowDetour() {
HydrologyTerrainSampler terrain = (x, z) -> {
boolean first = Math.abs(z) <= 8 && x >= -8 && x <= 136;
boolean middle = Math.abs(x - 128) <= 8 && z >= -8 && z <= 136;
boolean last = Math.abs(z - 128) <= 8 && x >= 120 && x <= 264;
return land(first || middle || last ? "water" : "lava");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 150D)), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(128, 70, 128), new HydrologyPoint(256, 70, 128));
assertEquals(guide, new HydrologyRegionalTerrainRefiner(planner).refine(guide, "water", false, planner.sampler));
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "water", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired.points());
for (int station = 0; station < centerline.size(); station++) {
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
}
}
private static HydrologyPlannerSettings withMeanders(HydrologyPlannerSettings.Meanders meanders) {
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlannerSettings.Geometry geometry = settings.geometry();
return new HydrologyPlannerSettings(settings.seaLevel(), settings.routing(), settings.surface(), settings.hydraulics(),
settings.underground(), settings.outlets(), new HydrologyPlannerSettings.Geometry(meanders,
geometry.surface(), geometry.underground(), geometry.grottos(), geometry.drops()), settings.deepFluids(),
settings.surfacePools(), settings.widestShoreBiomeWidth(), settings.seaCaves(), settings.surfacePolicyBounds());
}
private static HydrologyTerrainSample land(String profile) {
return new HydrologyTerrainSample(70, 0D, false, false, 38, 40,
true, true, true, false, false, false,
0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
"land", "land", "land", "land", "land", "land", List.of(profile), List.of(),
Double.NaN, null, Double.NaN, true, SurfaceRiverPolicy.INHERIT);
}
}
@@ -0,0 +1,192 @@
package art.arcane.iris.generation.hydrology;
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
import org.junit.Test;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalTerrainTest {
@Test(timeout = 10000)
public void clearingDuringSamplingCannotPublishOldTerrain() throws Exception {
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger calls = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
int call = calls.incrementAndGet();
if (call == 1) {
started.countDown();
try {
assertTrue(release.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException(interrupted);
}
}
return HydrologyTerrainSample.openLand(call == 1 ? 70 : 80, 0D, "land");
};
HydrologyRegionalTerrain refiner = new HydrologyRegionalTerrain(
new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain));
try (ExecutorService workers = Executors.newSingleThreadExecutor()) {
Future<HydrologyTerrainSample> first = workers.submit(() -> refiner.sample(0, 0));
try {
assertTrue(started.await(3, TimeUnit.SECONDS));
refiner.clear();
assertEquals(80, refiner.sample(0, 0).naturalHeight());
} finally {
release.countDown();
}
assertEquals(70, first.get(5, TimeUnit.SECONDS).naturalHeight());
assertEquals(80, refiner.sample(0, 0).naturalHeight());
assertEquals(2, calls.get());
}
}
@Test(timeout = 10000)
public void independentDiagonalSamplesDoNotWaitForEachOthersTerrain() throws Exception {
CountDownLatch sampling = new CountDownLatch(2);
AtomicInteger calls = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
calls.incrementAndGet();
sampling.countDown();
try {
assertTrue("Independent terrain samples were serialized", sampling.await(3, TimeUnit.SECONDS));
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException(interrupted);
}
return HydrologyTerrainSample.openLand(70 + x, 0D, "land");
};
HydrologyRegionalTerrain refiner = new HydrologyRegionalTerrain(
new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain));
try (ExecutorService workers = Executors.newFixedThreadPool(2)) {
Future<HydrologyTerrainSample> first = workers.submit(() -> refiner.sample(0, 0));
Future<HydrologyTerrainSample> second = workers.submit(() -> refiner.sample(1, 1));
assertEquals(70, first.get(5, TimeUnit.SECONDS).naturalHeight());
assertEquals(71, second.get(5, TimeUnit.SECONDS).naturalHeight());
assertEquals(first.get(), refiner.sample(0, 0));
assertEquals(second.get(), refiner.sample(1, 1));
assertEquals(2, calls.get());
}
}
@Test
public void shortRepairedPrefixDoesNotHideALongEnoughGuide() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 406 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= 400 ? 60 : 70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(0, 70, 96),
new HydrologyPoint(128, 70, 96), new HydrologyPoint(256, 70, 96), new HydrologyPoint(256, 70, 0),
new HydrologyPoint(512, 70, 0));
HydrologyRegionalRoute.Refinement selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain,
candidate -> {
double length = 0D;
for (int index = 1; index < candidate.points().size(); index++) {
length += StrictMath.sqrt(candidate.points().get(index - 1).distanceSquared2D(candidate.points().get(index)));
}
assertTrue(length >= planner.settings.routing().regional().minimumLength());
return new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null);
}).refinement();
assertNull(selected.toString(), selected.rejection());
assertEquals(new HydrologyPoint(399, 63, 0), selected.oceanEntry().landward());
assertEquals(new HydrologyPoint(406, 63, 0), selected.oceanEntry().receiving());
}
@Test
public void shortRepairReversalsReceiveEnoughReachToMeetTwentyDegrees() {
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 1, 20D)),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(160, 70, 32), new HydrologyPoint(160, 70, 16),
new HydrologyPoint(192, 70, 16), new HydrologyPoint(192, 70, 128), new HydrologyPoint(512, 70, 128));
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
assertEquals(guide.getFirst(), repaired.points().getFirst());
assertEquals(guide.getLast(), repaired.points().getLast());
}
@Test
public void roundedCurveTurnLimitIsMeasuredBeforeBlockQuantization() throws Exception {
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 20D)),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(128, 70, 128), new HydrologyPoint(256, 70, 128));
Method interpolate = HydrologyRegionalRoute.class.getDeclaredMethod("interpolateLinear", List.class);
Method round = HydrologyRegionalRoute.class.getDeclaredMethod("roundBends", List.class, double.class);
Method resample = HydrologyRegionalRoute.class.getDeclaredMethod("resample", List.class);
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateRounded", List.class, String.class, boolean.class, HydrologyTerrainSampler.class);
interpolate.setAccessible(true);
round.setAccessible(true);
resample.setAccessible(true);
validate.setAccessible(true);
Object linear = interpolate.invoke(route, guide);
Object rounded = round.invoke(route, linear, 128D);
Object curve = resample.invoke(route, rounded);
HydrologyRegionalRoute.Refinement accepted = (HydrologyRegionalRoute.Refinement) validate.invoke(route, curve, "default", false, planner.sampler);
assertNull(accepted.toString(), accepted.rejection());
assertEquals(guide.getFirst(), accepted.points().getFirst());
assertEquals(guide.getLast(), accepted.points().getLast());
Object sharper = resample.invoke(route, round.invoke(route, linear, 64D));
HydrologyRegionalRoute.Refinement rejected = (HydrologyRegionalRoute.Refinement) validate.invoke(route, sharper, "default", false, planner.sampler);
assertEquals(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, rejected.rejection());
}
@Test
public void splineOvershootCannotDiscardAValidNarrowDetour() {
HydrologyTerrainSampler terrain = (x, z) -> {
boolean first = Math.abs(z) <= 8 && x >= -8 && x <= 136;
boolean middle = Math.abs(x - 128) <= 8 && z >= -8 && z <= 136;
boolean last = Math.abs(z - 128) <= 8 && x >= 120 && x <= 264;
return land(first || middle || last ? "water" : "lava");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 150D)), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(128, 70, 128), new HydrologyPoint(256, 70, 128));
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "water", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired.points());
for (int station = 0; station < centerline.size(); station++) {
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
}
}
private static HydrologyPlannerSettings withMeanders(HydrologyPlannerSettings.Meanders meanders) {
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlannerSettings.Geometry geometry = settings.geometry();
return new HydrologyPlannerSettings(settings.seaLevel(), settings.routing(), settings.surface(), settings.hydraulics(),
settings.underground(), settings.outlets(), new HydrologyPlannerSettings.Geometry(meanders,
geometry.surface(), geometry.underground(), geometry.grottos(), geometry.drops()), settings.deepFluids(),
settings.surfacePools(), settings.widestShoreBiomeWidth(), settings.seaCaves(), settings.surfacePolicyBounds());
}
private static HydrologyTerrainSample land(String profile) {
return new HydrologyTerrainSample(70, 0D, false, false, 38, 40,
true, true, true, false, false, false,
0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
"land", "land", "land", "land", "land", "land", List.of(profile), List.of(),
Double.NaN, null, Double.NaN, true, SurfaceRiverPolicy.INHERIT);
}
}
@@ -27,8 +27,6 @@ public class HydrologyRegionalWidthSupportTest {
public void aWiderChannelContainsTheCenterDepressionWithoutRaisingLand() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 128 && x <= 136 && Math.abs(z) <= 1 ? 65 : x >= 256 && x <= 288 ? 86 : 80, 0D, "land");
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(SETTINGS);
assertEquals(80, hydraulics.supportedHead(station(terrain, 80), terrain));
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateHydraulics",
HydrologyRegionalRoute.Refinement.class, String.class, boolean.class, HydrologyTerrainSampler.class);
@@ -48,7 +46,7 @@ public class HydrologyRegionalWidthSupportTest {
assertTrue(course.hydraulicallyNonRising());
SurfaceLayerColumn center = null;
for (SurfaceLayerColumn column : footprint.columns()) {
assertTrue(column.layer().bedY() <= column.terrain().naturalHeight());
assertTrue(column.layer().bedY() - column.terrain().naturalHeight() <= SETTINGS.surface().maximumIncision());
assertTrue(column.terrain().naturalHeight() - column.layer().bedY() <= SETTINGS.surface().maximumIncision());
if (column.x() == 132 && column.z() == 0) {
center = column;
@@ -59,112 +57,8 @@ public class HydrologyRegionalWidthSupportTest {
assertEquals(80, center.layer().fluidHeadY());
SurfaceCourseResult narrow = build(terrain, geometry(4));
assertFalse(narrow.accepted());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, narrow.rejection());
assertEquals(23, narrow.rejectionDetail());
}
@Test
public void anInteriorWidthCanHoldHeadWhenBothAuthoredExtremaFail() {
HydrologyTerrainSampler terrain = narrowBanks();
assertEquals(65, bankHead(terrain, 4));
assertEquals(80, bankHead(terrain, 6));
assertEquals(65, bankHead(terrain, 8));
AtomicInteger samples = new AtomicInteger();
HydrologyTerrainSampler bounded = (x, z) -> {
assertTrue("A supported interior width must stop wider probes", Math.abs(z) <= 7);
samples.incrementAndGet();
return terrain.sample(x, z);
};
assertEquals(80, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 80), bounded));
assertTrue(samples.get() > 0);
}
@Test
public void fractionalLocalWidthIsCheckedBeforeUnneededWiderCandidates() {
HydrologyTerrainSampler source = narrowBanks();
HydrologyTerrainSampler terrain = (x, z) -> withWidthMultiplier(source.sample(x, z), 0.8125D);
assertEquals(80, bankHead(terrain, 6.5D));
HydrologyTerrainSampler bounded = (x, z) -> {
assertTrue("The local width6.5 already supports the incoming head", Math.abs(z) <= 6);
return terrain.sample(x, z);
};
assertEquals(80, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 80), bounded));
}
@Test
public void minimumWidthSupportStaysLazyAndCannotRaiseIncomingHead() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 128 && x <= 136 && z == 0 ? 65 : 80, 0D, "land");
AtomicInteger expectedSamples = new AtomicInteger();
bankHead((x, z) -> {
expectedSamples.incrementAndGet();
return terrain.sample(x, z);
}, 4);
AtomicInteger actualSamples = new AtomicInteger();
HydrologyTerrainSampler counted = (x, z) -> {
actualSamples.incrementAndGet();
return terrain.sample(x, z);
};
assertEquals(78, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 78), counted));
assertEquals(expectedSamples.get(), actualSamples.get());
}
@Test
public void incompleteWiderPerimetersCannotCertifyAHighHead() {
HydrologyTerrainSampler terrain = narrowBanks();
HydrologyTerrainSampler incomplete = (x, z) -> x == 133 ? null : terrain.sample(x, z);
assertEquals(65, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 80), incomplete));
}
@Test
public void additionalSamplingAndRadiusStayBoundedWhenWiderPerimetersAreIncomplete() {
HydrologyPlannerSettings settings = wideSettings();
HydrologyTerrainSample ground = withWidthMultiplier(HydrologyTerrainSample.openLand(65, 0D, "land"), 0.75D);
HydrologyTerrainSample bank = HydrologyTerrainSample.openLand(80, 0D, "land");
HydrologyTerrainSampler incomplete = (x, z) -> x == 133 ? null : x == 132 && z == 0 ? ground : bank;
AtomicInteger minimumSamples = new AtomicInteger();
HydrologyTerrainSampler minimum = (x, z) -> {
minimumSamples.incrementAndGet();
return incomplete.sample(x, z);
};
SurfaceBankSupport support = new SurfaceBankSupport(settings.surface(), settings.seaLevel());
SurfaceBankSupport.Station station = new SurfaceBankSupport.Station(132, 0, 1D, 0D, 2D);
SurfaceBankSupport.CrossSection cross = support.crossSection(minimum, station, true);
assertFalse(support.perimeter(minimum, station, cross.minimum()).complete());
AtomicInteger samples = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
assertTrue(Math.abs(x - 132) <= 96 && Math.abs(z) <= 96);
samples.incrementAndGet();
return incomplete.sample(x, z);
};
assertEquals(65, new HydrologyRegionalHydraulics(settings).supportedHead(
new HydrologyRegionalHydraulics.HeadStation(132, 0, 1D, 0D, ground, 80), terrain));
assertEquals(minimumSamples.get() + 2048, samples.get());
}
private static HydrologyTerrainSampler narrowBanks() {
return (x, z) -> HydrologyTerrainSample.openLand(
x >= 128 && x <= 136 && Math.abs(z) <= 1 || Math.abs(z) >= 7 ? 65 : 80, 0D, "land");
}
private static int bankHead(HydrologyTerrainSampler terrain, double width) {
SurfaceBankSupport support = new SurfaceBankSupport(SETTINGS.surface(), SETTINGS.seaLevel());
SurfaceBankSupport.Station station = new SurfaceBankSupport.Station(132, 0, 1D, 0D, width / 2D);
SurfaceBankSupport.CrossSection cross = support.crossSection(terrain, station, true);
assertFalse(cross.blocked());
SurfaceBankSupport.Perimeter perimeter = support.perimeter(terrain, station, cross.minimum());
assertTrue(perimeter.complete());
return perimeter.minimum();
}
private static HydrologyRegionalHydraulics.HeadStation station(HydrologyTerrainSampler terrain, int incoming) {
return new HydrologyRegionalHydraulics.HeadStation(132, 0, 1D, 0D, terrain.sample(132, 0), incoming);
assertTrue(narrow.accepted());
assertTrue(narrow.segments().stream().allMatch(segment -> segment.upstreamHeadY() >= segment.downstreamHeadY()));
}
private static HydrologyGeometrySampler geometry(int width) {
@@ -180,22 +74,4 @@ public class HydrologyRegionalWidthSupportTest {
.build(71L, 91L, "default", PATH, SurfaceTerminal.SINKHOLE, 40, 256);
}
private static HydrologyTerrainSample withWidthMultiplier(HydrologyTerrainSample source, double multiplier) {
return new HydrologyTerrainSample(source.naturalHeight(), source.slope(), source.ocean(), source.caveAvailable(),
source.caveFloorY(), source.caveFluidY(), source.transitAllowed(), source.outletAllowed(), source.surfaceSourceAllowed(),
source.surfaceSourceRequired(), source.undergroundSourceAllowed(), source.undergroundSourceRequired(), source.routingCost(),
source.surfaceSourceWeight(), source.undergroundSourceWeight(), multiplier, source.depthMultiplier(), source.incisionMultiplier(),
source.routingMultiplier(), source.bankMultiplier(), source.parentBiomeKey(), source.surfaceBiomeKey(), source.mouthBiomeKey(),
source.shoreBiomeKey(), source.bankBiomeKey(), source.floodedCaveBiomeKey(), source.preferredProfileKeys(), source.surfacePoolKeys(),
source.shoreBiomeWidth(), source.confinesKey(), source.shoreWidth(), source.erosion(), source.surfacePolicy());
}
private static HydrologyPlannerSettings wideSettings() {
HydrologyPlannerSettings.Surface surface = SETTINGS.surface();
return new HydrologyPlannerSettings(SETTINGS.seaLevel(), SETTINGS.routing(),
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(), 256,
surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(), surface.shoreWidth(), surface.banks()),
SETTINGS.hydraulics(), SETTINGS.underground(), SETTINGS.outlets(), SETTINGS.geometry(), SETTINGS.deepFluids(), SETTINGS.surfacePools(),
SETTINGS.widestShoreBiomeWidth(), SETTINGS.seaCaves(), SETTINGS.surfacePolicyBounds());
}
}
@@ -10,28 +10,6 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
public class HydrologyRouteHeightProbeTest {
@Test
public void crossDropUsesScalarHeightsIncludingMissingAndExtremeValues() {
HydrologyTerrainSample center = land(87);
HydrologyTerrainSample[] sides = {null, HydrologyTerrainSample.ocean(20, "ocean"),
land(20), land(100), land(Integer.MIN_VALUE), land(Integer.MAX_VALUE)};
for (HydrologyTerrainSample side : sides) {
Fixture fixture = fixture((x, z) -> side);
int lowest = side == null || side.ocean() ? 87 : Math.min(87, side.naturalHeight());
double expected = (double) (87 - lowest) * fixture.planner().settings.routing().valleyPreference()
* HydrologyRouteGeometry.CROSS_DROP_WEIGHT;
double actual = fixture.planner().routeGeometry.crossDropPenalty(
new HydrologyPoint(21, 87, -37), new RouteDirection(3D, 4D), center);
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
assertEquals(4, fixture.scalarCalls().get());
assertEquals(0, fixture.basisCalls().get());
}
Fixture untouched = fixture((x, z) -> land(20));
assertEquals(0D, untouched.planner().routeGeometry.crossDropPenalty(
new HydrologyPoint(0, 87, 0), new RouteDirection(0D, 0D), center), 0D);
assertEquals(0, untouched.scalarCalls().get());
}
@Test
public void pitChecksRetainMissingAndIntegerDifferenceSemantics() {
int threshold = HydrologyPlannerSettings.defaults().hydraulics().waterfallMinimumDrop();
@@ -1,254 +0,0 @@
package art.arcane.iris.generation.hydrology;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
public class HydrologyRouteTurnCacheTest {
@Test
public void repeatedRefinementMatchesExhaustiveUncachedRoutes() throws Exception {
Random random = new Random(712319L);
for (int scenario = 0; scenario < 40; scenario++) {
Solver solver = new Solver();
int count = 4 + random.nextInt(4);
HydrologyPoint[][] points = new HydrologyPoint[count][];
for (int layer = 0; layer < count; layer++) {
points[layer] = new HydrologyPoint[layer == 0 || layer == count - 1 ? 1 : 1 + random.nextInt(4)];
for (int candidate = 0; candidate < points[layer].length; candidate++) {
points[layer][candidate] = new HydrologyPoint(layer * 12 + random.nextInt(5), 80,
candidate * 9 + random.nextInt(7));
}
}
double maximumTurn = scenario % 2 == 0 ? 35D : 115D;
double[][][][] cache = new double[count][][][];
for (int refinement = 0; refinement < 8; refinement++) {
Fixture fixture = fixture(points, random);
assertArrayEquals("scenario=" + scenario + " refinement=" + refinement,
exhaustive(fixture, maximumTurn), solver.solve(fixture, maximumTurn, cache));
assertCacheGeometry(fixture, maximumTurn, cache);
}
}
}
@Test
public void twoLayerRefinementResetsReusedCostsAndKeepsFiniteRoutes() throws Exception {
Random random = new Random(173L);
HydrologyPoint[][] points = {
{new HydrologyPoint(0, 80, 0), new HydrologyPoint(0, 80, 4)},
{new HydrologyPoint(12, 80, 0), new HydrologyPoint(12, 80, 4)}
};
Solver solver = new Solver();
double[][][][] cache = new double[2][][][];
for (int refinement = 0; refinement < 32; refinement++) {
Fixture fixture = fixture(points, random);
assertArrayEquals(exhaustive(fixture, 75D), solver.solve(fixture, 75D, cache));
}
}
@Test
public void straightTiesAndDegenerateSegmentsKeepOriginalFirstChoice() throws Exception {
HydrologyPoint[][] points = {
{new HydrologyPoint(0, 80, 0)},
{new HydrologyPoint(0, 90, 0), new HydrologyPoint(0, 70, 0)},
{new HydrologyPoint(12, 80, 0), new HydrologyPoint(12, 80, 0)},
{new HydrologyPoint(24, 80, 0)}
};
Fixture fixture = fixture(points, new Random(2L));
for (double[] scores : fixture.scores()) {
Arrays.fill(scores, 0D);
}
for (double[] penalties : fixture.penalties()) {
Arrays.fill(penalties, 0D);
}
for (double[][] transitions : fixture.transitions()) {
if (transitions != null) {
for (double[] row : transitions) {
Arrays.fill(row, 0D);
}
}
}
assertArrayEquals(new int[]{0, 0, 0, 0}, new Solver().solve(fixture, 75D, new double[4][][][]));
}
private static Fixture fixture(HydrologyPoint[][] coordinates, Random random) {
int count = coordinates.length;
HydrologyPoint[][] points = new HydrologyPoint[count][];
double[][] scores = new double[count][];
double[][] penalties = new double[count][];
double[][][] transitions = new double[count][][];
for (int layer = 0; layer < count; layer++) {
int size = coordinates[layer].length;
points[layer] = new HydrologyPoint[size];
scores[layer] = new double[size];
penalties[layer] = new double[size];
for (int candidate = 0; candidate < size; candidate++) {
HydrologyPoint point = coordinates[layer][candidate];
points[layer][candidate] = new HydrologyPoint(point.x(), random.nextInt(200), point.z());
scores[layer][candidate] = random.nextDouble() * 20D;
penalties[layer][candidate] = random.nextDouble() * 10D;
}
if (layer > 0) {
transitions[layer] = new double[coordinates[layer - 1].length][size];
for (double[] row : transitions[layer]) {
for (int current = 0; current < size; current++) {
row[current] = random.nextInt(8) == 0 ? Double.POSITIVE_INFINITY : random.nextDouble() * 30D;
}
}
}
}
return new Fixture(points, scores, penalties, transitions);
}
private static int[] exhaustive(Fixture fixture, double maximumTurn) {
ArrayList<int[]> routes = new ArrayList<>();
enumerate(fixture.points(), new int[fixture.points().length], 0, routes);
int[] selected = new int[0];
double selectedCost = Double.POSITIVE_INFINITY;
for (int[] route : routes) {
double cost = fixture.scores()[0][route[0]] + fixture.scores()[1][route[1]]
+ fixture.penalties()[1][route[1]] + fixture.transitions()[1][route[0]][route[1]];
for (int layer = 2; layer < route.length && Double.isFinite(cost); layer++) {
double turn = turn(fixture.points()[layer - 2][route[layer - 2]],
fixture.points()[layer - 1][route[layer - 1]], fixture.points()[layer][route[layer]]);
if (turn > maximumTurn) {
cost = Double.POSITIVE_INFINITY;
break;
}
double excess = Math.max(0D, turn - 8D);
cost = cost + fixture.scores()[layer][route[layer]] + fixture.penalties()[layer][route[layer]]
+ fixture.transitions()[layer][route[layer - 1]][route[layer]]
+ excess * excess * 12D / maximumTurn;
}
if (cost < selectedCost || Double.isFinite(cost) && cost == selectedCost && precedes(route, selected)) {
selectedCost = cost;
selected = route;
}
}
return selected;
}
private static boolean precedes(int[] candidate, int[] selected) {
int last = candidate.length - 1;
if (candidate[last - 1] != selected[last - 1]) {
return candidate[last - 1] < selected[last - 1];
}
if (candidate[last] != selected[last]) {
return candidate[last] < selected[last];
}
for (int index = last - 2; index >= 0; index--) {
if (candidate[index] != selected[index]) {
return candidate[index] < selected[index];
}
}
return false;
}
private static void enumerate(HydrologyPoint[][] points, int[] route, int layer, List<int[]> routes) {
if (layer == route.length) {
routes.add(route.clone());
return;
}
for (int candidate = 0; candidate < points[layer].length; candidate++) {
route[layer] = candidate;
enumerate(points, route, layer + 1, routes);
}
}
private static void assertCacheGeometry(Fixture fixture, double maximumTurn, double[][][][] cache) {
int populated = 0;
for (int layer = 2; layer < cache.length; layer++) {
for (int previous = 0; previous < cache[layer].length; previous++) {
for (int current = 0; current < cache[layer][previous].length; current++) {
double[] costs = cache[layer][previous][current];
if (costs == null) {
continue;
}
for (int before = 0; before < costs.length; before++) {
if (Double.isNaN(costs[before])) {
continue;
}
double turn = turn(fixture.points()[layer - 2][before],
fixture.points()[layer - 1][previous], fixture.points()[layer][current]);
double excess = Math.max(0D, turn - 8D);
double expected = turn > maximumTurn ? Double.POSITIVE_INFINITY : excess * excess * 12D / maximumTurn;
assertEquals(Double.doubleToLongBits(expected), Double.doubleToLongBits(costs[before]));
populated++;
}
}
}
}
assertTrue(populated > 0);
}
private static double turn(HydrologyPoint before, HydrologyPoint previous, HydrologyPoint current) {
double incomingX = previous.x() - before.x();
double incomingZ = previous.z() - before.z();
double outgoingX = current.x() - previous.x();
double outgoingZ = current.z() - previous.z();
double incomingLength = StrictMath.hypot(incomingX, incomingZ);
double outgoingLength = StrictMath.hypot(outgoingX, outgoingZ);
if (incomingLength == 0D || outgoingLength == 0D) {
return 0D;
}
return StrictMath.toDegrees(StrictMath.acos(Math.max(-1D, Math.min(1D,
(incomingX * outgoingX + incomingZ * outgoingZ) / (incomingLength * outgoingLength)))));
}
private record Fixture(HydrologyPoint[][] points, double[][] scores, double[][] penalties, double[][][] transitions) {
}
private static final class Solver {
private final HydrologyRouteGeometry geometry = mock(HydrologyRouteGeometry.class, CALLS_REAL_METHODS);
private final Constructor<?> candidateConstructor;
private final Object tangent;
private final Method solve;
private final Method indices;
private HydrologyRouteGeometry.CurvatureWorkspace workspace;
private Solver() throws Exception {
Class<?> direction = RouteDirection.class;
Constructor<?> directionConstructor = direction.getDeclaredConstructor(double.class, double.class);
directionConstructor.setAccessible(true);
tangent = directionConstructor.newInstance(1D, 0D);
Class<?> candidate = RouteCandidate.class;
candidateConstructor = candidate.getDeclaredConstructor(HydrologyPoint.class, double.class, double.class,
double.class, double.class, double.class, direction, boolean.class);
candidateConstructor.setAccessible(true);
solve = HydrologyRouteGeometry.class.getDeclaredMethod("selectCurvatureAwareTerrainRoute", List.class,
double.class, double.class, double.class, double[][].class, double[][][].class, double[][][][].class,
HydrologyRouteGeometry.CurvatureWorkspace.class);
solve.setAccessible(true);
indices = solve.getReturnType().getDeclaredMethod("indices");
indices.setAccessible(true);
}
private int[] solve(Fixture fixture, double maximumTurn, double[][][][] cache) throws Exception {
ArrayList<List<Object>> layers = new ArrayList<>();
for (int layer = 0; layer < fixture.points().length; layer++) {
ArrayList<Object> candidates = new ArrayList<>();
for (int candidate = 0; candidate < fixture.points()[layer].length; candidate++) {
HydrologyPoint point = fixture.points()[layer][candidate];
candidates.add(candidateConstructor.newInstance(point, point.x(), point.z(), 0D,
fixture.scores()[layer][candidate], 0D, tangent, true));
}
layers.add(candidates);
}
if (workspace == null) {
workspace = new HydrologyRouteGeometry.CurvatureWorkspace(layers);
}
return (int[]) indices.invoke(solve.invoke(geometry, layers, 12D, maximumTurn, 12D,
fixture.penalties(), fixture.transitions(), cache, workspace));
}
}
}
@@ -259,7 +259,7 @@ public class HydrologySourceSpacingTest {
candidates.add(new OutletCandidate(node.index(), -1, new RiverOutlet(node.index() + 1000L,
HydrologyFeatureType.MOUTH, node.id(), point, point, 63, true)));
}
return new HydrologyRoutingPlan(potential, parent, outlets, lengths, List.copyOf(candidates), false);
return new HydrologyRoutingPlan(potential, parent, outlets, lengths, List.copyOf(candidates), false, null);
}
private static HydrologyTerrainSample land(int naturalHeight, double weight) {
@@ -64,6 +64,7 @@ public class HydrologyStyledGeometryTest {
Set.of(
HydrologyGeometrySampler.Field.SURFACE_WIDTH,
HydrologyGeometrySampler.Field.SURFACE_DEPTH,
HydrologyGeometrySampler.Field.SURFACE_BLEND_WIDTH,
HydrologyGeometrySampler.Field.UNDERGROUND_FLUID_LEVEL,
HydrologyGeometrySampler.Field.UNDERGROUND_WIDTH,
HydrologyGeometrySampler.Field.UNDERGROUND_DEPTH,
@@ -1,145 +0,0 @@
package art.arcane.iris.generation.hydrology;
import org.junit.Test;
import java.util.NavigableSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class HydrologySurfaceBankProbeTest {
@Test
public void raisingShoreWidthWidensTheBankProbeBandInsteadOfSlidingIt() {
NavigableSet<Integer> narrow = probedBankOffsets(1.5D);
NavigableSet<Integer> wide = probedBankOffsets(3D);
int innerRing = innerRing();
assertTrue("narrow band " + narrow, narrow.contains(innerRing));
assertTrue("wide band " + wide, wide.contains(innerRing));
assertTrue("band " + wide + " must still cover " + narrow, wide.containsAll(narrow));
assertTrue("band " + wide + " must reach past " + narrow, wide.size() > narrow.size());
}
@Test
public void theInnerProbeRingDoesNotMoveWithShoreWidth() {
int innerRing = innerRing();
for (double shoreWidth : new double[]{0D, 1.5D, 3D, 16D}) {
NavigableSet<Integer> band = probedBankOffsets(shoreWidth);
assertEquals("band " + band + " at shoreWidth " + shoreWidth,
innerRing, band.first().intValue());
}
}
@Test
public void bankHeightQueriesPreserveExactPenaltiesWithoutBankPolicySamples() {
HydrologyTerrainSampler terrain = (x, z) -> x + z == 7 ? null
: x < -3 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(80 + Math.floorMod(x * 3 + z * 7, 31), 0D, "land");
AtomicInteger basisCalls = new AtomicInteger();
AtomicInteger heightCalls = new AtomicInteger();
HydrologyNaturalTerrainSampler natural = new HydrologyNaturalTerrainSampler() {
@Override
public HydrologyTerrainSample sampleBasis(int x, int z) {
basisCalls.incrementAndGet();
return terrain.sample(x, z);
}
@Override
public double sampleLandHeight(int x, int z) {
heightCalls.incrementAndGet();
HydrologyTerrainSample sampled = terrain.sample(x, z);
return sampled == null || sampled.ocean() ? Double.NaN : sampled.naturalHeight();
}
@Override
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
throw new AssertionError("Bank probes do not require a routing grid");
}
@Override
public NaturalClassification classifyNatural(int x, int z) {
throw new AssertionError("Bank probes only require land heights");
}
};
for (double angle : new double[]{0D, 0.37D, 0.7853981633974483D}) {
HydrologyPlannerSettings settings = settings(3D);
HydrologyPlanner reference = new HydrologyPlanner(91L, settings, terrain);
HydrologyPlanner optimized = new HydrologyPlanner(91L, settings, terrain, natural,
HydrologyGeometrySampler.deterministic(terrain), -4096,
footprint -> new HydrologyTerrainCaveVoxelView(terrain, settings.seaLevel(), -4096, 4096));
HydrologyPoint point = new HydrologyPoint(0, 80, 0);
RouteCandidate candidate = new RouteCandidate(point, 0D, 0D, 0D, 0D, 0D,
new RouteDirection(StrictMath.cos(angle), StrictMath.sin(angle)), true);
basisCalls.set(0);
heightCalls.set(0);
double expected = reference.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate);
double actual = optimized.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate);
assertEquals(Double.doubleToLongBits(expected), Double.doubleToLongBits(actual));
assertEquals(1, basisCalls.get());
assertTrue(heightCalls.get() > 0);
optimized.planningSamples.set(new HydrologyPlanner.PlanningSamples());
try {
assertEquals(expected, optimized.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate), 0D);
int firstPassHeightCalls = heightCalls.get();
assertEquals(expected, optimized.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate), 0D);
assertEquals(firstPassHeightCalls, heightCalls.get());
} finally {
optimized.planningSamples.remove();
}
}
}
/** One block outside half the nominal maximum width - see HydrologySurfaceCoursePlanner. */
private int innerRing() {
return (int) StrictMath.ceil(
HydrologyPlannerSettings.defaults().surface().maximumWidth() / 2D) + 1;
}
private NavigableSet<Integer> probedBankOffsets(double shoreWidth) {
HydrologyPoint center = new HydrologyPoint(0, 84, 0);
NavigableSet<Integer> probed = new TreeSet<>();
HydrologyTerrainSampler sampler = (x, z) -> {
if (x == center.x()) {
probed.add(Math.abs(z - center.z()));
}
return HydrologyTerrainSample.openLand(84, 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(91L, settings(shoreWidth), sampler);
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
planner.surfaceCourses.surfaceRouteCandidateBankPenalty(new RouteCandidate(
center, center.x(), center.z(), 0D, 0D, 0D, new RouteDirection(1D, 0D), true));
probed.remove(0);
return probed;
}
private HydrologyPlannerSettings settings(double shoreWidth) {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Surface surface = base.surface();
return new HydrologyPlannerSettings(
base.seaLevel(),
base.routing(),
new HydrologyPlannerSettings.Surface(
surface.enabled(),
surface.sources(),
surface.minimumWidth(),
surface.maximumWidth(),
surface.minimumDepth(),
surface.maximumDepth(),
surface.maximumIncision(),
shoreWidth,
surface.banks()),
base.hydraulics(),
base.underground(),
base.outlets(),
base.geometry(),
base.deepFluids(),
base.surfacePools(),
base.widestShoreBiomeWidth(),
base.seaCaves(),
base.surfacePolicyBounds());
}
}
@@ -51,6 +51,75 @@ public final class HydrologySurfaceDropRasterTest {
assertThrows(UnsupportedOperationException.class, () -> raster.columns().clear());
}
@Test
public void diagonalDropCoordinatesResolveExactlyInBothDirectionsFromTheOrigin() {
for (int origin : new int[]{-128, 128}) {
HydraulicSegment fall = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
90, 80, 4, 3, true, true,
List.of(new HydrologyPoint(origin, 90, origin), new HydrologyPoint(origin + 12, 80, origin + 12)),
HydraulicChannelProfile.uniform(4, 3));
RiverCourse course = new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"water", 1, List.of(), List.of(fall));
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(SETTINGS, TERRAIN, GEOMETRY, course);
assertFalse(raster.columns().isEmpty());
for (HydrologyColumnSample column : raster.columns()) {
assertSame(column, raster.sample(column.x(), column.z()).orElseThrow());
}
assertTrue(raster.connects(origin, origin, 90));
assertTrue(raster.connects(origin + 12, origin + 12, 80));
assertTrue(raster.sample(origin + 512, origin + 512).isEmpty());
assertFalse(raster.connects(origin + 512, origin + 512, 80));
}
}
@Test
public void receivingDropBedsUseInletIncisionOnlyAtAuthorizedCourseStations() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 200
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(107, 0D, "parent");
for (int start : new int[]{50, 100, 150}) {
int end = start == 100 ? 110 : start + 1;
HydraulicSegment approach = new HydraulicSegment(1L, 1L, HydrologyFeatureType.SURFACE_POOL,
122, 122, 4, 3, false, false,
List.of(new HydrologyPoint(0, 122, 0), new HydrologyPoint(start, 122, 0)),
HydraulicChannelProfile.uniform(4, 3));
HydraulicSegment fall = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
122, 78, 4, 3, true, true,
List.of(new HydrologyPoint(start, 122, 0), new HydrologyPoint(end, 78, 0)),
HydraulicChannelProfile.uniform(4, 3));
HydraulicSegment downstream = new HydraulicSegment(3L, 1L, HydrologyFeatureType.CASCADE,
78, SETTINGS.seaLevel(), 4, 3, false, false,
List.of(new HydrologyPoint(end, 78, 0), new HydrologyPoint(199, SETTINGS.seaLevel(), 0)),
HydraulicChannelProfile.uniform(4, 3));
HydraulicSegment mouth = new HydraulicSegment(4L, 1L, HydrologyFeatureType.MOUTH,
SETTINGS.seaLevel(), SETTINGS.seaLevel(), 4, 3, false, false,
List.of(new HydrologyPoint(199, SETTINGS.seaLevel(), 0), new HydrologyPoint(200, SETTINGS.seaLevel(), 0)),
HydraulicChannelProfile.uniform(4, 3));
RiverCourse course = new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"water", 1, List.of(), List.of(approach, fall, downstream, mouth));
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(SETTINGS, terrain, GEOMETRY, course);
assertEquals(start != 50, raster.connects(end, 0, 78));
if (start != 50) {
HydrologyColumnSample receiver = raster.sample(end, 0).orElseThrow();
HydrologyColumnLayer water = receiver.layers().stream()
.filter(layer -> layer.channel() && layer.fluidOwned()).findFirst().orElseThrow();
int cut = receiver.naturalHeight() - water.bedY();
assertTrue(cut > SETTINGS.surface().maximumIncision());
assertTrue(cut <= SETTINGS.surface().banks().inlet().maximumIncision());
}
if (start == 100) {
assertFalse(raster.connects(102, 0, 78));
assertTrue(raster.connects(108, 0, 78));
}
SurfaceBounds bounds = new SurfaceBounds(start - 2, -8, end + 2, 8);
HydrologySurfaceDropRaster bounded = HydrologySurfaceDropRaster.compile(SETTINGS, terrain, GEOMETRY, course, bounds);
assertEquals(raster.columns().stream().filter(column -> bounds.contains(column.x(), column.z())).toList(),
bounded.columns());
}
}
@Test
public void surfaceDropsDoNotOwnSubmergedGroundOrRaiseWaterOnSeaLevelLand() {
RiverCourse course = course(true);
@@ -97,7 +166,7 @@ public final class HydrologySurfaceDropRasterTest {
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN, GEOMETRY);
HydrologyPlannerSettings.Excavation limits = SETTINGS.surface().banks().erosion().excavation();
FootprintLayerShape shape = new FootprintLayerShape(2, 2D, 40D, 79, 90, 90,
false, false, false, false, true);
false, false, false, false, true, false);
HydrologyFeatureRef feature = new HydrologyFeatureRef(5L, segment.type(), course.id(), segment.id(),
0, 90, 0, 1, 0, false);
HydrologyTerrainSample highTerrain = HydrologyTerrainSample.openLand(160, 0D, "parent");
@@ -5,54 +5,9 @@ import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class HydrologySurfaceRouteGeometryTest {
@Test
public void lazyTransitionGeometryIsReusedUntilResolvedHeightsChange() {
HydrologyPlanner planner = new HydrologyPlanner(91L, HydrologyPlannerSettings.defaults(),
(x, z) -> {
throw new AssertionError("Geometry lookup must not sample terrain.");
});
RouteCandidate previous = candidate(0D, 0D);
RouteCandidate current = spy(candidate(4D, 0D));
HydrologyRouteGeometry.CurvatureWorkspace workspace = new HydrologyRouteGeometry.CurvatureWorkspace(
List.of(List.of(previous), List.of(current)));
double expected = planner.routePaths.routeTransitionGeometryCost(previous, current, 4D);
clearInvocations(current);
for (int iteration = 0; iteration < 8; iteration++) {
double actual = planner.routeGeometry.resolvedTransitionCost(
previous, current, 4D, Double.NaN, workspace, 1, 0, 0);
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
}
verify(current, times(1)).offset();
RouteCandidate raised = spy(new RouteCandidate(new HydrologyPoint(4, 91, 0),
current.continuousX(), current.continuousZ(), current.offset(), current.localScore(),
current.terrainScore(), current.tangent(), true));
double raisedExpected = planner.routePaths.routeTransitionGeometryCost(previous, raised, 4D);
assertTrue(raisedExpected > expected);
clearInvocations(raised);
for (int iteration = 0; iteration < 8; iteration++) {
double actual = planner.routeGeometry.resolvedTransitionCost(
previous, raised, 4D, Double.NaN, workspace, 1, 0, 0);
assertEquals(Double.doubleToRawLongBits(raisedExpected), Double.doubleToRawLongBits(actual));
}
verify(raised, times(1)).offset();
assertEquals(Double.doubleToRawLongBits(-0D), Double.doubleToRawLongBits(
planner.routeGeometry.resolvedTransitionCost(previous, raised, 4D, -0D,
workspace, 1, 0, 0)));
assertEquals(Double.POSITIVE_INFINITY,
planner.routeGeometry.resolvedTransitionCost(previous, raised, 4D,
Double.POSITIVE_INFINITY, workspace, 1, 0, 0), 0D);
}
@Test
public void surfaceAnchorKeepsTheCoarseValleyWhenSmoothGroundIsMuchHigher() {
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(
@@ -85,7 +40,7 @@ public class HydrologySurfaceRouteGeometryTest {
}
@Test
public void steepSurfaceEdgesCanDetourAroundAContainedTerrainPit() {
public void undergroundEdgesRejectAContainedTerrainPit() {
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(
128 - Math.floorDiv(x, 2) - (x >= 30 && x <= 34 && Math.abs(z) <= 1 ? 14 : 0),
0D, "land");
@@ -94,47 +49,7 @@ public class HydrologySurfaceRouteGeometryTest {
HydrologyPoint end = new HydrologyPoint(64, 96, 0);
HydrologyPoint continuation = new HydrologyPoint(96, 80, 0);
List<HydrologyPoint> cave = planner.routeGeometry.refineEdge(1L, 2L, start, end, continuation, 4, false);
List<HydrologyPoint> surface = planner.routeGeometry.refineEdge(1L, 2L, start, end, continuation, 4, true);
List<HydrologyPoint> cave = planner.routeGeometry.refineUndergroundEdge(1L, 2L, start, end, continuation, 4);
assertTrue(cave.isEmpty());
assertFalse(surface.isEmpty());
assertTrue(surface.stream().anyMatch(point -> Math.abs(point.z()) > 1));
assertTrue(surface.stream().allMatch(point -> Math.abs(point.z()) <= 16));
assertTrue(planner.routePaths.traversableRoute(surface));
assertFalse(planner.routePaths.containsTerrainPit(surface));
}
@Test
public void curvatureUsesTheContinuousCurveBeforeBlockRounding() {
HydrologyPlanner planner = new HydrologyPlanner(91L, HydrologyPlannerSettings.defaults(),
(x, z) -> HydrologyTerrainSample.openLand(84, 0D, "land"));
RouteCandidate first = candidate(0D, 0D);
RouteCandidate middle = candidate(3.6D, 0.49D);
RouteCandidate last = candidate(7D, 2.1D);
assertTrue(HydrologyRoutePath.routeTurnDegrees(first.point(), middle.point(), last.point()) > 20D);
assertTrue(planner.routeGeometry.continuousTurnDegrees(first, middle, last) < 20D);
RouteCandidate[] route = planner.routeGeometry.selectCurvatureAwareTerrainRoute(
List.of(List.of(first), List.of(middle), List.of(last)), 4D, 20D, 2.5D);
assertEquals(3, route.length);
}
@Test
public void continuousCurvatureStillRejectsASharpTurn() {
HydrologyPlanner planner = new HydrologyPlanner(91L, HydrologyPlannerSettings.defaults(),
(x, z) -> HydrologyTerrainSample.openLand(84, 0D, "land"));
RouteCandidate[] route = planner.routeGeometry.selectCurvatureAwareTerrainRoute(
List.of(List.of(candidate(0D, 0D)), List.of(candidate(4D, 0D)), List.of(candidate(4D, 4D))),
4D, 20D, 2.5D);
assertEquals(0, route.length);
}
private RouteCandidate candidate(double x, double z) {
return new RouteCandidate(
new HydrologyPoint((int) StrictMath.round(x), 84, (int) StrictMath.round(z)),
x, z, 0D, 0D, 0D, new RouteDirection(1D, 0D), true);
}
}
@@ -6,6 +6,9 @@ import art.arcane.iris.testsupport.Await;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.cache.CacheKey;
import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import java.lang.reflect.Field;
import java.time.Duration;
@@ -50,6 +53,55 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
public class HydrologyTileCacheTest {
@Rule
public TemporaryFolder preparedFolder = new TemporaryFolder();
@Test
public void preparedPlansRestoreFarFromOriginWithoutTerrainPlanning() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(emptySettings());
HydrologyTileCache.SharedCacheScope scope = sharedScope();
HydrologyTileKey key = new HydrologyTileKey(501, -703);
HydrologyTile tile = preparedTile(key, scope);
Path root = preparedFolder.newFolder().toPath();
new PreparedHydrologyTileStore(root, scope, 64).save(tile);
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) {
cache.enableSharedCache(scope, root);
cache.setTerrainPreparation(() -> { throw new AssertionError("Restoration must not sample terrain"); });
HydrologyTile restored = cache.get(key);
assertEquals(tile.key(), restored.key());
assertEquals(tile.footprint(), restored.footprint());
verify(planner, never()).plan(any(HydrologyTileKey.class));
verify(planner).reuseResolvedTile(restored);
}
}
@Test
public void tileDemandAutomaticallyPersistsTheCompletedPlan() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(emptySettings());
HydrologyTileCache.SharedCacheScope scope = sharedScope();
HydrologyTileKey key = new HydrologyTileKey(125, -141);
HydrologyTile tile = preparedTile(key, scope);
when(planner.plan(key)).thenReturn(tile);
Path root = preparedFolder.newFolder().toPath();
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 64)) {
cache.enableSharedCache(scope, root);
assertSame(tile, cache.get(key));
verify(planner).plan(key);
}
HydrologyTile persisted = new PreparedHydrologyTileStore(root, scope, 64).load(key).orElseThrow();
assertEquals(tile.key(), persisted.key());
assertEquals(tile.worldSeed(), persisted.worldSeed());
assertEquals(tile.settingsFingerprint(), persisted.settingsFingerprint());
assertEquals(tile.footprint(), persisted.footprint());
}
private static HydrologyTile preparedTile(HydrologyTileKey key, HydrologyTileCache.SharedCacheScope scope) {
return new HydrologyTile(key, scope.worldSeed(), scope.settingsFingerprint(), 64,
List.of(), List.of(), List.of(), List.of(), Set.of(), List.of(), List.of(), RiverFootprint.empty());
}
@Test
public void weightedTileCapacityStopsSpeculationWithoutBlockingDemand() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
@@ -400,7 +452,7 @@ public class HydrologyTileCacheTest {
monitor.set(queue.get(cache));
if (pregeneration) {
cache.preparePregeneration(262144, 262144);
cache.preparePregeneration(pregenArea(262144, 262144));
} else {
cache.prefetchArea(261120, 261120, 263168, 263168, 262144, 262144);
}
@@ -409,8 +461,12 @@ public class HydrologyTileCacheTest {
assertEquals(prefetchRectangle(254, 257, 254, 257), new HashSet<>(planned.subList(0, 16)));
assertEquals(List.of(new HydrologyTileKey(255, 255), new HydrologyTileKey(256, 255),
new HydrologyTileKey(257, 255)), planned.subList(1, 4));
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned.subList(16, planned.size())));
assertEquals(22, planned.size());
if (pregeneration) {
assertEquals(16, planned.size());
} else {
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned.subList(16, planned.size())));
assertEquals(22, planned.size());
}
}
}
@@ -447,7 +503,7 @@ public class HydrologyTileCacheTest {
Runnable demand = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(demand);
cache.preparePregeneration(262144, 262144);
cache.preparePregeneration(pregenArea(262144, 262144));
assertFalse(active.isDone());
assertFalse(demanded.isDone());
@@ -485,9 +541,9 @@ public class HydrologyTileCacheTest {
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
cache.preparePregeneration(262144, 262144);
cache.preparePregeneration(263168, 262144);
cache.preparePregeneration(263168, 262144);
cache.preparePregeneration(pregenArea(262144, 262144));
cache.preparePregeneration(pregenArea(263168, 262144));
cache.preparePregeneration(pregenArea(263168, 262144));
drainPrefetchTasks(queued);
Set<HydrologyTileKey> expected = new HashSet<>(prefetchRectangle(255, 258, 254, 257));
@@ -496,7 +552,7 @@ public class HydrologyTileCacheTest {
assertEquals(17, planned.size());
cache.clear();
planned.clear();
cache.preparePregeneration(263168, 262144);
cache.preparePregeneration(pregenArea(263168, 262144));
drainPrefetchTasks(queued);
assertEquals(prefetchRectangle(255, 258, 254, 257), new HashSet<>(planned));
assertEquals(16, planned.size());
@@ -516,13 +572,113 @@ public class HydrologyTileCacheTest {
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
assertThrows(ArithmeticException.class, () -> cache.preparePregeneration(Integer.MAX_VALUE, 0));
assertThrows(ArithmeticException.class, () -> cache.preparePregeneration(0, Integer.MIN_VALUE));
assertThrows(IllegalArgumentException.class, () -> cache.preparePregeneration(
new HydrologyTileCache.PregenerationArea(0, 0, 1, -1, 2, 1)));
assertThrows(ArithmeticException.class, () -> cache.preparePregeneration(
new HydrologyTileCache.PregenerationArea(0, 0, Long.MIN_VALUE, -1, 1, 1)));
drainPrefetchTasks(queued);
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned));
assertEquals(6, planned.size());
}
@Test
public void pregenBoundsLimitSpeculationWithoutLimitingRequestedChunks() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
new HydrologyTileCache.PregenerationArea(0, 0, -1536, -1536, 2047, 2047))) {
cache.prefetchArea(-4096, -4096, 4095, 4095, 0, 0);
assertEquals(prefetchRectangle(-2, 2, -2, 2), new HashSet<>(planned));
planned.clear();
HydrologyTileKey demanded = new HydrologyTileKey(8, 8);
assertSame(tile, cache.get(demanded));
assertEquals(List.of(demanded), planned);
planned.clear();
assertFalse(cache.isPlanned(10_240, 10_240));
assertTrue(cache.isPlanned(10_240, 10_240));
assertEquals(prefetchRectangle(9, 10, 9, 10), new HashSet<>(planned));
planned.clear();
cache.prepareChunkColumns(-10_240, -10_240);
assertEquals(prefetchRectangle(-11, -10, -11, -10), new HashSet<>(planned));
}
planned.clear();
cache.prepareChunkColumns(20_480, 20_480);
assertEquals(prefetchRectangle(18, 21, 18, 21), new HashSet<>(planned));
cache.close();
}
@Test
public void pregenScopesRestoreStudioPrefetchAndIgnoreSupersededClose() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
cache.setNeighbourPrefetchEnabled(false);
HydrologyTileCache.PregenerationScope first = cache.preparePregeneration(pregenArea(0, 0));
HydrologyTileCache.PregenerationScope second = cache.preparePregeneration(pregenArea(262144, 262144));
first.close();
planned.clear();
cache.prefetchArea(16384, 16384, 16384, 16384, 16384, 16384);
assertTrue(planned.isEmpty());
second.close();
cache.prepareChunkColumns(16384, 16384);
assertEquals(prefetchRectangle(15, 16, 15, 16), new HashSet<>(planned));
HydrologyTileCache.PregenerationScope cleared = cache.preparePregeneration(pregenArea(0, 0));
cache.clear();
cleared.close();
planned.clear();
cache.prepareChunkColumns(32768, 32768);
assertEquals(prefetchRectangle(31, 32, 31, 32), new HashSet<>(planned));
HydrologyTileCache.PregenerationScope closed = cache.preparePregeneration(pregenArea(0, 0));
cache.close();
closed.close();
cache.prefetchArea(0, 0, 0, 0, 0, 0);
assertThrows(CancellationException.class, () -> cache.preparePregeneration(pregenArea(0, 0)));
}
@Test
public void largePregenBoundsKeepInitialWarmupLocal() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
new HydrologyTileCache.PregenerationArea(0, 0,
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE))) {
assertEquals(prefetchRectangle(-2, 1, -2, 1), new HashSet<>(planned));
assertEquals(16, planned.size());
}
cache.close();
}
private static HydrologyTileCache.PregenerationArea pregenArea(int x, int z) {
return new HydrologyTileCache.PregenerationArea(x, z, (long) x - 1024, (long) z - 1024,
(long) x + 1024, (long) z + 1024);
}
private static HydrologyPlannerSettings stalePrefetchSettings() {
HydrologyPlannerSettings settings = mock(HydrologyPlannerSettings.class);
when(settings.routing()).thenReturn(new HydrologyPlannerSettings.Routing(
@@ -1100,7 +1256,7 @@ public class HydrologyTileCacheTest {
order.clear();
cache.setNeighbourPrefetchEnabled(false);
cache.preparePregeneration(0, 0);
cache.preparePregeneration(pregenArea(0, 0));
assertEquals(new HydrologyTileKey(1, -1), order.getFirst());
assertTrue(order.contains(new HydrologyTileKey(1, 0)));
@@ -35,14 +35,14 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class StudioHydrologyTileStoreTest {
public class PreparedHydrologyTileStoreTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void roundTripsValidatedEntryTile() throws Exception {
HydrologyTile original = tile();
StudioHydrologyTileStore store = store();
PreparedHydrologyTileStore store = store();
store.save(original);
HydrologyTile restored = store.load(original.key()).orElseThrow();
@@ -93,7 +93,7 @@ public class StudioHydrologyTileStoreTest {
HydrologyTile original = new HydrologyTile(new HydrologyTileKey(0, 0), 91L, 17L, 64,
List.of(), List.of(), List.of(), List.of(course), Set.of(), List.of(plan), List.of(),
new RiverFootprint(Map.of(RiverFootprint.pack(0, 0), column)));
StudioHydrologyTileStore store = store();
PreparedHydrologyTileStore store = store();
store.save(original);
HydrologyTile restored = store.load(original.key()).orElseThrow();
assertEquals(original, restored);
@@ -118,7 +118,7 @@ public class StudioHydrologyTileStoreTest {
@Test
public void entryRequiresRegionalOwnershipMetadata() throws Exception {
HydrologyTile original = tile();
StudioHydrologyTileStore store = store();
PreparedHydrologyTileStore store = store();
store.save(original);
Path file = store.file(original.key());
JsonObject persisted;
@@ -136,7 +136,7 @@ public class StudioHydrologyTileStoreTest {
@Test
public void corruptEntryFallsBackToCacheMiss() throws Exception {
HydrologyTile original = tile();
StudioHydrologyTileStore store = store();
PreparedHydrologyTileStore store = store();
store.save(original);
Path file = store.file(original.key());
Files.writeString(file, "invalid", StandardCharsets.UTF_8);
@@ -149,15 +149,28 @@ public class StudioHydrologyTileStoreTest {
Path root = temporaryFolder.newFolder().toPath();
HydrologyTileCache.SharedCacheScope scope = scope();
HydrologyTile original = tile();
StudioHydrologyTileStore matching = new StudioHydrologyTileStore(root, scope, 64);
PreparedHydrologyTileStore matching = new PreparedHydrologyTileStore(root, scope, 64);
matching.save(original);
StudioHydrologyTileStore mismatched = new StudioHydrologyTileStore(root, scope, 128);
PreparedHydrologyTileStore mismatched = new PreparedHydrologyTileStore(root, scope, 128);
assertTrue(mismatched.load(original.key()).isEmpty());
}
private StudioHydrologyTileStore store() throws Exception {
return new StudioHydrologyTileStore(
@Test
public void copiedTileCannotCrossRuntimeIdentityScopes() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
PreparedHydrologyTileStore source = new PreparedHydrologyTileStore(root, scope(), 64);
PreparedHydrologyTileStore target = new PreparedHydrologyTileStore(root,
new HydrologyTileCache.SharedCacheScope("another-kernel", 91L, 384, "overworld", 17L), 64);
HydrologyTile tile = tile();
source.save(tile);
Files.createDirectories(target.file(tile.key()).getParent());
Files.copy(source.file(tile.key()), target.file(tile.key()));
assertTrue(target.load(tile.key()).isEmpty());
}
private PreparedHydrologyTileStore store() throws Exception {
return new PreparedHydrologyTileStore(
temporaryFolder.newFolder().toPath(),
scope(),
64
@@ -13,9 +13,11 @@ import art.arcane.iris.generation.hydrology.IrisRiverBedProfile;
import art.arcane.iris.generation.hydrology.IrisRiverBlendStyle;
import org.junit.Test;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntBinaryOperator;
import static org.junit.Assert.assertEquals;
@@ -31,6 +33,89 @@ public class ErosionFieldCompilerTest {
default -> request.minimum();
};
@Test
public void geometricExclusionAvoidsTerrainSamplesWithoutChangingAsymmetricBanks() {
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
AtomicInteger referenceSamples = new AtomicInteger();
AtomicInteger boundedSamples = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(z > 4 ? 100 : 80, 0D, "land");
ErosionField reference = rasterWithShoreBound(surface, (x, z) -> {
referenceSamples.incrementAndGet();
return terrain.sample(x, z);
}, 128D, null);
ErosionField bounded = rasterWithShoreBound(surface, (x, z) -> {
boundedSamples.incrementAndGet();
return terrain.sample(x, z);
}, surface.shoreWidth(), null);
assertEqualFields(reference, bounded);
assertNull(bounded.rejection());
assertTrue("sample counts " + boundedSamples.get() + "/" + referenceSamples.get(),
boundedSamples.get() < referenceSamples.get() * 0.8D);
assertNotNull(bounded.column(0, 18));
assertNull(bounded.column(0, -18));
}
@Test
public void geometricExclusionRetainsPolicyBandsAtExactAndAdjacentThresholds() {
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
for (double width : new double[]{Math.nextDown(1.75D), 1.75D, Math.nextUp(1.75D), 12D}) {
HydrologyTerrainSampler terrain = (x, z) -> land(80, z >= 0 ? width : 0D)
.withShoreWidth(z < 0 ? width : surface.shoreWidth());
double bound = Math.max(surface.shoreWidth(), width);
ErosionField reference = rasterWithShoreBound(surface, terrain, 128D, null);
ErosionField complete = rasterWithShoreBound(surface, terrain, bound, null);
assertEqualFields(reference, complete);
for (SurfaceBounds bounds : List.of(new SurfaceBounds(-16, -16, -1, 15),
new SurfaceBounds(0, -16, 15, 15))) {
ErosionField bounded = rasterWithShoreBound(surface, terrain, bound, bounds);
for (int z = bounds.minimumZ(); z <= bounds.maximumZ(); z++) {
for (int x = bounds.minimumX(); x <= bounds.maximumX(); x++) {
assertEquals("policy width " + width + " at " + x + "," + z,
complete.column(x, z), bounded.column(x, z));
}
}
}
if (width >= 1.75D) {
assertEquals(SurfaceRole.SHORE, complete.column(0, 5).role());
}
if (width == 12D) {
assertEquals(SurfaceRole.SHORE, complete.column(0, 15).role());
assertEquals(SurfaceRole.BANK, complete.column(0, -15).role());
}
}
}
private static ErosionField rasterWithShoreBound(HydrologyPlannerSettings.Surface surface,
HydrologyTerrainSampler terrain, double bound,
SurfaceBounds bounds) {
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(SEA_LEVEL, defaults.routing(), surface,
defaults.hydraulics(), defaults.underground(), defaults.outlets(), defaults.geometry(),
defaults.deepFluids(), defaults.surfacePools(), bound, defaults.seaCaves(), defaults.surfacePolicyBounds());
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
new HydrologyPoint(-48, 80, 0), new HydrologyPoint(48, 80, 0)));
double[] widths = new double[centerline.size()];
double[] depths = new double[centerline.size()];
double[] multipliers = new double[centerline.size()];
int[] heads = new int[centerline.size()];
Arrays.fill(widths, 3D);
Arrays.fill(depths, 2D);
Arrays.fill(multipliers, 1D);
Arrays.fill(heads, 80);
return new ErosionFieldCompiler(settings, terrain).compile(42L, centerline,
new ChannelProfile(widths, depths, multipliers), ValleyProfile.fromHeads(heads, heads.length),
SurfaceTerminal.SINKHOLE, 0, noPonds(), SurfaceRasterContext.bounded(bounds));
}
private static void assertEqualFields(ErosionField expected, ErosionField actual) {
assertEquals(expected.columns(), actual.columns());
assertEquals(expected.rejection(), actual.rejection());
assertEquals(expected.rejectionDetail(), actual.rejectionDetail());
assertEquals(expected.uncontainedWetCells(), actual.uncontainedWetCells());
assertEquals(expected.bankExcavation(), actual.bankExcavation());
}
@Test
public void mouthCutsDrySeaLevelSillBeforeReachingOceanWater() {
HydrologyTerrainSampler coast = (int x, int z) -> x >= 100
@@ -81,7 +166,7 @@ public class ErosionFieldCompilerTest {
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(new HydrologyPoint(-150, 0, 0), new HydrologyPoint(150, 0, 0)));
ChannelProfile channel = new ChannelProfileBuilder(surface, hillside, CONSTANT_GEOMETRY).build(centerline, "water", false);
ValleyProfile valley = new ValleyProfileSolver(surface, hillside, SEA_LEVEL, 64).solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
ErosionFieldCompiler compiler = new ErosionFieldCompiler(surface, hillside, SEA_LEVEL);
ErosionFieldCompiler compiler = new ErosionFieldCompiler(rasterSettings(surface), hillside);
ErosionField complete = compiler.compile(42L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 8);
SurfaceBounds bounds = new SurfaceBounds(-16, -16, 15, 15);
ErosionField bounded = compiler.compile(42L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 8, surface.banks().ponds(), SurfaceRasterContext.bounded(bounds));
@@ -93,6 +178,44 @@ public class ErosionFieldCompilerTest {
}
}
@Test
public void boundedRasterRaisesBanksFromHigherWaterAcrossTheChunkEdge() {
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
HydrologyTerrainSampler lowGround = (x, z) -> HydrologyTerrainSample.openLand(77, 0D, "land");
SurfaceCenterline centerline = SurfaceCenterline.densify(
List.of(new HydrologyPoint(-48, 80, 0), new HydrologyPoint(48, 78, 0)));
int count = centerline.size();
double[] widths = new double[count];
double[] depths = new double[count];
double[] multipliers = new double[count];
int[] heads = new int[count];
Arrays.fill(widths, 3D);
Arrays.fill(depths, 2D);
Arrays.fill(multipliers, 1D);
for (int station = 0; station < count; station++) {
heads[station] = centerline.x()[station] < 0 ? 80 : 78;
}
ChannelProfile channel = new ChannelProfile(widths, depths, multipliers);
ValleyProfile valley = ValleyProfile.fromHeads(heads, count);
ErosionFieldCompiler compiler = new ErosionFieldCompiler(rasterSettings(surface), lowGround);
ErosionField complete = compiler.compile(42L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 0);
assertNull(complete.rejection());
assertEquals(80, complete.column(0, 4).height());
for (SurfaceBounds bounds : List.of(new SurfaceBounds(-16, -16, -1, 15),
new SurfaceBounds(0, -16, 15, 15), new SurfaceBounds(0, 4, 15, 19))) {
ErosionField bounded = compiler.compile(42L, centerline, channel, valley,
SurfaceTerminal.SINKHOLE, 0, noPonds(), SurfaceRasterContext.bounded(bounds));
assertNull(bounded.rejection());
assertEquals(0, bounded.uncontainedWetCells());
for (int z = bounds.minimumZ(); z <= bounds.maximumZ(); z++) {
for (int x = bounds.minimumX(); x <= bounds.maximumX(); x++) {
assertEquals("bounded column " + x + "," + z, complete.column(x, z), bounded.column(x, z));
}
}
}
}
@Test
public void flatTerrainHoldsTheWaterFlushWithTheBankAndContainedByDefault() {
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
@@ -320,6 +443,12 @@ public class ErosionFieldCompilerTest {
surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(), surface.shoreWidth(), banks);
}
private static HydrologyPlannerSettings.Surface withMaximumIncision(HydrologyPlannerSettings.Surface surface, int maximumIncision) {
return new HydrologyPlannerSettings.Surface(
surface.enabled(), surface.sources(), surface.minimumWidth(), surface.maximumWidth(),
surface.minimumDepth(), surface.maximumDepth(), maximumIncision, surface.shoreWidth(), surface.banks());
}
@Test
public void bankColumnsBeyondTheBlendAreNotWritten() {
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
@@ -333,6 +462,14 @@ public class ErosionFieldCompilerTest {
}
}
private static void assertBoundedBankFill(Compiled compiled, int maximumFill) {
for (SurfaceColumn column : compiled.field().columns().values()) {
int fill = column.height() - column.terrain().naturalHeight();
assertTrue(column.x() + "," + column.z() + " fill " + fill,
fill <= (column.role() == SurfaceRole.CHANNEL ? 0 : maximumFill));
}
}
private static void assertChannelContained(Compiled compiled) {
for (SurfaceColumn column : compiled.field().columns().values()) {
if (column.role() != SurfaceRole.CHANNEL || column.apron()) {
@@ -376,7 +513,7 @@ public class ErosionFieldCompilerTest {
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64)
.solve(centerline, channel, terminal, terminalHead);
assertNull(valley.rejection());
ErosionField field = new ErosionFieldCompiler(surface, sampler, SEA_LEVEL)
ErosionField field = new ErosionFieldCompiler(rasterSettings(surface), sampler)
.compile(1234L, centerline, channel, valley, terminal, 8);
return new Compiled(field, valley);
}
@@ -624,7 +761,7 @@ public class ErosionFieldCompilerTest {
.solve(centerline, channel, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection());
ErosionField field = new ErosionFieldCompiler(surface, sampler, SEA_LEVEL)
ErosionField field = new ErosionFieldCompiler(rasterSettings(surface), sampler)
.compile(99L, centerline, channel, valley, SurfaceTerminal.OCEAN_MOUTH, 3);
int firstApron = Integer.MAX_VALUE;
@@ -754,10 +891,12 @@ public class ErosionFieldCompilerTest {
assertEquals(0, compiled.field().uncontainedWetCells());
assertChannelContained(compiled);
assertNoWriteAboveNatural(compiled);
assertBoundedBankFill(compiled, 8);
assertEquals(80, compiled.field().column(0, 0).headY());
SurfaceColumn nearRim = compiled.field().column(0, 4);
assertEquals(SurfaceRole.CHANNEL, nearRim.role());
assertTrue(compiled.field().columns().values().stream()
.anyMatch(column -> column.height() > column.terrain().naturalHeight()));
for (SurfaceColumn column : compiled.field().columns().values()) {
if (column.role() == SurfaceRole.CHANNEL) {
assertTrue(Math.abs(column.z()) <= 7);
@@ -805,19 +944,21 @@ public class ErosionFieldCompilerTest {
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, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
withMaximumIncision(zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
IrisRiverBlendStyle.TERRACED, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D,
new HydrologyPlannerSettings.Excavation(64, 64, 8192))),
new HydrologyPlannerSettings.Excavation(64, 64, 8192))), 32),
shelf, 300, SurfaceTerminal.SINKHOLE, 40);
assertNull(terraced.field().rejection());
Set<Integer> steps = bankHeights(terraced, 150, 9, 40);
assertTrue(steps.toString(), steps.size() >= 2 && steps.size() <= 4);
assertHolds(terraced);
Compiled cliff = compile(
zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
withMaximumIncision(zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
IrisRiverBlendStyle.CLIFF, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D,
new HydrologyPlannerSettings.Excavation(64, 64, 8192))),
new HydrologyPlannerSettings.Excavation(64, 64, 8192))), 32),
shelf, 300, SurfaceTerminal.SINKHOLE, 40);
assertNull(cliff.field().rejection());
Set<Integer> bench = bankHeights(cliff, 150, 9, 40);
assertEquals(bench.toString(), 2, bench.size());
assertTrue(bench.contains(80));
@@ -873,7 +1014,43 @@ public class ErosionFieldCompilerTest {
assertTrue(column.height() <= hillside.sample(150, z).naturalHeight());
previous = column.height();
}
assertHolds(rising);
assertEquals(0, rising.field().uncontainedWetCells());
assertChannelContained(rising);
assertBoundedBankFill(rising, 8);
assertEquals(92, rising.field().column(150, -7).height());
assertEquals(90, rising.field().column(150, -7).terrain().naturalHeight());
}
@Test
public void zeroBankDepthBudgetLeavesLowBanksUnfilledAndUncontained() {
HydrologyPlannerSettings.Erosion erosion = new HydrologyPlannerSettings.Erosion(
true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D,
IrisRiverBedProfile.BOWL, 0D, 0D, new HydrologyPlannerSettings.Excavation(0, 16, 256));
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface(0, erosion);
HydrologyTerrainSampler lowGround = (x, z) -> HydrologyTerrainSample.openLand(77, 0D, "land");
SurfaceCenterline centerline = SurfaceCenterline.densify(
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(16, 80, 0)));
int count = centerline.size();
double[] widths = new double[count];
double[] depths = new double[count];
double[] multipliers = new double[count];
int[] heads = new int[count];
Arrays.fill(widths, 3D);
Arrays.fill(depths, 2D);
Arrays.fill(multipliers, 1D);
Arrays.fill(heads, 80);
ErosionField field = new ErosionFieldCompiler(rasterSettings(surface), lowGround).compile(42L,
centerline, new ChannelProfile(widths, depths, multipliers), ValleyProfile.fromHeads(heads, count),
SurfaceTerminal.SINKHOLE, 0);
assertEquals(HydrologyCandidateRejection.SURFACE_WATER_CONTAINMENT, field.rejection());
assertTrue(field.uncontainedWetCells() > 0);
assertEquals(77, field.column(8, 4).height());
for (SurfaceColumn column : field.columns().values()) {
if (column.role() != SurfaceRole.CHANNEL) {
assertEquals(column.terrain().naturalHeight(), column.height());
}
}
}
@Test
@@ -987,4 +1164,11 @@ public class ErosionFieldCompilerTest {
assertEquals(SurfaceRole.SHORE, bare.field().column(150, 4).role());
assertHolds(bare);
}
private static HydrologyPlannerSettings rasterSettings(HydrologyPlannerSettings.Surface surface) {
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
return new HydrologyPlannerSettings(60, defaults.routing(), surface, defaults.hydraulics(),
defaults.underground(), defaults.outlets(), defaults.geometry(), defaults.deepFluids(),
defaults.surfacePools(), 12D, defaults.seaCaves(), defaults.surfacePolicyBounds());
}
}
@@ -0,0 +1,120 @@
package art.arcane.iris.generation.hydrology.surface;
import art.arcane.iris.generation.hydrology.HydrologyCandidateRejection;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyPoint;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class RiverFirstTerrainTest {
@Test
public void riverBuildsContainedBanksAroundALowNaturalPatch() {
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(77, 0D, "land");
ErosionField field = compile(sampler, null);
assertNull(field.rejection());
assertEquals(0, field.uncontainedWetCells());
SurfaceColumn bed = field.column(0, 0);
assertNotNull(bed);
assertEquals(bed.terrain().naturalHeight(), bed.height());
assertTrue(bed.height() < bed.headY());
int raisedBanks = 0;
for (SurfaceColumn column : field.columns().values()) {
if (column.role() != SurfaceRole.CHANNEL && column.height() > column.terrain().naturalHeight()) {
raisedBanks++;
assertTrue(column.height() - column.terrain().naturalHeight() <= 8);
}
}
assertTrue(raisedBanks > 0);
}
@Test
public void riverRejectsRequiredFillBeyondTheTerrainBudget() {
ErosionField field = compile((x, z) -> HydrologyTerrainSample.openLand(69, 0D, "land"), null);
assertEquals(HydrologyCandidateRejection.SURFACE_WATER_CONTAINMENT, field.rejection());
}
@Test
public void unavailableBankCannotCertifyTheWetPerimeter() {
HydrologyTerrainSample ground = HydrologyTerrainSample.openLand(77, 0D, "land");
ErosionField complete = compile((x, z) -> ground, null);
assertNull(complete.rejection());
SurfaceColumn bank = complete.columns().values().stream()
.filter(column -> column.x() == 0 && column.role() != SurfaceRole.CHANNEL
&& complete.column(0, column.z() - 1) != null
&& complete.column(0, column.z() - 1).role() == SurfaceRole.CHANNEL)
.findFirst().orElseThrow();
ErosionField unavailable = compile((x, z) -> x == bank.x() && z == bank.z() ? null : ground, null);
assertEquals(HydrologyCandidateRejection.SURFACE_WATER_CONTAINMENT, unavailable.rejection());
assertTrue(unavailable.uncontainedWetCells() > 0);
}
@Test
public void fineChannelIncisionLimitIsCheckedBeforePublication() {
ErosionField field = compile((x, z) -> HydrologyTerrainSample.openLand(x == 0 && z == 1 ? 110 : 82, 0D, "land"), null);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, field.rejection());
}
@Test
public void channelEdgesRetainWaterWithinTheLocalIncisionLimit() {
ErosionField field = compile((x, z) -> HydrologyTerrainSample.openLand(x == 0 && z == 1 ? 89 : 82, 0D, "land"), null);
assertNull(field.rejection());
SurfaceColumn column = field.column(0, 1);
assertEquals(SurfaceRole.CHANNEL, column.role());
assertEquals(79, column.height());
assertEquals(80, column.headY());
}
@Test
public void adjacentChunkRastersRetainIdenticalRaisedTerrain() {
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(77 + Math.floorMod(x + z, 3), 0D, "land");
ErosionField complete = compile(sampler, null);
assertNull(complete.rejection());
for (SurfaceBounds bounds : List.of(new SurfaceBounds(-16, -16, -1, 15),
new SurfaceBounds(0, -16, 15, 15))) {
ErosionField bounded = compile(sampler, bounds);
assertNull(bounded.rejection());
for (SurfaceColumn column : complete.columns().values()) {
if (bounds.contains(column.x(), column.z())) {
assertEquals(column, bounded.column(column.x(), column.z()));
}
}
}
}
private static ErosionField compile(HydrologyTerrainSampler sampler, SurfaceBounds bounds) {
HydrologyPlannerSettings.Surface settings = HydrologyPlannerSettings.defaults().surface();
SurfaceCenterline centerline = SurfaceCenterline.densify(
List.of(new HydrologyPoint(-48, 80, 0), new HydrologyPoint(48, 80, 0)));
int count = centerline.size();
double[] widths = new double[count];
double[] depths = new double[count];
double[] multipliers = new double[count];
int[] heads = new int[count];
Arrays.fill(widths, 3D);
Arrays.fill(depths, 2D);
Arrays.fill(multipliers, 1D);
Arrays.fill(heads, 80);
return new ErosionFieldCompiler(rasterSettings(settings), sampler).compile(17L, centerline,
new ChannelProfile(widths, depths, multipliers), ValleyProfile.fromHeads(heads, count),
SurfaceTerminal.SINKHOLE, 0, HydrologyPlannerSettings.Ponds.none(), SurfaceRasterContext.bounded(bounds));
}
private static HydrologyPlannerSettings rasterSettings(HydrologyPlannerSettings.Surface surface) {
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
return new HydrologyPlannerSettings(60, defaults.routing(), surface, defaults.hydraulics(),
defaults.underground(), defaults.outlets(), defaults.geometry(), defaults.deepFluids(),
defaults.surfacePools(), surface.shoreWidth(), defaults.seaCaves(), defaults.surfacePolicyBounds());
}
}
@@ -0,0 +1,112 @@
package art.arcane.iris.generation.hydrology.surface;
import art.arcane.iris.generation.hydrology.HydraulicSegment;
import art.arcane.iris.generation.hydrology.HydrologyCandidateRejection;
import art.arcane.iris.generation.hydrology.HydrologyGeometrySampler;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyPoint;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class RiverProfileSolverTest {
private static final HydrologyGeometrySampler GEOMETRY = request -> switch (request.field()) {
case SURFACE_WIDTH -> 6;
case SURFACE_DEPTH -> 2;
default -> request.minimum();
};
@Test
public void detailedTerrainDoesNotReplacePlannedRiverElevations() {
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(96, 80, 0));
for (int natural : List.of(78, 83, 84)) {
SurfaceCourseResult result = build(path, (x, z) -> HydrologyTerrainSample.openLand(natural, 0D, "land"));
assertNull(result.rejection());
assertEquals(80, result.lastHead());
for (HydraulicSegment segment : result.segments()) {
assertEquals(80, segment.upstreamHeadY());
assertEquals(80, segment.downstreamHeadY());
}
}
}
@Test
public void interpolatedSharedHeadsRemainNonRisingAcrossStations() {
List<HydrologyPoint> path = List.of(new HydrologyPoint(-96, 88, -16),
new HydrologyPoint(0, 80, 0), new HydrologyPoint(96, 72, 16));
SurfaceCourseResult result = build(path,
(x, z) -> HydrologyTerrainSample.openLand(82 - Math.floorDiv(x, 12), 0D, "land"));
assertNull(result.rejection());
int previous = Integer.MAX_VALUE;
for (HydraulicSegment segment : result.segments()) {
for (HydrologyPoint point : segment.centerline()) {
assertTrue(point.y() <= previous);
previous = point.y();
}
}
assertEquals(72, result.lastHead());
}
@Test
public void plannedProfileChecksTheCenterWithoutSearchingBankRings() {
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(96, 80, 0));
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
double[] widths = new double[centerline.size()];
double[] depths = new double[centerline.size()];
double[] banks = new double[centerline.size()];
Arrays.fill(widths, 3D);
Arrays.fill(depths, 2D);
Arrays.fill(banks, 1D);
AtomicInteger samples = new AtomicInteger();
HydrologyTerrainSampler sampler = (x, z) -> {
assertEquals(0, z);
samples.incrementAndGet();
return HydrologyTerrainSample.openLand(82, 0D, "land");
};
ValleyProfile profile = new RiverProfileSolver(
new RiverProfileSolver.Options(HydrologyPlannerSettings.defaults().surface(), 60), sampler)
.solve(path, centerline, new ChannelProfile(widths, depths, banks), SurfaceTerminal.SINKHOLE, 40, 32);
assertNull(profile.rejection());
assertEquals(centerline.size(), samples.get());
}
@Test
public void aFineCliffKeepsTheSourceHeadAndRaisesOnlyTheRequiredApproach() {
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(x < 32 ? 102 : 86, 0D, "land");
SurfaceCourseResult result = build(List.of(new HydrologyPoint(0, 100, 0),
new HydrologyPoint(64, 84, 0)), sampler);
assertNull(result.rejection());
assertEquals(100, result.segments().getFirst().upstreamHeadY());
assertEquals(84, result.lastHead());
int previous = 100;
for (HydraulicSegment segment : result.segments()) {
for (HydrologyPoint point : segment.centerline()) {
assertTrue(point.y() <= previous);
assertTrue(sampler.sample(point.x(), point.z()).naturalHeight() - point.y() + 2
<= HydrologyPlannerSettings.defaults().surface().maximumIncision());
previous = point.y();
}
}
}
@Test
public void anUncuttableFineTerrainRidgeStillRejectsTheCourse() {
SurfaceCourseResult result = build(List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(96, 80, 0)),
(x, z) -> HydrologyTerrainSample.openLand(x == 48 ? 110 : 82, 0D, "land"));
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, result.rejection());
}
private static SurfaceCourseResult build(List<HydrologyPoint> path, HydrologyTerrainSampler sampler) {
return new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, GEOMETRY, 60)
.build(7L, 19L, "default", path, SurfaceTerminal.SINKHOLE, 40, 32);
}
}
@@ -180,7 +180,7 @@ public class SurfaceBendProfileTest {
ValleyProfile valley = new ValleyProfileSolver(surface, FLAT, 60, 32)
.solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
assertTrue(valley.accepted());
return new ErosionFieldCompiler(surface, FLAT, 60).compile(42L, centerline, channel, valley,
return new ErosionFieldCompiler(rasterSettings(surface), FLAT).compile(42L, centerline, channel, valley,
SurfaceTerminal.SINKHOLE, 8, HydrologyPlannerSettings.Ponds.none(), context);
}
@@ -191,4 +191,11 @@ public class SurfaceBendProfileTest {
HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true,
HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.none()));
}
private static HydrologyPlannerSettings rasterSettings(HydrologyPlannerSettings.Surface surface) {
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
return new HydrologyPlannerSettings(60, defaults.routing(), surface, defaults.hydraulics(),
defaults.underground(), defaults.outlets(), defaults.geometry(), defaults.deepFluids(),
defaults.surfacePools(), surface.shoreWidth(), defaults.seaCaves(), defaults.surfacePolicyBounds());
}
}
@@ -13,6 +13,7 @@ import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -34,7 +35,8 @@ public class SurfaceCourseBuilderTest {
7L,
COURSE_ID,
"water",
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(140, 0, 0), new HydrologyPoint(280, 0, 0)),
List.of(new HydrologyPoint(0, 110, 0), new HydrologyPoint(140, 87, 0),
new HydrologyPoint(260, 67, 0), new HydrologyPoint(280, SEA_LEVEL, 0)),
SurfaceTerminal.OCEAN_MOUTH,
SEA_LEVEL,
64
@@ -72,7 +74,7 @@ public class SurfaceCourseBuilderTest {
7L,
COURSE_ID,
"water",
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(200, 80, 0)),
SurfaceTerminal.SINKHOLE,
40,
64
@@ -85,6 +87,41 @@ public class SurfaceCourseBuilderTest {
assertEquals(201, result.segments().getFirst().centerline().size());
}
@Test
public void coastalDropKeepsTheUpstreamGradedReachExposed() {
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(),
defaults.banks().withInlet(HydrologyPlannerSettings.Inlet.none()));
HydrologyTerrainSampler sampler = (x, z) -> x >= 40
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(100 - x, 0D, "land");
SurfaceCourseBuilder builder = new SurfaceCourseBuilder(surface, sampler, CONSTANT_GEOMETRY, SEA_LEVEL);
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(40, SEA_LEVEL, 0));
SurfaceCourseResult result = builder.build(7L, COURSE_ID, "water", path,
SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL, 16);
assertNull(result.rejection());
assertEquals(2, result.segments().size());
HydraulicSegment approach = result.segments().getFirst();
HydraulicSegment drop = result.segments().getLast();
assertEquals(HydrologyFeatureType.CASCADE, approach.type());
assertFalse(approach.fallingFluid());
assertEquals(100, approach.upstreamHeadY());
assertEquals(61, approach.downstreamHeadY());
assertEquals(new HydrologyPoint(20, 80, 0), approach.centerline().get(20));
assertEquals(HydrologyFeatureType.RIFFLE, drop.type());
assertTrue(drop.fallingFluid());
assertTrue(drop.receivingPool());
assertEquals(List.of(new HydrologyPoint(39, 61, 0), new HydrologyPoint(40, 60, 0)), drop.centerline());
assertEquals(approach.channelProfile().widthAt(39), drop.channelProfile().widthAt(0), 0D);
assertEquals(approach.channelProfile().depthAt(39), drop.channelProfile().depthAt(0), 0D);
assertTrue(approach.id() != drop.id());
assertEquals(result, builder.build(7L, COURSE_ID, "water", path,
SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL, 16));
}
@Test
public void cliffProducesAWaterfallSegment() {
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(x < 100 ? 100 : 90, 0D, "land");
@@ -92,7 +129,8 @@ public class SurfaceCourseBuilderTest {
7L,
COURSE_ID,
"water",
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 0),
new HydrologyPoint(100, 90, 0), new HydrologyPoint(200, 90, 0)),
SurfaceTerminal.SINKHOLE,
40,
64
@@ -106,14 +144,32 @@ public class SurfaceCourseBuilderTest {
assertEquals(HydrologyFeatureType.SURFACE_POOL, result.segments().get(2).type());
}
@Test
public void cliffIntoTheOceanUsesFallingFluidInsteadOfAnInlandSeaLevelChannel() {
HydrologyTerrainSampler sampler = (x, z) -> x >= 100
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(100, 0D, "land");
SurfaceCourseResult result = builder(sampler).build(7L, COURSE_ID, "water",
List.of(new HydrologyPoint(0, 97, 0), new HydrologyPoint(99, 97, 0),
new HydrologyPoint(100, SEA_LEVEL, 0)), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL, 64);
assertNull(result.rejection());
HydraulicSegment fall = result.segments().getLast();
assertEquals(HydrologyFeatureType.WATERFALL, fall.type());
assertTrue(fall.fallingFluid());
assertTrue(fall.receivingPool());
assertEquals(SEA_LEVEL, fall.downstreamHeadY());
assertEquals(100, fall.end().x());
}
@Test
public void gentleSlopeProducesRifflesAndSteepSlopeProducesCascades() {
HydrologyTerrainSampler gentle = (int x, int z) -> HydrologyTerrainSample.openLand(120 - x / 10, 0D, "land");
SurfaceCourseResult gentleResult = builder(gentle).build(7L, COURSE_ID, "water",
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)), SurfaceTerminal.SINKHOLE, 40, 64);
List.of(new HydrologyPoint(0, 120, 0), new HydrologyPoint(200, 100, 0)), SurfaceTerminal.SINKHOLE, 40, 64);
HydrologyTerrainSampler steep = (int x, int z) -> HydrologyTerrainSample.openLand(300 - x, 0D, "land");
SurfaceCourseResult steepResult = builder(steep).build(7L, COURSE_ID, "water",
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)), SurfaceTerminal.SINKHOLE, 40, 64);
List.of(new HydrologyPoint(0, 300, 0), new HydrologyPoint(200, 100, 0)), SurfaceTerminal.SINKHOLE, 40, 64);
assertNull(gentleResult.rejection());
assertNull("detail=" + steepResult.rejectionDetail(), steepResult.rejection());
@@ -138,7 +194,7 @@ public class SurfaceCourseBuilderTest {
7L,
COURSE_ID,
"water",
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
List.of(new HydrologyPoint(0, 50, 0), new HydrologyPoint(200, 50, 0)),
SurfaceTerminal.OCEAN_MOUTH,
SEA_LEVEL,
64
@@ -153,7 +209,8 @@ public class SurfaceCourseBuilderTest {
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 240
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(100 - x / 6, 0D, "land");
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(241, 0, 0));
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(239, 61, 0),
new HydrologyPoint(240, SEA_LEVEL, 0), new HydrologyPoint(241, SEA_LEVEL, 0));
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
SurfaceCourseResult result = builder(sampler).build(7L, COURSE_ID, "water", path, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL, 64);
@@ -2,6 +2,7 @@ package art.arcane.iris.generation.hydrology.surface;
import art.arcane.iris.generation.hydrology.HydraulicChannelProfile;
import art.arcane.iris.generation.hydrology.HydraulicSegment;
import art.arcane.iris.generation.hydrology.HydrologyCandidateRejection;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
@@ -131,6 +132,33 @@ public class SurfaceFootprintCompilerTest {
assertNull(column(footprint, 160, 12));
}
@Test
public void plannedCoastalWaterfallRetainsAConnectedOceanReceiverWithoutCuttingTheCliffToSeaLevel() {
int seaLevel = HydrologyPlannerSettings.defaults().seaLevel();
HydrologyTerrainSampler sampler = (x, z) -> x >= 100
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(100, 0D, "land");
SurfaceCourseResult result = new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(),
sampler, CONSTANT_GEOMETRY, seaLevel).build(7L, 1L, "water",
List.of(new HydrologyPoint(0, 97, 0), new HydrologyPoint(99, 97, 0),
new HydrologyPoint(100, seaLevel, 0)), SurfaceTerminal.OCEAN_MOUTH, seaLevel, 64);
assertNull(result.rejection());
ArrayList<HydraulicSegment> segments = new ArrayList<>(result.segments());
segments.add(segment(77L, HydrologyFeatureType.MOUTH, seaLevel, seaLevel,
List.of(result.pathEnd())));
SurfaceFootprint footprint = compiler(sampler).compile(course(segments));
assertNull(footprint.rejection());
assertEquals(0, footprint.uncontainedWetCells());
assertTrue(footprint.columns().stream().anyMatch(column -> column.layer().channel()
&& column.layer().fluidOwned() && column.layer().fluidHeadY() == 97));
for (SurfaceLayerColumn column : footprint.columns()) {
if (!column.apron() && column.role() == SurfaceRole.CHANNEL) {
assertTrue(column.layer().bedY() >= 90);
}
}
}
@Test
public void mouthOverTheOceanPublishesOnlyANonOwningApron() {
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 100
@@ -212,13 +240,101 @@ public class SurfaceFootprintCompilerTest {
}
}
@Test
public void inletAllowanceSurvivesAFallWithoutReachingOrdinaryUpstreamTerrain() {
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Banks banks = defaults.surface().banks();
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
HydrologyPlannerSettings.Erosion expandedVolume = new HydrologyPlannerSettings.Erosion(
erosion.enabled(), erosion.smoothingRadius(), erosion.thalwegFraction(), erosion.blendCurve(),
erosion.bedNoise(), erosion.style(), erosion.terraceSteps(), erosion.cliffFraction(), erosion.bedProfile(),
erosion.shoreRise(), erosion.blendBaseWidth(), new HydrologyPlannerSettings.Excavation(8, 16, 512));
HydrologyPlannerSettings.Banks expandedBanks = 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(), expandedVolume,
banks.ponds(), banks.channel(), banks.flow());
HydrologyPlannerSettings.Surface originalSurface = defaults.surface();
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
originalSurface.enabled(), originalSurface.sources(), originalSurface.minimumWidth(), originalSurface.maximumWidth(),
originalSurface.minimumDepth(), originalSurface.maximumDepth(), originalSurface.maximumIncision(),
originalSurface.shoreWidth(), expandedBanks);
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(defaults.seaLevel(), defaults.routing(), surface,
defaults.hydraulics(), defaults.underground(), defaults.outlets(), defaults.geometry(), defaults.deepFluids(),
defaults.surfacePools(), defaults.widestShoreBiomeWidth(), defaults.seaCaves(), defaults.surfacePolicyBounds());
int seaLevel = settings.seaLevel();
int upstreamHead = seaLevel + 22;
int downstreamHead = seaLevel + 12;
HydrologyTerrainSampler sampler = (x, z) -> x >= 200
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(x >= 120 && x <= 135 ? 100 : x < 151 ? 90 : 80, 0D, "land");
HydraulicSegment fall = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
upstreamHead, downstreamHead, 6, 3, true, true,
List.of(new HydrologyPoint(150, upstreamHead, 0), new HydrologyPoint(151, downstreamHead, 0)),
HydraulicChannelProfile.uniform(6, 3));
ArrayList<HydrologyPoint> ramp = new ArrayList<>();
for (int x = 180; x <= 192; x++) {
ramp.add(new HydrologyPoint(x, downstreamHead - (x - 180), 0));
}
RiverCourse course = course(List.of(
segment(1L, HydrologyFeatureType.SURFACE_POOL, upstreamHead, upstreamHead, points(0, 149, upstreamHead)),
fall,
segment(3L, HydrologyFeatureType.SURFACE_POOL, downstreamHead, downstreamHead, points(151, 180, downstreamHead)),
segment(4L, HydrologyFeatureType.CASCADE, downstreamHead, seaLevel, ramp),
segment(5L, HydrologyFeatureType.SURFACE_POOL, seaLevel, seaLevel, points(192, 199, seaLevel)),
segment(6L, HydrologyFeatureType.MOUTH, seaLevel, seaLevel, points(199, 201, seaLevel))));
SurfaceFootprintCompiler compiler = new SurfaceFootprintCompiler(settings, sampler, CONSTANT_GEOMETRY);
SurfaceFootprint full = compiler.compile(course);
assertNull("detail=" + full.rejectionDetail(), full.rejection());
assertEquals(0, full.uncontainedWetCells());
for (int x : new int[]{130, 195}) {
SurfaceLayerColumn channel = column(full, x, 0);
assertNotNull(channel);
assertTrue(channel.layer().channel());
assertEquals(x == 130 ? upstreamHead : seaLevel, channel.layer().fluidHeadY());
int cut = channel.terrain().naturalHeight() - channel.layer().bedY();
assertTrue(cut > settings.surface().maximumIncision());
assertTrue(cut <= settings.surface().banks().inlet().maximumIncision());
}
for (SurfaceBounds bounds : List.of(new SurfaceBounds(112, -8, 159, 8), new SurfaceBounds(144, -8, 199, 8))) {
SurfaceFootprint bounded = compiler.compile(course, bounds);
assertNull(bounded.rejection());
assertEquals(full.columns().stream().filter(value -> bounds.contains(value.x(), value.z())).toList(),
bounded.columns());
}
HydrologyTerrainSampler upstreamRidge = (x, z) -> x == 60
? HydrologyTerrainSample.openLand(100, 0D, "land") : sampler.sample(x, z);
SurfaceFootprint rejected = new SurfaceFootprintCompiler(settings, upstreamRidge, CONSTANT_GEOMETRY).compile(course);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, rejected.rejection());
}
@Test
public void standaloneMouthKeepsItsInletBudgetWithinTheDryCourseFraction() {
int seaLevel = HydrologyPlannerSettings.defaults().seaLevel();
for (int coast : new int[]{8, 100}) {
int ridgeX = Math.max(1, coast / 10);
HydrologyTerrainSampler sampler = (x, z) -> x >= coast
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(x == ridgeX ? 85 : 70, 0D, "land");
RiverCourse mouth = course(List.of(segment(1L, HydrologyFeatureType.MOUTH,
seaLevel, seaLevel, points(0, coast + 1, seaLevel))));
SurfaceFootprint rejected = compiler(sampler).compile(mouth);
assertEquals("coast=" + coast, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
rejected.rejection());
}
}
@Test
public void aContainedFallRetainsBothExposedRunsAndWholeCourseStationIndices() {
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(x < 100 ? 100 : 90, 0D, "land");
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
SurfaceTerminal.TRIBUTARY, 40, 64);
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 0),
new HydrologyPoint(100, 90, 0), new HydrologyPoint(200, 90, 0)),
SurfaceTerminal.TRIBUTARY, 90, 64);
assertNull(built.rejection());
RiverCourse course = course(built.segments());
HydraulicSegment fall = course.segments().stream().filter(HydraulicSegment::fallingFluid).findFirst().orElseThrow();
@@ -246,7 +362,13 @@ public class SurfaceFootprintCompilerTest {
assertEquals(full.columns().stream().filter(value -> bounds.contains(value.x(), value.z())).toList(), bounded.columns());
for (SurfaceLayerColumn value : full.columns()) {
if (value.layer().terrainOwned()) {
assertTrue(value.layer().bedY() <= value.terrain().naturalHeight());
int fill = value.layer().bedY() - value.terrain().naturalHeight();
if (value.layer().channel()) {
assertTrue(fill <= 0);
} else {
assertTrue(fill <= settings.surface().maximumIncision());
assertTrue(fill <= settings.surface().banks().erosion().excavation().maximumDepth());
}
}
}
}
@@ -256,8 +378,8 @@ public class SurfaceFootprintCompilerTest {
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(300 - x, 0D, "land");
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
SurfaceTerminal.TRIBUTARY, 40, 64);
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 300, 0), new HydrologyPoint(200, 100, 0)),
SurfaceTerminal.TRIBUTARY, 100, 64);
assertNull("detail=" + built.rejectionDetail(), built.rejection());
SurfaceFootprint footprint = compiler(sampler).compile(course(built.segments()));
assertNull("detail=" + footprint.rejectionDetail(), footprint.rejection());
@@ -276,8 +398,9 @@ public class SurfaceFootprintCompilerTest {
x + z < 200 ? 100 : 86, 0D, "land");
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 200)),
SurfaceTerminal.TRIBUTARY, 40, 64);
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 99),
new HydrologyPoint(100, 86, 100), new HydrologyPoint(200, 86, 200)),
SurfaceTerminal.TRIBUTARY, 86, 64);
assertNull(built.rejection());
RiverCourse course = course(built.segments());
assertTrue(course.segments().stream().anyMatch(segment -> segment.fallingFluid() && segment.drop() == 14));
@@ -311,8 +434,9 @@ public class SurfaceFootprintCompilerTest {
: HydrologyTerrainSample.openLand((x < 100 ? 100 : 90) + Math.abs(z) / 3, 0D, "land");
SurfaceCourseResult built = new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(), sampler,
CONSTANT_GEOMETRY, SEA_LEVEL).build(7L, 1L, "water",
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(180, 0, 0)),
SurfaceTerminal.TRIBUTARY, 40, 64);
List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 0),
new HydrologyPoint(100, 90, 0), new HydrologyPoint(180, 90, 0)),
SurfaceTerminal.TRIBUTARY, 90, 64);
assertNull(built.rejection());
RiverCourse falling = course(built.segments());
RiverCourse mouth = course(List.of(segment(1L, HydrologyFeatureType.MOUTH, SEA_LEVEL, SEA_LEVEL,
@@ -186,7 +186,7 @@ public class ValleyProfileSolverTest {
assertNull(valley.rejection());
assertEquals(100, valley.head()[130]);
assertEquals(98, valley.head()[149]);
ErosionField field = new ErosionFieldCompiler(HydrologyPlannerSettings.defaults().surface(), terrain, SEA_LEVEL)
ErosionField field = new ErosionFieldCompiler(rasterSettings(HydrologyPlannerSettings.defaults().surface()), terrain)
.compile(77L, centerline, channel, valley, SurfaceTerminal.TRIBUTARY, 0,
HydrologyPlannerSettings.Ponds.none());
assertEquals(0, field.uncontainedWetCells());
@@ -214,7 +214,7 @@ public class ValleyProfileSolverTest {
assertEquals(120, valley.head()[110]);
assertEquals(118, valley.head()[128]);
assertContained(valley);
ErosionField field = new ErosionFieldCompiler(surface, terrain, SEA_LEVEL)
ErosionField field = new ErosionFieldCompiler(rasterSettings(surface), terrain)
.compile(77L, centerline, channel, valley, SurfaceTerminal.TRIBUTARY, 0,
HydrologyPlannerSettings.Ponds.none());
assertEquals(0, field.uncontainedWetCells());
@@ -231,7 +231,7 @@ public class ValleyProfileSolverTest {
assertNull(valley.rejection());
assertTrue(valley.exposedStations() < 240);
assertEquals(92, valley.head()[valley.exposedStations() - 1]);
ErosionField field = new ErosionFieldCompiler(HydrologyPlannerSettings.defaults().surface(), terrain, SEA_LEVEL)
ErosionField field = new ErosionFieldCompiler(rasterSettings(HydrologyPlannerSettings.defaults().surface()), terrain)
.compile(77L, centerline, channel, valley, SurfaceTerminal.COASTAL_GROTTO, 0,
HydrologyPlannerSettings.Ponds.none());
assertEquals(0, field.uncontainedWetCells());
@@ -492,4 +492,11 @@ public class ValleyProfileSolverTest {
private static List<HydrologyPoint> path(int stations) {
return List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(stations - 1, 0, 0));
}
private static HydrologyPlannerSettings rasterSettings(HydrologyPlannerSettings.Surface surface) {
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
return new HydrologyPlannerSettings(60, defaults.routing(), surface, defaults.hydraulics(),
defaults.underground(), defaults.outlets(), defaults.geometry(), defaults.deepFluids(),
defaults.surfacePools(), surface.shoreWidth(), defaults.seaCaves(), defaults.surfacePolicyBounds());
}
}
@@ -261,7 +261,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
@Test
@SuppressWarnings("unchecked")
public void translatedTreeSchedulesDistantOwnerChunks() throws Exception {
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() throws Exception {
File objectFile = temporaryFolder.newFile("tree.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
@@ -293,8 +293,8 @@ public class MantleObjectComponentBoundaryRadiusTest {
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
assertEquals(33, component.getRadius());
assertEquals(33, component.getOutputRadius());
assertEquals(0, component.getInputRadius());
assertEquals(0, component.getOutputRadius());
assertEquals(82, component.getInputRadius());
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
@@ -322,9 +322,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
assertEquals(1, directOrigins.get());
assertEquals(49, directOrigins.get());
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
assertEquals(2, directOrigins.get());
assertEquals(49, directOrigins.get());
List<String> generatedChunks = new ArrayList<>();
doAnswer(invocation -> {
@@ -342,7 +342,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
));
generator.generateMatter(0, 0, false, mock(ChunkContext.class));
assertEquals(49, generatedChunks.size());
assertEquals(List.of("0,0"), generatedChunks);
List<String> replayedSources = new ArrayList<>();
MantleObjectComponent.replaySourceChunks(
@@ -0,0 +1,207 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MantleObjectComponentPlacementDeterminismTest {
private static final Position LEFT = new Position(15, 10, 0);
private static final Position RIGHT = new Position(16, 10, 0);
private static final Position SUPPORT = new Position(15, 11, 0);
@Test
public void ordinaryObjectsKeepPrerequisitesAndSameSourceReadsAcrossDestinationOrders() throws Exception {
Fixture forward = new Fixture();
forward.generate(List.of(0, 1));
Fixture reverse = new Fixture();
reverse.generate(List.of(1, 0));
Fixture parallel = new Fixture();
parallel.generateParallel();
Map<Position, String> expected = Map.of(
LEFT, "minecraft:mangrove_wood",
RIGHT, "minecraft:mangrove_wood",
SUPPORT, "minecraft:mangrove_wood"
);
assertEquals(expected, forward.snapshot());
assertEquals(expected, reverse.snapshot());
assertEquals(expected, parallel.snapshot());
assertEquals(12, parallel.sourceCalls.size());
for (Integer calls : parallel.sourceCalls.values()) {
assertEquals(1, calls.intValue());
}
}
@Test
public void ordinaryObjectSourcePlansRemainReusableAfterNeighborPublicationAndHotload() {
Fixture fixture = new Fixture();
fixture.generate(List.of(1, 0));
Map<Position, String> expected = fixture.snapshot();
Map<Chunk, Integer> calls = Map.copyOf(fixture.sourceCalls);
fixture.generate(List.of(0, 1));
assertEquals(expected, fixture.snapshot());
assertEquals(calls, fixture.sourceCalls);
fixture.component.hotload();
fixture.generate(List.of(0, 1));
assertEquals(expected, fixture.snapshot());
for (Map.Entry<Chunk, Integer> entry : fixture.sourceCalls.entrySet()) {
assertEquals(2, entry.getValue().intValue());
}
}
private static final class Fixture {
private final NativeBlockState stone = state("minecraft:stone");
private final NativeBlockState oak = state("minecraft:oak_wood");
private final NativeBlockState mangrove = state("minecraft:mangrove_wood");
private final NativeBlockState wrongRead = state("minecraft:dirt");
private final Map<Position, NativeBlockState> published = new ConcurrentHashMap<>();
private final Map<Chunk, Integer> sourceCalls = new ConcurrentHashMap<>();
private final ThreadLocal<Chunk> destination = new ThreadLocal<>();
private final MantleWriter writer;
private final MantleObjectComponent component;
private final ChunkContext context = mock(ChunkContext.class);
@SuppressWarnings("unchecked")
private Fixture() {
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllRegions(any())).thenReturn(new KList<>());
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getEngine()).thenReturn(engine);
Mantle<Matter> mantle = mock(Mantle.class);
when(mantle.getWorldHeight()).thenReturn(64);
writer = mock(MantleWriter.class);
when(writer.getMantle()).thenReturn(mantle);
when(writer.getEngine()).thenReturn(engine);
when(writer.getPrerequisiteBlock(anyInt(), anyInt(), anyInt())).thenReturn(stone);
when(writer.get(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> published.getOrDefault(
new Position(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)),
stone));
doAnswer(invocation -> {
destination.set(new Chunk(invocation.getArgument(0), invocation.getArgument(1)));
try {
Runnable task = invocation.getArgument(2);
task.run();
} finally {
destination.remove();
}
return null;
}).when(writer).withChunkFence(anyInt(), anyInt(), any(Runnable.class));
doAnswer(invocation -> {
Position position = new Position(
invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2));
NativeBlockState value = invocation.getArgument(3);
assertEquals(new Chunk(position.x() >> 4, position.z() >> 4), destination.get());
published.put(position, value);
return null;
}).when(writer).setData(anyInt(), anyInt(), anyInt(), any(NativeBlockState.class));
component = new OrdinaryObjects(engineMantle, this);
}
private void generate(List<Integer> destinations) {
for (Integer chunkX : destinations) {
component.generateLayer(writer, chunkX, 0, context);
}
}
private void generateParallel() throws Exception {
CountDownLatch ready = new CountDownLatch(2);
CountDownLatch start = new CountDownLatch(1);
try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
Future<?> left = executor.submit(() -> generateAfterBarrier(0, ready, start));
Future<?> right = executor.submit(() -> generateAfterBarrier(1, ready, start));
assertTrue(ready.await(5L, TimeUnit.SECONDS));
start.countDown();
left.get(5L, TimeUnit.SECONDS);
right.get(5L, TimeUnit.SECONDS);
}
}
private void generateAfterBarrier(int chunkX, CountDownLatch ready, CountDownLatch start) {
ready.countDown();
try {
assertTrue(start.await(5L, TimeUnit.SECONDS));
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException(failure);
}
component.generateLayer(writer, chunkX, 0, context);
}
private Map<Position, String> snapshot() {
Map<Position, String> result = new HashMap<>();
for (Map.Entry<Position, NativeBlockState> entry : published.entrySet()) {
result.put(entry.getKey(), entry.getValue().key());
}
return result;
}
private static NativeBlockState state(String key) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
return state;
}
}
private static final class OrdinaryObjects extends MantleObjectComponent {
private final Fixture fixture;
private OrdinaryObjects(EngineMantle engineMantle, Fixture fixture) {
super(engineMantle);
this.fixture = fixture;
}
@Override
protected int computeRadius() {
return 16;
}
@Override
void generateOrigin(ObjectPassPlacer placer, int chunkX, int chunkZ, ChunkContext context) {
fixture.sourceCalls.merge(new Chunk(chunkX, chunkZ), 1, Integer::sum);
if (chunkZ != 0 || chunkX < 0 || chunkX > 1) {
return;
}
NativeBlockState prerequisite = placer.get(LEFT.x(), LEFT.y(), LEFT.z());
NativeBlockState wood = chunkX == 0 ? fixture.oak : fixture.mangrove;
placer.set(SUPPORT.x(), SUPPORT.y(), SUPPORT.z(), wood);
boolean sameSourceSupport = placer.get(SUPPORT.x(), SUPPORT.y(), SUPPORT.z()) == wood;
NativeBlockState placed = prerequisite == fixture.stone && sameSourceSupport ? wood : fixture.wrongRead;
placer.set(LEFT.x(), LEFT.y(), LEFT.z(), placed);
placer.set(RIGHT.x(), RIGHT.y(), RIGHT.z(), placed);
}
}
private record Position(int x, int y, int z) {
}
private record Chunk(int x, int z) {
}
}
@@ -93,6 +93,35 @@ public class ObjectDestinationTransactionTest {
verify(writer).setData(1, 4, 0, source);
}
@Test
public void applyingSourcePlanSkipsForeignChunksBeforeOverlayingAndPreservesLocalOrder() {
MantleWriter writer = writer();
Marker foreign = new Marker("foreign");
Marker prerequisite = new Marker("prerequisite");
Marker first = new Marker("first");
Marker second = new Marker("second");
when(writer.getPrerequisiteDataIfPresent(0, 4, 0, Marker.class)).thenReturn(prerequisite);
ObjectDestinationTransaction source = new ObjectDestinationTransaction(writer, 0, 0);
source.setData(-1, 4, 0, first);
source.setData(0, 4, 0, foreign);
source.setData(-1, 4, 0, second);
source.setData(-1, 4, 16, foreign);
ObjectDestinationTransaction destination = new ObjectDestinationTransaction(writer, -1, 0);
destination.apply(source.sourcePlanSince(0));
assertEquals(2, destination.mutationCheckpoint());
assertSame(second, destination.getDataIfPresent(-1, 4, 0, Marker.class));
assertSame(prerequisite, destination.getDataIfPresent(0, 4, 0, Marker.class));
assertNull(destination.getDataIfPresent(-1, 4, 16, Marker.class));
destination.commit();
InOrder order = inOrder(writer);
order.verify(writer).setData(-1, 4, 0, first);
order.verify(writer).setData(-1, 4, 0, second);
verify(writer, never()).setData(0, 4, 0, foreign);
verify(writer, never()).setData(-1, 4, 16, foreign);
}
@Test
public void sourcePlanRejectsAnInvalidCheckpoint() {
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer(), 0, 0);
@@ -3,11 +3,15 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.noise.IrisInterpolator;
import art.arcane.iris.generation.stream.CachedStream2D;
import art.arcane.iris.spi.IrisServices;
import art.arcane.volmlib.util.interpolation.InterpolationMethod;
import art.arcane.volmlib.util.interpolation.NoiseBounds;
import art.arcane.volmlib.util.interpolation.NoiseBoundsProvider;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
@@ -15,6 +19,8 @@ import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
@@ -23,8 +29,69 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.mockingDetails;
public class IrisComplexBoundsBiomeCacheTest {
@Test
public void cachedBiomeColumnsReuseFractionalSamplesWithSignedParity() throws Exception {
IrisBiome first = new IrisBiome();
IrisBiome second = new IrisBiome();
ProceduralStream<IrisBiome> source = ProceduralStream.of(
(x, z) -> x.intValue() < 0 || z.intValue() < 0 ? first : second,
Interpolated.of(value -> 0D, value -> first));
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
services.when(() -> IrisServices.get(PreservationRegistry.class))
.thenReturn(mock(PreservationRegistry.class));
CachedStream2D<IrisBiome> stream = spy(new CachedStream2D<>("bounds", mock(Engine.class), source, 16));
Fixture fixture = new Fixture(stream);
IrisInterpolator interpolator = new IrisInterpolator()
.setFunction(InterpolationMethod.STARCAST_12).setHorizontalScale(1.25D);
fixture.setBounds(interpolator, first, -15.25D, 63.5D);
fixture.setBounds(interpolator, second, 7.75D, 127.25D);
AtomicInteger requestedSamples = new AtomicInteger();
Set<Long> columns = new HashSet<>();
for (int z = -2; z <= 2; z++) {
for (int x = -2; x <= 2; x++) {
NoiseBounds expected = interpolator.interpolateBounds(x, z, (xx, zz) -> {
requestedSamples.incrementAndGet();
columns.add(((long) (int) xx << 32) | ((int) zz & 0xffffffffL));
return (int) xx < 0 || (int) zz < 0
? new NoiseBounds(-15.25D, 63.5D) : new NoiseBounds(7.75D, 127.25D);
});
assertBits(expected, fixture.sample(interpolator, x, z));
}
}
long reads = mockingDetails(stream).getInvocations().stream()
.filter(invocation -> invocation.getMethod().getName().equals("get"))
.count();
assertTrue(reads <= columns.size() * 2L);
assertTrue(reads < requestedSamples.get() / 4L);
}
}
@Test
public void uncachedBiomeStreamsPreserveFractionalSamplesWithinOneColumn() throws Exception {
IrisBiome first = new IrisBiome();
IrisBiome second = new IrisBiome();
ProceduralStream<IrisBiome> source = ProceduralStream.of(
(x, z) -> x < -0.5D || z > 0.5D ? first : second,
Interpolated.of(value -> 0D, value -> first));
Fixture fixture = new Fixture(source);
IrisInterpolator sampling = new IrisInterpolator() {
@Override
public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) {
return provider.noise(x, z);
}
};
fixture.setBounds(sampling, first, -10D, 20D);
fixture.setBounds(sampling, second, 50D, 80D);
assertBits(new NoiseBounds(-10D, 20D), fixture.sample(sampling, -0.75D, 0.25D));
assertBits(new NoiseBounds(50D, 80D), fixture.sample(sampling, -0.25D, 0.25D));
assertBits(new NoiseBounds(-10D, 20D), fixture.sample(sampling, -0.25D, 0.75D));
}
@Test
public void adjacentPassesReuseSamplesWithoutChangingInterpolationBits() throws Exception {
IrisBiome first = new IrisBiome();
@@ -841,7 +841,9 @@ public class IrisEngineGenerationRuntimeScopeTest {
private static IrisBiome carvingBiome(IrisEngine engine, IrisDimensionCarvingResolver.State state) {
IrisDimensionCarvingEntry root = IrisDimensionCarvingResolver.resolveRootEntry(engine, 80, state);
IrisDimensionCarvingEntry child = IrisDimensionCarvingResolver.resolveFromRoot(engine, root, 19, -3, state);
return IrisDimensionCarvingResolver.resolveEntryBiome(engine, child, state);
IrisBiome expected = IrisDimensionCarvingResolver.resolveEntryBiome(engine, child, state);
assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(engine, 19, 80, -3, state));
return expected;
}
private static GenerationHistoryRuntimeRouter historyRouter(IrisEngine engine) {
@@ -0,0 +1,171 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.world.history.GenerationKernelRegistry;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import org.junit.Test;
import org.junit.Rule;
import org.junit.ClassRule;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import art.arcane.iris.pack.PackValidationCache;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.studio.StudioMode;
import static org.junit.Assert.assertNotNull;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.mockito.Mockito.mockStatic;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class PreparedHydrologyCacheIdentityTest {
@ClassRule
public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform();
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void frozenStudioAndProductionCopiesShareUntilPackBytesChange() throws Exception {
Path studio = folder.newFolder("studio").toPath();
Path production = folder.newFolder("production").toPath();
Files.writeString(studio.resolve("terrain.json"), "{\"height\":80}");
Files.copy(studio.resolve("terrain.json"), production.resolve("terrain.json"));
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
try (MockedStatic<PackValidationCache> validation = mockStatic(PackValidationCache.class)) {
validation.when(PackValidationCache::contextFingerprint).thenReturn("platform");
PreparedHydrologyCacheIdentity prepared = PreparedHydrologyCacheIdentity.capture(target(studio), kernel, null, true);
assertEquals(prepared, PreparedHydrologyCacheIdentity.capture(target(production), kernel, null, false));
Files.writeString(production.resolve("terrain.json"), "{\"height\":81}");
assertNotEquals(prepared, PreparedHydrologyCacheIdentity.capture(target(production), kernel, null, false));
}
}
@Test
public void biomeBuffetLayoutsAreIsolatedWhileNormalStudioSharesProduction() throws Exception {
Path pack = folder.newFolder("layout").toPath();
Files.writeString(pack.resolve("terrain.json"), "{}");
EngineTarget target = target(pack);
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
when(IrisPlatforms.get().platformName()).thenReturn("bukkit");
try (MockedStatic<PackValidationCache> validation = mockStatic(PackValidationCache.class)) {
validation.when(PackValidationCache::contextFingerprint).thenReturn("platform");
PreparedHydrologyCacheIdentity normal = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
assertNotNull(normal);
assertEquals(normal, PreparedHydrologyCacheIdentity.capture(target, kernel, null, true));
when(target.getDimension().getStudioMode()).thenReturn(StudioMode.BIOME_BUFFET_3x3);
PreparedHydrologyCacheIdentity buffet = PreparedHydrologyCacheIdentity.capture(target, kernel, null, true);
assertNotEquals(normal, buffet);
assertEquals(normal, PreparedHydrologyCacheIdentity.capture(target, kernel, null, false));
when(target.getDimension().getStudioMode()).thenReturn(StudioMode.BIOME_BUFFET_9x9);
assertNotEquals(buffet, PreparedHydrologyCacheIdentity.capture(target, kernel, null, true));
}
}
@Test
public void visibleFallbackPackBytesAndNamesSeparatePlans() throws Exception {
Path targetPack = folder.newFolder("target").toPath();
Path packs = folder.newFolder("packs").toPath();
Path dependency = Files.createDirectory(packs.resolve("shared"));
Path content = dependency.resolve("terrain.json");
Files.writeString(content, "{\"height\":80}");
EngineTarget target = target(targetPack);
when(target.getData().hasGenerationRegistryContract()).thenReturn(false);
when(IrisPlatforms.get().packsFolderNoCreate()).thenReturn(packs.toFile());
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
try (MockedStatic<PackValidationCache> validation = mockStatic(PackValidationCache.class)) {
validation.when(PackValidationCache::contextFingerprint).thenReturn("platform");
PreparedHydrologyCacheIdentity initial = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
assertNotNull(initial);
Files.writeString(content, "{\"height\":81}");
PreparedHydrologyCacheIdentity changed = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
assertNotNull(changed);
assertNotEquals(initial, changed);
Files.move(dependency, packs.resolve("renamed"));
PreparedHydrologyCacheIdentity renamed = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
assertNotNull(renamed);
assertNotEquals(changed, renamed);
Path hidden = Files.createDirectory(packs.resolve(".hidden"));
Files.writeString(hidden.resolve("terrain.json"), "{\"height\":90}");
assertEquals(renamed, PreparedHydrologyCacheIdentity.capture(target, kernel, null, false));
}
}
@Test
public void visiblePackSymlinksUseLogicalNamesAndResolvedContent() throws Exception {
Path targetPack = folder.newFolder("symlink-target").toPath();
Path first = folder.newFolder("first-packs").toPath();
Path second = folder.newFolder("second-packs").toPath();
Path dependency = Files.createDirectory(first.resolve("shared"));
Files.writeString(dependency.resolve("terrain.json"), "{}");
Files.createSymbolicLink(second.resolve("shared"), dependency);
EngineTarget target = target(targetPack);
when(target.getData().hasGenerationRegistryContract()).thenReturn(false);
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
try (MockedStatic<PackValidationCache> validation = mockStatic(PackValidationCache.class)) {
validation.when(PackValidationCache::contextFingerprint).thenReturn("platform");
when(IrisPlatforms.get().packsFolderNoCreate()).thenReturn(first.toFile());
PreparedHydrologyCacheIdentity direct = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
assertNotNull(direct);
when(IrisPlatforms.get().packsFolderNoCreate()).thenReturn(second.toFile());
assertEquals(direct, PreparedHydrologyCacheIdentity.capture(target, kernel, null, false));
}
}
private static EngineTarget target(Path path) {
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(path.toFile());
when(data.hasGenerationRegistryContract()).thenReturn(true);
IrisWorld world = mock(IrisWorld.class);
when(world.minHeight()).thenReturn(-64);
when(world.maxHeight()).thenReturn(320);
EngineTarget target = mock(EngineTarget.class);
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getStudioMode()).thenReturn(StudioMode.NORMAL);
when(target.getDimension()).thenReturn(dimension);
when(target.getData()).thenReturn(data);
when(target.getWorld()).thenReturn(world);
return target;
}
@Test
public void identicalInputsShareRegardlessOfWorldLocation() {
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
assertEquals(identity("pack", "closed", "platform", kernel, null, -64),
identity("pack", "closed", "platform", kernel, null, -64));
}
@Test
public void everyExternalInputAndTransitionSeparatesPlans() {
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
PreparedHydrologyCacheIdentity base = identity("pack", "closed", "platform", kernel, null, -64);
assertNotEquals(base, identity("changed", "closed", "platform", kernel, null, -64));
assertNotEquals(base, identity("pack", "dependency-changed", "platform", kernel, null, -64));
assertNotEquals(base, identity("pack", "closed", "platform-changed", kernel, null, -64));
assertNotEquals(base, identity("pack", "closed", "platform", kernel("changed"), null, -64));
assertNotEquals(base, identity("pack", "closed", "platform", kernel, null, 0));
TransitionGenerationPlan.Specification transition = new TransitionGenerationPlan.Specification(
1L, "old", "new", 1, 256, "boundary", "terrain");
assertNotEquals(base, identity("pack", "closed", "platform", kernel, transition, -64));
assertNotEquals(identity("pack", "closed", "platform", kernel, transition, -64),
identity("pack", "closed", "platform", kernel,
new TransitionGenerationPlan.Specification(1L, "old", "new", 1, 256, "other", "terrain"), -64));
}
private static PreparedHydrologyCacheIdentity identity(String pack, String dependencies, String platform,
GenerationKernelRegistry.RuntimeKernel kernel, TransitionGenerationPlan.Specification transition, int minY) {
return PreparedHydrologyCacheIdentity.fromInputs(pack, dependencies, platform, kernel, transition, minY, 320, 0);
}
private static GenerationKernelRegistry.RuntimeKernel kernel(String implementation) {
GenerationKernelRegistry.RuntimeKernel kernel = mock(GenerationKernelRegistry.RuntimeKernel.class);
when(kernel.version()).thenReturn(new GenerationKernelRegistry.Version(1, 1, 1));
when(kernel.implementationFingerprint()).thenReturn(implementation);
return kernel;
}
}
@@ -31,6 +31,50 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class HydrologyBankTerrainRuntimeTest {
@Test
public void raisedOwnedBanksFillThroughTheirContainmentHeightWithoutDensityOrNeighbourQueries() {
AtomicInteger sampled = new AtomicInteger();
Terrain3DColumn natural = Terrain3DColumnFixtures.spans(70, 0, 70, 76, 80);
HydrologyBankTerrainRuntime runtime = HydrologyBankTerrainRuntime.withNoise(
new HydrologyBankTerrainRuntime.Sources((x, z) -> natural, (x, z) -> {
assertEquals(-1, x);
assertEquals(-17, z);
sampled.incrementAndGet();
return HydrologyColumnSnapshot.ready(bank(84, false, true));
}), options(16), (x, y, z) -> {
throw new AssertionError("A filled bank must not sample erosion density");
});
Terrain3DColumn column = runtime.column(-1, -17);
assertEquals(84, column.topY());
assertEquals(1, column.spanCount());
for (int y = 0; y <= 84; y++) {
assertTrue("Required containment at " + y, column.isSolid(y));
}
assertFalse(column.isSolid(85));
assertSame(column, runtime.column(-1, -17));
assertEquals(1, sampled.get());
}
@Test
public void raisedBankFillIsClampedToTheWorldHeightAndDoesNotAffectUnownedBands() {
Terrain3DColumn natural = Terrain3DColumn.unshaped(250, 256);
HydrologyBankTerrainRuntime runtime = HydrologyBankTerrainRuntime.withNoise(
new HydrologyBankTerrainRuntime.Sources((x, z) -> natural,
(x, z) -> HydrologyColumnSnapshot.ready(bank(270, false, x == 0))),
options(16), (x, y, z) -> {
throw new AssertionError("Raised bank fill must not sample density");
});
Terrain3DColumn filled = runtime.column(0, 0);
assertEquals(255, filled.topY());
assertTrue(filled.isSolid(255));
assertFalse(filled.isSolid(256));
assertSame(natural, runtime.column(1, 0));
}
@Test
public void unavailableSupportColumnsDoNotPoisonLaterAcceptedBankGeometry() {
AtomicBoolean ready = new AtomicBoolean();
@@ -99,6 +99,8 @@ public class IrisDimensionCarvingResolverParityTest {
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ);
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state);
assertSame("entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved);
assertSame(legacyResolveEntryBiome(fixture.engine, legacyResolved),
IrisDimensionCarvingResolver.resolveBiome(fixture.engine, worldX, worldY, worldZ, state));
}
}
fixture.clearRecordedCalls();
@@ -120,6 +122,8 @@ public class IrisDimensionCarvingResolverParityTest {
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ);
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state);
assertSame("mixed entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved);
assertSame(legacyResolveEntryBiome(fixture.engine, legacyResolved),
IrisDimensionCarvingResolver.resolveBiome(fixture.engine, worldX, worldY, worldZ, state));
}
}
fixture.clearRecordedCalls();
@@ -232,6 +236,54 @@ public class IrisDimensionCarvingResolverParityTest {
assertSame(firstBiome, IrisDimensionCarvingResolver.resolveEntryBiome(engine, entry, state));
}
@Test
public void biomeResolutionReadsRuntimeBindingOnce() {
Fixture fixture = createFixture();
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
IrisBiome expected = IrisDimensionCarvingResolver.resolveBiome(fixture.engine, -17, 83, 23, state);
assertNotNull(expected);
fixture.clearRecordedCalls();
assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(fixture.engine, -17, 83, 23, state));
verify(fixture.engine).getDimension();
verify(fixture.engine).getData();
}
@Test
public void biomeResolutionInvalidatesAcrossDataDimensionsAndEngines() {
for (boolean explicitState : new boolean[]{false, true}) {
IrisDimensionCarvingResolver.State state = explicitState ? new IrisDimensionCarvingResolver.State() : null;
Fixture fixture = createFixture();
IrisData firstData = fixture.engine.getData();
IrisDimension dimension = fixture.engine.getDimension();
IrisData replacementData = mock(IrisData.class);
IrisDimensionCarvingEntry entry = mock(IrisDimensionCarvingEntry.class);
IrisBiome firstBiome = new IrisBiome();
IrisBiome replacementBiome = new IrisBiome();
doReturn(true).when(entry).isEnabled();
doReturn(firstBiome).when(entry).getRealBiome(firstData);
doReturn(replacementBiome).when(entry).getRealBiome(replacementData);
doReturn(new KList<>(entry)).when(dimension).getCarving();
assertSame(firstBiome, IrisDimensionCarvingResolver.resolveBiome(fixture.engine, -17, 83, 23, state));
doReturn(replacementData).when(fixture.engine).getData();
assertSame(replacementBiome, IrisDimensionCarvingResolver.resolveBiome(fixture.engine, -17, 83, 23, state));
doReturn(firstData).when(fixture.engine).getData();
assertSame(firstBiome, IrisDimensionCarvingResolver.resolveBiome(fixture.engine, -17, 83, 23, state));
IrisDimension emptyDimension = new IrisDimension();
doReturn(emptyDimension).when(fixture.engine).getDimension();
assertSame(null, IrisDimensionCarvingResolver.resolveBiome(fixture.engine, -17, 83, 23, state));
Fixture replacement = createMixedDepthFixture();
IrisDimensionCarvingEntry expectedEntry = legacyResolveFromRoot(replacement.engine,
legacyResolveRootEntry(replacement.engine, 83), -17, 23);
IrisBiome expected = legacyResolveEntryBiome(replacement.engine, expectedEntry);
assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(replacement.engine, -17, 83, 23, state));
assertSame(null, IrisDimensionCarvingResolver.resolveBiome(fixture.engine, -17, 83, 23, state));
}
}
@Test
public void resolverStateUsesWeakRuntimeIdentityBindings() throws NoSuchFieldException {
Field engineIdentity = IrisDimensionCarvingResolver.State.class.getDeclaredField("engineIdentity");
@@ -5,6 +5,8 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -19,6 +21,34 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class Terrain3DRuntimeTest {
@Test
public void alignedColumnsOnlySampleContributingAnchors() {
for (int x = -8; x <= -7; x++) {
for (int z = -8; z <= -7; z++) {
Set<List<Double>> samples = new HashSet<>();
Terrain3DRuntime runtime = runtime(profile(), (sampleX, sampleY, sampleZ) -> {
samples.add(List.of(sampleX, sampleZ));
return 0D;
});
Terrain3DColumn column = runtime.column(x, z);
assertEquals(96, column.topY());
assertEquals(1, column.spanCount());
Set<List<Double>> expected = new HashSet<>();
expected.add(List.of(-8D, -8D));
if (x != -8) {
expected.add(List.of(-4D, -8D));
}
if (z != -8) {
expected.add(List.of(-8D, -4D));
}
if (x != -8 && z != -8) {
expected.add(List.of(-4D, -4D));
}
assertEquals(expected, samples);
}
}
}
@Test
public void omittedProfilesPreserveExactHeightsWithoutCreatingNoise() {
AtomicInteger compilations = new AtomicInteger();
@@ -24,6 +24,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Files;
@@ -43,6 +44,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mockStatic;
public class PackValidationRegistryTest {
@Rule
@@ -137,7 +139,7 @@ public class PackValidationRegistryTest {
Path mismatchedTarget = temporaryFolder.newFolder("copy-mismatched-target").toPath();
PackValidationResult result = new PackValidationResult(
"source", List.of(), List.of("source warning"), 7L);
PackValidationRegistry.publish(sourceRoot, result, "fingerprint-a");
PackValidationRegistry.publish(sourceRoot, result, "fingerprint-a", PackValidationCache.contextFingerprint());
assertSame(result, PackValidationRegistry.publishMatchingCopy(
sourceRoot,
@@ -151,13 +153,94 @@ public class PackValidationRegistryTest {
assertNull(PackValidationRegistry.get(mismatchedTarget));
}
@Test
public void matchingLookupRetainsRootGenerationAndRejectsMutation() throws Exception {
Path packRoot = temporaryFolder.newFolder("matching-source").toPath();
PackValidationResult validation = new PackValidationResult("source", List.of(), List.of(), 11L);
PackValidationRegistry.publish(packRoot, validation, "fingerprint-a", PackValidationCache.contextFingerprint());
PackValidationRegistry.ValidationTicket ticket = PackValidationRegistry.tryBeginValidation(packRoot);
assertSame(validation, PackValidationRegistry.getMatching(packRoot, "fingerprint-a"));
assertNull(PackValidationRegistry.getMatching(packRoot, "fingerprint-b"));
assertTrue(PackValidationRegistry.publishIfCurrent(ticket, validation));
assertNull(PackValidationRegistry.getMatching(packRoot, "fingerprint-a"));
PackValidationRegistry.publish(packRoot, validation, "fingerprint-a", PackValidationCache.contextFingerprint());
try (PackValidationRegistry.RootMutation mutation = PackValidationRegistry.beginRootMutation(packRoot)) {
assertNull(PackValidationRegistry.getMatching(packRoot, "fingerprint-a"));
mutation.stage(validation);
mutation.commit();
}
assertNull(PackValidationRegistry.getMatching(packRoot, "fingerprint-a"));
}
@Test
public void changedValidationContextRejectsSourceAndCopiedReuse() throws Exception {
Path source = temporaryFolder.newFolder("context-source").toPath();
Path target = temporaryFolder.newFolder("context-copy").toPath();
PackValidationResult validation = new PackValidationResult("source", List.of(), List.of(), 13L);
try (MockedStatic<PackValidationCache> context = mockStatic(PackValidationCache.class)) {
context.when(PackValidationCache::contextFingerprint).thenReturn("original-context");
PackValidationRegistry.publish(source, validation, "fingerprint-a", PackValidationCache.contextFingerprint());
assertSame(validation, PackValidationRegistry.getMatching(source, "fingerprint-a"));
context.when(PackValidationCache::contextFingerprint).thenReturn("changed-context");
assertNull(PackValidationRegistry.getMatching(source, "fingerprint-a"));
assertNull(PackValidationRegistry.publishMatchingCopy(source, target, "fingerprint-a"));
assertNull(PackValidationRegistry.get(target));
}
}
@Test
public void changedContextBeforePublicationCannotAuthorizeFingerprintReuse() throws Exception {
Path source = temporaryFolder.newFolder("changed-context-publication").toPath();
Path target = temporaryFolder.newFolder("changed-context-publication-copy").toPath();
PackValidationResult validation = new PackValidationResult("source", List.of(), List.of(), 19L);
try (MockedStatic<PackValidationCache> context = mockStatic(PackValidationCache.class)) {
context.when(PackValidationCache::contextFingerprint).thenReturn("original-context");
String validatedContext = PackValidationCache.contextFingerprint();
context.when(PackValidationCache::contextFingerprint).thenReturn("changed-context");
PackValidationRegistry.publish(source, validation, "fingerprint-a", validatedContext);
assertSame(validation, PackValidationRegistry.get(source));
assertNull(PackValidationRegistry.getMatching(source, "fingerprint-a"));
assertNull(PackValidationRegistry.publishMatchingCopy(source, target, "fingerprint-a"));
context.when(PackValidationCache::contextFingerprint).thenReturn("original-context");
assertNull(PackValidationRegistry.getMatching(source, "fingerprint-a"));
}
}
@Test
public void mutationDuringContextCheckRejectsMatchingValidation() throws Exception {
Path source = temporaryFolder.newFolder("interleaved-context-mutation").toPath();
PackValidationResult validation = new PackValidationResult("source", List.of(), List.of(), 17L);
AtomicReference<PackValidationRegistry.RootMutation> mutation = new AtomicReference<>();
try (MockedStatic<PackValidationCache> context = mockStatic(PackValidationCache.class)) {
context.when(PackValidationCache::contextFingerprint).thenReturn("context");
PackValidationRegistry.publish(source, validation, "fingerprint-a", PackValidationCache.contextFingerprint());
context.when(PackValidationCache::contextFingerprint).thenAnswer(invocation -> {
mutation.set(PackValidationRegistry.beginRootMutation(source));
return "context";
});
assertNull(PackValidationRegistry.getMatching(source, "fingerprint-a"));
assertNotNull(mutation.get());
assertNull(PackValidationRegistry.get(source));
} finally {
if (mutation.get() != null) {
mutation.get().close();
}
}
}
@Test
public void unfingerprintedRepublishRevokesCopiedValidationReuse() throws Exception {
Path sourceRoot = temporaryFolder.newFolder("republished-source").toPath();
Path targetRoot = temporaryFolder.newFolder("republished-target").toPath();
PackValidationResult initial = new PackValidationResult("source", List.of(), List.of(), 3L);
PackValidationResult replacement = new PackValidationResult("source", List.of(), List.of(), 5L);
PackValidationRegistry.publish(sourceRoot, initial, "old-fingerprint");
PackValidationRegistry.publish(sourceRoot, initial, "old-fingerprint", PackValidationCache.contextFingerprint());
PackValidationRegistry.publish(sourceRoot, replacement);
@@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "7950fa5a3f3abf7fdd2cea1bd4b1fd335745b348ce64e67404cc86e898d18f16";
private static final String EXPECTED_SCHEMA_DIGEST = "415345947f2c96fc468848aefa064854a085ae5631ea0f780c77c99973bff594";
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class,
@@ -0,0 +1,156 @@
package art.arcane.iris.platform.generation;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
import art.arcane.iris.generation.runtime.EngineTarget;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionRuntimeContract;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
import art.arcane.iris.studio.StudioMode;
import art.arcane.iris.world.IrisWorld;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.generator.WorldInfo;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class BukkitChunkGeneratorEntryMulticoreTest {
@Test
public void ordinaryEntryUsesMulticoreUntilEntryCompletes() throws Exception {
try (Fixture fixture = new Fixture(false)) {
fixture.generate();
fixture.generator.completeInitialEntry();
fixture.generate();
assertEquals(List.of(true, false), fixture.multicore);
}
}
@Test
public void forceMulticoreStillAppliesAfterOrdinaryEntry() throws Exception {
try (Fixture fixture = new Fixture(false)) {
fixture.generator.completeInitialEntry();
fixture.settings.getPerformance().getEngineSVC().setForceMulticoreWrite(true);
fixture.generate();
fixture.settings.getPerformance().getEngineSVC().setForceMulticoreWrite(false);
fixture.generate();
assertEquals(List.of(true, false), fixture.multicore);
}
}
@Test
public void pregenerationStillUsesMulticoreAfterOrdinaryEntry() throws Exception {
try (Fixture fixture = new Fixture(false)) {
fixture.generator.completeInitialEntry();
when(fixture.hooks.isPregeneratorActive(fixture.engine)).thenReturn(true);
fixture.generate();
when(fixture.hooks.isPregeneratorActive(fixture.engine)).thenReturn(false);
fixture.generate();
assertEquals(List.of(true, false), fixture.multicore);
}
}
@Test
public void studioBootstrapUsesMulticoreUntilItsOwnLifecycleEnds() throws Exception {
try (Fixture fixture = new Fixture(true)) {
fixture.generate();
fixture.generator.completeInitialEntry();
fixture.generate();
fixture.generator.endStudioEntryBootstrap();
fixture.generate();
assertEquals(List.of(true, true, false), fixture.multicore);
}
}
private static void field(BukkitChunkGenerator generator, String name, Object value) throws Exception {
Field field = BukkitChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
private static final class Fixture implements AutoCloseable {
private final BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
private final Engine engine = mock(Engine.class);
private final EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
private final IrisSettings settings = new IrisSettings();
private final WorldInfo world = mock(WorldInfo.class);
private final ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
private final ArrayList<Boolean> multicore = new ArrayList<>();
private final MockedStatic<IrisSettings> settingsAccess = mockStatic(IrisSettings.class);
private final MockedStatic<INMS> nativeAccess = mockStatic(INMS.class);
private Fixture(boolean studio) throws Exception {
settingsAccess.when(IrisSettings::get).thenReturn(settings);
INMSBinding binding = mock(INMSBinding.class);
nativeAccess.when(INMS::get).thenReturn(binding);
when(binding.applyChunkDataBlocks(any(), any())).thenReturn(true);
when(chunk.getMaxHeight()).thenReturn(16);
when(engine.getPlatformHooks()).thenReturn(hooks);
when(engine.acquireGenerationLease("bukkit_terrain_stage")).thenReturn(GenerationSessionLease.noop());
doCallRealMethod().when(engine).shouldGenerateMulticore();
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getLoadKey()).thenReturn("entry-multicore-test");
when(dimension.getStudioMode()).thenReturn(StudioMode.NORMAL);
when(engine.getDimension()).thenReturn(dimension);
EngineTarget target = mock(EngineTarget.class);
when(target.getDimension()).thenReturn(dimension);
doReturn(target).when(generator).getTarget();
field(generator, "world", mock(IrisWorld.class));
field(generator, "validatedDimension", dimension);
field(generator, "validatedWorldContract", new IrisDimensionRuntimeContract("iris:test", 0, 16, 16));
field(generator, "startupReady", CompletableFuture.completedFuture(null));
field(generator, "startupContentReady", CompletableFuture.completedFuture(null));
field(generator, "setup", new AtomicBoolean(true));
field(generator, "studio", studio);
field(generator, "studioEntryBootstrapActive", new AtomicBoolean(studio));
generator.setLastMode(StudioMode.NORMAL);
if (!studio) {
generator.beginInitialEntry(false);
}
generator.setEngine(engine);
doAnswer(invocation -> {
assertSame(engine, IrisContext.require().getEngine());
multicore.add(invocation.getArgument(4, Boolean.class));
return null;
}).when(engine).generate(anyInt(), anyInt(), any(), any(), anyBoolean());
}
private void generate() {
generator.generateNoise(world, new Random(1L), -2, 3, chunk);
assertNull(IrisContext.get());
}
@Override
public void close() {
nativeAccess.close();
settingsAccess.close();
}
}
}
@@ -2,6 +2,15 @@ package art.arcane.iris.platform.generation;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.world.history.GenerationActivation;
import art.arcane.iris.world.history.GenerationEpoch;
import art.arcane.iris.world.history.GenerationHistoryPaths;
import art.arcane.iris.world.history.GenerationKernelRegistry;
import art.arcane.iris.world.history.GenerationRegistryContract;
import org.mockito.MockedConstruction;
import java.lang.reflect.Method;
import static org.mockito.Mockito.mockConstruction;
import art.arcane.iris.generation.runtime.EngineTarget;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.world.history.GenerationHistory;
@@ -23,6 +32,30 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class BukkitChunkGeneratorHistoryTargetTest {
@Test
public void selectedFrozenRegistryIsBoundBeforeRuntimeConstruction() throws Exception {
Fixture fixture = new Fixture();
GenerationActivation activation = mock(GenerationActivation.class);
GenerationEpoch epoch = mock(GenerationEpoch.class);
GenerationRegistryContract registry = mock(GenerationRegistryContract.class);
GenerationHistoryPaths paths = mock(GenerationHistoryPaths.class);
when(fixture.history.activeActivation()).thenReturn(activation);
when(fixture.history.activeEpoch()).thenReturn(epoch);
when(fixture.history.paths()).thenReturn(paths);
when(activation.isInitial()).thenReturn(true);
when(activation.activationId()).thenReturn(1L);
when(epoch.registryContract()).thenReturn(registry);
when(epoch.kernelVersion()).thenReturn(new GenerationKernelRegistry.Version(1, 1, 1));
when(paths.activationMantleRoot(1L)).thenReturn(Path.of("generation", "mantle", "1"));
try (MockedConstruction<IrisEngine> engines = mockConstruction(IrisEngine.class,
(engine, context) -> verify(fixture.data).bindGenerationRegistryContract(registry))) {
Method create = BukkitChunkGenerator.class.getDeclaredMethod("createEngine", EngineTarget.class);
create.setAccessible(true);
Object created = create.invoke(fixture.generator, fixture.target);
assertSame(engines.constructed().getFirst(), created);
}
}
@Test
public void unchangedVerifiedPackRetainsThePreviouslyLoadedTarget() throws Exception {
Fixture fixture = new Fixture();
@@ -4,14 +4,17 @@ import art.arcane.iris.pack.datapack.ServerConfigurator;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.AtomicDirectoryPublisher;
import art.arcane.iris.pack.BrokenPackException;
import art.arcane.iris.pack.PackValidationCache;
import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.history.GenerationPackFingerprint;
import art.arcane.iris.world.task.J;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.Assume;
@@ -278,6 +281,56 @@ public class StudioSVCWorldPackPublishTest {
assertTrue(PackValidationRegistry.isBroken(packRoot));
}
@Test
public void unchangedGenerationCandidateReusesStartupValidation() throws Exception {
Path source = temporaryFolder.newFolder("validated-generation-source").toPath();
writeValidPack(source);
PackValidationResult validation = new PackValidationResult(
"validated-generation-source", List.of(), List.of("retained warning"), 31L);
PackValidationRegistry.publish(source, validation,
ServerConfigurator.computePackTreeFingerprint(source.toFile()), PackValidationCache.contextFingerprint());
String fingerprint = GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION);
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
assertSame(validation, StudioSVC.validateGenerationCandidate(source, fingerprint));
validator.verifyNoInteractions();
}
assertSame(validation, PackValidationRegistry.getMatching(source, fingerprint));
}
@Test
public void editedGenerationCandidateCannotReuseStartupValidation() throws Exception {
Path source = temporaryFolder.newFolder("edited-generation-source").toPath();
writeValidPack(source);
PackValidationRegistry.publish(source, new PackValidationResult(
"edited-generation-source", List.of(), List.of(), 37L),
ServerConfigurator.computePackTreeFingerprint(source.toFile()), PackValidationCache.contextFingerprint());
Files.writeString(source.resolve("dimensions/main.json"), "{");
String fingerprint = GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION);
assertThrows(BrokenPackException.class,
() -> StudioSVC.validateGenerationCandidate(source, fingerprint));
assertTrue(PackValidationRegistry.isBroken(source));
}
@Test
public void unchangedBrokenGenerationCandidateRetainsValidationFailure() throws Exception {
Path source = temporaryFolder.newFolder("rejected-generation-source").toPath();
writeValidPack(source);
PackValidationResult validation = new PackValidationResult(
"rejected-generation-source", List.of("registry content unavailable"), List.of(), 41L);
PackValidationRegistry.publish(source, validation,
ServerConfigurator.computePackTreeFingerprint(source.toFile()), PackValidationCache.contextFingerprint());
String fingerprint = GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION);
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
assertThrows(BrokenPackException.class,
() -> StudioSVC.validateGenerationCandidate(source, fingerprint));
validator.verifyNoInteractions();
}
assertSame(validation, PackValidationRegistry.getMatching(source, fingerprint));
}
@Test
public void exactCopiedFingerprintReusesSourceSemanticValidation() throws Exception {
Path root = temporaryFolder.newFolder("matching-validation-copy").toPath();
@@ -289,7 +342,7 @@ public class StudioSVCWorldPackPublishTest {
String copiedFingerprint = ServerConfigurator.computePackTreeFingerprint(target.toFile());
PackValidationResult sourceValidation = new PackValidationResult(
"source", List.of(), List.of("preserved source warning"), 17L);
PackValidationRegistry.publish(source, sourceValidation, sourceFingerprint);
PackValidationRegistry.publish(source, sourceValidation, sourceFingerprint, PackValidationCache.contextFingerprint());
PackValidationResult reused = StudioSVC.validatePublishedPack(
target,
@@ -310,7 +363,7 @@ public class StudioSVCWorldPackPublishTest {
PackValidationResult sourceValidation = new PackValidationResult(
"snapshot-source", List.of(), List.of("source warning"), 23L);
PackValidationRegistry.publish(source, sourceValidation,
ServerConfigurator.computePackTreeFingerprint(source.toFile()));
ServerConfigurator.computePackTreeFingerprint(source.toFile()), PackValidationCache.contextFingerprint());
IrisData installed = IrisData.openDatapackCompiler(target.toFile());
try (MockedStatic<IrisData> data = mockStatic(IrisData.class, CALLS_REAL_METHODS)) {
@@ -330,7 +383,7 @@ public class StudioSVCWorldPackPublishTest {
writeValidPack(source);
StudioSVC.copyPackTree(source, target);
PackValidationRegistry.publish(source, new PackValidationResult("snapshot-source", List.of(), List.of(), 29L),
ServerConfigurator.computePackTreeFingerprint(source.toFile()));
ServerConfigurator.computePackTreeFingerprint(source.toFile()), PackValidationCache.contextFingerprint());
Files.writeString(target.resolve("dimensions/main.json"), "{");
assertThrows(BrokenPackException.class, () -> StudioSVC.loadInstalledDimension(target, source, "main"));
@@ -347,7 +400,7 @@ public class StudioSVCWorldPackPublishTest {
String sourceFingerprint = ServerConfigurator.computePackTreeFingerprint(source.toFile());
PackValidationResult sourceValidation = new PackValidationResult(
"source", List.of(), List.of(), 19L);
PackValidationRegistry.publish(source, sourceValidation, sourceFingerprint);
PackValidationRegistry.publish(source, sourceValidation, sourceFingerprint, PackValidationCache.contextFingerprint());
Files.writeString(target.resolve("dimensions/main.json"), "{");
String copiedFingerprint = ServerConfigurator.computePackTreeFingerprint(target.toFile());
@@ -374,7 +427,7 @@ public class StudioSVCWorldPackPublishTest {
"source", List.of(), List.of(), 23L);
PackValidationResult staleTargetValidation = new PackValidationResult(
"pack", List.of(), List.of("stale target"), 11L);
PackValidationRegistry.publish(sourcePack, sourceValidation, sourceFingerprint);
PackValidationRegistry.publish(sourcePack, sourceValidation, sourceFingerprint, PackValidationCache.contextFingerprint());
PackValidationRegistry.publish(target, staleTargetValidation);
assertSame(staleTargetValidation, PackValidationRegistry.requireLoadable(target));
@@ -3,6 +3,9 @@ package art.arcane.iris.world.pregen;
import art.arcane.volmlib.util.math.Position2;
import org.junit.Test;
import java.util.LinkedHashSet;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -55,7 +58,7 @@ public class PregenTaskBoundsOverflowTest {
int[] bounds = task.regionBounds();
assertEquals(-58594, bounds[0]);
assertEquals(58594, bounds[2]);
assertEquals(58593, bounds[2]);
}
@Test
@@ -93,6 +96,38 @@ public class PregenTaskBoundsOverflowTest {
assertArrayEqualsMessage(new int[]{-2, -1, 2, 1}, task.regionBounds());
}
@Test
public void thousandBlockRadiusVisitsOnlyTheSixteenOccupiedRegions() {
PregenTask task = PregenTask.builder().radiusX(1000).radiusZ(1000).build();
assertArrayEqualsMessage(new int[]{-63, -63, 63, 63}, task.chunkBounds());
assertArrayEqualsMessage(new int[]{-2, -2, 1, 1}, task.regionBounds());
assertEquals(16_129L, task.chunkCount());
assertRegionTraversal(task);
}
@Test
public void asymmetricRegionTraversalRetainsEveryChunkAndStartsAtTheCenter() {
int[][] areas = {
{0, 0, 1, 1},
{497, -497, 1, 17},
{-513, 511, 32, 17},
{511, -513, 1000, 32},
{512, -512, 512, 256},
{0, 0, 1024, 512},
{PregenTask.MAX_WORLD_BLOCK - 32, 0, 32, 1},
{0, -PregenTask.MAX_WORLD_BLOCK + 32, 1, 32}
};
for (int[] area : areas) {
PregenTask task = PregenTask.builder()
.center(new Position2(area[0], area[1]))
.radiusX(area[2])
.radiusZ(area[3])
.build();
assertRegionTraversal(task);
}
}
@Test
public void clampSaturatesInsteadOfWrapping() {
assertEquals(Integer.MAX_VALUE, PregenTask.clampBlock((long) Integer.MAX_VALUE + 1L));
@@ -101,6 +136,28 @@ public class PregenTaskBoundsOverflowTest {
assertEquals(-7, PregenTask.clampBlock(-7L));
}
private static void assertRegionTraversal(PregenTask task) {
int[] chunks = task.chunkBounds();
LinkedHashSet<Position2> expected = new LinkedHashSet<>();
for (int regionX = chunks[0] >> 5; regionX <= chunks[2] >> 5; regionX++) {
for (int regionZ = chunks[1] >> 5; regionZ <= chunks[3] >> 5; regionZ++) {
expected.add(new Position2(regionX, regionZ));
}
}
LinkedHashSet<Position2> actual = new LinkedHashSet<>();
AtomicLong visitedChunks = new AtomicLong();
task.iterateRegions((regionX, regionZ) -> {
assertTrue(actual.add(new Position2(regionX, regionZ)));
long before = visitedChunks.get();
task.iterateChunks(regionX, regionZ, (chunkX, chunkZ) -> visitedChunks.incrementAndGet());
assertTrue(visitedChunks.get() > before);
});
assertEquals(expected, actual);
assertEquals(new Position2(task.getCenter().getX() >> 9, task.getCenter().getZ() >> 9), actual.getFirst());
assertEquals(task.chunkCount(), visitedChunks.get());
}
private static void assertArrayEqualsMessage(int[] expected, int[] actual) {
assertEquals(expected.length, actual.length);
for (int index = 0; index < expected.length; index++) {