mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Reduced hydrology planning work
- Downstream route lengths are evaluated once per routing grid - Footprint construction sorts primitive column keys without a temporary tree - Footprint lookups use primitive keys and preserve immutable ordered views - Closed cave components keep one ordered flood-fill membership set - Surface pools and rivers share their layer construction - Regression tests cover routing limits, cycles, coordinate bounds, and footprint ordering
This commit is contained in:
1 parent
dde3c149ad
commit
20b9b3dce3
6 files changed
+262
-82
No files matched your search
@@ -2646,7 +2646,7 @@ public final class HydrologyPlanner {
|
|||||||
potential,
|
potential,
|
||||||
parent,
|
parent,
|
||||||
outletIndex,
|
outletIndex,
|
||||||
routeLengths(grid, parent),
|
routeLengths(parent, grid.width(), settings.routing()),
|
||||||
List.copyOf(outlets),
|
List.copyOf(outlets),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
@@ -3222,31 +3222,43 @@ public final class HydrologyPlanner {
|
|||||||
return count + (HydrologyHash.unit(stable) < remainder ? 1 : 0);
|
return count + (HydrologyHash.unit(stable) < remainder ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int[] routeLengths(SampledGrid grid, int[] parent) {
|
static int[] routeLengths(int[] parent, int width, HydrologyPlannerSettings.Routing routing) {
|
||||||
int[] lengths = new int[parent.length];
|
int[] lengths = new int[parent.length];
|
||||||
|
int[] pending = new int[parent.length];
|
||||||
|
double[] distances = new double[parent.length];
|
||||||
|
Arrays.fill(distances, Double.NaN);
|
||||||
|
double diagonalDistance = StrictMath.hypot(routing.sampleSpacing(), routing.sampleSpacing());
|
||||||
for (int sourceIndex = 0; sourceIndex < parent.length; sourceIndex++) {
|
for (int sourceIndex = 0; sourceIndex < parent.length; sourceIndex++) {
|
||||||
lengths[sourceIndex] = routeLength(sourceIndex, grid, parent);
|
if (!Double.isNaN(distances[sourceIndex])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int current = sourceIndex;
|
||||||
|
int pendingCount = 0;
|
||||||
|
while (current >= 0 && Double.isNaN(distances[current])) {
|
||||||
|
pending[pendingCount++] = current;
|
||||||
|
distances[current] = -1D;
|
||||||
|
current = parent[current];
|
||||||
|
}
|
||||||
|
if (current >= 0 && distances[current] < 0D) {
|
||||||
|
throw new IllegalStateException("Hydrology drainage routing contains a cycle.");
|
||||||
|
}
|
||||||
|
double distance = current < 0 ? 0D : distances[current];
|
||||||
|
while (pendingCount > 0) {
|
||||||
|
int node = pending[--pendingCount];
|
||||||
|
int next = parent[node];
|
||||||
|
if (next >= 0) {
|
||||||
|
boolean cardinal = node / width == next / width || node % width == next % width;
|
||||||
|
distance += cardinal ? routing.sampleSpacing() : diagonalDistance;
|
||||||
|
}
|
||||||
|
distances[node] = distance;
|
||||||
|
lengths[node] = distance > routing.maximumRouteLength()
|
||||||
|
? Integer.MAX_VALUE
|
||||||
|
: (int) StrictMath.ceil(distance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return lengths;
|
return lengths;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int routeLength(int sourceIndex, SampledGrid grid, int[] parent) {
|
|
||||||
int current = sourceIndex;
|
|
||||||
int steps = 0;
|
|
||||||
double length = 0D;
|
|
||||||
while (parent[current] >= 0) {
|
|
||||||
int next = parent[current];
|
|
||||||
GridNode currentNode = grid.node(current);
|
|
||||||
GridNode nextNode = grid.node(next);
|
|
||||||
length += StrictMath.hypot(currentNode.x() - nextNode.x(), currentNode.z() - nextNode.z());
|
|
||||||
if (length > settings.routing().maximumRouteLength() || ++steps > settings.routing().maximumRouteNodes()) {
|
|
||||||
return Integer.MAX_VALUE;
|
|
||||||
}
|
|
||||||
current = next;
|
|
||||||
}
|
|
||||||
return (int) StrictMath.ceil(length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int[] countContributions(List<Integer> sources, RoutingPlan routing) {
|
private int[] countContributions(List<Integer> sources, RoutingPlan routing) {
|
||||||
int[] counts = new int[routing.parent().length];
|
int[] counts = new int[routing.parent().length];
|
||||||
for (int source : sources) {
|
for (int source : sources) {
|
||||||
|
|||||||
@@ -3,46 +3,48 @@ package art.arcane.iris.engine.hydrology;
|
|||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
public final class RiverFootprint {
|
public final class RiverFootprint {
|
||||||
|
private static final RiverFootprint EMPTY = new RiverFootprint(Map.of());
|
||||||
|
|
||||||
|
private final Long2ObjectLinkedOpenHashMap<HydrologyColumnSample> indexed;
|
||||||
private final Map<Long, HydrologyColumnSample> columns;
|
private final Map<Long, HydrologyColumnSample> columns;
|
||||||
|
|
||||||
public RiverFootprint(Map<Long, HydrologyColumnSample> columns) {
|
public RiverFootprint(Map<Long, HydrologyColumnSample> columns) {
|
||||||
if (columns == null) {
|
if (columns == null) {
|
||||||
throw new IllegalArgumentException("columns must not be null.");
|
throw new IllegalArgumentException("columns must not be null.");
|
||||||
}
|
}
|
||||||
TreeMap<Long, HydrologyColumnSample> ordered = new TreeMap<>();
|
indexed = new Long2ObjectLinkedOpenHashMap<>(columns.size());
|
||||||
for (Map.Entry<Long, HydrologyColumnSample> entry : columns.entrySet()) {
|
for (Map.Entry<Long, HydrologyColumnSample> entry : columns.entrySet()) {
|
||||||
HydrologyColumnSample sample = entry.getValue();
|
HydrologyColumnSample sample = entry.getValue();
|
||||||
if (sample == null || entry.getKey() != pack(sample.x(), sample.z())) {
|
if (sample == null || entry.getKey() != pack(sample.x(), sample.z())) {
|
||||||
throw new IllegalArgumentException("Footprint keys must match their column coordinates.");
|
throw new IllegalArgumentException("Footprint keys must match their column coordinates.");
|
||||||
}
|
}
|
||||||
ordered.put(entry.getKey(), sample);
|
indexed.put(entry.getKey().longValue(), sample);
|
||||||
}
|
}
|
||||||
Long2ObjectLinkedOpenHashMap<HydrologyColumnSample> indexed =
|
long[] order = indexed.keySet().toLongArray();
|
||||||
new Long2ObjectLinkedOpenHashMap<>(ordered.size());
|
Arrays.sort(order);
|
||||||
for (Map.Entry<Long, HydrologyColumnSample> entry : ordered.entrySet()) {
|
for (long key : order) {
|
||||||
indexed.put(entry.getKey().longValue(), entry.getValue());
|
indexed.getAndMoveToLast(key);
|
||||||
}
|
}
|
||||||
columns = Collections.unmodifiableMap(indexed);
|
this.columns = Collections.unmodifiableMap(indexed);
|
||||||
this.columns = columns;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RiverFootprint empty() {
|
public static RiverFootprint empty() {
|
||||||
return new RiverFootprint(Map.of());
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<HydrologyColumnSample> sample(int x, int z) {
|
public Optional<HydrologyColumnSample> sample(int x, int z) {
|
||||||
return Optional.ofNullable(columns.get(pack(x, z)));
|
return Optional.ofNullable(indexed.get(pack(x, z)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public HydrologyRenderSample renderSample(int x, int z) {
|
public HydrologyRenderSample renderSample(int x, int z) {
|
||||||
HydrologyColumnSample sample = columns.get(pack(x, z));
|
HydrologyColumnSample sample = indexed.get(pack(x, z));
|
||||||
return sample == null ? new HydrologyRenderSample(x, z, List.of()) : sample.renderSample();
|
return sample == null ? new HydrologyRenderSample(x, z, List.of()) : sample.renderSample();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ public final class RiverFootprint {
|
|||||||
if (area < columns.size()) {
|
if (area < columns.size()) {
|
||||||
for (long z = minimumZ; z < maximumZ; z++) {
|
for (long z = minimumZ; z < maximumZ; z++) {
|
||||||
for (long x = minimumX; x < maximumX; x++) {
|
for (long x = minimumX; x < maximumX; x++) {
|
||||||
HydrologyColumnSample sample = columns.get(pack((int) x, (int) z));
|
HydrologyColumnSample sample = indexed.get(pack((int) x, (int) z));
|
||||||
if (sample != null) {
|
if (sample != null) {
|
||||||
selected.add(sample);
|
selected.add(sample);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-8
@@ -9,7 +9,6 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -1132,8 +1131,8 @@ public final class HydrologyCaveContainmentPlanner {
|
|||||||
List<CavePosition> throat
|
List<CavePosition> throat
|
||||||
) {
|
) {
|
||||||
Queue<CavePosition> queue = new ArrayDeque<>();
|
Queue<CavePosition> queue = new ArrayDeque<>();
|
||||||
Set<CavePosition> queued = new HashSet<>();
|
Set<CavePosition> queued = new LinkedHashSet<>();
|
||||||
Set<CavePosition> resolved = new LinkedHashSet<>();
|
int visitedCount = 0;
|
||||||
|
|
||||||
queue.add(source.target());
|
queue.add(source.target());
|
||||||
queued.add(source.target());
|
queued.add(source.target());
|
||||||
@@ -1148,10 +1147,7 @@ public final class HydrologyCaveContainmentPlanner {
|
|||||||
if (positionRejection != HydrologyCaveRejection.NONE) {
|
if (positionRejection != HydrologyCaveRejection.NONE) {
|
||||||
return ComponentResult.rejected(positionRejection);
|
return ComponentResult.rejected(positionRejection);
|
||||||
}
|
}
|
||||||
if (!resolved.add(position)) {
|
if (++visitedCount > settings.maxFloodVolume()) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (resolved.size() > settings.maxFloodVolume()) {
|
|
||||||
return ComponentResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
|
return ComponentResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1171,7 +1167,7 @@ public final class HydrologyCaveContainmentPlanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ComponentResult.accepted(resolved);
|
return ComponentResult.accepted(queued);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HydrologyCaveRejection addThroatContacts(
|
private HydrologyCaveRejection addThroatContacts(
|
||||||
|
|||||||
+14
-41
@@ -91,11 +91,12 @@ public final class SurfaceFootprintCompiler {
|
|||||||
&& column.x() == centerline.x()[0] && column.z() == centerline.z()[0];
|
&& column.x() == centerline.x()[0] && column.z() == centerline.z()[0];
|
||||||
int y = column.role() == SurfaceRole.CHANNEL ? column.headY() : column.height();
|
int y = column.role() == SurfaceRole.CHANNEL ? column.headY() : column.height();
|
||||||
HydrologyFeatureRef feature = features.feature(segment, column.role(), source, column.x(), y, column.z(), flowX, flowZ);
|
HydrologyFeatureRef feature = features.feature(segment, column.role(), source, column.x(), y, column.z(), flowX, flowZ);
|
||||||
|
String biomeOverride = pool ? poolBiome == null ? column.terrain().parentBiomeKey() : poolBiome : null;
|
||||||
columns.add(new SurfaceLayerColumn(
|
columns.add(new SurfaceLayerColumn(
|
||||||
column.x(),
|
column.x(),
|
||||||
column.z(),
|
column.z(),
|
||||||
column.terrain(),
|
column.terrain(),
|
||||||
pool ? poolLayer(feature, column, course.profileKey(), poolBiome) : layer(feature, column, course.profileKey()),
|
layer(feature, column, course.profileKey(), biomeOverride),
|
||||||
column.role(),
|
column.role(),
|
||||||
column.apron()
|
column.apron()
|
||||||
));
|
));
|
||||||
@@ -103,40 +104,6 @@ public final class SurfaceFootprintCompiler {
|
|||||||
return new SurfaceFootprint(columns, field.uncontainedWetCells());
|
return new SurfaceFootprint(columns, field.uncontainedWetCells());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A pool's bowl and rim carry the pool biome when one is configured, otherwise the surrounding biome. */
|
|
||||||
private static HydrologyColumnLayer poolLayer(
|
|
||||||
HydrologyFeatureRef feature,
|
|
||||||
SurfaceColumn column,
|
|
||||||
String profileKey,
|
|
||||||
String poolBiome
|
|
||||||
) {
|
|
||||||
HydrologyTerrainSample terrain = column.terrain();
|
|
||||||
String biome = poolBiome == null ? terrain.parentBiomeKey() : poolBiome;
|
|
||||||
boolean channel = column.role() == SurfaceRole.CHANNEL;
|
|
||||||
boolean shore = column.role() == SurfaceRole.SHORE;
|
|
||||||
return new HydrologyColumnLayer(
|
|
||||||
feature,
|
|
||||||
column.height(),
|
|
||||||
column.headY(),
|
|
||||||
column.headY(),
|
|
||||||
channel,
|
|
||||||
shore,
|
|
||||||
!channel,
|
|
||||||
channel,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
channel,
|
|
||||||
false,
|
|
||||||
profileKey,
|
|
||||||
biome,
|
|
||||||
biome,
|
|
||||||
biome,
|
|
||||||
biome,
|
|
||||||
terrain.floodedCaveBiomeKey()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChannelProfile poolProfile(HydraulicSegment segment, SurfaceCenterline centerline) {
|
private ChannelProfile poolProfile(HydraulicSegment segment, SurfaceCenterline centerline) {
|
||||||
int count = centerline.size();
|
int count = centerline.size();
|
||||||
double[] halfWidth = new double[count];
|
double[] halfWidth = new double[count];
|
||||||
@@ -160,9 +127,15 @@ public final class SurfaceFootprintCompiler {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HydrologyColumnLayer layer(HydrologyFeatureRef feature, SurfaceColumn column, String profileKey) {
|
/** A pool's bowl and rim carry the pool biome when one is configured, otherwise the surrounding biome. */
|
||||||
|
private static HydrologyColumnLayer layer(
|
||||||
|
HydrologyFeatureRef feature,
|
||||||
|
SurfaceColumn column,
|
||||||
|
String profileKey,
|
||||||
|
String biomeOverride
|
||||||
|
) {
|
||||||
HydrologyTerrainSample terrain = column.terrain();
|
HydrologyTerrainSample terrain = column.terrain();
|
||||||
if (column.apron()) {
|
if (column.apron() && biomeOverride == null) {
|
||||||
return new HydrologyColumnLayer(
|
return new HydrologyColumnLayer(
|
||||||
feature,
|
feature,
|
||||||
column.headY(),
|
column.headY(),
|
||||||
@@ -194,10 +167,10 @@ public final class SurfaceFootprintCompiler {
|
|||||||
channel,
|
channel,
|
||||||
false,
|
false,
|
||||||
profileKey,
|
profileKey,
|
||||||
terrain.surfaceBiomeKey(),
|
biomeOverride == null ? terrain.surfaceBiomeKey() : biomeOverride,
|
||||||
terrain.mouthBiomeKey(),
|
biomeOverride == null ? terrain.mouthBiomeKey() : biomeOverride,
|
||||||
terrain.shoreBiomeKey(),
|
biomeOverride == null ? terrain.shoreBiomeKey() : biomeOverride,
|
||||||
terrain.bankBiomeKey(),
|
biomeOverride == null ? terrain.bankBiomeKey() : biomeOverride,
|
||||||
terrain.floodedCaveBiomeKey()
|
terrain.floodedCaveBiomeKey()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
|
public class HydrologyRouteLengthsTest {
|
||||||
|
@Test
|
||||||
|
public void branchingRoutesMatchIndependentDownstreamWalks() {
|
||||||
|
Random random = new Random(912541L);
|
||||||
|
for (int width : new int[]{2, 3, 9, 17, 33, 65}) {
|
||||||
|
for (int spacing : new int[]{4, 16, 64}) {
|
||||||
|
for (int maximumLength : new int[]{63, 128, 257, 4096}) {
|
||||||
|
int[] parent = forest(width, random);
|
||||||
|
HydrologyPlannerSettings.Routing routing = routing(spacing, maximumLength);
|
||||||
|
assertArrayEquals(walks(parent, width, routing),
|
||||||
|
HydrologyPlanner.routeLengths(parent, width, routing));
|
||||||
|
reverse(parent);
|
||||||
|
assertArrayEquals(walks(parent, width, routing),
|
||||||
|
HydrologyPlanner.routeLengths(parent, width, routing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exactLengthLimitAndMissingOutletsKeepTheirBounds() {
|
||||||
|
int[] parent = {1, 2, 3, 4, -1, -1, -1, -1, -1};
|
||||||
|
assertArrayEquals(new int[]{Integer.MAX_VALUE, 12, 8, 4, 0, 0, 0, 0, 0},
|
||||||
|
HydrologyPlanner.routeLengths(parent, 9, routing(4, 12)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void longRoutesDoNotUseTheCallStack() {
|
||||||
|
int width = 257;
|
||||||
|
int[] parent = new int[width * width];
|
||||||
|
for (int row = 0; row < width; row++) {
|
||||||
|
for (int column = 0; column < width; column++) {
|
||||||
|
int index = row * width + column;
|
||||||
|
int direction = row % 2 == 0 ? 1 : -1;
|
||||||
|
int nextColumn = column + direction;
|
||||||
|
parent[index] = nextColumn >= 0 && nextColumn < width ? index + direction : index + width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent[parent.length - 1] = -1;
|
||||||
|
int[] lengths = HydrologyPlanner.routeLengths(parent, width, routing(4, Integer.MAX_VALUE));
|
||||||
|
assertEquals((parent.length - 1) * 4, lengths[0]);
|
||||||
|
assertEquals(0, lengths[lengths.length - 1]);
|
||||||
|
reverse(parent);
|
||||||
|
lengths = HydrologyPlanner.routeLengths(parent, width, routing(4, Integer.MAX_VALUE));
|
||||||
|
assertEquals((parent.length - 1) * 4, lengths[lengths.length - 1]);
|
||||||
|
assertEquals(0, lengths[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cyclesFailBeforeTheyCanStallPlanning() {
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> HydrologyPlanner.routeLengths(new int[]{1, 0}, 2, routing(4, 128)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] forest(int width, Random random) {
|
||||||
|
int[] parent = new int[width * width];
|
||||||
|
Arrays.fill(parent, -1);
|
||||||
|
int[] candidates = new int[4];
|
||||||
|
for (int index = 1; index < parent.length; index++) {
|
||||||
|
int x = index % width;
|
||||||
|
int z = index / width;
|
||||||
|
int count = 0;
|
||||||
|
if (x > 0) {
|
||||||
|
candidates[count++] = index - 1;
|
||||||
|
}
|
||||||
|
if (z > 0) {
|
||||||
|
candidates[count++] = index - width;
|
||||||
|
if (x > 0) {
|
||||||
|
candidates[count++] = index - width - 1;
|
||||||
|
}
|
||||||
|
if (x + 1 < width) {
|
||||||
|
candidates[count++] = index - width + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (random.nextInt(16) != 0) {
|
||||||
|
parent[index] = candidates[random.nextInt(count)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reverse(int[] parent) {
|
||||||
|
int[] original = parent.clone();
|
||||||
|
for (int index = 0; index < parent.length; index++) {
|
||||||
|
int next = original[parent.length - 1 - index];
|
||||||
|
parent[index] = next < 0 ? -1 : parent.length - 1 - next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] walks(int[] parent, int width, HydrologyPlannerSettings.Routing routing) {
|
||||||
|
int[] lengths = new int[parent.length];
|
||||||
|
for (int source = 0; source < parent.length; source++) {
|
||||||
|
double distance = 0D;
|
||||||
|
int steps = 0;
|
||||||
|
int current = source;
|
||||||
|
while (parent[current] >= 0) {
|
||||||
|
int next = parent[current];
|
||||||
|
distance += StrictMath.hypot((current % width - next % width) * routing.sampleSpacing(),
|
||||||
|
(current / width - next / width) * routing.sampleSpacing());
|
||||||
|
if (distance > routing.maximumRouteLength() || ++steps > routing.maximumRouteNodes()) {
|
||||||
|
distance = Integer.MAX_VALUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
lengths[source] = (int) StrictMath.ceil(distance);
|
||||||
|
}
|
||||||
|
return lengths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HydrologyPlannerSettings.Routing routing(int spacing, int maximumLength) {
|
||||||
|
return new HydrologyPlannerSettings.Routing(spacing * 8, spacing, 1_000_000, maximumLength,
|
||||||
|
0, 0, 1D, 1D, 1D, 1D, 1D, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class RiverFootprintTest {
|
||||||
|
@Test
|
||||||
|
public void primitiveIndexKeepsSignedKeyOrderAndFullCoordinateRange() {
|
||||||
|
Random random = new Random(836412L);
|
||||||
|
HashMap<Long, HydrologyColumnSample> input = new HashMap<>();
|
||||||
|
for (int index = 0; index < 2_000; index++) {
|
||||||
|
HydrologyColumnSample column = column(random.nextInt(), random.nextInt());
|
||||||
|
input.put(RiverFootprint.pack(column.x(), column.z()), column);
|
||||||
|
}
|
||||||
|
for (int x : new int[]{Integer.MIN_VALUE, -1, 0, 1, Integer.MAX_VALUE}) {
|
||||||
|
for (int z : new int[]{Integer.MIN_VALUE, -1, 0, 1, Integer.MAX_VALUE}) {
|
||||||
|
input.put(RiverFootprint.pack(x, z), column(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TreeMap<Long, HydrologyColumnSample> expected = new TreeMap<>(input);
|
||||||
|
RiverFootprint footprint = new RiverFootprint(input);
|
||||||
|
input.clear();
|
||||||
|
assertEquals(new ArrayList<>(expected.keySet()), new ArrayList<>(footprint.columns().keySet()));
|
||||||
|
assertEquals(expected, footprint.columns());
|
||||||
|
for (HydrologyColumnSample column : expected.values()) {
|
||||||
|
assertSame(column, footprint.sample(column.x(), column.z()).orElseThrow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exposedEntriesCannotMutateTheLookupIndex() {
|
||||||
|
HydrologyColumnSample original = column(-5, 9);
|
||||||
|
RiverFootprint footprint = new RiverFootprint(Map.of(RiverFootprint.pack(-5, 9), original));
|
||||||
|
Map.Entry<Long, HydrologyColumnSample> entry = footprint.columns().entrySet().iterator().next();
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> entry.setValue(column(-5, 9)));
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> footprint.columns().values().clear());
|
||||||
|
assertSame(original, footprint.sample(-5, 9).orElseThrow());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void areaQueriesKeepBothLookupPathsAndExclusiveBounds() {
|
||||||
|
HashMap<Long, HydrologyColumnSample> input = new HashMap<>();
|
||||||
|
for (int x = -8; x < 8; x++) {
|
||||||
|
for (int z = -8; z < 8; z++) {
|
||||||
|
input.put(RiverFootprint.pack(x, z), column(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RiverFootprint footprint = new RiverFootprint(input);
|
||||||
|
assertEquals(List.of(column(-1, -1), column(0, -1), column(-1, 0), column(0, 0)),
|
||||||
|
footprint.columnsIn(-1, -1, 1, 1));
|
||||||
|
assertEquals(new ArrayList<>(new TreeMap<>(input).values()),
|
||||||
|
footprint.columnsIn(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE));
|
||||||
|
assertTrue(footprint.columnsIn(0, 0, 0, 1).isEmpty());
|
||||||
|
assertTrue(footprint.columnsIn(100, 100, 101, 101).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HydrologyColumnSample column(int x, int z) {
|
||||||
|
return new HydrologyColumnSample(x, z, 90, 63, false, "parent", List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user