mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Hydrology fixes
This commit is contained in:
1 parent
577f924133
commit
bf12f30d26
168 files changed
+13544
-582
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user