River transect probe

- riverTransectProbe renders each surface course as a plan and five cross-sections
- Summary gates ocean writes and spilling channel cells
- Grading columns now report the bank biome
This commit is contained in:
Brian Neumann-Fopiano
2026-09-01 19:18:59 -04:00
parent 676372efab
commit 2585b2105a
4 changed files with 585 additions and 3 deletions
@@ -266,7 +266,7 @@ public class HydrologyModelTest {
}
@Test
public void outerGradingUsesTheExactParentBiome() {
public void outerGradingUsesTheBankBiome() {
HydrologyFeatureRef feature = feature(HydrologyFeatureType.SURFACE_POOL, 3L, 70);
HydrologyColumnLayer grading = layer(feature, 72, 72, 72, false, false, false);
grading = new HydrologyColumnLayer(
@@ -292,7 +292,7 @@ public class HydrologyModelTest {
);
HydrologyColumnSample sample = new HydrologyColumnSample(0, 0, 74, 63, false, "exact_parent", List.of(grading));
assertTrue(grading.biomeKey() == null);
assertEquals("dry", grading.biomeKey());
assertEquals("exact_parent", sample.parentBiomeKey());
assertFalse(sample.hasConnectedFluid());
}
+36 -1
View File
@@ -82,6 +82,9 @@ Provider<String> probeMaximumChunkZ = providers.gradleProperty('probeMaximumChun
Provider<String> probeParallelism = providers.gradleProperty('probeParallelism')
Provider<String> probeShuffleSeed = providers.gradleProperty('probeShuffleSeed')
Provider<String> probeJfr = providers.gradleProperty('probeJfr')
Provider<String> probeTileX = providers.gradleProperty('probeTileX')
Provider<String> probeTileZ = providers.gradleProperty('probeTileZ')
Provider<String> probeOutput = providers.gradleProperty('probeOutput')
tasks.register('genProbe', JavaExec) {
group = 'verification'
@@ -187,7 +190,39 @@ tasks.register('generationOrderProbe', JavaExec) {
}
}
['hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe'].each { String taskName ->
tasks.register('riverTransectProbe', JavaExec) {
group = 'verification'
description = 'Plans one hydrology tile over a real pack and renders every surface river as a plan, cross-sections and a summary with ocean-write and containment gates.'
mainClass = 'art.arcane.iris.probe.RiverTransectProbe'
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
dependsOn(':core:compileJava')
doFirst {
Map<String, Provider<String>> required = [
probePack: probePack,
probeDimension: probeDimension,
probeSeed: probeSeed,
probeTileX: probeTileX,
probeTileZ: probeTileZ,
probeOutput: probeOutput
]
required.each { String name, Provider<String> value ->
if (!value.isPresent() || value.get().isBlank()) {
throw new GradleException("riverTransectProbe requires -P${name}=<value>")
}
}
args(
probePack.get(),
probeDimension.get(),
probeSeed.get(),
probeTileX.get(),
probeTileZ.get(),
probeOutput.get(),
probeStudio.get())
}
}
['hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe', 'riverTransectProbe'].each { String taskName ->
tasks.named(taskName, JavaExec).configure { JavaExec task ->
doFirst {
if (probeJfr.isPresent()) {
@@ -0,0 +1,473 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.hydrology.HydraulicSegment;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import art.arcane.iris.engine.hydrology.HydrologyTile;
import art.arcane.iris.engine.hydrology.HydrologyTileKey;
import art.arcane.iris.engine.hydrology.RiverCourse;
import art.arcane.iris.engine.hydrology.RiverCourseType;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.hydrology.surface.SurfaceCenterline;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
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.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
/**
* Plans one hydrology tile over a real pack and renders every exposed surface course as a
* top-down plan plus five cross-sections, with a summary of cut depth, bank steps, ocean writes
* and spilling channel cells. Exit code 1 when any course writes the ocean or spills.
*/
public final class RiverTransectProbe {
static final int NO_WATER = Integer.MIN_VALUE;
private static final String PREFIX = "[transect]";
private static final int MARGIN = 48;
private static final int HALF_SECTION = 40;
private static final int SECTIONS = 5;
private static final int SECTION_SCALE = 4;
private static final int SECTION_ROW_HEIGHT = 120;
private static final int MAXIMUM_PLAN_COLUMNS = 6_000_000;
private RiverTransectProbe() {
}
enum Role {
NONE,
CHANNEL,
SHORE,
BANK,
APRON;
boolean owned() {
return this == CHANNEL || this == SHORE || this == BANK;
}
}
record ColumnView(int x, int z, int natural, int terrain, int water, Role role) {
int cut() {
return natural - terrain;
}
}
record CourseSummary(
long id,
int stations,
int ownedColumns,
int minimumCut,
int maximumCut,
int maximumBankStep,
int oceanWrites,
int uncontainedWetCells
) {
boolean passes() {
return oceanWrites == 0 && uncontainedWetCells == 0;
}
String line() {
return String.format(Locale.ROOT,
"%s course=%d %s stations=%d owned=%d cut=%d..%d bankStep=%d oceanWrites=%d uncontained=%d",
PREFIX, id, passes() ? "PASS" : "FAIL", stations, ownedColumns, minimumCut, maximumCut,
maximumBankStep, oceanWrites, uncontainedWetCells);
}
}
record Configuration(File pack, String dimension, long seed, int tileX, int tileZ, File output, boolean studio) {
static Configuration parse(String[] args) {
if (args.length < 6 || args.length > 7) {
throw new IllegalArgumentException(
"usage: <pack> <dimension> <seed> <tileX> <tileZ> <outputDir> [studio]");
}
boolean studio = args.length == 7 && RealPackProbeSupport.parseBoolean(args[6], "studio");
return new Configuration(
new File(args[0]),
args[1],
Long.parseLong(args[2].trim()),
Integer.parseInt(args[3].trim()),
Integer.parseInt(args[4].trim()),
new File(args[5]),
studio
);
}
}
private record Bounds(int minimumX, int minimumZ, int maximumX, int maximumZ) {
static Bounds of(SurfaceCenterline centerline, int margin) {
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (int station = 0; station < centerline.size(); station++) {
minimumX = Math.min(minimumX, centerline.x()[station]);
minimumZ = Math.min(minimumZ, centerline.z()[station]);
maximumX = Math.max(maximumX, centerline.x()[station]);
maximumZ = Math.max(maximumZ, centerline.z()[station]);
}
return new Bounds(minimumX - margin, minimumZ - margin, maximumX + margin, maximumZ + margin);
}
int width() {
return maximumX - minimumX + 1;
}
int depth() {
return maximumZ - minimumZ + 1;
}
}
public static void main(String[] args) {
Configuration configuration;
try {
configuration = Configuration.parse(args);
} catch (Throwable failure) {
System.out.println(PREFIX + " FAIL: " + failure.getMessage());
System.exit(2);
return;
}
int exitCode;
try {
exitCode = run(configuration) ? 0 : 1;
} catch (Throwable failure) {
System.out.println(PREFIX + " FAIL: probe execution failed");
failure.printStackTrace(System.out);
exitCode = 2;
}
System.exit(exitCode);
}
static boolean run(Configuration configuration) throws Exception {
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
configuration.pack(), configuration.dimension(), PREFIX);
RealPackProbeSupport.EngineSession session = workspace.openEngine(
configuration.seed(), configuration.studio(), "transect")) {
Engine engine = session.engine();
IrisComplex complex = engine.getComplex();
IrisHydrologyRuntime runtime = complex.getHydrologyRuntime();
if (runtime == null) {
throw new IllegalStateException("Dimension '" + configuration.dimension() + "' has no hydrology runtime.");
}
Files.createDirectories(configuration.output().toPath());
HydrologyTileKey key = new HydrologyTileKey(configuration.tileX(), configuration.tileZ());
long planStart = System.nanoTime();
HydrologyTile tile = runtime.tile(key);
double planMillis = (System.nanoTime() - planStart) / 1_000_000D;
int seaLevel = runtime.settings().seaLevel();
System.out.println(String.format(Locale.ROOT,
"%s tile=%d,%d seed=%d courses=%d planMs=%.1f",
PREFIX, key.tileX(), key.tileZ(), configuration.seed(), tile.courses().size(), planMillis));
List<CourseSummary> summaries = new ArrayList<>();
for (RiverCourse course : tile.courses()) {
if (course.type() != RiverCourseType.SURFACE) {
continue;
}
List<HydrologyPoint> exposedPath = exposedPath(course);
if (exposedPath.size() < 2) {
continue;
}
SurfaceCenterline centerline = SurfaceCenterline.densify(exposedPath);
Bounds bounds = Bounds.of(centerline, MARGIN);
Map<Long, ColumnView> columns = sampleColumns(complex, runtime, bounds);
CourseSummary summary = summarize(course.id(), centerline.size(), seaLevel, columns);
summaries.add(summary);
writePlan(new File(configuration.output(), "course-" + course.id() + ".png"), bounds, columns);
writeSections(new File(configuration.output(), "course-" + course.id() + "-sections.png"),
centerline, complex, runtime);
System.out.println(summary.line());
}
writeSummary(new File(configuration.output(), "summary.txt"), configuration, tile, summaries);
boolean pass = summaries.stream().allMatch(CourseSummary::passes);
System.out.println(PREFIX + " " + (pass ? "PASS" : "FAIL")
+ " surfaceCourses=" + summaries.size()
+ " output=" + configuration.output().getAbsolutePath());
return pass;
}
}
static int[] sectionStations(int stations) {
int[] selected = new int[SECTIONS];
for (int section = 0; section < SECTIONS; section++) {
double fraction = 0.1D + 0.2D * section;
selected[section] = Math.max(0, Math.min(stations - 1, (int) Math.floor(stations * fraction)));
}
return selected;
}
static CourseSummary summarize(long id, int stations, int seaLevel, Map<Long, ColumnView> columns) {
int owned = 0;
int minimumCut = Integer.MAX_VALUE;
int maximumCut = Integer.MIN_VALUE;
int maximumBankStep = 0;
int oceanWrites = 0;
int uncontained = 0;
for (ColumnView column : columns.values()) {
boolean submerged = column.natural() <= seaLevel;
if (submerged && (column.terrain() != column.natural() || column.role().owned())) {
oceanWrites++;
}
if (!column.role().owned()) {
continue;
}
owned++;
minimumCut = Math.min(minimumCut, column.cut());
maximumCut = Math.max(maximumCut, column.cut());
if (column.role() == Role.CHANNEL) {
if (column.water() != NO_WATER && spills(column, columns)) {
uncontained++;
}
continue;
}
maximumBankStep = Math.max(maximumBankStep, bankStep(column, columns.get(RiverFootprint.pack(column.x() + 1, column.z()))));
maximumBankStep = Math.max(maximumBankStep, bankStep(column, columns.get(RiverFootprint.pack(column.x(), column.z() + 1))));
}
if (owned == 0) {
minimumCut = 0;
maximumCut = 0;
}
return new CourseSummary(id, stations, owned, minimumCut, maximumCut, maximumBankStep, oceanWrites, uncontained);
}
private static boolean spills(ColumnView channel, Map<Long, ColumnView> columns) {
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] offset : offsets) {
ColumnView neighbour = columns.get(RiverFootprint.pack(channel.x() + offset[0], channel.z() + offset[1]));
if (neighbour == null || neighbour.role() == Role.CHANNEL) {
continue;
}
if (neighbour.terrain() < channel.water()) {
return true;
}
}
return false;
}
private static int bankStep(ColumnView bank, ColumnView neighbour) {
if (neighbour == null || neighbour.role() == Role.CHANNEL) {
return 0;
}
return Math.abs(bank.terrain() - neighbour.terrain());
}
private static List<HydrologyPoint> exposedPath(RiverCourse course) {
ArrayList<HydrologyPoint> path = new ArrayList<>();
for (HydraulicSegment segment : course.segments()) {
if (!SurfaceFootprintCompiler.exposedSegment(segment)) {
break;
}
for (HydrologyPoint point : segment.centerline()) {
if (!path.isEmpty()) {
HydrologyPoint last = path.getLast();
if (last.x() == point.x() && last.z() == point.z()) {
continue;
}
}
path.add(point);
}
}
return path;
}
private static Map<Long, ColumnView> sampleColumns(IrisComplex complex, IrisHydrologyRuntime runtime, Bounds bounds) {
long area = (long) bounds.width() * bounds.depth();
if (area > MAXIMUM_PLAN_COLUMNS) {
throw new IllegalStateException("Course bounding box too large to render: " + area + " columns.");
}
HashMap<Long, ColumnView> columns = new HashMap<>((int) Math.min(Integer.MAX_VALUE, area * 2));
for (int z = bounds.minimumZ(); z <= bounds.maximumZ(); z++) {
for (int x = bounds.minimumX(); x <= bounds.maximumX(); x++) {
columns.put(RiverFootprint.pack(x, z), column(complex, runtime, x, z));
}
}
return columns;
}
private static ColumnView column(IrisComplex complex, IrisHydrologyRuntime runtime, int x, int z) {
Optional<HydrologyColumnSample> sample = runtime.sample(x, z);
int natural = sample.map(HydrologyColumnSample::naturalHeight)
.orElseGet(() -> (int) Math.round(complex.getNaturalHeightStream().getDouble(x, z)));
int terrain = (int) Math.round(complex.getHeightStream().getDouble(x, z));
HydrologyColumnLayer surface = sample.flatMap(HydrologyColumnSample::primarySurfaceLayer).orElse(null);
HydrologyColumnLayer fluid = sample.flatMap(HydrologyColumnSample::primarySurfaceFluidLayer).orElse(null);
int water = fluid == null ? NO_WATER : fluid.fluidHeadY();
return new ColumnView(x, z, natural, terrain, water, role(surface));
}
private static Role role(HydrologyColumnLayer layer) {
if (layer == null) {
return Role.NONE;
}
if (layer.oceanApron()) {
return Role.APRON;
}
if (layer.channel()) {
return Role.CHANNEL;
}
if (layer.shore()) {
return Role.SHORE;
}
return layer.terrainOwned() ? Role.BANK : Role.NONE;
}
private static void writePlan(File file, Bounds bounds, Map<Long, ColumnView> columns) throws IOException {
int minimumNatural = Integer.MAX_VALUE;
int maximumNatural = Integer.MIN_VALUE;
for (ColumnView column : columns.values()) {
minimumNatural = Math.min(minimumNatural, column.natural());
maximumNatural = Math.max(maximumNatural, column.natural());
}
double range = Math.max(1, maximumNatural - minimumNatural);
BufferedImage image = new BufferedImage(bounds.width(), bounds.depth(), BufferedImage.TYPE_INT_RGB);
for (int z = bounds.minimumZ(); z <= bounds.maximumZ(); z++) {
for (int x = bounds.minimumX(); x <= bounds.maximumX(); x++) {
ColumnView column = columns.get(RiverFootprint.pack(x, z));
int gray = 40 + (int) Math.round(190D * (column.natural() - minimumNatural) / range);
int red = gray;
int green = gray;
int blue = gray;
switch (column.role()) {
case CHANNEL -> {
red = 30;
green = 90;
blue = 220;
}
case SHORE -> {
red = 214;
green = 184;
blue = 122;
}
case BANK -> {
int darken = Math.min(120, column.cut() * 12);
red = Math.max(0, gray - darken + 20);
green = Math.max(0, gray - darken);
blue = Math.max(0, gray - darken - 20);
}
case APRON -> {
red = 120;
green = 170;
blue = 230;
}
case NONE -> {
if (column.terrain() != column.natural()) {
red = 220;
green = 40;
blue = 40;
}
}
}
image.setRGB(x - bounds.minimumX(), z - bounds.minimumZ(), (red << 16) | (green << 8) | blue);
}
}
ImageIO.write(image, "png", file);
}
private static void writeSections(
File file,
SurfaceCenterline centerline,
IrisComplex complex,
IrisHydrologyRuntime runtime
) throws IOException {
int[] stations = sectionStations(centerline.size());
int span = HALF_SECTION * 2 + 1;
int width = span * SECTION_SCALE;
BufferedImage image = new BufferedImage(width, SECTION_ROW_HEIGHT * SECTIONS, BufferedImage.TYPE_INT_RGB);
for (int section = 0; section < SECTIONS; section++) {
int station = stations[section];
double normalX = centerline.normalX(station);
double normalZ = centerline.normalZ(station);
ColumnView[] profile = new ColumnView[span];
int minimum = Integer.MAX_VALUE;
int maximum = Integer.MIN_VALUE;
for (int offset = -HALF_SECTION; offset <= HALF_SECTION; offset++) {
int x = (int) Math.round(centerline.x()[station] + normalX * offset);
int z = (int) Math.round(centerline.z()[station] + normalZ * offset);
ColumnView column = column(complex, runtime, x, z);
profile[offset + HALF_SECTION] = column;
minimum = Math.min(minimum, Math.min(column.natural(), column.terrain()));
maximum = Math.max(maximum, Math.max(column.natural(), column.terrain()));
if (column.water() != NO_WATER) {
maximum = Math.max(maximum, column.water());
}
}
int rowTop = section * SECTION_ROW_HEIGHT;
fill(image, 0, rowTop, width, SECTION_ROW_HEIGHT, 0x1E1E1E);
double scale = (SECTION_ROW_HEIGHT - 12D) / Math.max(1, maximum - minimum + 1);
for (int index = 0; index < span; index++) {
ColumnView column = profile[index];
int left = index * SECTION_SCALE;
int naturalY = rowY(rowTop, column.natural(), minimum, scale);
int terrainY = rowY(rowTop, column.terrain(), minimum, scale);
fill(image, left, naturalY, SECTION_SCALE, rowTop + SECTION_ROW_HEIGHT - naturalY, 0x555555);
fill(image, left, terrainY, SECTION_SCALE, rowTop + SECTION_ROW_HEIGHT - terrainY, 0x9A7B4F);
if (column.water() != NO_WATER && column.water() >= column.terrain()) {
int waterY = rowY(rowTop, column.water(), minimum, scale);
fill(image, left, waterY, SECTION_SCALE, Math.max(1, terrainY - waterY), 0x2E6BE6);
}
if (column.role() == Role.SHORE) {
fill(image, left, terrainY - 2, SECTION_SCALE, 2, 0xD6B87A);
}
}
fill(image, HALF_SECTION * SECTION_SCALE, rowTop, 1, 6, 0xFFFFFF);
}
ImageIO.write(image, "png", file);
}
private static int rowY(int rowTop, int height, int minimum, double scale) {
return rowTop + SECTION_ROW_HEIGHT - 6 - (int) Math.round((height - minimum + 1) * scale);
}
private static void fill(BufferedImage image, int left, int top, int width, int height, int rgb) {
int right = Math.min(image.getWidth(), left + width);
int bottom = Math.min(image.getHeight(), top + height);
for (int y = Math.max(0, top); y < bottom; y++) {
for (int x = Math.max(0, left); x < right; x++) {
image.setRGB(x, y, rgb);
}
}
}
private static void writeSummary(
File file,
Configuration configuration,
HydrologyTile tile,
List<CourseSummary> summaries
) throws IOException {
StringBuilder text = new StringBuilder();
text.append("tile=").append(tile.key().tileX()).append(',').append(tile.key().tileZ())
.append(" seed=").append(configuration.seed())
.append(" dimension=").append(configuration.dimension())
.append(" courses=").append(tile.courses().size())
.append(" surfaceCourses=").append(summaries.size())
.append('\n');
int maximumCut = 0;
int maximumBankStep = 0;
int oceanWrites = 0;
int uncontained = 0;
for (CourseSummary summary : summaries) {
text.append(summary.line()).append('\n');
maximumCut = Math.max(maximumCut, summary.maximumCut());
maximumBankStep = Math.max(maximumBankStep, summary.maximumBankStep());
oceanWrites += summary.oceanWrites();
uncontained += summary.uncontainedWetCells();
}
text.append("maximumCut=").append(maximumCut)
.append(" maximumBankStep=").append(maximumBankStep)
.append(" oceanWrites=").append(oceanWrites)
.append(" uncontainedWetCells=").append(uncontained)
.append('\n');
Files.writeString(file.toPath(), text.toString(), StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,74 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class RiverTransectProbeTest {
@Test
public void sectionStationsSitAtTenThirtyFiftySeventyAndNinetyPercent() {
assertArrayEquals(new int[] {2, 6, 10, 14, 18}, RiverTransectProbe.sectionStations(20));
assertArrayEquals(new int[] {0, 0, 0, 0, 0}, RiverTransectProbe.sectionStations(1));
assertArrayEquals(new int[] {0, 0, 1, 2, 2}, RiverTransectProbe.sectionStations(3));
}
@Test
public void summaryCountsCutsBankStepsOceanWritesAndSpillingChannels() {
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
put(columns, 0, 0, 72, 68, 70, RiverTransectProbe.Role.CHANNEL);
put(columns, 1, 0, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
put(columns, -1, 0, 72, 69, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
put(columns, 0, 1, 72, 70, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.SHORE);
put(columns, 0, -1, 72, 72, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.NONE);
put(columns, 2, 0, 72, 72, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.NONE);
put(columns, 5, 5, 60, 58, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.NONE);
put(columns, 6, 6, 60, 60, 63, RiverTransectProbe.Role.APRON);
RiverTransectProbe.CourseSummary summary = RiverTransectProbe.summarize(7L, 12, 63, columns);
assertEquals(7L, summary.id());
assertEquals(12, summary.stations());
assertEquals(4, summary.ownedColumns());
assertEquals(1, summary.minimumCut());
assertEquals(4, summary.maximumCut());
assertEquals(1, summary.maximumBankStep());
assertEquals(1, summary.oceanWrites());
assertEquals(1, summary.uncontainedWetCells());
assertFalse(summary.passes());
}
@Test
public void containedCourseAwayFromTheOceanPasses() {
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
put(columns, 0, 0, 72, 68, 70, RiverTransectProbe.Role.CHANNEL);
put(columns, 1, 0, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
put(columns, -1, 0, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
put(columns, 0, 1, 72, 70, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.SHORE);
put(columns, 0, -1, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.SHORE);
RiverTransectProbe.CourseSummary summary = RiverTransectProbe.summarize(1L, 3, 63, columns);
assertTrue(summary.passes());
assertEquals(0, summary.oceanWrites());
assertEquals(0, summary.uncontainedWetCells());
}
private static void put(
Map<Long, RiverTransectProbe.ColumnView> columns,
int x,
int z,
int natural,
int terrain,
int water,
RiverTransectProbe.Role role
) {
columns.put(RiverFootprint.pack(x, z), new RiverTransectProbe.ColumnView(x, z, natural, terrain, water, role));
}
}