mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 14:30:14 +00:00
Improved riverbed generation
- Pond levels follow intersecting lower channels - Pond sampling covers requested flood margins - Water steps align with sea level - River crests grade into surrounding beds - Riverbeds include varied cross sections and finer noise - Submerged soils use stable block types - Hydrology probes inspect wet bed geometry
This commit is contained in:
1 parent
494cdb1a5c
commit
75837d757b
25 files changed
+1566
-144
No files matched your search
@@ -11,6 +11,7 @@ import art.arcane.iris.generation.hydrology.grid.FlowAccumulator;
|
|||||||
import art.arcane.iris.generation.hydrology.grid.HeightLattice;
|
import art.arcane.iris.generation.hydrology.grid.HeightLattice;
|
||||||
import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer;
|
import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer;
|
||||||
import art.arcane.iris.generation.hydrology.grid.PriorityFlood;
|
import art.arcane.iris.generation.hydrology.grid.PriorityFlood;
|
||||||
|
import art.arcane.iris.generation.hydrology.raster.PondChannelSettlement;
|
||||||
import art.arcane.iris.generation.hydrology.underground.SeaInlet;
|
import art.arcane.iris.generation.hydrology.underground.SeaInlet;
|
||||||
import art.arcane.iris.generation.hydrology.underground.SeaInletPlanner;
|
import art.arcane.iris.generation.hydrology.underground.SeaInletPlanner;
|
||||||
import art.arcane.iris.generation.hydrology.water.CupSet;
|
import art.arcane.iris.generation.hydrology.water.CupSet;
|
||||||
@@ -108,16 +109,16 @@ public final class WindowPlanner {
|
|||||||
}
|
}
|
||||||
List<Pond> water = new ArrayList<>(ponds.ponds(raw, drainage.filled(), drainage.basins(), lakes, fine, cups));
|
List<Pond> water = new ArrayList<>(ponds.ponds(raw, drainage.filled(), drainage.basins(), lakes, fine, cups));
|
||||||
water.addAll(lakes);
|
water.addAll(lakes);
|
||||||
ShapedNetwork network = shaper.shape(drainage.surface(), drainage.receivers(), drainage.accumulation(), drainage.channels(), water,
|
ShapedNetwork network = PondChannelSettlement.settle(shaper.shape(drainage.surface(), drainage.receivers(), drainage.accumulation(),
|
||||||
fine, settings.seed());
|
drainage.channels(), water, fine, settings.seed()), settings.seaLevel());
|
||||||
int passes = water.size() + 1;
|
long passes = settlementPasses(water);
|
||||||
for (int pass = 1; lowered(water, network.ponds()); pass++) {
|
for (long pass = 1; lowered(water, network.ponds()); pass++) {
|
||||||
if (pass >= passes) {
|
if (pass >= passes) {
|
||||||
throw new IllegalStateException("Window " + key + " ponds did not settle within " + passes + " shaping passes");
|
throw new IllegalStateException("Window " + key + " ponds did not settle within " + passes + " shaping passes");
|
||||||
}
|
}
|
||||||
water = replanned(settled(raw, cups, water, network.ponds()));
|
water = replanned(settled(raw, cups, water, network.ponds()));
|
||||||
network = shaper.shape(drainage.surface(), drainage.receivers(), drainage.accumulation(), drainage.channels(), water, fine,
|
network = PondChannelSettlement.settle(shaper.shape(drainage.surface(), drainage.receivers(), drainage.accumulation(),
|
||||||
settings.seed());
|
drainage.channels(), water, fine, settings.seed()), settings.seaLevel());
|
||||||
}
|
}
|
||||||
List<Reach> reaches = network.reaches();
|
List<Reach> reaches = network.reaches();
|
||||||
List<Pond> windowPonds = windowPonds(network.ponds());
|
List<Pond> windowPonds = windowPonds(network.ponds());
|
||||||
@@ -263,6 +264,16 @@ public final class WindowPlanner {
|
|||||||
return settled;
|
return settled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long settlementPasses(List<Pond> water) {
|
||||||
|
long passes = 1L;
|
||||||
|
for (Pond pond : water) {
|
||||||
|
if (pond.kind() != PondKind.LAKE) {
|
||||||
|
passes += Math.max(0L, (long) pond.level() - settings.seaLevel()) + 1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return passes;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean lowered(List<Pond> planned, List<Pond> shaped) {
|
private static boolean lowered(List<Pond> planned, List<Pond> shaped) {
|
||||||
for (int index = 0; index < shaped.size(); index++) {
|
for (int index = 0; index < shaped.size(); index++) {
|
||||||
if (shaped.get(index).level() < planned.get(index).level()) {
|
if (shaped.get(index).level() < planned.get(index).level()) {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package art.arcane.iris.generation.hydrology.raster;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.grid.CellHash;
|
||||||
|
|
||||||
|
final class BedProfile {
|
||||||
|
private static final long SALT_BROAD = 0x42454442524f4144L;
|
||||||
|
private static final long SALT_DETAIL = 0x4245444445544149L;
|
||||||
|
private static final long SALT_TEXTURE = 0x4245445445585455L;
|
||||||
|
|
||||||
|
private BedProfile() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static double variation(long seed, int x, int z) {
|
||||||
|
return 0.65D * noise(seed, SALT_BROAD, x, z, 32) + 0.35D * noise(seed, SALT_DETAIL, x, z, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static double detail(long seed, int x, int z) {
|
||||||
|
return 0.35D * noise(seed, SALT_TEXTURE, x, z, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static double position(double u, double variation) {
|
||||||
|
double bounded = Math.clamp(u, 0D, 1D);
|
||||||
|
return bounded + 0.8D * variation * bounded * (1D - bounded);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int channelDepth(double depth, double ratio, double variation, double detail) {
|
||||||
|
double across = position(ratio, -variation);
|
||||||
|
double bowl = 1D - across * across;
|
||||||
|
double cut = bowl * (depth + variation * Math.min(2D, depth * 0.3D) + detail);
|
||||||
|
return (int) Math.clamp(Math.round(cut), 1L, Math.max(1L, Math.round(depth)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int pondDepth(int maximum, double u, double variation, double detail) {
|
||||||
|
double across = position(u, variation);
|
||||||
|
double bowl = across * across * (3D - 2D * across);
|
||||||
|
double cut = 1D + bowl * (maximum - 1D + variation * Math.min(2D, (maximum - 1D) * 0.3D) + detail);
|
||||||
|
return (int) Math.clamp(Math.round(cut), 1L, maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double noise(long seed, long salt, int x, int z, int scale) {
|
||||||
|
int ix = Math.floorDiv(x, scale);
|
||||||
|
int iz = Math.floorDiv(z, scale);
|
||||||
|
double u = fade(Math.floorMod(x, scale) / (double) scale);
|
||||||
|
double v = fade(Math.floorMod(z, scale) / (double) scale);
|
||||||
|
double a = CellHash.unit(CellHash.mix(seed, salt, ix, iz));
|
||||||
|
double b = CellHash.unit(CellHash.mix(seed, salt, ix + 1, iz));
|
||||||
|
double c = CellHash.unit(CellHash.mix(seed, salt, ix, iz + 1));
|
||||||
|
double d = CellHash.unit(CellHash.mix(seed, salt, ix + 1, iz + 1));
|
||||||
|
return 2D * ((a + (b - a) * u) * (1D - v) + (c + (d - c) * u) * v) - 1D;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double fade(double value) {
|
||||||
|
return value * value * value * (value * (value * 6D - 15D) + 10D);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,7 +50,10 @@ final class ChunkFeatures {
|
|||||||
List<SeaInlet> inlets = new ArrayList<>();
|
List<SeaInlet> inlets = new ArrayList<>();
|
||||||
for (IndexedWindow window : windows) {
|
for (IndexedWindow window : windows) {
|
||||||
for (SeaInlet inlet : window.plan().inlets()) {
|
for (SeaInlet inlet : window.plan().inlets()) {
|
||||||
if (SeaInletRasterizer.touches(inlet, chunkX, chunkZ)) {
|
int[] bounds = SeaInletRasterizer.bounds(inlet);
|
||||||
|
int padding = RiffleProfile.RADIUS + 1;
|
||||||
|
if (bounds[0] <= chunkX * CHUNK + CHUNK - 1 + padding && bounds[2] >= chunkX * CHUNK - padding
|
||||||
|
&& bounds[1] <= chunkZ * CHUNK + CHUNK - 1 + padding && bounds[3] >= chunkZ * CHUNK - padding) {
|
||||||
inlets.add(inlet);
|
inlets.add(inlet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +86,25 @@ final class ChunkFeatures {
|
|||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean hasOnlyLevel(int head, int seaLevel) {
|
||||||
|
for (Pond pond : ponds) {
|
||||||
|
if (pond.level() - Math.floorMod(pond.level() - seaLevel, 2) != head) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Pond lake : lakes) {
|
||||||
|
if (lake.level() - Math.floorMod(lake.level() - seaLevel, 2) != head) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (SeaInlet inlet : inlets) {
|
||||||
|
if (inlet.waterLevel() - Math.floorMod(inlet.waterLevel() - seaLevel, 2) != head) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segments.hasOnlyLevel(head, seaLevel);
|
||||||
|
}
|
||||||
|
|
||||||
int ponds() {
|
int ponds() {
|
||||||
return ponds.size();
|
return ponds.size();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ public final class ChunkRasterizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static List<RegionKey> lakeRegions(int chunkX, int chunkZ) {
|
public static List<RegionKey> lakeRegions(int chunkX, int chunkZ) {
|
||||||
return RegionKey.lakeOwners(chunkX * CHUNK, chunkZ * CHUNK, chunkX * CHUNK + CHUNK - 1, chunkZ * CHUNK + CHUNK - 1);
|
int halo = RiffleProfile.RADIUS + 1;
|
||||||
|
return RegionKey.lakeOwners(chunkX * CHUNK - halo, chunkZ * CHUNK - halo,
|
||||||
|
chunkX * CHUNK + CHUNK - 1 + halo, chunkZ * CHUNK + CHUNK - 1 + halo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChunkWaterRaster rasterize(int chunkX, int chunkZ, IntBinaryOperator naturalHeight, List<IndexedWindow> windows,
|
public ChunkWaterRaster rasterize(int chunkX, int chunkZ, IntBinaryOperator naturalHeight, List<IndexedWindow> windows,
|
||||||
|
|||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
package art.arcane.iris.generation.hydrology.raster;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.water.Pond;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.PondKind;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.PondMask;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.ShapedNetwork;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.BitSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class PondChannelSettlement {
|
||||||
|
private static final int[][] SIDES = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
|
|
||||||
|
private PondChannelSettlement() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShapedNetwork settle(ShapedNetwork network, int seaLevel) {
|
||||||
|
int[] initial = new int[network.ponds().size()];
|
||||||
|
for (int index = 0; index < initial.length; index++) {
|
||||||
|
initial[index] = network.ponds().get(index).level();
|
||||||
|
}
|
||||||
|
int[] levels = levels(network.reaches(), network.ponds(), initial, seaLevel);
|
||||||
|
List<Pond> water = new ArrayList<>(initial.length);
|
||||||
|
for (int index = 0; index < initial.length; index++) {
|
||||||
|
Pond pond = network.ponds().get(index);
|
||||||
|
water.add(levels[index] == initial[index] ? pond : new Pond(pond.id(), pond.kind(), levels[index], pond.floor(), pond.spillX(),
|
||||||
|
pond.spillZ(), pond.spillHeight(), pond.mask(), pond.outletReachId(), pond.damRaise(), pond.fluid(), pond.floorX(), pond.floorZ()));
|
||||||
|
}
|
||||||
|
return new ShapedNetwork(network.reaches(), water);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int[] levels(List<Reach> reaches, List<Pond> ponds, int[] initial, int seaLevel) {
|
||||||
|
int[] levels = initial.clone();
|
||||||
|
if (reaches.isEmpty() || ponds.isEmpty()) {
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
boolean eligible = false;
|
||||||
|
for (int pond = 0; pond < ponds.size() && !eligible; pond++) {
|
||||||
|
eligible = ponds.get(pond).kind() != PondKind.LAKE && levels[pond] > seaLevel;
|
||||||
|
}
|
||||||
|
if (!eligible) {
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
ReachIndex index = ReachIndex.build(reaches, List.of());
|
||||||
|
List<Reach> nearby = new ArrayList<>();
|
||||||
|
List<Pond> unused = new ArrayList<>();
|
||||||
|
for (int pond = 0; pond < ponds.size(); pond++) {
|
||||||
|
Pond water = ponds.get(pond);
|
||||||
|
if (water.kind() == PondKind.LAKE || levels[pond] <= seaLevel || water.mask().width() == 0 || water.mask().depth() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Bounds bounds = Bounds.of(water.mask());
|
||||||
|
int chunkX = (bounds.minX() + (bounds.maxX() - bounds.minX()) / 2) >> 4;
|
||||||
|
int chunkZ = (bounds.minZ() + (bounds.maxZ() - bounds.minZ()) / 2) >> 4;
|
||||||
|
int halo = Math.max(0, Math.max(Math.max((chunkX << 4) - bounds.minX(), bounds.maxX() - (chunkX << 4) - 15),
|
||||||
|
Math.max((chunkZ << 4) - bounds.minZ(), bounds.maxZ() - (chunkZ << 4) - 15))) + 1;
|
||||||
|
nearby.clear();
|
||||||
|
index.candidates(chunkX, chunkZ, halo, nearby, unused);
|
||||||
|
if (!nearby.isEmpty()) {
|
||||||
|
levels[pond] = lower(nearby, water.mask(), bounds, levels[pond], seaLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int lower(List<Reach> reaches, PondMask mask, Bounds bounds, int level, int seaLevel) {
|
||||||
|
ReachSegments segments = new ReachSegments(64);
|
||||||
|
for (Reach reach : reaches) {
|
||||||
|
segments.addNear(reach, bounds.minX() - 1, bounds.minZ() - 1, bounds.maxX() + 1, bounds.maxZ() + 1, 0f);
|
||||||
|
}
|
||||||
|
ReachCursor cursor = new ReachCursor();
|
||||||
|
int width = bounds.maxX() - bounds.minX() + 1;
|
||||||
|
BitSet visited = new BitSet(width * (bounds.maxZ() - bounds.minZ() + 1));
|
||||||
|
for (Reach reach : reaches) {
|
||||||
|
for (int segment = 0; segment + 1 < reach.xs().length; segment++) {
|
||||||
|
if (Math.min(head(reach.grade()[segment], seaLevel), head(reach.grade()[segment + 1], seaLevel)) >= head(level, seaLevel)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float radius = Math.max(reach.halfWidth()[segment], reach.halfWidth()[segment + 1]) + 1f;
|
||||||
|
int minX = Math.max(bounds.minX(), (int) Math.ceil(Math.min(reach.xs()[segment], reach.xs()[segment + 1]) - radius));
|
||||||
|
int minZ = Math.max(bounds.minZ(), (int) Math.ceil(Math.min(reach.zs()[segment], reach.zs()[segment + 1]) - radius));
|
||||||
|
int maxX = Math.min(bounds.maxX(), (int) Math.floor(Math.max(reach.xs()[segment], reach.xs()[segment + 1]) + radius));
|
||||||
|
int maxZ = Math.min(bounds.maxZ(), (int) Math.floor(Math.max(reach.zs()[segment], reach.zs()[segment + 1]) + radius));
|
||||||
|
for (int z = minZ; z <= maxZ; z++) {
|
||||||
|
for (int x = minX; x <= maxX; x++) {
|
||||||
|
int cell = (z - bounds.minZ()) * width + x - bounds.minX();
|
||||||
|
if (visited.get(cell) || !mask.contains(x, z)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited.set(cell);
|
||||||
|
level = Math.min(level, touchingHead(segments, cursor, x, z, seaLevel, level));
|
||||||
|
for (int[] side : SIDES) {
|
||||||
|
int nx = x + side[0];
|
||||||
|
int nz = z + side[1];
|
||||||
|
if (!mask.contains(nx, nz)) {
|
||||||
|
level = Math.min(level, touchingHead(segments, cursor, nx, nz, seaLevel, level));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int touchingHead(ReachSegments segments, ReachCursor cursor, int x, int z, int seaLevel, int level) {
|
||||||
|
segments.nearest(x, z, ReachSegments.Scope.CHANNEL, cursor);
|
||||||
|
ReachCursor.Choice channel = cursor.channel;
|
||||||
|
if (channel.reach == null) {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
int head = head(channel.reach.grade()[channel.point()], seaLevel);
|
||||||
|
return head < head(level, seaLevel) ? head : level;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int head(int level, int seaLevel) {
|
||||||
|
return level - Math.floorMod(level - seaLevel, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Bounds(int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
private static Bounds of(PondMask mask) {
|
||||||
|
int minX = mask.originX() - (mask.step() >> 1);
|
||||||
|
int minZ = mask.originZ() - (mask.step() >> 1);
|
||||||
|
return new Bounds(minX, minZ, minX + mask.width() * mask.step() - 1, minZ + mask.depth() * mask.step() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
-25
@@ -67,6 +67,7 @@ final class RasterComposer {
|
|||||||
column(cell);
|
column(cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
gradeRiffles();
|
||||||
raiseDams();
|
raiseDams();
|
||||||
bermEdges();
|
bermEdges();
|
||||||
measureLakeShores();
|
measureLakeShores();
|
||||||
@@ -80,8 +81,8 @@ final class RasterComposer {
|
|||||||
int ground = natural[cell];
|
int ground = natural[cell];
|
||||||
SeaInlet inlet = features.openInletAt(x, z, inletColumn);
|
SeaInlet inlet = features.openInletAt(x, z, inletColumn);
|
||||||
if (inlet != null) {
|
if (inlet != null) {
|
||||||
head[cell] = inlet.waterLevel();
|
head[cell] = level(inlet.waterLevel());
|
||||||
terrain[cell] = Math.min(ground, inletColumn[0]);
|
terrain[cell] = Math.min(Math.min(ground, inletColumn[0]), head[cell] - 1);
|
||||||
role[cell] = WaterRole.INLET;
|
role[cell] = WaterRole.INLET;
|
||||||
feature[cell] = inlet.id();
|
feature[cell] = inlet.id();
|
||||||
fluid[cell] = HydrologyCaveCell.SEA_FLUID;
|
fluid[cell] = HydrologyCaveCell.SEA_FLUID;
|
||||||
@@ -91,72 +92,77 @@ final class RasterComposer {
|
|||||||
int pondIndex = lake >= 0 ? -1 : features.pondAt(x, z);
|
int pondIndex = lake >= 0 ? -1 : features.pondAt(x, z);
|
||||||
boolean shore = interior(cell) && lake < 0 && pondIndex < 0;
|
boolean shore = interior(cell) && lake < 0 && pondIndex < 0;
|
||||||
features.segments().nearest(x, z, shore ? ReachSegments.Scope.SHORE : ReachSegments.Scope.CHANNEL, cursor);
|
features.segments().nearest(x, z, shore ? ReachSegments.Scope.SHORE : ReachSegments.Scope.CHANNEL, cursor);
|
||||||
|
double variation = BedProfile.variation(settings.seed(), x, z);
|
||||||
if (lake >= 0) {
|
if (lake >= 0) {
|
||||||
Pond water = features.lake(lake);
|
Pond water = features.lake(lake);
|
||||||
standing(cell, water, trench(ground, water.level(), ground));
|
standing(cell, water, trench(ground, level(water.level()), ground, variation, BedProfile.detail(settings.seed(), x, z)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pondIndex >= 0) {
|
if (pondIndex >= 0) {
|
||||||
Pond water = features.pond(pondIndex);
|
Pond water = features.pond(pondIndex);
|
||||||
pond[cell] = pondIndex;
|
pond[cell] = pondIndex;
|
||||||
int floor = interior(cell) ? pondFloor(pondIndex, water.level(), x, z, ground) : ground;
|
double detail = BedProfile.detail(settings.seed(), x, z);
|
||||||
standing(cell, water, trench(floor, water.level(), ground));
|
int floor = interior(cell) ? pondFloor(pondIndex, level(water.level()), x, z, ground, variation, detail) : ground;
|
||||||
|
standing(cell, water, trench(floor, level(water.level()), ground, variation, detail));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (cursor.channel.reach != null) {
|
if (cursor.channel.reach != null) {
|
||||||
channel(cell, ground);
|
channel(cell, ground, variation, BedProfile.detail(settings.seed(), x, z));
|
||||||
} else if (interior(cell)) {
|
} else if (interior(cell)) {
|
||||||
dry(cell, x, z, ground);
|
dry(cell, x, z, ground, variation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void standing(int cell, Pond water, int floor) {
|
private void standing(int cell, Pond water, int floor) {
|
||||||
head[cell] = water.level();
|
head[cell] = level(water.level());
|
||||||
terrain[cell] = floor;
|
terrain[cell] = Math.min(floor, head[cell] - 1);
|
||||||
role[cell] = WaterRole.POND;
|
role[cell] = WaterRole.POND;
|
||||||
feature[cell] = water.id();
|
feature[cell] = water.id();
|
||||||
fluid[cell] = water.fluid();
|
fluid[cell] = water.fluid();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int trench(int floor, int level, int ground) {
|
private int trench(int floor, int level, int ground, double variation, double detail) {
|
||||||
ReachCursor.Choice hit = cursor.channel;
|
ReachCursor.Choice hit = cursor.channel;
|
||||||
if (hit.reach == null) {
|
if (hit.reach == null) {
|
||||||
return floor;
|
return floor;
|
||||||
}
|
}
|
||||||
return Math.min(floor, bed(hit, Math.min(level, hit.reach.grade()[hit.point()]), ground));
|
return Math.min(floor, bed(hit, Math.min(level, level(hit.reach.grade()[hit.point()])), ground, variation, detail));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void channel(int cell, int ground) {
|
private void channel(int cell, int ground, double variation, double detail) {
|
||||||
ReachCursor.Choice hit = cursor.channel;
|
ReachCursor.Choice hit = cursor.channel;
|
||||||
Reach reach = hit.reach;
|
Reach reach = hit.reach;
|
||||||
int point = hit.point();
|
int point = hit.point();
|
||||||
head[cell] = reach.grade()[point];
|
head[cell] = level(reach.grade()[point]);
|
||||||
terrain[cell] = bed(hit, head[cell], ground);
|
terrain[cell] = bed(hit, head[cell], ground, variation, detail);
|
||||||
role[cell] = role(reach.style()[point]);
|
role[cell] = role(reach.style()[point]);
|
||||||
feature[cell] = reach.id();
|
feature[cell] = reach.id();
|
||||||
tier[cell] = reach.tier();
|
tier[cell] = reach.tier();
|
||||||
fluid[cell] = reach.tier().fluid();
|
fluid[cell] = reach.tier().fluid();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int bed(ReachCursor.Choice hit, int surface, int ground) {
|
private static int bed(ReachCursor.Choice hit, int surface, int ground, double variation, double detail) {
|
||||||
double width = hit.halfWidth();
|
double width = hit.halfWidth();
|
||||||
double ratio = width > 0D ? hit.distance / width : 0D;
|
double ratio = width > 0D ? hit.distance / width : 0D;
|
||||||
long cut = Math.max(1L, Math.round(hit.depth() * (1D - ratio * ratio)));
|
long cut = BedProfile.channelDepth(hit.depth(), ratio, variation, detail);
|
||||||
return (int) Math.min(ground, surface - cut);
|
return (int) Math.min(ground, surface - cut);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dry(int cell, int x, int z, int ground) {
|
private void dry(int cell, int x, int z, int ground, double variation) {
|
||||||
int cut = ground;
|
int cut = ground;
|
||||||
int raise = Integer.MIN_VALUE;
|
int raise = Integer.MIN_VALUE;
|
||||||
double bankEdge = Double.POSITIVE_INFINITY;
|
double bankEdge = Double.POSITIVE_INFINITY;
|
||||||
ReachCursor.Choice bank = cursor.bank;
|
ReachCursor.Choice bank = cursor.bank;
|
||||||
if (bank.reach != null) {
|
if (bank.reach != null) {
|
||||||
HydrologyTier bankTier = bank.reach.tier();
|
HydrologyTier bankTier = bank.reach.tier();
|
||||||
int surface = bank.reach.grade()[bank.point()];
|
int surface = level(bank.reach.grade()[bank.point()]);
|
||||||
bankEdge = bank.distance - bank.halfWidth();
|
bankEdge = bank.distance - bank.halfWidth();
|
||||||
double u = bankEdge / bankTier.bankBand();
|
double u = bankEdge / bankTier.bankBand();
|
||||||
|
if (bankTier.bankStyle() != IrisBankStyle.CLIFF && bankTier.bankStyle() != IrisBankStyle.TERRACED) {
|
||||||
|
u = BedProfile.position(u, -variation);
|
||||||
|
}
|
||||||
if (ground >= surface) {
|
if (ground >= surface) {
|
||||||
cut = BankProfile.cut(bankTier.bankStyle(), surface, ground, u, coneSlope(cell) * bankEdge, noise(x, z));
|
cut = BankProfile.cut(bankTier.bankStyle(), surface, ground, u, coneSlope(cell) * u * bankTier.bankBand(), noise(x, z));
|
||||||
} else {
|
} else {
|
||||||
raise = BankProfile.levee(bankTier.bankStyle(), surface, ground, u, noise(x, z), settings.deformation().maxFill());
|
raise = BankProfile.levee(bankTier.bankStyle(), surface, ground, u, noise(x, z), settings.deformation().maxFill());
|
||||||
}
|
}
|
||||||
@@ -172,8 +178,8 @@ final class RasterComposer {
|
|||||||
}
|
}
|
||||||
int taper = settings.ponds().shore().taper();
|
int taper = settings.ponds().shore().taper();
|
||||||
boolean tapered = nearPond >= 0 && pondEdge <= taper;
|
boolean tapered = nearPond >= 0 && pondEdge <= taper;
|
||||||
if (tapered && ground > features.pond(nearPond).level()) {
|
if (tapered && ground > level(features.pond(nearPond).level())) {
|
||||||
cut = Math.min(cut, taperCut(features.pond(nearPond).level(), ground, pondEdge / taper));
|
cut = Math.min(cut, taperCut(level(features.pond(nearPond).level()), ground, BedProfile.position(pondEdge / taper, -variation)));
|
||||||
}
|
}
|
||||||
terrain[cell] = raise == Integer.MIN_VALUE ? cut : Math.max(cut, raise);
|
terrain[cell] = raise == Integer.MIN_VALUE ? cut : Math.max(cut, raise);
|
||||||
if (bank.reach != null && (!tapered || bankEdge <= pondEdge)) {
|
if (bank.reach != null && (!tapered || bankEdge <= pondEdge)) {
|
||||||
@@ -215,11 +221,58 @@ final class RasterComposer {
|
|||||||
return (int) Math.clamp((long) Math.floor(level + rise), level, ground);
|
return (int) Math.clamp((long) Math.floor(level + rise), level, ground);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int pondFloor(int index, int level, int x, int z, int ground) {
|
private int level(int planned) {
|
||||||
|
return planned - Math.floorMod(planned - settings.seaLevel(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void gradeRiffles() {
|
||||||
|
int surface = Integer.MIN_VALUE;
|
||||||
|
for (int cell = 0; cell < CELLS && surface == Integer.MIN_VALUE; cell++) {
|
||||||
|
if (interior(cell)) {
|
||||||
|
surface = head[cell];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (surface == Integer.MIN_VALUE || features.hasOnlyLevel(surface, settings.seaLevel())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RiffleProfile riffles = new RiffleProfile((originX + 1) >> 4, (originZ + 1) >> 4, this::headAt);
|
||||||
|
for (int cell = 0; cell < CELLS; cell++) {
|
||||||
|
if (interior(cell) && head[cell] != Integer.MIN_VALUE) {
|
||||||
|
int x = originX + cell % GRID;
|
||||||
|
int z = originZ + cell / GRID;
|
||||||
|
terrain[cell] = riffles.bed(cell % GRID - 1, cell / GRID - 1, terrain[cell], head[cell],
|
||||||
|
BedProfile.variation(settings.seed(), x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int headAt(int x, int z) {
|
||||||
|
int gx = x - originX;
|
||||||
|
int gz = z - originZ;
|
||||||
|
if (gx >= 0 && gx < GRID && gz >= 0 && gz < GRID && !corner(gz * GRID + gx)) {
|
||||||
|
return head[gz * GRID + gx];
|
||||||
|
}
|
||||||
|
SeaInlet inlet = features.openInletAt(x, z, inletColumn);
|
||||||
|
if (inlet != null) {
|
||||||
|
return level(inlet.waterLevel());
|
||||||
|
}
|
||||||
|
int lake = features.lakeCovers(x, z) ? features.lakeAt(x, z, naturalAt(x, z)) : -1;
|
||||||
|
if (lake >= 0) {
|
||||||
|
return level(features.lake(lake).level());
|
||||||
|
}
|
||||||
|
int pond = features.pondAt(x, z);
|
||||||
|
if (pond >= 0) {
|
||||||
|
return level(features.pond(pond).level());
|
||||||
|
}
|
||||||
|
features.segments().nearest(x, z, ReachSegments.Scope.CHANNEL, cursor);
|
||||||
|
return cursor.channel.reach == null ? Integer.MIN_VALUE : level(cursor.channel.reach.grade()[cursor.channel.point()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int pondFloor(int index, int level, int x, int z, int ground, double variation, double detail) {
|
||||||
HydrologySettings.PondShore shore = settings.ponds().shore();
|
HydrologySettings.PondShore shore = settings.ponds().shore();
|
||||||
double bankDistance = features.pondBankDistance(index, x, z);
|
double bankDistance = features.pondBankDistance(index, x, z);
|
||||||
double u = shore.shelf() <= 0 ? 1D : Math.clamp((bankDistance - 1D) / shore.shelf(), 0D, 1D);
|
double u = shore.shelf() <= 0 ? 1D : Math.clamp((bankDistance - 1D) / shore.shelf(), 0D, 1D);
|
||||||
long bowl = Math.max(1L, Math.round(1D + (shore.maxDepth() - 1D) * BankProfile.shape(IrisBankStyle.SMOOTH, u)));
|
long bowl = BedProfile.pondDepth(shore.maxDepth(), u, variation, detail);
|
||||||
return (int) Math.min(ground, level - bowl);
|
return (int) Math.min(ground, level - bowl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,8 +289,9 @@ final class RasterComposer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Pond held = features.pond(index);
|
Pond held = features.pond(index);
|
||||||
if (held.damRaise() > 0 && terrain[cell] < held.level() && held.level() + 1 > crest) {
|
int level = level(held.level());
|
||||||
crest = held.level() + 1;
|
if (held.damRaise() > 0 && terrain[cell] < level && level + 1 > crest) {
|
||||||
|
crest = level + 1;
|
||||||
dam = held.id();
|
dam = held.id();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,18 @@ final class ReachSegments {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean hasOnlyLevel(int head, int seaLevel) {
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
int[] grade = reaches[index].grade();
|
||||||
|
int start = grade[segments[index]];
|
||||||
|
int end = grade[segments[index] + 1];
|
||||||
|
if (start - Math.floorMod(start - seaLevel, 2) != head || end - Math.floorMod(end - seaLevel, 2) != head) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void addNear(Reach reach, float minX, float minZ, float maxX, float maxZ, float outward) {
|
void addNear(Reach reach, float minX, float minZ, float maxX, float maxZ, float outward) {
|
||||||
float[] xs = reach.xs();
|
float[] xs = reach.xs();
|
||||||
float[] zs = reach.zs();
|
float[] zs = reach.zs();
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package art.arcane.iris.generation.hydrology.raster;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
final class RiffleProfile {
|
||||||
|
static final int RADIUS = 16;
|
||||||
|
private static final int CHUNK = 16;
|
||||||
|
private static final int PADDING = RADIUS + 1;
|
||||||
|
private static final int GRID = CHUNK + 2 * PADDING;
|
||||||
|
private static final int OUTPUT = CHUNK + 2;
|
||||||
|
private static final int LIMIT = 10 * RADIUS;
|
||||||
|
private static final int DRY = Integer.MIN_VALUE;
|
||||||
|
private static final int[] SIDES = {1, -1, GRID, -GRID};
|
||||||
|
|
||||||
|
private final List<Field> fields = new ArrayList<>();
|
||||||
|
|
||||||
|
RiffleProfile(int chunkX, int chunkZ, IntBinaryOperator headAt) {
|
||||||
|
int[] heads = new int[GRID * GRID];
|
||||||
|
int[] crests = new int[GRID * GRID];
|
||||||
|
int originX = (chunkX << 4) - PADDING;
|
||||||
|
int originZ = (chunkZ << 4) - PADDING;
|
||||||
|
Arrays.fill(crests, DRY);
|
||||||
|
for (int cell = 0; cell < heads.length; cell++) {
|
||||||
|
heads[cell] = headAt.applyAsInt(originX + cell % GRID, originZ + cell / GRID);
|
||||||
|
}
|
||||||
|
TreeSet<Integer> levels = new TreeSet<>();
|
||||||
|
for (int z = 1; z < GRID - 1; z++) {
|
||||||
|
for (int x = 1; x < GRID - 1; x++) {
|
||||||
|
int cell = z * GRID + x;
|
||||||
|
if (heads[cell] == DRY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int side : SIDES) {
|
||||||
|
if (heads[cell + side] != DRY && heads[cell + side] <= heads[cell] - 2) {
|
||||||
|
crests[cell] = heads[cell] - 1;
|
||||||
|
levels.add(crests[cell]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (levels.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DistanceField distances = new DistanceField(heads);
|
||||||
|
for (int crest : levels) {
|
||||||
|
distances.reset();
|
||||||
|
for (int cell = 0; cell < crests.length; cell++) {
|
||||||
|
if (crests[cell] == crest) {
|
||||||
|
distances.offer(cell, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
distances.spread();
|
||||||
|
int[] output = distances.output();
|
||||||
|
if (output != null) {
|
||||||
|
fields.add(new Field(crest, output));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int bed(int localX, int localZ, int original, int head, double variation) {
|
||||||
|
if (head == DRY) {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
int cell = (localZ + 1) * OUTPUT + localX + 1;
|
||||||
|
double raised = original;
|
||||||
|
for (Field field : fields) {
|
||||||
|
int crest = Math.min(head - 1, field.crest());
|
||||||
|
if (crest <= original) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double reach = 10D * Math.min(RADIUS, Math.max(4D, 2D * (crest - (double) original)))
|
||||||
|
* (0.925D + 0.075D * variation);
|
||||||
|
if (field.distances()[cell] >= reach) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double u = field.distances()[cell] / reach;
|
||||||
|
double blend = 1D - u * u * (3D - 2D * u);
|
||||||
|
raised = Math.max(raised, original + (crest - (double) original) * blend);
|
||||||
|
}
|
||||||
|
return (int) Math.max(original, Math.min(head - 1L, Math.round(raised)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Field(int crest, int[] distances) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DistanceField {
|
||||||
|
private final int[] heads;
|
||||||
|
private final int[] distances = new int[GRID * GRID];
|
||||||
|
private final int[] next = new int[GRID * GRID];
|
||||||
|
private final int[] previous = new int[GRID * GRID];
|
||||||
|
private final int[] first = new int[LIMIT];
|
||||||
|
|
||||||
|
private DistanceField(int[] heads) {
|
||||||
|
this.heads = heads;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reset() {
|
||||||
|
Arrays.fill(distances, LIMIT);
|
||||||
|
Arrays.fill(next, -1);
|
||||||
|
Arrays.fill(previous, -1);
|
||||||
|
Arrays.fill(first, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void offer(int cell, int distance) {
|
||||||
|
if (distances[cell] < LIMIT) {
|
||||||
|
remove(cell);
|
||||||
|
}
|
||||||
|
distances[cell] = distance;
|
||||||
|
next[cell] = first[distance];
|
||||||
|
previous[cell] = -1;
|
||||||
|
if (next[cell] >= 0) {
|
||||||
|
previous[next[cell]] = cell;
|
||||||
|
}
|
||||||
|
first[distance] = cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void remove(int cell) {
|
||||||
|
if (previous[cell] < 0) {
|
||||||
|
first[distances[cell]] = next[cell];
|
||||||
|
} else {
|
||||||
|
next[previous[cell]] = next[cell];
|
||||||
|
}
|
||||||
|
if (next[cell] >= 0) {
|
||||||
|
previous[next[cell]] = previous[cell];
|
||||||
|
}
|
||||||
|
next[cell] = -1;
|
||||||
|
previous[cell] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spread() {
|
||||||
|
for (int distance = 0; distance < LIMIT; distance++) {
|
||||||
|
while (first[distance] >= 0) {
|
||||||
|
int cell = first[distance];
|
||||||
|
remove(cell);
|
||||||
|
int x = cell % GRID;
|
||||||
|
int z = cell / GRID;
|
||||||
|
for (int dz = -1; dz <= 1; dz++) {
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
int nx = x + dx;
|
||||||
|
int nz = z + dz;
|
||||||
|
if (dx == 0 && dz == 0 || nx < 0 || nx >= GRID || nz < 0 || nz >= GRID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int neighbour = nz * GRID + nx;
|
||||||
|
int candidate = distance + (dx != 0 && dz != 0 ? 14 : 10);
|
||||||
|
if (heads[neighbour] == DRY || candidate >= distances[neighbour]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (dx != 0 && dz != 0 && (heads[z * GRID + nx] == DRY || heads[nz * GRID + x] == DRY)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
offer(neighbour, candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] output() {
|
||||||
|
int[] output = new int[OUTPUT * OUTPUT];
|
||||||
|
boolean affected = false;
|
||||||
|
for (int z = 0; z < OUTPUT; z++) {
|
||||||
|
for (int x = 0; x < OUTPUT; x++) {
|
||||||
|
int distance = distances[(z + PADDING - 1) * GRID + x + PADDING - 1];
|
||||||
|
output[z * OUTPUT + x] = distance;
|
||||||
|
affected |= distance < LIMIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return affected ? output : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -126,8 +126,10 @@ public final class PondPlanner {
|
|||||||
public Pond reflood(DemGrid raw, HeightLattice fine, CupSet cups, Pond pond) {
|
public Pond reflood(DemGrid raw, HeightLattice fine, CupSet cups, Pond pond) {
|
||||||
PondMask mask = pond.mask();
|
PondMask mask = pond.mask();
|
||||||
int step = mask.step();
|
int step = mask.step();
|
||||||
Box box = box(raw, fine, mask.originX() - step, mask.originZ() - step, mask.originX() + mask.width() * step,
|
int margin = Math.max(step, fine.cell());
|
||||||
mask.originZ() + mask.depth() * step, cups, Math.max(1, step / fine.cell()));
|
Box box = box(raw, fine, mask.originX() - margin, mask.originZ() - margin,
|
||||||
|
mask.originX() + (mask.width() - 1) * step + margin, mask.originZ() + (mask.depth() - 1) * step + margin,
|
||||||
|
cups, Math.max(1, step / fine.cell()));
|
||||||
if (box == null) {
|
if (box == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -669,6 +671,9 @@ public final class PondPlanner {
|
|||||||
}
|
}
|
||||||
int i = node % box.width();
|
int i = node % box.width();
|
||||||
int j = node / box.width();
|
int j = node / box.width();
|
||||||
|
if (i == 0 || j == 0 || i == box.width() - 1 || j == box.depth() - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
for (int axis = 0; axis < AXIS_X.length; axis++) {
|
for (int axis = 0; axis < AXIS_X.length; axis++) {
|
||||||
int neighbour = (j + AXIS_Z[axis]) * box.width() + i + AXIS_X[axis];
|
int neighbour = (j + AXIS_Z[axis]) * box.width() + i + AXIS_X[axis];
|
||||||
if (!wet[neighbour] && ground[neighbour] < level) {
|
if (!wet[neighbour] && ground[neighbour] < level) {
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ final class EffectiveWater {
|
|||||||
if (head == DRY) {
|
if (head == DRY) {
|
||||||
return highest == DRY ? column : column(Math.max(ground, highest + 1), DRY);
|
return highest == DRY ? column : column(Math.max(ground, highest + 1), DRY);
|
||||||
}
|
}
|
||||||
int oneAbove = oneAbove(head, east) + oneAbove(head, west) + oneAbove(head, south) + oneAbove(head, north);
|
return column;
|
||||||
if (oneAbove >= 2) {
|
|
||||||
return column(Math.max(ground, highest), DRY);
|
|
||||||
}
|
|
||||||
return dropsBelow(head, east, west, south, north) ? column(Math.max(ground, head - 1), head) : column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void settleGrid(int originX, int originZ, int width, int depth, Columns columns, long[] settled) {
|
static void settleGrid(int originX, int originZ, int width, int depth, Columns columns, long[] settled) {
|
||||||
@@ -61,18 +57,6 @@ final class EffectiveWater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int oneAbove(int head, long neighbour) {
|
|
||||||
return head(neighbour) != DRY && head(neighbour) == head + 1 ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean dropsBelow(int head, long east, long west, long south, long north) {
|
|
||||||
return dropsBelow(head, east) || dropsBelow(head, west) || dropsBelow(head, south) || dropsBelow(head, north);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean dropsBelow(int head, long neighbour) {
|
|
||||||
return head(neighbour) != DRY && head(neighbour) <= head - 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Columns {
|
interface Columns {
|
||||||
long pre(int x, int z);
|
long pre(int x, int z);
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ import lombok.Getter;
|
|||||||
public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBlockState> {
|
public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBlockState> {
|
||||||
private static final BoundBlockState BEDROCK = BoundBlockState.of("BEDROCK");
|
private static final BoundBlockState BEDROCK = BoundBlockState.of("BEDROCK");
|
||||||
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
|
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
|
||||||
|
private static final BoundBlockState DIRT = BoundBlockState.of("minecraft:dirt");
|
||||||
|
private static final BoundBlockState NETHERRACK = BoundBlockState.of("minecraft:netherrack");
|
||||||
private static final int SLOT_SEA_LAYERS = 1;
|
private static final int SLOT_SEA_LAYERS = 1;
|
||||||
@Getter
|
@Getter
|
||||||
private final RNG rng;
|
private final RNG rng;
|
||||||
@@ -132,6 +134,17 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
|||||||
return painted == null ? layerBlock : painted;
|
return painted == null ? layerBlock : painted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static NativeBlockState submergedSoil(NativeBlockState block) {
|
||||||
|
if (block == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return switch (IrisProceduralBlocks.materialKey(block)) {
|
||||||
|
case "minecraft:grass_block", "minecraft:mycelium", "minecraft:dirt_path", "minecraft:farmland" -> DIRT.get();
|
||||||
|
case "minecraft:crimson_nylium", "minecraft:warped_nylium" -> NETHERRACK.get();
|
||||||
|
default -> block;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private final class ColumnPainter {
|
private final class ColumnPainter {
|
||||||
private final int x;
|
private final int x;
|
||||||
private final int z;
|
private final int z;
|
||||||
@@ -214,6 +227,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
|||||||
int topY = Math.min(hf, chunkHeight - 1);
|
int topY = Math.min(hf, chunkHeight - 1);
|
||||||
WaterRole role = waterRoleStream.get(realX, realZ);
|
WaterRole role = waterRoleStream.get(realX, realZ);
|
||||||
boolean wet = role.wet();
|
boolean wet = role.wet();
|
||||||
|
boolean submerged = wet && he < surfaceFluidHeight;
|
||||||
boolean seaFloor = !wet || role == WaterRole.INLET;
|
boolean seaFloor = !wet || role == WaterRole.INLET;
|
||||||
IrisHydrologyMaterials materials = role == WaterRole.NONE || role == WaterRole.INLET ? null
|
IrisHydrologyMaterials materials = role == WaterRole.NONE || role == WaterRole.INLET ? null
|
||||||
: complex.resolveWaterMaterials(realX, realZ);
|
: complex.resolveWaterMaterials(realX, realZ);
|
||||||
@@ -292,7 +306,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
|||||||
if (i <= he) {
|
if (i <= he) {
|
||||||
int depth = layerSurfaceY - i;
|
int depth = layerSurfaceY - i;
|
||||||
if (depth == 0 && mappedSurfaceBlock != null) {
|
if (depth == 0 && mappedSurfaceBlock != null) {
|
||||||
put(i, mappedSurfaceBlock);
|
put(i, submerged && layerSurfaceY == he ? submergedSoil(mappedSurfaceBlock) : mappedSurfaceBlock);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (blocks == null) {
|
if (blocks == null) {
|
||||||
@@ -327,6 +341,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
|||||||
NativeBlockState layerBlock = paintWaterMaterial(
|
NativeBlockState layerBlock = paintWaterMaterial(
|
||||||
blocks.hasIndex(strataIndex) ? blocks.get(strataIndex) : null,
|
blocks.hasIndex(strataIndex) ? blocks.get(strataIndex) : null,
|
||||||
rolePalette, paintDepth, depth, rng, realX, i, realZ, data);
|
rolePalette, paintDepth, depth, rng, realX, i, realZ, data);
|
||||||
|
if (submerged && layerSurfaceY == he) {
|
||||||
|
layerBlock = submergedSoil(layerBlock);
|
||||||
|
}
|
||||||
if (layerBlock != null) {
|
if (layerBlock != null) {
|
||||||
if (keepGravityAway && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
|
if (keepGravityAway && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
|
||||||
layerBlock = rock;
|
layerBlock = rock;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import art.arcane.iris.generation.hydrology.config.IrisHydrology;
|
|||||||
import art.arcane.iris.generation.hydrology.grid.HeightLattice;
|
import art.arcane.iris.generation.hydrology.grid.HeightLattice;
|
||||||
import art.arcane.iris.generation.hydrology.underground.SeaInlet;
|
import art.arcane.iris.generation.hydrology.underground.SeaInlet;
|
||||||
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
|
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.CupSet;
|
||||||
import art.arcane.iris.generation.hydrology.water.Pond;
|
import art.arcane.iris.generation.hydrology.water.Pond;
|
||||||
import art.arcane.iris.generation.hydrology.water.PondKind;
|
import art.arcane.iris.generation.hydrology.water.PondKind;
|
||||||
import art.arcane.iris.generation.hydrology.water.PondMask;
|
import art.arcane.iris.generation.hydrology.water.PondMask;
|
||||||
@@ -24,6 +25,7 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@@ -32,6 +34,8 @@ import static org.junit.Assert.assertTrue;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
|
|
||||||
public class WindowPlannerTest {
|
public class WindowPlannerTest {
|
||||||
@@ -49,6 +53,51 @@ public class WindowPlannerTest {
|
|||||||
assertTrue(plan.ponds().isEmpty());
|
assertTrue(plan.ponds().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shrinkingPondReplansUntilItsChangedOutletStopsLowering() {
|
||||||
|
HydrologySettings settings = settings(new IrisHydrology());
|
||||||
|
SyntheticTerrain terrain = SyntheticTerrain.flat(100D).withPit(64D, 64D, 20D, 20D);
|
||||||
|
int width = 41;
|
||||||
|
long[] bits = new long[(width * width + 63) >>> 6];
|
||||||
|
for (int z = -20; z <= 20; z++) {
|
||||||
|
for (int x = -20; x <= 20; x++) {
|
||||||
|
if (x * x + z * z < 400) {
|
||||||
|
int cell = (z + 20) * width + x + 20;
|
||||||
|
bits[cell >>> 6] |= 1L << cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Pond initial = new Pond(19L, PondKind.NATURAL, 100, 80, 84, 64, 100f,
|
||||||
|
new PondMask(44, 44, 1, width, width, bits), 0L, 0, "water", 64, 64);
|
||||||
|
PondPlanner ponds = spy(new PondPlanner(settings));
|
||||||
|
doReturn(CupSet.EMPTY).when(ponds).subtractCups(any(), any(), any(), any());
|
||||||
|
doReturn(List.of(initial)).when(ponds).ponds(any(), any(), any(), any(), any(), any());
|
||||||
|
ReachShaper shaper = mock(ReachShaper.class);
|
||||||
|
AtomicInteger passes = new AtomicInteger();
|
||||||
|
doAnswer(call -> {
|
||||||
|
List<Pond> water = call.getArgument(4);
|
||||||
|
Pond pond = single(water);
|
||||||
|
passes.incrementAndGet();
|
||||||
|
int head = pond.level() > 96 ? 96 : 94;
|
||||||
|
Reach outlet = new Reach(20L, settings.tiers().get(0), 0L, new float[]{64f, 64f}, new float[]{32f, 96f},
|
||||||
|
new int[]{head, head}, new float[]{2f, 2f}, new float[]{2f, 2f}, new ReachStyle[]{ReachStyle.RUN, ReachStyle.RUN}, 1000f);
|
||||||
|
return new ShapedNetwork(List.of(outlet), List.of(pond.withOutletReachId(outlet.id())));
|
||||||
|
}).when(shaper).shape(any(), any(), any(), any(), any(), any(), anyLong());
|
||||||
|
WindowPlanner planner = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18),
|
||||||
|
new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 18), ponds, shaper);
|
||||||
|
|
||||||
|
WindowComputation settled = planner.compute(ORIGIN, noRegions(ORIGIN));
|
||||||
|
|
||||||
|
Pond pond = single(settled.allPonds());
|
||||||
|
assertEquals(3, passes.get());
|
||||||
|
assertEquals(94, pond.level());
|
||||||
|
assertEquals(20L, pond.outletReachId());
|
||||||
|
assertTrue(initial.mask().contains(83, 64));
|
||||||
|
assertFalse(pond.mask().contains(83, 64));
|
||||||
|
assertTrue(pond.mask().contains(64, 64));
|
||||||
|
assertEquals(94, settled.allReaches().get(0).grade()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void burnedTrunkCapturesLocalDrainage() {
|
public void burnedTrunkCapturesLocalDrainage() {
|
||||||
HydrologySettings settings = settings(new IrisHydrology());
|
HydrologySettings settings = settings(new IrisHydrology());
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package art.arcane.iris.generation.hydrology.raster;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class BedProfileTest {
|
||||||
|
@Test
|
||||||
|
public void fineDetailHasBoundedAmplitudeAndAdjacentSteps() {
|
||||||
|
double minimum = Double.POSITIVE_INFINITY;
|
||||||
|
double maximum = Double.NEGATIVE_INFINITY;
|
||||||
|
for (long seed : new long[]{0L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE}) {
|
||||||
|
for (int x = -48; x <= 48; x++) {
|
||||||
|
for (int z = -48; z <= 48; z++) {
|
||||||
|
double detail = BedProfile.detail(seed, x, z);
|
||||||
|
minimum = Math.min(minimum, detail);
|
||||||
|
maximum = Math.max(maximum, detail);
|
||||||
|
assertTrue(Math.abs(detail) <= 0.35D);
|
||||||
|
assertTrue(Math.abs(detail - BedProfile.detail(seed, x + 1, z)) <= 0.35D);
|
||||||
|
assertTrue(Math.abs(detail - BedProfile.detail(seed, x, z + 1)) <= 0.35D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(minimum < -0.3D);
|
||||||
|
assertTrue(maximum > 0.3D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fineDetailBreaksUpLevelContoursWithoutTallFaces() {
|
||||||
|
int changedChannels = 0;
|
||||||
|
int changedPonds = 0;
|
||||||
|
for (int x = -64; x <= 64; x++) {
|
||||||
|
for (int z = -32; z <= 32; z++) {
|
||||||
|
double detail = BedProfile.detail(1337L, x, z);
|
||||||
|
int channel = BedProfile.channelDepth(6D, 0.5D, 0D, detail);
|
||||||
|
int pond = BedProfile.pondDepth(8, 0.5D, 0D, detail);
|
||||||
|
changedChannels += channel != BedProfile.channelDepth(6D, 0.5D, 0D, 0D) ? 1 : 0;
|
||||||
|
changedPonds += pond != BedProfile.pondDepth(8, 0.5D, 0D, 0D) ? 1 : 0;
|
||||||
|
assertTrue(channel == 4 || channel == 5);
|
||||||
|
assertTrue(pond == 4 || pond == 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(changedChannels > 1000);
|
||||||
|
assertTrue(changedPonds > 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void depthLimitsAndShallowEdgesSurviveFineDetail() {
|
||||||
|
for (int maximum : new int[]{1, 2, 6, 8, 16, 64}) {
|
||||||
|
for (double variation : new double[]{-1D, -0.5D, 0D, 0.5D, 1D}) {
|
||||||
|
for (double detail : new double[]{-0.35D, 0D, 0.35D}) {
|
||||||
|
assertEquals(1, BedProfile.channelDepth(maximum, 1D, variation, detail));
|
||||||
|
assertEquals(1, BedProfile.pondDepth(maximum, 0D, variation, detail));
|
||||||
|
for (int step = 0; step <= 100; step++) {
|
||||||
|
double position = step / 100D;
|
||||||
|
int channel = BedProfile.channelDepth(maximum, position, variation, detail);
|
||||||
|
int pond = BedProfile.pondDepth(maximum, position, variation, detail);
|
||||||
|
assertTrue(channel >= 1 && channel <= maximum);
|
||||||
|
assertTrue(pond >= 1 && pond <= maximum);
|
||||||
|
assertTrue(Math.abs(channel - BedProfile.channelDepth(maximum, position, variation, 0D)) <= 1);
|
||||||
|
assertTrue(Math.abs(pond - BedProfile.pondDepth(maximum, position, variation, 0D)) <= 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void detailUsesWorldCoordinatesAcrossNegativeChunkBoundaries() {
|
||||||
|
double[][] expected = new double[66][66];
|
||||||
|
for (int x = -33; x <= 32; x++) {
|
||||||
|
for (int z = -33; z <= 32; z++) {
|
||||||
|
expected[x + 33][z + 33] = BedProfile.detail(1337L, x, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int differentSeeds = 0;
|
||||||
|
for (int chunkX = 2; chunkX >= -3; chunkX--) {
|
||||||
|
for (int chunkZ = 2; chunkZ >= -3; chunkZ--) {
|
||||||
|
for (int localX = 16; localX >= -1; localX--) {
|
||||||
|
for (int localZ = 16; localZ >= -1; localZ--) {
|
||||||
|
int x = (chunkX << 4) + localX;
|
||||||
|
int z = (chunkZ << 4) + localZ;
|
||||||
|
if (x < -33 || x > 32 || z < -33 || z > 32) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double detail = BedProfile.detail(1337L, x, z);
|
||||||
|
assertEquals(expected[x + 33][z + 33], detail, 0D);
|
||||||
|
differentSeeds += detail != BedProfile.detail(1338L, x, z) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(differentSeeds > 4000);
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
-23
@@ -47,6 +47,41 @@ public class ChunkWaterRasterTest {
|
|||||||
private static final float SPACING = 2f;
|
private static final float SPACING = 2f;
|
||||||
private static final int GENTLE_STEP = 3;
|
private static final int GENTLE_STEP = 3;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flatLakesSkipExtraTerrainSamplingForRiffles() {
|
||||||
|
AtomicInteger samples = new AtomicInteger();
|
||||||
|
Scene scene = new Scene(settings(new IrisHydrology()), (x, z) -> {
|
||||||
|
samples.incrementAndGet();
|
||||||
|
return 70;
|
||||||
|
}).lake(pond(8L, PondKind.LAKE, 80, circle(8, 8, 100, 1), 8, 8, 0));
|
||||||
|
|
||||||
|
assertEquals(80, scene.raster(0, 0).waterHead(8, 8));
|
||||||
|
assertEquals(320, samples.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shallowStandingWaterRetainsItsFootprintWhenItsHeadLowersOneBlock() {
|
||||||
|
int desiredLevel = 75;
|
||||||
|
Pond water = pond(9L, PondKind.NATURAL, desiredLevel, circle(-16, -16, 12, 1), -16, -16, 0);
|
||||||
|
HydrologySettings settings = settings(new IrisHydrology());
|
||||||
|
for (Scene scene : List.of(new Scene(settings, (x, z) -> desiredLevel - 1).pond(water),
|
||||||
|
new Scene(settings, (x, z) -> desiredLevel - 1).lake(water))) {
|
||||||
|
int wet = 0;
|
||||||
|
for (int x = -32; x <= 0; x++) {
|
||||||
|
for (int z = -32; z <= 0; z++) {
|
||||||
|
if (!water.mask().contains(x, z)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
wet++;
|
||||||
|
assertEquals("head at " + x + "," + z, desiredLevel - 1, scene.head(x, z));
|
||||||
|
assertTrue("bed at " + x + "," + z, scene.terrain(x, z) < scene.head(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue("wet footprint " + wet, wet > 400);
|
||||||
|
assertEquals(0, scene.leaks(-32, -32, 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void channelBedBelowWaterHead() {
|
public void channelBedBelowWaterHead() {
|
||||||
IntBinaryOperator natural = (x, z) -> x >= 40 && x < 50 ? 82 : 90 + (int) Math.round(2D * Math.sin(x / 9D));
|
IntBinaryOperator natural = (x, z) -> x >= 40 && x < 50 ? 82 : 90 + (int) Math.round(2D * Math.sin(x / 9D));
|
||||||
@@ -62,15 +97,15 @@ public class ChunkWaterRasterTest {
|
|||||||
channel++;
|
channel++;
|
||||||
int bed = scene.terrain(x, z);
|
int bed = scene.terrain(x, z);
|
||||||
int ground = natural.applyAsInt(x, z);
|
int ground = natural.applyAsInt(x, z);
|
||||||
assertEquals(87, scene.head(x, z));
|
assertEquals(86, scene.head(x, z));
|
||||||
assertTrue("bed " + bed + " below the water head at " + x + "," + z, bed < 87);
|
assertTrue("bed " + bed + " below the water head at " + x + "," + z, bed < 86);
|
||||||
assertTrue("bed never above natural at " + x + "," + z, bed <= ground);
|
assertTrue("bed never above natural at " + x + "," + z, bed <= ground);
|
||||||
if (ground < 84) {
|
if (ground < 84) {
|
||||||
pooled++;
|
pooled++;
|
||||||
assertEquals("a depression under the channel keeps its natural floor at " + x + "," + z, ground, bed);
|
assertEquals("a depression under the channel keeps its natural floor at " + x + "," + z, ground, bed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertTrue("full depth on the centre line at x=" + x, 87 - scene.terrain(x, 8) >= 3);
|
assertTrue("the centre line keeps a submerged bed at x=" + x, 86 - scene.terrain(x, 8) >= 2);
|
||||||
}
|
}
|
||||||
assertTrue("channel columns " + channel, channel > 900);
|
assertTrue("channel columns " + channel, channel > 900);
|
||||||
assertTrue("pooled columns " + pooled, pooled > 50);
|
assertTrue("pooled columns " + pooled, pooled > 50);
|
||||||
@@ -99,6 +134,69 @@ public class ChunkWaterRasterTest {
|
|||||||
assertEquals("far columns sit at the cap", ChunkWaterRaster.MAX_SHORE_DISTANCE, scene.shore(0, -40));
|
assertEquals("far columns sit at the cap", ChunkWaterRaster.MAX_SHORE_DISTANCE, scene.shore(0, -40));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void channelBedsAndBanksVaryAlongConstantWidthReaches() {
|
||||||
|
IrisHydrology config = new IrisHydrology();
|
||||||
|
config.getDeformation().setBankNoise(0D);
|
||||||
|
HydrologySettings settings = settings(config);
|
||||||
|
HydrologyTier river = tier("river", 8f, 0.75f, 24, IrisBankStyle.SMOOTH);
|
||||||
|
Reach reach = line(15L, river, 0L, -112f, 0f, 112f, 0f, (x, z) -> 100);
|
||||||
|
Scene scene = new Scene(settings, (x, z) -> 120).reach(reach);
|
||||||
|
Set<Integer> bedHeights = new HashSet<>();
|
||||||
|
Set<Integer> bankHeights = new HashSet<>();
|
||||||
|
int previousBed = scene.terrain(-80, 4);
|
||||||
|
int previousBank = scene.terrain(-80, 20);
|
||||||
|
for (int x = -80; x <= 80; x++) {
|
||||||
|
int bed = scene.terrain(x, 4);
|
||||||
|
int bank = scene.terrain(x, 20);
|
||||||
|
bedHeights.add(bed);
|
||||||
|
bankHeights.add(bank);
|
||||||
|
assertEquals(100, scene.head(x, 4));
|
||||||
|
assertEquals(Integer.MIN_VALUE, scene.head(x, 20));
|
||||||
|
assertTrue("depth stays within the tier limit", bed >= 94 && bed < 100);
|
||||||
|
assertTrue("bank stays between the water and terrain", bank >= 100 && bank <= 120);
|
||||||
|
assertTrue("bed variation remains coherent across chunk boundaries", Math.abs(bed - previousBed) <= 1);
|
||||||
|
assertTrue("bank variation remains coherent across chunk boundaries", Math.abs(bank - previousBank) <= 1);
|
||||||
|
previousBed = bed;
|
||||||
|
previousBank = bank;
|
||||||
|
}
|
||||||
|
assertTrue("constant-width bed has varying contours", bedHeights.size() >= 3);
|
||||||
|
assertTrue("constant-distance bank has varying contours", bankHeights.size() >= 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void pondShelvesAndTapersVaryWithoutChangingWaterCoverage() {
|
||||||
|
HydrologySettings settings = settings(new IrisHydrology());
|
||||||
|
long[] bits = new long[192 * 64 / 64];
|
||||||
|
Arrays.fill(bits, -1L);
|
||||||
|
Pond water = pond(16L, PondKind.NATURAL, 100, new PondMask(-96, -32, 1, 192, 64, bits), 0, 0, 0);
|
||||||
|
Scene scene = new Scene(settings, (x, z) -> 110).pond(water);
|
||||||
|
Set<Integer> shelfHeights = new HashSet<>();
|
||||||
|
Set<Integer> deepHeights = new HashSet<>();
|
||||||
|
Set<Integer> taperHeights = new HashSet<>();
|
||||||
|
for (int x = -64; x <= 64; x++) {
|
||||||
|
shelfHeights.add(scene.terrain(x, -28));
|
||||||
|
deepHeights.add(scene.terrain(x, 0));
|
||||||
|
taperHeights.add(scene.terrain(x, -40));
|
||||||
|
for (int z = -48; z <= 47; z++) {
|
||||||
|
assertEquals("the shoreline remains fixed", water.mask().contains(x, z), scene.water(x, z));
|
||||||
|
if (scene.water(x, z)) {
|
||||||
|
assertEquals(100, scene.head(x, z));
|
||||||
|
assertTrue("pond depth remains bounded", scene.terrain(x, z) >= 92 && scene.terrain(x, z) < 100);
|
||||||
|
} else {
|
||||||
|
assertTrue("dry taper stays above water", scene.terrain(x, z) >= 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue("shelf contours vary along a straight shore", shelfHeights.size() >= 3);
|
||||||
|
assertTrue("deep floor has relief", deepHeights.size() >= 2);
|
||||||
|
assertTrue("dry taper varies along a straight shore", taperHeights.size() >= 2);
|
||||||
|
Scene reverse = new Scene(settings, (x, z) -> 110).pond(water);
|
||||||
|
for (int x = 64; x >= -64; x--) {
|
||||||
|
assertEquals("generation order leaves the floor unchanged", scene.terrain(x, -28), reverse.terrain(x, -28));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void lakeRimShoreDistanceMatchesNearestLakeWater() {
|
public void lakeRimShoreDistanceMatchesNearestLakeWater() {
|
||||||
IntBinaryOperator natural = (x, z) -> 60 + (int) (Math.hypot(x - 24, z - 24) / 2D);
|
IntBinaryOperator natural = (x, z) -> 60 + (int) (Math.hypot(x - 24, z - 24) / 2D);
|
||||||
@@ -189,14 +287,14 @@ public class ChunkWaterRasterTest {
|
|||||||
wet++;
|
wet++;
|
||||||
chunks.add(pack(Math.floorDiv(x, 16), Math.floorDiv(z, 16)));
|
chunks.add(pack(Math.floorDiv(x, 16), Math.floorDiv(z, 16)));
|
||||||
assertEquals(pond.id(), scene.feature(x, z));
|
assertEquals(pond.id(), scene.feature(x, z));
|
||||||
assertEquals("pond level at " + x + "," + z, 75, scene.head(x, z));
|
assertEquals("pond level at " + x + "," + z, 74, scene.head(x, z));
|
||||||
assertTrue("pond floor below the level at " + x + "," + z, scene.terrain(x, z) < 75);
|
assertTrue("pond floor below the level at " + x + "," + z, scene.terrain(x, z) < 74);
|
||||||
assertEquals("water", scene.fluid(x, z));
|
assertEquals("water", scene.fluid(x, z));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertTrue("pond columns " + wet, wet > 400);
|
assertTrue("pond columns " + wet, wet > 400);
|
||||||
assertEquals("the pond spans four chunks", 4, chunks.size());
|
assertEquals("the pond spans four chunks", 4, chunks.size());
|
||||||
assertEquals("open water reaches full depth", 75 - 8, scene.terrain(16, 16));
|
assertTrue("open water stays within the maximum depth", scene.terrain(16, 16) >= 74 - 8 && scene.terrain(16, 16) < 74);
|
||||||
assertEquals(WaterRole.POND_SHORE, scene.role(16, 30));
|
assertEquals(WaterRole.POND_SHORE, scene.role(16, 30));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +304,7 @@ public class ChunkWaterRasterTest {
|
|||||||
float startZ = -20f;
|
float startZ = -20f;
|
||||||
float endX = 80f;
|
float endX = 80f;
|
||||||
float endZ = 60f;
|
float endZ = 60f;
|
||||||
IntBinaryOperator natural = (x, z) -> 80 + (int) Math.floor(0.35D * perpendicular(x, z, startX, startZ, endX, endZ));
|
IntBinaryOperator natural = (x, z) -> 79 + (int) Math.floor(0.35D * perpendicular(x, z, startX, startZ, endX, endZ));
|
||||||
HydrologyTier river = tier("river", 3f, 0.5f, 12, IrisBankStyle.SMOOTH);
|
HydrologyTier river = tier("river", 3f, 0.5f, 12, IrisBankStyle.SMOOTH);
|
||||||
Scene scene = new Scene(settings(new IrisHydrology()), natural).reach(line(31L, river, 0L, startX, startZ, endX, endZ, (x, z) -> 79));
|
Scene scene = new Scene(settings(new IrisHydrology()), natural).reach(line(31L, river, 0L, startX, startZ, endX, endZ, (x, z) -> 79));
|
||||||
int interior = 0;
|
int interior = 0;
|
||||||
@@ -253,12 +351,12 @@ public class ChunkWaterRasterTest {
|
|||||||
.reach(line(44L, stream, 0L, 8f, 8f, 56f, 8f, (x, z) -> Math.clamp(eastSlope.applyAsInt((int) x, (int) z) - 1, 94, 99)));
|
.reach(line(44L, stream, 0L, 8f, 8f, 56f, 8f, (x, z) -> Math.clamp(eastSlope.applyAsInt((int) x, (int) z) - 1, 94, 99)));
|
||||||
assertContained("chained ponds", chained, -16, -8, 80, 24, true);
|
assertContained("chained ponds", chained, -16, -8, 80, 24, true);
|
||||||
|
|
||||||
IntBinaryOperator cliff = (x, z) -> (x < 24 ? 100 : 80) - Math.max(0, z - 8);
|
IntBinaryOperator cliff = (x, z) -> (x < 24 ? 99 : 79) - Math.max(0, z - 8);
|
||||||
Scene waterfall = new Scene(settings, cliff).reach(line(45L, stream, 0L, -24f, 8f, 72f, 8f, (x, z) -> x < 24 ? 99 : 79));
|
Scene waterfall = new Scene(settings, cliff).reach(line(45L, stream, 0L, -24f, 8f, 72f, 8f, (x, z) -> x < 24 ? 99 : 79));
|
||||||
assertContained("waterfall", waterfall, -16, -8, 64, 24, true);
|
assertContained("waterfall", waterfall, -16, -8, 64, 24, true);
|
||||||
assertEquals(WaterRole.WATERFALL, waterfall.role(24, 8));
|
assertEquals(WaterRole.WATERFALL, waterfall.role(24, 8));
|
||||||
assertEquals(99, waterfall.head(22, 8));
|
assertEquals(98, waterfall.head(22, 8));
|
||||||
assertEquals(79, waterfall.head(24, 8));
|
assertEquals(78, waterfall.head(24, 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -455,7 +553,7 @@ public class ChunkWaterRasterTest {
|
|||||||
IrisHydrology config = new IrisHydrology();
|
IrisHydrology config = new IrisHydrology();
|
||||||
config.getDeformation().setBankSlope(0D).setBankSteepFactor(0D).setBankNoise(0D);
|
config.getDeformation().setBankSlope(0D).setBankSteepFactor(0D).setBankNoise(0D);
|
||||||
HydrologySettings settings = settings(config);
|
HydrologySettings settings = settings(config);
|
||||||
IntBinaryOperator natural = (x, z) -> z >= 8 ? 70 + (z - 8) : 68 - (8 - z) / 4;
|
IntBinaryOperator natural = (x, z) -> z >= 8 ? 69 + (z - 8) : 67 - (8 - z) / 4;
|
||||||
Map<IrisBankStyle, int[]> cuts = new EnumMap<>(IrisBankStyle.class);
|
Map<IrisBankStyle, int[]> cuts = new EnumMap<>(IrisBankStyle.class);
|
||||||
for (IrisBankStyle style : IrisBankStyle.values()) {
|
for (IrisBankStyle style : IrisBankStyle.values()) {
|
||||||
Scene scene = new Scene(settings, natural).reach(line(90L + style.ordinal(), tier("river", 2f, 0.5f, 16, style), 0L, -48f, 8f, 64f, 8f,
|
Scene scene = new Scene(settings, natural).reach(line(90L + style.ordinal(), tier("river", 2f, 0.5f, 16, style), 0L, -48f, 8f, 64f, 8f,
|
||||||
@@ -466,7 +564,7 @@ public class ChunkWaterRasterTest {
|
|||||||
int height = scene.terrain(8, z);
|
int height = scene.terrain(8, z);
|
||||||
assertFalse(style + " bank is dry at z=" + z, scene.water(8, z));
|
assertFalse(style + " bank is dry at z=" + z, scene.water(8, z));
|
||||||
assertTrue(style + " bank role at z=" + z, scene.role(8, z) == WaterRole.BANK || scene.role(8, z) == WaterRole.SHORE);
|
assertTrue(style + " bank role at z=" + z, scene.role(8, z) == WaterRole.BANK || scene.role(8, z) == WaterRole.SHORE);
|
||||||
assertTrue(style + " bank between water and natural at z=" + z + ": " + height, height >= 69 && height <= natural.applyAsInt(8, z));
|
assertTrue(style + " bank between water and natural at z=" + z + ": " + height, height >= 68 && height <= natural.applyAsInt(8, z));
|
||||||
assertTrue(style + " bank never falls toward the ground at z=" + z, outward == 1 || height >= cut[outward - 2]);
|
assertTrue(style + " bank never falls toward the ground at z=" + z, outward == 1 || height >= cut[outward - 2]);
|
||||||
cut[outward - 1] = height;
|
cut[outward - 1] = height;
|
||||||
int levee = scene.terrain(8, 6 - outward);
|
int levee = scene.terrain(8, 6 - outward);
|
||||||
@@ -475,7 +573,7 @@ public class ChunkWaterRasterTest {
|
|||||||
levee >= ground && levee <= ground + settings.deformation().maxFill());
|
levee >= ground && levee <= ground + settings.deformation().maxFill());
|
||||||
}
|
}
|
||||||
assertEquals(style + " meets natural at the band edge", natural.applyAsInt(8, 26), cut[15]);
|
assertEquals(style + " meets natural at the band edge", natural.applyAsInt(8, 26), cut[15]);
|
||||||
assertTrue(style + " levee holds the water", scene.terrain(8, 5) >= 70);
|
assertTrue(style + " levee holds the water", scene.terrain(8, 5) >= 69);
|
||||||
assertEquals(style + " levee meets natural at the band edge", natural.applyAsInt(8, -10), scene.terrain(8, -10));
|
assertEquals(style + " levee meets natural at the band edge", natural.applyAsInt(8, -10), scene.terrain(8, -10));
|
||||||
assertEquals(style + " leaks", 0, scene.leaks(-16, -16, 32, 32));
|
assertEquals(style + " leaks", 0, scene.leaks(-16, -16, 32, 32));
|
||||||
cuts.put(style, cut);
|
cuts.put(style, cut);
|
||||||
@@ -494,16 +592,16 @@ public class ChunkWaterRasterTest {
|
|||||||
assertTrue(ordered);
|
assertTrue(ordered);
|
||||||
int[] cliff = cuts.get(IrisBankStyle.CLIFF);
|
int[] cliff = cuts.get(IrisBankStyle.CLIFF);
|
||||||
for (int index = 0; index < 8; index++) {
|
for (int index = 0; index < 8; index++) {
|
||||||
assertEquals("cliff bench at outward " + (index + 1), 69, cliff[index]);
|
assertEquals("cliff bench at outward " + (index + 1), 68, cliff[index]);
|
||||||
}
|
}
|
||||||
assertEquals("cliff wall reaches natural", natural.applyAsInt(8, 22), cliff[11]);
|
assertEquals("cliff wall reaches natural", natural.applyAsInt(8, 22), cliff[11]);
|
||||||
int[] terraced = cuts.get(IrisBankStyle.TERRACED);
|
int[] terraced = cuts.get(IrisBankStyle.TERRACED);
|
||||||
boolean level = false;
|
boolean level = false;
|
||||||
for (int index = 0; index < 15; index++) {
|
for (int index = 0; index < 15; index++) {
|
||||||
if (terraced[index] < natural.applyAsInt(8, 11 + index)) {
|
if (terraced[index] < natural.applyAsInt(8, 11 + index)) {
|
||||||
assertEquals("terrace step height at outward " + (index + 1), 0, (terraced[index] - 69) % 3);
|
assertEquals("terrace step height at outward " + (index + 1), 0, (terraced[index] - 68) % 3);
|
||||||
}
|
}
|
||||||
level |= terraced[index] > 69 && terraced[index] == terraced[index + 1];
|
level |= terraced[index] > 68 && terraced[index] == terraced[index + 1];
|
||||||
}
|
}
|
||||||
assertTrue("terraces hold level steps", level);
|
assertTrue("terraces hold level steps", level);
|
||||||
List<IrisBankStyle> styles = List.of(IrisBankStyle.values());
|
List<IrisBankStyle> styles = List.of(IrisBankStyle.values());
|
||||||
@@ -521,13 +619,13 @@ public class ChunkWaterRasterTest {
|
|||||||
int maxFill = settings.deformation().maxFill();
|
int maxFill = settings.deformation().maxFill();
|
||||||
HydrologyTier stream = tier("stream", 1.5f, 0.5f, 0, IrisBankStyle.SMOOTH);
|
HydrologyTier stream = tier("stream", 1.5f, 0.5f, 0, IrisBankStyle.SMOOTH);
|
||||||
Reach perchedReach = line(101L, stream, 0L, -32f, 8f, 48f, 8f, (x, z) -> 79);
|
Reach perchedReach = line(101L, stream, 0L, -32f, 8f, 48f, 8f, (x, z) -> 79);
|
||||||
IntBinaryOperator slope = (x, z) -> 80 - Math.max(0, z - 8);
|
IntBinaryOperator slope = (x, z) -> 79 - Math.max(0, z - 8);
|
||||||
Scene scene = new Scene(settings, slope).reach(perchedReach);
|
Scene scene = new Scene(settings, slope).reach(perchedReach);
|
||||||
for (int x = -24; x < 40; x++) {
|
for (int x = -24; x < 40; x++) {
|
||||||
assertTrue("channel water at x=" + x, scene.water(x, 9));
|
assertTrue("channel water at x=" + x, scene.water(x, 9));
|
||||||
assertFalse("dry downhill column at x=" + x, scene.water(x, 10));
|
assertFalse("dry downhill column at x=" + x, scene.water(x, 10));
|
||||||
assertEquals("natural sits below the water at x=" + x, 78, slope.applyAsInt(x, 10));
|
assertEquals("natural sits below the water at x=" + x, 77, slope.applyAsInt(x, 10));
|
||||||
assertEquals("berm one block above the water at x=" + x, 80, scene.terrain(x, 10));
|
assertEquals("berm one block above the water at x=" + x, 79, scene.terrain(x, 10));
|
||||||
assertEquals(WaterRole.SHORE, scene.role(x, 10));
|
assertEquals(WaterRole.SHORE, scene.role(x, 10));
|
||||||
assertEquals(perchedReach.id(), scene.feature(x, 10));
|
assertEquals(perchedReach.id(), scene.feature(x, 10));
|
||||||
assertEquals(stream, scene.tier(x, 10));
|
assertEquals(stream, scene.tier(x, 10));
|
||||||
@@ -536,11 +634,11 @@ public class ChunkWaterRasterTest {
|
|||||||
assertEquals(0, scene.leaks(-24, -8, 40, 24));
|
assertEquals(0, scene.leaks(-24, -8, 40, 24));
|
||||||
|
|
||||||
Scene steep = new Scene(settings, (x, z) -> z > 9 ? 60 : 80).reach(perchedReach);
|
Scene steep = new Scene(settings, (x, z) -> z > 9 ? 60 : 80).reach(perchedReach);
|
||||||
assertEquals("past the fill limit the guard berms to the water itself", 79, steep.terrain(0, 10));
|
assertEquals("past the fill limit the guard berms to the water itself", 78, steep.terrain(0, 10));
|
||||||
assertTrue("the guard rises past the fill limit", steep.terrain(0, 10) > 60 + maxFill);
|
assertTrue("the guard rises past the fill limit", steep.terrain(0, 10) > 60 + maxFill);
|
||||||
assertEquals(0, steep.leaks(-24, -8, 40, 24));
|
assertEquals(0, steep.leaks(-24, -8, 40, 24));
|
||||||
Scene below = new Scene(settings, (x, z) -> z > 9 ? 30 : 80).reach(perchedReach);
|
Scene below = new Scene(settings, (x, z) -> z > 9 ? 30 : 80).reach(perchedReach);
|
||||||
assertEquals("a column under the sea surface is bermed when the water stands above it", 79, below.terrain(0, 10));
|
assertEquals("a column under the sea surface is bermed when the water stands above it", 78, below.terrain(0, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -559,7 +657,7 @@ public class ChunkWaterRasterTest {
|
|||||||
}
|
}
|
||||||
dams++;
|
dams++;
|
||||||
assertEquals(dammed.id(), scene.feature(x, z));
|
assertEquals(dammed.id(), scene.feature(x, z));
|
||||||
assertEquals("dam crest one block above the level at " + x + "," + z, 96, scene.terrain(x, z));
|
assertEquals("dam crest one block above the level at " + x + "," + z, 95, scene.terrain(x, z));
|
||||||
if (scene.terrain(x, z) - slope.applyAsInt(x, z) > maxFill) {
|
if (scene.terrain(x, z) - slope.applyAsInt(x, z) > maxFill) {
|
||||||
tall++;
|
tall++;
|
||||||
}
|
}
|
||||||
@@ -596,7 +694,7 @@ public class ChunkWaterRasterTest {
|
|||||||
drawn.catchment());
|
drawn.catchment());
|
||||||
Scene scene = new Scene(settings(new IrisHydrology()), coast).reach(coastal);
|
Scene scene = new Scene(settings(new IrisHydrology()), coast).reach(coastal);
|
||||||
assertEquals("the lip carries the fall", WaterRole.WATERFALL, scene.role(38, 8));
|
assertEquals("the lip carries the fall", WaterRole.WATERFALL, scene.role(38, 8));
|
||||||
assertEquals(59, scene.head(38, 8));
|
assertEquals(58, scene.head(38, 8));
|
||||||
int mouths = 0;
|
int mouths = 0;
|
||||||
for (int x = -20; x <= 56; x++) {
|
for (int x = -20; x <= 56; x++) {
|
||||||
for (int z = 0; z <= 16; z++) {
|
for (int z = 0; z <= 16; z++) {
|
||||||
|
|||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
package art.arcane.iris.generation.hydrology.raster;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.config.IrisBankStyle;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.Pond;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.PondKind;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.PondMask;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.ReachStyle;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class PondChannelSettlementTest {
|
||||||
|
private static final int SEA_LEVEL = 40;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aLowerChannelDrainsThePondWhereOnlyItsWidthOverlaps() {
|
||||||
|
for (int offset : new int[]{-512, -19, 0, 513}) {
|
||||||
|
Pond pond = pond(1L, PondKind.CHAINED, 100, circle(offset, offset, 8));
|
||||||
|
Reach river = line(2L, offset + 12, offset - 20, offset + 12, offset + 20, 5f, 95, 95);
|
||||||
|
assertEquals(94, settle(List.of(river), pond));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cardinalWaterContactDrainsButDiagonalProximityDoesNot() {
|
||||||
|
Pond pond = pond(1L, PondKind.NATURAL, 100, circle(0, 0, 0));
|
||||||
|
Reach cardinal = line(2L, 1, 0, 1, 1, 0.25f, 94, 94);
|
||||||
|
Reach diagonal = line(2L, 1, 1, 1, 2, 0.25f, 94, 94);
|
||||||
|
|
||||||
|
assertEquals(94, settle(List.of(cardinal), pond));
|
||||||
|
assertEquals(100, settle(List.of(diagonal), pond));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drySeparationHigherChannelsAndFixedLakesKeepTheirLevels() {
|
||||||
|
PondMask mask = circle(0, 0, 8);
|
||||||
|
Pond pond = pond(1L, PondKind.NATURAL, 100, mask);
|
||||||
|
Pond lake = pond(1L, PondKind.LAKE, 100, mask);
|
||||||
|
Reach separate = line(2L, 15, -20, 15, 20, 4f, 94, 94);
|
||||||
|
Reach higher = line(2L, 0, -20, 0, 20, 4f, 104, 104);
|
||||||
|
Reach lower = line(2L, 0, -20, 0, 20, 4f, 94, 94);
|
||||||
|
|
||||||
|
assertEquals(100, settle(List.of(separate), pond));
|
||||||
|
assertEquals(100, settle(List.of(higher), pond));
|
||||||
|
assertEquals(100, settle(List.of(lower), lake));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void equalCanonicalHeadsDoNotTriggerAnotherReflood() {
|
||||||
|
Pond pond = pond(1L, PondKind.CHAINED, 101, circle(0, 0, 8));
|
||||||
|
Reach river = line(2L, 0, -20, 0, 20, 4f, 100, 100);
|
||||||
|
|
||||||
|
assertEquals(101, settle(List.of(river), pond));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nearerChannelsAndIdentityTiesMatchTheRasterWinner() {
|
||||||
|
Pond pond = pond(1L, PondKind.CHAINED, 100, circle(0, 0, 4));
|
||||||
|
Reach lower = line(4L, 2, -20, 2, 20, 8f, 94, 94);
|
||||||
|
Reach nearer = line(3L, 0, -20, 0, 20, 8f, 100, 100);
|
||||||
|
Pond single = pond(2L, PondKind.NATURAL, 100, circle(0, 0, 0));
|
||||||
|
Reach tied = line(3L, 2, -20, 2, 20, 8f, 100, 100);
|
||||||
|
|
||||||
|
assertEquals(100, settle(List.of(lower, nearer), single));
|
||||||
|
assertEquals(100, settle(List.of(lower, tied), pond));
|
||||||
|
assertEquals(100, settle(List.of(tied, lower), pond));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void projectedStationChoosesOnlyTheTouchingSideOfADrop() {
|
||||||
|
Reach river = line(2L, -20, 0, 20, 0, 2f, 100, 94);
|
||||||
|
|
||||||
|
assertEquals(100, settle(List.of(river), pond(1L, PondKind.NATURAL, 100, circle(-5, 0, 0))));
|
||||||
|
assertEquals(94, settle(List.of(river), pond(1L, PondKind.NATURAL, 100, circle(5, 0, 0))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void adjacentPondsSettleIndependentlyOfReachAndPondOrder() {
|
||||||
|
Pond first = pond(1L, PondKind.CHAINED, 104, circle(0, 0, 8));
|
||||||
|
Pond second = pond(2L, PondKind.CHAINED, 102, circle(17, 0, 8));
|
||||||
|
Reach across = line(3L, -20, 0, 40, 0, 4f, 96, 96);
|
||||||
|
Reach lower = line(4L, 17, -20, 17, 20, 4f, 94, 94);
|
||||||
|
|
||||||
|
assertArrayEquals(new int[]{96, 94}, PondChannelSettlement.levels(List.of(across, lower), List.of(first, second),
|
||||||
|
new int[]{104, 102}, SEA_LEVEL));
|
||||||
|
assertArrayEquals(new int[]{94, 96}, PondChannelSettlement.levels(List.of(lower, across), List.of(second, first),
|
||||||
|
new int[]{102, 104}, SEA_LEVEL));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int settle(List<Reach> reaches, Pond pond) {
|
||||||
|
return PondChannelSettlement.levels(reaches, List.of(pond), new int[]{pond.level()}, SEA_LEVEL)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pond pond(long id, PondKind kind, int level, PondMask mask) {
|
||||||
|
return new Pond(id, kind, level, 90, mask.originX(), mask.originZ(), level + 1f, mask, 0L, 0, "water",
|
||||||
|
mask.originX(), mask.originZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PondMask circle(int x, int z, int radius) {
|
||||||
|
int size = 2 * radius + 1;
|
||||||
|
long[] bits = new long[(size * size + 63) >>> 6];
|
||||||
|
for (int dz = -radius; dz <= radius; dz++) {
|
||||||
|
for (int dx = -radius; dx <= radius; dx++) {
|
||||||
|
if (dx * dx + dz * dz <= radius * radius) {
|
||||||
|
int cell = (dz + radius) * size + dx + radius;
|
||||||
|
bits[cell >>> 6] |= 1L << cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new PondMask(x - radius, z - radius, 1, size, size, bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Reach line(long id, int x0, int z0, int x1, int z1, float width, int first, int last) {
|
||||||
|
HydrologyTier tier = new HydrologyTier("river", 0f, width, width, 0f, 0.5f, 2, true, 6, 0, IrisBankStyle.SMOOTH,
|
||||||
|
false, null, "water", List.of());
|
||||||
|
float[] widths = new float[]{width, width};
|
||||||
|
float[] depth = new float[]{2f, 2f};
|
||||||
|
ReachStyle[] styles = new ReachStyle[2];
|
||||||
|
Arrays.fill(styles, ReachStyle.RUN);
|
||||||
|
return new Reach(id, tier, 0L, new float[]{x0, x1}, new float[]{z0, z1}, new int[]{first, last}, widths, depth, styles, 1000f);
|
||||||
|
}
|
||||||
|
}
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
package art.arcane.iris.generation.hydrology.raster;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class RiffleProfileTest {
|
||||||
|
private static final int DRY = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flatPoolsRetainEveryBedDepth() {
|
||||||
|
IntBinaryOperator heads = (x, z) -> 100;
|
||||||
|
Surface surface = new Surface(heads);
|
||||||
|
for (int x = -32; x <= 32; x++) {
|
||||||
|
for (int z = -16; z <= 16; z++) {
|
||||||
|
int bed = 64 + Math.floorMod(x + z, 30);
|
||||||
|
assertEquals(bed, surface.bed(x, z, bed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void twoBlockDropsHaveGradedApproachesAndNoTallSubmergedFace() {
|
||||||
|
Surface surface = new Surface((x, z) -> x < 0 ? 100 : 98);
|
||||||
|
for (int z = -24; z <= 24; z++) {
|
||||||
|
assertEquals(99, surface.bed(-1, z, 92));
|
||||||
|
assertEquals(97, surface.bed(0, z, 92));
|
||||||
|
for (int x = -24; x <= 24; x++) {
|
||||||
|
int bed = surface.bed(x, z, 92);
|
||||||
|
int next = surface.bed(x + 1, z, 92);
|
||||||
|
assertTrue(x + "," + z + " bed " + bed, bed < (x < 0 ? 100 : 98));
|
||||||
|
int headDrop = x == -1 ? 2 : 0;
|
||||||
|
assertTrue(x + "," + z + " face " + bed + " to " + next, Math.abs(bed - next) <= Math.max(1, headDrop));
|
||||||
|
}
|
||||||
|
assertEquals(92, surface.bed(-18, z, 92));
|
||||||
|
assertEquals(92, surface.bed(17, z, 92));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deeperBedsFadeWithoutACliffAtTheRadiusBoundary() {
|
||||||
|
Surface surface = new Surface((x, z) -> x < 0 ? 100 : 98);
|
||||||
|
for (int z = -8; z <= 8; z++) {
|
||||||
|
for (int x : new int[]{-18, -17, -16, 14, 15, 16, 17}) {
|
||||||
|
assertTrue(x + "," + z, Math.abs(surface.bed(x, z, 64) - surface.bed(x + 1, z, 64)) <= 1);
|
||||||
|
}
|
||||||
|
assertEquals(64, surface.bed(-18, z, 64));
|
||||||
|
assertEquals(64, surface.bed(17, z, 64));
|
||||||
|
assertEquals(99, surface.bed(-1, z, 64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lowerPoolsRecoverTheirDepthWithoutAWideFlatShelf() {
|
||||||
|
Surface surface = new Surface((x, z) -> x < 0 ? 104 : 100);
|
||||||
|
for (int z = -8; z <= 8; z++) {
|
||||||
|
assertEquals(103, surface.bed(-1, z, 96));
|
||||||
|
assertEquals(99, surface.bed(0, z, 96));
|
||||||
|
assertTrue(surface.bed(3, z, 96) < 99);
|
||||||
|
assertEquals(96, surface.bed(6, z, 96));
|
||||||
|
assertTrue(surface.bed(-6, z, 96) > 96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nearbyDisconnectedPoolsKeepTheirDepth() {
|
||||||
|
Surface surface = new Surface((x, z) -> z == 0 ? DRY : z > 0 ? 100 : x < 0 ? 100 : 98);
|
||||||
|
for (int x = -24; x <= 24; x++) {
|
||||||
|
assertEquals(92, surface.bed(x, 1, 92));
|
||||||
|
}
|
||||||
|
assertEquals(99, surface.bed(-1, -1, 92));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aNearerLowerDropNeverCutsOffAHigherRiffle() {
|
||||||
|
Surface mixed = new Surface((x, z) -> x < 0 ? 104 : z < 0 ? 102 : 100);
|
||||||
|
Surface high = new Surface((x, z) -> x < 0 ? 104 : 100);
|
||||||
|
Surface low = new Surface((x, z) -> z < 0 ? 102 : 100);
|
||||||
|
for (int x = 0; x <= 20; x++) {
|
||||||
|
for (int z = 0; z <= 20; z++) {
|
||||||
|
int bed = mixed.bed(x, z, 94);
|
||||||
|
assertTrue(x + "," + z + " lost the higher influence", bed >= high.bed(x, z, 94));
|
||||||
|
assertTrue(x + "," + z + " lost the lower influence", bed >= low.bed(x, z, 94));
|
||||||
|
assertTrue(x + "," + z + " east face", Math.abs(bed - mixed.bed(x + 1, z, 94)) <= 1);
|
||||||
|
assertTrue(x + "," + z + " south face", Math.abs(bed - mixed.bed(x, z + 1, 94)) <= 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void diagonalJoinsHaveTheSameProfileAcrossChunkBoundaries() {
|
||||||
|
IntBinaryOperator heads = (x, z) -> x + z < -7 ? 100 : 98;
|
||||||
|
for (int chunkX = -2; chunkX <= 1; chunkX++) {
|
||||||
|
for (int chunkZ = -2; chunkZ <= 1; chunkZ++) {
|
||||||
|
RiffleProfile first = new RiffleProfile(chunkX, chunkZ, heads);
|
||||||
|
RiffleProfile east = new RiffleProfile(chunkX + 1, chunkZ, heads);
|
||||||
|
RiffleProfile south = new RiffleProfile(chunkX, chunkZ + 1, heads);
|
||||||
|
for (int edge = 0; edge < 16; edge++) {
|
||||||
|
int x = (chunkX + 1) << 4;
|
||||||
|
int z = (chunkZ << 4) + edge;
|
||||||
|
double variation = BedProfile.variation(1337L, x, z);
|
||||||
|
assertEquals(first.bed(16, edge, 92, heads.applyAsInt(x, z), variation),
|
||||||
|
east.bed(0, edge, 92, heads.applyAsInt(x, z), variation));
|
||||||
|
x = (chunkX << 4) + edge;
|
||||||
|
z = (chunkZ + 1) << 4;
|
||||||
|
variation = BedProfile.variation(1337L, x, z);
|
||||||
|
assertEquals(first.bed(edge, 16, 92, heads.applyAsInt(x, z), variation),
|
||||||
|
south.bed(edge, 0, 92, heads.applyAsInt(x, z), variation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Surface {
|
||||||
|
private final IntBinaryOperator heads;
|
||||||
|
private final Map<Long, RiffleProfile> chunks = new HashMap<>();
|
||||||
|
|
||||||
|
private Surface(IntBinaryOperator heads) {
|
||||||
|
this.heads = heads;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int bed(int x, int z, int original) {
|
||||||
|
int cx = x >> 4;
|
||||||
|
int cz = z >> 4;
|
||||||
|
long key = ((long) cx << 32) | (cz & 0xFFFFFFFFL);
|
||||||
|
RiffleProfile profile = chunks.computeIfAbsent(key, ignored -> new RiffleProfile(cx, cz, heads));
|
||||||
|
return profile.bed(x & 15, z & 15, original, heads.applyAsInt(x, z), BedProfile.variation(1337L, x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ import java.util.function.DoubleBinaryOperator;
|
|||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
@@ -177,6 +178,51 @@ public class PondPlannerTest {
|
|||||||
assertNull(planner.reflood(sampled.grid(), fine, cups, drained));
|
assertNull(planner.reflood(sampled.grid(), fine, cups, drained));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refloodingBlockMasksKeepsADryFineLatticeMargin() {
|
||||||
|
HydrologySettings settings = settings(config -> config.getPonds().getShore().setRoundness(0D), MIN_Y, MAX_Y);
|
||||||
|
int[][] origins = {{0, 0}, {-533, -303}, {529, -301}};
|
||||||
|
for (int[] origin : origins) {
|
||||||
|
int x = origin[0];
|
||||||
|
int z = origin[1];
|
||||||
|
DoubleBinaryOperator height = (px, pz) -> 100D + 2D * Math.max(Math.abs(px - x - 4), Math.abs(pz - z - 4));
|
||||||
|
Terrain terrain = new Terrain(height, 1f, ALLOW_ALL, null, MIN_Y, MAX_Y);
|
||||||
|
HeightLattice fine = new HeightLattice(terrain, 4, false, 4096);
|
||||||
|
BitSet wet = new BitSet(81);
|
||||||
|
wet.set(0, 81);
|
||||||
|
PondMask mask = new PondMask(x, z, 1, 9, 9, wet.toLongArray());
|
||||||
|
Pond pond = new Pond(1L, PondKind.NATURAL, 109, 100, x + 12, z + 4, 112f, mask, 7L, 0, "water", x + 4, z + 4);
|
||||||
|
DemGrid raw = new DemGrid((Math.floorDiv(x, 8) - 1) * 8, (Math.floorDiv(z, 8) - 1) * 8, 8, 6, 6);
|
||||||
|
|
||||||
|
Pond reflooded = new PondPlanner(settings).reflood(raw, fine, CupSet.EMPTY, pond);
|
||||||
|
|
||||||
|
assertNotNull("reflooded pond at " + x + "," + z, reflooded);
|
||||||
|
assertEquals(pond.level(), reflooded.level());
|
||||||
|
assertEquals(pond.outletReachId(), reflooded.outletReachId());
|
||||||
|
assertTrue(reflooded.mask().contains(x + 4, z + 4));
|
||||||
|
for (int[] node : wetNodes(reflooded.mask())) {
|
||||||
|
assertTrue("reflood remains within its previous mask", mask.contains(node[0], node[1]));
|
||||||
|
}
|
||||||
|
assertContained(reflooded, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refloodingRejectsWetRimsClippedByThePlanningGrid() {
|
||||||
|
HydrologySettings settings = settings(config -> config.getPonds().getShore().setRoundness(0D), MIN_Y, MAX_Y);
|
||||||
|
Terrain terrain = new Terrain((x, z) -> x >= 0 && x <= 8 && z >= 0 && z <= 8 ? 100D : 112D,
|
||||||
|
1f, ALLOW_ALL, null, MIN_Y, MAX_Y);
|
||||||
|
BitSet wet = new BitSet(81);
|
||||||
|
wet.set(0, 81);
|
||||||
|
Pond pond = new Pond(1L, PondKind.NATURAL, 109, 100, 12, 4, 112f, new PondMask(0, 0, 1, 9, 9, wet.toLongArray()),
|
||||||
|
0L, 0, "water", 4, 4);
|
||||||
|
|
||||||
|
Pond reflooded = new PondPlanner(settings).reflood(new DemGrid(0, 0, 8, 2, 2),
|
||||||
|
new HeightLattice(terrain, 4, false, 4096), CupSet.EMPTY, pond);
|
||||||
|
|
||||||
|
assertNull(reflooded);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void seededCupOnSlopeIsPerchedAndContained() {
|
public void seededCupOnSlopeIsPerchedAndContained() {
|
||||||
HydrologySettings settings = settings(config -> {
|
HydrologySettings settings = settings(config -> {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
|||||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
import art.arcane.iris.generation.hydrology.water.Pond;
|
import art.arcane.iris.generation.hydrology.water.Pond;
|
||||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.ReachStyle;
|
||||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||||
import art.arcane.iris.generation.mantle.MantleHydrologyComponent;
|
import art.arcane.iris.generation.mantle.MantleHydrologyComponent;
|
||||||
import art.arcane.iris.generation.mantle.MantleWriter;
|
import art.arcane.iris.generation.mantle.MantleWriter;
|
||||||
@@ -84,23 +85,18 @@ public class EffectiveWaterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void cornerColumnsBelowTwoOneBlockHigherNeighboursBecomeRimsAtTheUpperHead() {
|
public void lowerCornerColumnsRemainWetBetweenHigherNeighbours() {
|
||||||
long corner = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(80), wet(80), wet(79), NONE);
|
long corner = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(81), wet(81), wet(79), NONE);
|
||||||
assertEquals(DRY, EffectiveWater.head(corner));
|
assertEquals(EffectiveWater.column(77, 79), corner);
|
||||||
assertEquals(80, EffectiveWater.ground(corner));
|
|
||||||
|
|
||||||
long mixed = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(80), wet(80), wet(86), NONE);
|
long mixed = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(81), wet(81), wet(87), NONE);
|
||||||
assertEquals(DRY, EffectiveWater.head(mixed));
|
assertEquals(EffectiveWater.column(77, 79), mixed);
|
||||||
assertEquals(86, EffectiveWater.ground(mixed));
|
|
||||||
|
|
||||||
long edge = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(80), wet(79), NONE, NONE);
|
|
||||||
assertEquals(EffectiveWater.column(77, 79), edge);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void columnsAboveADropOfTwoOrMoreHoldWaterOnlyInTheirTopBlock() {
|
public void wetColumnsKeepTheirCanonicalBedAcrossDrops() {
|
||||||
assertEquals(EffectiveWater.column(98, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), wet(79), wet(99), NONE, NONE));
|
assertEquals(EffectiveWater.column(94, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), wet(79), wet(99), NONE, NONE));
|
||||||
assertEquals(EffectiveWater.column(98, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), NONE, wet(97), NONE, NONE));
|
assertEquals(EffectiveWater.column(94, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), NONE, wet(97), NONE, NONE));
|
||||||
assertEquals(EffectiveWater.column(94, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), wet(98), NONE, NONE, NONE));
|
assertEquals(EffectiveWater.column(94, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), wet(98), NONE, NONE, NONE));
|
||||||
assertEquals(EffectiveWater.column(98, 99), EffectiveWater.settle(EffectiveWater.column(98, 99), wet(70), NONE, NONE, NONE));
|
assertEquals(EffectiveWater.column(98, 99), EffectiveWater.settle(EffectiveWater.column(98, 99), wet(70), NONE, NONE, NONE));
|
||||||
}
|
}
|
||||||
@@ -197,33 +193,27 @@ public class EffectiveWaterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout = 180_000L)
|
@Test(timeout = 180_000L)
|
||||||
public void staircaseStepsTurnExactlyTheirCornerColumnsIntoRims() throws Exception {
|
public void diagonalStaircaseStepsPreserveEveryWetColumnWithoutSourceConversion() throws Exception {
|
||||||
Scene scene = staircase();
|
Scene scene = staircase();
|
||||||
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
|
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
|
||||||
Drops drops = new Drops(run.complex(), 0, 7, 0, 7);
|
Drops drops = new Drops(run.complex(), 0, 7, 0, 7);
|
||||||
int rims = 0;
|
int wet = 0;
|
||||||
for (int x = 24; x <= 104; x++) {
|
for (int x = 24; x <= 104; x++) {
|
||||||
for (int z = 24; z <= 104; z++) {
|
for (int z = 24; z <= 104; z++) {
|
||||||
int head = rasterHead(run.hydrology(), x, z);
|
int head = rasterHead(run.hydrology(), x, z);
|
||||||
if (head == DRY) {
|
if (head == DRY) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int above = 0;
|
wet++;
|
||||||
int highest = DRY;
|
assertEquals("water head at " + x + "," + z, head, drops.head(x, z));
|
||||||
|
assertTrue("submerged bed at " + x + "," + z, drops.ground(x, z) < head);
|
||||||
for (int[] side : SIDES) {
|
for (int[] side : SIDES) {
|
||||||
int neighbour = rasterHead(run.hydrology(), x + side[0], z + side[1]);
|
int neighbour = rasterHead(run.hydrology(), x + side[0], z + side[1]);
|
||||||
above += neighbour == head + 1 ? 1 : 0;
|
assertFalse("one-block drop at " + x + "," + z, neighbour != DRY && Math.abs(neighbour - head) == 1);
|
||||||
highest = Math.max(highest, neighbour);
|
|
||||||
}
|
|
||||||
boolean rim = above >= 2;
|
|
||||||
assertEquals("rim at " + x + "," + z, rim, drops.head(x, z) == DRY);
|
|
||||||
if (rim) {
|
|
||||||
rims++;
|
|
||||||
assertEquals("rim ground at " + x + "," + z, highest, drops.ground(x, z));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertTrue("rims " + rims, rims > 10);
|
assertTrue("wet columns " + wet, wet > 200);
|
||||||
drops.assertSealed(24, 104, 24, 104);
|
drops.assertSealed(24, 104, 24, 104);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,7 +224,6 @@ public class EffectiveWaterTest {
|
|||||||
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
|
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
|
||||||
Drops drops = new Drops(run.complex(), 0, 7, 0, 5);
|
Drops drops = new Drops(run.complex(), 0, 7, 0, 5);
|
||||||
int lips = 0;
|
int lips = 0;
|
||||||
int raised = 0;
|
|
||||||
int falls = 0;
|
int falls = 0;
|
||||||
for (int x = 16; x <= 112; x++) {
|
for (int x = 16; x <= 112; x++) {
|
||||||
for (int z = 30; z <= 50; z++) {
|
for (int z = 30; z <= 50; z++) {
|
||||||
@@ -250,17 +239,22 @@ public class EffectiveWaterTest {
|
|||||||
if (lip) {
|
if (lip) {
|
||||||
lips++;
|
lips++;
|
||||||
assertEquals("lip ground at " + x + "," + z, head - 1, drops.ground(x, z));
|
assertEquals("lip ground at " + x + "," + z, head - 1, drops.ground(x, z));
|
||||||
raised += rasterGround(run.hydrology(), x, z) < head - 1 ? 1 : 0;
|
assertEquals("canonical lip at " + x + "," + z, rasterGround(run.hydrology(), x, z), drops.ground(x, z));
|
||||||
}
|
}
|
||||||
if (drops.falling(x, head + 1, z)) {
|
if (drops.falling(x, head + 1, z)) {
|
||||||
falls++;
|
falls++;
|
||||||
assertTrue(drops.falling(x, head + 3, z));
|
int highest = head;
|
||||||
assertFalse(drops.falling(x, head + 4, z));
|
for (int[] side : SIDES) {
|
||||||
|
highest = Math.max(highest, drops.head(x + side[0], z + side[1]));
|
||||||
|
}
|
||||||
|
for (int y = head + 1; y <= highest; y++) {
|
||||||
|
assertTrue("fall column at " + x + "," + y + "," + z, drops.falling(x, y, z));
|
||||||
|
}
|
||||||
|
assertFalse(drops.falling(x, highest + 1, z));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertTrue("lips " + lips, lips > 10);
|
assertTrue("lips " + lips, lips > 10);
|
||||||
assertTrue("lips raised from deeper beds " + raised, raised > 0);
|
|
||||||
assertTrue("falls " + falls, falls > 10);
|
assertTrue("falls " + falls, falls > 10);
|
||||||
drops.assertSealed(16, 112, 30, 50);
|
drops.assertSealed(16, 112, 30, 50);
|
||||||
}
|
}
|
||||||
@@ -268,11 +262,24 @@ public class EffectiveWaterTest {
|
|||||||
|
|
||||||
@Test(timeout = 180_000L)
|
@Test(timeout = 180_000L)
|
||||||
public void noFallAnywhereMeetsTheSourceConversionCondition() throws Exception {
|
public void noFallAnywhereMeetsTheSourceConversionCondition() throws Exception {
|
||||||
for (Scene scene : List.of(staircase(), cascade(), outlet())) {
|
for (Scene scene : List.of(staircase(), cascade(), outlet(), mouth())) {
|
||||||
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
|
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
|
||||||
Drops drops = new Drops(run.complex(), 0, 7, 0, 7);
|
Drops drops = new Drops(run.complex(), 0, 7, 0, 7);
|
||||||
|
int wet = 0;
|
||||||
|
for (int x = 8; x <= 120; x++) {
|
||||||
|
for (int z = 8; z <= 120; z++) {
|
||||||
|
int head = rasterHead(run.hydrology(), x, z);
|
||||||
|
if (head != DRY) {
|
||||||
|
wet++;
|
||||||
|
assertEquals(scene.name + " water stays connected at " + x + "," + z, head, drops.head(x, z));
|
||||||
|
assertTrue(scene.name + " bed stays submerged at " + x + "," + z, drops.ground(x, z) < head);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(scene.name + " wet columns " + wet, wet > 100);
|
||||||
assertEquals(scene.name + " conversions", 0, drops.conversions(8, 120, 8, 120));
|
assertEquals(scene.name + " conversions", 0, drops.conversions(8, 120, 8, 120));
|
||||||
assertEquals(scene.name + " unsupported falls", 0, drops.unsupported(8, 120, 8, 120));
|
assertEquals(scene.name + " unsupported falls", 0, drops.unsupported(8, 120, 8, 120));
|
||||||
|
drops.assertSealed(8, 120, 8, 120);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,6 +331,21 @@ public class EffectiveWaterTest {
|
|||||||
List.of(WaterScene.pond(74L, 191, 48, 48, 10)));
|
List.of(WaterScene.pond(74L, 191, 48, 48, 10)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Scene mouth() {
|
||||||
|
IntBinaryOperator natural = (x, z) -> x < 64 ? SEA + 12 : SEA - 8;
|
||||||
|
Reach reach = WaterScene.line(75L, WaterScene.stream(), 8, 40, 120, 40,
|
||||||
|
(x, z) -> x < 64 ? SEA + 11 : SEA);
|
||||||
|
ReachStyle[] styles = reach.style().clone();
|
||||||
|
for (int point = 0; point < styles.length; point++) {
|
||||||
|
if (reach.xs()[point] >= 64) {
|
||||||
|
styles[point] = ReachStyle.MOUTH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reach coastal = new Reach(reach.id(), reach.tier(), reach.downstreamId(), reach.xs(), reach.zs(), reach.grade(),
|
||||||
|
reach.halfWidth(), reach.depth(), styles, reach.catchment());
|
||||||
|
return new Scene("mouth", natural, List.of(coastal), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
private HydrologyFixture fixture() throws Exception {
|
private HydrologyFixture fixture() throws Exception {
|
||||||
return new HydrologyFixture(HydrologyFixture.pack(temporaryFolder.newFolder().toPath(), null, null, true));
|
return new HydrologyFixture(HydrologyFixture.pack(temporaryFolder.newFolder().toPath(), null, null, true));
|
||||||
}
|
}
|
||||||
@@ -410,9 +432,11 @@ public class EffectiveWaterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int[] column(int x, int z) {
|
private int[] column(int x, int z) {
|
||||||
return columns.computeIfAbsent(((long) x << 32) | (z & 0xFFFFFFFFL), key -> new int[]{
|
return columns.computeIfAbsent(((long) x << 32) | (z & 0xFFFFFFFFL), key -> {
|
||||||
(int) Math.round(complex.resolveHydrologyTerrainHeight(x, z)),
|
int ground = (int) Math.round(complex.resolveHydrologyTerrainHeight(x, z));
|
||||||
complex.resolveWaterRole(x, z).wet() ? (int) Math.round(complex.resolveWaterHead(x, z)) : DRY});
|
int head = (int) Math.round(complex.resolveWaterHead(x, z));
|
||||||
|
return new int[]{ground, head > ground ? head : DRY};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean still(int x, int y, int z) {
|
private boolean still(int x, int y, int z) {
|
||||||
|
|||||||
+4
-4
@@ -206,7 +206,7 @@ public class IrisComplexHeightStreamTest {
|
|||||||
assertTrue(fixture.complex.isHydrologyPlanned(8, 8));
|
assertTrue(fixture.complex.isHydrologyPlanned(8, 8));
|
||||||
bulk.fillChunkDoubles(0, 0, values);
|
bulk.fillChunkDoubles(0, 0, values);
|
||||||
assertBits(landed.terrainHeight(8, 8), values[8 << 4 | 8]);
|
assertBits(landed.terrainHeight(8, 8), values[8 << 4 | 8]);
|
||||||
assertBits(HydrologyFixture.POND_LEVEL, fixture.complex.getHeightFluidStream().getDouble(8, 8));
|
assertBits(HydrologyFixture.POND_LEVEL - 1, fixture.complex.getHeightFluidStream().getDouble(8, 8));
|
||||||
fixture.mainThread.set(false);
|
fixture.mainThread.set(false);
|
||||||
bulk.fillChunkDoubles(0, 0, values);
|
bulk.fillChunkDoubles(0, 0, values);
|
||||||
assertBits(landed.terrainHeight(8, 8), values[8 << 4 | 8]);
|
assertBits(landed.terrainHeight(8, 8), values[8 << 4 | 8]);
|
||||||
@@ -247,14 +247,14 @@ public class IrisComplexHeightStreamTest {
|
|||||||
public void waterStreamsFollowTheRaster() throws Exception {
|
public void waterStreamsFollowTheRaster() throws Exception {
|
||||||
try (HydrologyFixture fixture = new HydrologyFixture(hydrologyPack())) {
|
try (HydrologyFixture fixture = new HydrologyFixture(hydrologyPack())) {
|
||||||
ChunkWaterRaster raster = fixture.hydrology.runtime.raster(0, 0);
|
ChunkWaterRaster raster = fixture.hydrology.runtime.raster(0, 0);
|
||||||
assertBits(HydrologyFixture.POND_LEVEL, fixture.complex.getWaterHeadStream().getDouble(8, 8));
|
assertBits(HydrologyFixture.POND_LEVEL - 1, fixture.complex.getWaterHeadStream().getDouble(8, 8));
|
||||||
assertSame(WaterRole.POND, fixture.complex.getWaterRoleStream().get(8, 8));
|
assertSame(WaterRole.POND, fixture.complex.getWaterRoleStream().get(8, 8));
|
||||||
assertSame(raster.role(15, 15), fixture.complex.getWaterRoleStream().get(15.5, 15.5));
|
assertSame(raster.role(15, 15), fixture.complex.getWaterRoleStream().get(15.5, 15.5));
|
||||||
assertBits(HydrologyFixture.SEA_LEVEL, fixture.complex.getWaterHeadStream().getDouble(640, 640));
|
assertBits(HydrologyFixture.SEA_LEVEL, fixture.complex.getWaterHeadStream().getDouble(640, 640));
|
||||||
assertSame(WaterRole.NONE, fixture.complex.getWaterRoleStream().get(640, 640));
|
assertSame(WaterRole.NONE, fixture.complex.getWaterRoleStream().get(640, 640));
|
||||||
EngineMantle mantle = mock(EngineMantle.class, CALLS_REAL_METHODS);
|
EngineMantle mantle = mock(EngineMantle.class, CALLS_REAL_METHODS);
|
||||||
doReturn(fixture.complex).when(mantle).getComplex();
|
doReturn(fixture.complex).when(mantle).getComplex();
|
||||||
assertEquals(HydrologyFixture.POND_LEVEL, mantle.getFluidHeight(8, 8));
|
assertEquals(HydrologyFixture.POND_LEVEL - 1, mantle.getFluidHeight(8, 8));
|
||||||
assertEquals(HydrologyFixture.SEA_LEVEL, mantle.getFluidHeight(640, 640));
|
assertEquals(HydrologyFixture.SEA_LEVEL, mantle.getFluidHeight(640, 640));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,7 +266,7 @@ public class IrisComplexHeightStreamTest {
|
|||||||
doReturn(fixture.engine).when(placer).getEngine();
|
doReturn(fixture.engine).when(placer).getEngine();
|
||||||
doReturn(fixture.engine.getDimension().getFluidHeight()).when(placer).getFluidHeight();
|
doReturn(fixture.engine.getDimension().getFluidHeight()).when(placer).getFluidHeight();
|
||||||
|
|
||||||
assertEquals(HydrologyFixture.POND_LEVEL, placer.getFluidHeight(8, 8));
|
assertEquals(HydrologyFixture.POND_LEVEL - 1, placer.getFluidHeight(8, 8));
|
||||||
assertEquals(HydrologyFixture.SEA_LEVEL, placer.getFluidHeight(640, 640));
|
assertEquals(HydrologyFixture.SEA_LEVEL, placer.getFluidHeight(640, 640));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-18
@@ -165,7 +165,7 @@ public class IrisComplexTransitionTest {
|
|||||||
if (terrain > 150) {
|
if (terrain > 150) {
|
||||||
berms++;
|
berms++;
|
||||||
assertEquals(terrain, ground, 0D);
|
assertEquals(terrain, ground, 0D);
|
||||||
assertTrue(ground >= 156);
|
assertTrue(ground >= 155);
|
||||||
}
|
}
|
||||||
assertContained(complex, x, z, ground);
|
assertContained(complex, x, z, ground);
|
||||||
}
|
}
|
||||||
@@ -195,13 +195,13 @@ public class IrisComplexTransitionTest {
|
|||||||
double ground = complex.resolveHydrologyTerrainHeight(x, z);
|
double ground = complex.resolveHydrologyTerrainHeight(x, z);
|
||||||
assertTrue(raster.terrainHeight(x, z) < 150);
|
assertTrue(raster.terrainHeight(x, z) < 150);
|
||||||
assertContained(complex, x, z, ground);
|
assertContained(complex, x, z, ground);
|
||||||
if (ground >= 148) {
|
if (ground >= 147) {
|
||||||
dried++;
|
dried++;
|
||||||
assertEquals(127D, complex.resolveWaterHead(x, z), 0D);
|
assertEquals(127D, complex.resolveWaterHead(x, z), 0D);
|
||||||
assertSame(WaterRole.NONE, complex.resolveWaterRole(x, z));
|
assertSame(WaterRole.NONE, complex.resolveWaterRole(x, z));
|
||||||
} else {
|
} else {
|
||||||
wet++;
|
wet++;
|
||||||
assertEquals(148D, complex.resolveWaterHead(x, z), 0D);
|
assertEquals(147D, complex.resolveWaterHead(x, z), 0D);
|
||||||
assertSame(WaterRole.POND, complex.resolveWaterRole(x, z));
|
assertSame(WaterRole.POND, complex.resolveWaterRole(x, z));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ public class IrisComplexTransitionTest {
|
|||||||
FakeHydrology hydrology = seam.hydrology;
|
FakeHydrology hydrology = seam.hydrology;
|
||||||
IrisComplex complex = seam.natural((x, z) -> 150D).complex();
|
IrisComplex complex = seam.natural((x, z) -> 150D).complex();
|
||||||
for (int z = 7; z <= 9; z++) {
|
for (int z = 7; z <= 9; z++) {
|
||||||
assertEquals(148, hydrology.runtime.raster(1, 0).waterHead(0, z));
|
assertEquals(147, hydrology.runtime.raster(1, 0).waterHead(0, z));
|
||||||
assertSame(WaterRole.NONE, complex.resolveWaterRole(16, z));
|
assertSame(WaterRole.NONE, complex.resolveWaterRole(16, z));
|
||||||
assertEquals(150D, complex.resolveHydrologyTerrainHeight(16, z), 0D);
|
assertEquals(150D, complex.resolveHydrologyTerrainHeight(16, z), 0D);
|
||||||
}
|
}
|
||||||
@@ -308,9 +308,9 @@ public class IrisComplexTransitionTest {
|
|||||||
}
|
}
|
||||||
if (stage.raster(1).waterHead(lx, lz) != Integer.MIN_VALUE && oldChunkWeight(x, lz) > 0D && oldChunkWeight(x - 1, lz) == 0D) {
|
if (stage.raster(1).waterHead(lx, lz) != Integer.MIN_VALUE && oldChunkWeight(x, lz) > 0D && oldChunkWeight(x - 1, lz) == 0D) {
|
||||||
dams++;
|
dams++;
|
||||||
assertTrue(x + "," + lz + " dam lost its crest", blended.getRaw(lx, 157, lz) == stage.stone);
|
assertTrue(x + "," + lz + " dam lost its crest", blended.getRaw(lx, 156, lz) == stage.stone);
|
||||||
}
|
}
|
||||||
if (blended.getRaw(lx, 156, lz) != stage.water) {
|
if (blended.getRaw(lx, 155, lz) != stage.water) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
wet++;
|
wet++;
|
||||||
@@ -318,7 +318,7 @@ public class IrisComplexTransitionTest {
|
|||||||
int nx = lx + side[0];
|
int nx = lx + side[0];
|
||||||
int nz = lz + side[1];
|
int nz = lz + side[1];
|
||||||
if (nx >= 0 && nx < 16 && nz >= 0 && nz < 16) {
|
if (nx >= 0 && nx < 16 && nz >= 0 && nz < 16) {
|
||||||
assertFalse(x + "," + lz + " leaks toward " + (16 + nx) + "," + nz, stage.open(blended.getRaw(nx, 156, nz)));
|
assertFalse(x + "," + lz + " leaks toward " + (16 + nx) + "," + nz, stage.open(blended.getRaw(nx, 155, nz)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,13 +339,13 @@ public class IrisComplexTransitionTest {
|
|||||||
int dams = 0;
|
int dams = 0;
|
||||||
int water = 0;
|
int water = 0;
|
||||||
for (int lz = 0; lz < 16; lz++) {
|
for (int lz = 0; lz < 16; lz++) {
|
||||||
assertTrue(stage.complex.naturalTrueHeight(16, lz) >= 156);
|
assertTrue(stage.complex.naturalTrueHeight(16, lz) >= 155);
|
||||||
if (raster.waterHead(1, lz) != Integer.MIN_VALUE) {
|
if (raster.waterHead(1, lz) != Integer.MIN_VALUE) {
|
||||||
dams++;
|
dams++;
|
||||||
assertSame(WaterRole.NONE, stage.complex.resolveWaterRole(17, lz));
|
assertSame(WaterRole.NONE, stage.complex.resolveWaterRole(17, lz));
|
||||||
assertTrue(17 + "," + lz + " dam at " + stage.complex.resolveHydrologyTerrainHeight(17, lz),
|
assertTrue(17 + "," + lz + " dam at " + stage.complex.resolveHydrologyTerrainHeight(17, lz),
|
||||||
stage.complex.resolveHydrologyTerrainHeight(17, lz) >= 157D);
|
stage.complex.resolveHydrologyTerrainHeight(17, lz) >= 156D);
|
||||||
assertSame(stage.stone, blended.getRaw(1, 157, lz));
|
assertSame(stage.stone, blended.getRaw(1, 156, lz));
|
||||||
}
|
}
|
||||||
for (int lx = 0; lx < 16; lx++) {
|
for (int lx = 0; lx < 16; lx++) {
|
||||||
for (int y = 0; y < TransitionStage.HEIGHT; y++) {
|
for (int y = 0; y < TransitionStage.HEIGHT; y++) {
|
||||||
@@ -373,7 +373,7 @@ public class IrisComplexTransitionTest {
|
|||||||
public void seamDamHoldsNewWaterBesideAnOpenOldRiverOrLakeEdge() throws Exception {
|
public void seamDamHoldsNewWaterBesideAnOpenOldRiverOrLakeEdge() throws Exception {
|
||||||
try (TransitionStage stage = new TransitionStage(IrisComplexTransitionTest::oldChunkWeight, true, (x, z) -> 150.3D,
|
try (TransitionStage stage = new TransitionStage(IrisComplexTransitionTest::oldChunkWeight, true, (x, z) -> 150.3D,
|
||||||
TransitionStage.savedColumn(140, 160, -1))) {
|
TransitionStage.savedColumn(140, 160, -1))) {
|
||||||
assertTrue(stage.complex.resolveWaterHead(16, 4) < 156D);
|
assertTrue(stage.complex.resolveWaterHead(16, 4) < 155D);
|
||||||
Hunk<NativeBlockState> blended = stage.blend(1, stage.paint(1));
|
Hunk<NativeBlockState> blended = stage.blend(1, stage.paint(1));
|
||||||
assertTrue(stage.dams(blended) > 0);
|
assertTrue(stage.dams(blended) > 0);
|
||||||
stage.assertNoWaterTouchesAir(blended);
|
stage.assertNoWaterTouchesAir(blended);
|
||||||
@@ -421,9 +421,9 @@ public class IrisComplexTransitionTest {
|
|||||||
if (stage.raster(1).waterHead(1, lz) != Integer.MIN_VALUE) {
|
if (stage.raster(1).waterHead(1, lz) != Integer.MIN_VALUE) {
|
||||||
wet++;
|
wet++;
|
||||||
assertSame(WaterRole.POND, stage.complex.resolveWaterRole(17, lz));
|
assertSame(WaterRole.POND, stage.complex.resolveWaterRole(17, lz));
|
||||||
assertEquals(156D, stage.complex.resolveWaterHead(17, lz), 0D);
|
assertEquals(155D, stage.complex.resolveWaterHead(17, lz), 0D);
|
||||||
assertSame(stage.water, blended.getRaw(1, 156, lz));
|
assertSame(stage.water, blended.getRaw(1, 155, lz));
|
||||||
assertSame(stage.stone, blended.getRaw(0, 156, lz));
|
assertSame(stage.stone, blended.getRaw(0, 155, lz));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertTrue(wet > 0);
|
assertTrue(wet > 0);
|
||||||
@@ -455,7 +455,7 @@ public class IrisComplexTransitionTest {
|
|||||||
boolean beside = oldChunkWeight(x + 1, z) == 0D || oldChunkWeight(x - 1, z) == 0D
|
boolean beside = oldChunkWeight(x + 1, z) == 0D || oldChunkWeight(x - 1, z) == 0D
|
||||||
|| oldChunkWeight(x, z + 1) == 0D || oldChunkWeight(x, z - 1) == 0D;
|
|| oldChunkWeight(x, z + 1) == 0D || oldChunkWeight(x, z - 1) == 0D;
|
||||||
if (raster.waterHead(index & 15, index >> 4) != Integer.MIN_VALUE && oldChunkWeight(x, z) > 0D && beside
|
if (raster.waterHead(index & 15, index >> 4) != Integer.MIN_VALUE && oldChunkWeight(x, z) > 0D && beside
|
||||||
&& stage.complex.resolveHydrologyTerrainHeight(x, z) < 156D + 1D) {
|
&& stage.complex.resolveHydrologyTerrainHeight(x, z) < 155D + 1D) {
|
||||||
fail(x + "," + z + " stays wet beside a saved column that opens below its head");
|
fail(x + "," + z + " stays wet beside a saved column that opens below its head");
|
||||||
}
|
}
|
||||||
if (role == WaterRole.NONE && raster.waterHead(index & 15, index >> 4) != Integer.MIN_VALUE && beside
|
if (role == WaterRole.NONE && raster.waterHead(index & 15, index >> 4) != Integer.MIN_VALUE && beside
|
||||||
@@ -663,8 +663,8 @@ public class IrisComplexTransitionTest {
|
|||||||
for (int lz = 0; lz < 16; lz++) {
|
for (int lz = 0; lz < 16; lz++) {
|
||||||
if (raster(1).waterHead(1, lz) != Integer.MIN_VALUE && complex.resolveWaterRole(17, lz) == WaterRole.NONE) {
|
if (raster(1).waterHead(1, lz) != Integer.MIN_VALUE && complex.resolveWaterRole(17, lz) == WaterRole.NONE) {
|
||||||
dams++;
|
dams++;
|
||||||
assertTrue(17 + "," + lz + " dam below the crest", complex.resolveHydrologyTerrainHeight(17, lz) >= 157D);
|
assertTrue(17 + "," + lz + " dam below the crest", complex.resolveHydrologyTerrainHeight(17, lz) >= 156D);
|
||||||
assertSame(stone, blended.getRaw(1, 157, lz));
|
assertSame(stone, blended.getRaw(1, 156, lz));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dams;
|
return dams;
|
||||||
@@ -821,7 +821,7 @@ public class IrisComplexTransitionTest {
|
|||||||
dams++;
|
dams++;
|
||||||
assertEquals(127D, head, 0D);
|
assertEquals(127D, head, 0D);
|
||||||
assertSame(WaterRole.NONE, role);
|
assertSame(WaterRole.NONE, role);
|
||||||
assertTrue(x + "," + z + " dam at " + ground, ground >= 157D);
|
assertTrue(x + "," + z + " dam at " + ground, ground >= 156D);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-4
@@ -24,6 +24,7 @@ import art.arcane.volmlib.util.stream.ProceduralStream;
|
|||||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.Before;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -51,6 +52,14 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
|||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public final PlatformBinding platform = PlatformBinding.mockPlatform();
|
public final PlatformBinding platform = PlatformBinding.mockPlatform();
|
||||||
|
private final NativeBlockState submergedDirt = state("minecraft:dirt");
|
||||||
|
private final NativeBlockState submergedNetherrack = state("minecraft:netherrack");
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bindSubmergedSoils() {
|
||||||
|
when(platform.registries().block("minecraft:dirt")).thenReturn(submergedDirt);
|
||||||
|
when(platform.registries().block("minecraft:netherrack")).thenReturn(submergedNetherrack);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void hydrologyOwnedFluidBypassesBiomeSeaLayers() {
|
public void hydrologyOwnedFluidBypassesBiomeSeaLayers() {
|
||||||
@@ -100,7 +109,7 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
|||||||
|
|
||||||
Hunk<NativeBlockState> output = painter.paint();
|
Hunk<NativeBlockState> output = painter.paint();
|
||||||
|
|
||||||
assertSame(role.name(), painter.grass, output.get(0, GROUND, 0));
|
assertSame(role.name(), submergedDirt, output.get(0, GROUND, 0));
|
||||||
for (int y = GROUND + 1; y <= HEAD; y++) {
|
for (int y = GROUND + 1; y <= HEAD; y++) {
|
||||||
assertSame(role + " y " + y, painter.channelFluid, output.get(0, y, 0));
|
assertSame(role + " y " + y, painter.channelFluid, output.get(0, y, 0));
|
||||||
}
|
}
|
||||||
@@ -117,7 +126,7 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
|||||||
|
|
||||||
Hunk<NativeBlockState> sea = ocean.paint();
|
Hunk<NativeBlockState> sea = ocean.paint();
|
||||||
|
|
||||||
assertSame("the sea floor keeps the biome's own top", ocean.grass, sea.get(0, GROUND, 0));
|
assertSame("the sea floor keeps a submerged soil top", submergedDirt, sea.get(0, GROUND, 0));
|
||||||
assertSame(ocean.dirt, sea.get(0, GROUND - 1, 0));
|
assertSame(ocean.dirt, sea.get(0, GROUND - 1, 0));
|
||||||
assertSame("the slot takes the sea layers", ocean.ice, sea.get(0, HEAD, 0));
|
assertSame("the slot takes the sea layers", ocean.ice, sea.get(0, HEAD, 0));
|
||||||
for (int y = GROUND + 1; y < HEAD; y++) {
|
for (int y = GROUND + 1; y < HEAD; y++) {
|
||||||
@@ -178,7 +187,7 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
|||||||
NativeBlockState expectedTop = switch (role) {
|
NativeBlockState expectedTop = switch (role) {
|
||||||
case NONE -> painter.grass;
|
case NONE -> painter.grass;
|
||||||
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL -> painter.bed;
|
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL -> painter.bed;
|
||||||
case INLET -> painter.grass;
|
case INLET -> submergedDirt;
|
||||||
case SHORE, POND_SHORE, DAM -> painter.shore;
|
case SHORE, POND_SHORE, DAM -> painter.shore;
|
||||||
case BANK -> painter.bank;
|
case BANK -> painter.bank;
|
||||||
};
|
};
|
||||||
@@ -192,11 +201,68 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
|||||||
}
|
}
|
||||||
materials.setBank(null).setShore(null).setBed(null);
|
materials.setBank(null).setShore(null).setBed(null);
|
||||||
output = painter.paint();
|
output = painter.paint();
|
||||||
assertSame(role.name(), painter.grass, output.get(0, GROUND, 0));
|
assertSame(role.name(), role.wet() ? submergedDirt : painter.grass, output.get(0, GROUND, 0));
|
||||||
assertSame(role.name(), painter.dirt, output.get(0, GROUND - 1, 0));
|
assertSame(role.name(), painter.dirt, output.get(0, GROUND - 1, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void submergedSoilLayersAndExplicitPalettesUseStableSubstrates() {
|
||||||
|
for (String key : List.of("grass_block[snowy=false]", "mycelium", "dirt_path", "farmland[moisture=7]",
|
||||||
|
"crimson_nylium", "warped_nylium")) {
|
||||||
|
NativeBlockState livingSoil = state("minecraft:" + key);
|
||||||
|
NativeBlockState expected = key.endsWith("nylium") ? submergedNetherrack : submergedDirt;
|
||||||
|
for (WaterRole role : WET) {
|
||||||
|
Painter painter = new Painter(role, HEAD);
|
||||||
|
painter.layers(livingSoil, livingSoil);
|
||||||
|
painter.materials(true).setBed(painter.palette(livingSoil));
|
||||||
|
|
||||||
|
Hunk<NativeBlockState> output = painter.paint();
|
||||||
|
|
||||||
|
assertSame(role + " " + key, expected, output.get(0, GROUND, 0));
|
||||||
|
assertSame(role + " submerged side " + key, expected, output.get(0, GROUND - 1, 0));
|
||||||
|
assertSame(painter.channelFluid, output.get(0, GROUND + 1, 0));
|
||||||
|
}
|
||||||
|
for (WaterRole role : List.of(WaterRole.NONE, WaterRole.BANK, WaterRole.SHORE, WaterRole.POND_SHORE)) {
|
||||||
|
Painter painter = new Painter(role, GROUND);
|
||||||
|
painter.layers(livingSoil, livingSoil);
|
||||||
|
assertSame(role + " " + key, livingSoil, painter.paint().get(0, GROUND, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void submergedImageMapSoilUsesStableSubstrate() {
|
||||||
|
Painter painter = new Painter(WaterRole.CHANNEL, HEAD);
|
||||||
|
when(painter.complex.getImageMapRuntime().sampleSurfaceBlock(11, -4)).thenReturn(painter.grass);
|
||||||
|
|
||||||
|
assertSame(submergedDirt, painter.paint().get(0, GROUND, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void submergedPodzolAndMossKeepTheirAuthoredMaterials() {
|
||||||
|
for (String key : List.of("podzol[snowy=false]", "moss_block", "mud", "clay")) {
|
||||||
|
Painter painter = new Painter(WaterRole.POND, HEAD);
|
||||||
|
NativeBlockState stable = state("minecraft:" + key);
|
||||||
|
painter.layers(stable, stable);
|
||||||
|
Hunk<NativeBlockState> output = painter.paint();
|
||||||
|
assertSame(key, stable, output.get(0, GROUND, 0));
|
||||||
|
assertSame(key, stable, output.get(0, GROUND - 1, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dryLowerTerrainSpansKeepTheirLivingSoilsBelowAWetBed() {
|
||||||
|
Painter painter = new Painter(WaterRole.POND, HEAD);
|
||||||
|
painter.carved();
|
||||||
|
|
||||||
|
Hunk<NativeBlockState> output = painter.paint();
|
||||||
|
|
||||||
|
assertSame(submergedDirt, output.get(0, GROUND, 0));
|
||||||
|
assertSame(painter.grass, output.get(0, 40, 0));
|
||||||
|
assertSame(painter.dirt, output.get(0, 39, 0));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sandSurvivesUnlessTheMaterialsKeepFallingBlocksAwayFromTheWater() {
|
public void sandSurvivesUnlessTheMaterialsKeepFallingBlocksAwayFromTheWater() {
|
||||||
for (WaterRole role : List.of(WaterRole.CHANNEL, WaterRole.POND, WaterRole.SHORE, WaterRole.BANK)) {
|
for (WaterRole role : List.of(WaterRole.CHANNEL, WaterRole.POND, WaterRole.SHORE, WaterRole.BANK)) {
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ public final class GenerationSemanticCaptureTest {
|
|||||||
ChunkGenerationSemantics pond = GenerationSemanticCapture.capture(fixture.engine, 0, 0, 5L);
|
ChunkGenerationSemantics pond = GenerationSemanticCapture.capture(fixture.engine, 0, 0, 5L);
|
||||||
for (ChunkGenerationSemantics.RiverFeatureOccurrence occurrence : pond.riverFeatures()) {
|
for (ChunkGenerationSemantics.RiverFeatureOccurrence occurrence : pond.riverFeatures()) {
|
||||||
assertEquals("water", occurrence.profileKey());
|
assertEquals("water", occurrence.profileKey());
|
||||||
assertEquals(63 - 64, occurrence.position().y());
|
assertEquals(62 - 64, occurrence.position().y());
|
||||||
assertEquals(WaterRole.POND, hydrology.runtime.raster(0, 0).role(occurrence.position().x() & 15,
|
assertEquals(WaterRole.POND, hydrology.runtime.raster(0, 0).role(occurrence.position().x() & 15,
|
||||||
occurrence.position().z() & 15));
|
occurrence.position().z() & 15));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
final class HydrologyBlockMaps {
|
||||||
|
private HydrologyBlockMaps() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> write(HydrologyBlockProbe.Configuration configuration,
|
||||||
|
Map<Long, HydrologyBlockProbe.PlannedColumn> columns, HydrologyVoxels.Volume volume,
|
||||||
|
int worldHeight) throws IOException {
|
||||||
|
int minimumX = configuration.minimumChunkX() << 4;
|
||||||
|
int minimumZ = configuration.minimumChunkZ() << 4;
|
||||||
|
int width = (configuration.maximumChunkX() - configuration.minimumChunkX() + 1) << 4;
|
||||||
|
int depth = (configuration.maximumChunkZ() - configuration.minimumChunkZ() + 1) << 4;
|
||||||
|
BufferedImage raw = new BufferedImage(width, depth, BufferedImage.TYPE_INT_RGB);
|
||||||
|
BufferedImage effective = new BufferedImage(width, depth, BufferedImage.TYPE_INT_RGB);
|
||||||
|
BufferedImage loss = new BufferedImage(width, depth, BufferedImage.TYPE_INT_RGB);
|
||||||
|
BufferedImage fill = new BufferedImage(width, depth, BufferedImage.TYPE_INT_RGB);
|
||||||
|
File csv = new File(configuration.output(), "columns.csv");
|
||||||
|
try (BufferedWriter writer = Files.newBufferedWriter(csv.toPath(), StandardCharsets.UTF_8)) {
|
||||||
|
writer.write("x,z,role,natural_height,raster_terrain,effective_terrain,raster_head,effective_head,water_lost,terrain_delta,wet_bed_raise,generated_fill_solids,lowest_neighbor_head,known_neighbors\n");
|
||||||
|
for (int z = 0; z < depth; z++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
int blockX = minimumX + x;
|
||||||
|
int blockZ = minimumZ + z;
|
||||||
|
HydrologyBlockProbe.PlannedColumn column = columns.get(HydrologyVoxels.columnKey(blockX, blockZ));
|
||||||
|
boolean lost = column.rawHead() != HydrologyVoxels.NO_WATER && column.head() == HydrologyVoxels.NO_WATER;
|
||||||
|
int raised = column.wetBedRaise();
|
||||||
|
HydrologyBlockProbe.BedNeighbours neighbours = HydrologyBlockProbe.bedNeighbours(columns, blockX, blockZ);
|
||||||
|
raw.setRGB(x, z, relief(columns, column, blockX, blockZ, false, worldHeight));
|
||||||
|
effective.setRGB(x, z, relief(columns, column, blockX, blockZ, true, worldHeight));
|
||||||
|
loss.setRGB(x, z, lost ? 0xD33B31 : column.head() != HydrologyVoxels.NO_WATER ? 0x247AC4 : 0xC4C4C4);
|
||||||
|
fill.setRGB(x, z, raised > 0 ? (0xFF0000 | (Math.max(0, 224 - 28 * raised) << 8) | 0x20)
|
||||||
|
: column.head() != HydrologyVoxels.NO_WATER ? 0x247AC4 : 0xC4C4C4);
|
||||||
|
writer.write(blockX + "," + blockZ + "," + column.role() + "," + column.natural() + "," + column.terrain() + ","
|
||||||
|
+ column.effectiveTerrain() + "," + waterHead(column.rawHead()) + "," + waterHead(column.head()) + "," + lost + ","
|
||||||
|
+ (column.effectiveTerrain() - column.terrain()) + "," + raised + ","
|
||||||
|
+ HydrologyBlockProbe.raisedBedSolids(column, volume, blockX, blockZ) + ","
|
||||||
|
+ waterHead(neighbours.lowestHead()) + "," + neighbours.known() + "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> artifacts = new ArrayList<>();
|
||||||
|
artifacts.add(csv.getAbsolutePath());
|
||||||
|
artifacts.add(writeImage(configuration.output(), "raster-bed.png", raw));
|
||||||
|
artifacts.add(writeImage(configuration.output(), "effective-bed.png", effective));
|
||||||
|
artifacts.add(writeImage(configuration.output(), "water-loss.png", loss));
|
||||||
|
artifacts.add(writeImage(configuration.output(), "bed-fill.png", fill));
|
||||||
|
return List.copyOf(artifacts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String waterHead(int head) {
|
||||||
|
return head == HydrologyVoxels.NO_WATER ? "" : Integer.toString(head);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String writeImage(File output, String name, BufferedImage image) throws IOException {
|
||||||
|
File file = new File(output, name);
|
||||||
|
ImageIO.write(image, "png", file);
|
||||||
|
return file.getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int relief(Map<Long, HydrologyBlockProbe.PlannedColumn> columns, HydrologyBlockProbe.PlannedColumn column,
|
||||||
|
int x, int z, boolean effective, int worldHeight) {
|
||||||
|
int height = effective ? column.effectiveTerrain() : column.terrain();
|
||||||
|
int head = effective ? column.head() : column.rawHead();
|
||||||
|
double dx = height(columns, x + 1, z, effective, height) - height(columns, x - 1, z, effective, height);
|
||||||
|
double dz = height(columns, x, z + 1, effective, height) - height(columns, x, z - 1, effective, height);
|
||||||
|
double light = Math.clamp((dx + dz + 4D) / Math.sqrt(6D * (dx * dx + dz * dz + 4D)), 0D, 1D);
|
||||||
|
double shade = (0.5D + 0.5D * light) * (Math.floorMod(height, 4) == 0 ? 0.82D : 1D);
|
||||||
|
double altitude = Math.clamp(height / (double) Math.max(1, worldHeight - 1), 0D, 1D);
|
||||||
|
boolean wet = head != HydrologyVoxels.NO_WATER;
|
||||||
|
int red = (int) Math.round(shade * (wet ? 50D + 60D * altitude : 160D + 50D * altitude));
|
||||||
|
int green = (int) Math.round(shade * (wet ? 130D + 65D * altitude : 145D + 60D * altitude));
|
||||||
|
int blue = (int) Math.round(shade * (wet ? 190D + 50D * altitude : 110D + 80D * altitude));
|
||||||
|
return (red << 16) | (green << 8) | blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int height(Map<Long, HydrologyBlockProbe.PlannedColumn> columns, int x, int z, boolean effective, int fallback) {
|
||||||
|
HydrologyBlockProbe.PlannedColumn column = columns.get(HydrologyVoxels.columnKey(x, z));
|
||||||
|
return column == null ? fallback : effective ? column.effectiveTerrain() : column.terrain();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,14 @@ public final class HydrologyBlockProbe {
|
|||||||
|
|
||||||
static final class Evidence {
|
static final class Evidence {
|
||||||
long plannedWetColumns;
|
long plannedWetColumns;
|
||||||
|
long effectiveWetColumns;
|
||||||
|
long lostWetColumns;
|
||||||
|
long raisedWetBedColumns;
|
||||||
|
long raisedWetBedBlocks;
|
||||||
|
int maximumWetBedRaise;
|
||||||
|
long solidRaisedBedBlocks;
|
||||||
|
long dropAdjacentRaisedWetBedColumns;
|
||||||
|
long censoredRaisedWetBedColumns;
|
||||||
long channelColumns;
|
long channelColumns;
|
||||||
long pondColumns;
|
long pondColumns;
|
||||||
long bankColumns;
|
long bankColumns;
|
||||||
@@ -88,7 +96,16 @@ public final class HydrologyBlockProbe {
|
|||||||
List<String> artifacts) {
|
List<String> artifacts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
record PlannedColumn(WaterRole role, int terrain, int natural, int head) {
|
record PlannedColumn(WaterRole role, int terrain, int natural, int rawHead, int effectiveTerrain, int head) {
|
||||||
|
int wetBedRaise() {
|
||||||
|
return rawHead == HydrologyVoxels.NO_WATER || head == HydrologyVoxels.NO_WATER ? 0 : Math.max(0, effectiveTerrain - terrain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record BedNeighbours(int lowestHead, int known) {
|
||||||
|
boolean dropBelow(int head) {
|
||||||
|
return lowestHead != HydrologyVoxels.NO_WATER && lowestHead <= head - 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
@@ -96,8 +113,10 @@ public final class HydrologyBlockProbe {
|
|||||||
try {
|
try {
|
||||||
Result result = run(Configuration.parse(args));
|
Result result = run(Configuration.parse(args));
|
||||||
Evidence evidence = result.evidence();
|
Evidence evidence = result.evidence();
|
||||||
System.out.println("IRIS_HYDROLOGY_BLOCK_RESULT version=2 status=" + result.status() + " seed=" + result.configuration().seed()
|
System.out.println("IRIS_HYDROLOGY_BLOCK_RESULT version=4 status=" + result.status() + " seed=" + result.configuration().seed()
|
||||||
+ " plannedWetColumns=" + evidence.plannedWetColumns + " missingSurfaceColumns=" + evidence.scan.missingSurfaces
|
+ " plannedWetColumns=" + evidence.plannedWetColumns + " missingSurfaceColumns=" + evidence.scan.missingSurfaces
|
||||||
|
+ " effectiveWetColumns=" + evidence.effectiveWetColumns + " lostWetColumns=" + evidence.lostWetColumns
|
||||||
|
+ " raisedWetBedColumns=" + evidence.raisedWetBedColumns + " raisedWetBedBlocks=" + evidence.raisedWetBedBlocks
|
||||||
+ " containmentLeaks=" + evidence.scan.containmentLeaks + " tickConversionCells=" + evidence.scan.tickConversionCells
|
+ " containmentLeaks=" + evidence.scan.containmentLeaks + " tickConversionCells=" + evidence.scan.tickConversionCells
|
||||||
+ " cane=" + evidence.validCaneRoots + " failures=" + result.failures().size()
|
+ " cane=" + evidence.validCaneRoots + " failures=" + result.failures().size()
|
||||||
+ " output=" + result.configuration().output().getAbsolutePath());
|
+ " output=" + result.configuration().output().getAbsolutePath());
|
||||||
@@ -135,7 +154,8 @@ public final class HydrologyBlockProbe {
|
|||||||
failures.add("The engine reported " + reports.size() + " generation errors.");
|
failures.add("The engine reported " + reports.size() + " generation errors.");
|
||||||
}
|
}
|
||||||
Files.createDirectories(configuration.output().toPath());
|
Files.createDirectories(configuration.output().toPath());
|
||||||
List<String> artifacts = writeImages(configuration, volume, engine);
|
List<String> artifacts = new ArrayList<>(writeImages(configuration, volume, engine));
|
||||||
|
artifacts.addAll(HydrologyBlockMaps.write(configuration, planned, volume, engine.getHeight()));
|
||||||
Result result = new Result(failures.isEmpty() ? "PASS" : "FAIL", configuration, seaLevel, evidence, List.copyOf(failures), artifacts);
|
Result result = new Result(failures.isEmpty() ? "PASS" : "FAIL", configuration, seaLevel, evidence, List.copyOf(failures), artifacts);
|
||||||
Files.writeString(new File(configuration.output(), "hydrology-blocks.json").toPath(), json(result), StandardCharsets.UTF_8);
|
Files.writeString(new File(configuration.output(), "hydrology-blocks.json").toPath(), json(result), StandardCharsets.UTF_8);
|
||||||
return result;
|
return result;
|
||||||
@@ -155,7 +175,9 @@ public final class HydrologyBlockProbe {
|
|||||||
int head = heads[(lz << 4) | lx];
|
int head = heads[(lz << 4) | lx];
|
||||||
int natural = complex.naturalTrueHeight(x, z);
|
int natural = complex.naturalTrueHeight(x, z);
|
||||||
int terrain = own.empty() ? natural : own.terrainHeight(lx, lz);
|
int terrain = own.empty() ? natural : own.terrainHeight(lx, lz);
|
||||||
planned.put(HydrologyVoxels.columnKey(x, z), new PlannedColumn(own.role(lx, lz), terrain, natural, head));
|
int effectiveTerrain = (int) Math.round(complex.getRawHeightStream().getDouble(x, z));
|
||||||
|
planned.put(HydrologyVoxels.columnKey(x, z), new PlannedColumn(own.role(lx, lz), terrain, natural,
|
||||||
|
own.waterHead(lx, lz), effectiveTerrain, head));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,8 +225,15 @@ public final class HydrologyBlockProbe {
|
|||||||
default -> {
|
default -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (column.head() != HydrologyVoxels.NO_WATER) {
|
inspectWater(column, x, z, evidence);
|
||||||
evidence.plannedWetColumns++;
|
if (column.wetBedRaise() > 0) {
|
||||||
|
evidence.solidRaisedBedBlocks += raisedBedSolids(column, volume, x, z);
|
||||||
|
BedNeighbours neighbours = bedNeighbours(planned, x, z);
|
||||||
|
if (neighbours.dropBelow(column.head())) {
|
||||||
|
evidence.dropAdjacentRaisedWetBedColumns++;
|
||||||
|
} else if (neighbours.known() < CARDINALS.length) {
|
||||||
|
evidence.censoredRaisedWetBedColumns++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (HydrologyBlockMetrics.scanned(columns, x, z)) {
|
if (HydrologyBlockMetrics.scanned(columns, x, z)) {
|
||||||
evidence.scan.column(volume, x, z, HydrologyBlockMetrics.scanFloor(columns, seaLevel, x, z), height - 1);
|
evidence.scan.column(volume, x, z, HydrologyBlockMetrics.scanFloor(columns, seaLevel, x, z), height - 1);
|
||||||
@@ -214,6 +243,58 @@ public final class HydrologyBlockProbe {
|
|||||||
return evidence;
|
return evidence;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void inspectWater(PlannedColumn column, int x, int z, Evidence evidence) {
|
||||||
|
if (column.head() != HydrologyVoxels.NO_WATER) {
|
||||||
|
evidence.effectiveWetColumns++;
|
||||||
|
}
|
||||||
|
if (column.rawHead() == HydrologyVoxels.NO_WATER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
evidence.plannedWetColumns++;
|
||||||
|
if (column.head() == HydrologyVoxels.NO_WATER) {
|
||||||
|
evidence.lostWetColumns++;
|
||||||
|
evidence.example("lost planned water at " + x + "," + column.rawHead() + "," + z + " role=" + column.role()
|
||||||
|
+ " bed=" + column.terrain() + " effectiveGround=" + column.effectiveTerrain());
|
||||||
|
} else if (column.wetBedRaise() > 0) {
|
||||||
|
int raised = column.wetBedRaise();
|
||||||
|
evidence.raisedWetBedColumns++;
|
||||||
|
evidence.raisedWetBedBlocks += raised;
|
||||||
|
evidence.maximumWetBedRaise = Math.max(evidence.maximumWetBedRaise, raised);
|
||||||
|
evidence.example("raised wet bed at " + x + "," + z + " role=" + column.role() + " rasterGround=" + column.terrain()
|
||||||
|
+ " effectiveGround=" + column.effectiveTerrain() + " fill=" + raised + " remainingDepth="
|
||||||
|
+ (column.head() - column.effectiveTerrain()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int raisedBedSolids(PlannedColumn column, HydrologyVoxels.Volume volume, int x, int z) {
|
||||||
|
if (column.wetBedRaise() == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int solids = 0;
|
||||||
|
for (int y = column.terrain() + 1; y <= column.effectiveTerrain(); y++) {
|
||||||
|
if (volume.at(x, y, z) == HydrologyVoxels.SOLID) {
|
||||||
|
solids++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return solids;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BedNeighbours bedNeighbours(Map<Long, PlannedColumn> columns, int x, int z) {
|
||||||
|
int lowest = Integer.MAX_VALUE;
|
||||||
|
int known = 0;
|
||||||
|
for (int[] side : CARDINALS) {
|
||||||
|
PlannedColumn neighbour = columns.get(HydrologyVoxels.columnKey(x + side[0], z + side[1]));
|
||||||
|
if (neighbour == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
known++;
|
||||||
|
if (neighbour.head() != HydrologyVoxels.NO_WATER) {
|
||||||
|
lowest = Math.min(lowest, neighbour.head());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new BedNeighbours(lowest == Integer.MAX_VALUE ? HydrologyVoxels.NO_WATER : lowest, known);
|
||||||
|
}
|
||||||
|
|
||||||
private static void inspectCane(HydrologyVoxels.Volume volume, int x, int z, int height, Evidence evidence) {
|
private static void inspectCane(HydrologyVoxels.Volume volume, int x, int z, int height, Evidence evidence) {
|
||||||
for (int y = 1; y < height; y++) {
|
for (int y = 1; y < height; y++) {
|
||||||
if (volume.at(x, y, z) != HydrologyVoxels.CANE || volume.at(x, y - 1, z) == HydrologyVoxels.CANE) {
|
if (volume.at(x, y, z) != HydrologyVoxels.CANE || volume.at(x, y - 1, z) == HydrologyVoxels.CANE) {
|
||||||
@@ -249,6 +330,13 @@ public final class HydrologyBlockProbe {
|
|||||||
if (evidence.invalidCaneSubstrates > 0 || evidence.dryCaneRoots > 0) {
|
if (evidence.invalidCaneSubstrates > 0 || evidence.dryCaneRoots > 0) {
|
||||||
failures.add("Generated sugar cane has invalid substrate or no adjacent water.");
|
failures.add("Generated sugar cane has invalid substrate or no adjacent water.");
|
||||||
}
|
}
|
||||||
|
if (evidence.lostWetColumns > 0) {
|
||||||
|
failures.add("Water settling removes planned water from " + evidence.lostWetColumns + " columns in fresh generation.");
|
||||||
|
}
|
||||||
|
if (evidence.raisedWetBedColumns > 0) {
|
||||||
|
failures.add("Water settling fills " + evidence.raisedWetBedBlocks + " blocks above the raster bed in "
|
||||||
|
+ evidence.raisedWetBedColumns + " wet columns in fresh generation.");
|
||||||
|
}
|
||||||
if (evidence.scan.missingSurfaces > 0) {
|
if (evidence.scan.missingSurfaces > 0) {
|
||||||
failures.add("Generated blocks miss the planned water surface in " + evidence.scan.missingSurfaces + " columns.");
|
failures.add("Generated blocks miss the planned water surface in " + evidence.scan.missingSurfaces + " columns.");
|
||||||
}
|
}
|
||||||
@@ -288,6 +376,14 @@ public final class HydrologyBlockProbe {
|
|||||||
Evidence evidence = result.evidence();
|
Evidence evidence = result.evidence();
|
||||||
Map<String, Object> counts = new LinkedHashMap<>();
|
Map<String, Object> counts = new LinkedHashMap<>();
|
||||||
counts.put("plannedWetColumns", evidence.plannedWetColumns);
|
counts.put("plannedWetColumns", evidence.plannedWetColumns);
|
||||||
|
counts.put("effectiveWetColumns", evidence.effectiveWetColumns);
|
||||||
|
counts.put("lostWetColumns", evidence.lostWetColumns);
|
||||||
|
counts.put("raisedWetBedColumns", evidence.raisedWetBedColumns);
|
||||||
|
counts.put("raisedWetBedBlocks", evidence.raisedWetBedBlocks);
|
||||||
|
counts.put("maximumWetBedRaise", evidence.maximumWetBedRaise);
|
||||||
|
counts.put("solidRaisedBedBlocks", evidence.solidRaisedBedBlocks);
|
||||||
|
counts.put("dropAdjacentRaisedWetBedColumns", evidence.dropAdjacentRaisedWetBedColumns);
|
||||||
|
counts.put("censoredRaisedWetBedColumns", evidence.censoredRaisedWetBedColumns);
|
||||||
counts.put("missingSurfaceColumns", evidence.scan.missingSurfaces);
|
counts.put("missingSurfaceColumns", evidence.scan.missingSurfaces);
|
||||||
counts.put("channelColumns", evidence.channelColumns);
|
counts.put("channelColumns", evidence.channelColumns);
|
||||||
counts.put("pondColumns", evidence.pondColumns);
|
counts.put("pondColumns", evidence.pondColumns);
|
||||||
@@ -313,6 +409,9 @@ public final class HydrologyBlockProbe {
|
|||||||
report.put("artifacts", result.artifacts());
|
report.put("artifacts", result.artifacts());
|
||||||
report.put("coordinateConvention", "Block X/Z are world coordinates; Y is relative to the world minimum height.");
|
report.put("coordinateConvention", "Block X/Z are world coordinates; Y is relative to the world minimum height.");
|
||||||
report.put("imageLegend", "open=light blue,water=blue,lava=orange,cane=lime,solid=gray");
|
report.put("imageLegend", "open=light blue,water=blue,lava=orange,cane=lime,solid=gray");
|
||||||
|
report.put("bedMapLegend", "raster-bed and effective-bed show elevation, slope shading and four-block contours on the same world-height scale; water-loss: red=planned wet made dry, blue=retained water, gray=dry");
|
||||||
|
report.put("bedFillLegend", "bed-fill: yellow to red=one to eight or more blocks of wet bed fill, blue=wet bed retained, gray=dry; dry bank and dam fill is excluded");
|
||||||
|
report.put("columnData", "columns.csv is ordered by Z then X; blank water heads mean dry; heights are relative to the world minimum height");
|
||||||
return new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
|
return new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class HydrologyBlockProbeTest {
|
public class HydrologyBlockProbeTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder folder = new TemporaryFolder();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void failuresNameMissingSurfaceLeaksTicksAndCoverage() {
|
public void failuresNameMissingSurfaceLeaksTicksAndCoverage() {
|
||||||
HydrologyBlockProbe.Evidence evidence = new HydrologyBlockProbe.Evidence();
|
HydrologyBlockProbe.Evidence evidence = new HydrologyBlockProbe.Evidence();
|
||||||
@@ -21,4 +36,110 @@ public class HydrologyBlockProbeTest {
|
|||||||
"Fluid ticks would move generated water in 4 cells.",
|
"Fluid ticks would move generated water in 4 cells.",
|
||||||
"Required generated coverage is absent: pond"), HydrologyBlockProbe.failures(evidence, Set.of("channel", "pond")));
|
"Required generated coverage is absent: pond"), HydrologyBlockProbe.failures(evidence, Set.of("channel", "pond")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dryRidgesFailEvenWhenTheRemainingWaterIsSealed() {
|
||||||
|
HydrologyBlockProbe.Evidence evidence = new HydrologyBlockProbe.Evidence();
|
||||||
|
HydrologyBlockProbe.inspectWater(new HydrologyBlockProbe.PlannedColumn(WaterRole.CHANNEL, 77, 82, 79, 80,
|
||||||
|
HydrologyVoxels.NO_WATER), -103, -1391, evidence);
|
||||||
|
HydrologyBlockProbe.inspectWater(new HydrologyBlockProbe.PlannedColumn(WaterRole.WATERFALL, 70, 82, 79, 70, 79),
|
||||||
|
-102, -1391, evidence);
|
||||||
|
HydrologyBlockProbe.inspectWater(new HydrologyBlockProbe.PlannedColumn(WaterRole.BANK, 82, 82, HydrologyVoxels.NO_WATER,
|
||||||
|
82, HydrologyVoxels.NO_WATER), -104, -1391, evidence);
|
||||||
|
|
||||||
|
assertEquals(2L, evidence.plannedWetColumns);
|
||||||
|
assertEquals(1L, evidence.effectiveWetColumns);
|
||||||
|
assertEquals(1L, evidence.lostWetColumns);
|
||||||
|
assertEquals(List.of("Water settling removes planned water from 1 columns in fresh generation."),
|
||||||
|
HydrologyBlockProbe.failures(evidence, Set.of()));
|
||||||
|
assertEquals(List.of("lost planned water at -103,79,-1391 role=CHANNEL bed=77 effectiveGround=80"), evidence.examples);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void submergedChannelAndPondFillFailsWithoutAnyLostSurfaceWater() {
|
||||||
|
HydrologyBlockProbe.PlannedColumn channel = new HydrologyBlockProbe.PlannedColumn(WaterRole.CHANNEL, 318, 330, 326, 324, 326);
|
||||||
|
HydrologyBlockProbe.PlannedColumn pond = new HydrologyBlockProbe.PlannedColumn(WaterRole.POND, 316, 330, 326, 324, 326);
|
||||||
|
HydrologyBlockProbe.Evidence evidence = new HydrologyBlockProbe.Evidence();
|
||||||
|
HydrologyBlockProbe.inspectWater(channel, -372, -441, evidence);
|
||||||
|
HydrologyBlockProbe.inspectWater(pond, -371, -441, evidence);
|
||||||
|
HydrologyBlockProbe.inspectWater(new HydrologyBlockProbe.PlannedColumn(WaterRole.DAM, 318, 318, HydrologyVoxels.NO_WATER,
|
||||||
|
330, HydrologyVoxels.NO_WATER), -373, -441, evidence);
|
||||||
|
|
||||||
|
assertEquals(0L, evidence.lostWetColumns);
|
||||||
|
assertEquals(2L, evidence.effectiveWetColumns);
|
||||||
|
assertEquals(2L, evidence.raisedWetBedColumns);
|
||||||
|
assertEquals(14L, evidence.raisedWetBedBlocks);
|
||||||
|
assertEquals(8, evidence.maximumWetBedRaise);
|
||||||
|
assertEquals(List.of("Water settling fills 14 blocks above the raster bed in 2 wet columns in fresh generation."),
|
||||||
|
HydrologyBlockProbe.failures(evidence, Set.of()));
|
||||||
|
assertEquals("raised wet bed at -372,-441 role=CHANNEL rasterGround=318 effectiveGround=324 fill=6 remainingDepth=2",
|
||||||
|
evidence.examples.getFirst());
|
||||||
|
|
||||||
|
HydrologyVoxels.ChunkBlocks chunk = HydrologyVoxels.ChunkBlocks.empty(-24, -28, 384);
|
||||||
|
for (int y = 319; y <= 324; y++) {
|
||||||
|
chunk.set(12, y, 7, HydrologyVoxels.SOLID);
|
||||||
|
}
|
||||||
|
chunk.set(12, 325, 7, HydrologyVoxels.WATER);
|
||||||
|
HydrologyVoxels.Volume volume = new HydrologyVoxels.Volume();
|
||||||
|
volume.put(chunk);
|
||||||
|
assertEquals(6, HydrologyBlockProbe.raisedBedSolids(channel, volume, -372, -441));
|
||||||
|
assertEquals(0, HydrologyBlockProbe.raisedBedSolids(pond, volume, -371, -441));
|
||||||
|
|
||||||
|
HydrologyBlockProbe.BedNeighbours neighbours = HydrologyBlockProbe.bedNeighbours(Map.of(
|
||||||
|
HydrologyVoxels.columnKey(-373, -441), new HydrologyBlockProbe.PlannedColumn(WaterRole.CHANNEL, 316, 330, 324, 316, 324)),
|
||||||
|
-372, -441);
|
||||||
|
assertTrue(neighbours.dropBelow(channel.head()));
|
||||||
|
assertEquals(1, neighbours.known());
|
||||||
|
assertEquals(324, neighbours.lowestHead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void columnDataAndBedImagesExposeHeightChangesBeneathOneWaterSurface() throws Exception {
|
||||||
|
File output = folder.newFolder("columns");
|
||||||
|
HydrologyBlockProbe.Configuration configuration = new HydrologyBlockProbe.Configuration(new File("/synthetic-pack"), "overworld",
|
||||||
|
1337L, -1, -1, -1, -1, Set.of(), output, false);
|
||||||
|
Map<Long, HydrologyBlockProbe.PlannedColumn> columns = new HashMap<>();
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
boolean lost = x == 4 && z == 5;
|
||||||
|
boolean dry = x == 0 && z == 0;
|
||||||
|
boolean raised = x == 6 && z == 5;
|
||||||
|
columns.put(HydrologyVoxels.columnKey(x - 16, z - 16), new HydrologyBlockProbe.PlannedColumn(
|
||||||
|
dry ? WaterRole.BANK : WaterRole.CHANNEL, 60 + x, 120, dry ? HydrologyVoxels.NO_WATER : 100,
|
||||||
|
lost ? 101 : raised ? 72 : 60 + x, lost || dry ? HydrologyVoxels.NO_WATER : 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HydrologyVoxels.ChunkBlocks chunk = HydrologyVoxels.ChunkBlocks.empty(-1, -1, 128);
|
||||||
|
for (int y = 67; y <= 72; y++) {
|
||||||
|
chunk.set(6, y, 5, HydrologyVoxels.SOLID);
|
||||||
|
}
|
||||||
|
HydrologyVoxels.Volume volume = new HydrologyVoxels.Volume();
|
||||||
|
volume.put(chunk);
|
||||||
|
|
||||||
|
List<String> artifacts = HydrologyBlockMaps.write(configuration, columns, volume, 320);
|
||||||
|
|
||||||
|
assertEquals(5, artifacts.size());
|
||||||
|
List<String> csv = Files.readAllLines(new File(output, "columns.csv").toPath());
|
||||||
|
assertEquals(257, csv.size());
|
||||||
|
assertEquals("x,z,role,natural_height,raster_terrain,effective_terrain,raster_head,effective_head,water_lost,terrain_delta,wet_bed_raise,generated_fill_solids,lowest_neighbor_head,known_neighbors", csv.getFirst());
|
||||||
|
assertEquals("-16,-16,BANK,120,60,60,,,false,0,0,0,100,2", csv.get(1));
|
||||||
|
assertTrue(csv.contains("-12,-11,CHANNEL,120,64,101,100,,true,37,0,0,100,4"));
|
||||||
|
assertTrue(csv.contains("-10,-11,CHANNEL,120,66,72,100,100,false,6,6,6,100,4"));
|
||||||
|
assertFalse(String.join("\n", csv).contains(Integer.toString(HydrologyVoxels.NO_WATER)));
|
||||||
|
BufferedImage raw = ImageIO.read(new File(output, "raster-bed.png"));
|
||||||
|
BufferedImage effective = ImageIO.read(new File(output, "effective-bed.png"));
|
||||||
|
BufferedImage loss = ImageIO.read(new File(output, "water-loss.png"));
|
||||||
|
BufferedImage fill = ImageIO.read(new File(output, "bed-fill.png"));
|
||||||
|
assertEquals(16, raw.getWidth());
|
||||||
|
assertEquals(16, raw.getHeight());
|
||||||
|
assertNotEquals(raw.getRGB(1, 10), raw.getRGB(13, 10));
|
||||||
|
assertNotEquals(raw.getRGB(4, 5), effective.getRGB(4, 5));
|
||||||
|
assertEquals(raw.getRGB(10, 10), effective.getRGB(10, 10));
|
||||||
|
assertEquals(0xD33B31, loss.getRGB(4, 5) & 0xFFFFFF);
|
||||||
|
assertEquals(0x247AC4, loss.getRGB(10, 10) & 0xFFFFFF);
|
||||||
|
assertEquals(0xC4C4C4, loss.getRGB(0, 0) & 0xFFFFFF);
|
||||||
|
assertEquals(0xFF3820, fill.getRGB(6, 5) & 0xFFFFFF);
|
||||||
|
assertEquals(0x247AC4, fill.getRGB(10, 10) & 0xFFFFFF);
|
||||||
|
assertEquals(0xC4C4C4, fill.getRGB(0, 0) & 0xFFFFFF);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user