Hydrology fixes

This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-10 23:07:46 -04:00
1 parent 577f924133
commit bf12f30d26
168 files changed
+13544 -582

No files matched your search

+31 -1
View File
@@ -94,6 +94,36 @@ Provider<String> probeTileZ = providers.gradleProperty('probeTileZ')
Provider<String> probeOutput = providers.gradleProperty('probeOutput')
Provider<String> probeStrictGeometry = providers.gradleProperty('probeStrictGeometry').orElse('false')
tasks.register('hydrologyBlockProbe', JavaExec) {
group = 'verification'
description = 'Generates up to 64 real pack chunks and checks final cane support, channel fluid and mouth ocean connectivity, with block category images.'
mainClass = 'art.arcane.iris.probe.HydrologyBlockProbe'
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,
probeMinimumChunkX: probeMinimumChunkX,
probeMaximumChunkX: probeMaximumChunkX,
probeMinimumChunkZ: probeMinimumChunkZ,
probeMaximumChunkZ: probeMaximumChunkZ,
probeRequiredCoverage: probeRequiredCoverage,
probeOutput: probeOutput
]
required.each { String name, Provider<String> value ->
if (!value.isPresent() || value.get().isBlank()) {
throw new GradleException("hydrologyBlockProbe requires -P${name}=<value>")
}
}
args(probePack.get(), probeDimension.get(), probeSeed.get(), probeMinimumChunkX.get(),
probeMaximumChunkX.get(), probeMinimumChunkZ.get(), probeMaximumChunkZ.get(),
probeRequiredCoverage.get(), probeOutput.get(), probeStudio.get())
}
}
tasks.register('terrain3DProbe', JavaExec) {
group = 'verification'
description = 'Generates real pack chunks, verifies volumetric terrain spans and required biome overhang coverage, and exports actual block cross-sections.'
@@ -260,7 +290,7 @@ tasks.register('riverTransectProbe', JavaExec) {
}
}
['hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe', 'riverTransectProbe', 'terrain3DProbe'].each { String taskName ->
['hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe', 'riverTransectProbe', 'terrain3DProbe', 'hydrologyBlockProbe'].each { String taskName ->
tasks.named(taskName, JavaExec).configure { JavaExec task ->
classpath += configurations.studioProbeApi
doFirst {
@@ -0,0 +1,523 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.spi.PlatformBlockState;
import com.google.gson.GsonBuilder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public final class HydrologyBlockProbe {
private static final int MAXIMUM_CHUNKS = 64;
private static final long MAXIMUM_VOXELS = 16_777_216L;
private static final int MAXIMUM_EXAMPLES = 16;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private HydrologyBlockProbe() {
}
record Configuration(File pack, String dimension, long seed, int minimumChunkX, int maximumChunkX,
int minimumChunkZ, int maximumChunkZ, Set<String> requiredCoverage, File output,
boolean studio) {
Configuration {
if (pack == null || !pack.isAbsolute() || dimension == null || dimension.isBlank()
|| output == null || !output.isAbsolute()) {
throw new IllegalArgumentException("Absolute pack/output paths and a dimension key are required.");
}
long width = (long) maximumChunkX - minimumChunkX + 1;
long depth = (long) maximumChunkZ - minimumChunkZ + 1;
if (width < 1 || depth < 1 || width > MAXIMUM_CHUNKS || depth > MAXIMUM_CHUNKS
|| width * depth > MAXIMUM_CHUNKS
|| minimumChunkX < Integer.MIN_VALUE / 16 || minimumChunkZ < Integer.MIN_VALUE / 16
|| maximumChunkX > Integer.MAX_VALUE / 16 - 1 || maximumChunkZ > Integer.MAX_VALUE / 16 - 1) {
throw new IllegalArgumentException("Bounds must contain 1..64 chunks and fit block coordinates.");
}
requiredCoverage = Set.copyOf(requiredCoverage);
if (!Set.of("cane", "mouth", "channel", "bank").containsAll(requiredCoverage)) {
throw new IllegalArgumentException("Required coverage accepts cane,mouth,channel,bank or -.");
}
}
static Configuration parse(String[] args) {
if (args.length != 10) {
throw new IllegalArgumentException("Expected <pack> <dimension> <seed> <minChunkX> <maxChunkX> "
+ "<minChunkZ> <maxChunkZ> <requiredCoverage|-> <outputDir> <studio>.");
}
Set<String> required = new LinkedHashSet<>();
if (!args[7].equals("-")) {
for (String value : args[7].split(",", -1)) {
required.add(value.trim());
}
}
return new Configuration(new File(args[0]), args[1], Long.parseLong(args[2]), Integer.parseInt(args[3]),
Integer.parseInt(args[4]), Integer.parseInt(args[5]), Integer.parseInt(args[6]), required,
new File(args[8]), RealPackProbeSupport.parseBoolean(args[9], "studio"));
}
}
record PlannedColumn(int naturalY, boolean ocean, HydrologyColumnLayer surface, PlatformBlockState fluid) {
boolean mouthAt(int seaLevel) {
return surface != null && fluid != null && fluid.isWater()
&& surface.feature().type() == HydrologyFeatureType.MOUTH
&& surface.channel() && (surface.fluidOwned() || surface.oceanApron())
&& surface.fluidHeadY() == seaLevel;
}
}
static final class Evidence {
long caneRoots;
long validCaneRoots;
long invalidCaneSubstrates;
long dryCaneRoots;
long censoredCaneRoots;
long plannedWetVoxels;
long missingWetVoxels;
long bankColumns;
long bankExcavation;
int maximumBankCut;
long oceanBedWrites;
long drySillCuts;
long invalidDrySillCuts;
int connectedMouthComponents;
int disconnectedMouthComponents;
int censoredMouthComponents;
final List<String> examples = new ArrayList<>();
void example(String value) {
if (examples.size() < MAXIMUM_EXAMPLES) {
examples.add(value);
}
}
}
record Result(String status, Configuration configuration, int worldMinimumY, int seaLevel,
Evidence evidence, List<String> failures, List<String> artifacts) {
}
static final class Volume {
private final Map<Long, RealPackProbeSupport.GeneratedChunk> chunks;
private final int minimumX;
private final int maximumX;
private final int minimumZ;
private final int maximumZ;
private final int height;
Volume(Map<Long, RealPackProbeSupport.GeneratedChunk> chunks) {
if (chunks.isEmpty()) {
throw new IllegalArgumentException("Generated chunks are required.");
}
this.chunks = Map.copyOf(chunks);
int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minZ = Integer.MAX_VALUE;
int maxZ = Integer.MIN_VALUE;
int selectedHeight = chunks.values().iterator().next().height();
for (RealPackProbeSupport.GeneratedChunk chunk : chunks.values()) {
if (chunk.height() != selectedHeight) {
throw new IllegalArgumentException("Generated chunks must have the same height.");
}
minX = Math.min(minX, chunk.chunkX() << 4);
maxX = Math.max(maxX, (chunk.chunkX() << 4) + 15);
minZ = Math.min(minZ, chunk.chunkZ() << 4);
maxZ = Math.max(maxZ, (chunk.chunkZ() << 4) + 15);
}
minimumX = minX;
maximumX = maxX;
minimumZ = minZ;
maximumZ = maxZ;
height = selectedHeight;
}
boolean contains(int x, int z) {
return chunks.containsKey(RiverFootprint.pack(Math.floorDiv(x, 16), Math.floorDiv(z, 16)));
}
PlatformBlockState block(int x, int y, int z) {
RealPackProbeSupport.GeneratedChunk chunk = chunks.get(
RiverFootprint.pack(Math.floorDiv(x, 16), Math.floorDiv(z, 16)));
return chunk == null || y < 0 || y >= height ? null : chunk.blockAt(x, y, z);
}
}
public static void main(String[] args) {
int exitCode = 2;
try {
Result result = run(Configuration.parse(args));
Evidence evidence = result.evidence();
System.out.println("IRIS_HYDROLOGY_BLOCK_RESULT version=1 status=" + result.status()
+ " seed=" + result.configuration().seed() + " cane=" + evidence.validCaneRoots
+ " wetVoxels=" + evidence.plannedWetVoxels + " missingWet=" + evidence.missingWetVoxels
+ " mouths=" + evidence.connectedMouthComponents + " failures=" + result.failures().size()
+ " output=" + result.configuration().output().getAbsolutePath());
exitCode = result.failures().isEmpty() ? 0 : 1;
} catch (Throwable failure) {
failure.printStackTrace(System.out);
}
System.exit(exitCode);
}
static Result run(Configuration configuration) throws Exception {
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
configuration.pack(), configuration.dimension(), "[hydrology-blocks]");
RealPackProbeSupport.EngineSession session = workspace.openEngine(
configuration.seed(), configuration.studio(), "hydrology-blocks")) {
Engine engine = session.engine();
IrisComplex complex = engine.getComplex();
if (complex.getHydrologyRuntime() == null) {
throw new IllegalStateException("The dimension must enable hydrology.");
}
long chunks = ((long) configuration.maximumChunkX() - configuration.minimumChunkX() + 1)
* ((long) configuration.maximumChunkZ() - configuration.minimumChunkZ() + 1);
if (chunks * 256 * engine.getTarget().getHeight() > MAXIMUM_VOXELS) {
throw new IllegalArgumentException("Generated bounds exceed the 16777216 voxel limit.");
}
Map<Long, RealPackProbeSupport.GeneratedChunk> generated = new HashMap<>();
for (int z = configuration.minimumChunkZ(); z <= configuration.maximumChunkZ(); z++) {
for (int x = configuration.minimumChunkX(); x <= configuration.maximumChunkX(); x++) {
generated.put(RiverFootprint.pack(x, z), RealPackProbeSupport.generateChunk(engine, x, z));
}
}
Volume volume = new Volume(generated);
Map<Long, PlannedColumn> planned = new HashMap<>();
for (int z = volume.minimumZ; z <= volume.maximumZ; z++) {
for (int x = volume.minimumX; x <= volume.maximumX; x++) {
HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z);
int natural = sample == null ? (int) Math.round(complex.getNaturalHeightStream().getDouble(x, z))
: sample.naturalHeight();
HydrologyColumnLayer layer = surfaceForInspection(sample);
PlatformBlockState fluid = layer == null ? null : complex.resolveHydrologyFluid(
layer.profileKey(), x, z);
planned.put(RiverFootprint.pack(x, z), new PlannedColumn(natural, sample == null
? complex.getBridgeStream().get(x, z) == InferredType.SEA : sample.ocean(), layer, fluid));
}
}
int seaLevel = complex.getHydrologyRuntime().settings().seaLevel();
Evidence evidence = inspect(volume, planned, seaLevel);
int maximumCut = complex.getHydrologyRuntime().settings().surface().banks().erosion()
.excavation().maximumDepth();
List<String> failures = failures(evidence, maximumCut, configuration.requiredCoverage());
List<Throwable> reports = RealPackProbeSupport.drainReported();
RealPackProbeSupport.printReports("[hydrology-blocks]", "generation", reports);
if (!reports.isEmpty()) {
failures.add("The engine reported " + reports.size() + " generation errors.");
}
Files.createDirectories(configuration.output().toPath());
List<String> artifacts = writeImages(configuration.output(), volume, planned, seaLevel);
Result result = new Result(failures.isEmpty() ? "PASS" : "FAIL", configuration, engine.getMinHeight(),
seaLevel, evidence, List.copyOf(failures), artifacts);
Files.writeString(new File(configuration.output(), "hydrology-blocks.json").toPath(),
json(result), StandardCharsets.UTF_8);
return result;
}
}
static String json(Result result) {
Configuration configuration = result.configuration();
Map<String, Object> settings = Map.ofEntries(
Map.entry("pack", configuration.pack().getAbsolutePath()),
Map.entry("dimension", configuration.dimension()),
Map.entry("seed", configuration.seed()),
Map.entry("minimumChunkX", configuration.minimumChunkX()),
Map.entry("maximumChunkX", configuration.maximumChunkX()),
Map.entry("minimumChunkZ", configuration.minimumChunkZ()),
Map.entry("maximumChunkZ", configuration.maximumChunkZ()),
Map.entry("requiredCoverage", configuration.requiredCoverage()),
Map.entry("output", configuration.output().getAbsolutePath()),
Map.entry("studio", configuration.studio()));
Map<String, Object> report = Map.of("status", result.status(), "configuration", settings,
"worldMinimumY", result.worldMinimumY(), "seaLevel", result.seaLevel(),
"evidence", result.evidence(), "failures", result.failures(), "artifacts", result.artifacts(),
"coordinateConvention", "Block X/Z are world coordinates; evidence Y and seaLevel are relative to worldMinimumY.",
"imageLegend", "air=light blue,water=blue,lava=orange,cane=lime,vegetation=green,sand=tan,soil=brown,other=gray");
return new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
}
static HydrologyColumnLayer surfaceForInspection(HydrologyColumnSample sample) {
if (sample == null) {
return null;
}
HydrologyColumnLayer primary = sample.primarySurfaceLayerOrNull();
if (primary != null) {
return primary;
}
for (HydrologyColumnLayer layer : sample.layers()) {
if (layer.oceanApron() && layer.feature().type() == HydrologyFeatureType.MOUTH) {
return layer;
}
}
return null;
}
static Evidence inspect(Volume volume, Map<Long, PlannedColumn> planned, int seaLevel) {
Evidence evidence = new Evidence();
for (int z = volume.minimumZ; z <= volume.maximumZ; z++) {
for (int x = volume.minimumX; x <= volume.maximumX; x++) {
if (!volume.contains(x, z)) {
continue;
}
for (int y = 0; y < volume.height; y++) {
PlatformBlockState block = volume.block(x, y, z);
if (key(block).equals("minecraft:sugar_cane")
&& !key(volume.block(x, y - 1, z)).equals("minecraft:sugar_cane")) {
inspectCane(volume, block, x, y, z, evidence);
}
}
PlannedColumn column = planned.get(RiverFootprint.pack(x, z));
if (column == null || column.surface() == null) {
continue;
}
HydrologyColumnLayer surface = column.surface();
int cut = Math.max(0, column.naturalY() - surface.bedY());
if (surface.terrainOwned() && (column.ocean() || column.naturalY() < seaLevel)) {
evidence.oceanBedWrites++;
}
if (surface.terrainOwned() && column.naturalY() == seaLevel && cut > 0) {
evidence.drySillCuts++;
if (!surface.channel() || !surface.fluidOwned() || surface.fluidHeadY() != seaLevel) {
evidence.invalidDrySillCuts++;
}
}
if (surface.terrainOwned() && !surface.channel()) {
evidence.bankColumns++;
evidence.bankExcavation += cut;
evidence.maximumBankCut = Math.max(evidence.maximumBankCut, cut);
}
if (!surface.fluidOwned() || !surface.channel() || column.fluid() == null) {
continue;
}
for (int y = Math.max(0, surface.bedY() + 1); y <= surface.fluidHeadY() && y < volume.height; y++) {
evidence.plannedWetVoxels++;
PlatformBlockState actual = volume.block(x, y, z);
boolean matches = column.fluid().isWater() ? water(actual)
: key(column.fluid()).equals(key(actual));
if (!matches) {
evidence.missingWetVoxels++;
evidence.example("missing wet voxel " + x + "," + y + "," + z + "=" + key(actual));
}
}
}
}
inspectMouths(volume, planned, seaLevel, evidence);
return evidence;
}
private static void inspectCane(Volume volume, PlatformBlockState cane, int x, int y, int z, Evidence evidence) {
evidence.caneRoots++;
PlatformBlockState substrate = volume.block(x, y - 1, z);
if (substrate == null || !cane.canPlaceOnto(substrate)) {
evidence.invalidCaneSubstrates++;
evidence.example("unsupported cane " + x + "," + y + "," + z + " on " + key(substrate));
return;
}
boolean censored = false;
for (int[] offset : CARDINALS) {
int nx = x + offset[0];
int nz = z + offset[1];
if (!volume.contains(nx, nz)) {
censored = true;
continue;
}
PlatformBlockState adjacent = volume.block(nx, y - 1, nz);
if (water(adjacent) || key(adjacent).equals("minecraft:frosted_ice")) {
evidence.validCaneRoots++;
return;
}
}
if (censored) {
evidence.censoredCaneRoots++;
} else {
evidence.dryCaneRoots++;
evidence.example("dry cane root " + x + "," + y + "," + z);
}
}
private static void inspectMouths(Volume volume, Map<Long, PlannedColumn> planned, int seaLevel, Evidence evidence) {
Set<Long> visited = new HashSet<>();
TreeMap<Long, PlannedColumn> ordered = new TreeMap<>(planned);
for (Map.Entry<Long, PlannedColumn> entry : ordered.entrySet()) {
if (!entry.getValue().mouthAt(seaLevel) || !visited.add(entry.getKey())) {
continue;
}
int startX = (int) (entry.getKey() >> 32);
int startZ = (int) (long) entry.getKey();
if (!water(volume.block(startX, seaLevel, startZ))) {
evidence.disconnectedMouthComponents++;
evidence.example("dry mouth " + startX + "," + seaLevel + "," + startZ);
continue;
}
ArrayDeque<Long> queue = new ArrayDeque<>();
queue.add(entry.getKey());
long courseId = entry.getValue().surface().feature().courseId();
boolean ocean = false;
boolean channel = false;
boolean censored = false;
while (!queue.isEmpty()) {
long packed = queue.removeFirst();
int x = (int) (packed >> 32);
int z = (int) packed;
PlannedColumn column = planned.get(packed);
ocean |= column != null && column.ocean() && column.naturalY() < seaLevel
&& (column.surface() == null || !column.surface().terrainOwned());
channel |= column != null && column.surface() != null && column.surface().channel()
&& column.surface().fluidOwned() && !column.surface().oceanApron()
&& column.surface().fluidHeadY() == seaLevel
&& column.surface().feature().courseId() == courseId;
for (int[] offset : CARDINALS) {
int nx = x + offset[0];
int nz = z + offset[1];
if (!volume.contains(nx, nz)) {
censored = true;
continue;
}
long next = RiverFootprint.pack(nx, nz);
if (water(volume.block(nx, seaLevel, nz)) && visited.add(next)) {
queue.addLast(next);
}
}
}
if (ocean && channel) {
evidence.connectedMouthComponents++;
} else if (censored) {
evidence.censoredMouthComponents++;
} else {
evidence.disconnectedMouthComponents++;
evidence.example("mouth without connected course and ocean water "
+ startX + "," + seaLevel + "," + startZ);
}
}
}
static List<String> failures(Evidence evidence, int maximumBankCut, Set<String> required) {
List<String> failures = new ArrayList<>();
if (evidence.invalidCaneSubstrates > 0 || evidence.dryCaneRoots > 0) {
failures.add("Generated sugar cane has invalid substrate or no adjacent supporting water.");
}
if (evidence.missingWetVoxels > 0) {
failures.add("Generated blocks do not retain every planned surface channel fluid voxel.");
}
if (evidence.disconnectedMouthComponents > 0) {
failures.add("A fully sampled mouth water component does not connect to untouched natural ocean water.");
}
if (evidence.oceanBedWrites > 0 || evidence.invalidDrySillCuts > 0) {
failures.add("Hydrology owns ocean-bed terrain or an invalid dry shoreline sill cut.");
}
if (evidence.maximumBankCut > maximumBankCut) {
failures.add("Published dry-bank cut depth exceeds its configured budget.");
}
for (String coverage : required) {
boolean covered = switch (coverage) {
case "cane" -> evidence.validCaneRoots > 0;
case "mouth" -> evidence.connectedMouthComponents > 0;
case "channel" -> evidence.plannedWetVoxels > 0;
case "bank" -> evidence.bankColumns > 0;
default -> false;
};
if (!covered) {
failures.add("Required generated coverage is absent: " + coverage);
}
}
return failures;
}
private static List<String> writeImages(File output, Volume volume, Map<Long, PlannedColumn> planned, int seaLevel)
throws Exception {
int width = volume.maximumX - volume.minimumX + 1;
int depth = volume.maximumZ - volume.minimumZ + 1;
BufferedImage overhead = new BufferedImage(width, depth, BufferedImage.TYPE_INT_RGB);
int selectedX = volume.minimumX + width / 2;
int selectedZ = volume.minimumZ + depth / 2;
for (int z = volume.minimumZ; z <= volume.maximumZ; z++) {
for (int x = volume.minimumX; x <= volume.maximumX; x++) {
for (int y = volume.height - 1; y >= 0; y--) {
PlatformBlockState block = volume.block(x, y, z);
if (block != null && !block.isAir()) {
overhead.setRGB(x - volume.minimumX, z - volume.minimumZ, color(block));
break;
}
}
PlannedColumn column = planned.get(RiverFootprint.pack(x, z));
if (column != null && column.mouthAt(seaLevel)) {
selectedX = x;
selectedZ = z;
}
}
}
File plan = new File(output, "generated-overhead.png");
ImageIO.write(overhead, "png", plan);
List<String> artifacts = new ArrayList<>();
artifacts.add(plan.getAbsolutePath());
for (boolean alongX : new boolean[]{true, false}) {
int span = alongX ? width : depth;
BufferedImage section = new BufferedImage(span, volume.height, BufferedImage.TYPE_INT_RGB);
for (int position = 0; position < span; position++) {
int x = alongX ? volume.minimumX + position : selectedX;
int z = alongX ? selectedZ : volume.minimumZ + position;
for (int y = 0; y < volume.height; y++) {
section.setRGB(position, volume.height - y - 1, color(volume.block(x, y, z)));
}
}
File file = new File(output, alongX ? "generated-section-z-" + selectedZ + ".png"
: "generated-section-x-" + selectedX + ".png");
ImageIO.write(section, "png", file);
artifacts.add(file.getAbsolutePath());
}
return List.copyOf(artifacts);
}
private static int color(PlatformBlockState block) {
if (block == null || block.isAir()) {
return 0xC9E3F0;
}
if (water(block)) {
return 0x247AC4;
}
String key = key(block);
if (key.equals("minecraft:lava")) {
return 0xEC6F20;
}
if (key.equals("minecraft:sugar_cane")) {
return 0x9BDD42;
}
if (key.contains("grass") || key.contains("moss") || key.contains("leaves")) {
return 0x4E7845;
}
if (key.contains("sand")) {
return 0xDCCFA5;
}
if (key.contains("dirt") || key.contains("mud")) {
return 0x82634D;
}
return 0x888C90;
}
private static boolean water(PlatformBlockState block) {
return block != null && (block.isWater() || block.isWaterLogged());
}
private static String key(PlatformBlockState block) {
if (block == null) {
return "minecraft:air";
}
String key = block.key();
int bracket = key.indexOf('[');
return bracket < 0 ? key : key.substring(0, bracket);
}
}
@@ -35,6 +35,7 @@ import art.arcane.iris.engine.mantle.components.MantleHydrologyCaveVoxelView;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRiverBlendStyle;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.matter.MatterUpdate;
@@ -1682,7 +1683,8 @@ public final class HydrologyPackProbe {
uncontainedSurfaceBankEdges += bankEdges.total();
missingSurfaceBankEdges += bankEdges.missing();
lowSurfaceBankEdges += bankEdges.low();
BankContinuityMetrics bankContinuity = bankContinuityMetrics(tile.footprint());
BankContinuityMetrics bankContinuity = bankContinuityMetrics(tile.footprint(),
settings.surface().banks().erosion().style() == IrisRiverBlendStyle.SMOOTH);
bankStepViolations += bankContinuity.violations();
maximumBankStep = Math.max(maximumBankStep, bankContinuity.maximumStep());
observeSurfaceIncision(tile, settings, ownedCompleteSurfaceCourseIds);
@@ -1743,7 +1745,7 @@ public final class HydrologyPackProbe {
TreeMap<Long, long[]> totals = new TreeMap<>();
for (HydrologyColumnSample sample : footprint.columns().values()) {
HydrologyColumnLayer layer = sample.primarySurfaceLayer().orElse(null);
if (layer == null || !layer.channel()
if (layer == null || !layer.channel() || layer.oceanApron() || !layer.terrainOwned()
|| !scopedCourseIds.contains(layer.feature().courseId())) {
continue;
}
@@ -1805,7 +1807,7 @@ public final class HydrologyPackProbe {
}
String machineLine() {
return "IRIS_HYDROLOGY_PACK_SHAPE version=15 " + summary()
return "IRIS_HYDROLOGY_PACK_SHAPE version=16 " + summary()
+ " " + publishedMorphology.summary();
}
@@ -1902,7 +1904,7 @@ public final class HydrologyPackProbe {
bankStepViolations == 0,
String.format(
Locale.ROOT,
"bank_step_violations=%d maximum_bank_step=%d maximum_allowed_step=%d",
"bank_step_violations=%d maximum_bank_step=%d maximum_added_step=%d",
bankStepViolations,
maximumBankStep,
MAXIMUM_GRADED_STEP
@@ -2126,9 +2128,9 @@ public final class HydrologyPackProbe {
return Set.copyOf(ownedCourseIds);
}
private void observeOceanIntegrity(HydrologyColumnSample sample) {
void observeOceanIntegrity(HydrologyColumnSample sample) {
boolean ocean = sample.ocean();
boolean naturallySubmerged = sample.naturalHeight() <= sample.seaLevel();
boolean naturallySubmerged = sample.naturalHeight() < sample.seaLevel();
if (!ocean && !naturallySubmerged) {
if (carriesOceanApron(sample)) {
oceanApronLandColumns++;
@@ -2173,11 +2175,11 @@ public final class HydrologyPackProbe {
return false;
}
static int bankContinuityViolations(RiverFootprint footprint) {
return bankContinuityMetrics(footprint).violations();
static int bankContinuityViolations(RiverFootprint footprint, boolean smoothBanks) {
return bankContinuityMetrics(footprint, smoothBanks).violations();
}
private static BankContinuityMetrics bankContinuityMetrics(RiverFootprint footprint) {
private static BankContinuityMetrics bankContinuityMetrics(RiverFootprint footprint, boolean smoothBanks) {
int violations = 0;
int maximumStep = 0;
int[] offsets = {1, 0, 0, 1};
@@ -2195,9 +2197,10 @@ public final class HydrologyPackProbe {
continue;
}
int step = Math.abs(height - neighbor.terrainHeight());
if (step > MAXIMUM_GRADED_STEP) {
int naturalStep = Math.abs(sample.naturalHeight() - neighbor.naturalHeight());
maximumStep = Math.max(maximumStep, step);
if (smoothBanks && step - naturalStep > MAXIMUM_GRADED_STEP) {
violations++;
maximumStep = Math.max(maximumStep, step);
}
}
}
@@ -2244,12 +2247,12 @@ public final class HydrologyPackProbe {
}
if (neighbor != null
&& layer.fluidHeadY() <= neighbor.seaLevel()
&& (neighbor.ocean() || neighbor.naturalHeight() <= neighbor.seaLevel())) {
&& neighbor.ocean() && neighbor.naturalHeight() < neighbor.seaLevel()) {
continue;
}
if (neighbor == null) {
missing++;
} else if (neighbor.terrainHeight() <= layer.fluidHeadY()) {
} else if (neighbor.terrainHeight() < layer.fluidHeadY()) {
low++;
}
}
@@ -5,6 +5,7 @@ import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.HydrologyGeometrySampler;
import art.arcane.iris.engine.hydrology.HydrologyPlanner;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
@@ -31,6 +32,7 @@ public final class HydrologyPlannerProbe {
private static final long SURFACE_INLAND_SEED = 7012L;
private static final long UNDERGROUND_INLAND_SEED = 7013L;
private static final long COASTAL_SEED = 7014L;
private static final long WATERFALL_SEED = 7015L;
static final Set<HydrologyFeatureType> REQUIRED_FEATURE_TYPES =
// The synthetic probe settings define no standing pools, so that type is not expected either.
Set.copyOf(EnumSet.complementOf(EnumSet.of(HydrologyFeatureType.RIDGE_BORE, HydrologyFeatureType.STANDING_POOL)));
@@ -96,6 +98,7 @@ public final class HydrologyPlannerProbe {
collectAccepted(firstFeatures, surfaceInlandTile());
collectAccepted(firstFeatures, undergroundInlandTile());
collectAccepted(firstFeatures, surfaceMouthTile());
collectAccepted(firstFeatures, surfaceWaterfallTile());
collectAccepted(firstFeatures, coastalGrottoTile());
requireCompleteCoverage(firstFeatures.keySet());
@@ -141,6 +144,7 @@ public final class HydrologyPlannerProbe {
collectAccepted(coverage, surfaceInlandTile());
collectAccepted(coverage, undergroundInlandTile());
collectAccepted(coverage, surfaceMouthTile());
collectAccepted(coverage, surfaceWaterfallTile());
collectAccepted(coverage, coastalGrottoTile());
requireCompleteCoverage(coverage.keySet());
return Map.copyOf(coverage);
@@ -167,7 +171,7 @@ public final class HydrologyPlannerProbe {
return new HydrologyPlanner(
SURFACE_INLAND_SEED,
inlandSettings(0D, 0D),
inlandTerrain(true, false),
inlandTerrain(true, false, false),
solidCaveView()
).plan(ORIGIN);
}
@@ -176,12 +180,12 @@ public final class HydrologyPlannerProbe {
return new HydrologyPlanner(
UNDERGROUND_INLAND_SEED,
inlandSettings(0D, 1D),
inlandTerrain(false, true),
inlandTerrain(false, true, false),
solidCaveView()
).plan(ORIGIN);
}
private static HydrologyTile surfaceMouthTile() {
static HydrologyTile surfaceMouthTile() {
return new HydrologyPlanner(
COASTAL_SEED,
settings(false),
@@ -189,6 +193,16 @@ public final class HydrologyPlannerProbe {
).plan(ORIGIN);
}
static HydrologyTile surfaceWaterfallTile() {
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
case SURFACE_WIDTH -> Math.clamp(6, request.minimum(), request.maximum());
case SURFACE_DEPTH -> Math.clamp(3, request.minimum(), request.maximum());
default -> request.minimum();
};
return new HydrologyPlanner(WATERFALL_SEED, inlandSettings(0D, 0D), inlandTerrain(true, false, true),
geometry, -4096, surface -> solidCaveView()).plan(ORIGIN);
}
private static HydrologyTile coastalGrottoTile() {
return new HydrologyPlanner(
COASTAL_SEED,
@@ -232,7 +246,7 @@ public final class HydrologyPlannerProbe {
0.5D,
0.1D,
1D,
0
0, HydrologyPlannerSettings.Regional.disabled()
),
new HydrologyPlannerSettings.Surface(
true,
@@ -281,12 +295,13 @@ public final class HydrologyPlannerProbe {
private static HydrologyTerrainSampler inlandTerrain(
boolean surfaceSource,
boolean undergroundSource
boolean undergroundSource,
boolean waterfall
) {
return (int x, int z) -> {
boolean source = x == 0 && z == 0;
boolean outlet = x == 64 && z == 64;
int height = 120 - Math.floorDiv(x + z, 16);
int height = 120 - Math.floorDiv(x + z, 16) - (waterfall && x + z >= 64 ? 10 : 0);
return new HydrologyTerrainSample(
height,
1D,
@@ -330,15 +345,15 @@ public final class HydrologyPlannerProbe {
}
int height;
double slope;
if (x < 104) {
if (!cliffCoast) {
height = 100 - Math.floorDiv(x, 6);
slope = 1D;
} else if (x < 104) {
height = 100 - Math.floorDiv(x, 8);
slope = 1D;
} else if (cliffCoast) {
} else {
height = 84 - Math.floorDiv(x - 104, 26);
slope = 1D;
} else {
height = 79 - Math.floorDiv(x - 104, 12);
slope = x < 112 ? 4D : 1D;
}
boolean surfaceSource = x <= 40;
boolean undergroundSource = x <= 64;
@@ -458,7 +473,7 @@ public final class HydrologyPlannerProbe {
0.5D,
0.2D,
1D,
0
0, HydrologyPlannerSettings.Regional.disabled()
),
new HydrologyPlannerSettings.Surface(
true,
@@ -9,7 +9,11 @@ import art.arcane.iris.engine.hydrology.HydrologyCandidateKind;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticCandidate;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.surface.SurfaceExcavationMetrics;
import art.arcane.iris.engine.hydrology.HydrologyTile;
import art.arcane.iris.engine.hydrology.HydrologyTileKey;
import art.arcane.iris.engine.hydrology.RiverCourse;
@@ -25,6 +29,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
@@ -68,12 +73,24 @@ public final class RiverTransectProbe {
}
}
record ColumnView(int x, int z, int natural, int terrain, int water, Role role) {
record ColumnPlan(boolean ocean, boolean terrainOwned, boolean fluidOwned, boolean mouth, int bedY, long courseId) {
}
record ColumnView(int x, int z, int natural, int terrain, int water, Role role, ColumnPlan plan) {
int cut() {
return natural - terrain;
}
}
record SummaryOptions(long courseId, SurfaceCenterline centerline, int seaLevel,
HydrologyPlannerSettings.Excavation limits) {
SummaryOptions {
if (centerline == null || centerline.size() < 1 || limits == null) {
throw new IllegalArgumentException("A nonempty centerline and excavation limits are required.");
}
}
}
record CourseSummary(
long id,
int stations,
@@ -83,17 +100,31 @@ public final class RiverTransectProbe {
int maximumBankStep,
int oceanWrites,
int uncontainedWetCells,
int drySillCuts,
int invalidDrySillCuts,
int maximumBankCut,
long bankExcavation,
int estimatedBankVolumePerBlock,
int bankVolumeLimit,
int bankBudgetViolations,
int connectedMouthColumns,
int disconnectedMouthColumns,
List<String> details
) {
boolean passes() {
return oceanWrites == 0 && uncontainedWetCells == 0;
return oceanWrites == 0 && uncontainedWetCells == 0 && invalidDrySillCuts == 0
&& bankBudgetViolations == 0 && disconnectedMouthColumns == 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",
"%s course=%d %s stations=%d owned=%d cut=%d..%d bankStep=%d oceanWrites=%d uncontained=%d "
+ "drySillCuts=%d invalidSills=%d bankCut=%d bankVolume=%d estimatedBankVolumePerBlock=%d bankVolumeLimit=%d bankBudgetViolations=%d "
+ "connectedMouthColumns=%d disconnectedMouthColumns=%d",
PREFIX, id, passes() ? "PASS" : "FAIL", stations, ownedColumns, minimumCut, maximumCut,
maximumBankStep, oceanWrites, uncontainedWetCells);
maximumBankStep, oceanWrites, uncontainedWetCells, drySillCuts, invalidDrySillCuts,
maximumBankCut, bankExcavation, estimatedBankVolumePerBlock, bankVolumeLimit,
bankBudgetViolations, connectedMouthColumns, disconnectedMouthColumns);
}
}
@@ -209,7 +240,8 @@ public final class RiverTransectProbe {
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);
CourseSummary summary = summarize(new SummaryOptions(course.id(), centerline, seaLevel,
runtime.settings().surface().banks().erosion().excavation()), 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"),
@@ -222,7 +254,7 @@ public final class RiverTransectProbe {
}
}
writeSummary(new File(configuration.output(), "summary.txt"), configuration, tile, summaries);
boolean pass = summaries.stream().allMatch(CourseSummary::passes);
boolean pass = !summaries.isEmpty() && summaries.stream().allMatch(CourseSummary::passes);
System.out.println(PREFIX + " " + (pass ? "PASS" : "FAIL")
+ " surfaceCourses=" + summaries.size()
+ " output=" + configuration.output().getAbsolutePath());
@@ -262,34 +294,57 @@ public final class RiverTransectProbe {
return selected;
}
static CourseSummary summarize(long id, int stations, int seaLevel, Map<Long, ColumnView> columns) {
static CourseSummary summarize(SummaryOptions options, Map<Long, ColumnView> columns) {
int seaLevel = options.seaLevel();
int bankCutLimit = options.limits().maximumDepth();
int owned = 0;
int minimumCut = Integer.MAX_VALUE;
int maximumCut = Integer.MIN_VALUE;
int maximumBankStep = 0;
int oceanWrites = 0;
int uncontained = 0;
int drySills = 0;
int invalidSills = 0;
int maximumBankCut = 0;
long bankExcavation = 0L;
int bankBudgetViolations = 0;
int connectedMouthColumns = 0;
int disconnectedMouthColumns = 0;
HashSet<Long> oceanConnected = oceanConnected(columns, seaLevel);
ArrayList<String> details = new ArrayList<>();
String worstStep = null;
ArrayList<ColumnView> ordered = new ArrayList<>(columns.values());
ordered.sort(Comparator.comparingInt(ColumnView::z).thenComparingInt(ColumnView::x));
for (ColumnView column : ordered) {
boolean submerged = column.natural() <= seaLevel;
if (submerged && (column.terrain() != column.natural() || column.role().owned())) {
boolean submerged = column.natural() < seaLevel || column.plan().ocean();
if (submerged && (column.terrain() != column.natural() || column.plan().terrainOwned())) {
oceanWrites++;
if (details.size() < MAXIMUM_DETAILS) {
details.add("oceanWrite " + column.x() + "," + column.z()
+ " natural=" + column.natural() + " terrain=" + column.terrain() + " role=" + column.role());
}
}
if (!column.role().owned()) {
if (column.plan().mouth() && column.role() == Role.CHANNEL && column.water() == seaLevel) {
if (oceanConnected.contains(RiverFootprint.pack(column.x(), column.z()))) {
connectedMouthColumns++;
} else {
disconnectedMouthColumns++;
}
}
if (!column.plan().terrainOwned()) {
continue;
}
if (column.natural() == seaLevel && column.cut() > 0) {
drySills++;
if (column.role() != Role.CHANNEL || !column.plan().fluidOwned() || column.water() != seaLevel) {
invalidSills++;
}
}
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)) {
if (column.water() != NO_WATER && spills(column, columns, seaLevel)) {
uncontained++;
if (details.size() < MAXIMUM_DETAILS) {
details.add("uncontained " + column.x() + "," + column.z()
@@ -299,6 +354,10 @@ public final class RiverTransectProbe {
}
continue;
}
int bankCut = Math.max(0, column.natural() - column.plan().bedY());
maximumBankCut = Math.max(maximumBankCut, bankCut);
bankExcavation += bankCut;
bankBudgetViolations += bankCut > bankCutLimit ? 1 : 0;
for (int[] offset : new int[][] {{1, 0}, {0, 1}}) {
ColumnView neighbour = columns.get(RiverFootprint.pack(column.x() + offset[0], column.z() + offset[1]));
int step = bankStep(column, neighbour);
@@ -318,7 +377,70 @@ public final class RiverTransectProbe {
minimumCut = 0;
maximumCut = 0;
}
return new CourseSummary(id, stations, owned, minimumCut, maximumCut, maximumBankStep, oceanWrites, uncontained, List.copyOf(details));
return new CourseSummary(options.courseId(), options.centerline().size(), owned, minimumCut, maximumCut, maximumBankStep, oceanWrites,
uncontained, drySills, invalidSills, maximumBankCut, bankExcavation,
estimatedBankVolumePerBlock(options, columns), options.limits().maximumVolumePerBlock(), bankBudgetViolations,
connectedMouthColumns, disconnectedMouthColumns, List.copyOf(details));
}
static int estimatedBankVolumePerBlock(SummaryOptions options, Map<Long, ColumnView> columns) {
SurfaceCenterline centerline = options.centerline();
Map<Long, List<Integer>> buckets = new HashMap<>();
for (int station = 0; station < centerline.size(); station++) {
long bucket = RiverFootprint.pack(Math.floorDiv(centerline.x()[station], 16),
Math.floorDiv(centerline.z()[station], 16));
buckets.computeIfAbsent(bucket, ignored -> new ArrayList<>()).add(station);
}
int[] stationVolumes = new int[centerline.size()];
for (ColumnView column : columns.values()) {
if (!column.plan().terrainOwned() || column.role() == Role.CHANNEL
|| column.plan().courseId() != options.courseId()) {
continue;
}
int cut = Math.max(0, column.natural() - column.plan().bedY());
if (cut > 0) {
int station = nearestStation(centerline, buckets, column.x(), column.z());
stationVolumes[station] = Math.addExact(stationVolumes[station], cut);
}
}
return SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, stationVolumes);
}
private static int nearestStation(SurfaceCenterline centerline, Map<Long, List<Integer>> buckets, int x, int z) {
int bucketX = Math.floorDiv(x, 16);
int bucketZ = Math.floorDiv(z, 16);
int bestStation = -1;
double bestDistance = Double.POSITIVE_INFINITY;
for (int ring = 0; bestStation < 0 || nearestOutsideDistance(x, z, bucketX, bucketZ, ring - 1) <= bestDistance + 2D; ring++) {
for (int dz = -ring; dz <= ring; dz++) {
for (int dx = -ring; dx <= ring; dx++) {
if (ring > 0 && Math.abs(dx) != ring && Math.abs(dz) != ring) {
continue;
}
List<Integer> stations = buckets.get(RiverFootprint.pack(bucketX + dx, bucketZ + dz));
if (stations == null) {
continue;
}
for (int station : stations) {
double distance = centerline.distanceToSegment(station, x, z);
if (distance < bestDistance - 1.0e-7D
|| Math.abs(distance - bestDistance) <= 1.0e-7D && station > bestStation) {
bestDistance = distance;
bestStation = station;
}
}
}
}
}
return bestStation;
}
private static double nearestOutsideDistance(int x, int z, int bucketX, int bucketZ, int ring) {
double minX = ((double) bucketX - ring) * 16D;
double minZ = ((double) bucketZ - ring) * 16D;
double maxX = ((double) bucketX + ring + 1) * 16D;
double maxZ = ((double) bucketZ + ring + 1) * 16D;
return Math.min(Math.min(x - minX, maxX - x), Math.min(z - minZ, maxZ - z));
}
private static String neighbourHeights(ColumnView channel, Map<Long, ColumnView> columns) {
@@ -333,13 +455,16 @@ public final class RiverTransectProbe {
return text.toString();
}
private static boolean spills(ColumnView channel, Map<Long, ColumnView> columns) {
private static boolean spills(ColumnView channel, Map<Long, ColumnView> columns, int seaLevel) {
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.plan().ocean() && neighbour.natural() < seaLevel && channel.water() <= seaLevel) {
continue;
}
if (neighbour.terrain() < channel.water()) {
return true;
}
@@ -347,6 +472,29 @@ public final class RiverTransectProbe {
return false;
}
private static HashSet<Long> oceanConnected(Map<Long, ColumnView> columns, int seaLevel) {
HashSet<Long> visited = new HashSet<>();
ArrayDeque<Long> queue = new ArrayDeque<>();
for (Map.Entry<Long, ColumnView> entry : columns.entrySet()) {
ColumnView column = entry.getValue();
if (column.plan().ocean() && column.natural() < seaLevel && !column.plan().terrainOwned()) {
visited.add(entry.getKey());
queue.add(entry.getKey());
}
}
while (!queue.isEmpty()) {
ColumnView current = columns.get(queue.removeFirst());
for (int[] offset : new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}) {
long key = RiverFootprint.pack(current.x() + offset[0], current.z() + offset[1]);
ColumnView next = columns.get(key);
if (next != null && next.water() == seaLevel && next.terrain() < seaLevel && visited.add(key)) {
queue.addLast(key);
}
}
}
return visited;
}
private static int bankStep(ColumnView bank, ColumnView neighbour) {
if (neighbour == null || neighbour.role() == Role.CHANNEL) {
return 0;
@@ -395,7 +543,13 @@ public final class RiverTransectProbe {
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));
boolean ocean = sample.map(HydrologyColumnSample::ocean)
.orElseGet(() -> complex.getBridgeStream().get(x, z) == InferredType.SEA);
ColumnPlan plan = new ColumnPlan(ocean, surface != null && surface.terrainOwned(),
surface != null && surface.fluidOwned(),
surface != null && surface.feature().type() == HydrologyFeatureType.MOUTH,
surface == null ? natural : surface.bedY(), surface == null ? 0L : surface.feature().courseId());
return new ColumnView(x, z, natural, terrain, water, role(surface), plan);
}
private static Role role(HydrologyColumnLayer layer) {
@@ -226,7 +226,19 @@ public final class StubPlatform implements IrisPlatform {
@Override
public boolean canPlaceOnto(PlatformBlockState onto) {
return true;
if (!blockKey.equals("minecraft:sugar_cane")) {
return true;
}
if (onto == null) {
return false;
}
return switch (ParsedState.parse(onto.key()).blockKey()) {
case "minecraft:sugar_cane", "minecraft:grass_block", "minecraft:dirt", "minecraft:coarse_dirt",
"minecraft:podzol", "minecraft:mycelium", "minecraft:rooted_dirt", "minecraft:moss_block",
"minecraft:pale_moss_block", "minecraft:mud", "minecraft:muddy_mangrove_roots",
"minecraft:sand", "minecraft:red_sand", "minecraft:suspicious_sand" -> true;
default -> false;
};
}
@Override
@@ -0,0 +1,235 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.util.project.hunk.Hunk;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.File;
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.assertThrows;
import static org.junit.Assert.assertTrue;
public final class HydrologyBlockProbeTest {
@ClassRule
public static final PlatformLeakGuard LEAK_GUARD = PlatformLeakGuard.clean();
@BeforeClass
public static void bindPlatform() {
IrisPlatforms.bind(new StubPlatform());
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void generatedCaneChecksRootSubstrateAndActualWaterAcrossNegativeChunkBoundary() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(-1, 0);
set(chunks, -1, 4, 8, "sugar_cane[age=0]");
set(chunks, -1, 5, 8, "sugar_cane[age=0]");
set(chunks, -1, 3, 8, "sand");
set(chunks, 0, 3, 8, "water[level=0]");
HydrologyBlockProbe.Evidence valid = inspect(chunks, Map.of());
assertEquals(1, valid.caneRoots);
assertEquals(1, valid.validCaneRoots);
assertEquals(0, valid.censoredCaneRoots);
set(chunks, -1, 3, 8, "stone");
HydrologyBlockProbe.Evidence stone = inspect(chunks, Map.of());
assertEquals(1, stone.invalidCaneSubstrates);
assertFalse(HydrologyBlockProbe.failures(stone, 8, Set.of()).isEmpty());
set(chunks, -1, 3, 8, "sand");
set(chunks, 0, 3, 8, "ice");
assertEquals(1, inspect(chunks, Map.of()).dryCaneRoots);
set(chunks, 0, 3, 8, "frosted_ice[age=0]");
assertEquals(1, inspect(chunks, Map.of()).validCaneRoots);
}
@Test
public void outerCaneBoundaryIsCensoredWithoutPretendingItPassed() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
set(chunks, 0, 4, 8, "sugar_cane");
set(chunks, 0, 3, 8, "dirt");
HydrologyBlockProbe.Evidence evidence = inspect(chunks, Map.of());
assertEquals(1, evidence.censoredCaneRoots);
assertEquals(0, evidence.dryCaneRoots);
assertFalse(HydrologyBlockProbe.failures(evidence, 8, Set.of("cane")).isEmpty());
}
@Test
public void retainedWaterloggedVoxelsCountButSolidAndDryChannelVoxelsFail() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
HydrologyColumnLayer layer = layer(HydrologyFeatureType.RIFFLE, 2, 4, true);
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = Map.of(
RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(8, false, layer, state("water")));
set(chunks, 8, 3, 8, "stone");
set(chunks, 8, 4, 8, "brain_coral[waterlogged=true]");
HydrologyBlockProbe.Evidence evidence = inspect(chunks, planned);
assertEquals(2, evidence.plannedWetVoxels);
assertEquals(1, evidence.missingWetVoxels);
set(chunks, 8, 3, 8, "water[level=0]");
assertEquals(0, inspect(chunks, planned).missingWetVoxels);
}
@Test
public void mouthRequiresActualCardinalWaterToAnOceanColumn() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
HydrologyColumnLayer mouth = layer(HydrologyFeatureType.MOUTH, 5, 6, true);
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = new HashMap<>();
planned.put(RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(6, false, mouth, state("water")));
planned.put(RiverFootprint.pack(9, 8), new HydrologyBlockProbe.PlannedColumn(5, true, null, null));
set(chunks, 8, 6, 8, "water");
set(chunks, 9, 6, 8, "water");
HydrologyBlockProbe.Evidence connected = inspect(chunks, planned);
assertEquals(1, connected.connectedMouthComponents);
assertEquals(1, connected.drySillCuts);
assertEquals(0, connected.oceanBedWrites);
assertTrue(HydrologyBlockProbe.failures(connected, 8, Set.of("mouth", "channel")).isEmpty());
planned.put(RiverFootprint.pack(9, 8), new HydrologyBlockProbe.PlannedColumn(5, false, null, null));
assertEquals(1, inspect(chunks, planned).disconnectedMouthComponents);
planned.put(RiverFootprint.pack(9, 8), new HydrologyBlockProbe.PlannedColumn(5, true, null, null));
set(chunks, 9, 6, 8, "stone");
assertEquals(1, inspect(chunks, planned).disconnectedMouthComponents);
}
@Test
public void oceanApronRequiresConnectedOwnedWaterWhenAPoolOwnsTheShoreline() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
HydrologyColumnLayer pool = layer(HydrologyFeatureType.SURFACE_POOL, 5, 6, true);
HydrologyFeatureRef mouth = new HydrologyFeatureRef(4L, HydrologyFeatureType.MOUTH,
2L, 5L, 9, 6, 8, 1, 0, false);
HydrologyColumnLayer apron = new HydrologyColumnLayer(mouth, 6, 6, 6,
true, false, false, true, false, false, false, false, true,
"water", "surface", "mouth", "shore", "bank", "cave");
HydrologyColumnSample ocean = new HydrologyColumnSample(9, 8, 5, 6, true, "ocean", List.of(apron));
assertTrue(ocean.primarySurfaceLayer().isEmpty());
assertEquals(apron, HydrologyBlockProbe.surfaceForInspection(ocean));
assertEquals(pool, HydrologyBlockProbe.surfaceForInspection(
new HydrologyColumnSample(8, 8, 6, 6, false, "shore", List.of(pool, apron))));
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = new HashMap<>();
planned.put(RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(6, false, pool, state("water")));
planned.put(RiverFootprint.pack(9, 8), new HydrologyBlockProbe.PlannedColumn(5, true, apron, state("water")));
set(chunks, 8, 6, 8, "water");
set(chunks, 9, 6, 8, "water");
assertEquals(1, inspect(chunks, planned).connectedMouthComponents);
set(chunks, 8, 6, 8, "stone");
HydrologyBlockProbe.Evidence disconnected = inspect(chunks, planned);
assertEquals(0, disconnected.connectedMouthComponents);
assertEquals(1, disconnected.disconnectedMouthComponents);
HydrologyFeatureRef other = new HydrologyFeatureRef(6L, HydrologyFeatureType.SURFACE_POOL,
99L, 7L, 8, 6, 8, 1, 0, false);
HydrologyColumnLayer unrelated = new HydrologyColumnLayer(other, 5, 6, 6,
true, false, false, true, false, false, true, true, false,
"water", "surface", "mouth", "shore", "bank", "cave");
planned.put(RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(6, false, unrelated, state("water")));
set(chunks, 8, 6, 8, "water");
assertEquals(0, inspect(chunks, planned).connectedMouthComponents);
}
@Test
public void diagonalOceanWaterAndOutOfBoundsWaterDoNotProveMouthCoverage() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
HydrologyColumnLayer mouth = layer(HydrologyFeatureType.MOUTH, 5, 6, true);
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = new HashMap<>();
planned.put(RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(6, false, mouth, state("water")));
planned.put(RiverFootprint.pack(9, 9), new HydrologyBlockProbe.PlannedColumn(5, true, null, null));
set(chunks, 8, 6, 8, "water");
set(chunks, 9, 6, 9, "water");
assertEquals(1, inspect(chunks, planned).disconnectedMouthComponents);
planned.clear();
planned.put(RiverFootprint.pack(0, 8), new HydrologyBlockProbe.PlannedColumn(6, false, mouth, state("water")));
set(chunks, 0, 6, 8, "water");
HydrologyBlockProbe.Evidence censored = inspect(chunks, planned);
assertEquals(1, censored.censoredMouthComponents);
assertFalse(HydrologyBlockProbe.failures(censored, 8, Set.of("mouth")).isEmpty());
}
@Test
public void bankBudgetsAndOceanBedOwnershipRemainIndependentGates() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = Map.of(
RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(
12, false, layer(HydrologyFeatureType.RIFFLE, 3, 3, false), null),
RiverFootprint.pack(9, 8), new HydrologyBlockProbe.PlannedColumn(
5, true, layer(HydrologyFeatureType.MOUTH, 3, 6, true), state("water")));
HydrologyBlockProbe.Evidence evidence = inspect(chunks, planned);
assertEquals(9, evidence.maximumBankCut);
assertEquals(9, evidence.bankExcavation);
assertEquals(1, evidence.oceanBedWrites);
assertTrue(HydrologyBlockProbe.failures(evidence, 8, Set.of()).stream()
.anyMatch(value -> value.contains("budget")));
}
@Test
public void configurationBoundsAndRequiredCoveragePreventUnboundedOrVacuousProof() {
assertThrows(IllegalArgumentException.class, () -> new HydrologyBlockProbe.Configuration(
new File("/pack"), "overworld", 1L, 0, 8, 0, 8, Set.of("mouth"), new File("/output"), false));
assertThrows(IllegalArgumentException.class, () -> new HydrologyBlockProbe.Configuration(
new File("/pack"), "overworld", 1L, 0, 0, 0, 0, Set.of("river"), new File("/output"), false));
assertEquals(4, HydrologyBlockProbe.failures(new HydrologyBlockProbe.Evidence(), 8,
Set.of("mouth", "cane", "channel", "bank")).size());
}
@Test
public void reportSerializesAbsolutePathsWithoutReflectingIntoFile() {
HydrologyBlockProbe.Configuration configuration = new HydrologyBlockProbe.Configuration(
new File("/pack"), "overworld", 1L, 0, 0, 0, 0, Set.of(), new File("/output"), false);
String json = HydrologyBlockProbe.json(new HydrologyBlockProbe.Result("PASS", configuration,
-64, 127, new HydrologyBlockProbe.Evidence(), List.of(), List.of()));
assertTrue(json.contains("\"pack\": \"/pack\""));
assertTrue(json.contains("worldMinimumY"));
}
private static HydrologyBlockProbe.Evidence inspect(Map<Long, RealPackProbeSupport.GeneratedChunk> chunks,
Map<Long, HydrologyBlockProbe.PlannedColumn> planned) {
return HydrologyBlockProbe.inspect(new HydrologyBlockProbe.Volume(chunks), planned, 6);
}
private static Map<Long, RealPackProbeSupport.GeneratedChunk> chunks(int minimumX, int maximumX) {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = new HashMap<>();
for (int x = minimumX; x <= maximumX; x++) {
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(16, 16, 16);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, 16, 16);
chunks.put(RiverFootprint.pack(x, 0), new RealPackProbeSupport.GeneratedChunk(x, 0, 16, blocks, biomes));
}
return chunks;
}
private static void set(Map<Long, RealPackProbeSupport.GeneratedChunk> chunks, int x, int y, int z, String key) {
chunks.get(RiverFootprint.pack(Math.floorDiv(x, 16), Math.floorDiv(z, 16)))
.blocks().set(Math.floorMod(x, 16), y, Math.floorMod(z, 16), state(key));
}
private static PlatformBlockState state(String key) {
return IrisPlatforms.get().registries().block("minecraft:" + key);
}
private static HydrologyColumnLayer layer(HydrologyFeatureType type, int bed, int head, boolean channel) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(1L, type, 2L, 3L, 8, head, 8, 1, 0, false);
return new HydrologyColumnLayer(feature, bed, head, head, channel, false, !channel, channel,
false, false, true, channel, false, "water", "surface", "mouth", "shore", "bank", "cave");
}
}
@@ -548,6 +548,15 @@ public final class HydrologyPackProbeTest {
0, 0, 60, 63, false, "parent", List.of(gradedLayer(55))));
}
@Test
public void drySillsAreNotReportedAsOceanBedWrites() {
HydrologyPackProbe.ShapeMetrics metrics = new HydrologyPackProbe.ShapeMetrics();
HydrologyColumnSample sill = new HydrologyColumnSample(
0, 0, 63, 63, false, "parent", channelColumn(0, 0, 60, 63).layers());
metrics.observeOceanIntegrity(sill);
assertTrue(gate(metrics.gates(List.of()), "ocean_integrity").passed());
}
@Test
public void landColumnsCarryingAnOceanApronAreDetected() {
HydrologyColumnSample land = new HydrologyColumnSample(
@@ -560,9 +569,23 @@ public final class HydrologyPackProbeTest {
@Test
public void bankContinuityAllowsSingleBlockStepsAndRejectsLedges() {
assertEquals(0, HydrologyPackProbe.ShapeMetrics.bankContinuityViolations(
gradedRow(70, 71, 72)));
gradedRow(70, 71, 72), true));
assertEquals(1, HydrologyPackProbe.ShapeMetrics.bankContinuityViolations(
gradedRow(70, 71, 74)));
gradedRow(70, 71, 74), true));
assertEquals(0, HydrologyPackProbe.ShapeMetrics.bankContinuityViolations(
gradedRow(70, 71, 74), false));
}
@Test
public void bankContinuityPreservesNaturalSteepStepsAndContainmentAllowsFlushBanks() {
HydrologyColumnSample low = new HydrologyColumnSample(
0, 0, 80, 63, false, "parent", List.of(gradedLayer(74)));
HydrologyColumnSample high = new HydrologyColumnSample(
1, 0, 95, 63, false, "parent", List.of(gradedLayer(87)));
assertEquals(0, HydrologyPackProbe.ShapeMetrics.bankContinuityViolations(new RiverFootprint(Map.of(
RiverFootprint.pack(0, 0), low, RiverFootprint.pack(1, 0), high)), true));
assertEquals(0, HydrologyPackProbe.ShapeMetrics.uncontainedSurfaceBankEdges(
surfaceBankFootprint(70), Set.of(32L)));
}
@Test
@@ -576,7 +599,7 @@ public final class HydrologyPackProbeTest {
RiverFootprint.pack(west.x(), west.z()), west,
RiverFootprint.pack(channel.x(), channel.z()), channel,
RiverFootprint.pack(east.x(), east.z()), east
))));
)), true));
}
@Test
@@ -585,7 +608,7 @@ public final class HydrologyPackProbeTest {
String machineLine = metrics.machineLine();
assertTrue(machineLine.startsWith("IRIS_HYDROLOGY_PACK_SHAPE version=15 "));
assertTrue(machineLine.startsWith("IRIS_HYDROLOGY_PACK_SHAPE version=16 "));
assertTrue(machineLine.contains("shallow_incision_courses=0"));
assertTrue(machineLine.contains("ocean_apron_land_columns=0"));
assertTrue(machineLine.contains("bank_step_violations=0"));
@@ -712,7 +735,7 @@ public final class HydrologyPackProbeTest {
}
@Test
public void surfaceBankIntegrityRequiresFreeboardAroundOwnedFluid() {
public void surfaceBankIntegrityRequiresContainmentAroundOwnedFluid() {
assertEquals(
0,
HydrologyPackProbe.ShapeMetrics.uncontainedSurfaceBankEdges(
@@ -723,7 +746,7 @@ public final class HydrologyPackProbeTest {
assertEquals(
1,
HydrologyPackProbe.ShapeMetrics.uncontainedSurfaceBankEdges(
surfaceBankFootprint(70),
surfaceBankFootprint(69),
Set.of(32L)
)
);
@@ -738,6 +761,25 @@ public final class HydrologyPackProbeTest {
);
}
@Test
public void belowSeaLandIsNotReceivingOceanWhileSeaLevelLandIsAFlushBank() {
for (boolean ocean : new boolean[]{true, false}) {
for (int height : new int[]{62, 63}) {
HashMap<Long, HydrologyColumnSample> columns = new HashMap<>();
HydrologyColumnSample wet = channelColumn(0, 0, 60, 63);
columns.put(RiverFootprint.pack(0, 0), wet);
columns.put(RiverFootprint.pack(-1, 0), bankColumn(-1, 0, 64));
columns.put(RiverFootprint.pack(0, -1), bankColumn(0, -1, 64));
columns.put(RiverFootprint.pack(0, 1), bankColumn(0, 1, 64));
columns.put(RiverFootprint.pack(1, 0), new HydrologyColumnSample(
1, 0, height, 63, ocean, "parent", List.of()));
assertEquals(ocean || height == 63 ? 0 : 1,
HydrologyPackProbe.ShapeMetrics.uncontainedSurfaceBankEdges(
new RiverFootprint(columns), Set.of(wet.layers().getFirst().feature().courseId())));
}
}
}
@Test
public void seedsWithoutSurfaceCoursesDoNotMaskPublishedFailures() {
HydrologyPackProbe.CourseMorphologyResult acceptedCourse =
@@ -2,6 +2,7 @@ package art.arcane.iris.probe;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.HydrologyTile;
import org.junit.Test;
import java.util.EnumSet;
@@ -9,8 +10,26 @@ import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class HydrologyPlannerProbeTest {
@Test
public void coastalFixturePublishesAnAcceptedOceanMouth() {
HydrologyTile tile = HydrologyPlannerProbe.surfaceMouthTile();
assertTrue(tile.diagnosticCandidates().stream().limit(16).toList().toString(),
tile.features().stream().anyMatch(feature -> feature.type() == HydrologyFeatureType.MOUTH));
}
@Test
public void inlandFixturePublishesAContainedWaterfall() {
HydrologyTile tile = HydrologyPlannerProbe.surfaceWaterfallTile();
assertTrue(tile.diagnosticCandidates().stream().limit(16).toList().toString(),
tile.features().stream().anyMatch(feature -> feature.type() == HydrologyFeatureType.WATERFALL));
assertTrue(tile.courses().stream().anyMatch(course -> course.segments().stream().anyMatch(segment -> segment.fallingFluid()
&& segment.type() == HydrologyFeatureType.WATERFALL)
&& tile.cavePlans().stream().anyMatch(plan -> plan.source().sourceId() == course.id())));
}
@Test
public void deterministicAcceptedCoveragePublishesEveryReachableFeatureType() {
Map<HydrologyFeatureType, HydrologyFeatureRef> coverage =
@@ -1,10 +1,14 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.surface.SurfaceCenterline;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -31,7 +35,7 @@ public class RiverTransectProbeTest {
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);
RiverTransectProbe.CourseSummary summary = RiverTransectProbe.summarize(options(7L, 12), columns);
assertEquals(7L, summary.id());
assertEquals(12, summary.stations());
@@ -57,13 +61,59 @@ public class RiverTransectProbeTest {
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);
RiverTransectProbe.CourseSummary summary = RiverTransectProbe.summarize(options(1L, 3), columns);
assertTrue(summary.passes());
assertEquals(0, summary.oceanWrites());
assertEquals(0, summary.uncontainedWetCells());
}
@Test
public void drySillMouthCutsConnectToUntouchedOceanWithoutCountingAsOceanBedWrites() {
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
columns.put(RiverFootprint.pack(0, 0), new RiverTransectProbe.ColumnView(
0, 0, 63, 60, 63, RiverTransectProbe.Role.CHANNEL,
new RiverTransectProbe.ColumnPlan(false, true, true, true, 60, 1L)));
put(columns, 1, 0, 60, 60, 63, RiverTransectProbe.Role.APRON);
RiverTransectProbe.CourseSummary connected = RiverTransectProbe.summarize(options(1L, 2), columns);
assertEquals(1, connected.drySillCuts());
assertEquals(0, connected.oceanWrites());
assertEquals(1, connected.connectedMouthColumns());
assertTrue(connected.passes());
columns.remove(RiverFootprint.pack(1, 0));
assertEquals(1, RiverTransectProbe.summarize(options(1L, 2), columns).disconnectedMouthColumns());
}
@Test
public void bankCutBudgetUsesPublishedLowerFloorEvenWhenUpperTerrainSpanIsRetained() {
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
columns.put(RiverFootprint.pack(0, 0), new RiverTransectProbe.ColumnView(
0, 0, 80, 80, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK,
new RiverTransectProbe.ColumnPlan(false, true, false, false, 70, 1L)));
RiverTransectProbe.CourseSummary summary = RiverTransectProbe.summarize(options(1L, 2), columns);
assertEquals(10, summary.maximumBankCut());
assertEquals(10, summary.bankExcavation());
assertEquals(1, summary.bankBudgetViolations());
assertFalse(summary.passes());
}
@Test
public void reconstructedReachVolumesUseTheSharedSixteenBlockMetric() {
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
for (int x = 0; x <= 32; x++) {
put(columns, x, 2, 80, 72, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
}
assertEquals(9, RiverTransectProbe.estimatedBankVolumePerBlock(options(1L, 33), columns));
assertEquals(0, RiverTransectProbe.estimatedBankVolumePerBlock(options(2L, 33), columns));
}
private static RiverTransectProbe.SummaryOptions options(long id, int stations) {
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
new HydrologyPoint(0, 70, 0), new HydrologyPoint(stations - 1, 70, 0)));
return new RiverTransectProbe.SummaryOptions(id, centerline, 63,
new HydrologyPlannerSettings.Excavation(8, 16, 128));
}
private static void put(
Map<Long, RiverTransectProbe.ColumnView> columns,
int x,
@@ -73,6 +123,8 @@ public class RiverTransectProbeTest {
int water,
RiverTransectProbe.Role role
) {
columns.put(RiverFootprint.pack(x, z), new RiverTransectProbe.ColumnView(x, z, natural, terrain, water, role));
columns.put(RiverFootprint.pack(x, z), new RiverTransectProbe.ColumnView(x, z, natural, terrain, water, role,
new RiverTransectProbe.ColumnPlan(role == RiverTransectProbe.Role.APRON,
role.owned(), role == RiverTransectProbe.Role.CHANNEL, false, terrain, 1L)));
}
}
@@ -104,6 +104,19 @@ public final class StubPlatformStateTest {
assertFalse(IrisSpeleothems.isSupported(spike, blocks, 0, 0, 1));
}
@Test
public void sugarCaneUsesNativeSubstratesInGeneratedChunkProbes() {
PlatformBlockState cane = state("minecraft:sugar_cane[age=0]");
for (String substrate : new String[]{"sugar_cane", "grass_block", "dirt", "coarse_dirt", "podzol",
"mycelium", "rooted_dirt", "moss_block", "pale_moss_block", "mud", "muddy_mangrove_roots",
"sand", "red_sand", "suspicious_sand"}) {
assertTrue(substrate, cane.canPlaceOnto(state("minecraft:" + substrate)));
}
for (String substrate : new String[]{"stone", "gravel", "clay", "farmland", "dirt_path", "air", "water"}) {
assertFalse(substrate, cane.canPlaceOnto(state("minecraft:" + substrate)));
}
}
private PlatformBlockState state(String key) {
return IrisPlatforms.get().registries().block(key);
}