mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +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
+30
-12
@@ -2,8 +2,9 @@ package art.arcane.iris.probe;
|
||||
|
||||
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.world.pregen.PregenHydrology;
|
||||
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
||||
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -31,11 +32,16 @@ import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public final class OfflineRegionGenerator {
|
||||
private static final int CHUNKS_PER_HYDROLOGY_REGION = WindowKey.SIZE >> 4;
|
||||
|
||||
private OfflineRegionGenerator() {
|
||||
}
|
||||
|
||||
@@ -97,14 +103,13 @@ public final class OfflineRegionGenerator {
|
||||
PregenPerformanceProfile.apply(session.engine());
|
||||
System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize()
|
||||
+ " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true");
|
||||
IrisHydrologyRuntime hydrology = session.engine().getComplex().getHydrologyRuntime();
|
||||
try (HydrologyTileCache.PregenerationScope hydrologyScope = hydrology == null ? null
|
||||
: hydrology.preparePregeneration(configuration.hydrologyArea());
|
||||
try (HydrologyPregeneration pregeneration = PregenHydrology.open(session.engine());
|
||||
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
||||
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
|
||||
configuration.noiseSettings(), spigot));
|
||||
RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(),
|
||||
RegionGenerationWindow.Policy.STANDALONE)) {
|
||||
PregenHydrology.prepareAll(pregeneration, configuration.hydrologyRegions());
|
||||
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers);
|
||||
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
|
||||
Files.createDirectory(regionDirectory);
|
||||
@@ -239,14 +244,27 @@ public final class OfflineRegionGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyTileCache.PregenerationArea hydrologyArea() {
|
||||
List<long[]> hydrologyRegions() {
|
||||
int halo = NativeRegionPlan.terrainHalo();
|
||||
long minimumX = ((long) chunkX - halo) * 16L;
|
||||
long minimumZ = ((long) chunkZ - halo) * 16L;
|
||||
long maximumX = ((long) chunkX + (long) width * regionsX + halo) * 16L - 1L;
|
||||
long maximumZ = ((long) chunkZ + (long) width * regionsZ + halo) * 16L - 1L;
|
||||
return new HydrologyTileCache.PregenerationArea(Math.toIntExact((minimumX + maximumX) / 2L),
|
||||
Math.toIntExact((minimumZ + maximumZ) / 2L), minimumX, minimumZ, maximumX, maximumZ);
|
||||
Set<Long> seen = new HashSet<>();
|
||||
List<long[]> regions = new ArrayList<>();
|
||||
for (int row = 0; row < regionsZ; row++) {
|
||||
for (int column = 0; column < regionsX; column++) {
|
||||
int regionX = row % 2 == 0 ? column : regionsX - column - 1;
|
||||
int minimumX = Math.floorDiv(chunkX + regionX * width - halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||
int maximumX = Math.floorDiv(chunkX + (regionX + 1) * width - 1 + halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||
int minimumZ = Math.floorDiv(chunkZ + row * width - halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||
int maximumZ = Math.floorDiv(chunkZ + (row + 1) * width - 1 + halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||
for (int hydrologyZ = minimumZ; hydrologyZ <= maximumZ; hydrologyZ++) {
|
||||
for (int hydrologyX = minimumX; hydrologyX <= maximumX; hydrologyX++) {
|
||||
if (seen.add(((long) hydrologyX << 32) | (hydrologyZ & 0xFFFFFFFFL))) {
|
||||
regions.add(new long[]{hydrologyX, hydrologyZ});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
static Configuration parse(String[] arguments) throws IOException {
|
||||
|
||||
+1
-1
@@ -137,7 +137,7 @@ public class HeadlessChunkGeneratorTest {
|
||||
}
|
||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||
{"name":"Native columns","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
||||
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
||||
"landChance":1,"carvingEnabled":false}
|
||||
""");
|
||||
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||
|
||||
+5
-7
@@ -1,7 +1,6 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
@@ -68,11 +67,10 @@ public final class HeadlessRegionTerrainTest {
|
||||
OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration(
|
||||
workspace.pack(), "main", 69420L, -3, 2, 1, 2, 3, 4, configuration,
|
||||
"probe", Level.OVERWORLD, NoiseGeneratorSettings.OVERWORLD, root.resolve("export"));
|
||||
HydrologyTileCache.PregenerationArea bounds = export.hydrologyArea();
|
||||
assertEquals(-80L, bounds.minimumBlockX());
|
||||
assertEquals(0L, bounds.minimumBlockZ());
|
||||
assertEquals(15L, bounds.maximumBlockX());
|
||||
assertEquals(111L, bounds.maximumBlockZ());
|
||||
List<long[]> regions = export.hydrologyRegions();
|
||||
assertEquals(2, regions.size());
|
||||
assertArrayEquals(new long[]{-1L, 0L}, regions.get(0));
|
||||
assertArrayEquals(new long[]{0L, 0L}, regions.get(1));
|
||||
assertEquals(1, result.targetChunks());
|
||||
assertEquals(529, result.chunks().size());
|
||||
long terrain = result.chunks().stream().filter(chunk -> chunk.getPersistedStatus() == ChunkStatus.TERRAIN).count();
|
||||
@@ -189,7 +187,7 @@ public final class HeadlessRegionTerrainTest {
|
||||
}
|
||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||
{"name":"Native region","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
||||
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
||||
"landChance":1,"carvingEnabled":false}
|
||||
""");
|
||||
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||
|
||||
@@ -14,7 +14,7 @@ dependencies {
|
||||
// log4j-core Filter interface, so its constructor reference needs the type resolvable.
|
||||
compileOnly(libs.log4j.core)
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(libs.paper.api)
|
||||
testImplementation(libs.bstats)
|
||||
testImplementation(project(':core'))
|
||||
|
||||
@@ -31,8 +31,7 @@ import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.structure.placement.StructureReachability;
|
||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
@@ -196,9 +195,9 @@ public class CommandFind implements DirectorExecutor {
|
||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find an accepted hydrology feature", sync = true)
|
||||
@Director(description = "Find a hydrology water feature", sync = true)
|
||||
public void river(
|
||||
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class)
|
||||
@Param(description = "Feature type: stream, river, major_river, cascade, waterfall, mouth, pond, lake, dam, sea_inlet or cave_pool", customHandler = HydrologyTypeHandler.class)
|
||||
String type,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
@@ -212,42 +211,32 @@ public class CommandFind implements DirectorExecutor {
|
||||
}
|
||||
return;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
||||
HydrologyFeatureQuery query;
|
||||
HydrologyFeatureType featureType;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
featureType = HydrologyFeatureType.parse(type);
|
||||
} catch (IllegalArgumentException error) {
|
||||
commandSender.sendMessage(C.RED + error.getMessage());
|
||||
return;
|
||||
}
|
||||
String label = featureType.key().replace('_', ' ');
|
||||
Location origin = target.getLocation();
|
||||
World originWorld = origin.getWorld();
|
||||
int requestedDistance = runtime == null
|
||||
? 8192
|
||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime == null
|
||||
? requestedDistance
|
||||
: runtime.maximumFeatureSearchDistance(
|
||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
||||
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
||||
int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||
commandSender.sendMessage(C.GRAY + GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance));
|
||||
J.a(() -> {
|
||||
try {
|
||||
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
|
||||
activeEngine,
|
||||
query,
|
||||
featureType,
|
||||
origin.getBlockX(),
|
||||
origin.getBlockZ(),
|
||||
maximumDistance,
|
||||
(int visited) -> sendStructureMessage(target, commandSender,
|
||||
C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...")
|
||||
maximumDistance
|
||||
).orElse(null);
|
||||
if (feature == null) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No accepted " + type + " hydrology feature found within "
|
||||
+ maximumDistance + " blocks.");
|
||||
C.YELLOW + GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance));
|
||||
return;
|
||||
}
|
||||
String label = type + " hydrology feature";
|
||||
if (!teleport) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
|
||||
@@ -259,8 +248,9 @@ public class CommandFind implements DirectorExecutor {
|
||||
target, originWorld, commandSender, label, destination, true);
|
||||
} catch (Throwable error) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate accepted hydrology feature '" + type + "'.", error);
|
||||
C.RED + "Could not search for " + label + ": " + error.getClass().getSimpleName()
|
||||
+ (error.getMessage() == null ? "" : " - " + error.getMessage()));
|
||||
Iris.reportError("Could not locate hydrology feature '" + featureType.key() + "'.", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+32
-35
@@ -17,8 +17,7 @@ import art.arcane.iris.platform.bukkit.terrain.IrisSampleLimits;
|
||||
import art.arcane.iris.platform.bukkit.terrain.IrisSurfaceClassifier;
|
||||
import art.arcane.iris.platform.bukkit.terrain.IrisWorldInfoFactory;
|
||||
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.raster.WaterRole;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
@@ -108,15 +107,15 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
|
||||
try {
|
||||
int surface = engine.getHeight(blockX, blockZ);
|
||||
HydrologyColumnSample hydrology = hydrologySample(engine, blockX, blockZ);
|
||||
WaterRole role = engine.getComplex().getWaterRoleStream().get(blockX, blockZ);
|
||||
int fluid = (int) Math.round(
|
||||
engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
||||
engine.getComplex().getWaterHeadStream().get(blockX, blockZ));
|
||||
InferredType inferredType = null;
|
||||
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
|
||||
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
||||
inferredType = biome == null ? null : biome.getInferredType();
|
||||
}
|
||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, hydrology);
|
||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, role);
|
||||
} catch (Throwable error) {
|
||||
reportQueryFault("surfaceKind", world, error);
|
||||
return IrisSurfaceKind.UNKNOWN;
|
||||
@@ -266,12 +265,12 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
return false;
|
||||
}
|
||||
|
||||
HydrologyColumnSample hydrology = wantRiver
|
||||
? hydrologySample(engine, blockX, blockZ)
|
||||
: null;
|
||||
WaterRole role = wantRiver
|
||||
? engine.getComplex().getWaterRoleStream().get(blockX, blockZ)
|
||||
: WaterRole.NONE;
|
||||
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
||||
int fluid = wantRiver
|
||||
? (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ))
|
||||
? (int) Math.round(engine.getComplex().getWaterHeadStream().get(blockX, blockZ))
|
||||
: engine.getDimension().getFluidHeight();
|
||||
boolean needsBiome = wantBiome
|
||||
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
||||
@@ -281,29 +280,23 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
surface,
|
||||
fluid,
|
||||
biome == null ? null : biome.getInferredType(),
|
||||
hydrology
|
||||
role
|
||||
)
|
||||
: IrisSurfaceKind.UNKNOWN;
|
||||
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
|
||||
int natural = wantNaturalHeight
|
||||
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
|
||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||
HydrologyColumnLayer surfaceLayer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
boolean riverPresent = surfaceLayer != null;
|
||||
IrisRiverState riverState = wantRiverState
|
||||
? riverState(hydrology)
|
||||
? riverState(role)
|
||||
: IrisRiverState.NONE;
|
||||
double riverDistance = wantRiverDistance && riverPresent
|
||||
? engine.getComplex().getRiverDistanceStream().get(blockX, blockZ)
|
||||
double riverDistance = wantRiverDistance
|
||||
? riverDistance(role)
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||
int riverFlow = wantRiverFlow && riverPresent
|
||||
? (int) Math.round(engine.getComplex().getRiverFlowStream().get(blockX, blockZ))
|
||||
int riverFlow = wantRiverFlow
|
||||
? riverFlow(role)
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||
int riverWaterSurfaceY = wantRiverWaterSurface
|
||||
&& surfaceLayer != null
|
||||
&& surfaceLayer.connectedFluid()
|
||||
int riverWaterSurfaceY = wantRiverWaterSurface && role.wet()
|
||||
? fluid + minHeight
|
||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||
sink.accept(new IrisColumnSample(
|
||||
@@ -327,21 +320,25 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
}
|
||||
}
|
||||
|
||||
static IrisRiverState riverState(HydrologyColumnSample hydrology) {
|
||||
HydrologyColumnLayer layer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
if (layer == null || !layer.channel()) {
|
||||
return IrisRiverState.NONE;
|
||||
}
|
||||
return layer.connectedFluid() ? IrisRiverState.WET : IrisRiverState.DRY;
|
||||
static IrisRiverState riverState(WaterRole role) {
|
||||
return role.wet() ? IrisRiverState.WET : IrisRiverState.NONE;
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample hydrologySample(Engine engine, int blockX, int blockZ) {
|
||||
if (engine.getComplex().getHydrologyRuntime() == null) {
|
||||
return null;
|
||||
}
|
||||
return engine.getComplex().getHydrologyRuntime().sample(blockX, blockZ).orElse(null);
|
||||
static double riverDistance(WaterRole role) {
|
||||
return switch (role) {
|
||||
case NONE -> IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> 0D;
|
||||
case SHORE, POND_SHORE -> 1D;
|
||||
case BANK, DAM -> 2D;
|
||||
};
|
||||
}
|
||||
|
||||
static int riverFlow(WaterRole role) {
|
||||
return switch (role) {
|
||||
case NONE, SHORE, POND_SHORE, BANK, DAM -> IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||
case CHANNEL, MOUTH, CASCADE, WATERFALL -> 1;
|
||||
case POND, INLET -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
private static Optional<String> key(IrisBiome biome) {
|
||||
|
||||
+8
-18
@@ -1,8 +1,7 @@
|
||||
package art.arcane.iris.platform.bukkit.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
|
||||
public final class IrisSurfaceClassifier {
|
||||
@@ -29,25 +28,16 @@ public final class IrisSurfaceClassifier {
|
||||
int engineSurfaceHeight,
|
||||
int engineFluidHeight,
|
||||
InferredType inferredType,
|
||||
HydrologyColumnSample hydrology
|
||||
WaterRole role
|
||||
) {
|
||||
if (engineSurfaceHeight <= 0) {
|
||||
return IrisSurfaceKind.VOID;
|
||||
}
|
||||
HydrologyColumnLayer layer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
if (layer != null) {
|
||||
if (layer.shore()) {
|
||||
return IrisSurfaceKind.RIVER_SHORE;
|
||||
}
|
||||
if (layer.channel()) {
|
||||
return layer.connectedFluid() ? IrisSurfaceKind.RIVER : IrisSurfaceKind.DRY_CHANNEL;
|
||||
}
|
||||
if (layer.grading()) {
|
||||
return IrisSurfaceKind.LAND;
|
||||
}
|
||||
}
|
||||
return classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
||||
return switch (role) {
|
||||
case NONE -> classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
||||
case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE;
|
||||
case BANK, DAM -> IrisSurfaceKind.LAND;
|
||||
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER;
|
||||
};
|
||||
}
|
||||
}
|
||||
+77
-10
@@ -1,7 +1,7 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
@@ -22,6 +22,7 @@ import java.util.OptionalLong;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
@@ -45,7 +46,7 @@ public class CommandFindHydrologyTest {
|
||||
@Test
|
||||
public void worldChangeDuringSearchDoesNotLoadOrTeleportIntoTheNewWorld() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
||||
fixture.search.get().run();
|
||||
|
||||
@@ -58,7 +59,7 @@ public class CommandFindHydrologyTest {
|
||||
@Test
|
||||
public void disconnectDuringSearchDoesNotLoadTheDestination() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
when(fixture.player.isOnline()).thenReturn(false);
|
||||
fixture.search.get().run();
|
||||
|
||||
@@ -69,7 +70,7 @@ public class CommandFindHydrologyTest {
|
||||
@Test
|
||||
public void worldChangeWhileLoadingTheDestinationCancelsTeleport() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
fixture.search.get().run();
|
||||
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
||||
fixture.chunk.complete(mock(Chunk.class));
|
||||
@@ -82,7 +83,7 @@ public class CommandFindHydrologyTest {
|
||||
@Test
|
||||
public void successIsReportedOnlyAfterTeleportCompletes() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
fixture.search.get().run();
|
||||
fixture.chunk.complete(mock(Chunk.class));
|
||||
|
||||
@@ -95,7 +96,7 @@ public class CommandFindHydrologyTest {
|
||||
@Test
|
||||
public void rejectedTeleportReportsFailureWithoutSuccess() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
fixture.search.get().run();
|
||||
fixture.chunk.complete(mock(Chunk.class));
|
||||
fixture.teleport.complete(false);
|
||||
@@ -109,7 +110,7 @@ public class CommandFindHydrologyTest {
|
||||
public void failedTeleportReportsTheFullCauseWithoutSuccess() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
IllegalStateException failure = new IllegalStateException("destination unloaded");
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
fixture.search.get().run();
|
||||
fixture.chunk.complete(mock(Chunk.class));
|
||||
fixture.teleport.completeExceptionally(failure);
|
||||
@@ -120,6 +121,72 @@ public class CommandFindHydrologyTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyFeatureTypeIsSearchedOffTheCommandThreadAtTheRuntimeRadius() {
|
||||
for (HydrologyFeatureType type : HydrologyFeatureType.values()) {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river(type.key(), false);
|
||||
fixture.queries.verify(() -> GenerationSemanticQueries.nearestRiver(any(), any(), anyInt(), anyInt(), anyInt()), never());
|
||||
fixture.search.get().run();
|
||||
|
||||
fixture.queries.verify(() -> GenerationSemanticQueries.nearestRiver(fixture.engine, type, 0, 0,
|
||||
GenerationSemanticQueries.RIVER_SEARCH_DISTANCE));
|
||||
verify(fixture.sender).sendMessage(contains("Found " + type.key().replace('_', ' ') + " at 32, 63, 32."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownTypeIsRejectedWithTheAcceptedTypesBeforeAnySearch() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
|
||||
verify(fixture.sender).sendMessage(contains("Use one of: stream, river, major_river, cascade, waterfall, mouth, pond, lake, dam, sea_inlet, cave_pool."));
|
||||
assertNull(fixture.search.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingCavePoolIsReportedAsNotFoundInGeneratedChunks() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||
.thenReturn(Optional.empty());
|
||||
fixture.command.river("cave_pool", true);
|
||||
fixture.search.get().run();
|
||||
|
||||
verify(fixture.sender).sendMessage(contains("Searching generated chunks for the nearest cave pool within 7168 blocks"));
|
||||
verify(fixture.sender).sendMessage(contains("No cave pool found in generated chunks within 7168 blocks."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingFeatureIsReportedAsNotFound() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||
.thenReturn(Optional.empty());
|
||||
fixture.command.river("sea_inlet", true);
|
||||
fixture.search.get().run();
|
||||
|
||||
verify(fixture.sender).sendMessage(contains("No sea inlet found within 7168 blocks."));
|
||||
fixture.platform.verify(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true)), never());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerFailureIsReportedAsAnErrorNotAsNotFound() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
IllegalStateException failure = new IllegalStateException("window planner exploded");
|
||||
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||
.thenThrow(failure);
|
||||
fixture.command.river("lake", true);
|
||||
fixture.search.get().run();
|
||||
|
||||
verify(fixture.sender).sendMessage(contains("Could not search for lake: IllegalStateException - window planner exploded"));
|
||||
verify(fixture.sender, never()).sendMessage(contains("No lake found"));
|
||||
fixture.logging.verify(() -> Iris.reportError(any(String.class), eq(failure)));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final CommandFind command = mock(CommandFind.class, CALLS_REAL_METHODS);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
@@ -132,7 +199,7 @@ public class CommandFindHydrologyTest {
|
||||
private final CompletableFuture<Boolean> teleport = new CompletableFuture<>();
|
||||
private final MockedStatic<J> scheduler = mockStatic(J.class);
|
||||
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
||||
private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class);
|
||||
private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class, CALLS_REAL_METHODS);
|
||||
private final MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class, invocation -> "teleported");
|
||||
private final MockedStatic<Iris> logging = mockStatic(Iris.class);
|
||||
|
||||
@@ -160,10 +227,10 @@ public class CommandFindHydrologyTest {
|
||||
});
|
||||
platform.when(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true))).thenReturn(chunk);
|
||||
platform.when(() -> BukkitPlatform.teleportAsync(eq(player), any(Location.class))).thenReturn(teleport);
|
||||
queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt(), any()))
|
||||
queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt()))
|
||||
.thenReturn(Optional.of(new GenerationSemanticQueries.RiverResult(
|
||||
GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION,
|
||||
HydrologyFeatureType.RIFFLE, Optional.empty(), 1L, 32, 63, 32, OptionalLong.empty())));
|
||||
HydrologyFeatureType.RIVER, 1L, 32, 63, 32, OptionalLong.empty())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-5
@@ -20,26 +20,26 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
public class CommandObjectFluidHeightTest {
|
||||
@Test
|
||||
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() {
|
||||
public void absoluteObjectPlacementShiftsTheColumnWaterHeadFromNegativeMinY() {
|
||||
World world = mock(World.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ProceduralStream<Double> riverHead = mock(ProceduralStream.class);
|
||||
ProceduralStream<Double> waterHead = mock(ProceduralStream.class);
|
||||
Map<Block, BlockData> future = new HashMap<>();
|
||||
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(dimension.getFluidHeight()).thenReturn(127);
|
||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead);
|
||||
when(riverHead.get(12, -7)).thenReturn(131D);
|
||||
when(complex.getWaterHeadStream()).thenReturn(waterHead);
|
||||
when(waterHead.get(12, -7)).thenReturn(131D);
|
||||
|
||||
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
||||
|
||||
assertEquals(63, placer.getFluidHeight());
|
||||
assertEquals(67, placer.getFluidHeight(12, -7));
|
||||
verify(riverHead).get(12, -7);
|
||||
verify(waterHead).get(12, -7);
|
||||
}
|
||||
}
|
||||
+60
-45
@@ -9,16 +9,18 @@ import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
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.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
import art.arcane.iris.api.terrain.IrisColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
@@ -26,6 +28,7 @@ import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -112,10 +115,32 @@ public class IrisTerrainSVCTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedHydrologyMapsToThePublicDiagnosticStates() {
|
||||
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(null));
|
||||
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(true)));
|
||||
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(false)));
|
||||
public void waterRolesMapToThePublicRiverFields() {
|
||||
for (WaterRole role : WaterRole.values()) {
|
||||
assertEquals(role.name(), role.wet() ? IrisRiverState.WET : IrisRiverState.NONE, IrisTerrainSVC.riverState(role));
|
||||
double distance = switch (role) {
|
||||
case NONE -> IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> 0D;
|
||||
case SHORE, POND_SHORE -> 1D;
|
||||
case BANK, DAM -> 2D;
|
||||
};
|
||||
assertEquals(role.name(), distance, IrisTerrainSVC.riverDistance(role), 0D);
|
||||
int flow = switch (role) {
|
||||
case NONE, SHORE, POND_SHORE, BANK, DAM -> IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||
case CHANNEL, MOUTH, CASCADE, WATERFALL -> 1;
|
||||
case POND, INLET -> 0;
|
||||
};
|
||||
assertEquals(role.name(), flow, IrisTerrainSVC.riverFlow(role));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnSamplesReportTheWaterRoleOfEachColumn() throws Exception {
|
||||
assertEquals(List.of("RIVER", "WET", "0.0", "1", "6"), sampled(WaterRole.CHANNEL, 70));
|
||||
assertEquals(List.of("RIVER", "WET", "0.0", "0", "6"), sampled(WaterRole.POND, 70));
|
||||
assertEquals(List.of("RIVER_SHORE", "NONE", "1.0", "-1", "unavailable"), sampled(WaterRole.SHORE, 50));
|
||||
assertEquals(List.of("LAND", "NONE", "2.0", "-1", "unavailable"), sampled(WaterRole.BANK, 50));
|
||||
assertEquals(List.of("LAND", "NONE", "NaN", "-1", "unavailable"), sampled(WaterRole.NONE, 50));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -219,6 +244,33 @@ public class IrisTerrainSVCTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> sampled(WaterRole role, int head) throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
when(biome.getInferredType()).thenReturn(InferredType.LAND);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
|
||||
when(engine.getHeight(0, 0)).thenReturn(60);
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getSurfaceBiome(0, 0)).thenReturn(biome);
|
||||
when(complex.getWaterRoleStream()).thenReturn(ProceduralStream.of((x, z) -> role, Interpolated.of(value -> 0D, value -> role)));
|
||||
when(complex.getWaterHeadStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> (double) head));
|
||||
IrisColumnQuery query = IrisColumnQuery.rect(0, 0, 0, 0, 1, EnumSet.of(IrisColumnField.SURFACE_KIND,
|
||||
IrisColumnField.RIVER_STATE, IrisColumnField.RIVER_DISTANCE, IrisColumnField.RIVER_FLOW,
|
||||
IrisColumnField.RIVER_WATER_SURFACE_Y));
|
||||
List<IrisColumnSample> samples = new ArrayList<>();
|
||||
try (MockedStatic<IrisSettings> settings = mockStatic(IrisSettings.class)) {
|
||||
settings.when(IrisSettings::get).thenReturn(new IrisSettings());
|
||||
assertTrue(enabledService().sampleColumns(world(engine), query, samples::add));
|
||||
}
|
||||
assertEquals(1, samples.size());
|
||||
IrisColumnSample sample = samples.get(0);
|
||||
return List.of(sample.surfaceKind().name(), sample.riverState().name(), String.valueOf(sample.riverDistance()),
|
||||
String.valueOf(sample.riverFlow()),
|
||||
sample.riverWaterSurfaceY() == IrisColumnSample.UNAVAILABLE_HEIGHT ? "unavailable" : String.valueOf(sample.riverWaterSurfaceY()));
|
||||
}
|
||||
|
||||
private static World world(Engine engine) {
|
||||
World world = mock(World.class);
|
||||
ChunkGenerator generator = mock(ChunkGenerator.class, withSettings().extraInterfaces(PlatformChunkGenerator.class));
|
||||
@@ -235,41 +287,4 @@ public class IrisTerrainSVCTest {
|
||||
((AtomicBoolean) enabled.get(service)).set(true);
|
||||
return service;
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample river(boolean connectedFluid) {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
1L,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
2L,
|
||||
3L,
|
||||
0,
|
||||
63,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
false
|
||||
);
|
||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
||||
feature,
|
||||
60,
|
||||
63,
|
||||
63,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
connectedFluid,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
connectedFluid,
|
||||
false,
|
||||
"water",
|
||||
"river",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"cave"
|
||||
);
|
||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
||||
}
|
||||
}
|
||||
+18
-93
@@ -1,15 +1,10 @@
|
||||
package art.arcane.iris.platform.bukkit.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
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.raster.WaterRole;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -69,98 +64,28 @@ public class IrisSurfaceClassifierTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedSurfaceRolesOverrideGenericOceanAndLandKinds() {
|
||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
63,
|
||||
InferredType.SEA,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
63,
|
||||
InferredType.SEA,
|
||||
hydrology(HydrologyFeatureType.MOUTH, true, false, true)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify(
|
||||
64,
|
||||
63,
|
||||
InferredType.SHORE,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, true, false)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
60,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, false)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
60,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, false, false)
|
||||
));
|
||||
public void waterRolesOverrideGenericOceanAndLandKinds() {
|
||||
for (WaterRole role : WaterRole.values()) {
|
||||
IrisSurfaceKind expected = switch (role) {
|
||||
case NONE -> IrisSurfaceKind.OCEAN;
|
||||
case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE;
|
||||
case BANK, DAM -> IrisSurfaceKind.LAND;
|
||||
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER;
|
||||
};
|
||||
assertEquals(role.name(), expected, IrisSurfaceClassifier.classify(60, 63, InferredType.SEA, role));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void voidClassificationWinsOverAcceptedRiverGeometry() {
|
||||
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
|
||||
0,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
||||
));
|
||||
public void voidClassificationWinsOverWaterRoles() {
|
||||
for (WaterRole role : WaterRole.values()) {
|
||||
assertEquals(role.name(), IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(0, 63, InferredType.LAND, role));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentRejectedCandidatesDoNotCreatePublicRiverKinds() {
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
||||
64,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample hydrology(
|
||||
HydrologyFeatureType type,
|
||||
boolean channel,
|
||||
boolean shore,
|
||||
boolean connectedFluid
|
||||
) {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
1L,
|
||||
type,
|
||||
2L,
|
||||
3L,
|
||||
0,
|
||||
63,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
false
|
||||
);
|
||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
||||
feature,
|
||||
60,
|
||||
63,
|
||||
63,
|
||||
channel,
|
||||
shore,
|
||||
!channel && !shore,
|
||||
connectedFluid,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
connectedFluid && channel,
|
||||
false,
|
||||
"water",
|
||||
"river",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"cave"
|
||||
);
|
||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
||||
public void columnsWithoutAWaterRoleKeepTheGenericKinds() {
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(64, 63, InferredType.LAND, WaterRole.NONE));
|
||||
assertEquals(IrisSurfaceKind.SHORE, IrisSurfaceClassifier.classify(64, 63, InferredType.SHORE, WaterRole.NONE));
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,7 @@ dependencies {
|
||||
String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation'
|
||||
add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(testFixtures('art.arcane:core'))
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
@@ -236,6 +236,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
||||
options.release.set((int) irisModTarget.java)
|
||||
}
|
||||
|
||||
apply from: file('../../gradle/mockito-agent.gradle')
|
||||
|
||||
tasks.named('test').configure {
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ dependencies {
|
||||
? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||
: minecraft.dependency("iris-${buildLane}", "net.minecraftforge:forge:${forgeVersion}"))
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(testFixtures('art.arcane:core'))
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
@@ -272,6 +272,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
||||
options.release.set((int) irisModTarget.java)
|
||||
}
|
||||
|
||||
apply from: file('../../gradle/mockito-agent.gradle')
|
||||
|
||||
tasks.named('test').configure {
|
||||
}
|
||||
|
||||
|
||||
+2
-20
@@ -25,7 +25,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.structure.placement.StructureReachability;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
@@ -61,7 +61,7 @@ final class ModdedCommandSuggestions {
|
||||
static final SuggestionProvider<NativeCommandSource> OBJECT_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
|
||||
static final SuggestionProvider<NativeCommandSource> STRUCTURE_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
|
||||
static final SuggestionProvider<NativeCommandSource> POI_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(List.of("buried_treasure"), builder);
|
||||
static final SuggestionProvider<NativeCommandSource> HYDROLOGY_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestHydrologyTypes(context, builder);
|
||||
static final SuggestionProvider<NativeCommandSource> HYDROLOGY_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(HydrologyFeatureType.keys(), builder);
|
||||
static final SuggestionProvider<NativeCommandSource> PACK_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
||||
static final SuggestionProvider<NativeCommandSource> DIMENSION_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
||||
|
||||
@@ -87,24 +87,6 @@ final class ModdedCommandSuggestions {
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestHydrologyTypes(
|
||||
CommandContext<NativeCommandSource> context,
|
||||
SuggestionsBuilder builder
|
||||
) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().world());
|
||||
if (engine != null && engine.getComplex().getHydrologyRuntime() != null) {
|
||||
return NativeCommandRegistration.suggest(
|
||||
engine.getComplex().getHydrologyRuntime().featureQueryKeys(), builder);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
warnTabFailure("hydrology types", context.getSource(), error);
|
||||
}
|
||||
return NativeCommandRegistration.suggest(
|
||||
HydrologyFeatureQuery.suggestions(List.of()), builder);
|
||||
}
|
||||
|
||||
static Set<String> reachableBiomeKeys(Engine engine) {
|
||||
return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
|
||||
}
|
||||
|
||||
+13
-22
@@ -39,8 +39,7 @@ import art.arcane.iris.structure.placement.StructureReachability;
|
||||
import art.arcane.iris.generation.runtime.WrongEngineBroException;
|
||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
@@ -143,39 +142,31 @@ final class ModdedLocateCommands {
|
||||
IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
||||
HydrologyFeatureQuery query;
|
||||
HydrologyFeatureType featureType;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
featureType = HydrologyFeatureType.parse(type);
|
||||
} catch (IllegalArgumentException error) {
|
||||
IrisModdedCommands.fail(source, error.getMessage());
|
||||
return 0;
|
||||
}
|
||||
String label = featureType.key().replace('_', ' ');
|
||||
int originX = player.blockX();
|
||||
int originZ = player.blockZ();
|
||||
int requestedDistance = runtime == null
|
||||
? 8192
|
||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime == null
|
||||
? requestedDistance
|
||||
: runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
|
||||
int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||
NativeModdedServer server = source.server();
|
||||
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
|
||||
IrisModdedCommands.ok(source, GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance));
|
||||
MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
|
||||
engine, query, originX, originZ, maximumDistance,
|
||||
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
|
||||
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null))
|
||||
engine, featureType, originX, originZ, maximumDistance).orElse(null))
|
||||
.whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> {
|
||||
if (error != null) {
|
||||
ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
|
||||
IrisModdedCommands.fail(source,
|
||||
"Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
||||
Throwable cause = error instanceof CompletionException && error.getCause() != null ? error.getCause() : error;
|
||||
ModdedIrisLog.error("Hydrology locate failed for {}", featureType.key(), cause);
|
||||
IrisModdedCommands.fail(source, "Could not search for " + label + ": " + cause.getClass().getSimpleName()
|
||||
+ (cause.getMessage() == null ? "" : " - " + cause.getMessage()));
|
||||
return;
|
||||
}
|
||||
if (feature == null) {
|
||||
IrisModdedCommands.fail(source,
|
||||
"No accepted " + type + " hydrology feature found within "
|
||||
+ maximumDistance + " blocks.");
|
||||
IrisModdedCommands.fail(source, GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance));
|
||||
return;
|
||||
}
|
||||
teleportToStructure(
|
||||
@@ -185,7 +176,7 @@ final class ModdedLocateCommands {
|
||||
feature.x(),
|
||||
feature.y() + 2,
|
||||
feature.z(),
|
||||
type + " hydrology feature"
|
||||
label
|
||||
);
|
||||
}));
|
||||
return 1;
|
||||
|
||||
+14
@@ -23,6 +23,7 @@ import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||
import art.arcane.iris.world.pregen.PregenHydrology;
|
||||
import art.arcane.iris.world.pregen.PregenListener;
|
||||
import art.arcane.iris.world.pregen.PregenMantleBackpressure;
|
||||
import art.arcane.iris.world.pregen.PregeneratorMethod;
|
||||
@@ -30,6 +31,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.modded.ModdedGenPool;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -71,6 +73,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
private final PregenMantleBackpressure backpressure;
|
||||
private final PauseWhenEmptyGuard pauseGuard;
|
||||
private final LongSupplier clock;
|
||||
private final AtomicBoolean closing = new AtomicBoolean();
|
||||
private final PregenHydrology hydrology;
|
||||
|
||||
public ModdedPregenMethod(Configuration configuration) {
|
||||
this(configuration, System::nanoTime);
|
||||
@@ -80,6 +84,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
runtime = configuration.runtime();
|
||||
engine = configuration.engine();
|
||||
hydrology = new PregenHydrology(() -> engine);
|
||||
sync = configuration.sync();
|
||||
pauseGuard = new PauseWhenEmptyGuard(runtime);
|
||||
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
|
||||
@@ -121,6 +126,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closing.set(true);
|
||||
hydrology.close();
|
||||
try {
|
||||
awaitPendingChunks();
|
||||
ModdedIrisLog.info("Iris modded pregen closing: dim={} completed={} pending={} peakInFlight={} finalLimit={}",
|
||||
@@ -341,6 +348,13 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegionsAhead(List<long[]> regions) {
|
||||
if (!closing.get()) {
|
||||
hydrology.prepare(regions);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod(int x, int z) {
|
||||
return "Modded";
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureRef;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeStructureQueries;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedConstruction;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockConstruction;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedLocateRiverTest {
|
||||
private static final int DISTANCE = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||
|
||||
@Test
|
||||
public void unknownTypeFailsBeforeSearching() {
|
||||
Fixture fixture = new Fixture();
|
||||
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||
assertEquals(0, ModdedLocateCommands.gotoRiver(fixture.source, "ocean_trench"));
|
||||
|
||||
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()));
|
||||
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), never());
|
||||
verify(fixture.server, never()).execute(any());
|
||||
verify(fixture.runtime, never()).nearestFeature(anyInt(), anyInt(), any(), anyInt(), any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingFeatureReportsNotFound() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any())).thenReturn(Optional.empty());
|
||||
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "river"));
|
||||
fixture.finishOnServerThread();
|
||||
|
||||
commands.verify(() -> IrisModdedCommands.fail(fixture.source, "No river found within " + DISTANCE + " blocks."));
|
||||
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerFailureIsReportedAsAnErrorNotAsNotFound() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any()))
|
||||
.thenThrow(new IllegalStateException("window planner exploded"));
|
||||
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "major_river"));
|
||||
fixture.finishOnServerThread();
|
||||
|
||||
commands.verify(() -> IrisModdedCommands.fail(fixture.source,
|
||||
"Could not search for major river: IllegalStateException - window planner exploded"));
|
||||
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), eq("No major river found within " + DISTANCE + " blocks.")),
|
||||
never());
|
||||
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void foundFeatureTeleportsAboveItFromThePlayersPosition() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any()))
|
||||
.thenReturn(Optional.of(new HydrologyFeatureRef(9L, HydrologyFeatureType.WATERFALL, 400, 130, -250)));
|
||||
when(fixture.player.teleport(fixture.world, 400.5D, 68D, -249.5D)).thenReturn(true);
|
||||
try (MockedStatic<IrisModdedCommands> commands = fixture.commands();
|
||||
MockedConstruction<NativeStructureQueries> queries = mockConstruction(NativeStructureQueries.class)) {
|
||||
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "waterfall"));
|
||||
fixture.finishOnServerThread();
|
||||
|
||||
ArgumentCaptor<HydrologyFeatureQuery> query = ArgumentCaptor.forClass(HydrologyFeatureQuery.class);
|
||||
verify(fixture.runtime).nearestFeature(eq(120), eq(-75), query.capture(), eq(DISTANCE), any());
|
||||
assertEquals(HydrologyFeatureQuery.of(HydrologyFeatureType.WATERFALL), query.getValue());
|
||||
verify(queries.constructed().getFirst()).loadChunk(400 >> 4, -250 >> 4);
|
||||
verify(fixture.player).teleport(fixture.world, 400.5D, 68D, -249.5D);
|
||||
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), times(2));
|
||||
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()), never());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final NativeCommandSource source = mock(NativeCommandSource.class);
|
||||
private final NativeWorld world = mock(NativeWorld.class);
|
||||
private final NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||
private final NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||
private final CompletableFuture<Runnable> completion = new CompletableFuture<>();
|
||||
|
||||
private Fixture() {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(source.player()).thenReturn(player);
|
||||
when(source.world()).thenReturn(world);
|
||||
when(source.server()).thenReturn(server);
|
||||
when(player.blockX()).thenReturn(120);
|
||||
when(player.blockZ()).thenReturn(-75);
|
||||
when(player.connected()).thenReturn(true);
|
||||
when(player.inWorld(world)).thenReturn(true);
|
||||
when(world.minHeight()).thenReturn(-64);
|
||||
when(world.maxHeight()).thenReturn(320);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(dimension.getMinHeight()).thenReturn(-64);
|
||||
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(runtime.enabled()).thenReturn(true);
|
||||
doAnswer(invocation -> {
|
||||
completion.complete(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(server).execute(any());
|
||||
}
|
||||
|
||||
private MockedStatic<IrisModdedCommands> commands() {
|
||||
MockedStatic<IrisModdedCommands> commands = mockStatic(IrisModdedCommands.class);
|
||||
commands.when(() -> IrisModdedCommands.engineFor(world)).thenReturn(engine);
|
||||
return commands;
|
||||
}
|
||||
|
||||
private void finishOnServerThread() throws Exception {
|
||||
completion.get(10L, TimeUnit.SECONDS).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.modded.ModdedGenPool;
|
||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||
@@ -11,8 +12,10 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -31,9 +34,11 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -89,6 +94,25 @@ public class ModdedPregenMethodLifecycleTest {
|
||||
verify(runtime, never()).execute(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeWithdrawsQueuedHydrologyPrefetchAndRefusesLaterAnnouncements() {
|
||||
HydrologyPregeneration epoch = mock(HydrologyPregeneration.class);
|
||||
when(engine.getComplex().getHydrologyRuntime().openPregeneration()).thenReturn(epoch);
|
||||
when(runtime.isServerThread()).thenReturn(true);
|
||||
assertTrue(method.deferFinalSaveToServerThread());
|
||||
List<long[]> ahead = List.of(new long[]{0L, 0L}, new long[]{1L, 0L});
|
||||
|
||||
method.onRegionsAhead(ahead);
|
||||
method.close();
|
||||
method.onRegionsAhead(ahead);
|
||||
|
||||
InOrder order = inOrder(epoch);
|
||||
order.verify(epoch).prepare(ahead);
|
||||
order.verify(epoch).close();
|
||||
verify(epoch, times(1)).prepare(anyList());
|
||||
method.completeDeferredFinalSave();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void asynchronousCallerCannotClaimTheShutdownSave() {
|
||||
assertFalse(method.deferFinalSaveToServerThread());
|
||||
|
||||
@@ -141,7 +141,7 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
||||
|
||||
dependencies {
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(testFixtures('art.arcane:core'))
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
@@ -261,6 +261,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
||||
options.release.set((int) irisModTarget.java)
|
||||
}
|
||||
|
||||
apply from: file('../../gradle/mockito-agent.gradle')
|
||||
|
||||
tasks.named('test').configure {
|
||||
useJUnitPlatform()
|
||||
classpath = classpath.minus(developmentModLibraries).plus(sourceSets.modLibraries.output)
|
||||
|
||||
Reference in new issue
Block a user