mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Tunabelly
This commit is contained in:
1 parent
197d2c2461
commit
30c533feaf
114 files changed
+4071
-3994
No files matched your search
+3
-3
@@ -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());
|
||||
}
|
||||
}
|
||||
+66
@@ -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));
|
||||
}
|
||||
}
|
||||
+191
@@ -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) {
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+13
-12
@@ -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),
|
||||
|
||||
+9
-62
@@ -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) {
|
||||
|
||||
+5
-54
@@ -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));
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -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);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+5
-5
@@ -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;
|
||||
|
||||
+4
-175
@@ -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) {
|
||||
|
||||
-169
@@ -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));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
-541
@@ -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);
|
||||
}
|
||||
}
|
||||
+192
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
-127
@@ -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());
|
||||
}
|
||||
}
|
||||
-22
@@ -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();
|
||||
|
||||
-254
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
-145
@@ -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());
|
||||
}
|
||||
}
|
||||
+70
-1
@@ -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");
|
||||
|
||||
+2
-87
@@ -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);
|
||||
}
|
||||
}
|
||||
+167
-11
@@ -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)));
|
||||
|
||||
+22
-9
@@ -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
|
||||
+193
-9
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+120
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+112
@@ -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);
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+64
-7
@@ -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);
|
||||
|
||||
+133
-9
@@ -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,
|
||||
|
||||
+10
-3
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+6
-6
@@ -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(
|
||||
|
||||
+207
@@ -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) {
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
|
||||
+67
@@ -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();
|
||||
|
||||
+3
-1
@@ -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) {
|
||||
|
||||
+171
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
@@ -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();
|
||||
|
||||
+52
@@ -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,
|
||||
|
||||
+156
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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++) {
|
||||
|
||||
Reference in new issue
Block a user