mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +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.HydrologySampleBuffer;
|
||||
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.SeaInletPlanner;
|
||||
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));
|
||||
water.addAll(lakes);
|
||||
ShapedNetwork network = shaper.shape(drainage.surface(), drainage.receivers(), drainage.accumulation(), drainage.channels(), water,
|
||||
fine, settings.seed());
|
||||
int passes = water.size() + 1;
|
||||
for (int pass = 1; lowered(water, network.ponds()); pass++) {
|
||||
ShapedNetwork network = PondChannelSettlement.settle(shaper.shape(drainage.surface(), drainage.receivers(), drainage.accumulation(),
|
||||
drainage.channels(), water, fine, settings.seed()), settings.seaLevel());
|
||||
long passes = settlementPasses(water);
|
||||
for (long pass = 1; lowered(water, network.ponds()); pass++) {
|
||||
if (pass >= passes) {
|
||||
throw new IllegalStateException("Window " + key + " ponds did not settle within " + passes + " shaping passes");
|
||||
}
|
||||
water = replanned(settled(raw, cups, water, network.ponds()));
|
||||
network = shaper.shape(drainage.surface(), drainage.receivers(), drainage.accumulation(), drainage.channels(), water, fine,
|
||||
settings.seed());
|
||||
network = PondChannelSettlement.settle(shaper.shape(drainage.surface(), drainage.receivers(), drainage.accumulation(),
|
||||
drainage.channels(), water, fine, settings.seed()), settings.seaLevel());
|
||||
}
|
||||
List<Reach> reaches = network.reaches();
|
||||
List<Pond> windowPonds = windowPonds(network.ponds());
|
||||
@@ -263,6 +264,16 @@ public final class WindowPlanner {
|
||||
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) {
|
||||
for (int index = 0; index < shaped.size(); index++) {
|
||||
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<>();
|
||||
for (IndexedWindow window : windows) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -83,6 +86,25 @@ final class ChunkFeatures {
|
||||
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() {
|
||||
return ponds.size();
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ public final class ChunkRasterizer {
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
gradeRiffles();
|
||||
raiseDams();
|
||||
bermEdges();
|
||||
measureLakeShores();
|
||||
@@ -80,8 +81,8 @@ final class RasterComposer {
|
||||
int ground = natural[cell];
|
||||
SeaInlet inlet = features.openInletAt(x, z, inletColumn);
|
||||
if (inlet != null) {
|
||||
head[cell] = inlet.waterLevel();
|
||||
terrain[cell] = Math.min(ground, inletColumn[0]);
|
||||
head[cell] = level(inlet.waterLevel());
|
||||
terrain[cell] = Math.min(Math.min(ground, inletColumn[0]), head[cell] - 1);
|
||||
role[cell] = WaterRole.INLET;
|
||||
feature[cell] = inlet.id();
|
||||
fluid[cell] = HydrologyCaveCell.SEA_FLUID;
|
||||
@@ -91,72 +92,77 @@ final class RasterComposer {
|
||||
int pondIndex = lake >= 0 ? -1 : features.pondAt(x, z);
|
||||
boolean shore = interior(cell) && lake < 0 && pondIndex < 0;
|
||||
features.segments().nearest(x, z, shore ? ReachSegments.Scope.SHORE : ReachSegments.Scope.CHANNEL, cursor);
|
||||
double variation = BedProfile.variation(settings.seed(), x, z);
|
||||
if (lake >= 0) {
|
||||
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;
|
||||
}
|
||||
if (pondIndex >= 0) {
|
||||
Pond water = features.pond(pondIndex);
|
||||
pond[cell] = pondIndex;
|
||||
int floor = interior(cell) ? pondFloor(pondIndex, water.level(), x, z, ground) : ground;
|
||||
standing(cell, water, trench(floor, water.level(), ground));
|
||||
double detail = BedProfile.detail(settings.seed(), x, z);
|
||||
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;
|
||||
}
|
||||
if (cursor.channel.reach != null) {
|
||||
channel(cell, ground);
|
||||
channel(cell, ground, variation, BedProfile.detail(settings.seed(), x, z));
|
||||
} else if (interior(cell)) {
|
||||
dry(cell, x, z, ground);
|
||||
dry(cell, x, z, ground, variation);
|
||||
}
|
||||
}
|
||||
|
||||
private void standing(int cell, Pond water, int floor) {
|
||||
head[cell] = water.level();
|
||||
terrain[cell] = floor;
|
||||
head[cell] = level(water.level());
|
||||
terrain[cell] = Math.min(floor, head[cell] - 1);
|
||||
role[cell] = WaterRole.POND;
|
||||
feature[cell] = water.id();
|
||||
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;
|
||||
if (hit.reach == null) {
|
||||
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;
|
||||
Reach reach = hit.reach;
|
||||
int point = hit.point();
|
||||
head[cell] = reach.grade()[point];
|
||||
terrain[cell] = bed(hit, head[cell], ground);
|
||||
head[cell] = level(reach.grade()[point]);
|
||||
terrain[cell] = bed(hit, head[cell], ground, variation, detail);
|
||||
role[cell] = role(reach.style()[point]);
|
||||
feature[cell] = reach.id();
|
||||
tier[cell] = reach.tier();
|
||||
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 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);
|
||||
}
|
||||
|
||||
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 raise = Integer.MIN_VALUE;
|
||||
double bankEdge = Double.POSITIVE_INFINITY;
|
||||
ReachCursor.Choice bank = cursor.bank;
|
||||
if (bank.reach != null) {
|
||||
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();
|
||||
double u = bankEdge / bankTier.bankBand();
|
||||
if (bankTier.bankStyle() != IrisBankStyle.CLIFF && bankTier.bankStyle() != IrisBankStyle.TERRACED) {
|
||||
u = BedProfile.position(u, -variation);
|
||||
}
|
||||
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 {
|
||||
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();
|
||||
boolean tapered = nearPond >= 0 && pondEdge <= taper;
|
||||
if (tapered && ground > features.pond(nearPond).level()) {
|
||||
cut = Math.min(cut, taperCut(features.pond(nearPond).level(), ground, pondEdge / taper));
|
||||
if (tapered && ground > level(features.pond(nearPond).level())) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
double bankDistance = features.pondBankDistance(index, x, z);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -236,8 +289,9 @@ final class RasterComposer {
|
||||
continue;
|
||||
}
|
||||
Pond held = features.pond(index);
|
||||
if (held.damRaise() > 0 && terrain[cell] < held.level() && held.level() + 1 > crest) {
|
||||
crest = held.level() + 1;
|
||||
int level = level(held.level());
|
||||
if (held.damRaise() > 0 && terrain[cell] < level && level + 1 > crest) {
|
||||
crest = level + 1;
|
||||
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) {
|
||||
float[] xs = reach.xs();
|
||||
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) {
|
||||
PondMask mask = pond.mask();
|
||||
int step = mask.step();
|
||||
Box box = box(raw, fine, mask.originX() - step, mask.originZ() - step, mask.originX() + mask.width() * step,
|
||||
mask.originZ() + mask.depth() * step, cups, Math.max(1, step / fine.cell()));
|
||||
int margin = Math.max(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) {
|
||||
return null;
|
||||
}
|
||||
@@ -669,6 +671,9 @@ public final class PondPlanner {
|
||||
}
|
||||
int i = 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++) {
|
||||
int neighbour = (j + AXIS_Z[axis]) * box.width() + i + AXIS_X[axis];
|
||||
if (!wet[neighbour] && ground[neighbour] < level) {
|
||||
|
||||
@@ -29,11 +29,7 @@ final class EffectiveWater {
|
||||
if (head == 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);
|
||||
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;
|
||||
return column;
|
||||
}
|
||||
|
||||
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 {
|
||||
long pre(int x, int z);
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ import lombok.Getter;
|
||||
public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBlockState> {
|
||||
private static final BoundBlockState BEDROCK = BoundBlockState.of("BEDROCK");
|
||||
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;
|
||||
@Getter
|
||||
private final RNG rng;
|
||||
@@ -132,6 +134,17 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
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 int x;
|
||||
private final int z;
|
||||
@@ -214,6 +227,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
int topY = Math.min(hf, chunkHeight - 1);
|
||||
WaterRole role = waterRoleStream.get(realX, realZ);
|
||||
boolean wet = role.wet();
|
||||
boolean submerged = wet && he < surfaceFluidHeight;
|
||||
boolean seaFloor = !wet || role == WaterRole.INLET;
|
||||
IrisHydrologyMaterials materials = role == WaterRole.NONE || role == WaterRole.INLET ? null
|
||||
: complex.resolveWaterMaterials(realX, realZ);
|
||||
@@ -292,7 +306,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
if (i <= he) {
|
||||
int depth = layerSurfaceY - i;
|
||||
if (depth == 0 && mappedSurfaceBlock != null) {
|
||||
put(i, mappedSurfaceBlock);
|
||||
put(i, submerged && layerSurfaceY == he ? submergedSoil(mappedSurfaceBlock) : mappedSurfaceBlock);
|
||||
continue;
|
||||
}
|
||||
if (blocks == null) {
|
||||
@@ -327,6 +341,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
NativeBlockState layerBlock = paintWaterMaterial(
|
||||
blocks.hasIndex(strataIndex) ? blocks.get(strataIndex) : null,
|
||||
rolePalette, paintDepth, depth, rng, realX, i, realZ, data);
|
||||
if (submerged && layerSurfaceY == he) {
|
||||
layerBlock = submergedSoil(layerBlock);
|
||||
}
|
||||
if (layerBlock != null) {
|
||||
if (keepGravityAway && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
|
||||
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.underground.SeaInlet;
|
||||
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.PondKind;
|
||||
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.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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.anyLong;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
public class WindowPlannerTest {
|
||||
@@ -49,6 +53,51 @@ public class WindowPlannerTest {
|
||||
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
|
||||
public void burnedTrunkCapturesLocalDrainage() {
|
||||
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 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
|
||||
public void channelBedBelowWaterHead() {
|
||||
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++;
|
||||
int bed = scene.terrain(x, z);
|
||||
int ground = natural.applyAsInt(x, z);
|
||||
assertEquals(87, scene.head(x, z));
|
||||
assertTrue("bed " + bed + " below the water head at " + x + "," + z, bed < 87);
|
||||
assertEquals(86, scene.head(x, z));
|
||||
assertTrue("bed " + bed + " below the water head at " + x + "," + z, bed < 86);
|
||||
assertTrue("bed never above natural at " + x + "," + z, bed <= ground);
|
||||
if (ground < 84) {
|
||||
pooled++;
|
||||
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("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));
|
||||
}
|
||||
|
||||
@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
|
||||
public void lakeRimShoreDistanceMatchesNearestLakeWater() {
|
||||
IntBinaryOperator natural = (x, z) -> 60 + (int) (Math.hypot(x - 24, z - 24) / 2D);
|
||||
@@ -189,14 +287,14 @@ public class ChunkWaterRasterTest {
|
||||
wet++;
|
||||
chunks.add(pack(Math.floorDiv(x, 16), Math.floorDiv(z, 16)));
|
||||
assertEquals(pond.id(), scene.feature(x, z));
|
||||
assertEquals("pond level at " + x + "," + z, 75, scene.head(x, z));
|
||||
assertTrue("pond floor below the level at " + x + "," + z, scene.terrain(x, z) < 75);
|
||||
assertEquals("pond level at " + x + "," + z, 74, scene.head(x, z));
|
||||
assertTrue("pond floor below the level at " + x + "," + z, scene.terrain(x, z) < 74);
|
||||
assertEquals("water", scene.fluid(x, z));
|
||||
}
|
||||
}
|
||||
assertTrue("pond columns " + wet, wet > 400);
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -206,7 +304,7 @@ public class ChunkWaterRasterTest {
|
||||
float startZ = -20f;
|
||||
float endX = 80f;
|
||||
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);
|
||||
Scene scene = new Scene(settings(new IrisHydrology()), natural).reach(line(31L, river, 0L, startX, startZ, endX, endZ, (x, z) -> 79));
|
||||
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)));
|
||||
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));
|
||||
assertContained("waterfall", waterfall, -16, -8, 64, 24, true);
|
||||
assertEquals(WaterRole.WATERFALL, waterfall.role(24, 8));
|
||||
assertEquals(99, waterfall.head(22, 8));
|
||||
assertEquals(79, waterfall.head(24, 8));
|
||||
assertEquals(98, waterfall.head(22, 8));
|
||||
assertEquals(78, waterfall.head(24, 8));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -455,7 +553,7 @@ public class ChunkWaterRasterTest {
|
||||
IrisHydrology config = new IrisHydrology();
|
||||
config.getDeformation().setBankSlope(0D).setBankSteepFactor(0D).setBankNoise(0D);
|
||||
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);
|
||||
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,
|
||||
@@ -466,7 +564,7 @@ public class ChunkWaterRasterTest {
|
||||
int height = scene.terrain(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 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]);
|
||||
cut[outward - 1] = height;
|
||||
int levee = scene.terrain(8, 6 - outward);
|
||||
@@ -475,7 +573,7 @@ public class ChunkWaterRasterTest {
|
||||
levee >= ground && levee <= ground + settings.deformation().maxFill());
|
||||
}
|
||||
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 + " leaks", 0, scene.leaks(-16, -16, 32, 32));
|
||||
cuts.put(style, cut);
|
||||
@@ -494,16 +592,16 @@ public class ChunkWaterRasterTest {
|
||||
assertTrue(ordered);
|
||||
int[] cliff = cuts.get(IrisBankStyle.CLIFF);
|
||||
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]);
|
||||
int[] terraced = cuts.get(IrisBankStyle.TERRACED);
|
||||
boolean level = false;
|
||||
for (int index = 0; index < 15; 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);
|
||||
List<IrisBankStyle> styles = List.of(IrisBankStyle.values());
|
||||
@@ -521,13 +619,13 @@ public class ChunkWaterRasterTest {
|
||||
int maxFill = settings.deformation().maxFill();
|
||||
HydrologyTier stream = tier("stream", 1.5f, 0.5f, 0, IrisBankStyle.SMOOTH);
|
||||
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);
|
||||
for (int x = -24; x < 40; x++) {
|
||||
assertTrue("channel water at x=" + x, scene.water(x, 9));
|
||||
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("berm one block above the water at x=" + x, 80, scene.terrain(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, 79, scene.terrain(x, 10));
|
||||
assertEquals(WaterRole.SHORE, scene.role(x, 10));
|
||||
assertEquals(perchedReach.id(), scene.feature(x, 10));
|
||||
assertEquals(stream, scene.tier(x, 10));
|
||||
@@ -536,11 +634,11 @@ public class ChunkWaterRasterTest {
|
||||
assertEquals(0, scene.leaks(-24, -8, 40, 24));
|
||||
|
||||
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);
|
||||
assertEquals(0, steep.leaks(-24, -8, 40, 24));
|
||||
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
|
||||
@@ -559,7 +657,7 @@ public class ChunkWaterRasterTest {
|
||||
}
|
||||
dams++;
|
||||
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) {
|
||||
tall++;
|
||||
}
|
||||
@@ -596,7 +694,7 @@ public class ChunkWaterRasterTest {
|
||||
drawn.catchment());
|
||||
Scene scene = new Scene(settings(new IrisHydrology()), coast).reach(coastal);
|
||||
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;
|
||||
for (int x = -20; x <= 56; x++) {
|
||||
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.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -177,6 +178,51 @@ public class PondPlannerTest {
|
||||
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
|
||||
public void seededCupOnSlopeIsPerchedAndContained() {
|
||||
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.water.Pond;
|
||||
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.MantleHydrologyComponent;
|
||||
import art.arcane.iris.generation.mantle.MantleWriter;
|
||||
@@ -84,23 +85,18 @@ public class EffectiveWaterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cornerColumnsBelowTwoOneBlockHigherNeighboursBecomeRimsAtTheUpperHead() {
|
||||
long corner = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(80), wet(80), wet(79), NONE);
|
||||
assertEquals(DRY, EffectiveWater.head(corner));
|
||||
assertEquals(80, EffectiveWater.ground(corner));
|
||||
public void lowerCornerColumnsRemainWetBetweenHigherNeighbours() {
|
||||
long corner = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(81), wet(81), wet(79), NONE);
|
||||
assertEquals(EffectiveWater.column(77, 79), corner);
|
||||
|
||||
long mixed = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(80), wet(80), wet(86), NONE);
|
||||
assertEquals(DRY, EffectiveWater.head(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);
|
||||
long mixed = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(81), wet(81), wet(87), NONE);
|
||||
assertEquals(EffectiveWater.column(77, 79), mixed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnsAboveADropOfTwoOrMoreHoldWaterOnlyInTheirTopBlock() {
|
||||
assertEquals(EffectiveWater.column(98, 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));
|
||||
public void wetColumnsKeepTheirCanonicalBedAcrossDrops() {
|
||||
assertEquals(EffectiveWater.column(94, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), wet(79), wet(99), 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(98, 99), EffectiveWater.settle(EffectiveWater.column(98, 99), wet(70), NONE, NONE, NONE));
|
||||
}
|
||||
@@ -197,33 +193,27 @@ public class EffectiveWaterTest {
|
||||
}
|
||||
|
||||
@Test(timeout = 180_000L)
|
||||
public void staircaseStepsTurnExactlyTheirCornerColumnsIntoRims() throws Exception {
|
||||
public void diagonalStaircaseStepsPreserveEveryWetColumnWithoutSourceConversion() throws Exception {
|
||||
Scene scene = staircase();
|
||||
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
|
||||
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 z = 24; z <= 104; z++) {
|
||||
int head = rasterHead(run.hydrology(), x, z);
|
||||
if (head == DRY) {
|
||||
continue;
|
||||
}
|
||||
int above = 0;
|
||||
int highest = DRY;
|
||||
wet++;
|
||||
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) {
|
||||
int neighbour = rasterHead(run.hydrology(), x + side[0], z + side[1]);
|
||||
above += neighbour == head + 1 ? 1 : 0;
|
||||
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));
|
||||
assertFalse("one-block drop at " + x + "," + z, neighbour != DRY && Math.abs(neighbour - head) == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue("rims " + rims, rims > 10);
|
||||
assertTrue("wet columns " + wet, wet > 200);
|
||||
drops.assertSealed(24, 104, 24, 104);
|
||||
}
|
||||
}
|
||||
@@ -234,7 +224,6 @@ public class EffectiveWaterTest {
|
||||
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
|
||||
Drops drops = new Drops(run.complex(), 0, 7, 0, 5);
|
||||
int lips = 0;
|
||||
int raised = 0;
|
||||
int falls = 0;
|
||||
for (int x = 16; x <= 112; x++) {
|
||||
for (int z = 30; z <= 50; z++) {
|
||||
@@ -250,17 +239,22 @@ public class EffectiveWaterTest {
|
||||
if (lip) {
|
||||
lips++;
|
||||
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)) {
|
||||
falls++;
|
||||
assertTrue(drops.falling(x, head + 3, z));
|
||||
assertFalse(drops.falling(x, head + 4, z));
|
||||
int highest = head;
|
||||
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 raised from deeper beds " + raised, raised > 0);
|
||||
assertTrue("falls " + falls, falls > 10);
|
||||
drops.assertSealed(16, 112, 30, 50);
|
||||
}
|
||||
@@ -268,11 +262,24 @@ public class EffectiveWaterTest {
|
||||
|
||||
@Test(timeout = 180_000L)
|
||||
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)) {
|
||||
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 + " 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)));
|
||||
}
|
||||
|
||||
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 {
|
||||
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) {
|
||||
return columns.computeIfAbsent(((long) x << 32) | (z & 0xFFFFFFFFL), key -> new int[]{
|
||||
(int) Math.round(complex.resolveHydrologyTerrainHeight(x, z)),
|
||||
complex.resolveWaterRole(x, z).wet() ? (int) Math.round(complex.resolveWaterHead(x, z)) : DRY});
|
||||
return columns.computeIfAbsent(((long) x << 32) | (z & 0xFFFFFFFFL), key -> {
|
||||
int ground = (int) Math.round(complex.resolveHydrologyTerrainHeight(x, z));
|
||||
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) {
|
||||
|
||||
+4
-4
@@ -206,7 +206,7 @@ public class IrisComplexHeightStreamTest {
|
||||
assertTrue(fixture.complex.isHydrologyPlanned(8, 8));
|
||||
bulk.fillChunkDoubles(0, 0, values);
|
||||
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);
|
||||
bulk.fillChunkDoubles(0, 0, values);
|
||||
assertBits(landed.terrainHeight(8, 8), values[8 << 4 | 8]);
|
||||
@@ -247,14 +247,14 @@ public class IrisComplexHeightStreamTest {
|
||||
public void waterStreamsFollowTheRaster() throws Exception {
|
||||
try (HydrologyFixture fixture = new HydrologyFixture(hydrologyPack())) {
|
||||
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(raster.role(15, 15), fixture.complex.getWaterRoleStream().get(15.5, 15.5));
|
||||
assertBits(HydrologyFixture.SEA_LEVEL, fixture.complex.getWaterHeadStream().getDouble(640, 640));
|
||||
assertSame(WaterRole.NONE, fixture.complex.getWaterRoleStream().get(640, 640));
|
||||
EngineMantle mantle = mock(EngineMantle.class, CALLS_REAL_METHODS);
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -266,7 +266,7 @@ public class IrisComplexHeightStreamTest {
|
||||
doReturn(fixture.engine).when(placer).getEngine();
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -165,7 +165,7 @@ public class IrisComplexTransitionTest {
|
||||
if (terrain > 150) {
|
||||
berms++;
|
||||
assertEquals(terrain, ground, 0D);
|
||||
assertTrue(ground >= 156);
|
||||
assertTrue(ground >= 155);
|
||||
}
|
||||
assertContained(complex, x, z, ground);
|
||||
}
|
||||
@@ -195,13 +195,13 @@ public class IrisComplexTransitionTest {
|
||||
double ground = complex.resolveHydrologyTerrainHeight(x, z);
|
||||
assertTrue(raster.terrainHeight(x, z) < 150);
|
||||
assertContained(complex, x, z, ground);
|
||||
if (ground >= 148) {
|
||||
if (ground >= 147) {
|
||||
dried++;
|
||||
assertEquals(127D, complex.resolveWaterHead(x, z), 0D);
|
||||
assertSame(WaterRole.NONE, complex.resolveWaterRole(x, z));
|
||||
} else {
|
||||
wet++;
|
||||
assertEquals(148D, complex.resolveWaterHead(x, z), 0D);
|
||||
assertEquals(147D, complex.resolveWaterHead(x, z), 0D);
|
||||
assertSame(WaterRole.POND, complex.resolveWaterRole(x, z));
|
||||
}
|
||||
}
|
||||
@@ -220,7 +220,7 @@ public class IrisComplexTransitionTest {
|
||||
FakeHydrology hydrology = seam.hydrology;
|
||||
IrisComplex complex = seam.natural((x, z) -> 150D).complex();
|
||||
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));
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
wet++;
|
||||
@@ -318,7 +318,7 @@ public class IrisComplexTransitionTest {
|
||||
int nx = lx + side[0];
|
||||
int nz = lz + side[1];
|
||||
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 water = 0;
|
||||
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) {
|
||||
dams++;
|
||||
assertSame(WaterRole.NONE, stage.complex.resolveWaterRole(17, lz));
|
||||
assertTrue(17 + "," + lz + " dam at " + stage.complex.resolveHydrologyTerrainHeight(17, lz),
|
||||
stage.complex.resolveHydrologyTerrainHeight(17, lz) >= 157D);
|
||||
assertSame(stage.stone, blended.getRaw(1, 157, lz));
|
||||
stage.complex.resolveHydrologyTerrainHeight(17, lz) >= 156D);
|
||||
assertSame(stage.stone, blended.getRaw(1, 156, lz));
|
||||
}
|
||||
for (int lx = 0; lx < 16; lx++) {
|
||||
for (int y = 0; y < TransitionStage.HEIGHT; y++) {
|
||||
@@ -373,7 +373,7 @@ public class IrisComplexTransitionTest {
|
||||
public void seamDamHoldsNewWaterBesideAnOpenOldRiverOrLakeEdge() throws Exception {
|
||||
try (TransitionStage stage = new TransitionStage(IrisComplexTransitionTest::oldChunkWeight, true, (x, z) -> 150.3D,
|
||||
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));
|
||||
assertTrue(stage.dams(blended) > 0);
|
||||
stage.assertNoWaterTouchesAir(blended);
|
||||
@@ -421,9 +421,9 @@ public class IrisComplexTransitionTest {
|
||||
if (stage.raster(1).waterHead(1, lz) != Integer.MIN_VALUE) {
|
||||
wet++;
|
||||
assertSame(WaterRole.POND, stage.complex.resolveWaterRole(17, lz));
|
||||
assertEquals(156D, stage.complex.resolveWaterHead(17, lz), 0D);
|
||||
assertSame(stage.water, blended.getRaw(1, 156, lz));
|
||||
assertSame(stage.stone, blended.getRaw(0, 156, lz));
|
||||
assertEquals(155D, stage.complex.resolveWaterHead(17, lz), 0D);
|
||||
assertSame(stage.water, blended.getRaw(1, 155, lz));
|
||||
assertSame(stage.stone, blended.getRaw(0, 155, lz));
|
||||
}
|
||||
}
|
||||
assertTrue(wet > 0);
|
||||
@@ -455,7 +455,7 @@ public class IrisComplexTransitionTest {
|
||||
boolean beside = oldChunkWeight(x + 1, z) == 0D || oldChunkWeight(x - 1, z) == 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
|
||||
&& 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");
|
||||
}
|
||||
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++) {
|
||||
if (raster(1).waterHead(1, lz) != Integer.MIN_VALUE && complex.resolveWaterRole(17, lz) == WaterRole.NONE) {
|
||||
dams++;
|
||||
assertTrue(17 + "," + lz + " dam below the crest", complex.resolveHydrologyTerrainHeight(17, lz) >= 157D);
|
||||
assertSame(stone, blended.getRaw(1, 157, lz));
|
||||
assertTrue(17 + "," + lz + " dam below the crest", complex.resolveHydrologyTerrainHeight(17, lz) >= 156D);
|
||||
assertSame(stone, blended.getRaw(1, 156, lz));
|
||||
}
|
||||
}
|
||||
return dams;
|
||||
@@ -821,7 +821,7 @@ public class IrisComplexTransitionTest {
|
||||
dams++;
|
||||
assertEquals(127D, head, 0D);
|
||||
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 org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.Before;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -51,6 +52,14 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
||||
|
||||
@Rule
|
||||
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
|
||||
public void hydrologyOwnedFluidBypassesBiomeSeaLayers() {
|
||||
@@ -100,7 +109,7 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
||||
|
||||
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++) {
|
||||
assertSame(role + " y " + y, painter.channelFluid, output.get(0, y, 0));
|
||||
}
|
||||
@@ -117,7 +126,7 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
||||
|
||||
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("the slot takes the sea layers", ocean.ice, sea.get(0, HEAD, 0));
|
||||
for (int y = GROUND + 1; y < HEAD; y++) {
|
||||
@@ -178,7 +187,7 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
||||
NativeBlockState expectedTop = switch (role) {
|
||||
case NONE -> painter.grass;
|
||||
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL -> painter.bed;
|
||||
case INLET -> painter.grass;
|
||||
case INLET -> submergedDirt;
|
||||
case SHORE, POND_SHORE, DAM -> painter.shore;
|
||||
case BANK -> painter.bank;
|
||||
};
|
||||
@@ -192,11 +201,68 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
||||
}
|
||||
materials.setBank(null).setShore(null).setBed(null);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
public void sandSurvivesUnlessTheMaterialsKeepFallingBlocksAwayFromTheWater() {
|
||||
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);
|
||||
for (ChunkGenerationSemantics.RiverFeatureOccurrence occurrence : pond.riverFeatures()) {
|
||||
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,
|
||||
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 {
|
||||
long plannedWetColumns;
|
||||
long effectiveWetColumns;
|
||||
long lostWetColumns;
|
||||
long raisedWetBedColumns;
|
||||
long raisedWetBedBlocks;
|
||||
int maximumWetBedRaise;
|
||||
long solidRaisedBedBlocks;
|
||||
long dropAdjacentRaisedWetBedColumns;
|
||||
long censoredRaisedWetBedColumns;
|
||||
long channelColumns;
|
||||
long pondColumns;
|
||||
long bankColumns;
|
||||
@@ -88,7 +96,16 @@ public final class HydrologyBlockProbe {
|
||||
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) {
|
||||
@@ -96,8 +113,10 @@ public final class HydrologyBlockProbe {
|
||||
try {
|
||||
Result result = run(Configuration.parse(args));
|
||||
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
|
||||
+ " effectiveWetColumns=" + evidence.effectiveWetColumns + " lostWetColumns=" + evidence.lostWetColumns
|
||||
+ " raisedWetBedColumns=" + evidence.raisedWetBedColumns + " raisedWetBedBlocks=" + evidence.raisedWetBedBlocks
|
||||
+ " containmentLeaks=" + evidence.scan.containmentLeaks + " tickConversionCells=" + evidence.scan.tickConversionCells
|
||||
+ " cane=" + evidence.validCaneRoots + " failures=" + result.failures().size()
|
||||
+ " output=" + result.configuration().output().getAbsolutePath());
|
||||
@@ -135,7 +154,8 @@ public final class HydrologyBlockProbe {
|
||||
failures.add("The engine reported " + reports.size() + " generation errors.");
|
||||
}
|
||||
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);
|
||||
Files.writeString(new File(configuration.output(), "hydrology-blocks.json").toPath(), json(result), StandardCharsets.UTF_8);
|
||||
return result;
|
||||
@@ -155,7 +175,9 @@ public final class HydrologyBlockProbe {
|
||||
int head = heads[(lz << 4) | lx];
|
||||
int natural = complex.naturalTrueHeight(x, z);
|
||||
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 -> {
|
||||
}
|
||||
}
|
||||
if (column.head() != HydrologyVoxels.NO_WATER) {
|
||||
evidence.plannedWetColumns++;
|
||||
inspectWater(column, x, z, evidence);
|
||||
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)) {
|
||||
evidence.scan.column(volume, x, z, HydrologyBlockMetrics.scanFloor(columns, seaLevel, x, z), height - 1);
|
||||
@@ -214,6 +243,58 @@ public final class HydrologyBlockProbe {
|
||||
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) {
|
||||
for (int y = 1; y < height; y++) {
|
||||
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) {
|
||||
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) {
|
||||
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();
|
||||
Map<String, Object> counts = new LinkedHashMap<>();
|
||||
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("channelColumns", evidence.channelColumns);
|
||||
counts.put("pondColumns", evidence.pondColumns);
|
||||
@@ -313,6 +409,9 @@ public final class HydrologyBlockProbe {
|
||||
report.put("artifacts", result.artifacts());
|
||||
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("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";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import org.junit.Rule;
|
||||
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.Map;
|
||||
import java.util.Set;
|
||||
|
||||
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 {
|
||||
@Rule
|
||||
public TemporaryFolder folder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void failuresNameMissingSurfaceLeaksTicksAndCoverage() {
|
||||
HydrologyBlockProbe.Evidence evidence = new HydrologyBlockProbe.Evidence();
|
||||
@@ -21,4 +36,110 @@ public class HydrologyBlockProbeTest {
|
||||
"Fluid ticks would move generated water in 4 cells.",
|
||||
"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