mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Basin hydrology
- The route-and-reject hydrology system and its probes are removed - Hydrology is configured under dimension hydrology and per biome, region and dimension hydrologyPolicy - riverPolicy and the old hydrology config types are gone - Grid kernels cover priority flood, D8 routing, flow accumulation, basin extraction, breaching and stream burning - A basin spills at its pour point and an interior bump can never be the spill - Region plans cover 4096-block regions on a 64-block grid for major rivers and lakes - Window plans cover 512-block windows on an 8-block grid for reaches and ponds - Plans are cached in memory, persisted per world under iris/hydrology and keyed by pack, seed, world height and settings - Only the main runtime of an engine persists plans and stores swap on hotload and default runtime change - Demand rasters outrank prefetch and stopping pregen withdraws its queued prefetch - Ponds come from natural basins, seeded cups and chained cups with strictly falling levels - A pond level never exceeds its rim plus maxFill or the end grade of any inflow - Pond outlet loops are opened at their lowest pond - Reaches split at junctions, pond outlets and 256-block grid lines - Reaches meander, fit the valley, grade to a running minimum and smooth with Chaikin passes - Drops of two blocks or more get a lip and one-block corner steps become dry rim ledges - Cascades and waterfalls write falling fluid from the mantle at level eight - Inflows into region lakes end at the lake level - River mouths and water beside the sea sit at the sea surface - The per-chunk raster gives terrain height, water head, role, tier, fluid, cave ceiling and shore distance - WaterRole codes are stable and exposed to packs through the WATER_ROLE engine stream - WATER_HEAD replaces the old river engine streams and an unknown stream name fails the pack load - Banks take SMOOTH, LINEAR, CONCAVE, TERRACED or CLIFF profiles - Containment berms grow as tall as the water needs - Columns within two blocks of water stay solid and keep no Terrain3D overhang - Seam dams hold water where new chunks meet chunks generated earlier - The transition blend skips hydrology-owned columns - Caves stay at least two blocks below river and pond beds - The painter is the only writer of still water - Objects cannot open water, falls or bank ground and solid objects may stand in water - On-water objects need fluid at their anchor column - Floating child islands stay out of channels - Sea inlets run inland from cliff coasts at sea level and end in a chamber pool - Inlet passages are sealed against carvers, subterrain features and the post pass - Cave pools settle into carved lows, keep one fluid and stay out of inlet space - Pool and inlet cells use their own cave actions in mantle data - Water vegetation lists sit on tiers, ponds and hydrologyPolicy with SEA_FLOOR, SEA_SURFACE and SHORE_LINE parts - Pack validation compiles hydrology settings and rejects empty or water-holding palettes - Chunk generation fails closed when its 3x3 water rasters are not planned - /iris find covers stream, river, major river, cascade, waterfall, mouth, pond, lake, dam, sea inlet and cave pool - Generation history records the new feature types and drops old hydrology entries - The studio map draws water from rasters up close and from plans when zoomed out - Pregen prepares the current region and one region ahead - hydrologyProbe, riverTransectProbe, hydrologyBlockProbe, seaInletProbe and cavePoolProbe report containment, rises, seams, tick conversion and missing surface - Test forks run with a 2 GB heap, a 60-minute timeout and the Mockito agent loaded explicitly - Generation with hydrology off is unchanged
This commit is contained in:
1 parent
2d211cf106
commit
db0f98d103
710 files changed
+41791
-91883
No files matched your search
+105
-76
@@ -17,7 +17,7 @@ dependencies {
|
||||
implementation(project(':core'))
|
||||
compileOnly(libs.spigot)
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(libs.gson)
|
||||
testImplementation(testFixtures(project(':core')))
|
||||
testImplementation('com.github.VolmitSoftware.VolmLib:volmlib-shared:master-SNAPSHOT') {
|
||||
@@ -55,15 +55,6 @@ tasks.named('test').configure {
|
||||
dependsOn(':core:compileJava')
|
||||
}
|
||||
|
||||
tasks.register('hydrologyProbe', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Scans deterministic hydrology seeds and reports accepted feature coordinates plus cold and warm planner timings.'
|
||||
mainClass = 'art.arcane.iris.probe.HydrologyPlannerProbe'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector')
|
||||
dependsOn(':core:compileJava')
|
||||
}
|
||||
|
||||
Provider<String> probePack = providers.gradleProperty('probePack')
|
||||
Provider<String> probeDimension = providers.gradleProperty('probeDimension')
|
||||
Provider<String> probeWarmupChunks = providers.gradleProperty('probeWarmupChunks').orElse('256')
|
||||
@@ -74,11 +65,6 @@ Provider<String> probeMulticore = providers.gradleProperty('probeMulticore').orE
|
||||
Provider<String> probeStudio = providers.gradleProperty('probeStudio').orElse('false')
|
||||
Provider<String> explicitProbeMulticore = providers.gradleProperty('probeMulticore')
|
||||
Provider<String> explicitProbeStudio = providers.gradleProperty('probeStudio')
|
||||
Provider<String> probeSeeds = providers.gradleProperty('probeSeeds')
|
||||
Provider<String> probeMinimumTileX = providers.gradleProperty('probeMinimumTileX')
|
||||
Provider<String> probeMaximumTileX = providers.gradleProperty('probeMaximumTileX')
|
||||
Provider<String> probeMinimumTileZ = providers.gradleProperty('probeMinimumTileZ')
|
||||
Provider<String> probeMaximumTileZ = providers.gradleProperty('probeMaximumTileZ')
|
||||
Provider<String> probeRequiredCoverage = providers.gradleProperty('probeRequiredCoverage')
|
||||
Provider<String> probeSeed = providers.gradleProperty('probeSeed')
|
||||
Provider<String> probeMinimumChunkX = providers.gradleProperty('probeMinimumChunkX')
|
||||
@@ -88,17 +74,71 @@ Provider<String> probeMaximumChunkZ = providers.gradleProperty('probeMaximumChun
|
||||
Provider<String> probeParallelism = providers.gradleProperty('probeParallelism')
|
||||
Provider<String> probeShuffleSeed = providers.gradleProperty('probeShuffleSeed')
|
||||
Provider<String> probeJfr = providers.gradleProperty('probeJfr')
|
||||
Provider<String> probeTileX = providers.gradleProperty('probeTileX')
|
||||
Provider<String> probeTileZ = providers.gradleProperty('probeTileZ')
|
||||
Provider<String> probeOutput = providers.gradleProperty('probeOutput')
|
||||
Provider<String> probeStrictGeometry = providers.gradleProperty('probeStrictGeometry').orElse('false')
|
||||
Provider<String> probeWindows = providers.gradleProperty('probeWindows')
|
||||
|
||||
tasks.register('hydrologyProbe', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Plans hydrology over a window range of a real pack and reports cold first-chunk latency, plan cost, yield, containment, rising water, seams and fluid-tick checks per window.'
|
||||
mainClass = 'art.arcane.iris.probe.HydrologyProbe'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector')
|
||||
minHeapSize = '2g'
|
||||
maxHeapSize = '6g'
|
||||
dependsOn(':core:compileJava')
|
||||
doFirst {
|
||||
Map<String, Provider<String>> required = [
|
||||
probePack: probePack,
|
||||
probeSeed: probeSeed,
|
||||
probeWindows: probeWindows
|
||||
]
|
||||
required.each { String name, Provider<String> value ->
|
||||
if (!value.isPresent() || value.get().isBlank()) {
|
||||
throw new GradleException("hydrologyProbe requires -P${name}=<value>")
|
||||
}
|
||||
}
|
||||
args(probePack.get(), probeDimension.getOrElse('-'), probeSeed.get(), probeWindows.get(),
|
||||
providers.gradleProperty('probeThreads').getOrElse('8'), probeStudio.get())
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('riverTransectProbe', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Plans hydrology over a window range of a real pack and renders each window as a plan, river cross-sections and pond outlines.'
|
||||
mainClass = 'art.arcane.iris.probe.RiverTransectProbe'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
|
||||
minHeapSize = '2g'
|
||||
maxHeapSize = '6g'
|
||||
dependsOn(':core:compileJava')
|
||||
doFirst {
|
||||
Map<String, Provider<String>> required = [
|
||||
probePack: probePack,
|
||||
probeSeed: probeSeed,
|
||||
probeWindows: probeWindows,
|
||||
probeOutput: probeOutput
|
||||
]
|
||||
required.each { String name, Provider<String> value ->
|
||||
if (!value.isPresent() || value.get().isBlank()) {
|
||||
throw new GradleException("riverTransectProbe requires -P${name}=<value>")
|
||||
}
|
||||
}
|
||||
java.nio.file.Path output = new File(probeOutput.get()).canonicalFile.toPath()
|
||||
if (output.startsWith(rootProject.projectDir.canonicalFile.toPath())) {
|
||||
throw new GradleException('probeOutput must point outside the repository')
|
||||
}
|
||||
args(probePack.get(), probeDimension.getOrElse('-'), probeSeed.get(), probeWindows.get(), output.toString(), probeStudio.get())
|
||||
}
|
||||
}
|
||||
|
||||
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.'
|
||||
description = 'Generates up to 64 real pack chunks and checks the planned water surface, containment, fluid-tick stability and cane support, with block images.'
|
||||
mainClass = 'art.arcane.iris.probe.HydrologyBlockProbe'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
|
||||
maxHeapSize = '4g'
|
||||
dependsOn(':core:compileJava')
|
||||
doFirst {
|
||||
Map<String, Provider<String>> required = [
|
||||
@@ -109,7 +149,6 @@ tasks.register('hydrologyBlockProbe', JavaExec) {
|
||||
probeMaximumChunkX: probeMaximumChunkX,
|
||||
probeMinimumChunkZ: probeMinimumChunkZ,
|
||||
probeMaximumChunkZ: probeMaximumChunkZ,
|
||||
probeRequiredCoverage: probeRequiredCoverage,
|
||||
probeOutput: probeOutput
|
||||
]
|
||||
required.each { String name, Provider<String> value ->
|
||||
@@ -117,9 +156,12 @@ tasks.register('hydrologyBlockProbe', JavaExec) {
|
||||
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())
|
||||
java.nio.file.Path output = new File(probeOutput.get()).canonicalFile.toPath()
|
||||
if (output.startsWith(rootProject.projectDir.canonicalFile.toPath())) {
|
||||
throw new GradleException('probeOutput must point outside the repository')
|
||||
}
|
||||
args(probePack.get(), probeDimension.get(), probeSeed.get(), probeMinimumChunkX.get(), probeMaximumChunkX.get(),
|
||||
probeMinimumChunkZ.get(), probeMaximumChunkZ.get(), probeRequiredCoverage.getOrElse('-'), output.toString(), probeStudio.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,43 +279,6 @@ tasks.register('genDiff', JavaExec) {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('hydrologyPackProbe', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Scans explicitly bounded real-pack hydrology tiles across multiple engine seeds and enforces accepted feature/profile coverage.'
|
||||
mainClass = 'art.arcane.iris.probe.HydrologyPackProbe'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector')
|
||||
dependsOn(':core:compileJava')
|
||||
doFirst {
|
||||
Map<String, Provider<String>> required = [
|
||||
probePack: probePack,
|
||||
probeDimension: probeDimension,
|
||||
probeSeeds: probeSeeds,
|
||||
probeMinimumTileX: probeMinimumTileX,
|
||||
probeMaximumTileX: probeMaximumTileX,
|
||||
probeMinimumTileZ: probeMinimumTileZ,
|
||||
probeMaximumTileZ: probeMaximumTileZ,
|
||||
probeRequiredCoverage: probeRequiredCoverage,
|
||||
probeStudio: explicitProbeStudio
|
||||
]
|
||||
required.each { String name, Provider<String> value ->
|
||||
if (!value.isPresent() || value.get().isBlank()) {
|
||||
throw new GradleException("hydrologyPackProbe requires -P${name}=<value>")
|
||||
}
|
||||
}
|
||||
args(
|
||||
probePack.get(),
|
||||
probeDimension.get(),
|
||||
probeSeeds.get(),
|
||||
probeMinimumTileX.get(),
|
||||
probeMaximumTileX.get(),
|
||||
probeMinimumTileZ.get(),
|
||||
probeMaximumTileZ.get(),
|
||||
probeRequiredCoverage.get(),
|
||||
explicitProbeStudio.get())
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('generationOrderProbe', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Compares complete real-pack chunk block and biome hashes across forward, reverse, shuffled and bounded-parallel generation orders.'
|
||||
@@ -315,35 +320,58 @@ tasks.register('generationOrderProbe', JavaExec) {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('riverTransectProbe', JavaExec) {
|
||||
tasks.register('cavePoolProbe', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Plans one hydrology tile over a real pack and renders every surface river as a plan, cross-sections and a summary with ocean-write and containment gates.'
|
||||
mainClass = 'art.arcane.iris.probe.RiverTransectProbe'
|
||||
description = 'Generates real pack chunks with cave pools and reports containment, pool sizes and generation-order hashes.'
|
||||
mainClass = 'art.arcane.iris.probe.CavePoolProbe'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector')
|
||||
minHeapSize = '2g'
|
||||
maxHeapSize = '8g'
|
||||
dependsOn(':core:compileJava')
|
||||
doFirst {
|
||||
Provider<String> threads = providers.gradleProperty('probeThreads').orElse('8')
|
||||
Provider<String> work = providers.gradleProperty('probeWork').orElse(System.getProperty('java.io.tmpdir'))
|
||||
Map<String, Provider<String>> required = [
|
||||
probePack: probePack,
|
||||
probeDimension: probeDimension,
|
||||
probeSeed: probeSeed,
|
||||
probeTileX: probeTileX,
|
||||
probeTileZ: probeTileZ,
|
||||
probeOutput: probeOutput
|
||||
probeMinimumChunkX: probeMinimumChunkX,
|
||||
probeMaximumChunkX: probeMaximumChunkX,
|
||||
probeMinimumChunkZ: probeMinimumChunkZ,
|
||||
probeMaximumChunkZ: probeMaximumChunkZ
|
||||
]
|
||||
required.each { String name, Provider<String> value ->
|
||||
if (!value.isPresent() || value.get().isBlank()) {
|
||||
throw new GradleException("riverTransectProbe requires -P${name}=<value>")
|
||||
throw new GradleException("cavePoolProbe requires -P${name}=<value>")
|
||||
}
|
||||
}
|
||||
args(
|
||||
probePack.get(),
|
||||
probeDimension.get(),
|
||||
probeSeed.get(),
|
||||
probeTileX.get(),
|
||||
probeTileZ.get(),
|
||||
probeOutput.get(),
|
||||
probeStudio.get())
|
||||
args(probePack.get(), probeDimension.get(), probeSeed.get(), probeMinimumChunkX.get(), probeMaximumChunkX.get(),
|
||||
probeMinimumChunkZ.get(), probeMaximumChunkZ.get(), threads.get(), work.get())
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('seaInletProbe', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Plans and generates real pack chunks with and without sea inlets and reports containment, seal, roof, sea level, ocean connection and order determinism.'
|
||||
mainClass = 'art.arcane.iris.probe.SeaInletProbe'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
|
||||
minHeapSize = '2g'
|
||||
maxHeapSize = '8g'
|
||||
dependsOn(':core:compileJava')
|
||||
doFirst {
|
||||
Provider<String> packOff = providers.gradleProperty('probePackOff')
|
||||
Provider<String> windows = providers.gradleProperty('probeWindows')
|
||||
Provider<String> mode = providers.gradleProperty('probeMode').orElse('gate')
|
||||
if (!probePack.isPresent() || !packOff.isPresent() || !probeDimension.isPresent() || !windows.isPresent() || !probeOutput.isPresent()) {
|
||||
throw new GradleException('seaInletProbe requires probePack, probePackOff, probeDimension, probeWindows=x0..x1,z0..z1 and probeOutput')
|
||||
}
|
||||
String[] axes = windows.get().split(',')
|
||||
String[] xs = axes[0].split('\\.\\.')
|
||||
String[] zs = axes[1].split('\\.\\.')
|
||||
args(probePack.get(), packOff.get(), probeDimension.get(), probeSeed.orElse('1337').get(), xs[0], xs[1], zs[0], zs[1], mode.get(),
|
||||
probeOutput.get(), providers.gradleProperty('probeThreads').orElse('6').get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +399,8 @@ tasks.register('transitionProbe', JavaExec) {
|
||||
}
|
||||
}
|
||||
|
||||
['transitionProbe', 'hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe', 'riverTransectProbe', 'terrain3DProbe', 'hydrologyBlockProbe'].each { String taskName ->
|
||||
['transitionProbe', 'hydrologyProbe', 'genProbe', 'generationOrderProbe', 'riverTransectProbe', 'terrain3DProbe',
|
||||
'hydrologyBlockProbe', 'cavePoolProbe', 'seaInletProbe'].each { String taskName ->
|
||||
tasks.named(taskName, JavaExec).configure { JavaExec task ->
|
||||
classpath += configurations.studioProbeApi
|
||||
doFirst {
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class CavePoolProbe {
|
||||
private static final String LOG_PREFIX = "[cave-pool-probe]";
|
||||
private static final int[] DX = {1, -1, 0, 0, 0, 0};
|
||||
private static final int[] DY = {0, 0, 0, 0, -1, 1};
|
||||
private static final int[] DZ = {0, 0, 1, -1, 0, 0};
|
||||
private static final int UP = 5;
|
||||
|
||||
private CavePoolProbe() {
|
||||
}
|
||||
|
||||
record Configuration(File pack, String dimension, long seed, int minX, int maxX, int minZ, int maxZ, int threads, Path work) {
|
||||
static Configuration parse(String[] arguments) {
|
||||
if (arguments.length != 9) {
|
||||
throw new IllegalArgumentException("Expected: <pack> <dimension> <seed> <minChunkX> <maxChunkX> <minChunkZ> <maxChunkZ> "
|
||||
+ "<threads> <workParent>");
|
||||
}
|
||||
return new Configuration(new File(arguments[0]), arguments[1], Long.parseLong(arguments[2]), Integer.parseInt(arguments[3]),
|
||||
Integer.parseInt(arguments[4]), Integer.parseInt(arguments[5]), Integer.parseInt(arguments[6]),
|
||||
Integer.parseInt(arguments[7]), Path.of(arguments[8]));
|
||||
}
|
||||
|
||||
Area area() {
|
||||
return new Area(minX, maxX, minZ, maxZ);
|
||||
}
|
||||
}
|
||||
|
||||
record Area(int minX, int maxX, int minZ, int maxZ) {
|
||||
boolean inner(int chunkX, int chunkZ) {
|
||||
return chunkX >= minX && chunkX <= maxX && chunkZ >= minZ && chunkZ <= maxZ;
|
||||
}
|
||||
|
||||
List<long[]> chunks(int margin) {
|
||||
List<long[]> chunks = new ArrayList<>();
|
||||
for (int z = minZ - margin; z <= maxZ + margin; z++) {
|
||||
for (int x = minX - margin; x <= maxX + margin; x++) {
|
||||
chunks.add(new long[]{x, z});
|
||||
}
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
|
||||
record Counts(int chunks, int pools, int checked, int guards, Map<String, Integer> fluids, List<Integer> areas, List<Integer> depths,
|
||||
Map<Integer, Integer> chunkSpans, int airLeaks, int openLeaks, int skyLeaks, int foreignFluid, int waterloggableWalls,
|
||||
int unsupportedGravity, int fluidAbove, int gravityCeilings, Map<String, Integer> replaced, List<String> examples) {
|
||||
long failures() {
|
||||
return (long) airLeaks + openLeaks + skyLeaks + foreignFluid + waterloggableWalls + unsupportedGravity + fluidAbove + gravityCeilings;
|
||||
}
|
||||
}
|
||||
|
||||
static Counts check(Engine engine, Area area, ExecutorService workers) throws Exception {
|
||||
return count(survey(engine, area, workers), area);
|
||||
}
|
||||
|
||||
public static void main(String[] arguments) {
|
||||
try {
|
||||
scan(Configuration.parse(arguments));
|
||||
System.exit(0);
|
||||
} catch (Throwable failure) {
|
||||
System.out.println(LOG_PREFIX + " FAIL");
|
||||
failure.printStackTrace(System.out);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static RealPackProbeSupport.Workspace workspace(Configuration configuration) throws Exception {
|
||||
return RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(configuration.pack(), configuration.dimension(),
|
||||
LOG_PREFIX, null, configuration.work()));
|
||||
}
|
||||
|
||||
private static void scan(Configuration configuration) throws Exception {
|
||||
try (RealPackProbeSupport.Workspace workspace = workspace(configuration)) {
|
||||
Survey forward;
|
||||
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(configuration.seed(), false, "forward")) {
|
||||
Engine engine = session.engine();
|
||||
preplan(engine, configuration);
|
||||
forward = survey(engine, configuration.area(), null);
|
||||
requireQuiet(engine, "forward");
|
||||
}
|
||||
report(count(forward, configuration.area()));
|
||||
Map<Long, Long> shuffled;
|
||||
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(configuration.seed(), false, "shuffled")) {
|
||||
Engine engine = session.engine();
|
||||
preplan(engine, configuration);
|
||||
shuffled = hashes(engine, configuration);
|
||||
requireQuiet(engine, "shuffled");
|
||||
}
|
||||
compare(forward.hashes(), shuffled);
|
||||
}
|
||||
}
|
||||
|
||||
private static void preplan(Engine engine, Configuration configuration) throws Exception {
|
||||
HydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
|
||||
if (hydrology == null || !hydrology.enabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
hydrology.prepareWindowsAround(configuration.minX() - 1, configuration.minZ() - 1, configuration.maxX() + 1,
|
||||
configuration.maxZ() + 1, 1).get();
|
||||
} catch (ExecutionException failure) {
|
||||
throw new IllegalStateException("Hydrology preplan failed", failure.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireQuiet(Engine engine, String label) throws InterruptedException {
|
||||
List<Throwable> reports = RealPackProbeSupport.settleAndDrain(engine);
|
||||
if (!reports.isEmpty()) {
|
||||
RealPackProbeSupport.printReports(LOG_PREFIX, label, reports);
|
||||
IllegalStateException failure = new IllegalStateException(label + " generation reported " + reports.size() + " error(s)");
|
||||
reports.forEach(failure::addSuppressed);
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
record Cell(long position, String fluid) {
|
||||
}
|
||||
|
||||
record ChunkRecord(long[] open, long[] fluidOrAir, long[] gravity, long[] waterloggable, int[] roof) {
|
||||
boolean open(int localX, int y, int localZ) {
|
||||
return bit(open, localX, y, localZ);
|
||||
}
|
||||
|
||||
boolean gravity(int localX, int y, int localZ) {
|
||||
return bit(gravity, localX, y, localZ);
|
||||
}
|
||||
|
||||
boolean waterloggable(int localX, int y, int localZ) {
|
||||
return bit(waterloggable, localX, y, localZ);
|
||||
}
|
||||
|
||||
boolean fluidOrAir(int localX, int y, int localZ) {
|
||||
return bit(fluidOrAir, localX, y, localZ);
|
||||
}
|
||||
|
||||
private static boolean bit(long[] bits, int localX, int y, int localZ) {
|
||||
int index = (y << 8) | (localZ << 4) | localX;
|
||||
return (bits[index >>> 6] & (1L << (index & 63))) != 0L;
|
||||
}
|
||||
}
|
||||
|
||||
record Survey(Map<Long, Long> hashes, Map<Long, ChunkRecord> chunks, List<Cell> pool, Map<String, Integer> replaced, int guards) {
|
||||
}
|
||||
|
||||
private static Survey survey(Engine engine, Area area, ExecutorService workers) throws Exception {
|
||||
List<long[]> chunks = area.chunks(2);
|
||||
Map<Long, Long> hashes = new HashMap<>();
|
||||
Map<Long, ChunkRecord> records = new HashMap<>();
|
||||
List<Cell> pool = new ArrayList<>();
|
||||
Map<String, Integer> replaced = new TreeMap<>();
|
||||
int height = engine.getTarget().getHeight();
|
||||
List<Future<Hunk<NativeBlockState>>> generated = new ArrayList<>(chunks.size());
|
||||
if (workers != null) {
|
||||
for (long[] chunk : chunks) {
|
||||
generated.add(workers.submit(() -> generate(engine, (int) chunk[0], (int) chunk[1], height, hashes)));
|
||||
}
|
||||
}
|
||||
int guards = 0;
|
||||
for (int index = 0; index < chunks.size(); index++) {
|
||||
int chunkX = (int) chunks.get(index)[0];
|
||||
int chunkZ = (int) chunks.get(index)[1];
|
||||
Hunk<NativeBlockState> blocks;
|
||||
try {
|
||||
blocks = workers == null ? generate(engine, chunkX, chunkZ, height, hashes) : generated.get(index).get();
|
||||
} catch (ExecutionException failure) {
|
||||
throw new IllegalStateException("Failed to generate " + chunkX + "," + chunkZ, failure.getCause());
|
||||
}
|
||||
records.put(key(chunkX, chunkZ), record(blocks, height));
|
||||
guards += poolCells(engine, chunkX, chunkZ, blocks, pool, replaced);
|
||||
if (workers == null) {
|
||||
progress(index + 1, chunks.size());
|
||||
}
|
||||
}
|
||||
return new Survey(hashes, records, pool, replaced, guards);
|
||||
}
|
||||
|
||||
private static Hunk<NativeBlockState> generate(Engine engine, int chunkX, int chunkZ, int height, Map<Long, Long> hashes) {
|
||||
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
|
||||
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, height, 16);
|
||||
try {
|
||||
engine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Failed to generate " + chunkX + "," + chunkZ, failure);
|
||||
}
|
||||
long hash = hash(blocks, biomes, height);
|
||||
synchronized (hashes) {
|
||||
hashes.put(key(chunkX, chunkZ), hash);
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static int poolCells(Engine engine, int chunkX, int chunkZ, Hunk<NativeBlockState> blocks, List<Cell> pool,
|
||||
Map<String, Integer> replaced) {
|
||||
MantleChunk<Matter> chunk = engine.getMantle().getMantle().useChunk(chunkX, chunkZ);
|
||||
int[] guards = new int[1];
|
||||
try {
|
||||
chunk.iterate(HydrologyCaveCell.class, (x, y, z, cell) -> {
|
||||
if (cell == null) {
|
||||
return;
|
||||
}
|
||||
if (cell.action() == HydrologyCaveAction.SEAL_GUARD) {
|
||||
guards[0]++;
|
||||
return;
|
||||
}
|
||||
if (cell.action() != HydrologyCaveAction.POOL_SOURCE) {
|
||||
return;
|
||||
}
|
||||
pool.add(new Cell(pack((chunkX << 4) + x, y, (chunkZ << 4) + z), cell.fluidProfileKey()));
|
||||
NativeBlockState state = blocks.get(x, y, z);
|
||||
if (!source(state, cell.fluidProfileKey())) {
|
||||
replaced.merge(cell.fluidProfileKey() + "->" + (state == null ? "null" : state.key()), 1, Integer::sum);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
return guards[0];
|
||||
}
|
||||
|
||||
private static boolean source(NativeBlockState state, String fluid) {
|
||||
if (state == null || !state.isFluid()) {
|
||||
return false;
|
||||
}
|
||||
String level = IrisProceduralBlocks.propertyValue(state, "level");
|
||||
boolean still = level == null || "0".equals(level);
|
||||
boolean matches = "lava".equals(fluid) ? state.key().startsWith("minecraft:lava") : state.isWater();
|
||||
return still && matches;
|
||||
}
|
||||
|
||||
private static ChunkRecord record(Hunk<NativeBlockState> blocks, int height) {
|
||||
long[] open = new long[height * 4];
|
||||
long[] fluidOrAir = new long[height * 4];
|
||||
long[] gravity = new long[height * 4];
|
||||
long[] waterloggable = new long[height * 4];
|
||||
int[] roof = new int[256];
|
||||
Arrays.fill(roof, -1);
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
NativeBlockState state = blocks.get(x, y, z);
|
||||
int index = (y << 8) | (z << 4) | x;
|
||||
boolean air = state == null || state.isAir();
|
||||
boolean fluid = !air && state.isFluid();
|
||||
boolean solid = !air && !fluid && state.isSolid();
|
||||
boolean logged = !air && !fluid && IrisProceduralBlocks.hasProperty(state, "waterlogged");
|
||||
if (!solid && !fluid && !logged) {
|
||||
open[index >>> 6] |= 1L << (index & 63);
|
||||
}
|
||||
if (logged) {
|
||||
waterloggable[index >>> 6] |= 1L << (index & 63);
|
||||
}
|
||||
if (air || fluid) {
|
||||
fluidOrAir[index >>> 6] |= 1L << (index & 63);
|
||||
}
|
||||
if (solid) {
|
||||
roof[(z << 4) | x] = y;
|
||||
if (IrisProceduralBlocks.isGravityAffected(state)) {
|
||||
gravity[index >>> 6] |= 1L << (index & 63);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ChunkRecord(open, fluidOrAir, gravity, waterloggable, roof);
|
||||
}
|
||||
|
||||
private static long hash(Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes, int height) {
|
||||
long hash = 0x9e3779b97f4a7c15L;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
NativeBlockState state = blocks.get(x, y, z);
|
||||
NativeBiome biome = biomes.get(x, y, z);
|
||||
hash = mix(hash ^ (state == null ? 0 : state.key().hashCode()));
|
||||
hash = mix(hash ^ (biome == null ? 0 : biome.key().hashCode()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static Map<Long, Long> hashes(Engine engine, Configuration configuration) throws Exception {
|
||||
List<long[]> chunks = new ArrayList<>(configuration.area().chunks(2));
|
||||
Collections.shuffle(chunks, new Random(configuration.seed() ^ 0x5DEECE66DL));
|
||||
ExecutorService executor = Executors.newFixedThreadPool(configuration.threads());
|
||||
try {
|
||||
List<Future<long[]>> futures = new ArrayList<>();
|
||||
for (long[] chunk : chunks) {
|
||||
futures.add(executor.submit(() -> {
|
||||
int height = engine.getTarget().getHeight();
|
||||
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
|
||||
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, height, 16);
|
||||
engine.generate((int) chunk[0] << 4, (int) chunk[1] << 4, blocks, biomes, true);
|
||||
return new long[]{chunk[0], chunk[1], hash(blocks, biomes, height)};
|
||||
}));
|
||||
}
|
||||
Map<Long, Long> hashes = new HashMap<>();
|
||||
for (int index = 0; index < futures.size(); index++) {
|
||||
long[] result = futures.get(index).get();
|
||||
hashes.put(key((int) result[0], (int) result[1]), result[2]);
|
||||
progress(index + 1, futures.size());
|
||||
}
|
||||
return hashes;
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(60L, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private static void compare(Map<Long, Long> forward, Map<Long, Long> shuffled) {
|
||||
int mismatches = 0;
|
||||
List<String> examples = new ArrayList<>();
|
||||
for (Map.Entry<Long, Long> entry : forward.entrySet()) {
|
||||
if (!entry.getValue().equals(shuffled.get(entry.getKey()))) {
|
||||
mismatches++;
|
||||
if (examples.size() < 16) {
|
||||
examples.add((int) (entry.getKey() >> 32) + "," + (int) (long) entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
long signature = 0L;
|
||||
for (Map.Entry<Long, Long> entry : new TreeMap<>(forward).entrySet()) {
|
||||
signature = mix(signature ^ entry.getKey() ^ mix(entry.getValue()));
|
||||
}
|
||||
System.out.printf(Locale.ROOT, "CAVE_POOL_ORDER chunks=%d mismatches=%d signature=%016x examples=%s%n", forward.size(), mismatches,
|
||||
signature, examples);
|
||||
}
|
||||
|
||||
static Counts count(Survey survey, Area area) {
|
||||
Map<Long, Integer> index = new HashMap<>();
|
||||
for (int cell = 0; cell < survey.pool().size(); cell++) {
|
||||
index.put(survey.pool().get(cell).position(), cell);
|
||||
}
|
||||
int[] parent = new int[survey.pool().size()];
|
||||
for (int cell = 0; cell < parent.length; cell++) {
|
||||
parent[cell] = cell;
|
||||
}
|
||||
for (int cell = 0; cell < parent.length; cell++) {
|
||||
long position = survey.pool().get(cell).position();
|
||||
for (int direction = 0; direction < 6; direction++) {
|
||||
Integer other = index.get(pack(x(position) + DX[direction], y(position) + DY[direction], z(position) + DZ[direction]));
|
||||
if (other != null) {
|
||||
parent[find(parent, cell)] = find(parent, other);
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<Integer, int[]> levels = new HashMap<>();
|
||||
for (int cell = 0; cell < parent.length; cell++) {
|
||||
int root = find(parent, cell);
|
||||
int y = y(survey.pool().get(cell).position());
|
||||
int[] range = levels.computeIfAbsent(root, ignored -> new int[]{Integer.MAX_VALUE, Integer.MIN_VALUE});
|
||||
range[0] = Math.min(range[0], y);
|
||||
range[1] = Math.max(range[1], y);
|
||||
}
|
||||
int airLeaks = 0;
|
||||
int openLeaks = 0;
|
||||
int skyLeaks = 0;
|
||||
int foreignFluid = 0;
|
||||
int unsupportedGravity = 0;
|
||||
int waterloggableWalls = 0;
|
||||
int fluidAbove = 0;
|
||||
int gravityCeilings = 0;
|
||||
int checked = 0;
|
||||
Map<Integer, Set<Long>> columns = new HashMap<>();
|
||||
Map<Integer, Set<Long>> spans = new HashMap<>();
|
||||
Map<Integer, String> fluids = new HashMap<>();
|
||||
Set<Integer> innerPools = new HashSet<>();
|
||||
List<String> examples = new ArrayList<>();
|
||||
for (int cell = 0; cell < parent.length; cell++) {
|
||||
Cell pool = survey.pool().get(cell);
|
||||
long position = pool.position();
|
||||
int x = x(position);
|
||||
int y = y(position);
|
||||
int z = z(position);
|
||||
int root = find(parent, cell);
|
||||
columns.computeIfAbsent(root, ignored -> new HashSet<>()).add(key(x, z));
|
||||
spans.computeIfAbsent(root, ignored -> new HashSet<>()).add(key(x >> 4, z >> 4));
|
||||
fluids.put(root, pool.fluid());
|
||||
if (!area.inner(x >> 4, z >> 4)) {
|
||||
continue;
|
||||
}
|
||||
innerPools.add(root);
|
||||
checked++;
|
||||
int level = levels.get(root)[1];
|
||||
ChunkRecord own = survey.chunks().get(key(x >> 4, z >> 4));
|
||||
if (y >= own.roof()[((z & 15) << 4) | (x & 15)]) {
|
||||
skyLeaks++;
|
||||
}
|
||||
for (int direction = 0; direction < 6; direction++) {
|
||||
int neighbourY = y + DY[direction];
|
||||
int neighbourX = x + DX[direction];
|
||||
int neighbourZ = z + DZ[direction];
|
||||
if (index.containsKey(pack(neighbourX, neighbourY, neighbourZ))) {
|
||||
continue;
|
||||
}
|
||||
ChunkRecord record = survey.chunks().get(key(neighbourX >> 4, neighbourZ >> 4));
|
||||
int localX = neighbourX & 15;
|
||||
int localZ = neighbourZ & 15;
|
||||
if (direction == UP && neighbourY > level) {
|
||||
if (record.fluidOrAir(localX, neighbourY, localZ) && !record.open(localX, neighbourY, localZ)) {
|
||||
fluidAbove++;
|
||||
if (examples.size() < 12) {
|
||||
examples.add("fluid-above " + neighbourX + "," + neighbourY + "," + neighbourZ);
|
||||
}
|
||||
} else if (record.gravity(localX, neighbourY, localZ)) {
|
||||
gravityCeilings++;
|
||||
if (examples.size() < 12) {
|
||||
examples.add("gravity-ceiling " + neighbourX + "," + neighbourY + "," + neighbourZ);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (record.open(localX, neighbourY, localZ)) {
|
||||
if (record.fluidOrAir(localX, neighbourY, localZ)) {
|
||||
airLeaks++;
|
||||
} else {
|
||||
openLeaks++;
|
||||
}
|
||||
if (examples.size() < 12) {
|
||||
examples.add(x + "," + y + "," + z + "->" + neighbourX + "," + neighbourY + "," + neighbourZ);
|
||||
}
|
||||
} else if (record.fluidOrAir(localX, neighbourY, localZ)) {
|
||||
foreignFluid++;
|
||||
} else if (record.waterloggable(localX, neighbourY, localZ)) {
|
||||
waterloggableWalls++;
|
||||
if (examples.size() < 12) {
|
||||
examples.add("waterloggable " + neighbourX + "," + neighbourY + "," + neighbourZ);
|
||||
}
|
||||
} else if (record.gravity(localX, neighbourY, localZ) && neighbourY > 0
|
||||
&& (index.containsKey(pack(neighbourX, neighbourY - 1, neighbourZ))
|
||||
|| record.open(localX, neighbourY - 1, localZ) || record.fluidOrAir(localX, neighbourY - 1, localZ))) {
|
||||
unsupportedGravity++;
|
||||
if (examples.size() < 12) {
|
||||
examples.add("gravity " + neighbourX + "," + neighbourY + "," + neighbourZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Integer> areas = new ArrayList<>();
|
||||
List<Integer> depths = new ArrayList<>();
|
||||
Map<String, Integer> byFluid = new TreeMap<>();
|
||||
Map<Integer, Integer> chunkSpans = new TreeMap<>();
|
||||
for (int root : innerPools) {
|
||||
chunkSpans.merge(spans.get(root).size(), 1, Integer::sum);
|
||||
areas.add(columns.get(root).size());
|
||||
int[] range = levels.get(root);
|
||||
depths.add(range[1] - range[0] + 1);
|
||||
byFluid.merge(fluids.get(root), 1, Integer::sum);
|
||||
}
|
||||
Collections.sort(areas);
|
||||
Collections.sort(depths);
|
||||
return new Counts(survey.hashes().size(), innerPools.size(), checked, survey.guards(), byFluid, areas, depths, chunkSpans, airLeaks,
|
||||
openLeaks, skyLeaks, foreignFluid, waterloggableWalls, unsupportedGravity, fluidAbove, gravityCeilings, survey.replaced(),
|
||||
examples);
|
||||
}
|
||||
|
||||
private static void report(Counts counts) {
|
||||
List<Integer> areas = counts.areas();
|
||||
Map<Integer, Integer> depthHistogram = new TreeMap<>();
|
||||
counts.depths().forEach(depth -> depthHistogram.merge(depth, 1, Integer::sum));
|
||||
long totalArea = areas.stream().mapToLong(Integer::longValue).sum();
|
||||
System.out.printf(Locale.ROOT, "CAVE_POOL_SURVEY chunks=%d pools=%d pool_cells_checked=%d guards=%d fluids=%s%n",
|
||||
counts.chunks(), counts.pools(), counts.checked(), counts.guards(), counts.fluids());
|
||||
System.out.printf(Locale.ROOT, "CAVE_POOL_AREA total=%d min=%d median=%d mean=%.1f max=%d%n", totalArea,
|
||||
areas.isEmpty() ? 0 : areas.get(0), areas.isEmpty() ? 0 : areas.get(areas.size() / 2),
|
||||
areas.isEmpty() ? 0D : totalArea / (double) areas.size(), areas.isEmpty() ? 0 : areas.get(areas.size() - 1));
|
||||
System.out.printf(Locale.ROOT, "CAVE_POOL_DEPTH histogram=%s%n", depthHistogram);
|
||||
System.out.printf(Locale.ROOT, "CAVE_POOL_CHUNKS_SPANNED histogram=%s%n", counts.chunkSpans());
|
||||
System.out.printf(Locale.ROOT, "CAVE_POOL_CONTAINMENT air_leaks=%d open_leaks=%d sky_leaks=%d foreign_fluid_contacts=%d "
|
||||
+ "waterloggable_walls=%d unsupported_gravity_walls=%d fluid_above_surface=%d gravity_ceilings=%d replaced_cells=%s "
|
||||
+ "examples=%s%n", counts.airLeaks(), counts.openLeaks(), counts.skyLeaks(), counts.foreignFluid(),
|
||||
counts.waterloggableWalls(), counts.unsupportedGravity(), counts.fluidAbove(), counts.gravityCeilings(), counts.replaced(),
|
||||
counts.examples());
|
||||
}
|
||||
|
||||
private static int find(int[] parent, int cell) {
|
||||
while (parent[cell] != cell) {
|
||||
parent[cell] = parent[parent[cell]];
|
||||
cell = parent[cell];
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
private static void progress(int done, int total) {
|
||||
if (done == total || done % 1024 == 0) {
|
||||
System.out.println(LOG_PREFIX + " " + done + "/" + total);
|
||||
}
|
||||
}
|
||||
|
||||
private static long key(int x, int z) {
|
||||
return ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
}
|
||||
|
||||
private static long pack(int x, int y, int z) {
|
||||
return ((long) (x + (1 << 25)) << 38) | ((long) (z + (1 << 25)) << 12) | (y & 4095);
|
||||
}
|
||||
|
||||
private static int x(long position) {
|
||||
return (int) (position >>> 38) - (1 << 25);
|
||||
}
|
||||
|
||||
private static int z(long position) {
|
||||
return (int) ((position >>> 12) & ((1L << 26) - 1)) - (1 << 25);
|
||||
}
|
||||
|
||||
private static int y(long position) {
|
||||
return (int) (position & 4095);
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
long mixed = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
mixed = (mixed ^ (mixed >>> 27)) * 0x94d049bb133111ebL;
|
||||
return mixed ^ (mixed >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -38,13 +38,14 @@ import art.arcane.iris.generation.runtime.MeteredCache;
|
||||
import art.arcane.iris.generation.runtime.PreservationRegistry;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.IrisBoundarySignatureSampler;
|
||||
import art.arcane.iris.world.pregen.PregenHydrology;
|
||||
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
@@ -540,9 +541,8 @@ public final class GenerationProbe {
|
||||
return generate(generator, configuration, options);
|
||||
}
|
||||
PregenPerformanceProfile.apply(engine);
|
||||
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
|
||||
try (HydrologyTileCache.PregenerationScope ignored = hydrology == null ? null
|
||||
: hydrology.preparePregeneration(pregenerationArea(configuration))) {
|
||||
try (HydrologyPregeneration pregeneration = PregenHydrology.open(engine)) {
|
||||
PregenHydrology.prepareAll(pregeneration, hydrologyRegions(configuration));
|
||||
preplanHydrology(engine, configuration, options);
|
||||
return generate(generator, configuration, options);
|
||||
} finally {
|
||||
@@ -558,10 +558,10 @@ public final class GenerationProbe {
|
||||
Optional<ProbeHydrologyPreplan> preplan = ProbeHydrologyPreplan.run(engine, configuration);
|
||||
printDistinctCauses("hydrology preplan reported errors", drainReported());
|
||||
System.out.println(preplan.map(ProbeHydrologyPreplan::machineLine)
|
||||
.orElse("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=0 skipped=no-hydrology"));
|
||||
.orElse("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=2 windows=0 skipped=no-hydrology"));
|
||||
}
|
||||
|
||||
static HydrologyTileCache.PregenerationArea pregenerationArea(ProbeConfiguration configuration) {
|
||||
static int[] scheduledChunkBounds(ProbeConfiguration configuration) {
|
||||
List<ChunkCoordinate> coordinates = scheduleCoordinates(
|
||||
configuration.warmupChunks() + configuration.measuredChunks(),
|
||||
configuration.centerChunkX(), configuration.centerChunkZ());
|
||||
@@ -575,13 +575,26 @@ public final class GenerationProbe {
|
||||
maximumX = Math.max(maximumX, coordinate.x());
|
||||
maximumZ = Math.max(maximumZ, coordinate.z());
|
||||
}
|
||||
int centerBlockX = Math.toIntExact(Math.clamp(((long) configuration.centerChunkX() << 4) + 8L,
|
||||
(long) minimumX << 4, (((long) maximumX + 1L) << 4) - 1L));
|
||||
int centerBlockZ = Math.toIntExact(Math.clamp(((long) configuration.centerChunkZ() << 4) + 8L,
|
||||
(long) minimumZ << 4, (((long) maximumZ + 1L) << 4) - 1L));
|
||||
return new HydrologyTileCache.PregenerationArea(centerBlockX, centerBlockZ,
|
||||
(long) minimumX << 4, (long) minimumZ << 4,
|
||||
(((long) maximumX + 1L) << 4) - 1L, (((long) maximumZ + 1L) << 4) - 1L);
|
||||
return new int[]{minimumX, minimumZ, maximumX, maximumZ};
|
||||
}
|
||||
|
||||
static List<long[]> hydrologyRegions(ProbeConfiguration configuration) {
|
||||
int[] bounds = scheduledChunkBounds(configuration);
|
||||
int chunks = WindowKey.SIZE >> 4;
|
||||
int minimumX = Math.floorDiv(bounds[0], chunks);
|
||||
int minimumZ = Math.floorDiv(bounds[1], chunks);
|
||||
int maximumX = Math.floorDiv(bounds[2], chunks);
|
||||
int maximumZ = Math.floorDiv(bounds[3], chunks);
|
||||
int centerX = Math.floorDiv(configuration.centerChunkX(), chunks);
|
||||
int centerZ = Math.floorDiv(configuration.centerChunkZ(), chunks);
|
||||
List<long[]> regions = new ArrayList<>();
|
||||
for (int regionZ = minimumZ; regionZ <= maximumZ; regionZ++) {
|
||||
for (int regionX = minimumX; regionX <= maximumX; regionX++) {
|
||||
regions.add(new long[]{regionX, regionZ});
|
||||
}
|
||||
}
|
||||
regions.sort(Comparator.comparingLong((long[] region) -> Math.max(Math.abs(region[0] - centerX), Math.abs(region[1] - centerZ))));
|
||||
return regions;
|
||||
}
|
||||
|
||||
static List<Integer> spiralOrder(List<ChunkCoordinate> coordinates, int fromIndex, int centerChunkX,
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
final class HydrologyBlockMetrics {
|
||||
private static final int BED_MARGIN = 2;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
private final HydrologyProbe.Inputs inputs;
|
||||
private final HydrologyRasterMetrics rasters;
|
||||
private final HydrologyWindows.WindowRange range;
|
||||
private final Consumer<String> examples;
|
||||
private final Consumer<String> failures;
|
||||
|
||||
record Counts(HydrologyVoxels.WaterScan scan, long generated, long errors) {
|
||||
}
|
||||
|
||||
interface Columns {
|
||||
boolean known(int x, int z);
|
||||
|
||||
WaterRole role(int x, int z);
|
||||
|
||||
int terrain(int x, int z);
|
||||
|
||||
int natural(int x, int z);
|
||||
}
|
||||
|
||||
HydrologyBlockMetrics(HydrologyProbe.Inputs inputs, HydrologyRasterMetrics rasters, HydrologyWindows.WindowRange range,
|
||||
Consumer<String> examples, Consumer<String> failures) {
|
||||
this.inputs = inputs;
|
||||
this.rasters = rasters;
|
||||
this.range = range;
|
||||
this.examples = examples;
|
||||
this.failures = failures;
|
||||
}
|
||||
|
||||
static boolean scanned(Columns columns, int x, int z) {
|
||||
return columns.role(x, z) != WaterRole.NONE || besideWater(columns, x, z);
|
||||
}
|
||||
|
||||
static int scanFloor(Columns columns, int seaLevel, int x, int z) {
|
||||
int floor = columns.terrain(x, z) - BED_MARGIN;
|
||||
if (columns.role(x, z).wet() || columns.natural(x, z) >= seaLevel || besideWater(columns, x, z)) {
|
||||
return floor;
|
||||
}
|
||||
return Math.max(floor, seaLevel + 1);
|
||||
}
|
||||
|
||||
private static boolean besideWater(Columns columns, int x, int z) {
|
||||
for (int[] offset : CARDINALS) {
|
||||
int nx = x + offset[0];
|
||||
int nz = z + offset[1];
|
||||
if (columns.known(nx, nz) && columns.role(nx, nz).wet()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Counts count(WindowKey key) throws InterruptedException {
|
||||
Columns columns = new Columns() {
|
||||
@Override
|
||||
public boolean known(int x, int z) {
|
||||
return range.containsBlock(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaterRole role(int x, int z) {
|
||||
return rasters.role(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int terrain(int x, int z) {
|
||||
return rasters.terrain(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int natural(int x, int z) {
|
||||
return inputs.naturalHeight().applyAsInt(x, z);
|
||||
}
|
||||
};
|
||||
int minX = key.wx() * WindowKey.SIZE;
|
||||
int minZ = key.wz() * WindowKey.SIZE;
|
||||
List<int[]> scan = new ArrayList<>();
|
||||
Set<Long> chunks = new LinkedHashSet<>();
|
||||
for (int z = minZ; z < minZ + WindowKey.SIZE; z++) {
|
||||
for (int x = minX; x < minX + WindowKey.SIZE; x++) {
|
||||
if (!scanned(columns, x, z)) {
|
||||
continue;
|
||||
}
|
||||
scan.add(new int[]{x, z, scanFloor(columns, inputs.settings().seaLevel(), x, z)});
|
||||
chunks.add(HydrologyVoxels.chunkKey(x >> 4, z >> 4));
|
||||
for (int[] offset : CARDINALS) {
|
||||
chunks.add(HydrologyVoxels.chunkKey((x + offset[0]) >> 4, (z + offset[1]) >> 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Callable<HydrologyVoxels.ChunkBlocks>> tasks = new ArrayList<>(chunks.size());
|
||||
for (long chunk : chunks) {
|
||||
int chunkX = (int) (chunk >> 32);
|
||||
int chunkZ = (int) chunk;
|
||||
tasks.add(() -> inputs.blocks().generate(chunkX, chunkZ));
|
||||
}
|
||||
HydrologyVoxels.Volume volume = new HydrologyVoxels.Volume();
|
||||
long errors = 0L;
|
||||
for (Future<HydrologyVoxels.ChunkBlocks> future : inputs.workers().invokeAll(tasks)) {
|
||||
try {
|
||||
volume.put(future.get());
|
||||
} catch (ExecutionException failure) {
|
||||
errors++;
|
||||
failures.accept("chunk generation: " + HydrologyPlanHarness.cause(failure));
|
||||
}
|
||||
}
|
||||
HydrologyVoxels.WaterScan water = new HydrologyVoxels.WaterScan();
|
||||
for (int[] column : scan) {
|
||||
water.column(volume, column[0], column[1], column[2], inputs.worldHeight() - 1);
|
||||
}
|
||||
for (String example : water.examples) {
|
||||
examples.accept(example);
|
||||
}
|
||||
return new Counts(water, chunks.size(), errors);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -15,54 +12,45 @@ 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.LinkedHashMap;
|
||||
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 static final Set<String> IMPLICIT_WATER_BLOCKS = Set.of(
|
||||
"minecraft:seagrass", "minecraft:tall_seagrass", "minecraft:kelp", "minecraft:kelp_plant",
|
||||
"minecraft:bubble_column");
|
||||
|
||||
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) {
|
||||
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()) {
|
||||
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) {
|
||||
if (width < 1 || depth < 1 || 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 -.");
|
||||
if (!Set.of("channel", "pond", "bank", "cane").containsAll(requiredCoverage)) {
|
||||
throw new IllegalArgumentException("Required coverage accepts channel,pond,bank,cane or -.");
|
||||
}
|
||||
}
|
||||
|
||||
static Configuration parse(String[] args) {
|
||||
if (args.length != 10) {
|
||||
throw new IllegalArgumentException("Expected <pack> <dimension> <seed> <minChunkX> <maxChunkX> "
|
||||
+ "<minChunkZ> <maxChunkZ> <requiredCoverage|-> <outputDir> <studio>.");
|
||||
throw new IllegalArgumentException("Expected <pack> <dimension> <seed> <minChunkX> <maxChunkX> <minChunkZ> <maxChunkZ> "
|
||||
+ "<requiredCoverage|-> <outputDir> <studio>.");
|
||||
}
|
||||
Set<String> required = new LinkedHashSet<>();
|
||||
if (!args[7].equals("-")) {
|
||||
@@ -71,38 +59,22 @@ public final class HydrologyBlockProbe {
|
||||
}
|
||||
}
|
||||
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, NativeBlockState 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;
|
||||
Integer.parseInt(args[4]), Integer.parseInt(args[5]), Integer.parseInt(args[6]), required, new File(args[8]),
|
||||
RealPackProbeSupport.parseBoolean(args[9], "studio"));
|
||||
}
|
||||
}
|
||||
|
||||
static final class Evidence {
|
||||
long plannedWetColumns;
|
||||
long channelColumns;
|
||||
long pondColumns;
|
||||
long bankColumns;
|
||||
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 Map<String, Long> plannedWetBlockCounts = new TreeMap<>();
|
||||
HydrologyVoxels.WaterScan scan = new HydrologyVoxels.WaterScan();
|
||||
final List<String> examples = new ArrayList<>();
|
||||
|
||||
void example(String value) {
|
||||
@@ -112,53 +84,11 @@ public final class HydrologyBlockProbe {
|
||||
}
|
||||
}
|
||||
|
||||
record Result(String status, Configuration configuration, int worldMinimumY, int seaLevel,
|
||||
Evidence evidence, List<String> failures, List<String> artifacts) {
|
||||
record Result(String status, Configuration configuration, 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)));
|
||||
}
|
||||
|
||||
NativeBlockState 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);
|
||||
}
|
||||
record PlannedColumn(WaterRole role, int terrain, int natural, int head) {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -166,10 +96,10 @@ public final class HydrologyBlockProbe {
|
||||
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()
|
||||
System.out.println("IRIS_HYDROLOGY_BLOCK_RESULT version=2 status=" + result.status() + " seed=" + result.configuration().seed()
|
||||
+ " plannedWetColumns=" + evidence.plannedWetColumns + " missingSurfaceColumns=" + evidence.scan.missingSurfaces
|
||||
+ " containmentLeaks=" + evidence.scan.containmentLeaks + " tickConversionCells=" + evidence.scan.tickConversionCells
|
||||
+ " cane=" + evidence.validCaneRoots + " failures=" + result.failures().size()
|
||||
+ " output=" + result.configuration().output().getAbsolutePath());
|
||||
exitCode = result.failures().isEmpty() ? 0 : 1;
|
||||
} catch (Throwable failure) {
|
||||
@@ -179,261 +109,161 @@ public final class HydrologyBlockProbe {
|
||||
}
|
||||
|
||||
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")) {
|
||||
StubPlatform.vanillaSolidity(true);
|
||||
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) {
|
||||
HydrologyRuntime runtime = complex.getHydrologyRuntime();
|
||||
if (runtime == null || !runtime.enabled()) {
|
||||
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<>();
|
||||
HydrologyVoxels.Volume volume = new HydrologyVoxels.Volume();
|
||||
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.put(HydrologyVoxels.generate(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);
|
||||
NativeBlockState 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());
|
||||
Map<Long, PlannedColumn> planned = planned(complex, runtime, configuration);
|
||||
int seaLevel = engine.getDimension().getFluidHeight();
|
||||
Evidence evidence = inspect(configuration, volume, planned, engine);
|
||||
List<String> failures = failures(evidence, 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);
|
||||
List<String> artifacts = writeImages(configuration, volume, engine);
|
||||
Result result = new Result(failures.isEmpty() ? "PASS" : "FAIL", configuration, seaLevel, evidence, List.copyOf(failures), artifacts);
|
||||
Files.writeString(new File(configuration.output(), "hydrology-blocks.json").toPath(), json(result), StandardCharsets.UTF_8);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
private static Map<Long, PlannedColumn> planned(IrisComplex complex, HydrologyRuntime runtime, Configuration configuration) {
|
||||
Map<Long, PlannedColumn> planned = new HashMap<>();
|
||||
for (int chunkZ = configuration.minimumChunkZ(); chunkZ <= configuration.maximumChunkZ(); chunkZ++) {
|
||||
for (int chunkX = configuration.minimumChunkX(); chunkX <= configuration.maximumChunkX(); chunkX++) {
|
||||
int[] heads = HydrologyVoxels.effectiveHeads(complex, chunkX, chunkZ);
|
||||
ChunkWaterRaster own = runtime.raster(chunkX, chunkZ);
|
||||
for (int lz = 0; lz < 16; lz++) {
|
||||
for (int lx = 0; lx < 16; lx++) {
|
||||
int x = (chunkX << 4) + lx;
|
||||
int z = (chunkZ << 4) + lz;
|
||||
int head = heads[(lz << 4) | lx];
|
||||
int natural = complex.naturalTrueHeight(x, z);
|
||||
int terrain = own.empty() ? natural : own.terrainHeight(lx, lz);
|
||||
planned.put(HydrologyVoxels.columnKey(x, z), new PlannedColumn(own.role(lx, lz), terrain, natural, head));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return planned;
|
||||
}
|
||||
|
||||
static Evidence inspect(Volume volume, Map<Long, PlannedColumn> planned, int seaLevel) {
|
||||
static Evidence inspect(Configuration configuration, HydrologyVoxels.Volume volume, Map<Long, PlannedColumn> planned, Engine engine) {
|
||||
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++) {
|
||||
NativeBlockState 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);
|
||||
int minimumX = configuration.minimumChunkX() << 4;
|
||||
int minimumZ = configuration.minimumChunkZ() << 4;
|
||||
int maximumX = (configuration.maximumChunkX() << 4) + 15;
|
||||
int maximumZ = (configuration.maximumChunkZ() << 4) + 15;
|
||||
int height = engine.getTarget().getHeight();
|
||||
int seaLevel = engine.getDimension().getFluidHeight();
|
||||
HydrologyBlockMetrics.Columns columns = new HydrologyBlockMetrics.Columns() {
|
||||
@Override
|
||||
public boolean known(int x, int z) {
|
||||
return planned.containsKey(HydrologyVoxels.columnKey(x, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaterRole role(int x, int z) {
|
||||
return planned.get(HydrologyVoxels.columnKey(x, z)).role();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int terrain(int x, int z) {
|
||||
return planned.get(HydrologyVoxels.columnKey(x, z)).terrain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int natural(int x, int z) {
|
||||
return planned.get(HydrologyVoxels.columnKey(x, z)).natural();
|
||||
}
|
||||
};
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
PlannedColumn column = planned.get(HydrologyVoxels.columnKey(x, z));
|
||||
inspectCane(volume, x, z, height, evidence);
|
||||
switch (column.role()) {
|
||||
case CHANNEL, MOUTH, CASCADE, WATERFALL -> evidence.channelColumns++;
|
||||
case POND -> evidence.pondColumns++;
|
||||
case BANK -> evidence.bankColumns++;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
PlannedColumn column = planned.get(RiverFootprint.pack(x, z));
|
||||
if (column == null || column.surface() == null) {
|
||||
continue;
|
||||
if (column.head() != HydrologyVoxels.NO_WATER) {
|
||||
evidence.plannedWetColumns++;
|
||||
}
|
||||
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++;
|
||||
NativeBlockState actual = volume.block(x, y, z);
|
||||
String actualKey = key(actual);
|
||||
evidence.plannedWetBlockCounts.merge(actualKey, 1L, Long::sum);
|
||||
boolean matches = column.fluid().isWater() ? water(actual)
|
||||
: key(column.fluid()).equals(actualKey);
|
||||
if (!matches) {
|
||||
evidence.missingWetVoxels++;
|
||||
evidence.example("missing wet voxel " + x + "," + y + "," + z + "=" + actualKey);
|
||||
}
|
||||
if (HydrologyBlockMetrics.scanned(columns, x, z)) {
|
||||
evidence.scan.column(volume, x, z, HydrologyBlockMetrics.scanFloor(columns, seaLevel, x, z), height - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
inspectMouths(volume, planned, seaLevel, evidence);
|
||||
return evidence;
|
||||
}
|
||||
|
||||
private static void inspectCane(Volume volume, NativeBlockState cane, int x, int y, int z, Evidence evidence) {
|
||||
evidence.caneRoots++;
|
||||
NativeBlockState 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;
|
||||
private static void inspectCane(HydrologyVoxels.Volume volume, int x, int z, int height, Evidence evidence) {
|
||||
for (int y = 1; y < height; y++) {
|
||||
if (volume.at(x, y, z) != HydrologyVoxels.CANE || volume.at(x, y - 1, z) == HydrologyVoxels.CANE) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockState 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())) {
|
||||
evidence.caneRoots++;
|
||||
byte substrate = volume.at(x, y - 1, z);
|
||||
if (substrate != HydrologyVoxels.SOLID) {
|
||||
evidence.invalidCaneSubstrates++;
|
||||
evidence.example("unsupported cane " + x + "," + y + "," + z);
|
||||
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);
|
||||
}
|
||||
}
|
||||
boolean wet = false;
|
||||
for (int[] offset : CARDINALS) {
|
||||
byte adjacent = volume.at(x + offset[0], y - 1, z + offset[1]);
|
||||
censored |= adjacent == HydrologyVoxels.UNKNOWN;
|
||||
wet |= HydrologyVoxels.water(adjacent);
|
||||
}
|
||||
if (ocean && channel) {
|
||||
evidence.connectedMouthComponents++;
|
||||
if (wet) {
|
||||
evidence.validCaneRoots++;
|
||||
} else if (censored) {
|
||||
evidence.censoredMouthComponents++;
|
||||
evidence.censoredCaneRoots++;
|
||||
} else {
|
||||
evidence.disconnectedMouthComponents++;
|
||||
evidence.example("mouth without connected course and ocean water "
|
||||
+ startX + "," + seaLevel + "," + startZ);
|
||||
evidence.dryCaneRoots++;
|
||||
evidence.example("dry cane root " + x + "," + y + "," + z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> failures(Evidence evidence, int maximumBankCut, Set<String> required) {
|
||||
static List<String> failures(Evidence evidence, 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.");
|
||||
failures.add("Generated sugar cane has invalid substrate or no adjacent water.");
|
||||
}
|
||||
if (evidence.missingWetVoxels > 0) {
|
||||
failures.add("Generated blocks do not retain every planned surface channel fluid voxel.");
|
||||
if (evidence.scan.missingSurfaces > 0) {
|
||||
failures.add("Generated blocks miss the planned water surface in " + evidence.scan.missingSurfaces + " columns.");
|
||||
}
|
||||
if (evidence.disconnectedMouthComponents > 0) {
|
||||
failures.add("A fully sampled mouth water component does not connect to untouched natural ocean water.");
|
||||
if (evidence.scan.containmentLeaks > 0) {
|
||||
failures.add("Generated water leaks into open ground in " + evidence.scan.containmentLeaks + " cells.");
|
||||
}
|
||||
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.");
|
||||
if (evidence.scan.tickConversionCells > 0) {
|
||||
failures.add("Fluid ticks would move generated water in " + evidence.scan.tickConversionCells + " cells.");
|
||||
}
|
||||
for (String coverage : required) {
|
||||
boolean covered = switch (coverage) {
|
||||
case "cane" -> evidence.validCaneRoots > 0;
|
||||
case "mouth" -> evidence.connectedMouthComponents > 0;
|
||||
case "channel" -> evidence.plannedWetVoxels > 0;
|
||||
case "channel" -> evidence.channelColumns > 0;
|
||||
case "pond" -> evidence.pondColumns > 0;
|
||||
case "bank" -> evidence.bankColumns > 0;
|
||||
case "cane" -> evidence.validCaneRoots > 0;
|
||||
default -> false;
|
||||
};
|
||||
if (!covered) {
|
||||
@@ -443,44 +273,84 @@ public final class HydrologyBlockProbe {
|
||||
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;
|
||||
static String json(Result result) {
|
||||
Configuration configuration = result.configuration();
|
||||
Map<String, Object> settings = new LinkedHashMap<>();
|
||||
settings.put("pack", configuration.pack().getAbsolutePath());
|
||||
settings.put("dimension", configuration.dimension());
|
||||
settings.put("seed", configuration.seed());
|
||||
settings.put("minimumChunkX", configuration.minimumChunkX());
|
||||
settings.put("maximumChunkX", configuration.maximumChunkX());
|
||||
settings.put("minimumChunkZ", configuration.minimumChunkZ());
|
||||
settings.put("maximumChunkZ", configuration.maximumChunkZ());
|
||||
settings.put("requiredCoverage", configuration.requiredCoverage());
|
||||
settings.put("studio", configuration.studio());
|
||||
Evidence evidence = result.evidence();
|
||||
Map<String, Object> counts = new LinkedHashMap<>();
|
||||
counts.put("plannedWetColumns", evidence.plannedWetColumns);
|
||||
counts.put("missingSurfaceColumns", evidence.scan.missingSurfaces);
|
||||
counts.put("channelColumns", evidence.channelColumns);
|
||||
counts.put("pondColumns", evidence.pondColumns);
|
||||
counts.put("bankColumns", evidence.bankColumns);
|
||||
counts.put("waterCells", evidence.scan.waterCells);
|
||||
counts.put("containmentLeaks", evidence.scan.containmentLeaks);
|
||||
counts.put("tickConversionCells", evidence.scan.tickConversionCells);
|
||||
counts.put("censoredCells", evidence.scan.censoredCells);
|
||||
counts.put("caneRoots", evidence.caneRoots);
|
||||
counts.put("validCaneRoots", evidence.validCaneRoots);
|
||||
counts.put("dryCaneRoots", evidence.dryCaneRoots);
|
||||
counts.put("invalidCaneSubstrates", evidence.invalidCaneSubstrates);
|
||||
counts.put("censoredCaneRoots", evidence.censoredCaneRoots);
|
||||
List<String> examples = new ArrayList<>(evidence.examples);
|
||||
examples.addAll(evidence.scan.examples);
|
||||
Map<String, Object> report = new LinkedHashMap<>();
|
||||
report.put("status", result.status());
|
||||
report.put("configuration", settings);
|
||||
report.put("seaLevel", result.seaLevel());
|
||||
report.put("evidence", counts);
|
||||
report.put("examples", examples);
|
||||
report.put("failures", result.failures());
|
||||
report.put("artifacts", result.artifacts());
|
||||
report.put("coordinateConvention", "Block X/Z are world coordinates; Y is relative to the world minimum height.");
|
||||
report.put("imageLegend", "open=light blue,water=blue,lava=orange,cane=lime,solid=gray");
|
||||
return new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
|
||||
}
|
||||
|
||||
private static List<String> writeImages(Configuration configuration, HydrologyVoxels.Volume volume, Engine engine) throws Exception {
|
||||
int minimumX = configuration.minimumChunkX() << 4;
|
||||
int minimumZ = configuration.minimumChunkZ() << 4;
|
||||
int width = (configuration.maximumChunkX() - configuration.minimumChunkX() + 1) << 4;
|
||||
int depth = (configuration.maximumChunkZ() - configuration.minimumChunkZ() + 1) << 4;
|
||||
int height = engine.getTarget().getHeight();
|
||||
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--) {
|
||||
NativeBlockState block = volume.block(x, y, z);
|
||||
if (block != null && !block.isAir()) {
|
||||
overhead.setRGB(x - volume.minimumX, z - volume.minimumZ, color(block));
|
||||
for (int z = 0; z < depth; z++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y = height - 1; y >= 0; y--) {
|
||||
byte voxel = volume.at(minimumX + x, y, minimumZ + z);
|
||||
if (voxel != HydrologyVoxels.OPEN) {
|
||||
overhead.setRGB(x, z, color(voxel));
|
||||
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<>();
|
||||
File plan = new File(configuration.output(), "generated-overhead.png");
|
||||
ImageIO.write(overhead, "png", plan);
|
||||
artifacts.add(plan.getAbsolutePath());
|
||||
int selectedX = minimumX + width / 2;
|
||||
int selectedZ = minimumZ + depth / 2;
|
||||
for (boolean alongX : new boolean[]{true, false}) {
|
||||
int span = alongX ? width : depth;
|
||||
BufferedImage section = new BufferedImage(span, volume.height, BufferedImage.TYPE_INT_RGB);
|
||||
BufferedImage section = new BufferedImage(span, 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)));
|
||||
int x = alongX ? minimumX + position : selectedX;
|
||||
int z = alongX ? selectedZ : minimumZ + position;
|
||||
for (int y = 0; y < height; y++) {
|
||||
section.setRGB(position, height - y - 1, color(volume.at(x, y, z)));
|
||||
}
|
||||
}
|
||||
File file = new File(output, alongX ? "generated-section-z-" + selectedZ + ".png"
|
||||
File file = new File(configuration.output(), alongX ? "generated-section-z-" + selectedZ + ".png"
|
||||
: "generated-section-x-" + selectedX + ".png");
|
||||
ImageIO.write(section, "png", file);
|
||||
artifacts.add(file.getAbsolutePath());
|
||||
@@ -488,42 +358,16 @@ public final class HydrologyBlockProbe {
|
||||
return List.copyOf(artifacts);
|
||||
}
|
||||
|
||||
private static int color(NativeBlockState block) {
|
||||
if (block == null || block.isAir()) {
|
||||
return 0xC9E3F0;
|
||||
}
|
||||
if (water(block)) {
|
||||
private static int color(byte voxel) {
|
||||
if (HydrologyVoxels.water(voxel)) {
|
||||
return 0x247AC4;
|
||||
}
|
||||
String key = key(block);
|
||||
if (key.equals("minecraft:lava")) {
|
||||
if (HydrologyVoxels.fluid(voxel)) {
|
||||
return 0xEC6F20;
|
||||
}
|
||||
if (key.equals("minecraft:sugar_cane")) {
|
||||
if (voxel == HydrologyVoxels.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(NativeBlockState block) {
|
||||
return block != null && (block.isWater() || block.isWaterLogged() || IMPLICIT_WATER_BLOCKS.contains(key(block)));
|
||||
}
|
||||
|
||||
private static String key(NativeBlockState block) {
|
||||
if (block == null) {
|
||||
return "minecraft:air";
|
||||
}
|
||||
String key = block.key();
|
||||
int bracket = key.indexOf('[');
|
||||
return bracket < 0 ? key : key.substring(0, bracket);
|
||||
return voxel == HydrologyVoxels.SOLID ? 0x888C90 : 0xC9E3F0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanners;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologyPlanCache;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.plan.RegionKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.RegionPlan;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlanner;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
final class HydrologyColdRuns {
|
||||
private HydrologyColdRuns() {
|
||||
}
|
||||
|
||||
record ColdRun(long nanos, long naturalSamples, long baseSamples, int depth) {
|
||||
static final ColdRun FAILED = new ColdRun(-1L, 0L, 0L, 0);
|
||||
|
||||
double millis() {
|
||||
return nanos / 1_000_000D;
|
||||
}
|
||||
}
|
||||
|
||||
static ColdRun cold(HydrologyProbe.Inputs inputs, WindowKey key) {
|
||||
HydrologySettings settings = inputs.settings();
|
||||
HydrologyPlanHarness.CountingTerrain terrain = new HydrologyPlanHarness.CountingTerrain(inputs.terrain());
|
||||
HydrologyPlanners fresh = HydrologyPlanners.create(settings, terrain);
|
||||
int chunkX = key.wx() * HydrologyWindows.CHUNKS + HydrologyWindows.CHUNKS / 2;
|
||||
int chunkZ = key.wz() * HydrologyWindows.CHUNKS + HydrologyWindows.CHUNKS / 2;
|
||||
long[] demandedSamples = new long[2];
|
||||
PlanGauge gauge = new PlanGauge(new HashSet<>(HydrologyWindows.neighbourhood(key)), () -> {
|
||||
demandedSamples[0] = terrain.natural();
|
||||
demandedSamples[1] = terrain.base();
|
||||
});
|
||||
HydrologyPlanCache cache = new HydrologyPlanCache(settings, gauge.region(fresh.regions()::plan), gauge.window(fresh.windows()::plan),
|
||||
inputs.planningPool(), () -> false);
|
||||
HydrologyRuntime runtime = new HydrologyRuntime(settings, cache, inputs.naturalHeight(), () -> false, System::nanoTime);
|
||||
long elapsed;
|
||||
try {
|
||||
long started = System.nanoTime();
|
||||
runtime.raster(chunkX, chunkZ);
|
||||
elapsed = System.nanoTime() - started;
|
||||
} finally {
|
||||
runtime.close();
|
||||
gauge.awaitIdle(inputs.planningPool());
|
||||
}
|
||||
return new ColdRun(elapsed, demandedSamples[0], demandedSamples[1], gauge.deepest());
|
||||
}
|
||||
|
||||
static final class PlanGauge {
|
||||
private final Map<Object, Integer> planning = new ConcurrentHashMap<>();
|
||||
private final AtomicInteger active = new AtomicInteger();
|
||||
private final AtomicInteger deepest = new AtomicInteger();
|
||||
private final Set<WindowKey> demanded;
|
||||
private final AtomicInteger demandedLeft;
|
||||
private final Runnable demandedPlanned;
|
||||
|
||||
PlanGauge(Set<WindowKey> demanded, Runnable demandedPlanned) {
|
||||
this.demanded = Set.copyOf(demanded);
|
||||
this.demandedLeft = new AtomicInteger(demanded.size());
|
||||
this.demandedPlanned = demandedPlanned;
|
||||
}
|
||||
|
||||
Function<RegionKey, RegionPlan> region(Function<RegionKey, RegionPlan> planner) {
|
||||
return key -> run(key, () -> planner.apply(key));
|
||||
}
|
||||
|
||||
BiFunction<WindowKey, List<RegionPlan>, WindowPlan> window(BiFunction<WindowKey, List<RegionPlan>, WindowPlan> planner) {
|
||||
return (key, plans) -> run(key, () -> planner.apply(key, plans));
|
||||
}
|
||||
|
||||
private <T> T run(Object key, Supplier<T> plan) {
|
||||
if (awaitedByRunningPlan(key)) {
|
||||
deepest.accumulateAndGet(1, Math::max);
|
||||
}
|
||||
planning.merge(key, 1, Integer::sum);
|
||||
active.incrementAndGet();
|
||||
try {
|
||||
T result = plan.get();
|
||||
if (key instanceof WindowKey window && demanded.contains(window) && demandedLeft.decrementAndGet() == 0) {
|
||||
demandedPlanned.run();
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
planning.computeIfPresent(key, (ignored, count) -> count == 1 ? null : count - 1);
|
||||
active.decrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean awaitedByRunningPlan(Object key) {
|
||||
if (!(key instanceof RegionKey region)) {
|
||||
return false;
|
||||
}
|
||||
for (Object running : planning.keySet()) {
|
||||
if (running instanceof WindowKey window && WindowPlanner.lakeRegions(window).contains(region)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int deepest() {
|
||||
return deepest.get();
|
||||
}
|
||||
|
||||
void awaitIdle(ForkJoinPool pool) {
|
||||
while (active.get() > 0 || !pool.awaitQuiescence(50L, TimeUnit.MILLISECONDS)) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,159 +0,0 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTile;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Plans a rectangle of hydrology tiles of a real pack and prints one digest per tile (the prepared-plan
|
||||
* encoding, owner resolution included) with the planning CPU. Two builds plan identically when every
|
||||
* digest matches; an expected-digest file makes the probe fail on the first difference. With a pregen
|
||||
* radius the tiles are planned the way a pregeneration of that radius plans them: prepared-plan store,
|
||||
* pregeneration scope and speculative roots.
|
||||
*/
|
||||
public final class HydrologyPlanDigestProbe {
|
||||
private static final String LOG_PREFIX = "[hydroplan]";
|
||||
|
||||
private HydrologyPlanDigestProbe() {
|
||||
}
|
||||
|
||||
public static void main(String[] arguments) throws Exception {
|
||||
if (arguments.length < 7) {
|
||||
throw new IllegalArgumentException("Expected: <pack> <dimension> <seed> <minimumTileX> <maximumTileX> "
|
||||
+ "<minimumTileZ> <maximumTileZ> [digestOut|-] [expectedDigests|-] [pregenRadius]");
|
||||
}
|
||||
File pack = new File(arguments[0]);
|
||||
String dimension = arguments[1];
|
||||
long seed = Long.parseLong(arguments[2]);
|
||||
int minimumTileX = Integer.parseInt(arguments[3]);
|
||||
int maximumTileX = Integer.parseInt(arguments[4]);
|
||||
int minimumTileZ = Integer.parseInt(arguments[5]);
|
||||
int maximumTileZ = Integer.parseInt(arguments[6]);
|
||||
Path output = arguments.length > 7 && !arguments[7].equals("-") ? Path.of(arguments[7]) : null;
|
||||
List<String> expected = arguments.length > 8 && !arguments[8].equals("-") ? Files.readAllLines(Path.of(arguments[8])) : null;
|
||||
Integer pregenRadius = arguments.length > 9 ? Integer.valueOf(arguments[9]) : null;
|
||||
List<HydrologyTileKey> keys = nearestFirst(minimumTileX, maximumTileX, minimumTileZ, maximumTileZ);
|
||||
OperatingSystemMXBean system = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
|
||||
ArrayList<String> lines = new ArrayList<>(keys.size());
|
||||
int mismatches;
|
||||
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(pack, dimension, LOG_PREFIX)) {
|
||||
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(seed, false, "plan-" + seed)) {
|
||||
Engine engine = session.engine();
|
||||
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
||||
if (runtime == null) {
|
||||
throw new IllegalStateException("Dimension '" + dimension + "' has no active hydrology runtime.");
|
||||
}
|
||||
Path store = pregenRadius == null ? null : Files.createTempDirectory("iris-hydroplan-store-");
|
||||
try {
|
||||
if (store != null) {
|
||||
runtime.enableSharedCache("probe", store);
|
||||
}
|
||||
mismatches = plan(runtime, keys, pregenRadius, expected, lines, system);
|
||||
} finally {
|
||||
if (store != null) {
|
||||
delete(store);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (output != null) {
|
||||
Files.write(output, lines, StandardCharsets.UTF_8);
|
||||
}
|
||||
System.exit(mismatches == 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
private static int plan(IrisHydrologyRuntime runtime, List<HydrologyTileKey> keys, Integer pregenRadius,
|
||||
List<String> expected, List<String> lines,
|
||||
OperatingSystemMXBean system) throws Exception {
|
||||
int mismatches = 0;
|
||||
long cpuStart = system.getProcessCpuTime();
|
||||
long wallStart = System.nanoTime();
|
||||
try (HydrologyTileCache.PregenerationScope ignored = pregenRadius == null ? null
|
||||
: runtime.preparePregeneration(pregenerationArea(pregenRadius))) {
|
||||
for (HydrologyTileKey key : keys) {
|
||||
long tileCpu = system.getProcessCpuTime();
|
||||
long tileWall = System.nanoTime();
|
||||
HydrologyTile tile = runtime.tile(key);
|
||||
String line = key.tileX() + "," + key.tileZ() + " " + digest(tile);
|
||||
System.out.printf(Locale.ROOT, "%s tile %s courses=%d columns=%d wall_ms=%d cpu_ms=%d%n", LOG_PREFIX,
|
||||
line, tile.courses().size(), tile.footprint().size(),
|
||||
(System.nanoTime() - tileWall) / 1_000_000L, (system.getProcessCpuTime() - tileCpu) / 1_000_000L);
|
||||
if (expected != null && !expected.contains(line)) {
|
||||
mismatches++;
|
||||
System.out.println(LOG_PREFIX + " MISMATCH " + line);
|
||||
}
|
||||
lines.add(line);
|
||||
}
|
||||
}
|
||||
System.out.printf(Locale.ROOT, "%s RESULT tiles=%d wall_ms=%d cpu_ms=%d mismatches=%d%n", LOG_PREFIX,
|
||||
keys.size(), (System.nanoTime() - wallStart) / 1_000_000L,
|
||||
(system.getProcessCpuTime() - cpuStart) / 1_000_000L, mismatches);
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
private static void delete(Path root) throws Exception {
|
||||
try (Stream<Path> paths = Files.walk(root)) {
|
||||
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
|
||||
Files.delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The region-aligned area a square pregeneration of the radius covers, as the pregenerator reports it. */
|
||||
static HydrologyTileCache.PregenerationArea pregenerationArea(int radius) {
|
||||
long minimum = Math.floorDiv(-radius, 512) * 512L;
|
||||
long maximum = (Math.floorDiv(radius, 512) + 1L) * 512L - 1L;
|
||||
return new HydrologyTileCache.PregenerationArea(0, 0, minimum, minimum, maximum, maximum);
|
||||
}
|
||||
|
||||
private static List<HydrologyTileKey> nearestFirst(int minimumTileX, int maximumTileX, int minimumTileZ, int maximumTileZ) {
|
||||
ArrayList<HydrologyTileKey> keys = new ArrayList<>();
|
||||
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
|
||||
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
|
||||
keys.add(new HydrologyTileKey(tileX, tileZ));
|
||||
}
|
||||
}
|
||||
keys.sort(Comparator.comparingLong((HydrologyTileKey key) -> distanceSquared(key))
|
||||
.thenComparingInt(HydrologyTileKey::tileZ).thenComparingInt(HydrologyTileKey::tileX));
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static long distanceSquared(HydrologyTileKey key) {
|
||||
long x = key.tileX() * 2L + 1L;
|
||||
long z = key.tileZ() * 2L + 1L;
|
||||
return x * x + z * z;
|
||||
}
|
||||
|
||||
private static String digest(HydrologyTile tile) throws Exception {
|
||||
Class<?> codecType = Class.forName("art.arcane.iris.generation.hydrology.HydrologyTileCodec");
|
||||
Constructor<?> constructor = codecType.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Method write = codecType.getDeclaredMethod("write", DataOutputStream.class, HydrologyTile.class, String.class);
|
||||
write.setAccessible(true);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
||||
write.invoke(constructor.newInstance(), output, tile, "probe");
|
||||
}
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes.toByteArray()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanners;
|
||||
import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer;
|
||||
import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologyPlanCodec;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.plan.IndexedWindow;
|
||||
import art.arcane.iris.generation.hydrology.plan.RegionKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.RegionPlan;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlanner;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.ReachIndex;
|
||||
import art.arcane.iris.generation.hydrology.water.Pond;
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
final class HydrologyPlanHarness {
|
||||
private static final double TILE_AREA = 1024D * 1024D;
|
||||
private static final Comparator<RegionKey> REGION_ORDER = Comparator.comparingInt(RegionKey::rz).thenComparingInt(RegionKey::rx);
|
||||
|
||||
private HydrologyPlanHarness() {
|
||||
}
|
||||
|
||||
static final class Plans {
|
||||
private final HydrologySettings settings;
|
||||
private final IntBinaryOperator naturalHeight;
|
||||
private final HydrologyWindows.WindowRange planned;
|
||||
private final CountingTerrain counting;
|
||||
private final HydrologyPlanners planners;
|
||||
private final ChunkRasterizer rasterizer;
|
||||
private final Map<RegionKey, RegionPlan> regions = new HashMap<>();
|
||||
private final Map<RegionKey, Double> regionCpu = new HashMap<>();
|
||||
private final Map<WindowKey, IndexedWindow> windows = new HashMap<>();
|
||||
private final Map<WindowKey, Double> windowCpu = new HashMap<>();
|
||||
private final List<String> failures = new ArrayList<>();
|
||||
|
||||
Plans(HydrologySettings settings, HydrologyTerrainSource terrain, IntBinaryOperator naturalHeight, HydrologyWindows.WindowRange range) {
|
||||
this.settings = settings;
|
||||
this.naturalHeight = naturalHeight;
|
||||
this.planned = range.grown(1);
|
||||
this.counting = new CountingTerrain(terrain);
|
||||
this.planners = HydrologyPlanners.create(settings, counting);
|
||||
this.rasterizer = new ChunkRasterizer(settings);
|
||||
Set<RegionKey> regionKeys = new TreeSet<>(REGION_ORDER);
|
||||
for (WindowKey key : planned.keys()) {
|
||||
regionKeys.addAll(WindowPlanner.lakeRegions(key));
|
||||
}
|
||||
for (RegionKey key : regionKeys) {
|
||||
double started = cpuMillis();
|
||||
try {
|
||||
regions.put(key, planners.regions().plan(key));
|
||||
} catch (RuntimeException failure) {
|
||||
failures.add("region " + key.rx() + "," + key.rz() + " plan: " + cause(failure));
|
||||
}
|
||||
regionCpu.put(key, cpuMillis() - started);
|
||||
}
|
||||
for (WindowKey key : planned.keys()) {
|
||||
List<RegionPlan> needed = new ArrayList<>();
|
||||
for (RegionKey region : WindowPlanner.lakeRegions(key)) {
|
||||
needed.add(regions.get(region));
|
||||
}
|
||||
if (needed.contains(null)) {
|
||||
failures.add("window " + key.wx() + "," + key.wz() + " plan: a region plan it needs failed");
|
||||
continue;
|
||||
}
|
||||
double started = cpuMillis();
|
||||
try {
|
||||
WindowPlan plan = planners.windows().plan(key, needed);
|
||||
windows.put(key, new IndexedWindow(plan, ReachIndex.build(plan.reaches(), plan.ponds())));
|
||||
} catch (RuntimeException failure) {
|
||||
failures.add("window " + key.wx() + "," + key.wz() + " plan: " + cause(failure));
|
||||
}
|
||||
windowCpu.put(key, cpuMillis() - started);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> failures() {
|
||||
return List.copyOf(failures);
|
||||
}
|
||||
|
||||
String digest(WindowKey key) {
|
||||
IndexedWindow window = windows.get(key);
|
||||
return window == null ? "failed" : digest(List.of(HydrologyPlanCodec.encode(settings, window.plan())));
|
||||
}
|
||||
|
||||
String digest() {
|
||||
List<byte[]> encoded = new ArrayList<>();
|
||||
for (WindowKey key : planned.keys()) {
|
||||
IndexedWindow window = windows.get(key);
|
||||
encoded.add(window == null ? new byte[0] : HydrologyPlanCodec.encode(settings, window.plan()));
|
||||
}
|
||||
for (RegionKey key : regionKeys()) {
|
||||
encoded.add(HydrologyPlanCodec.encode(settings, regions.get(key)));
|
||||
}
|
||||
return digest(encoded);
|
||||
}
|
||||
|
||||
private static String digest(List<byte[]> encoded) {
|
||||
try {
|
||||
MessageDigest sha = MessageDigest.getInstance("SHA-256");
|
||||
for (byte[] bytes : encoded) {
|
||||
sha.update(bytes);
|
||||
}
|
||||
return HexFormat.of().formatHex(sha.digest(), 0, 8);
|
||||
} catch (NoSuchAlgorithmException failure) {
|
||||
throw new IllegalStateException("SHA-256 is unavailable.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private List<RegionKey> regionKeys() {
|
||||
List<RegionKey> keys = new ArrayList<>(regions.keySet());
|
||||
keys.sort(REGION_ORDER);
|
||||
return keys;
|
||||
}
|
||||
|
||||
IndexedWindow window(WindowKey key) {
|
||||
return Objects.requireNonNull(windows.get(key), "window plan " + key);
|
||||
}
|
||||
|
||||
RegionPlan region(RegionKey key) {
|
||||
return Objects.requireNonNull(regions.get(key), "region plan " + key);
|
||||
}
|
||||
|
||||
Map<WindowKey, WindowPlan> windowPlans() {
|
||||
Map<WindowKey, WindowPlan> plans = new LinkedHashMap<>();
|
||||
for (WindowKey key : planned.keys()) {
|
||||
IndexedWindow window = windows.get(key);
|
||||
if (window != null) {
|
||||
plans.put(key, window.plan());
|
||||
}
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
|
||||
List<Pond> lakes() {
|
||||
List<Pond> lakes = new ArrayList<>();
|
||||
for (RegionKey key : regionKeys()) {
|
||||
lakes.addAll(regions.get(key).lakes());
|
||||
}
|
||||
return lakes;
|
||||
}
|
||||
|
||||
double windowCpu(WindowKey key) {
|
||||
return windowCpu.getOrDefault(key, 0D);
|
||||
}
|
||||
|
||||
double regionCpu(RegionKey key) {
|
||||
return regionCpu.getOrDefault(key, 0D);
|
||||
}
|
||||
|
||||
double meanRegionCpu() {
|
||||
double total = 0D;
|
||||
for (double cpu : regionCpu.values()) {
|
||||
total += cpu;
|
||||
}
|
||||
return regionCpu.isEmpty() ? 0D : total / regionCpu.size();
|
||||
}
|
||||
|
||||
int regionCount() {
|
||||
return regions.size();
|
||||
}
|
||||
|
||||
int windowCount() {
|
||||
return windows.size();
|
||||
}
|
||||
|
||||
long naturalSamples() {
|
||||
return counting.natural();
|
||||
}
|
||||
|
||||
long baseSamples() {
|
||||
return counting.base();
|
||||
}
|
||||
|
||||
double landTiles(WindowKey key) {
|
||||
int cells = WindowKey.SIZE / WindowPlanner.CELL;
|
||||
HydrologySampleBuffer samples = new HydrologySampleBuffer(cells * cells);
|
||||
planners.windowLattice().fill(key.wx() * cells, key.wz() * cells, cells, cells, samples);
|
||||
int land = 0;
|
||||
for (int cell = 0; cell < cells * cells; cell++) {
|
||||
if (!samples.ocean()[cell]) {
|
||||
land++;
|
||||
}
|
||||
}
|
||||
return land * (double) (WindowPlanner.CELL * WindowPlanner.CELL) / TILE_AREA;
|
||||
}
|
||||
|
||||
ChunkWaterRaster rasterize(int chunkX, int chunkZ) {
|
||||
WindowKey own = WindowKey.containing(chunkX << 4, chunkZ << 4);
|
||||
List<IndexedWindow> needed = new ArrayList<>(9);
|
||||
for (WindowKey key : HydrologyWindows.neighbourhood(own)) {
|
||||
IndexedWindow window = windows.get(key);
|
||||
if (window == null) {
|
||||
throw new IllegalStateException("Chunk " + chunkX + "," + chunkZ + " needs window " + key + ", which the probe did not plan.");
|
||||
}
|
||||
needed.add(window);
|
||||
}
|
||||
List<RegionPlan> lakes = new ArrayList<>();
|
||||
for (RegionKey region : ChunkRasterizer.lakeRegions(chunkX, chunkZ)) {
|
||||
lakes.add(Objects.requireNonNull(regions.get(region), "region plan " + region));
|
||||
}
|
||||
return rasterizer.rasterize(chunkX, chunkZ, naturalHeight, needed, lakes);
|
||||
}
|
||||
|
||||
HydrologySettings settings() {
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
static final class CountingTerrain implements HydrologyTerrainSource {
|
||||
private final HydrologyTerrainSource delegate;
|
||||
private final AtomicLong natural = new AtomicLong();
|
||||
private final AtomicLong base = new AtomicLong();
|
||||
|
||||
CountingTerrain(HydrologyTerrainSource delegate) {
|
||||
this.delegate = Objects.requireNonNull(delegate, "delegate");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sample(int[] xs, int[] zs, HydrologySampleBuffer out, boolean baseOnly) {
|
||||
(baseOnly ? base : natural).addAndGet(xs.length);
|
||||
delegate.sample(xs, zs, out, baseOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int seaLevel() {
|
||||
return delegate.seaLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int minY() {
|
||||
return delegate.minY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int maxY() {
|
||||
return delegate.maxY();
|
||||
}
|
||||
|
||||
long natural() {
|
||||
return natural.get();
|
||||
}
|
||||
|
||||
long base() {
|
||||
return base.get();
|
||||
}
|
||||
}
|
||||
|
||||
static final class RasterCache implements HydrologyRasterMetrics.Rasters {
|
||||
private final int capacity;
|
||||
private final BiFunction<Integer, Integer, ChunkWaterRaster> compute;
|
||||
private final LinkedHashMap<Long, ChunkWaterRaster> entries;
|
||||
|
||||
RasterCache(int capacity, BiFunction<Integer, Integer, ChunkWaterRaster> compute) {
|
||||
this.capacity = capacity;
|
||||
this.compute = compute;
|
||||
this.entries = new LinkedHashMap<>(capacity * 2, 0.75f, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkWaterRaster get(int chunkX, int chunkZ) {
|
||||
long key = HydrologyVoxels.chunkKey(chunkX, chunkZ);
|
||||
synchronized (entries) {
|
||||
ChunkWaterRaster known = entries.get(key);
|
||||
if (known != null) {
|
||||
return known;
|
||||
}
|
||||
}
|
||||
ChunkWaterRaster raster = compute.apply(chunkX, chunkZ);
|
||||
synchronized (entries) {
|
||||
entries.put(key, raster);
|
||||
if (entries.size() > capacity) {
|
||||
Long eldest = entries.keySet().iterator().next();
|
||||
entries.remove(eldest);
|
||||
}
|
||||
}
|
||||
return raster;
|
||||
}
|
||||
}
|
||||
|
||||
static String cause(Throwable failure) {
|
||||
Throwable root = failure;
|
||||
while (root.getCause() != null && root.getCause() != root) {
|
||||
root = root.getCause();
|
||||
}
|
||||
return root.getClass().getSimpleName() + ": " + root.getMessage();
|
||||
}
|
||||
|
||||
static double cpuMillis() {
|
||||
return ((OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean()).getProcessCpuTime() / 1_000_000D;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
|
||||
import art.arcane.iris.generation.hydrology.water.Pond;
|
||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
final class HydrologyPlanMetrics {
|
||||
static final float RIVER_LENGTH = 384f;
|
||||
static final int PERCHED_HEIGHT = 8;
|
||||
private static final int PERCHED_SEARCH = 1024;
|
||||
private static final int VERTEX_BUCKET = 64;
|
||||
|
||||
private final HydrologySettings settings;
|
||||
private final Map<WindowKey, WindowPlan> windows;
|
||||
private final List<Pond> lakes;
|
||||
private final HydrologyWindows.WindowRange range;
|
||||
private final Consumer<String> examples;
|
||||
private final Map<Long, Reach> reaches = new LinkedHashMap<>();
|
||||
private final Map<Long, WindowKey> reachOwners = new HashMap<>();
|
||||
private final Map<Long, WindowKey> pondOwners = new HashMap<>();
|
||||
private final Map<WindowKey, Long> seams = new HashMap<>();
|
||||
private final Map<Long, Pond> endPonds = new HashMap<>();
|
||||
private final Map<Long, Set<Long>> pondReaches = new HashMap<>();
|
||||
private final Map<WindowKey, Integer> rivers;
|
||||
private final VertexIndex vertices;
|
||||
|
||||
HydrologyPlanMetrics(HydrologySettings settings, Map<WindowKey, WindowPlan> windows, List<Pond> lakes, HydrologyWindows.WindowRange range,
|
||||
Consumer<String> examples) {
|
||||
this.settings = settings;
|
||||
this.windows = windows;
|
||||
this.lakes = List.copyOf(lakes);
|
||||
this.range = range;
|
||||
this.examples = examples;
|
||||
for (Map.Entry<WindowKey, WindowPlan> entry : windows.entrySet()) {
|
||||
WindowKey key = entry.getKey();
|
||||
for (Reach reach : entry.getValue().reaches()) {
|
||||
reaches.putIfAbsent(reach.id(), reach);
|
||||
WindowKey owner = reachOwners.putIfAbsent(reach.id(), key);
|
||||
if (owner != null) {
|
||||
seam(key, "reach " + reach.id() + " owned by " + owner + " and " + key);
|
||||
}
|
||||
}
|
||||
for (Pond pond : entry.getValue().ponds()) {
|
||||
WindowKey owner = pondOwners.putIfAbsent(pond.id(), key);
|
||||
if (owner != null) {
|
||||
seam(key, "pond " + pond.id() + " owned by " + owner + " and " + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
linkSeams();
|
||||
pondSeams();
|
||||
endPonds();
|
||||
this.rivers = stems();
|
||||
this.vertices = new VertexIndex(reaches.values());
|
||||
}
|
||||
|
||||
Map<Long, Reach> reaches() {
|
||||
return reaches;
|
||||
}
|
||||
|
||||
Pond endPond(Reach reach) {
|
||||
return endPonds.get(reach.id());
|
||||
}
|
||||
|
||||
long seams(WindowKey key) {
|
||||
return seams.getOrDefault(key, 0L);
|
||||
}
|
||||
|
||||
int rivers(WindowKey key) {
|
||||
return rivers.getOrDefault(key, 0);
|
||||
}
|
||||
|
||||
double streams(WindowKey key) {
|
||||
double streams = 0D;
|
||||
for (Reach reach : windows.get(key).reaches()) {
|
||||
if (!river(reach)) {
|
||||
streams += length(reach);
|
||||
}
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
int ponds(WindowKey key) {
|
||||
return windows.get(key).ponds().size();
|
||||
}
|
||||
|
||||
Set<Integer> levels(WindowKey key) {
|
||||
Set<Integer> levels = new TreeSet<>();
|
||||
for (Pond pond : windows.get(key).ponds()) {
|
||||
levels.add(pond.level());
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
|
||||
int perched(WindowKey key) {
|
||||
int perched = 0;
|
||||
for (Pond pond : windows.get(key).ponds()) {
|
||||
Set<Long> connected = pondReaches.getOrDefault(pond.id(), Set.of());
|
||||
int grade = vertices.nearestGrade(pond.floorX(), pond.floorZ(), settings.seaLevel(), connected);
|
||||
if (pond.level() >= grade + PERCHED_HEIGHT) {
|
||||
perched++;
|
||||
}
|
||||
}
|
||||
return perched;
|
||||
}
|
||||
|
||||
private void seam(WindowKey owner, String what) {
|
||||
if (range.contains(owner)) {
|
||||
seams.merge(owner, 1L, Long::sum);
|
||||
examples.accept("seam " + what);
|
||||
}
|
||||
}
|
||||
|
||||
private void linkSeams() {
|
||||
for (Reach reach : reaches.values()) {
|
||||
if (reach.downstreamId() != 0L && !reachOwners.containsKey(reach.downstreamId())) {
|
||||
seam(reachOwners.get(reach.id()), "reach " + reach.id() + " links to missing reach " + reach.downstreamId());
|
||||
}
|
||||
}
|
||||
for (Map.Entry<WindowKey, WindowPlan> entry : windows.entrySet()) {
|
||||
for (Pond pond : entry.getValue().ponds()) {
|
||||
if (pond.outletReachId() != 0L && !reachOwners.containsKey(pond.outletReachId())) {
|
||||
seam(entry.getKey(), "pond " + pond.id() + " drains to missing reach " + pond.outletReachId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void pondSeams() {
|
||||
List<WindowKey> keys = new ArrayList<>(windows.keySet());
|
||||
for (WindowKey key : keys) {
|
||||
for (WindowKey other : keys) {
|
||||
if (other.wx() < key.wx() - 1 || other.wx() > key.wx() + 1 || other.wz() < key.wz() - 1 || other.wz() > key.wz() + 1
|
||||
|| compare(other, key) <= 0) {
|
||||
continue;
|
||||
}
|
||||
for (Pond pond : windows.get(key).ponds()) {
|
||||
for (Pond neighbour : windows.get(other).ponds()) {
|
||||
if (pond.mask().overlaps(neighbour.mask())) {
|
||||
seam(range.contains(other) ? other : key, "ponds " + pond.id() + " and " + neighbour.id() + " from " + key
|
||||
+ " and " + other + " overlap");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int compare(WindowKey left, WindowKey right) {
|
||||
return left.wz() != right.wz() ? Integer.compare(left.wz(), right.wz()) : Integer.compare(left.wx(), right.wx());
|
||||
}
|
||||
|
||||
private void endPonds() {
|
||||
List<Pond> ponds = new ArrayList<>();
|
||||
for (WindowPlan plan : windows.values()) {
|
||||
ponds.addAll(plan.ponds());
|
||||
}
|
||||
ponds.addAll(lakes);
|
||||
for (Pond pond : ponds) {
|
||||
if (pond.outletReachId() != 0L) {
|
||||
pondReaches.computeIfAbsent(pond.id(), ignored -> new HashSet<>()).add(pond.outletReachId());
|
||||
}
|
||||
}
|
||||
for (Reach reach : reaches.values()) {
|
||||
if (reach.downstreamId() != 0L) {
|
||||
continue;
|
||||
}
|
||||
int last = reach.xs().length - 1;
|
||||
int x = (int) Math.floor(reach.xs()[last]);
|
||||
int z = (int) Math.floor(reach.zs()[last]);
|
||||
for (Pond pond : ponds) {
|
||||
if (pond.mask().contains(x, z)) {
|
||||
endPonds.put(reach.id(), pond);
|
||||
pondReaches.computeIfAbsent(pond.id(), ignored -> new HashSet<>()).add(reach.id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Reach onward(Reach reach) {
|
||||
if (reach.downstreamId() != 0L) {
|
||||
return reaches.get(reach.downstreamId());
|
||||
}
|
||||
Pond pond = endPonds.get(reach.id());
|
||||
return pond == null ? null : reaches.get(pond.outletReachId());
|
||||
}
|
||||
|
||||
private Map<WindowKey, Integer> stems() {
|
||||
Map<Long, Reach> mainFeeder = new HashMap<>();
|
||||
for (Reach reach : reaches.values()) {
|
||||
Reach next = onward(reach);
|
||||
if (!river(reach) || next == null) {
|
||||
continue;
|
||||
}
|
||||
mainFeeder.merge(next.id(), reach, (current, candidate) -> candidate.catchment() > current.catchment()
|
||||
|| candidate.catchment() == current.catchment() && candidate.id() < current.id() ? candidate : current);
|
||||
}
|
||||
Map<WindowKey, Integer> counts = new HashMap<>();
|
||||
for (Reach reach : reaches.values()) {
|
||||
if (!river(reach)) {
|
||||
continue;
|
||||
}
|
||||
Reach next = onward(reach);
|
||||
if (next != null && river(next) && mainFeeder.get(next.id()) == reach) {
|
||||
continue;
|
||||
}
|
||||
float length = 0f;
|
||||
Set<Long> seen = new HashSet<>();
|
||||
for (Reach stem = reach; stem != null && seen.add(stem.id()); stem = mainFeeder.get(stem.id())) {
|
||||
length += length(stem);
|
||||
}
|
||||
WindowKey owner = reachOwners.get(reach.id());
|
||||
if (length >= RIVER_LENGTH && range.contains(owner)) {
|
||||
counts.merge(owner, 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
private boolean river(Reach reach) {
|
||||
return !reach.tier().equals(settings.tiers().get(0));
|
||||
}
|
||||
|
||||
static float length(Reach reach) {
|
||||
float length = 0f;
|
||||
for (int point = 1; point < reach.xs().length; point++) {
|
||||
length += (float) Math.hypot(reach.xs()[point] - reach.xs()[point - 1], reach.zs()[point] - reach.zs()[point - 1]);
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
private static final class VertexIndex {
|
||||
private final Map<Long, List<long[]>> buckets = new HashMap<>();
|
||||
|
||||
private VertexIndex(Iterable<Reach> reaches) {
|
||||
for (Reach reach : reaches) {
|
||||
for (int point = 0; point < reach.xs().length; point++) {
|
||||
int x = (int) Math.floor(reach.xs()[point]);
|
||||
int z = (int) Math.floor(reach.zs()[point]);
|
||||
buckets.computeIfAbsent(bucket(Math.floorDiv(x, VERTEX_BUCKET), Math.floorDiv(z, VERTEX_BUCKET)), ignored -> new ArrayList<>())
|
||||
.add(new long[]{x, z, reach.grade()[point], reach.id()});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int nearestGrade(int x, int z, int fallback, Set<Long> excluded) {
|
||||
int bx = Math.floorDiv(x, VERTEX_BUCKET);
|
||||
int bz = Math.floorDiv(z, VERTEX_BUCKET);
|
||||
long best = Long.MAX_VALUE;
|
||||
int grade = fallback;
|
||||
int rings = PERCHED_SEARCH / VERTEX_BUCKET;
|
||||
for (int ring = 0; ring <= rings; ring++) {
|
||||
long reach = (long) Math.max(0, ring - 1) * VERTEX_BUCKET;
|
||||
if (best != Long.MAX_VALUE && best < reach * reach) {
|
||||
break;
|
||||
}
|
||||
for (int dz = -ring; dz <= ring; dz++) {
|
||||
for (int dx = -ring; dx <= ring; dx++) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != ring) {
|
||||
continue;
|
||||
}
|
||||
for (long[] vertex : buckets.getOrDefault(bucket(bx + dx, bz + dz), List.of())) {
|
||||
if (excluded.contains(vertex[3])) {
|
||||
continue;
|
||||
}
|
||||
long distance = (vertex[0] - x) * (vertex[0] - x) + (vertex[1] - z) * (vertex[1] - z);
|
||||
if (distance < best) {
|
||||
best = distance;
|
||||
grade = (int) vertex[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return grade;
|
||||
}
|
||||
|
||||
private static long bucket(int bx, int bz) {
|
||||
return ((long) bx << 32) | (bz & 0xFFFFFFFFL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,629 +0,0 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyGeometrySampler;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTile;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxel;
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public final class HydrologyPlannerProbe {
|
||||
private static final long[] DEFAULT_SEEDS = {1L, 19L, 52L, 77L, 331L, 501L, 712L, 992L};
|
||||
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)));
|
||||
private static final HydrologyTileKey ORIGIN = new HydrologyTileKey(0, 0);
|
||||
private static final int WARM_SAMPLES = 2_048;
|
||||
|
||||
private HydrologyPlannerProbe() {
|
||||
}
|
||||
|
||||
public static void main(String[] arguments) {
|
||||
long[] seeds = parseSeeds(arguments);
|
||||
long[] coldTimings = new long[seeds.length];
|
||||
long[] warmTimings = new long[seeds.length];
|
||||
long signature = 0xCBF29CE484222325L;
|
||||
EnumMap<HydrologyFeatureType, HydrologyFeatureRef> firstFeatures =
|
||||
new EnumMap<>(HydrologyFeatureType.class);
|
||||
|
||||
for (int seedIndex = 0; seedIndex < seeds.length; seedIndex++) {
|
||||
long seed = seeds[seedIndex];
|
||||
HydrologyPlanner planner = new HydrologyPlanner(seed, settings(), terrain(seed));
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 32);
|
||||
long coldStart = System.nanoTime();
|
||||
HydrologyTile tile = cache.get(ORIGIN);
|
||||
coldTimings[seedIndex] = System.nanoTime() - coldStart;
|
||||
HydrologyTile repeated = new HydrologyPlanner(seed, settings(), terrain(seed)).plan(ORIGIN);
|
||||
if (!tile.equals(repeated)) {
|
||||
throw new IllegalStateException("Hydrology plan changed across deterministic replay for seed " + seed);
|
||||
}
|
||||
|
||||
EnumMap<HydrologyFeatureType, Integer> counts = new EnumMap<>(HydrologyFeatureType.class);
|
||||
for (HydrologyFeatureRef feature : tile.features()) {
|
||||
counts.merge(feature.type(), 1, Integer::sum);
|
||||
signature = mix(signature, feature.id());
|
||||
signature = mix(signature, feature.type().ordinal());
|
||||
signature = mix(signature, feature.x());
|
||||
signature = mix(signature, feature.y());
|
||||
signature = mix(signature, feature.z());
|
||||
}
|
||||
collectAccepted(firstFeatures, tile);
|
||||
|
||||
long warmStart = System.nanoTime();
|
||||
for (int sampleIndex = 0; sampleIndex < WARM_SAMPLES; sampleIndex++) {
|
||||
int x = Math.floorMod(sampleIndex * 1_229, 256);
|
||||
int z = Math.floorMod(sampleIndex * 811, 256);
|
||||
Optional<HydrologyColumnSample> sample = cache.columnAt(x, z);
|
||||
signature = mix(signature, sample.map(HydrologyColumnSample::layers).map(List::size).orElse(0));
|
||||
}
|
||||
warmTimings[seedIndex] = System.nanoTime() - warmStart;
|
||||
System.out.printf(
|
||||
Locale.ROOT,
|
||||
"IRIS_HYDROLOGY_SEED seed=%d cold_nanos=%d warm_nanos=%d courses=%d features=%d diagnostics=%d counts=%s%n",
|
||||
seed,
|
||||
coldTimings[seedIndex],
|
||||
warmTimings[seedIndex],
|
||||
tile.courses().size(),
|
||||
tile.features().size(),
|
||||
cache.diagnosticCandidates(tile.key()).size(),
|
||||
counts
|
||||
);
|
||||
cache.close();
|
||||
}
|
||||
|
||||
collectAccepted(firstFeatures, surfaceInlandTile());
|
||||
collectAccepted(firstFeatures, undergroundInlandTile());
|
||||
collectAccepted(firstFeatures, surfaceMouthTile());
|
||||
collectAccepted(firstFeatures, surfaceWaterfallTile());
|
||||
collectAccepted(firstFeatures, coastalGrottoTile());
|
||||
requireCompleteCoverage(firstFeatures.keySet());
|
||||
|
||||
for (Map.Entry<HydrologyFeatureType, HydrologyFeatureRef> entry : firstFeatures.entrySet()) {
|
||||
HydrologyFeatureRef feature = entry.getValue();
|
||||
System.out.printf(
|
||||
Locale.ROOT,
|
||||
"IRIS_HYDROLOGY_COORDINATE type=%s x=%d y=%d z=%d id=%016x%n",
|
||||
entry.getKey(),
|
||||
feature.x(),
|
||||
feature.y(),
|
||||
feature.z(),
|
||||
feature.id()
|
||||
);
|
||||
}
|
||||
|
||||
long[] sortedCold = coldTimings.clone();
|
||||
long[] sortedWarm = warmTimings.clone();
|
||||
Arrays.sort(sortedCold);
|
||||
Arrays.sort(sortedWarm);
|
||||
System.out.printf(
|
||||
Locale.ROOT,
|
||||
"IRIS_HYDROLOGY_RESULT version=1 seeds=%d warm_samples=%d cold_median_nanos=%d cold_p95_nanos=%d warm_median_nanos=%d warm_p95_nanos=%d feature_types=%d signature=%016x%n",
|
||||
seeds.length,
|
||||
WARM_SAMPLES,
|
||||
percentile(sortedCold, 0.5D),
|
||||
percentile(sortedCold, 0.95D),
|
||||
percentile(sortedWarm, 0.5D),
|
||||
percentile(sortedWarm, 0.95D),
|
||||
firstFeatures.size(),
|
||||
signature
|
||||
);
|
||||
}
|
||||
|
||||
static Map<HydrologyFeatureType, HydrologyFeatureRef> deterministicAcceptedCoverage() {
|
||||
EnumMap<HydrologyFeatureType, HydrologyFeatureRef> coverage = new EnumMap<>(HydrologyFeatureType.class);
|
||||
for (long seed : DEFAULT_SEEDS) {
|
||||
collectAccepted(
|
||||
coverage,
|
||||
new HydrologyPlanner(seed, settings(), terrain(seed)).plan(ORIGIN)
|
||||
);
|
||||
}
|
||||
collectAccepted(coverage, surfaceInlandTile());
|
||||
collectAccepted(coverage, undergroundInlandTile());
|
||||
collectAccepted(coverage, surfaceMouthTile());
|
||||
collectAccepted(coverage, surfaceWaterfallTile());
|
||||
collectAccepted(coverage, coastalGrottoTile());
|
||||
requireCompleteCoverage(coverage.keySet());
|
||||
return Map.copyOf(coverage);
|
||||
}
|
||||
|
||||
private static void collectAccepted(
|
||||
Map<HydrologyFeatureType, HydrologyFeatureRef> features,
|
||||
HydrologyTile tile
|
||||
) {
|
||||
for (HydrologyFeatureRef feature : tile.features()) {
|
||||
features.putIfAbsent(feature.type(), feature);
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireCompleteCoverage(Set<HydrologyFeatureType> observed) {
|
||||
EnumSet<HydrologyFeatureType> missing = EnumSet.copyOf(REQUIRED_FEATURE_TYPES);
|
||||
missing.removeAll(observed);
|
||||
if (!missing.isEmpty()) {
|
||||
throw new IllegalStateException("Hydrology accepted-feature coverage is missing " + missing + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyTile surfaceInlandTile() {
|
||||
return new HydrologyPlanner(
|
||||
SURFACE_INLAND_SEED,
|
||||
inlandSettings(0D, 0D),
|
||||
inlandTerrain(true, false, false),
|
||||
solidCaveView()
|
||||
).plan(ORIGIN);
|
||||
}
|
||||
|
||||
private static HydrologyTile undergroundInlandTile() {
|
||||
return new HydrologyPlanner(
|
||||
UNDERGROUND_INLAND_SEED,
|
||||
inlandSettings(0D, 1D),
|
||||
inlandTerrain(false, true, false),
|
||||
solidCaveView()
|
||||
).plan(ORIGIN);
|
||||
}
|
||||
|
||||
static HydrologyTile surfaceMouthTile() {
|
||||
return new HydrologyPlanner(
|
||||
COASTAL_SEED,
|
||||
settings(false),
|
||||
coastalTerrain(false)
|
||||
).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,
|
||||
settings(true),
|
||||
coastalTerrain(true),
|
||||
solidCaveView()
|
||||
).plan(ORIGIN);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings inlandSettings(
|
||||
double surfaceDensity,
|
||||
double undergroundDensity
|
||||
) {
|
||||
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
|
||||
true,
|
||||
surfaceDensity,
|
||||
80,
|
||||
0,
|
||||
6,
|
||||
24
|
||||
);
|
||||
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(
|
||||
true,
|
||||
undergroundDensity,
|
||||
Integer.MIN_VALUE,
|
||||
0,
|
||||
4,
|
||||
32
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
63,
|
||||
new HydrologyPlannerSettings.Routing(
|
||||
128,
|
||||
16,
|
||||
512,
|
||||
256,
|
||||
32,
|
||||
16,
|
||||
0.5D,
|
||||
12D,
|
||||
0.5D,
|
||||
0.1D,
|
||||
1D,
|
||||
0, HydrologyPlannerSettings.Regional.disabled()
|
||||
),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
surfaceSources,
|
||||
4,
|
||||
18,
|
||||
2,
|
||||
4,
|
||||
10,
|
||||
1.5D,
|
||||
HydrologyPlannerSettings.Banks.defaults()
|
||||
),
|
||||
new HydrologyPlannerSettings.Hydraulics(4),
|
||||
HydrologyPlannerSettings.Underground.of(
|
||||
undergroundDensity > 0D,
|
||||
undergroundSources,
|
||||
68,
|
||||
82,
|
||||
4,
|
||||
12,
|
||||
2,
|
||||
4,
|
||||
5,
|
||||
9,
|
||||
true,
|
||||
0
|
||||
),
|
||||
HydrologyPlannerSettings.Outlets.of(
|
||||
false,
|
||||
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
|
||||
true,
|
||||
12,
|
||||
32,
|
||||
2,
|
||||
4,
|
||||
4
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled(),
|
||||
HydrologyPlannerSettings.SurfacePolicyBounds.NONE
|
||||
);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler inlandTerrain(
|
||||
boolean surfaceSource,
|
||||
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) - (waterfall && x + z >= 64 ? 10 : 0);
|
||||
return new HydrologyTerrainSample(
|
||||
height,
|
||||
1D,
|
||||
false,
|
||||
true,
|
||||
72,
|
||||
74,
|
||||
true,
|
||||
outlet,
|
||||
surfaceSource && source,
|
||||
surfaceSource && source,
|
||||
undergroundSource && source,
|
||||
undergroundSource && source,
|
||||
0D,
|
||||
surfaceSource && source ? 1D : 0D,
|
||||
undergroundSource && source ? 1D : 0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"parent",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
,
|
||||
Double.NaN,
|
||||
null, Double.NaN, true,
|
||||
SurfaceRiverPolicy.INHERIT
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler coastalTerrain(boolean cliffCoast) {
|
||||
return (int x, int z) -> {
|
||||
if (x >= 208) {
|
||||
return oceanTerrain();
|
||||
}
|
||||
int height;
|
||||
double slope;
|
||||
if (!cliffCoast) {
|
||||
height = 100 - Math.floorDiv(x, 6);
|
||||
slope = 1D;
|
||||
} else if (x < 104) {
|
||||
height = 100 - Math.floorDiv(x, 8);
|
||||
slope = 1D;
|
||||
} else {
|
||||
height = 84 - Math.floorDiv(x - 104, 26);
|
||||
slope = 1D;
|
||||
}
|
||||
boolean surfaceSource = x <= 40;
|
||||
boolean undergroundSource = x <= 64;
|
||||
return new HydrologyTerrainSample(
|
||||
height,
|
||||
slope,
|
||||
false,
|
||||
true,
|
||||
height - 30,
|
||||
height - 28,
|
||||
true,
|
||||
true,
|
||||
surfaceSource,
|
||||
surfaceSource,
|
||||
undergroundSource,
|
||||
undergroundSource,
|
||||
0D,
|
||||
surfaceSource ? 1D : 0D,
|
||||
undergroundSource ? 1D : 0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"parent",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"bank",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
,
|
||||
Double.NaN,
|
||||
null, Double.NaN, true,
|
||||
SurfaceRiverPolicy.INHERIT
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static CaveVoxelView solidCaveView() {
|
||||
return new CaveVoxelView() {
|
||||
@Override
|
||||
public boolean isInWorld(CavePosition position) {
|
||||
return position.y() > -4096 && position.y() < 4096;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxel voxelAt(CavePosition position) {
|
||||
return CaveVoxel.SOLID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpenToSurface(CavePosition position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAboveTerrainSurface(CavePosition position) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings settings() {
|
||||
return settings(true);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings settings(boolean coastalGrottosEnabled) {
|
||||
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
|
||||
true,
|
||||
4D,
|
||||
80,
|
||||
1,
|
||||
6,
|
||||
32
|
||||
);
|
||||
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(
|
||||
true,
|
||||
4D,
|
||||
Integer.MIN_VALUE,
|
||||
1,
|
||||
4,
|
||||
48
|
||||
);
|
||||
HydrologyPlannerSettings.DeepFluid deepFluid = new HydrologyPlannerSettings.DeepFluid(
|
||||
"deep_lava",
|
||||
true,
|
||||
3D,
|
||||
48,
|
||||
18,
|
||||
42,
|
||||
3,
|
||||
5,
|
||||
2,
|
||||
3,
|
||||
8,
|
||||
24,
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
4096,
|
||||
4,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
63,
|
||||
new HydrologyPlannerSettings.Routing(
|
||||
256,
|
||||
16,
|
||||
1_089,
|
||||
256,
|
||||
32,
|
||||
16,
|
||||
0.5D,
|
||||
12D,
|
||||
0.5D,
|
||||
0.2D,
|
||||
1D,
|
||||
0, HydrologyPlannerSettings.Regional.disabled()
|
||||
),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
surfaceSources,
|
||||
4,
|
||||
20,
|
||||
2,
|
||||
5,
|
||||
12,
|
||||
1.5D,
|
||||
HydrologyPlannerSettings.Banks.defaults()
|
||||
),
|
||||
new HydrologyPlannerSettings.Hydraulics(4),
|
||||
HydrologyPlannerSettings.Underground.of(
|
||||
true,
|
||||
undergroundSources,
|
||||
68,
|
||||
82,
|
||||
4,
|
||||
14,
|
||||
2,
|
||||
5,
|
||||
6,
|
||||
12,
|
||||
true,
|
||||
0
|
||||
),
|
||||
HydrologyPlannerSettings.Outlets.of(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Grotto(coastalGrottosEnabled, 4, 3, 3, 4096),
|
||||
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
|
||||
true,
|
||||
12,
|
||||
48,
|
||||
2,
|
||||
8,
|
||||
8
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(deepFluid), List.of(),
|
||||
0D,
|
||||
HydrologyPlannerSettings.SeaCaves.disabled(),
|
||||
HydrologyPlannerSettings.SurfacePolicyBounds.NONE
|
||||
);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler terrain(long seed) {
|
||||
return (int x, int z) -> {
|
||||
if (x >= 224) {
|
||||
return oceanTerrain();
|
||||
}
|
||||
double wave = StrictMath.sin((z + seed % 37L) / 19D) * 3D;
|
||||
double ridge = x >= 96 && x <= 128 ? 34D : 0D;
|
||||
double cliff = x >= 144 ? -24D : 0D;
|
||||
int height = 126 - Math.floorDiv(x, 13) + (int) StrictMath.round(wave + ridge + cliff);
|
||||
double slope = ridge > 0D || x >= 136 && x <= 144
|
||||
? 18D
|
||||
: 1D + StrictMath.abs(wave) * 0.25D;
|
||||
boolean surfaceSource = x >= 0 && x <= 48;
|
||||
boolean undergroundSource = x >= 0 && x <= 64;
|
||||
return new HydrologyTerrainSample(
|
||||
height,
|
||||
slope,
|
||||
false,
|
||||
true,
|
||||
70,
|
||||
74 + Math.floorMod(x + z, 7),
|
||||
true,
|
||||
true,
|
||||
surfaceSource,
|
||||
surfaceSource,
|
||||
undergroundSource,
|
||||
undergroundSource,
|
||||
0D,
|
||||
surfaceSource ? 1D : 0D,
|
||||
undergroundSource ? 1D : 0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"parent",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water"), List.of(),
|
||||
Double.NaN,
|
||||
null,
|
||||
Double.NaN,
|
||||
true,
|
||||
SurfaceRiverPolicy.INHERIT
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample oceanTerrain() {
|
||||
return new HydrologyTerrainSample(
|
||||
54,
|
||||
0D,
|
||||
true,
|
||||
false,
|
||||
30,
|
||||
32,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"ocean_parent",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water"), List.of()
|
||||
,
|
||||
Double.NaN,
|
||||
null, Double.NaN, true,
|
||||
SurfaceRiverPolicy.INHERIT
|
||||
);
|
||||
}
|
||||
|
||||
private static long[] parseSeeds(String[] arguments) {
|
||||
if (arguments.length == 0) {
|
||||
return DEFAULT_SEEDS.clone();
|
||||
}
|
||||
long[] seeds = new long[arguments.length];
|
||||
for (int index = 0; index < arguments.length; index++) {
|
||||
seeds[index] = Long.parseLong(arguments[index]);
|
||||
}
|
||||
return seeds;
|
||||
}
|
||||
|
||||
private static long percentile(long[] sorted, double percentile) {
|
||||
int index = Math.max(0, (int) StrictMath.ceil(sorted.length * percentile) - 1);
|
||||
return sorted[index];
|
||||
}
|
||||
|
||||
private static long mix(long hash, long value) {
|
||||
return (hash ^ value) * 0x100000001B3L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.hydrology.config.IrisHydrology;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyBinding;
|
||||
import art.arcane.iris.generation.hydrology.engine.IrisHydrologyTerrainSource;
|
||||
import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
public final class HydrologyProbe {
|
||||
private static final int RASTER_CACHE = 8192;
|
||||
private static final int WARMUP_OFFSET = 8;
|
||||
private static final int MAXIMUM_WINDOWS = 256;
|
||||
|
||||
private HydrologyProbe() {
|
||||
}
|
||||
|
||||
record Configuration(HydrologyWindows.Target target, int threads) {
|
||||
Configuration {
|
||||
Objects.requireNonNull(target, "target");
|
||||
if (target.windows().count() > MAXIMUM_WINDOWS) {
|
||||
throw new IllegalArgumentException("The probe measures at most " + MAXIMUM_WINDOWS + " windows, got " + target.windows().count() + ".");
|
||||
}
|
||||
if (threads < 1) {
|
||||
throw new IllegalArgumentException("Threads must be positive.");
|
||||
}
|
||||
}
|
||||
|
||||
static Configuration parse(String[] args) {
|
||||
if (args.length != 6) {
|
||||
throw new IllegalArgumentException("Expected <pack> <dimension|-> <seed> <x0..x1,z0..z1> <threads> <studio>.");
|
||||
}
|
||||
return new Configuration(HydrologyWindows.Target.parse(args[0], args[1], args[2], args[3], args[5]), Integer.parseInt(args[4]));
|
||||
}
|
||||
}
|
||||
|
||||
record Inputs(HydrologySettings settings, HydrologyTerrainSource terrain, IntBinaryOperator naturalHeight, ForkJoinPool planningPool,
|
||||
ExecutorService workers, HydrologyVoxels.ChunkSource blocks, HydrologyUnderground.Source underground, int worldHeight) {
|
||||
Inputs {
|
||||
Objects.requireNonNull(settings, "settings");
|
||||
Objects.requireNonNull(terrain, "terrain");
|
||||
Objects.requireNonNull(naturalHeight, "naturalHeight");
|
||||
Objects.requireNonNull(planningPool, "planningPool");
|
||||
Objects.requireNonNull(workers, "workers");
|
||||
Objects.requireNonNull(blocks, "blocks");
|
||||
Objects.requireNonNull(underground, "underground");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int exitCode = 2;
|
||||
try {
|
||||
HydrologyReport.Result result = run(Configuration.parse(args));
|
||||
exitCode = result.passes() ? 0 : 1;
|
||||
} catch (Throwable failure) {
|
||||
failure.printStackTrace(System.out);
|
||||
}
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
static HydrologyReport.Result run(Configuration configuration) throws Exception {
|
||||
StubPlatform.vanillaSolidity(true);
|
||||
HydrologyWindows.Target target = configuration.target();
|
||||
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(target.pack(), target.dimension(), "[hydrology]");
|
||||
RealPackProbeSupport.EngineSession session = workspace.openEngine(target.seed(), target.studio(), "hydrology")) {
|
||||
Engine engine = session.engine();
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
IrisHydrology config = dimension.getHydrology();
|
||||
if (config == null || !config.isEnabled()) {
|
||||
throw new IllegalStateException("The dimension " + target.dimension() + " must enable hydrology.");
|
||||
}
|
||||
HydrologySettings settings = HydrologySettings.compile(config, engine.getSeedManager().getBodies(), dimension.getFluidHeight(), 0,
|
||||
engine.getHeight());
|
||||
IrisHydrologyTerrainSource terrain = new IrisHydrologyTerrainSource(engine.getComplex(), dimension, settings, MultiBurst.hydrology,
|
||||
MultiBurst.hydrology.parallelism(), HydrologyBinding.UNBOUND);
|
||||
ForkJoinPool planningPool = MultiBurst.hydrology.submit((Callable<ForkJoinPool>) ForkJoinTask::getPool).get();
|
||||
ExecutorService workers = Executors.newFixedThreadPool(configuration.threads());
|
||||
try {
|
||||
Inputs inputs = new Inputs(settings, terrain, terrain::naturalHeight, planningPool, workers,
|
||||
(chunkX, chunkZ) -> HydrologyVoxels.generate(engine, chunkX, chunkZ), HydrologyUnderground.generated(engine, workers),
|
||||
engine.getTarget().getHeight());
|
||||
HydrologyReport.Result measured = measure(inputs, target.windows());
|
||||
List<Throwable> reports = RealPackProbeSupport.drainReported();
|
||||
RealPackProbeSupport.printReports("[hydrology]", "generation", reports);
|
||||
HydrologyReport.Result result = measured.withGenerationErrors(reports.size());
|
||||
for (String line : result.lines()) {
|
||||
System.out.println(line);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
workers.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static HydrologyReport.Result measure(Inputs inputs, HydrologyWindows.WindowRange range) throws Exception {
|
||||
return new Measurement(inputs, range).measure();
|
||||
}
|
||||
|
||||
private static final class Measurement {
|
||||
private final Inputs inputs;
|
||||
private final HydrologyWindows.WindowRange range;
|
||||
private final HydrologyReport.Examples examples = new HydrologyReport.Examples();
|
||||
private final Set<String> failures = Collections.synchronizedSet(new LinkedHashSet<>());
|
||||
private HydrologyPlanHarness.Plans plans;
|
||||
private HydrologyPlanHarness.RasterCache rasters;
|
||||
|
||||
private Measurement(Inputs inputs, HydrologyWindows.WindowRange range) {
|
||||
this.inputs = inputs;
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
private HydrologyReport.Result measure() throws Exception {
|
||||
double warmupMs = -1D;
|
||||
try {
|
||||
warmupMs = HydrologyColdRuns.cold(inputs, new WindowKey(range.minX() - WARMUP_OFFSET, range.minZ() - WARMUP_OFFSET)).millis();
|
||||
} catch (RuntimeException failure) {
|
||||
failures.add("warm-up cold run: " + HydrologyPlanHarness.cause(failure));
|
||||
}
|
||||
Map<WindowKey, HydrologyColdRuns.ColdRun> colds = new LinkedHashMap<>();
|
||||
for (WindowKey key : range.keys()) {
|
||||
try {
|
||||
colds.put(key, HydrologyColdRuns.cold(inputs, key));
|
||||
} catch (RuntimeException failure) {
|
||||
failures.add("cold window " + key.wx() + "," + key.wz() + ": " + HydrologyPlanHarness.cause(failure));
|
||||
colds.put(key, HydrologyColdRuns.ColdRun.FAILED);
|
||||
}
|
||||
}
|
||||
plans = new HydrologyPlanHarness.Plans(inputs.settings(), inputs.terrain(), inputs.naturalHeight(), range);
|
||||
rasters = new HydrologyPlanHarness.RasterCache(RASTER_CACHE, plans::rasterize);
|
||||
Map<WindowKey, WindowPlan> windows = plans.windowPlans();
|
||||
HydrologyPlanMetrics planMetrics = new HydrologyPlanMetrics(inputs.settings(), windows, plans.lakes(), range, examples);
|
||||
HydrologyRasterMetrics rasterMetrics = new HydrologyRasterMetrics(inputs.settings(), planMetrics, rasters, inputs.naturalHeight(), range,
|
||||
examples);
|
||||
HydrologyBlockMetrics blockMetrics = new HydrologyBlockMetrics(inputs, rasterMetrics, range, examples, failures::add);
|
||||
List<HydrologyReport.WindowMetrics> measured = new ArrayList<>();
|
||||
long generationErrors = 0L;
|
||||
for (WindowKey key : range.keys()) {
|
||||
WindowPlan plan = windows.get(key);
|
||||
if (plan == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
prepareRasters(key);
|
||||
HydrologyBlockMetrics.Counts blocks = blockMetrics.count(key);
|
||||
generationErrors += blocks.errors();
|
||||
HydrologyUnderground.Counts underground = underground(key, plan);
|
||||
HydrologyColdRuns.ColdRun cold = colds.get(key);
|
||||
measured.add(new HydrologyReport.WindowMetrics(key, cold.millis(), cold.naturalSamples(), cold.baseSamples(),
|
||||
plans.windowCpu(key), plans.regionCpu(key.region()), cold.depth(), plans.landTiles(key), planMetrics.rivers(key),
|
||||
planMetrics.streams(key), planMetrics.ponds(key), planMetrics.levels(key), planMetrics.perched(key),
|
||||
blocks.scan().containmentLeaks, rasterMetrics.rising(key, plan), planMetrics.seams(key) + rasterMetrics.chunkSeams(key),
|
||||
blocks.scan().tickConversionCells, blocks.scan().missingSurfaces, rasterMetrics.bermOverCap(key), underground.inlets(), underground.inletFailures(),
|
||||
underground.cavePools(), underground.cavePoolFailures(), blocks.generated(), plans.digest(key)));
|
||||
} catch (RuntimeException failure) {
|
||||
failures.add("window " + key.wx() + "," + key.wz() + ": " + HydrologyPlanHarness.cause(failure));
|
||||
}
|
||||
}
|
||||
failures.addAll(plans.failures());
|
||||
return new HydrologyReport.Result(measured, plans.meanRegionCpu(), plans.regionCount(), plans.windowCount(), plans.naturalSamples(),
|
||||
plans.baseSamples(), warmupMs, generationErrors, plans.digest(), List.copyOf(failures), examples.values());
|
||||
}
|
||||
|
||||
private HydrologyUnderground.Counts underground(WindowKey key, WindowPlan plan) {
|
||||
try {
|
||||
HydrologyUnderground.Counts counts = inputs.underground().measure(key, plan);
|
||||
for (String example : counts.examples()) {
|
||||
examples.accept(example);
|
||||
}
|
||||
return counts;
|
||||
} catch (Exception failure) {
|
||||
failures.add("underground window " + key.wx() + "," + key.wz() + ": " + HydrologyPlanHarness.cause(failure));
|
||||
return HydrologyUnderground.Counts.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareRasters(WindowKey key) throws InterruptedException {
|
||||
List<Callable<ChunkWaterRaster>> tasks = new ArrayList<>();
|
||||
List<String> chunks = new ArrayList<>();
|
||||
int minChunkX = key.wx() * HydrologyWindows.CHUNKS - 1;
|
||||
int minChunkZ = key.wz() * HydrologyWindows.CHUNKS - 1;
|
||||
for (int chunkZ = minChunkZ; chunkZ <= minChunkZ + HydrologyWindows.CHUNKS + 1; chunkZ++) {
|
||||
for (int chunkX = minChunkX; chunkX <= minChunkX + HydrologyWindows.CHUNKS + 1; chunkX++) {
|
||||
if (range.containsChunk(chunkX, chunkZ)) {
|
||||
int x = chunkX;
|
||||
int z = chunkZ;
|
||||
tasks.add(() -> rasters.get(x, z));
|
||||
chunks.add(x + "," + z);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Future<ChunkWaterRaster>> futures = inputs.workers().invokeAll(tasks);
|
||||
for (int index = 0; index < futures.size(); index++) {
|
||||
try {
|
||||
futures.get(index).get();
|
||||
} catch (ExecutionException failure) {
|
||||
failures.add("raster chunk " + chunks.get(index) + ": " + HydrologyPlanHarness.cause(failure));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.hydrology.water.Pond;
|
||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||
import art.arcane.iris.generation.hydrology.water.ReachStyle;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
final class HydrologyRasterMetrics {
|
||||
private final HydrologySettings settings;
|
||||
private final HydrologyPlanMetrics plans;
|
||||
private final Rasters rasters;
|
||||
private final IntBinaryOperator naturalHeight;
|
||||
private final HydrologyWindows.WindowRange range;
|
||||
private final Consumer<String> examples;
|
||||
|
||||
@FunctionalInterface
|
||||
interface Rasters {
|
||||
ChunkWaterRaster get(int chunkX, int chunkZ);
|
||||
}
|
||||
|
||||
HydrologyRasterMetrics(HydrologySettings settings, HydrologyPlanMetrics plans, Rasters rasters, IntBinaryOperator naturalHeight,
|
||||
HydrologyWindows.WindowRange range, Consumer<String> examples) {
|
||||
this.settings = settings;
|
||||
this.plans = plans;
|
||||
this.rasters = rasters;
|
||||
this.naturalHeight = naturalHeight;
|
||||
this.range = range;
|
||||
this.examples = examples;
|
||||
}
|
||||
|
||||
long rising(WindowKey key, WindowPlan plan) {
|
||||
long rising = 0L;
|
||||
for (Reach reach : plan.reaches()) {
|
||||
int[] heads = heads(reach);
|
||||
int previous = HydrologyVoxels.NO_WATER;
|
||||
for (int point = 0; point < heads.length; point++) {
|
||||
int head = heads[point];
|
||||
if (head == HydrologyVoxels.NO_WATER) {
|
||||
continue;
|
||||
}
|
||||
if (previous != HydrologyVoxels.NO_WATER && head > previous) {
|
||||
rising++;
|
||||
examples.accept("rise along reach " + reach.id() + " from " + previous + " to " + head);
|
||||
}
|
||||
if (reach.style()[point] == ReachStyle.MOUTH && head < settings.seaLevel()) {
|
||||
rising++;
|
||||
examples.accept("rise from the sea at " + settings.seaLevel() + " into the mouth of reach " + reach.id() + " at " + head);
|
||||
}
|
||||
previous = head;
|
||||
}
|
||||
if (previous == HydrologyVoxels.NO_WATER) {
|
||||
continue;
|
||||
}
|
||||
Reach downstream = plans.reaches().get(reach.downstreamId());
|
||||
if (downstream != null) {
|
||||
int first = first(heads(downstream));
|
||||
if (first != HydrologyVoxels.NO_WATER && first > previous) {
|
||||
rising++;
|
||||
examples.accept("rise from reach " + reach.id() + " at " + previous + " into reach " + downstream.id() + " at " + first);
|
||||
}
|
||||
}
|
||||
Pond end = plans.endPond(reach);
|
||||
if (end != null && end.level() > previous) {
|
||||
rising++;
|
||||
examples.accept("rise from reach " + reach.id() + " at " + previous + " into pond " + end.id() + " at " + end.level());
|
||||
}
|
||||
}
|
||||
for (Pond pond : plan.ponds()) {
|
||||
Reach outlet = plans.reaches().get(pond.outletReachId());
|
||||
if (outlet != null) {
|
||||
int first = first(heads(outlet));
|
||||
if (first != HydrologyVoxels.NO_WATER && first > pond.level()) {
|
||||
rising++;
|
||||
examples.accept("rise from pond " + pond.id() + " at " + pond.level() + " into outlet " + outlet.id() + " at " + first);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rising;
|
||||
}
|
||||
|
||||
long chunkSeams(WindowKey key) {
|
||||
long mismatches = 0L;
|
||||
int minX = key.wx() * WindowKey.SIZE;
|
||||
int minZ = key.wz() * WindowKey.SIZE;
|
||||
for (int z = minZ; z < minZ + WindowKey.SIZE; z++) {
|
||||
for (int x = minX; x < minX + WindowKey.SIZE; x++) {
|
||||
if ((x & 15) == 15 && range.containsBlock(x + 1, z) && seamMismatch(x, z, x + 1, z)) {
|
||||
mismatches++;
|
||||
}
|
||||
if ((z & 15) == 15 && range.containsBlock(x, z + 1) && seamMismatch(x, z, x, z + 1)) {
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
long bermOverCap(WindowKey key) {
|
||||
long overCap = 0L;
|
||||
int maxFill = settings.deformation().maxFill();
|
||||
int minX = key.wx() * WindowKey.SIZE;
|
||||
int minZ = key.wz() * WindowKey.SIZE;
|
||||
for (int z = minZ; z < minZ + WindowKey.SIZE; z++) {
|
||||
for (int x = minX; x < minX + WindowKey.SIZE; x++) {
|
||||
ChunkWaterRaster raster = rasters.get(x >> 4, z >> 4);
|
||||
WaterRole role = raster.role(x & 15, z & 15);
|
||||
if (raster.empty() || role == WaterRole.NONE || role == WaterRole.DAM || role.wet()) {
|
||||
continue;
|
||||
}
|
||||
int terrain = raster.terrainHeight(x & 15, z & 15);
|
||||
int natural = naturalHeight.applyAsInt(x, z);
|
||||
if (terrain > natural + maxFill) {
|
||||
overCap++;
|
||||
examples.accept("berm raised " + x + "," + z + " from " + natural + " to " + terrain);
|
||||
}
|
||||
}
|
||||
}
|
||||
return overCap;
|
||||
}
|
||||
|
||||
WaterRole role(int x, int z) {
|
||||
return rasters.get(x >> 4, z >> 4).role(x & 15, z & 15);
|
||||
}
|
||||
|
||||
int terrain(int x, int z) {
|
||||
ChunkWaterRaster raster = rasters.get(x >> 4, z >> 4);
|
||||
return raster.empty() ? naturalHeight.applyAsInt(x, z) : raster.terrainHeight(x & 15, z & 15);
|
||||
}
|
||||
|
||||
private boolean seamMismatch(int x, int z, int nx, int nz) {
|
||||
ChunkWaterRaster near = rasters.get(x >> 4, z >> 4);
|
||||
ChunkWaterRaster far = rasters.get(nx >> 4, nz >> 4);
|
||||
WaterRole nearRole = near.role(x & 15, z & 15);
|
||||
WaterRole farRole = far.role(nx & 15, nz & 15);
|
||||
int nearHead = near.waterHead(x & 15, z & 15);
|
||||
int farHead = far.waterHead(nx & 15, nz & 15);
|
||||
boolean mismatch;
|
||||
String what;
|
||||
if (nearRole.wet() && farRole.wet()) {
|
||||
mismatch = nearRole == WaterRole.POND && farRole == WaterRole.POND && near.featureId(x & 15, z & 15) == far.featureId(nx & 15, nz & 15)
|
||||
&& nearHead != farHead;
|
||||
what = "one pond at two levels";
|
||||
} else if (nearRole.wet()) {
|
||||
mismatch = unbermed(nearHead, nx, nz);
|
||||
what = "dry column left below its berm";
|
||||
} else if (farRole.wet()) {
|
||||
mismatch = unbermed(farHead, x, z);
|
||||
what = "dry column left below its berm";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (mismatch) {
|
||||
examples.accept("seam " + what + " across chunks at " + x + "," + z + " / " + nx + "," + nz);
|
||||
}
|
||||
return mismatch;
|
||||
}
|
||||
|
||||
private boolean unbermed(int head, int x, int z) {
|
||||
int ground = terrain(x, z);
|
||||
return ground < head && !(head <= settings.seaLevel() && ground < settings.seaLevel());
|
||||
}
|
||||
|
||||
private int[] heads(Reach reach) {
|
||||
int[] heads = new int[reach.xs().length];
|
||||
for (int point = 0; point < heads.length; point++) {
|
||||
int x = (int) Math.floor(reach.xs()[point]);
|
||||
int z = (int) Math.floor(reach.zs()[point]);
|
||||
heads[point] = HydrologyVoxels.NO_WATER;
|
||||
if (range.containsBlock(x, z)) {
|
||||
ChunkWaterRaster raster = rasters.get(x >> 4, z >> 4);
|
||||
if (raster.role(x & 15, z & 15).wet() && raster.featureId(x & 15, z & 15) == reach.id()) {
|
||||
heads[point] = raster.waterHead(x & 15, z & 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
return heads;
|
||||
}
|
||||
|
||||
private static int first(int[] heads) {
|
||||
for (int head : heads) {
|
||||
if (head != HydrologyVoxels.NO_WATER) {
|
||||
return head;
|
||||
}
|
||||
}
|
||||
return HydrologyVoxels.NO_WATER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
final class HydrologyReport {
|
||||
static final double POND_LEVEL_TILES = 9D;
|
||||
private static final int EXAMPLES_PER_KIND = 8;
|
||||
|
||||
private HydrologyReport() {
|
||||
}
|
||||
|
||||
record WindowMetrics(WindowKey key, double coldFirstChunkMs, long coldNaturalSamples, long coldBaseSamples, double windowCpuMs,
|
||||
double regionCpuMs, int maxDependencyDepth, double landTiles, int rivers, double streamBlocks, int ponds,
|
||||
Set<Integer> pondLevels, int pondsPerched8, long containmentLeaks, long risingWaterSegments, long seamMismatches,
|
||||
long tickConversionCells, long missingSurface, long bermOverCap, int inlets, long inletFailures, int cavePools, long cavePoolFailures,
|
||||
long generatedChunks, String planDigest) {
|
||||
String line() {
|
||||
return String.format(Locale.ROOT, "IRIS_HYDROLOGY_WINDOW wx=%d wz=%d coldFirstChunkMs=%.1f windowCpuMs=%.1f regionCpuMs=%.1f "
|
||||
+ "maxDependencyDepth=%d riversPerTile=%.2f streamBlocksPerTile=%.0f pondsPerTile=%.2f pondLevelsDistinct=%d "
|
||||
+ "pondsPerched8=%d containmentLeaks=%d risingWaterSegments=%d seamMismatches=%d tickConversionCells=%d "
|
||||
+ "missingSurface=%d bermOverCap=%d inlets=%d inletFailures=%d cavePools=%d cavePoolFailures=%d landTiles=%.3f rivers=%d ponds=%d "
|
||||
+ "coldNaturalSamples=%d coldBaseSamples=%d generatedChunks=%d planDigest=%s",
|
||||
key.wx(), key.wz(), coldFirstChunkMs, windowCpuMs, regionCpuMs, maxDependencyDepth, perTile(rivers, landTiles),
|
||||
perTile(streamBlocks, landTiles), perTile(ponds, landTiles), pondLevels.size(), pondsPerched8, containmentLeaks,
|
||||
risingWaterSegments, seamMismatches, tickConversionCells, missingSurface, bermOverCap, inlets, inletFailures, cavePools, cavePoolFailures,
|
||||
landTiles, rivers, ponds, coldNaturalSamples, coldBaseSamples, generatedChunks, planDigest);
|
||||
}
|
||||
}
|
||||
|
||||
record Result(List<WindowMetrics> windows, double regionCpuMs, int regionsPlanned, int windowsPlanned, long naturalSamples,
|
||||
long baseSamples, double warmupMs, long generationErrors, String planDigest, List<String> failures, List<String> examples) {
|
||||
boolean passes() {
|
||||
return maxDependencyDepth() == 0 && sum(WindowMetrics::containmentLeaks) == 0 && sum(WindowMetrics::risingWaterSegments) == 0
|
||||
&& sum(WindowMetrics::seamMismatches) == 0 && sum(WindowMetrics::tickConversionCells) == 0
|
||||
&& sum(WindowMetrics::missingSurface) == 0
|
||||
&& sum(WindowMetrics::inletFailures) == 0 && sum(WindowMetrics::cavePoolFailures) == 0 && generationErrors == 0
|
||||
&& failures.isEmpty();
|
||||
}
|
||||
|
||||
Result withGenerationErrors(long errors) {
|
||||
return new Result(windows, regionCpuMs, regionsPlanned, windowsPlanned, naturalSamples, baseSamples, warmupMs, generationErrors + errors,
|
||||
planDigest, failures, examples);
|
||||
}
|
||||
|
||||
int maxDependencyDepth() {
|
||||
int deepest = 0;
|
||||
for (WindowMetrics window : windows) {
|
||||
deepest = Math.max(deepest, window.maxDependencyDepth());
|
||||
}
|
||||
return deepest;
|
||||
}
|
||||
|
||||
long sum(Function<WindowMetrics, Long> field) {
|
||||
long total = 0L;
|
||||
for (WindowMetrics window : windows) {
|
||||
total += field.apply(window);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
double coldMax() {
|
||||
double max = 0D;
|
||||
for (WindowMetrics window : windows) {
|
||||
max = Math.max(max, window.coldFirstChunkMs());
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
double coldMedian() {
|
||||
double[] values = new double[windows.size()];
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
values[index] = windows.get(index).coldFirstChunkMs();
|
||||
}
|
||||
Arrays.sort(values);
|
||||
return values.length == 0 ? 0D : values[values.length / 2];
|
||||
}
|
||||
|
||||
List<String> missedTargets() {
|
||||
List<String> missed = new ArrayList<>();
|
||||
if (maxDependencyDepth() != 0) {
|
||||
missed.add("maxDependencyDepth");
|
||||
}
|
||||
if (perTile(riversTotal(), landTiles()) < 1D) {
|
||||
missed.add("riversPerTile");
|
||||
}
|
||||
if (perTile(pondsTotal(), landTiles()) < 6D) {
|
||||
missed.add("pondsPerTile");
|
||||
}
|
||||
if (POND_LEVEL_TILES * perTile(pondLevels().size(), landTiles()) < 6D) {
|
||||
missed.add("pondLevelsDistinct");
|
||||
}
|
||||
for (String gate : List.of("containmentLeaks", "risingWaterSegments", "seamMismatches", "tickConversionCells", "missingSurface",
|
||||
"bermOverCap", "inletFailures", "cavePoolFailures")) {
|
||||
if (gate(gate) != 0L) {
|
||||
missed.add(gate);
|
||||
}
|
||||
}
|
||||
return missed;
|
||||
}
|
||||
|
||||
private long gate(String name) {
|
||||
return switch (name) {
|
||||
case "containmentLeaks" -> sum(WindowMetrics::containmentLeaks);
|
||||
case "risingWaterSegments" -> sum(WindowMetrics::risingWaterSegments);
|
||||
case "seamMismatches" -> sum(WindowMetrics::seamMismatches);
|
||||
case "tickConversionCells" -> sum(WindowMetrics::tickConversionCells);
|
||||
case "missingSurface" -> sum(WindowMetrics::missingSurface);
|
||||
case "bermOverCap" -> sum(WindowMetrics::bermOverCap);
|
||||
case "inletFailures" -> sum(WindowMetrics::inletFailures);
|
||||
case "cavePoolFailures" -> sum(WindowMetrics::cavePoolFailures);
|
||||
default -> throw new IllegalArgumentException(name);
|
||||
};
|
||||
}
|
||||
|
||||
double landTiles() {
|
||||
double total = 0D;
|
||||
for (WindowMetrics window : windows) {
|
||||
total += window.landTiles();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int riversTotal() {
|
||||
int total = 0;
|
||||
for (WindowMetrics window : windows) {
|
||||
total += window.rivers();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int pondsTotal() {
|
||||
int total = 0;
|
||||
for (WindowMetrics window : windows) {
|
||||
total += window.ponds();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
Set<Integer> pondLevels() {
|
||||
Set<Integer> levels = new TreeSet<>();
|
||||
for (WindowMetrics window : windows) {
|
||||
levels.addAll(window.pondLevels());
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
|
||||
List<String> lines() {
|
||||
List<String> lines = new ArrayList<>();
|
||||
for (WindowMetrics window : windows) {
|
||||
lines.add(window.line());
|
||||
}
|
||||
double streamBlocks = 0D;
|
||||
double windowCpu = 0D;
|
||||
int perched = 0;
|
||||
long generated = 0L;
|
||||
int inlets = 0;
|
||||
int cavePools = 0;
|
||||
for (WindowMetrics window : windows) {
|
||||
streamBlocks += window.streamBlocks();
|
||||
windowCpu += window.windowCpuMs();
|
||||
perched += window.pondsPerched8();
|
||||
generated += window.generatedChunks();
|
||||
inlets += window.inlets();
|
||||
cavePools += window.cavePools();
|
||||
}
|
||||
List<String> missed = missedTargets();
|
||||
lines.add(String.format(Locale.ROOT, "IRIS_HYDROLOGY_TOTAL version=1 status=%s windows=%d coldFirstChunkMs=%.1f "
|
||||
+ "coldFirstChunkMedianMs=%.1f windowCpuMs=%.1f regionCpuMs=%.1f maxDependencyDepth=%d riversPerTile=%.2f "
|
||||
+ "streamBlocksPerTile=%.0f pondsPerTile=%.2f pondLevelsDistinct=%d pondsPerched8=%d containmentLeaks=%d "
|
||||
+ "risingWaterSegments=%d seamMismatches=%d tickConversionCells=%d missingSurface=%d bermOverCap=%d inlets=%d inletFailures=%d "
|
||||
+ "cavePools=%d cavePoolFailures=%d landTiles=%.3f "
|
||||
+ "rivers=%d ponds=%d windowsPlanned=%d regionsPlanned=%d naturalSamples=%d baseSamples=%d coldWarmupMs=%.1f "
|
||||
+ "generatedChunks=%d generationErrors=%d failures=%d planDigest=%s missedTargets=%s",
|
||||
passes() ? "PASS" : "FAIL", windows.size(), coldMax(), coldMedian(), windows.isEmpty() ? 0D : windowCpu / windows.size(),
|
||||
regionCpuMs, maxDependencyDepth(), perTile(riversTotal(), landTiles()), perTile(streamBlocks, landTiles()),
|
||||
perTile(pondsTotal(), landTiles()), pondLevels().size(), perched, sum(WindowMetrics::containmentLeaks),
|
||||
sum(WindowMetrics::risingWaterSegments), sum(WindowMetrics::seamMismatches), sum(WindowMetrics::tickConversionCells),
|
||||
sum(WindowMetrics::missingSurface),
|
||||
sum(WindowMetrics::bermOverCap), inlets, sum(WindowMetrics::inletFailures), cavePools, sum(WindowMetrics::cavePoolFailures),
|
||||
landTiles(), riversTotal(), pondsTotal(), windowsPlanned, regionsPlanned, naturalSamples,
|
||||
baseSamples, warmupMs, generated, generationErrors, failures.size(), planDigest, missed.isEmpty() ? "none" : String.join(",", missed)));
|
||||
for (String failure : failures) {
|
||||
lines.add("IRIS_HYDROLOGY_FAILURE " + failure);
|
||||
}
|
||||
for (String example : examples) {
|
||||
lines.add("IRIS_HYDROLOGY_EXAMPLE " + example);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Examples implements Consumer<String> {
|
||||
private final List<String> values = new ArrayList<>();
|
||||
private final Map<String, Integer> counts = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public synchronized void accept(String value) {
|
||||
String kind = value.substring(0, Math.max(0, value.indexOf(' ')));
|
||||
if (counts.merge(kind, 1, Integer::sum) <= EXAMPLES_PER_KIND) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized List<String> values() {
|
||||
return List.copyOf(values);
|
||||
}
|
||||
}
|
||||
|
||||
static double perTile(double count, double landTiles) {
|
||||
return landTiles <= 0D ? 0D : count / landTiles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
final class HydrologyUnderground {
|
||||
private HydrologyUnderground() {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface Source {
|
||||
Counts measure(WindowKey key, WindowPlan plan) throws Exception;
|
||||
}
|
||||
|
||||
record Counts(int inlets, long inletFailures, int cavePools, long cavePoolFailures, List<String> examples) {
|
||||
static final Counts NONE = new Counts(0, 0L, 0, 0L, List.of());
|
||||
}
|
||||
|
||||
static Source generated(Engine engine, ExecutorService workers) {
|
||||
return (key, plan) -> {
|
||||
SeaInletProbe.Checks inlets = SeaInletProbe.check(engine, plan.inlets(), workers);
|
||||
int minChunkX = key.wx() * HydrologyWindows.CHUNKS;
|
||||
int minChunkZ = key.wz() * HydrologyWindows.CHUNKS;
|
||||
CavePoolProbe.Counts pools = CavePoolProbe.check(engine, new CavePoolProbe.Area(minChunkX, minChunkX + HydrologyWindows.CHUNKS - 1,
|
||||
minChunkZ, minChunkZ + HydrologyWindows.CHUNKS - 1), workers);
|
||||
List<String> examples = new ArrayList<>();
|
||||
for (String example : inlets.examples()) {
|
||||
examples.add("inlet " + example);
|
||||
}
|
||||
for (String example : pools.examples()) {
|
||||
examples.add("pool " + example);
|
||||
}
|
||||
return new Counts(inlets.inlets(), inlets.failures(), pools.pools(), pools.failures(), examples);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
final class HydrologyVoxels {
|
||||
static final byte UNKNOWN = -1;
|
||||
static final byte OPEN = 0;
|
||||
static final byte SOLID = 1;
|
||||
static final byte CANE = 2;
|
||||
static final byte WATER = 16;
|
||||
static final byte LAVA = 32;
|
||||
static final int NO_WATER = Integer.MIN_VALUE;
|
||||
static final int COLUMNS = 256;
|
||||
private static final int FLUID_LEVELS = 16;
|
||||
private static final int FULL_SNOW = 8;
|
||||
private static final int EXAMPLES_PER_KIND = 8;
|
||||
private static final int GRID = 18;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final Set<String> WATER_SOURCE_BLOCKS = Set.of("minecraft:seagrass", "minecraft:tall_seagrass", "minecraft:kelp",
|
||||
"minecraft:kelp_plant", "minecraft:bubble_column");
|
||||
private static final Set<String> OPEN_BLOCKS = Set.of("minecraft:short_grass", "minecraft:grass", "minecraft:tall_grass",
|
||||
"minecraft:fern", "minecraft:large_fern", "minecraft:dead_bush", "minecraft:bush", "minecraft:firefly_bush",
|
||||
"minecraft:short_dry_grass", "minecraft:tall_dry_grass", "minecraft:vine", "minecraft:snow", "minecraft:sweet_berry_bush",
|
||||
"minecraft:cave_vines", "minecraft:cave_vines_plant", "minecraft:dandelion", "minecraft:poppy", "minecraft:blue_orchid",
|
||||
"minecraft:allium", "minecraft:azure_bluet", "minecraft:oxeye_daisy", "minecraft:cornflower", "minecraft:lily_of_the_valley",
|
||||
"minecraft:wither_rose", "minecraft:torchflower", "minecraft:sunflower", "minecraft:lilac", "minecraft:rose_bush",
|
||||
"minecraft:peony", "minecraft:pitcher_plant", "minecraft:wildflowers", "minecraft:leaf_litter", "minecraft:cactus_flower",
|
||||
"minecraft:open_eyeblossom", "minecraft:closed_eyeblossom", "minecraft:nether_sprouts", "minecraft:crimson_roots",
|
||||
"minecraft:warped_roots", "minecraft:torch", "minecraft:wall_torch", "minecraft:spore_blossom", "minecraft:pink_petals",
|
||||
"minecraft:tripwire", "minecraft:cobweb", "minecraft:wheat", "minecraft:carrots", "minecraft:potatoes", "minecraft:beetroots",
|
||||
"minecraft:melon_stem", "minecraft:pumpkin_stem", "minecraft:attached_melon_stem", "minecraft:attached_pumpkin_stem",
|
||||
"minecraft:torchflower_crop", "minecraft:pitcher_crop", "minecraft:nether_wart", "minecraft:fire", "minecraft:soul_fire",
|
||||
"minecraft:redstone_wire");
|
||||
private static final Map<NativeBlockState, Byte> CLASSES = new ConcurrentHashMap<>();
|
||||
|
||||
private HydrologyVoxels() {
|
||||
}
|
||||
|
||||
record ChunkBlocks(int chunkX, int chunkZ, int height, byte[] voxels, int[] heads, byte[] surfaces) {
|
||||
ChunkBlocks {
|
||||
if (height < 1 || voxels.length != COLUMNS * height || heads.length != COLUMNS || surfaces.length != COLUMNS) {
|
||||
throw new IllegalArgumentException("Chunk blocks need 256 x " + height + " voxels and 256 heads and surfaces, got "
|
||||
+ voxels.length + ", " + heads.length + " and " + surfaces.length);
|
||||
}
|
||||
}
|
||||
|
||||
static ChunkBlocks empty(int chunkX, int chunkZ, int height) {
|
||||
int[] heads = new int[COLUMNS];
|
||||
Arrays.fill(heads, NO_WATER);
|
||||
byte[] surfaces = new byte[COLUMNS];
|
||||
Arrays.fill(surfaces, WATER);
|
||||
return new ChunkBlocks(chunkX, chunkZ, height, new byte[COLUMNS * height], heads, surfaces);
|
||||
}
|
||||
|
||||
byte at(int lx, int y, int lz) {
|
||||
return y < 0 || y >= height ? UNKNOWN : voxels[(y << 8) | (lz << 4) | lx];
|
||||
}
|
||||
|
||||
void set(int lx, int y, int lz, byte voxel) {
|
||||
voxels[(y << 8) | (lz << 4) | lx] = voxel;
|
||||
}
|
||||
|
||||
int head(int lx, int lz) {
|
||||
return heads[(lz << 4) | lx];
|
||||
}
|
||||
|
||||
byte surface(int lx, int lz) {
|
||||
return surfaces[(lz << 4) | lx];
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ChunkSource {
|
||||
ChunkBlocks generate(int chunkX, int chunkZ);
|
||||
}
|
||||
|
||||
static ChunkBlocks generate(Engine engine, int chunkX, int chunkZ) {
|
||||
RealPackProbeSupport.GeneratedChunk chunk = RealPackProbeSupport.generateChunk(engine, chunkX, chunkZ);
|
||||
ChunkBlocks blocks = ChunkBlocks.empty(chunk.chunkX(), chunk.chunkZ(), chunk.height());
|
||||
for (int y = 0; y < chunk.height(); y++) {
|
||||
for (int lz = 0; lz < 16; lz++) {
|
||||
for (int lx = 0; lx < 16; lx++) {
|
||||
blocks.set(lx, y, lz, classify(chunk.blocks().get(lx, y, lz)));
|
||||
}
|
||||
}
|
||||
}
|
||||
IrisComplex complex = engine.getComplex();
|
||||
System.arraycopy(effectiveHeads(complex, chunkX, chunkZ), 0, blocks.heads(), 0, COLUMNS);
|
||||
ChunkWaterRaster own = complex.getHydrologyRuntime().raster(chunkX, chunkZ);
|
||||
MantleChunk<Matter> mantle = engine.getMantle().getMantle().useChunk(chunkX, chunkZ);
|
||||
try {
|
||||
for (int lz = 0; lz < 16; lz++) {
|
||||
for (int lx = 0; lx < 16; lx++) {
|
||||
int head = blocks.head(lx, lz);
|
||||
if (head == NO_WATER) {
|
||||
continue;
|
||||
}
|
||||
int x = (chunkX << 4) + lx;
|
||||
int z = (chunkZ << 4) + lz;
|
||||
blocks.surfaces()[(lz << 4) | lx] = covered(complex, own, mantle, lx, head, lz, x, z) ? SOLID
|
||||
: classify(complex.resolveWaterFluid(own.fluid(lx, lz), x, z));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mantle.release();
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static boolean covered(IrisComplex complex, ChunkWaterRaster raster, MantleChunk<Matter> mantle, int lx, int head, int lz, int x,
|
||||
int z) {
|
||||
NativeBlockState object = mantle.get(lx, head, lz, NativeBlockState.class);
|
||||
if (object != null && object.isSolid()) {
|
||||
return true;
|
||||
}
|
||||
return raster.role(lx, lz) == WaterRole.INLET
|
||||
&& complex.getTrueBiomeStream().get(x, z).getSeaLayers().isNotEmpty();
|
||||
}
|
||||
|
||||
static int[] effectiveHeads(IrisComplex complex, int chunkX, int chunkZ) {
|
||||
int[] interior = new int[COLUMNS];
|
||||
Arrays.fill(interior, NO_WATER);
|
||||
HydrologyRuntime runtime = complex.getHydrologyRuntime();
|
||||
if (runtime == null || !runtime.enabled()) {
|
||||
return interior;
|
||||
}
|
||||
ChunkWaterRaster[] around = new ChunkWaterRaster[9];
|
||||
for (int slot = 0; slot < around.length; slot++) {
|
||||
around[slot] = runtime.raster(chunkX + slot % 3 - 1, chunkZ + slot / 3 - 1);
|
||||
}
|
||||
int[] heads = new int[GRID * GRID];
|
||||
complex.effectiveWaterHeads(chunkX, chunkZ, around, heads);
|
||||
for (int lz = 0; lz < 16; lz++) {
|
||||
for (int lx = 0; lx < 16; lx++) {
|
||||
interior[(lz << 4) | lx] = heads[(lz + 1) * GRID + lx + 1];
|
||||
}
|
||||
}
|
||||
return interior;
|
||||
}
|
||||
|
||||
static final class Volume {
|
||||
private final Map<Long, ChunkBlocks> chunks = new HashMap<>();
|
||||
|
||||
void put(ChunkBlocks blocks) {
|
||||
chunks.put(chunkKey(blocks.chunkX(), blocks.chunkZ()), blocks);
|
||||
}
|
||||
|
||||
byte at(int x, int y, int z) {
|
||||
ChunkBlocks chunk = chunks.get(chunkKey(x >> 4, z >> 4));
|
||||
return chunk == null ? UNKNOWN : chunk.at(x & 15, y, z & 15);
|
||||
}
|
||||
|
||||
int head(int x, int z) {
|
||||
ChunkBlocks chunk = chunks.get(chunkKey(x >> 4, z >> 4));
|
||||
return chunk == null ? NO_WATER : chunk.head(x & 15, z & 15);
|
||||
}
|
||||
|
||||
byte surface(int x, int z) {
|
||||
ChunkBlocks chunk = chunks.get(chunkKey(x >> 4, z >> 4));
|
||||
return chunk == null ? WATER : chunk.surface(x & 15, z & 15);
|
||||
}
|
||||
}
|
||||
|
||||
static final class WaterScan {
|
||||
long waterCells;
|
||||
long containmentLeaks;
|
||||
long tickConversionCells;
|
||||
long missingSurfaces;
|
||||
long censoredCells;
|
||||
final List<String> examples = new ArrayList<>();
|
||||
|
||||
void column(Volume volume, int x, int z, int fromY, int toY) {
|
||||
int head = volume.head(x, z);
|
||||
if (head != NO_WATER && !holdsSurface(volume.surface(x, z), volume.at(x, head, z))) {
|
||||
missingSurfaces++;
|
||||
example("missing planned surface at " + x + "," + head + "," + z);
|
||||
}
|
||||
for (int y = Math.max(0, fromY); y <= toY; y++) {
|
||||
byte voxel = volume.at(x, y, z);
|
||||
if (!fluid(voxel)) {
|
||||
continue;
|
||||
}
|
||||
waterCells++;
|
||||
byte below = volume.at(x, y - 1, z);
|
||||
if (source(voxel)) {
|
||||
if (open(below) || opensSideways(volume, x, y, z)) {
|
||||
leak(x, y, z, open(below) ? "water over open ground" : "source beside open ground");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (open(below)) {
|
||||
leak(x, y, z, "flowing water over open ground");
|
||||
} else if (y > head && converts(volume, x, y, z, voxel, below)) {
|
||||
tick(x, y, z, "flowing cell converts to a source");
|
||||
} else if (!sameFluid(voxel, below) && amount(voxel) > dropOff(voxel) && opensSideways(volume, x, y, z)) {
|
||||
tick(x, y, z, "flowing cell spreads beside lower dry ground");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean holdsSurface(byte planned, byte voxel) {
|
||||
return planned == SOLID ? voxel == SOLID || fluid(voxel) : sameFluid(planned, voxel);
|
||||
}
|
||||
|
||||
private boolean opensSideways(Volume volume, int x, int y, int z) {
|
||||
boolean open = false;
|
||||
for (int[] offset : CARDINALS) {
|
||||
byte neighbour = volume.at(x + offset[0], y, z + offset[1]);
|
||||
if (neighbour == UNKNOWN) {
|
||||
censoredCells++;
|
||||
} else if (open(neighbour)) {
|
||||
open = true;
|
||||
}
|
||||
}
|
||||
return open;
|
||||
}
|
||||
|
||||
private static boolean converts(Volume volume, int x, int y, int z, byte voxel, byte below) {
|
||||
if (!water(voxel)) {
|
||||
return false;
|
||||
}
|
||||
boolean supported = below == SOLID || (sameFluid(voxel, below) && source(below));
|
||||
if (!supported) {
|
||||
return false;
|
||||
}
|
||||
int sources = 0;
|
||||
for (int[] offset : CARDINALS) {
|
||||
byte neighbour = volume.at(x + offset[0], y, z + offset[1]);
|
||||
if (sameFluid(voxel, neighbour) && source(neighbour)) {
|
||||
sources++;
|
||||
}
|
||||
}
|
||||
return sources >= 2;
|
||||
}
|
||||
|
||||
private void leak(int x, int y, int z, String what) {
|
||||
containmentLeaks++;
|
||||
example("leak " + what + " at " + x + "," + y + "," + z);
|
||||
}
|
||||
|
||||
private void tick(int x, int y, int z, String what) {
|
||||
tickConversionCells++;
|
||||
example("tick " + what + " at " + x + "," + y + "," + z);
|
||||
}
|
||||
|
||||
private void example(String value) {
|
||||
String kind = value.substring(0, Math.max(0, value.indexOf(' ')));
|
||||
int seen = 0;
|
||||
for (String example : examples) {
|
||||
if (example.startsWith(kind + " ")) {
|
||||
seen++;
|
||||
}
|
||||
}
|
||||
if (seen < EXAMPLES_PER_KIND) {
|
||||
examples.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static byte classify(NativeBlockState state) {
|
||||
if (state == null || state.isAir()) {
|
||||
return OPEN;
|
||||
}
|
||||
Byte known = CLASSES.get(state);
|
||||
if (known != null) {
|
||||
return known;
|
||||
}
|
||||
byte resolved = resolve(state);
|
||||
CLASSES.putIfAbsent(state, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static byte resolve(NativeBlockState state) {
|
||||
String key = state.key();
|
||||
int bracket = key.indexOf('[');
|
||||
String block = bracket < 0 ? key : key.substring(0, bracket);
|
||||
if (block.equals("minecraft:water")) {
|
||||
return (byte) (WATER + level(key));
|
||||
}
|
||||
if (block.equals("minecraft:lava")) {
|
||||
return (byte) (LAVA + level(key));
|
||||
}
|
||||
if (state.isWaterLogged() || key.contains("waterlogged=true") || WATER_SOURCE_BLOCKS.contains(block)) {
|
||||
return WATER;
|
||||
}
|
||||
if (key.contains("waterlogged=")) {
|
||||
return SOLID;
|
||||
}
|
||||
if (block.equals("minecraft:sugar_cane")) {
|
||||
return CANE;
|
||||
}
|
||||
if (block.equals("minecraft:snow") && number(key, "layers=", 1) >= FULL_SNOW) {
|
||||
return SOLID;
|
||||
}
|
||||
if (OPEN_BLOCKS.contains(block) || block.endsWith("_sapling") || block.endsWith("_tulip") || block.endsWith("_carpet")
|
||||
|| block.endsWith("_mushroom") || block.endsWith("_fungus") || block.endsWith("_button") || block.endsWith("_pressure_plate")) {
|
||||
return OPEN;
|
||||
}
|
||||
return SOLID;
|
||||
}
|
||||
|
||||
private static int level(String key) {
|
||||
return Math.min(FLUID_LEVELS - 1, number(key, "level=", 0));
|
||||
}
|
||||
|
||||
private static int number(String key, String property, int fallback) {
|
||||
int start = key.indexOf(property);
|
||||
if (start < 0) {
|
||||
return fallback;
|
||||
}
|
||||
int end = start + property.length();
|
||||
while (end < key.length() && Character.isDigit(key.charAt(end))) {
|
||||
end++;
|
||||
}
|
||||
return end == start + property.length() ? fallback : Integer.parseInt(key.substring(start + property.length(), end));
|
||||
}
|
||||
|
||||
static boolean open(byte voxel) {
|
||||
return voxel == OPEN;
|
||||
}
|
||||
|
||||
static boolean fluid(byte voxel) {
|
||||
return voxel >= WATER && voxel < LAVA + FLUID_LEVELS;
|
||||
}
|
||||
|
||||
static boolean water(byte voxel) {
|
||||
return voxel >= WATER && voxel < WATER + FLUID_LEVELS;
|
||||
}
|
||||
|
||||
static boolean source(byte voxel) {
|
||||
return voxel == WATER || voxel == LAVA;
|
||||
}
|
||||
|
||||
static boolean sameFluid(byte voxel, byte other) {
|
||||
return fluid(other) && water(voxel) == water(other);
|
||||
}
|
||||
|
||||
static int amount(byte voxel) {
|
||||
int level = (voxel - WATER) % FLUID_LEVELS;
|
||||
return level == 0 || level >= 8 ? 8 : 8 - level;
|
||||
}
|
||||
|
||||
private static int dropOff(byte voxel) {
|
||||
return water(voxel) ? 1 : 2;
|
||||
}
|
||||
|
||||
static long columnKey(int x, int z) {
|
||||
return ((long) x << 32) | (z & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
static long chunkKey(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class HydrologyWindows {
|
||||
static final int CHUNKS = WindowKey.SIZE >> 4;
|
||||
|
||||
private HydrologyWindows() {
|
||||
}
|
||||
|
||||
record WindowRange(int minX, int maxX, int minZ, int maxZ) {
|
||||
WindowRange {
|
||||
if (maxX < minX || maxZ < minZ) {
|
||||
throw new IllegalArgumentException("Window range " + minX + ".." + maxX + "," + minZ + ".." + maxZ + " is empty.");
|
||||
}
|
||||
}
|
||||
|
||||
static WindowRange parse(String text) {
|
||||
String[] axes = text.trim().split(",", -1);
|
||||
if (axes.length != 2) {
|
||||
throw new IllegalArgumentException("Windows must read x0..x1,z0..z1, got '" + text + "'.");
|
||||
}
|
||||
int[] x = bounds(axes[0], text);
|
||||
int[] z = bounds(axes[1], text);
|
||||
return new WindowRange(x[0], x[1], z[0], z[1]);
|
||||
}
|
||||
|
||||
private static int[] bounds(String axis, String text) {
|
||||
String[] parts = axis.trim().split("\\.\\.", -1);
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalArgumentException("Windows must read x0..x1,z0..z1, got '" + text + "'.");
|
||||
}
|
||||
return new int[]{Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim())};
|
||||
}
|
||||
|
||||
int count() {
|
||||
return (maxX - minX + 1) * (maxZ - minZ + 1);
|
||||
}
|
||||
|
||||
boolean contains(WindowKey key) {
|
||||
return key.wx() >= minX && key.wx() <= maxX && key.wz() >= minZ && key.wz() <= maxZ;
|
||||
}
|
||||
|
||||
boolean containsChunk(int chunkX, int chunkZ) {
|
||||
return contains(new WindowKey(Math.floorDiv(chunkX, CHUNKS), Math.floorDiv(chunkZ, CHUNKS)));
|
||||
}
|
||||
|
||||
boolean containsBlock(int x, int z) {
|
||||
return contains(WindowKey.containing(x, z));
|
||||
}
|
||||
|
||||
WindowRange grown(int rings) {
|
||||
return new WindowRange(minX - rings, maxX + rings, minZ - rings, maxZ + rings);
|
||||
}
|
||||
|
||||
List<WindowKey> keys() {
|
||||
List<WindowKey> keys = new ArrayList<>(count());
|
||||
for (int wz = minZ; wz <= maxZ; wz++) {
|
||||
for (int wx = minX; wx <= maxX; wx++) {
|
||||
keys.add(new WindowKey(wx, wz));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
record Target(File pack, String dimension, long seed, WindowRange windows, boolean studio) {
|
||||
Target {
|
||||
if (pack == null || !pack.isAbsolute() || dimension == null || dimension.isBlank() || windows == null) {
|
||||
throw new IllegalArgumentException("An absolute pack path, a dimension key and a window range are required.");
|
||||
}
|
||||
}
|
||||
|
||||
static Target parse(String pack, String dimension, String seed, String windows, String studio) {
|
||||
File file = new File(pack);
|
||||
return new Target(file, dimension.equals("-") ? onlyDimension(file) : dimension, Long.parseLong(seed), WindowRange.parse(windows),
|
||||
RealPackProbeSupport.parseBoolean(studio, "studio"));
|
||||
}
|
||||
|
||||
private static String onlyDimension(File pack) {
|
||||
File[] files = new File(pack, "dimensions").listFiles((directory, name) -> name.endsWith(".json"));
|
||||
if (files == null || files.length != 1) {
|
||||
throw new IllegalArgumentException("The pack " + pack + " must hold exactly one dimension, or name one with probeDimension.");
|
||||
}
|
||||
String name = files[0].getName();
|
||||
return name.substring(0, name.length() - ".json".length());
|
||||
}
|
||||
}
|
||||
|
||||
static List<WindowKey> neighbourhood(WindowKey key) {
|
||||
List<WindowKey> keys = new ArrayList<>(9);
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
keys.add(new WindowKey(key.wx() + dx, key.wz() + dz));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
@@ -18,83 +18,28 @@
|
||||
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
record ProbeHydrologyPreplan(int tiles, int tileSize, int publicationRadius, int reachChunks, long elapsedNanos) {
|
||||
private static final int PLANNING_THREADS = 4;
|
||||
record ProbeHydrologyPreplan(int windows, long elapsedNanos) {
|
||||
static final int RINGS = 1;
|
||||
private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4;
|
||||
|
||||
static Optional<ProbeHydrologyPreplan> run(Engine engine, GenerationProbe.ProbeConfiguration configuration)
|
||||
throws Exception {
|
||||
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
|
||||
if (hydrology == null) {
|
||||
HydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
|
||||
if (hydrology == null || !hydrology.enabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
HydrologyPlannerSettings settings = hydrology.settings();
|
||||
int tileSize = settings.routing().tileSize();
|
||||
int publicationRadius = settings.publicationRadius();
|
||||
int reachChunks = Math.max(engine.getMantle().getRadius(), engine.getMantle().getRealRadius());
|
||||
List<HydrologyTileKey> keys = tiles(configuration, tileSize, publicationRadius, reachChunks);
|
||||
int[] bounds = GenerationProbe.scheduledChunkBounds(configuration);
|
||||
long started = System.nanoTime();
|
||||
ExecutorService executor = Executors.newFixedThreadPool(Math.min(keys.size(), PLANNING_THREADS));
|
||||
try {
|
||||
List<Future<?>> plans = new ArrayList<>(keys.size());
|
||||
for (HydrologyTileKey key : keys) {
|
||||
plans.add(executor.submit(() -> hydrology.tile(key)));
|
||||
}
|
||||
for (Future<?> plan : plans) {
|
||||
await(plan);
|
||||
}
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
}
|
||||
return Optional.of(new ProbeHydrologyPreplan(keys.size(), tileSize, publicationRadius, reachChunks,
|
||||
System.nanoTime() - started));
|
||||
}
|
||||
|
||||
static List<HydrologyTileKey> tiles(GenerationProbe.ProbeConfiguration configuration, int tileSize,
|
||||
int publicationRadius, int reachChunks) {
|
||||
HydrologyTileCache.PregenerationArea area = GenerationProbe.pregenerationArea(configuration);
|
||||
long reach = (long) reachChunks * 16L + publicationRadius;
|
||||
int minimumX = Math.toIntExact(Math.floorDiv(area.minimumBlockX() - reach, tileSize)) - 1;
|
||||
int minimumZ = Math.toIntExact(Math.floorDiv(area.minimumBlockZ() - reach, tileSize)) - 1;
|
||||
int maximumX = Math.toIntExact(Math.floorDiv(area.maximumBlockX() + reach, tileSize)) + 1;
|
||||
int maximumZ = Math.toIntExact(Math.floorDiv(area.maximumBlockZ() + reach, tileSize)) + 1;
|
||||
int centerX = Math.floorDiv(area.centerBlockX(), tileSize);
|
||||
int centerZ = Math.floorDiv(area.centerBlockZ(), tileSize);
|
||||
List<HydrologyTileKey> keys = new ArrayList<>((maximumX - minimumX + 1) * (maximumZ - minimumZ + 1));
|
||||
for (int tileZ = minimumZ; tileZ <= maximumZ; tileZ++) {
|
||||
for (int tileX = minimumX; tileX <= maximumX; tileX++) {
|
||||
keys.add(new HydrologyTileKey(tileX, tileZ));
|
||||
}
|
||||
}
|
||||
keys.sort(Comparator.comparingInt((HydrologyTileKey key) ->
|
||||
Math.max(Math.abs(key.tileX() - centerX), Math.abs(key.tileZ() - centerZ))));
|
||||
return keys;
|
||||
}
|
||||
|
||||
String machineLine() {
|
||||
return String.format(Locale.ROOT,
|
||||
"IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=%d tile_size=%d publication_radius=%d reach_chunks=%d planned_ms=%.3f",
|
||||
tiles, tileSize, publicationRadius, reachChunks, elapsedNanos / 1_000_000D);
|
||||
}
|
||||
|
||||
private static void await(Future<?> plan) throws Exception {
|
||||
try {
|
||||
plan.get();
|
||||
hydrology.prepareWindowsAround(bounds[0], bounds[1], bounds[2], bounds[3], RINGS).get();
|
||||
} catch (ExecutionException failure) {
|
||||
Throwable cause = failure.getCause();
|
||||
if (cause instanceof Error error) {
|
||||
@@ -105,5 +50,17 @@ record ProbeHydrologyPreplan(int tiles, int tileSize, int publicationRadius, int
|
||||
}
|
||||
throw new IllegalStateException(cause);
|
||||
}
|
||||
return Optional.of(new ProbeHydrologyPreplan(windows(bounds), System.nanoTime() - started));
|
||||
}
|
||||
|
||||
static int windows(int[] chunkBounds) {
|
||||
int width = Math.floorDiv(chunkBounds[2], CHUNKS_PER_WINDOW) - Math.floorDiv(chunkBounds[0], CHUNKS_PER_WINDOW) + 1 + 2 * RINGS;
|
||||
int depth = Math.floorDiv(chunkBounds[3], CHUNKS_PER_WINDOW) - Math.floorDiv(chunkBounds[1], CHUNKS_PER_WINDOW) + 1 + 2 * RINGS;
|
||||
return width * depth;
|
||||
}
|
||||
|
||||
String machineLine() {
|
||||
return String.format(Locale.ROOT, "IRIS_GENPROBE_HYDROLOGY_PREPLAN version=2 windows=%d planned_ms=%.3f", windows,
|
||||
elapsedNanos / 1_000_000D);
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@ import art.arcane.iris.generation.runtime.EngineWorldManager;
|
||||
import art.arcane.iris.generation.runtime.EngineWorldManagerProvider;
|
||||
import art.arcane.iris.generation.runtime.MeteredCache;
|
||||
import art.arcane.iris.generation.runtime.PreservationRegistry;
|
||||
import art.arcane.iris.generation.mantle.MantleComponent;
|
||||
import art.arcane.iris.generation.mantle.MantleObjectComponent;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
@@ -84,34 +82,9 @@ final class RealPackProbeSupport {
|
||||
File workRoot = Files.createTempDirectory(options.parent(), "iris-real-pack-probe-").toFile();
|
||||
try {
|
||||
File pack = clonePack(packSource, workRoot);
|
||||
configureRuntime(new File(workRoot, "platform-validation"), false, bindings);
|
||||
configureRuntime(new File(workRoot, "platform-validation"), bindings);
|
||||
validatePack(pack, logPrefix);
|
||||
return new Workspace(workRoot, pack, dimensionKey, logPrefix, false, bindings);
|
||||
} catch (Throwable failure) {
|
||||
clearRuntime();
|
||||
Throwable cleanupFailure = deleteWorkRoot(workRoot);
|
||||
if (cleanupFailure != null) {
|
||||
failure.addSuppressed(cleanupFailure);
|
||||
}
|
||||
throwAsException(failure);
|
||||
throw new IllegalStateException("Unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
static Workspace openPreparedWorkspace(
|
||||
File pack,
|
||||
String dimensionKey,
|
||||
String logPrefix,
|
||||
boolean hydrologyGeneratedVerification
|
||||
) throws Exception {
|
||||
if (pack == null || !pack.isDirectory()) {
|
||||
throw new IllegalArgumentException("Prepared pack folder not found: "
|
||||
+ (pack == null ? "null" : pack.getAbsolutePath()));
|
||||
}
|
||||
File workRoot = Files.createTempDirectory("iris-generated-chunk-probe-").toFile();
|
||||
try {
|
||||
configureRuntime(new File(workRoot, "platform-validation"), hydrologyGeneratedVerification, null);
|
||||
return new Workspace(workRoot, pack, dimensionKey, logPrefix, hydrologyGeneratedVerification, null);
|
||||
return new Workspace(workRoot, pack, dimensionKey, logPrefix, bindings);
|
||||
} catch (Throwable failure) {
|
||||
clearRuntime();
|
||||
Throwable cleanupFailure = deleteWorkRoot(workRoot);
|
||||
@@ -292,7 +265,6 @@ final class RealPackProbeSupport {
|
||||
private final File pack;
|
||||
private final String dimensionKey;
|
||||
private final String logPrefix;
|
||||
private final boolean hydrologyGeneratedVerification;
|
||||
private final RuntimeBindings bindings;
|
||||
private int sessionSequence;
|
||||
private boolean sessionOpen;
|
||||
@@ -303,14 +275,12 @@ final class RealPackProbeSupport {
|
||||
File pack,
|
||||
String dimensionKey,
|
||||
String logPrefix,
|
||||
boolean hydrologyGeneratedVerification,
|
||||
RuntimeBindings bindings
|
||||
) {
|
||||
this.workRoot = workRoot;
|
||||
this.pack = pack;
|
||||
this.dimensionKey = dimensionKey;
|
||||
this.logPrefix = logPrefix;
|
||||
this.hydrologyGeneratedVerification = hydrologyGeneratedVerification;
|
||||
this.bindings = bindings;
|
||||
}
|
||||
|
||||
@@ -328,7 +298,7 @@ final class RealPackProbeSupport {
|
||||
IrisData data = null;
|
||||
Engine engine = null;
|
||||
try {
|
||||
configureRuntime(platformRoot, hydrologyGeneratedVerification, bindings);
|
||||
configureRuntime(platformRoot, bindings);
|
||||
data = IrisData.openRuntime(pack);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
@@ -551,16 +521,6 @@ final class RealPackProbeSupport {
|
||||
}
|
||||
|
||||
private static final class InertPlatformHooks implements EnginePlatformHooks {
|
||||
private final boolean hydrologyGeneratedVerification;
|
||||
|
||||
private InertPlatformHooks(boolean hydrologyGeneratedVerification) {
|
||||
this.hydrologyGeneratedVerification = hydrologyGeneratedVerification;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateMantleComponent(Engine engine, MantleComponent component) {
|
||||
return !hydrologyGeneratedVerification || !(component instanceof MantleObjectComponent);
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearRuntime() {
|
||||
@@ -572,7 +532,7 @@ final class RealPackProbeSupport {
|
||||
StubPlatform.errorSink(null);
|
||||
}
|
||||
|
||||
private static void configureRuntime(File platformRoot, boolean hydrologyGeneratedVerification, RuntimeBindings bindings) {
|
||||
private static void configureRuntime(File platformRoot, RuntimeBindings bindings) {
|
||||
REPORTED.clear();
|
||||
StubPlatform.bindGenerationStateHandlers();
|
||||
StubPlatform.verbose(false);
|
||||
@@ -582,7 +542,7 @@ final class RealPackProbeSupport {
|
||||
(EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
|
||||
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) InertEffects::new);
|
||||
IrisServices.register(EnginePlatformHooks.class, bindings == null
|
||||
? new InertPlatformHooks(hydrologyGeneratedVerification) : bindings);
|
||||
? new InertPlatformHooks() : bindings);
|
||||
IrisPlatforms.unbind();
|
||||
IrisPlatforms.bind(bindings == null ? new StubPlatform(platformRoot) : bindings.create(platformRoot));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -49,11 +49,16 @@ import java.util.function.Consumer;
|
||||
public final class StubPlatform implements IrisPlatform {
|
||||
private static volatile boolean VERBOSE = false;
|
||||
private static volatile Consumer<Throwable> ERROR_SINK = null;
|
||||
private static volatile boolean VANILLA_SOLIDITY = false;
|
||||
|
||||
public static void verbose(boolean verbose) {
|
||||
VERBOSE = verbose;
|
||||
}
|
||||
|
||||
public static void vanillaSolidity(boolean vanilla) {
|
||||
VANILLA_SOLIDITY = vanilla;
|
||||
}
|
||||
|
||||
public static void errorSink(Consumer<Throwable> sink) {
|
||||
ERROR_SINK = sink;
|
||||
}
|
||||
@@ -138,7 +143,14 @@ public final class StubPlatform implements IrisPlatform {
|
||||
|
||||
@Override
|
||||
public boolean isSolid() {
|
||||
return !isAir() && !isFluid();
|
||||
if (isAir() || isFluid()) {
|
||||
return false;
|
||||
}
|
||||
if (!VANILLA_SOLIDITY) {
|
||||
return true;
|
||||
}
|
||||
byte voxel = HydrologyVoxels.classify(this);
|
||||
return voxel == HydrologyVoxels.SOLID || voxel == HydrologyVoxels.WATER && isWaterLogged();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class CavePoolChecksTest {
|
||||
@Test
|
||||
public void everyContainmentCountFailsIncludingForeignFluidContacts() {
|
||||
CavePoolProbe.Counts foreign = counts(0, 0, 0, 1, 0, 0, 0, 0);
|
||||
assertEquals("a pool touching another fluid fails as the standalone probe does", 1L, foreign.failures());
|
||||
assertEquals(36L, counts(1, 2, 3, 4, 5, 6, 7, 8).failures());
|
||||
assertEquals(0L, counts(0, 0, 0, 0, 0, 0, 0, 0).failures());
|
||||
}
|
||||
|
||||
private static CavePoolProbe.Counts counts(int air, int open, int sky, int foreign, int waterlogged, int gravity, int fluidAbove,
|
||||
int gravityCeilings) {
|
||||
return new CavePoolProbe.Counts(1, 1, 1, 1, Map.of(), List.of(), List.of(), Map.of(), air, open, sky, foreign, waterlogged, gravity,
|
||||
fluidAbove, gravityCeilings, Map.of(), List.of());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -101,16 +100,27 @@ public final class GenerationProbeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenerationAreaCoversEveryScheduledChunk() {
|
||||
public void hydrologyRegionsCoverEveryScheduledChunkCentreFirst() {
|
||||
GenerationProbe.ProbeConfiguration configuration = new GenerationProbe.ProbeConfiguration(
|
||||
new File("/tmp/pack"), "overworld", 64, 256, 2048, 2048, true, false, 1337L);
|
||||
HydrologyTileCache.PregenerationArea area = GenerationProbe.pregenerationArea(configuration);
|
||||
new File("/tmp/pack"), "overworld", 64, 2048, 2048, 2048, true, false, 1337L);
|
||||
List<long[]> regions = GenerationProbe.hydrologyRegions(configuration);
|
||||
Set<Long> keys = new HashSet<>();
|
||||
for (long[] region : regions) {
|
||||
assertTrue(keys.add(region[0] << 32 | (region[1] & 0xFFFFFFFFL)));
|
||||
}
|
||||
|
||||
for (GenerationProbe.ChunkCoordinate coordinate : GenerationProbe.scheduleCoordinates(320, 2048, 2048)) {
|
||||
assertTrue(((long) coordinate.x() << 4) >= area.minimumBlockX());
|
||||
assertTrue((((long) coordinate.x() << 4) + 15L) <= area.maximumBlockX());
|
||||
assertTrue(((long) coordinate.z() << 4) >= area.minimumBlockZ());
|
||||
assertTrue((((long) coordinate.z() << 4) + 15L) <= area.maximumBlockZ());
|
||||
for (GenerationProbe.ChunkCoordinate coordinate : GenerationProbe.scheduleCoordinates(2112, 2048, 2048)) {
|
||||
long regionX = Math.floorDiv(coordinate.x(), WindowKey.SIZE >> 4);
|
||||
long regionZ = Math.floorDiv(coordinate.z(), WindowKey.SIZE >> 4);
|
||||
assertTrue(keys.contains(regionX << 32 | (regionZ & 0xFFFFFFFFL)));
|
||||
}
|
||||
assertEquals(64L, regions.getFirst()[0]);
|
||||
assertEquals(64L, regions.getFirst()[1]);
|
||||
long previousRing = 0L;
|
||||
for (long[] region : regions) {
|
||||
long ring = Math.max(Math.abs(region[0] - 64L), Math.abs(region[1] - 64L));
|
||||
assertTrue(ring >= previousRing);
|
||||
previousRing = ring;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,35 +204,21 @@ public final class GenerationProbeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hydrologyPreplanCoversEveryTileAScheduledChunkComposesFromPlusTheNeighbourRing() {
|
||||
public void hydrologyPreplanCountsTheScheduleWindowsPlusTheirNeighbourRing() {
|
||||
GenerationProbe.ProbeConfiguration configuration = new GenerationProbe.ProbeConfiguration(
|
||||
new File("/tmp/pack"), "overworld", 256, 1024, 2048, 2048, false, false, 1337L);
|
||||
int tileSize = 512;
|
||||
int publicationRadius = 96;
|
||||
int reachChunks = 13;
|
||||
List<HydrologyTileKey> keys = ProbeHydrologyPreplan.tiles(configuration, tileSize, publicationRadius, reachChunks);
|
||||
Set<HydrologyTileKey> planned = new HashSet<>(keys);
|
||||
int[] bounds = GenerationProbe.scheduledChunkBounds(configuration);
|
||||
|
||||
assertEquals(keys.size(), planned.size());
|
||||
for (GenerationProbe.ChunkCoordinate chunk : GenerationProbe.scheduleCoordinates(1280, 2048, 2048)) {
|
||||
for (int offsetX = -reachChunks; offsetX <= reachChunks; offsetX++) {
|
||||
for (int offsetZ = -reachChunks; offsetZ <= reachChunks; offsetZ++) {
|
||||
long minimumX = (long) (chunk.x() + offsetX) * 16L;
|
||||
long minimumZ = (long) (chunk.z() + offsetZ) * 16L;
|
||||
int firstX = Math.floorDiv(Math.toIntExact(minimumX - publicationRadius), tileSize) - 1;
|
||||
int lastX = Math.floorDiv(Math.toIntExact(minimumX + 15L + publicationRadius), tileSize) + 1;
|
||||
int firstZ = Math.floorDiv(Math.toIntExact(minimumZ - publicationRadius), tileSize) - 1;
|
||||
int lastZ = Math.floorDiv(Math.toIntExact(minimumZ + 15L + publicationRadius), tileSize) + 1;
|
||||
for (int tileX = firstX; tileX <= lastX; tileX++) {
|
||||
for (int tileZ = firstZ; tileZ <= lastZ; tileZ++) {
|
||||
assertTrue(planned.contains(new HydrologyTileKey(tileX, tileZ)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(chunk.x() >= bounds[0] && chunk.x() <= bounds[2]);
|
||||
assertTrue(chunk.z() >= bounds[1] && chunk.z() <= bounds[3]);
|
||||
}
|
||||
HydrologyTileKey first = keys.getFirst();
|
||||
assertEquals(new HydrologyTileKey(Math.floorDiv(2048 * 16 + 8, tileSize), Math.floorDiv(2048 * 16 + 8, tileSize)), first);
|
||||
int chunks = WindowKey.SIZE >> 4;
|
||||
int width = Math.floorDiv(bounds[2], chunks) - Math.floorDiv(bounds[0], chunks) + 3;
|
||||
int depth = Math.floorDiv(bounds[3], chunks) - Math.floorDiv(bounds[1], chunks) + 3;
|
||||
assertEquals(width * depth, ProbeHydrologyPreplan.windows(bounds));
|
||||
assertEquals("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=2 windows=16 planned_ms=1.500",
|
||||
new ProbeHydrologyPreplan(16, 1_500_000L).machineLine());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,283 +1,24 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.util.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();
|
||||
}
|
||||
|
||||
public class HydrologyBlockProbeTest {
|
||||
@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 implicitAquaticWaterCountsWhileDryPlantsAndOtherFluidsStillFail() {
|
||||
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
|
||||
HydrologyColumnLayer layer = layer(HydrologyFeatureType.RIFFLE, 2, 3, true);
|
||||
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = Map.of(
|
||||
RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(8, false, layer, state("water")));
|
||||
for (String block : List.of("seagrass", "tall_seagrass[half=lower]", "tall_seagrass[half=upper]",
|
||||
"kelp[age=3]", "kelp_plant", "bubble_column[drag=false]")) {
|
||||
set(chunks, 8, 3, 8, block);
|
||||
HydrologyBlockProbe.Evidence evidence = inspect(chunks, planned);
|
||||
assertEquals(block, 1, evidence.plannedWetVoxels);
|
||||
assertEquals(block, 0, evidence.missingWetVoxels);
|
||||
assertTrue(block, HydrologyBlockProbe.failures(evidence, 8, Set.of("channel")).isEmpty());
|
||||
}
|
||||
for (String block : List.of("short_grass", "tall_grass[half=lower]", "fern", "lily_pad",
|
||||
"sugar_cane", "dead_bush", "brain_coral[waterlogged=false]")) {
|
||||
set(chunks, 8, 3, 8, block);
|
||||
HydrologyBlockProbe.Evidence evidence = inspect(chunks, planned);
|
||||
assertEquals(block, 1, evidence.missingWetVoxels);
|
||||
assertFalse(block, HydrologyBlockProbe.failures(evidence, 8, Set.of("channel")).isEmpty());
|
||||
}
|
||||
set(chunks, 8, 3, 8, "kelp_plant");
|
||||
Map<Long, HydrologyBlockProbe.PlannedColumn> lava = Map.of(
|
||||
RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(8, false, layer, state("lava")));
|
||||
HydrologyBlockProbe.Evidence rejected = inspect(chunks, lava);
|
||||
assertEquals(1, rejected.missingWetVoxels);
|
||||
assertEquals(Map.of("minecraft:kelp_plant", 1L), rejected.plannedWetBlockCounts);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wetBlockCountsCoverEveryVoxelBeyondTheExampleLimit() {
|
||||
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
|
||||
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = new HashMap<>();
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 4; z++) {
|
||||
HydrologyColumnLayer layer = layer(HydrologyFeatureType.RIFFLE, 2, 3, true);
|
||||
planned.put(RiverFootprint.pack(x, z), new HydrologyBlockProbe.PlannedColumn(8, false, layer, state("water")));
|
||||
set(chunks, x, 3, z, x < 8 ? "seagrass" : "short_grass");
|
||||
}
|
||||
}
|
||||
HydrologyBlockProbe.Evidence evidence = inspect(chunks, planned);
|
||||
|
||||
assertEquals(64, evidence.plannedWetVoxels);
|
||||
assertEquals(32, evidence.missingWetVoxels);
|
||||
assertEquals(16, evidence.examples.size());
|
||||
assertEquals(Map.of("minecraft:seagrass", 32L, "minecraft:short_grass", 32L), evidence.plannedWetBlockCounts);
|
||||
}
|
||||
|
||||
@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<NativeBlockState> blocks = Hunk.newArrayHunk(16, 16, 16);
|
||||
Hunk<NativeBiome> 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 NativeBlockState 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");
|
||||
public void failuresNameMissingSurfaceLeaksTicksAndCoverage() {
|
||||
HydrologyBlockProbe.Evidence evidence = new HydrologyBlockProbe.Evidence();
|
||||
evidence.scan.missingSurfaces = 2;
|
||||
evidence.scan.containmentLeaks = 3;
|
||||
evidence.scan.tickConversionCells = 4;
|
||||
evidence.channelColumns = 5;
|
||||
assertEquals(List.of(
|
||||
"Generated blocks miss the planned water surface in 2 columns.",
|
||||
"Generated water leaks into open ground in 3 cells.",
|
||||
"Fluid ticks would move generated water in 4 cells.",
|
||||
"Required generated coverage is absent: pond"), HydrologyBlockProbe.failures(evidence, Set.of("channel", "pond")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.config.IrisHydrology;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.plan.RegionKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.RegionPlan;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlanner;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
|
||||
import art.arcane.iris.generation.hydrology.water.Pond;
|
||||
import art.arcane.iris.generation.hydrology.water.PondKind;
|
||||
import art.arcane.iris.generation.hydrology.water.PondMask;
|
||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||
import art.arcane.iris.generation.hydrology.water.ReachStyle;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HydrologyMetricsTest {
|
||||
private static final int SEA = 40;
|
||||
private static final int NATURAL = 70;
|
||||
private static final HydrologySettings SETTINGS = HydrologySettings.compile(new IrisHydrology().setEnabled(true), 1L, SEA, 0, 320);
|
||||
private static final HydrologyTier STREAM = SETTINGS.tiers().get(0);
|
||||
private static final HydrologyTier RIVER = SETTINGS.tiers().get(1);
|
||||
private static final WindowKey WEST = new WindowKey(0, 0);
|
||||
private static final WindowKey EAST = new WindowKey(1, 0);
|
||||
private static final HydrologyWindows.WindowRange BOTH = new HydrologyWindows.WindowRange(0, 1, 0, 0);
|
||||
private static final HydrologyWindows.WindowRange WEST_ONLY = new HydrologyWindows.WindowRange(0, 0, 0, 0);
|
||||
|
||||
@Test
|
||||
public void planSeamsFireOnSharedOwnershipMissingLinksAndOverlappingPonds() {
|
||||
Reach shared = reach(1L, STREAM, 99L, 1000f, new int[]{60, 59}, 10f, 10f, 20f, 10f);
|
||||
Pond west = pond(10L, 55, square(100, 100, 3), 98L, 104, 104);
|
||||
Pond east = pond(11L, 55, square(100, 100, 3), 0L, 108, 108);
|
||||
List<String> examples = new ArrayList<>();
|
||||
HydrologyPlanMetrics metrics = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(shared), List.of(west)),
|
||||
plan(EAST, List.of(shared), List.of(east))), List.of(), BOTH, examples::add);
|
||||
assertEquals(examples.toString(), 2L, metrics.seams(WEST));
|
||||
assertEquals(examples.toString(), 2L, metrics.seams(EAST));
|
||||
assertTrue(examples.toString(), examples.contains("seam reach 1 owned by " + WEST + " and " + EAST));
|
||||
assertTrue(examples.toString(), examples.contains("seam reach 1 links to missing reach 99"));
|
||||
assertTrue(examples.toString(), examples.contains("seam pond 10 drains to missing reach 98"));
|
||||
assertTrue(examples.toString(), examples.contains("seam ponds 10 and 11 from " + WEST + " and " + EAST + " overlap"));
|
||||
HydrologyPlanMetrics clean = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(reach(1L, STREAM, 0L, 1000f, new int[]{60, 59},
|
||||
10f, 10f, 20f, 10f)), List.of()), plan(EAST, List.of(), List.of())), List.of(), BOTH, examples::add);
|
||||
assertEquals(0L, clean.seams(WEST) + clean.seams(EAST));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aStemFollowsItsLargestFeederThroughAJunction() {
|
||||
Reach small = reach(1L, RIVER, 3L, 5000f, new int[]{90, 80}, 0f, 0f, 200f, 0f);
|
||||
Reach large = reach(2L, RIVER, 3L, 6000f, new int[]{90, 80}, 200f, 300f, 200f, 0f);
|
||||
Reach trunk = reach(3L, RIVER, 0L, 11000f, new int[]{80, 70}, 200f, 0f, 350f, 0f);
|
||||
HydrologyPlanMetrics metrics = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(small, large, trunk), List.of()),
|
||||
plan(EAST, List.of(), List.of())), List.of(), BOTH, ignored -> {
|
||||
});
|
||||
assertEquals(1, metrics.rivers(WEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aStemContinuesThroughThePondItFillsIntoThePondOutlet() {
|
||||
Pond pond = pond(10L, 75, square(240, -16, 8), 2L, 256, 0);
|
||||
Reach inflow = reach(1L, RIVER, 0L, 6000f, new int[]{90, 76}, 0f, 0f, 250f, 0f);
|
||||
Reach outlet = reach(2L, RIVER, 0L, 6000f, new int[]{75, 70}, 270f, 0f, 470f, 0f);
|
||||
HydrologyPlanMetrics metrics = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(inflow, outlet), List.of(pond)),
|
||||
plan(EAST, List.of(), List.of())), List.of(), BOTH, ignored -> {
|
||||
});
|
||||
assertEquals(pond, metrics.endPond(inflow));
|
||||
assertEquals(1, metrics.rivers(WEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aStemIsCountedOnlyByTheWindowThatOwnsItsLowestReach() {
|
||||
Reach upper = reach(1L, RIVER, 2L, 6000f, new int[]{90, 80}, 100f, 0f, 500f, 0f);
|
||||
Reach lower = reach(2L, RIVER, 0L, 6000f, new int[]{80, 70}, 500f, 0f, 700f, 0f);
|
||||
Map<WindowKey, WindowPlan> plans = windows(plan(WEST, List.of(upper), List.of()), plan(EAST, List.of(lower), List.of()));
|
||||
HydrologyPlanMetrics west = new HydrologyPlanMetrics(SETTINGS, plans, List.of(), WEST_ONLY, ignored -> {
|
||||
});
|
||||
HydrologyPlanMetrics both = new HydrologyPlanMetrics(SETTINGS, plans, List.of(), BOTH, ignored -> {
|
||||
});
|
||||
assertEquals(0, west.rivers(WEST));
|
||||
assertEquals(0, both.rivers(WEST));
|
||||
assertEquals(1, both.rivers(EAST));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPondIsPerchedAgainstReachesOtherThanItsOwnInflowsAndOutlet() {
|
||||
Pond high = pond(10L, 60, square(96, 96, 3), 2L, 100, 100);
|
||||
Reach outlet = reach(2L, STREAM, 0L, 1000f, new int[]{60, 59}, 100f, 100f, 100f, 140f);
|
||||
Reach below = reach(3L, STREAM, 0L, 1000f, new int[]{50, 49}, 200f, 100f, 200f, 140f);
|
||||
HydrologyPlanMetrics perched = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(outlet, below), List.of(high)),
|
||||
plan(EAST, List.of(), List.of())), List.of(), BOTH, ignored -> {
|
||||
});
|
||||
assertEquals(1, perched.perched(WEST));
|
||||
Pond low = pond(10L, 55, square(96, 96, 3), 2L, 100, 100);
|
||||
HydrologyPlanMetrics level = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(outlet, below), List.of(low)),
|
||||
plan(EAST, List.of(), List.of())), List.of(), BOTH, ignored -> {
|
||||
});
|
||||
assertEquals(0, level.perched(WEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void risingWaterFiresAlongReachesAcrossLinksOutOfPondsIntoPondsAndAtMouths() {
|
||||
Reach along = reach(1L, STREAM, 0L, 1000f, new int[]{61, 60, 59}, 10f, 10f, 14f, 10f, 18f, 10f);
|
||||
Map<Long, Column> columns = new HashMap<>();
|
||||
wet(columns, along, 60, 59, 61);
|
||||
assertRises(1L, "rise along reach 1 from 59 to 61", List.of(along), List.of(), columns);
|
||||
|
||||
Reach upper = reach(1L, STREAM, 2L, 1000f, new int[]{60, 55}, 10f, 10f, 14f, 10f);
|
||||
Reach lower = reach(2L, STREAM, 0L, 1000f, new int[]{55, 54}, 14f, 20f, 14f, 24f);
|
||||
Map<Long, Column> linked = new HashMap<>();
|
||||
wet(linked, upper, 60, 55);
|
||||
wet(linked, lower, 57, 54);
|
||||
assertRises(1L, "rise from reach 1 at 55 into reach 2 at 57", List.of(upper, lower), List.of(), linked);
|
||||
|
||||
Pond pond = pond(10L, 50, square(96, 96, 3), 3L, 100, 100);
|
||||
Reach outlet = reach(3L, STREAM, 0L, 1000f, new int[]{50, 49}, 120f, 100f, 130f, 100f);
|
||||
Map<Long, Column> outflow = new HashMap<>();
|
||||
wet(outflow, outlet, 52, 49);
|
||||
assertRises(1L, "rise from pond 10 at 50 into outlet 3 at 52", List.of(outlet), List.of(pond), outflow);
|
||||
|
||||
Reach inflow = reach(4L, STREAM, 0L, 1000f, new int[]{60, 50, 50}, 60f, 100f, 80f, 100f, 100f, 100f);
|
||||
Map<Long, Column> backwater = new HashMap<>();
|
||||
wet(backwater, inflow, 60, 48, HydrologyVoxels.NO_WATER);
|
||||
assertRises(1L, "rise from reach 4 at 48 into pond 10 at 50", List.of(inflow), List.of(pond(10L, 50, square(96, 96, 3), 0L, 100, 100)),
|
||||
backwater);
|
||||
|
||||
Reach mouth = reach(5L, STREAM, 0L, 1000f, new int[]{45, 40}, 10f, 10f, 14f, 10f);
|
||||
mouth.style()[1] = ReachStyle.MOUTH;
|
||||
Map<Long, Column> sea = new HashMap<>();
|
||||
wet(sea, mouth, 45, 39);
|
||||
assertRises(1L, "rise from the sea at 40 into the mouth of reach 5 at 39", List.of(mouth), List.of(), sea);
|
||||
|
||||
Map<Long, Column> level = new HashMap<>();
|
||||
wet(level, mouth, 45, 40);
|
||||
assertRises(0L, null, List.of(mouth), List.of(), level);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkSeamsFireOnDryColumnsLeftBelowTheirBermAndOnePondAtTwoLevels() {
|
||||
HydrologyPlanMetrics plans = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(), List.of()), plan(EAST, List.of(), List.of())),
|
||||
List.of(), BOTH, ignored -> {
|
||||
});
|
||||
Map<Long, Column> columns = new HashMap<>();
|
||||
columns.put(HydrologyVoxels.columnKey(15, 4), new Column(WaterRole.CHANNEL, 72, NATURAL, 1L));
|
||||
columns.put(HydrologyVoxels.columnKey(16, 4), new Column(WaterRole.BANK, HydrologyVoxels.NO_WATER, NATURAL - 1, 0L));
|
||||
columns.put(HydrologyVoxels.columnKey(31, 8), new Column(WaterRole.POND, 60, 58, 7L));
|
||||
columns.put(HydrologyVoxels.columnKey(32, 8), new Column(WaterRole.POND, 61, 58, 7L));
|
||||
List<String> examples = new ArrayList<>();
|
||||
HydrologyRasterMetrics metrics = new HydrologyRasterMetrics(SETTINGS, plans, rasters(columns), (x, z) -> NATURAL, BOTH, examples::add);
|
||||
assertEquals(examples.toString(), 2L, metrics.chunkSeams(WEST));
|
||||
assertTrue(examples.toString(), examples.contains("seam dry column left below its berm across chunks at 15,4 / 16,4"));
|
||||
assertTrue(examples.toString(), examples.contains("seam one pond at two levels across chunks at 31,8 / 32,8"));
|
||||
columns.put(HydrologyVoxels.columnKey(16, 4), new Column(WaterRole.BANK, HydrologyVoxels.NO_WATER, 72, 0L));
|
||||
columns.put(HydrologyVoxels.columnKey(32, 8), new Column(WaterRole.POND, 60, 58, 7L));
|
||||
assertEquals(0L, new HydrologyRasterMetrics(SETTINGS, plans, rasters(columns), (x, z) -> NATURAL, BOTH, ignored -> {
|
||||
}).chunkSeams(WEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aChunkSeamCountsAnyDryColumnBelowTheWaterBesideItExceptOverTheSea() {
|
||||
HydrologyPlanMetrics plans = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(), List.of()), plan(EAST, List.of(), List.of())),
|
||||
List.of(), BOTH, ignored -> {
|
||||
});
|
||||
Map<Long, Column> columns = new HashMap<>();
|
||||
columns.put(HydrologyVoxels.columnKey(15, 4), new Column(WaterRole.CHANNEL, 90, 88, 1L));
|
||||
columns.put(HydrologyVoxels.columnKey(16, 4), new Column(WaterRole.BANK, HydrologyVoxels.NO_WATER, 85, 0L));
|
||||
columns.put(HydrologyVoxels.columnKey(15, 20), new Column(WaterRole.MOUTH, SEA + 2, SEA - 3, 2L));
|
||||
columns.put(HydrologyVoxels.columnKey(16, 20), new Column(WaterRole.SHORE, HydrologyVoxels.NO_WATER, SEA - 2, 0L));
|
||||
columns.put(HydrologyVoxels.columnKey(15, 36), new Column(WaterRole.MOUTH, SEA, SEA - 3, 3L));
|
||||
columns.put(HydrologyVoxels.columnKey(16, 36), new Column(WaterRole.SHORE, HydrologyVoxels.NO_WATER, SEA - 2, 0L));
|
||||
List<String> examples = new ArrayList<>();
|
||||
HydrologyRasterMetrics metrics = new HydrologyRasterMetrics(SETTINGS, plans, rasters(columns), (x, z) -> NATURAL - 30, BOTH, examples::add);
|
||||
assertEquals(examples.toString(), 2L, metrics.chunkSeams(WEST));
|
||||
assertTrue(examples.toString(), examples.contains("seam dry column left below its berm across chunks at 15,4 / 16,4"));
|
||||
assertTrue(examples.toString(), examples.contains("seam dry column left below its berm across chunks at 15,20 / 16,20"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPlanMadeWhileAPlanThatNeedsItRunsIsDepthOneOnAnyThread() throws Exception {
|
||||
RegionKey needed = WindowPlanner.lakeRegions(WEST).get(0);
|
||||
RegionKey unrelated = new RegionKey(needed.rx() + 100, needed.rz() + 100);
|
||||
assertEquals(1, depth(regions -> regions.apply(needed)));
|
||||
ExecutorService other = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
assertEquals(1, depth(regions -> {
|
||||
Future<RegionPlan> elsewhere = other.submit(() -> regions.apply(needed));
|
||||
try {
|
||||
return elsewhere.get();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException(failure);
|
||||
}
|
||||
}));
|
||||
} finally {
|
||||
other.shutdownNow();
|
||||
}
|
||||
assertEquals(0, depth(regions -> regions.apply(unrelated)));
|
||||
HydrologyColdRuns.PlanGauge sequential = new HydrologyColdRuns.PlanGauge(Set.of(), () -> {
|
||||
});
|
||||
sequential.region(key -> null).apply(needed);
|
||||
sequential.window((key, plans) -> null).apply(WEST, List.of());
|
||||
assertEquals(0, sequential.deepest());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missedTargetsIgnoreColdLatencyAndNeedSixPondLevelsPerNineTiles() {
|
||||
HydrologyReport.Result spread = result(1000_000D, 2.25D, Set.of(50, 51));
|
||||
assertTrue(spread.missedTargets().toString(), !spread.missedTargets().contains("coldFirstChunkMs"));
|
||||
assertTrue(spread.missedTargets().toString(), !spread.missedTargets().contains("pondLevelsDistinct"));
|
||||
HydrologyReport.Result flat = result(10D, 9D, Set.of(50, 51, 52, 53, 54));
|
||||
assertTrue(flat.missedTargets().toString(), flat.missedTargets().contains("pondLevelsDistinct"));
|
||||
}
|
||||
|
||||
private static HydrologyReport.Result result(double coldMs, double landTiles, Set<Integer> levels) {
|
||||
HydrologyReport.WindowMetrics window = new HydrologyReport.WindowMetrics(WEST, coldMs, 0L, 0L, 0D, 0D, 0, landTiles, 100, 0D, 100,
|
||||
levels, 0, 0L, 0L, 0L, 0L, 0L, 0L, 0, 0L, 0, 0L, 0L, "digest");
|
||||
return new HydrologyReport.Result(List.of(window), 0D, 0, 1, 0L, 0L, 0D, 0L, "digest", List.of(), List.of());
|
||||
}
|
||||
|
||||
private static int depth(Function<Function<RegionKey, RegionPlan>, RegionPlan> body) {
|
||||
HydrologyColdRuns.PlanGauge gauge = new HydrologyColdRuns.PlanGauge(Set.of(), () -> {
|
||||
});
|
||||
Function<RegionKey, RegionPlan> regions = gauge.region(key -> null);
|
||||
BiFunction<WindowKey, List<RegionPlan>, WindowPlan> windows = gauge.window((key, plans) -> {
|
||||
body.apply(regions);
|
||||
return null;
|
||||
});
|
||||
windows.apply(WEST, List.of());
|
||||
return gauge.deepest();
|
||||
}
|
||||
|
||||
private static void assertRises(long expected, String example, List<Reach> reaches, List<Pond> ponds, Map<Long, Column> columns) {
|
||||
WindowPlan plan = plan(WEST, reaches, ponds);
|
||||
HydrologyPlanMetrics plans = new HydrologyPlanMetrics(SETTINGS, windows(plan, plan(EAST, List.of(), List.of())), List.of(), BOTH, ignored -> {
|
||||
});
|
||||
List<String> examples = new ArrayList<>();
|
||||
HydrologyRasterMetrics metrics = new HydrologyRasterMetrics(SETTINGS, plans, rasters(columns), (x, z) -> NATURAL, BOTH, examples::add);
|
||||
assertEquals(examples.toString(), expected, metrics.rising(WEST, plan));
|
||||
if (example != null) {
|
||||
assertTrue(examples.toString(), examples.contains(example));
|
||||
}
|
||||
}
|
||||
|
||||
private static void wet(Map<Long, Column> columns, Reach reach, int... heads) {
|
||||
for (int point = 0; point < heads.length; point++) {
|
||||
if (heads[point] == HydrologyVoxels.NO_WATER) {
|
||||
continue;
|
||||
}
|
||||
int x = (int) Math.floor(reach.xs()[point]);
|
||||
int z = (int) Math.floor(reach.zs()[point]);
|
||||
columns.put(HydrologyVoxels.columnKey(x, z), new Column(WaterRole.CHANNEL, heads[point], heads[point] - 2, reach.id()));
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyRasterMetrics.Rasters rasters(Map<Long, Column> columns) {
|
||||
Map<Long, ChunkWaterRaster> chunks = new HashMap<>();
|
||||
return (chunkX, chunkZ) -> chunks.computeIfAbsent(HydrologyVoxels.chunkKey(chunkX, chunkZ), ignored -> raster(columns, chunkX, chunkZ));
|
||||
}
|
||||
|
||||
private static ChunkWaterRaster raster(Map<Long, Column> columns, int chunkX, int chunkZ) {
|
||||
ChunkWaterRaster raster = mock(ChunkWaterRaster.class);
|
||||
when(raster.empty()).thenReturn(false);
|
||||
when(raster.role(anyInt(), anyInt())).thenAnswer(call -> column(columns, chunkX, chunkZ, call.getArgument(0), call.getArgument(1)).role());
|
||||
when(raster.waterHead(anyInt(), anyInt())).thenAnswer(call -> column(columns, chunkX, chunkZ, call.getArgument(0), call.getArgument(1)).head());
|
||||
when(raster.terrainHeight(anyInt(), anyInt())).thenAnswer(call -> column(columns, chunkX, chunkZ, call.getArgument(0), call.getArgument(1))
|
||||
.terrain());
|
||||
when(raster.featureId(anyInt(), anyInt())).thenAnswer(call -> column(columns, chunkX, chunkZ, call.getArgument(0), call.getArgument(1))
|
||||
.featureId());
|
||||
return raster;
|
||||
}
|
||||
|
||||
private static Column column(Map<Long, Column> columns, int chunkX, int chunkZ, int lx, int lz) {
|
||||
Column column = columns.get(HydrologyVoxels.columnKey((chunkX << 4) + lx, (chunkZ << 4) + lz));
|
||||
return column == null ? new Column(WaterRole.NONE, HydrologyVoxels.NO_WATER, NATURAL, 0L) : column;
|
||||
}
|
||||
|
||||
private record Column(WaterRole role, int head, int terrain, long featureId) {
|
||||
}
|
||||
|
||||
private static Map<WindowKey, WindowPlan> windows(WindowPlan... plans) {
|
||||
Map<WindowKey, WindowPlan> windows = new LinkedHashMap<>();
|
||||
for (WindowPlan plan : plans) {
|
||||
windows.put(plan.key(), plan);
|
||||
}
|
||||
return windows;
|
||||
}
|
||||
|
||||
private static WindowPlan plan(WindowKey key, List<Reach> reaches, List<Pond> ponds) {
|
||||
return new WindowPlan(key, reaches, ponds, List.of());
|
||||
}
|
||||
|
||||
private static Reach reach(long id, HydrologyTier tier, long downstream, float catchment, int[] grade, float... coordinates) {
|
||||
int points = coordinates.length / 2;
|
||||
float[] xs = new float[points];
|
||||
float[] zs = new float[points];
|
||||
float[] halfWidth = new float[points];
|
||||
float[] depth = new float[points];
|
||||
ReachStyle[] style = new ReachStyle[points];
|
||||
for (int point = 0; point < points; point++) {
|
||||
xs[point] = coordinates[2 * point];
|
||||
zs[point] = coordinates[2 * point + 1];
|
||||
halfWidth[point] = 1f;
|
||||
depth[point] = 1f;
|
||||
}
|
||||
Arrays.fill(style, ReachStyle.RUN);
|
||||
return new Reach(id, tier, downstream, xs, zs, grade, halfWidth, depth, style, catchment);
|
||||
}
|
||||
|
||||
private static Pond pond(long id, int level, PondMask mask, long outlet, int floorX, int floorZ) {
|
||||
return new Pond(id, PondKind.NATURAL, level, level - 4, floorX, floorZ, level + 1f, mask, outlet, 0, "minecraft:water", floorX, floorZ);
|
||||
}
|
||||
|
||||
private static PondMask square(int originX, int originZ, int nodes) {
|
||||
long[] bits = new long[(nodes * nodes + 63) >>> 6];
|
||||
for (int index = 0; index < nodes * nodes; index++) {
|
||||
bits[index >>> 6] |= 1L << index;
|
||||
}
|
||||
return new PondMask(originX, originZ, 8, nodes, nodes, bits);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,22 +0,0 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public final class HydrologyPlanDigestProbeTest {
|
||||
@Test
|
||||
public void pregenerationAreaCoversTheRegionsOfTheRadius() {
|
||||
assertArea(HydrologyPlanDigestProbe.pregenerationArea(1000), -1024L, 1023L);
|
||||
assertArea(HydrologyPlanDigestProbe.pregenerationArea(1500), -1536L, 1535L);
|
||||
assertArea(HydrologyPlanDigestProbe.pregenerationArea(2000), -2048L, 2047L);
|
||||
}
|
||||
|
||||
private static void assertArea(HydrologyTileCache.PregenerationArea area, long minimum, long maximum) {
|
||||
assertEquals(minimum, area.minimumBlockX());
|
||||
assertEquals(minimum, area.minimumBlockZ());
|
||||
assertEquals(maximum, area.maximumBlockX());
|
||||
assertEquals(maximum, area.maximumBlockZ());
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTile;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.EnumSet;
|
||||
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.localDiagnosticCandidates().stream().limit(16).toList().toString(),
|
||||
tile.features().stream().anyMatch(feature -> feature.type() == HydrologyFeatureType.MOUTH));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inlandFixturePublishesAContainedWaterfall() {
|
||||
HydrologyTile tile = HydrologyPlannerProbe.surfaceWaterfallTile();
|
||||
assertTrue(tile.localDiagnosticCandidates().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 =
|
||||
HydrologyPlannerProbe.deterministicAcceptedCoverage();
|
||||
|
||||
assertEquals(HydrologyPlannerProbe.REQUIRED_FEATURE_TYPES, coverage.keySet());
|
||||
for (Map.Entry<HydrologyFeatureType, HydrologyFeatureRef> entry : coverage.entrySet()) {
|
||||
assertEquals(entry.getKey(), entry.getValue().type());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requiredCoverageExcludesTheUnproducedRidgeBore() {
|
||||
assertFalse(HydrologyPlannerProbe.REQUIRED_FEATURE_TYPES.contains(HydrologyFeatureType.RIDGE_BORE));
|
||||
assertEquals(
|
||||
EnumSet.complementOf(EnumSet.of(HydrologyFeatureType.RIDGE_BORE, HydrologyFeatureType.STANDING_POOL)),
|
||||
HydrologyPlannerProbe.REQUIRED_FEATURE_TYPES
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyProbeTest {
|
||||
private static final HydrologyWindows.WindowRange LAND = new HydrologyWindows.WindowRange(4, 4, 4, 4);
|
||||
private static final List<String> METRICS = List.of("coldFirstChunkMs", "windowCpuMs", "regionCpuMs", "maxDependencyDepth",
|
||||
"riversPerTile", "streamBlocksPerTile", "pondsPerTile", "pondLevelsDistinct", "pondsPerched8", "containmentLeaks",
|
||||
"risingWaterSegments", "seamMismatches", "tickConversionCells", "missingSurface", "bermOverCap", "inlets", "inletFailures",
|
||||
"cavePools", "cavePoolFailures");
|
||||
|
||||
@Test
|
||||
public void reportsAllMetricFields() throws Exception {
|
||||
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
|
||||
List<String> lines = HydrologyProbe.measure(fixture.inputs(), LAND).lines();
|
||||
Map<String, String> window = fields(line(lines, "IRIS_HYDROLOGY_WINDOW"));
|
||||
Map<String, String> total = fields(line(lines, "IRIS_HYDROLOGY_TOTAL"));
|
||||
assertEquals("4", window.get("wx"));
|
||||
assertEquals("4", window.get("wz"));
|
||||
for (String metric : METRICS) {
|
||||
assertNumber(metric, window.get(metric));
|
||||
assertNumber(metric, total.get(metric));
|
||||
}
|
||||
assertEquals("1", total.get("windows"));
|
||||
assertEquals("0", total.get("maxDependencyDepth"));
|
||||
assertEquals("0", total.get("inlets"));
|
||||
assertEquals("0", total.get("cavePools"));
|
||||
assertTrue("land " + total.get("landTiles"), Double.parseDouble(total.get("landTiles")) > 0D);
|
||||
assertTrue("streams " + total.get("streamBlocksPerTile"), Double.parseDouble(total.get("streamBlocksPerTile")) > 0D);
|
||||
assertTrue("ponds " + total.get("pondsPerTile"), Double.parseDouble(total.get("pondsPerTile")) > 0D);
|
||||
assertTrue("cold " + total.get("coldFirstChunkMs"), Double.parseDouble(total.get("coldFirstChunkMs")) > 0D);
|
||||
assertTrue("generated " + total.get("generatedChunks"), Long.parseLong(total.get("generatedChunks")) > 0L);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failsOnContainmentLeak() throws Exception {
|
||||
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
|
||||
HydrologyReport.Result clean = HydrologyProbe.measure(fixture.inputs(), LAND);
|
||||
assertEquals(clean.lines().toString(), 0L, clean.sum(HydrologyReport.WindowMetrics::containmentLeaks));
|
||||
int[] hole = bankBesideWater(fixture);
|
||||
assertNotNull("no bank column beside water in window " + LAND, hole);
|
||||
HydrologyReport.Result leaky = HydrologyProbe.measure(fixture.inputs(blocks -> {
|
||||
if (blocks.chunkX() == hole[0] >> 4 && blocks.chunkZ() == hole[1] >> 4) {
|
||||
blocks.set(hole[0] & 15, hole[2], hole[1] & 15, HydrologyVoxels.OPEN);
|
||||
}
|
||||
return blocks;
|
||||
}), LAND);
|
||||
Map<String, String> total = fields(line(leaky.lines(), "IRIS_HYDROLOGY_TOTAL"));
|
||||
assertTrue(leaky.lines().toString(), Long.parseLong(total.get("containmentLeaks")) > 0L);
|
||||
assertEquals("FAIL", total.get("status"));
|
||||
assertTrue(total.get("missedTargets"), total.get("missedTargets").contains("containmentLeaks"));
|
||||
assertTrue(leaky.lines().toString(), leaky.lines().stream().anyMatch(line -> line.startsWith("IRIS_HYDROLOGY_EXAMPLE leak")
|
||||
&& line.endsWith(" at " + (hole[0] - hole[3]) + "," + hole[2] + "," + (hole[1] - hole[4]))));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failsOnMissingPlannedSurface() throws Exception {
|
||||
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
|
||||
HydrologyReport.Result clean = HydrologyProbe.measure(fixture.inputs(), LAND);
|
||||
assertEquals(clean.lines().toString(), 0L, clean.sum(HydrologyReport.WindowMetrics::missingSurface));
|
||||
int[] wet = bankBesideWater(fixture);
|
||||
assertNotNull("no wet column in window " + LAND, wet);
|
||||
int x = wet[0] - wet[3];
|
||||
int z = wet[1] - wet[4];
|
||||
HydrologyReport.Result covered = HydrologyProbe.measure(fixture.inputs(blocks -> {
|
||||
if (blocks.chunkX() == x >> 4 && blocks.chunkZ() == z >> 4) {
|
||||
blocks.set(x & 15, wet[2], z & 15, HydrologyVoxels.SOLID);
|
||||
}
|
||||
return blocks;
|
||||
}), LAND);
|
||||
Map<String, String> total = fields(line(covered.lines(), "IRIS_HYDROLOGY_TOTAL"));
|
||||
assertEquals(covered.lines().toString(), "1", total.get("missingSurface"));
|
||||
assertEquals("FAIL", total.get("status"));
|
||||
assertTrue(total.get("missedTargets"), total.get("missedTargets").contains("missingSurface"));
|
||||
assertTrue(covered.lines().toString(), covered.lines().contains("IRIS_HYDROLOGY_EXAMPLE missing planned surface at " + x + ","
|
||||
+ wet[2] + "," + z));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaFillOverNaturalSeaFloorIsNotAHydrologyLeak() throws Exception {
|
||||
HydrologyWindows.WindowRange coast = new HydrologyWindows.WindowRange(3, 3, 0, 0);
|
||||
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
|
||||
int[] sea = dryColumnInTheSea(fixture, coast);
|
||||
assertNotNull("no dry hydrology column over natural sea floor in window " + coast, sea);
|
||||
HydrologyReport.Result result = HydrologyProbe.measure(fixture.inputs(blocks -> {
|
||||
if (blocks.chunkX() == sea[2] >> 4 && blocks.chunkZ() == sea[3] >> 4) {
|
||||
blocks.set(sea[2] & 15, SyntheticHydrology.SEA_LEVEL, sea[3] & 15, HydrologyVoxels.OPEN);
|
||||
}
|
||||
return blocks;
|
||||
}), coast);
|
||||
String leak = "IRIS_HYDROLOGY_EXAMPLE leak source beside open ground at " + sea[0] + "," + SyntheticHydrology.SEA_LEVEL + "," + sea[1];
|
||||
assertTrue(result.lines().toString(), result.lines().stream().noneMatch(leak::equals));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aMouthOneBelowTheSeaBesideASeaFilledShoreLeaks() throws Exception {
|
||||
HydrologyWindows.WindowRange coast = new HydrologyWindows.WindowRange(3, 3, 0, 0);
|
||||
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
|
||||
int[] shore = seaShoreBesideWater(fixture, coast);
|
||||
assertNotNull("no sea-filled column beside hydrology water in window " + coast, shore);
|
||||
HydrologyReport.Result result = HydrologyProbe.measure(fixture.inputs(blocks -> {
|
||||
if (blocks.chunkX() == shore[2] >> 4 && blocks.chunkZ() == shore[3] >> 4) {
|
||||
blocks.set(shore[2] & 15, SyntheticHydrology.SEA_LEVEL, shore[3] & 15, HydrologyVoxels.OPEN);
|
||||
}
|
||||
return blocks;
|
||||
}), coast);
|
||||
String leak = "IRIS_HYDROLOGY_EXAMPLE leak source beside open ground at " + shore[0] + "," + SyntheticHydrology.SEA_LEVEL + "," + shore[1];
|
||||
assertTrue(result.lines().toString(), result.lines().contains(leak));
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] seaShoreBesideWater(SyntheticHydrology fixture, HydrologyWindows.WindowRange range) {
|
||||
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
for (int z = range.minZ() * WindowKey.SIZE + 1; z < (range.maxZ() + 1) * WindowKey.SIZE - 1; z++) {
|
||||
for (int x = range.minX() * WindowKey.SIZE + 1; x < (range.maxX() + 1) * WindowKey.SIZE - 1; x++) {
|
||||
ChunkWaterRaster raster = fixture.raster(x >> 4, z >> 4);
|
||||
if (raster.empty() || raster.role(x & 15, z & 15).wet() || fixture.naturalHeight(x, z) >= SyntheticHydrology.SEA_LEVEL
|
||||
|| raster.terrainHeight(x & 15, z & 15) >= SyntheticHydrology.SEA_LEVEL) {
|
||||
continue;
|
||||
}
|
||||
for (int[] offset : offsets) {
|
||||
int nx = x + offset[0];
|
||||
int nz = z + offset[1];
|
||||
ChunkWaterRaster neighbour = fixture.raster(nx >> 4, nz >> 4);
|
||||
if (neighbour.role(nx & 15, nz & 15).wet() && neighbour.waterHead(nx & 15, nz & 15) == SyntheticHydrology.SEA_LEVEL
|
||||
&& fixture.terrainHeight(nx, nz) < SyntheticHydrology.SEA_LEVEL) {
|
||||
return new int[]{x, z, nx, nz};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int[] dryColumnInTheSea(SyntheticHydrology fixture, HydrologyWindows.WindowRange range) {
|
||||
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
for (int z = range.minZ() * WindowKey.SIZE + 1; z < (range.maxZ() + 1) * WindowKey.SIZE - 1; z++) {
|
||||
for (int x = range.minX() * WindowKey.SIZE + 1; x < (range.maxX() + 1) * WindowKey.SIZE - 1; x++) {
|
||||
ChunkWaterRaster raster = fixture.raster(x >> 4, z >> 4);
|
||||
WaterRole role = raster.role(x & 15, z & 15);
|
||||
if (raster.empty() || role == WaterRole.NONE || role.wet() || fixture.naturalHeight(x, z) >= SyntheticHydrology.SEA_LEVEL
|
||||
|| raster.terrainHeight(x & 15, z & 15) >= SyntheticHydrology.SEA_LEVEL) {
|
||||
continue;
|
||||
}
|
||||
for (int[] offset : offsets) {
|
||||
int nx = x + offset[0];
|
||||
int nz = z + offset[1];
|
||||
if (fixture.raster(nx >> 4, nz >> 4).role(nx & 15, nz & 15) == WaterRole.NONE
|
||||
&& fixture.terrainHeight(nx, nz) < SyntheticHydrology.SEA_LEVEL) {
|
||||
return new int[]{x, z, nx, nz};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inletAndCavePoolChecksFeedTheWindowAndFailTheProbe() throws Exception {
|
||||
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
|
||||
HydrologyReport.Result result = HydrologyProbe.measure(fixture.inputs(UnaryOperator.identity(),
|
||||
(key, plan) -> new HydrologyUnderground.Counts(3, 2L, 5, 1L, List.of("inlet leak at 1,2,3", "pool air at 4,5,6"))), LAND);
|
||||
Map<String, String> total = fields(line(result.lines(), "IRIS_HYDROLOGY_TOTAL"));
|
||||
assertEquals("3", total.get("inlets"));
|
||||
assertEquals("2", total.get("inletFailures"));
|
||||
assertEquals("5", total.get("cavePools"));
|
||||
assertEquals("1", total.get("cavePoolFailures"));
|
||||
assertEquals("FAIL", total.get("status"));
|
||||
assertTrue(total.get("missedTargets"), total.get("missedTargets").contains("inletFailures")
|
||||
&& total.get("missedTargets").contains("cavePoolFailures"));
|
||||
assertTrue(result.lines().toString(), result.lines().contains("IRIS_HYDROLOGY_EXAMPLE inlet leak at 1,2,3"));
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] bankBesideWater(SyntheticHydrology fixture) {
|
||||
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
int min = LAND.minX() * HydrologyWindows.CHUNKS;
|
||||
for (int chunkZ = min; chunkZ < min + HydrologyWindows.CHUNKS; chunkZ++) {
|
||||
for (int chunkX = min; chunkX < min + HydrologyWindows.CHUNKS; chunkX++) {
|
||||
ChunkWaterRaster raster = fixture.raster(chunkX, chunkZ);
|
||||
for (int lz = 1; lz < 15; lz++) {
|
||||
for (int lx = 1; lx < 15; lx++) {
|
||||
int head = raster.waterHead(lx, lz);
|
||||
if (!raster.role(lx, lz).wet() || head <= SyntheticHydrology.SEA_LEVEL) {
|
||||
continue;
|
||||
}
|
||||
for (int[] offset : offsets) {
|
||||
int nx = lx + offset[0];
|
||||
int nz = lz + offset[1];
|
||||
if (raster.waterHead(nx, nz) == Integer.MIN_VALUE && raster.terrainHeight(nx, nz) >= head) {
|
||||
return new int[]{(chunkX << 4) + nx, (chunkZ << 4) + nz, head, offset[0], offset[1]};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String line(List<String> lines, String prefix) {
|
||||
for (String line : lines) {
|
||||
if (line.startsWith(prefix + " ")) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("No " + prefix + " line in " + lines);
|
||||
}
|
||||
|
||||
private static Map<String, String> fields(String line) {
|
||||
Map<String, String> fields = new LinkedHashMap<>();
|
||||
for (String token : line.trim().split(" ")) {
|
||||
int equals = token.indexOf('=');
|
||||
if (equals > 0) {
|
||||
fields.put(token.substring(0, equals), token.substring(equals + 1));
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static void assertNumber(String metric, String value) {
|
||||
assertNotNull(metric + " missing", value);
|
||||
Double.parseDouble(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HydrologyVoxelsTest {
|
||||
private static final int HEIGHT = 32;
|
||||
private static final int FLOOR = 9;
|
||||
private static final byte FALLING_WATER = (byte) (HydrologyVoxels.WATER + 8);
|
||||
|
||||
@Test
|
||||
public void classifiesFluidLevelsPlantsCaneAndSolids() {
|
||||
assertEquals(HydrologyVoxels.WATER, HydrologyVoxels.classify(state("minecraft:water[level=0]", false)));
|
||||
assertEquals(FALLING_WATER, HydrologyVoxels.classify(state("minecraft:water[level=8]", false)));
|
||||
assertEquals(HydrologyVoxels.WATER, HydrologyVoxels.classify(state("minecraft:water", false)));
|
||||
assertEquals(HydrologyVoxels.LAVA + 3, HydrologyVoxels.classify(state("minecraft:lava[level=3]", false)));
|
||||
assertEquals(HydrologyVoxels.WATER, HydrologyVoxels.classify(state("minecraft:seagrass", false)));
|
||||
assertEquals(HydrologyVoxels.WATER, HydrologyVoxels.classify(state("minecraft:oak_stairs[waterlogged=true]", true)));
|
||||
assertEquals(HydrologyVoxels.OPEN, HydrologyVoxels.classify(state("minecraft:short_grass", false)));
|
||||
assertEquals(HydrologyVoxels.OPEN, HydrologyVoxels.classify(state("minecraft:red_tulip", false)));
|
||||
assertEquals(HydrologyVoxels.OPEN, HydrologyVoxels.classify(state("minecraft:snow[layers=1]", false)));
|
||||
assertEquals(HydrologyVoxels.CANE, HydrologyVoxels.classify(state("minecraft:sugar_cane[age=0]", false)));
|
||||
assertEquals(HydrologyVoxels.SOLID, HydrologyVoxels.classify(state("minecraft:grass_block[snowy=false]", false)));
|
||||
assertEquals(HydrologyVoxels.OPEN, HydrologyVoxels.classify(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void waterloggableBlocksFullSnowAndMangroveRootsHoldWaterWhileCropsFireAndWireDoNot() {
|
||||
for (String key : new String[]{"minecraft:glow_lichen[down=true,waterlogged=false]", "minecraft:hanging_roots[waterlogged=false]",
|
||||
"minecraft:small_dripleaf[facing=north,half=lower,waterlogged=false]", "minecraft:big_dripleaf_stem[facing=north,waterlogged=false]",
|
||||
"minecraft:rail[shape=north_south,waterlogged=false]", "minecraft:glow_lichen", "minecraft:hanging_roots",
|
||||
"minecraft:mangrove_roots[waterlogged=false]", "minecraft:muddy_mangrove_roots[axis=y]", "minecraft:snow[layers=8]"}) {
|
||||
assertEquals(key, HydrologyVoxels.SOLID, HydrologyVoxels.classify(state(key, false)));
|
||||
}
|
||||
for (String key : new String[]{"minecraft:wheat[age=3]", "minecraft:pumpkin_stem[age=2]", "minecraft:fire[age=0]",
|
||||
"minecraft:redstone_wire[power=0]", "minecraft:crimson_roots", "minecraft:vine[up=true]"}) {
|
||||
assertEquals(key, HydrologyVoxels.OPEN, HydrologyVoxels.classify(state(key, false)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void containedPondHasNoLeaks() {
|
||||
HydrologyVoxels.ChunkBlocks chunk = pond(10);
|
||||
HydrologyVoxels.WaterScan scan = scan(volume(chunk));
|
||||
assertEquals(9L, scan.waterCells);
|
||||
assertEquals(0L, scan.containmentLeaks);
|
||||
assertEquals(0L, scan.tickConversionCells);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceBesideOpenGroundOrOverOpenGroundLeaks() {
|
||||
HydrologyVoxels.ChunkBlocks side = pond(10);
|
||||
side.set(6, 10, 4, HydrologyVoxels.OPEN);
|
||||
HydrologyVoxels.WaterScan sideScan = scan(volume(side));
|
||||
assertEquals(1L, sideScan.containmentLeaks);
|
||||
assertEquals("leak source beside open ground at 5,10,4", sideScan.examples.get(0));
|
||||
HydrologyVoxels.ChunkBlocks under = pond(10);
|
||||
under.set(4, FLOOR, 4, HydrologyVoxels.OPEN);
|
||||
HydrologyVoxels.WaterScan underScan = scan(volume(under));
|
||||
assertEquals(1L, underScan.containmentLeaks);
|
||||
assertEquals("leak water over open ground at 4,10,4", underScan.examples.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sugarCaneBesideOrUnderWaterHoldsIt() {
|
||||
HydrologyVoxels.ChunkBlocks chunk = pond(10);
|
||||
chunk.set(6, 10, 4, HydrologyVoxels.CANE);
|
||||
chunk.set(4, FLOOR, 4, HydrologyVoxels.CANE);
|
||||
HydrologyVoxels.WaterScan scan = scan(volume(chunk));
|
||||
assertEquals(0L, scan.containmentLeaks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceAboveItsHeadOverWaterBesideAirLeaks() {
|
||||
HydrologyVoxels.ChunkBlocks chunk = pond(10);
|
||||
clearAbove(chunk);
|
||||
chunk.set(4, 11, 4, HydrologyVoxels.WATER);
|
||||
HydrologyVoxels.WaterScan scan = scan(volume(chunk));
|
||||
assertEquals(1L, scan.containmentLeaks);
|
||||
assertEquals("leak source beside open ground at 4,11,4", scan.examples.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallingWaterOverWaterNeverSpreadsSideways() {
|
||||
HydrologyVoxels.ChunkBlocks chunk = pond(10);
|
||||
clearAbove(chunk);
|
||||
for (int y = 11; y <= 13; y++) {
|
||||
chunk.set(4, y, 4, FALLING_WATER);
|
||||
}
|
||||
HydrologyVoxels.WaterScan scan = scan(volume(chunk));
|
||||
assertEquals(0L, scan.containmentLeaks);
|
||||
assertEquals(0L, scan.tickConversionCells);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flowingCellsAboveThePlannedHeadThatWouldConvertOrSpreadAreTickCells() {
|
||||
HydrologyVoxels.ChunkBlocks converting = pond(10);
|
||||
converting.set(4, 10, 4, FALLING_WATER);
|
||||
HydrologyVoxels.WaterScan conversion = scan(volume(converting));
|
||||
assertEquals(1L, conversion.tickConversionCells);
|
||||
assertEquals("tick flowing cell converts to a source at 4,10,4", conversion.examples.get(0));
|
||||
HydrologyVoxels.ChunkBlocks atHead = pond(10);
|
||||
atHead.set(4, 10, 4, FALLING_WATER);
|
||||
atHead.heads()[(4 << 4) | 4] = 10;
|
||||
assertEquals(0L, scan(volume(atHead)).tickConversionCells);
|
||||
HydrologyVoxels.ChunkBlocks spreading = solid();
|
||||
spreading.set(4, 10, 4, (byte) (HydrologyVoxels.WATER + 3));
|
||||
spreading.set(5, 10, 4, HydrologyVoxels.OPEN);
|
||||
spreading.set(8, 10, 8, (byte) (HydrologyVoxels.WATER + 7));
|
||||
spreading.set(9, 10, 8, HydrologyVoxels.OPEN);
|
||||
HydrologyVoxels.Volume volume = volume(spreading);
|
||||
HydrologyVoxels.WaterScan spread = new HydrologyVoxels.WaterScan();
|
||||
spread.column(volume, 4, 4, FLOOR, HEIGHT - 1);
|
||||
spread.column(volume, 8, 8, FLOOR, HEIGHT - 1);
|
||||
assertEquals(1L, spread.tickConversionCells);
|
||||
assertEquals("tick flowing cell spreads beside lower dry ground at 4,10,4", spread.examples.get(0));
|
||||
assertEquals(0L, spread.containmentLeaks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lavaNeverConvertsAndSpreadsOnlyWithMoreThanTwoLevelsLeft() {
|
||||
HydrologyVoxels.ChunkBlocks pool = solid();
|
||||
for (int z = 3; z <= 5; z++) {
|
||||
for (int x = 3; x <= 5; x++) {
|
||||
pool.set(x, 10, z, HydrologyVoxels.LAVA);
|
||||
}
|
||||
}
|
||||
pool.set(4, 10, 4, (byte) (HydrologyVoxels.LAVA + 8));
|
||||
assertEquals(0L, scan(volume(pool)).tickConversionCells);
|
||||
HydrologyVoxels.ChunkBlocks spreading = solid();
|
||||
spreading.set(4, 10, 4, (byte) (HydrologyVoxels.LAVA + 6));
|
||||
spreading.set(5, 10, 4, HydrologyVoxels.OPEN);
|
||||
spreading.set(8, 10, 8, (byte) (HydrologyVoxels.LAVA + 4));
|
||||
spreading.set(9, 10, 8, HydrologyVoxels.OPEN);
|
||||
HydrologyVoxels.Volume volume = volume(spreading);
|
||||
HydrologyVoxels.WaterScan spread = new HydrologyVoxels.WaterScan();
|
||||
spread.column(volume, 4, 4, FLOOR, HEIGHT - 1);
|
||||
spread.column(volume, 8, 8, FLOOR, HEIGHT - 1);
|
||||
assertEquals(1L, spread.tickConversionCells);
|
||||
assertEquals("tick flowing cell spreads beside lower dry ground at 8,10,8", spread.examples.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannedHeadsWithoutTheirFluidMissTheSurface() {
|
||||
HydrologyVoxels.ChunkBlocks chunk = pond(10);
|
||||
for (int z = 3; z <= 5; z++) {
|
||||
for (int x = 3; x <= 5; x++) {
|
||||
chunk.heads()[(z << 4) | x] = 10;
|
||||
}
|
||||
}
|
||||
assertEquals(0L, scan(volume(chunk)).missingSurfaces);
|
||||
chunk.set(4, 10, 4, HydrologyVoxels.SOLID);
|
||||
chunk.surfaces()[(5 << 4) | 5] = HydrologyVoxels.LAVA;
|
||||
HydrologyVoxels.WaterScan scan = scan(volume(chunk));
|
||||
assertEquals(2L, scan.missingSurfaces);
|
||||
assertEquals("missing planned surface at 4,10,4", scan.examples.get(0));
|
||||
assertEquals("missing planned surface at 5,10,5", scan.examples.get(1));
|
||||
chunk.surfaces()[(4 << 4) | 4] = HydrologyVoxels.SOLID;
|
||||
chunk.surfaces()[(3 << 4) | 3] = HydrologyVoxels.SOLID;
|
||||
assertEquals(1L, scan(volume(chunk)).missingSurfaces);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownNeighboursAreCensoredNotLeaks() {
|
||||
HydrologyVoxels.ChunkBlocks chunk = solid();
|
||||
chunk.set(15, 10, 4, HydrologyVoxels.WATER);
|
||||
HydrologyVoxels.WaterScan scan = new HydrologyVoxels.WaterScan();
|
||||
scan.column(volume(chunk), 15, 4, FLOOR, HEIGHT - 1);
|
||||
assertEquals(0L, scan.containmentLeaks);
|
||||
assertEquals(1L, scan.censoredCells);
|
||||
}
|
||||
|
||||
private static HydrologyVoxels.WaterScan scan(HydrologyVoxels.Volume volume) {
|
||||
HydrologyVoxels.WaterScan scan = new HydrologyVoxels.WaterScan();
|
||||
for (int z = 2; z <= 6; z++) {
|
||||
for (int x = 2; x <= 6; x++) {
|
||||
scan.column(volume, x, z, FLOOR, HEIGHT - 1);
|
||||
}
|
||||
}
|
||||
return scan;
|
||||
}
|
||||
|
||||
private static HydrologyVoxels.Volume volume(HydrologyVoxels.ChunkBlocks chunk) {
|
||||
HydrologyVoxels.Volume volume = new HydrologyVoxels.Volume();
|
||||
volume.put(chunk);
|
||||
return volume;
|
||||
}
|
||||
|
||||
private static void clearAbove(HydrologyVoxels.ChunkBlocks chunk) {
|
||||
for (int y = 11; y <= 14; y++) {
|
||||
for (int z = 3; z <= 5; z++) {
|
||||
for (int x = 3; x <= 5; x++) {
|
||||
chunk.set(x, y, z, HydrologyVoxels.OPEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyVoxels.ChunkBlocks pond(int level) {
|
||||
HydrologyVoxels.ChunkBlocks chunk = solid();
|
||||
for (int z = 3; z <= 5; z++) {
|
||||
for (int x = 3; x <= 5; x++) {
|
||||
chunk.set(x, level, z, HydrologyVoxels.WATER);
|
||||
}
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private static HydrologyVoxels.ChunkBlocks solid() {
|
||||
HydrologyVoxels.ChunkBlocks chunk = HydrologyVoxels.ChunkBlocks.empty(0, 0, HEIGHT);
|
||||
for (int y = 0; y <= 14; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
chunk.set(x, y, z, HydrologyVoxels.SOLID);
|
||||
}
|
||||
}
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private static NativeBlockState state(String key, boolean waterlogged) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.key()).thenReturn(key);
|
||||
when(state.isAir()).thenReturn(false);
|
||||
when(state.isWaterLogged()).thenReturn(waterlogged);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,130 +1,107 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPoint;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
||||
import art.arcane.iris.generation.hydrology.config.IrisHydrology;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||
import art.arcane.iris.generation.hydrology.water.ReachStyle;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class RiverTransectProbeTest {
|
||||
@Rule
|
||||
public TemporaryFolder folder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void sectionStationsSitAtTenThirtyFiftySeventyAndNinetyPercent() {
|
||||
assertArrayEquals(new int[] {2, 6, 10, 14, 18}, RiverTransectProbe.sectionStations(20));
|
||||
assertArrayEquals(new int[] {0, 0, 0, 0, 0}, RiverTransectProbe.sectionStations(1));
|
||||
assertArrayEquals(new int[] {0, 0, 1, 2, 2}, RiverTransectProbe.sectionStations(3));
|
||||
public void stationsSitAtTenThirtyFiftySeventyAndNinetyPercentOfTheLength() {
|
||||
assertArrayEquals(new int[]{2, 6, 10, 14, 18}, RiverTransectProbe.stations(straight(21, 0f, 100f, 4f)));
|
||||
assertArrayEquals(new int[]{1, 1, 1, 1, 1}, RiverTransectProbe.stations(straight(2, 0f, 100f, 4f)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void summaryCountsCutsBankStepsOceanWritesAndSpillingChannels() {
|
||||
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
|
||||
put(columns, 0, 0, 72, 68, 70, RiverTransectProbe.Role.CHANNEL);
|
||||
put(columns, 1, 0, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
|
||||
put(columns, -1, 0, 72, 69, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
|
||||
put(columns, 0, 1, 72, 70, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.SHORE);
|
||||
put(columns, 0, -1, 72, 72, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.NONE);
|
||||
put(columns, 2, 0, 72, 72, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.NONE);
|
||||
put(columns, 5, 5, 60, 58, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.NONE);
|
||||
put(columns, 6, 6, 60, 60, 63, RiverTransectProbe.Role.APRON);
|
||||
|
||||
RiverTransectProbe.CourseSummary summary = RiverTransectProbe.summarize(options(7L, 12), columns);
|
||||
|
||||
assertEquals(7L, summary.id());
|
||||
assertEquals(12, summary.stations());
|
||||
assertEquals(4, summary.ownedColumns());
|
||||
assertEquals(1, summary.minimumCut());
|
||||
assertEquals(4, summary.maximumCut());
|
||||
assertEquals(1, summary.maximumBankStep());
|
||||
assertEquals(1, summary.oceanWrites());
|
||||
assertEquals(1, summary.uncontainedWetCells());
|
||||
assertFalse(summary.passes());
|
||||
assertEquals(3, summary.details().size());
|
||||
assertTrue(summary.details().get(0).startsWith("uncontained 0,0"));
|
||||
assertTrue(summary.details().get(1).startsWith("oceanWrite 5,5"));
|
||||
assertTrue(summary.details().get(2).startsWith("bankStep 1"));
|
||||
public void sectionReadsColumnsAcrossTheFlow() {
|
||||
Reach reach = straight(11, 0f, 100f, 4f);
|
||||
RiverTransectProbe.Section section = RiverTransectProbe.section(reach, 5, (x, z) -> Math.abs(z - 100) <= 2
|
||||
? new RiverTransectProbe.Column(70, 66, 68, WaterRole.CHANNEL)
|
||||
: z > 140 ? null : new RiverTransectProbe.Column(70, 70, RiverTransectProbe.NO_WATER, WaterRole.BANK));
|
||||
RiverTransectProbe.Column[] columns = section.columns();
|
||||
assertEquals(2 * RiverTransectProbe.SECTION_HALF + 1, columns.length);
|
||||
assertEquals(0.5D, section.station(), 1e-6D);
|
||||
assertEquals(68, columns[RiverTransectProbe.SECTION_HALF].head());
|
||||
assertEquals(68, columns[RiverTransectProbe.SECTION_HALF + 2].head());
|
||||
assertEquals(RiverTransectProbe.NO_WATER, columns[RiverTransectProbe.SECTION_HALF + 3].head());
|
||||
assertEquals(RiverTransectProbe.NO_WATER, columns[RiverTransectProbe.SECTION_HALF - 3].head());
|
||||
assertNull(columns[2 * RiverTransectProbe.SECTION_HALF]);
|
||||
assertEquals(WaterRole.BANK, columns[0].role());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void containedCourseAwayFromTheOceanPasses() {
|
||||
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
|
||||
put(columns, 0, 0, 72, 68, 70, RiverTransectProbe.Role.CHANNEL);
|
||||
put(columns, 1, 0, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
|
||||
put(columns, -1, 0, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
|
||||
put(columns, 0, 1, 72, 70, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.SHORE);
|
||||
put(columns, 0, -1, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.SHORE);
|
||||
|
||||
RiverTransectProbe.CourseSummary summary = RiverTransectProbe.summarize(options(1L, 3), columns);
|
||||
|
||||
assertTrue(summary.passes());
|
||||
assertEquals(0, summary.oceanWrites());
|
||||
assertEquals(0, summary.uncontainedWetCells());
|
||||
public void summaryListsReachesPondsAndSections() {
|
||||
HydrologySettings settings = HydrologySettings.compile(new IrisHydrology().setEnabled(true), 1L, 40, 0, 320);
|
||||
Reach reach = straight(11, 0f, 100f, 4f);
|
||||
RiverTransectProbe.Section section = RiverTransectProbe.section(reach, 5,
|
||||
(x, z) -> new RiverTransectProbe.Column(70, 66, Math.abs(z - 100) <= 2 ? 68 : RiverTransectProbe.NO_WATER, WaterRole.CHANNEL));
|
||||
List<String> summary = RiverTransectProbe.summary(new WindowKey(0, 0),
|
||||
new WindowPlan(new WindowKey(0, 0), List.of(reach), List.of(), List.of()), List.of(),
|
||||
List.of(section), settings);
|
||||
assertEquals("window 0,0 reaches=1 ponds=0 lakes=0 sections=1", summary.get(0));
|
||||
assertTrue(summary.get(1), summary.get(1).startsWith("reach 7 tier=stream length=40 from=0,100@70 to=40,100@60"));
|
||||
assertEquals("section reach=7 station=50% at=20,100 wetColumns=5 deepest=2", summary.get(2));
|
||||
}
|
||||
|
||||
@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);
|
||||
public void rendersPlanSectionsAndSummaryForASyntheticWindow() throws Exception {
|
||||
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
|
||||
HydrologyProbe.Inputs inputs = fixture.inputs();
|
||||
HydrologyWindows.WindowRange range = new HydrologyWindows.WindowRange(4, 4, 4, 4);
|
||||
HydrologyPlanHarness.Plans plans = new HydrologyPlanHarness.Plans(inputs.settings(), inputs.terrain(), inputs.naturalHeight(), range);
|
||||
File output = folder.newFolder("transect");
|
||||
RiverTransectProbe.Rendered rendered = RiverTransectProbe.render(plans, new WindowKey(4, 4), inputs.naturalHeight(), range, output);
|
||||
assertEquals(plans.window(new WindowKey(4, 4)).plan().reaches().size(), rendered.reaches());
|
||||
assertEquals(plans.window(new WindowKey(4, 4)).plan().ponds().size(), rendered.ponds());
|
||||
assertTrue("reaches " + rendered.reaches(), rendered.reaches() > 0);
|
||||
assertTrue("sections " + rendered.sections(), rendered.sections() > 0);
|
||||
BufferedImage plan = ImageIO.read(new File(rendered.directory(), "plan.png"));
|
||||
assertEquals(WindowKey.SIZE, plan.getWidth());
|
||||
assertEquals(WindowKey.SIZE, plan.getHeight());
|
||||
assertTrue(new File(rendered.directory(), "sections.png").isFile());
|
||||
List<String> summary = Files.readAllLines(new File(rendered.directory(), "summary.txt").toPath());
|
||||
assertTrue(summary.get(0), summary.get(0).startsWith("window 4,4 reaches=" + rendered.reaches() + " ponds=" + rendered.ponds()));
|
||||
assertTrue(rendered.line(), rendered.line().startsWith("IRIS_RIVER_TRANSECT version=2 wx=4 wz=4 "));
|
||||
}
|
||||
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,
|
||||
int z,
|
||||
int natural,
|
||||
int terrain,
|
||||
int water,
|
||||
RiverTransectProbe.Role role
|
||||
) {
|
||||
columns.put(RiverFootprint.pack(x, z), new RiverTransectProbe.ColumnView(x, z, natural, terrain, water, role,
|
||||
new RiverTransectProbe.ColumnPlan(role == RiverTransectProbe.Role.APRON,
|
||||
role.owned(), role == RiverTransectProbe.Role.CHANNEL, false, terrain, 1L)));
|
||||
private static Reach straight(int points, float x0, float z, float spacing) {
|
||||
HydrologySettings settings = HydrologySettings.compile(new IrisHydrology().setEnabled(true), 1L, 40, 0, 320);
|
||||
float[] xs = new float[points];
|
||||
float[] zs = new float[points];
|
||||
int[] grade = new int[points];
|
||||
float[] halfWidth = new float[points];
|
||||
float[] depth = new float[points];
|
||||
ReachStyle[] style = new ReachStyle[points];
|
||||
for (int point = 0; point < points; point++) {
|
||||
xs[point] = x0 + point * spacing;
|
||||
zs[point] = z;
|
||||
grade[point] = 70 - point;
|
||||
halfWidth[point] = 2f;
|
||||
depth[point] = 1f;
|
||||
}
|
||||
Arrays.fill(style, ReachStyle.RUN);
|
||||
return new Reach(7L, settings.tiers().get(0), 0L, xs, zs, grade, halfWidth, depth, style, 1000f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class SeaInletChecksTest {
|
||||
@Test
|
||||
public void roofBreachesAndLooseBedsAreReportedButOnlyContainmentAndSealChecksFail() {
|
||||
SeaInletProbe.Checks reported = new SeaInletProbe.Checks(3, 0L, 0L, 0L, 0L, 1939L, 0L, 0L, 0L, 0L, 952L, List.of());
|
||||
assertEquals(0L, reported.failures());
|
||||
SeaInletProbe.Checks failing = new SeaInletProbe.Checks(3, 1L, 2L, 3L, 4L, 0L, 5L, 6L, 7L, 8L, 0L, List.of());
|
||||
assertEquals(36L, failing.failures());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inletCellsAreClassifiedAgainstTheDimensionSeaFluid() {
|
||||
assertEquals(SeaInletProbe.classify(state("minecraft:water"), "minecraft:water"), SeaInletProbe.classify(state("minecraft:lava"),
|
||||
"minecraft:lava"));
|
||||
assertEquals(SeaInletProbe.classify(state("minecraft:water[level=8]"), "minecraft:water"),
|
||||
SeaInletProbe.classify(state("minecraft:lava[level=8]"), "minecraft:lava"));
|
||||
assertEquals(SeaInletProbe.classify(state("minecraft:lava"), "minecraft:water"), SeaInletProbe.classify(state("minecraft:water"),
|
||||
"minecraft:lava"));
|
||||
assertNotEquals(SeaInletProbe.classify(state("minecraft:lava"), "minecraft:lava"), SeaInletProbe.classify(state("minecraft:lava"),
|
||||
"minecraft:water"));
|
||||
}
|
||||
|
||||
private static NativeBlockState state(String key) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.key()).thenReturn(key);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,28 @@ public final class StubPlatformStateTest {
|
||||
assertTrue(stone.isSolid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vanillaSolidityLeavesPlantsVinesAndCaneOpenAndTheDefaultUnchanged() {
|
||||
String[] open = {"minecraft:vine[east=false,north=true,south=false,up=false,west=false]", "minecraft:short_grass",
|
||||
"minecraft:sugar_cane[age=0]", "minecraft:seagrass", "minecraft:oak_sapling[stage=0]"};
|
||||
String[] solid = {"minecraft:oak_log[axis=y]", "minecraft:oak_leaves[distance=1,persistent=false,waterlogged=false]",
|
||||
"minecraft:oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=true]"};
|
||||
for (String key : open) {
|
||||
assertTrue(key, state(key).isSolid());
|
||||
}
|
||||
StubPlatform.vanillaSolidity(true);
|
||||
try {
|
||||
for (String key : open) {
|
||||
assertFalse(key, state(key).isSolid());
|
||||
}
|
||||
for (String key : solid) {
|
||||
assertTrue(key, state(key).isSolid());
|
||||
}
|
||||
} finally {
|
||||
StubPlatform.vanillaSolidity(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void classifiesCanonicalWaterloggedProperty() {
|
||||
assertTrue(state("minecraft:brain_coral_fan[waterlogged=true]").isWaterLogged());
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.config.IrisHydrology;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanners;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.grid.CellHash;
|
||||
import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer;
|
||||
import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologyPlanCache;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
final class SyntheticHydrology implements AutoCloseable {
|
||||
static final long SEED = 1337L;
|
||||
static final int SEA_LEVEL = 40;
|
||||
static final int HEIGHT = 320;
|
||||
private static final byte ALLOW_ALL = HydrologySampleBuffer.ALLOW_STREAMS | HydrologySampleBuffer.ALLOW_PONDS;
|
||||
private static final long SALT_NOISE = 0x4e4f495345L;
|
||||
private static final byte FALLING_WATER = (byte) (HydrologyVoxels.WATER + 8);
|
||||
|
||||
private final HydrologySettings settings;
|
||||
private final Terrain terrain;
|
||||
private final ForkJoinPool planningPool;
|
||||
private final ExecutorService workers;
|
||||
private final HydrologyRuntime runtime;
|
||||
|
||||
SyntheticHydrology() {
|
||||
this.settings = HydrologySettings.compile(new IrisHydrology().setEnabled(true), SEED, SEA_LEVEL, 0, HEIGHT);
|
||||
this.terrain = new Terrain();
|
||||
this.planningPool = new ForkJoinPool(4);
|
||||
this.workers = Executors.newFixedThreadPool(4);
|
||||
HydrologyPlanners planners = HydrologyPlanners.create(settings, terrain);
|
||||
this.runtime = new HydrologyRuntime(settings, HydrologyPlanCache.create(settings, planners.regions(), planners.windows(), planningPool,
|
||||
() -> false), terrain::naturalHeight, () -> false, System::nanoTime);
|
||||
}
|
||||
|
||||
HydrologyProbe.Inputs inputs() {
|
||||
return inputs(UnaryOperator.identity());
|
||||
}
|
||||
|
||||
HydrologyProbe.Inputs inputs(UnaryOperator<HydrologyVoxels.ChunkBlocks> edit) {
|
||||
return inputs(edit, (key, plan) -> new HydrologyUnderground.Counts(plan.inlets().size(), 0L, 0, 0L, List.of()));
|
||||
}
|
||||
|
||||
HydrologyProbe.Inputs inputs(UnaryOperator<HydrologyVoxels.ChunkBlocks> edit, HydrologyUnderground.Source underground) {
|
||||
return new HydrologyProbe.Inputs(settings, terrain, terrain::naturalHeight, planningPool, workers,
|
||||
(chunkX, chunkZ) -> edit.apply(blocks(chunkX, chunkZ)), underground, HEIGHT);
|
||||
}
|
||||
|
||||
ChunkWaterRaster raster(int chunkX, int chunkZ) {
|
||||
return runtime.raster(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
int naturalHeight(int x, int z) {
|
||||
return terrain.naturalHeight(x, z);
|
||||
}
|
||||
|
||||
int terrainHeight(int x, int z) {
|
||||
ChunkWaterRaster raster = raster(x >> 4, z >> 4);
|
||||
return raster.empty() ? terrain.naturalHeight(x, z) : raster.terrainHeight(x & 15, z & 15);
|
||||
}
|
||||
|
||||
private HydrologyVoxels.ChunkBlocks blocks(int chunkX, int chunkZ) {
|
||||
HydrologyVoxels.ChunkBlocks blocks = HydrologyVoxels.ChunkBlocks.empty(chunkX, chunkZ, HEIGHT);
|
||||
ChunkWaterRaster own = raster(chunkX, chunkZ);
|
||||
for (int lz = 0; lz < 16; lz++) {
|
||||
for (int lx = 0; lx < 16; lx++) {
|
||||
int x = (chunkX << 4) + lx;
|
||||
int z = (chunkZ << 4) + lz;
|
||||
int ground = terrainHeight(x, z);
|
||||
int head = own.waterHead(lx, lz);
|
||||
blocks.heads()[(lz << 4) | lx] = head;
|
||||
for (int y = 0; y <= ground; y++) {
|
||||
blocks.set(lx, y, lz, HydrologyVoxels.SOLID);
|
||||
}
|
||||
int surface = head != Integer.MIN_VALUE ? head : ground < SEA_LEVEL ? SEA_LEVEL : ground;
|
||||
for (int y = ground + 1; y <= surface; y++) {
|
||||
blocks.set(lx, y, lz, HydrologyVoxels.WATER);
|
||||
}
|
||||
if (head != Integer.MIN_VALUE) {
|
||||
for (int y = head + 1; y <= highestNeighbour(x, z); y++) {
|
||||
blocks.set(lx, y, lz, FALLING_WATER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private int highestNeighbour(int x, int z) {
|
||||
int highest = Integer.MIN_VALUE;
|
||||
for (int[] offset : new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}) {
|
||||
int nx = x + offset[0];
|
||||
int nz = z + offset[1];
|
||||
highest = Math.max(highest, raster(nx >> 4, nz >> 4).waterHead(nx & 15, nz & 15));
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
runtime.close();
|
||||
workers.shutdownNow();
|
||||
planningPool.shutdownNow();
|
||||
}
|
||||
|
||||
static final class Terrain implements HydrologyTerrainSource {
|
||||
@Override
|
||||
public void sample(int[] xs, int[] zs, HydrologySampleBuffer out, boolean baseOnly) {
|
||||
for (int index = 0; index < xs.length; index++) {
|
||||
float value = (float) height(xs[index], zs[index]);
|
||||
out.height()[index] = value;
|
||||
out.ocean()[index] = value <= SEA_LEVEL;
|
||||
out.rainfall()[index] = 1f;
|
||||
out.pondDensity()[index] = 1f;
|
||||
out.permission()[index] = ALLOW_ALL;
|
||||
out.fluidProfile()[index] = null;
|
||||
}
|
||||
}
|
||||
|
||||
int naturalHeight(int x, int z) {
|
||||
return Math.round((float) height(x, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int seaLevel() {
|
||||
return SEA_LEVEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int minY() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int maxY() {
|
||||
return HEIGHT;
|
||||
}
|
||||
|
||||
private static double height(double x, double z) {
|
||||
double height = 70D;
|
||||
double amplitude = 60D;
|
||||
double wavelength = 4096D;
|
||||
for (int octave = 0; octave < 6; octave++) {
|
||||
height += amplitude * noise(SEED + octave, x / wavelength, z / wavelength);
|
||||
amplitude *= 0.5D;
|
||||
wavelength *= 0.5D;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
private static double noise(long seed, double x, double z) {
|
||||
int x0 = (int) Math.floor(x);
|
||||
int z0 = (int) Math.floor(z);
|
||||
double fx = smooth(x - x0);
|
||||
double fz = smooth(z - z0);
|
||||
double top = lerp(corner(seed, x0, z0), corner(seed, x0 + 1, z0), fx);
|
||||
double bottom = lerp(corner(seed, x0, z0 + 1), corner(seed, x0 + 1, z0 + 1), fx);
|
||||
return lerp(top, bottom, fz);
|
||||
}
|
||||
|
||||
private static double corner(long seed, int x, int z) {
|
||||
return 2D * CellHash.unit(CellHash.mix(seed, SALT_NOISE, x, z)) - 1D;
|
||||
}
|
||||
|
||||
private static double smooth(double t) {
|
||||
return t * t * (3D - 2D * t);
|
||||
}
|
||||
|
||||
private static double lerp(double a, double b, double t) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user