mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 16:17:44 +00:00
Merge terrain-first hydrology into master
Surface rivers v2: valley-first eroded channels, spring pools, standing pools, tributaries, bed padding, ring-ordered feature search, schema v2 hydrology configuration and the transect probe.
This commit is contained in:
commit
665b201e41
330 files changed
+60150
-606
No files matched your search
+6
-1
@@ -2,6 +2,11 @@ package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisColumnField {
|
||||
SURFACE_HEIGHT,
|
||||
NATURAL_HEIGHT,
|
||||
SURFACE_KIND,
|
||||
BIOME_KEY
|
||||
BIOME_KEY,
|
||||
RIVER_STATE,
|
||||
RIVER_DISTANCE,
|
||||
RIVER_FLOW,
|
||||
RIVER_WATER_SURFACE_Y
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record IrisColumnSample(
|
||||
int blockX,
|
||||
int blockZ,
|
||||
int surfaceHeight,
|
||||
int naturalHeight,
|
||||
IrisSurfaceKind surfaceKind,
|
||||
String biomeKey,
|
||||
IrisRiverState riverState,
|
||||
double riverDistance,
|
||||
int riverFlow,
|
||||
int riverWaterSurfaceY
|
||||
) {
|
||||
public static final int UNAVAILABLE_HEIGHT = Integer.MIN_VALUE;
|
||||
public static final double UNAVAILABLE_RIVER_DISTANCE = Double.NaN;
|
||||
public static final int UNAVAILABLE_RIVER_FLOW = -1;
|
||||
|
||||
public IrisColumnSample {
|
||||
Objects.requireNonNull(surfaceKind, "surfaceKind");
|
||||
Objects.requireNonNull(riverState, "riverState");
|
||||
biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey;
|
||||
if (!Double.isNaN(riverDistance) && (!Double.isFinite(riverDistance) || riverDistance < 0D)) {
|
||||
throw new IllegalArgumentException("riverDistance must be non-negative, finite, or unavailable");
|
||||
}
|
||||
if (riverFlow < UNAVAILABLE_RIVER_FLOW) {
|
||||
throw new IllegalArgumentException("riverFlow must be non-negative or unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSurfaceHeight() {
|
||||
return surfaceHeight != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
|
||||
public boolean hasNaturalHeight() {
|
||||
return naturalHeight != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
|
||||
public boolean hasSurfaceKind() {
|
||||
return surfaceKind != IrisSurfaceKind.UNKNOWN;
|
||||
}
|
||||
|
||||
public boolean hasBiomeKey() {
|
||||
return biomeKey != null;
|
||||
}
|
||||
|
||||
public boolean hasRiverState() {
|
||||
return riverState != IrisRiverState.NONE;
|
||||
}
|
||||
|
||||
public boolean hasRiverDistance() {
|
||||
return !Double.isNaN(riverDistance);
|
||||
}
|
||||
|
||||
public boolean hasRiverFlow() {
|
||||
return riverFlow != UNAVAILABLE_RIVER_FLOW;
|
||||
}
|
||||
|
||||
public boolean hasRiverWaterSurfaceY() {
|
||||
return riverWaterSurfaceY != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,5 +2,5 @@ package art.arcane.iris.api.terrain;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface IrisColumnSink {
|
||||
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
|
||||
void accept(IrisColumnSample sample);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisRiverState {
|
||||
NONE,
|
||||
WET,
|
||||
DRY
|
||||
}
|
||||
@@ -4,6 +4,9 @@ public enum IrisSurfaceKind {
|
||||
UNKNOWN,
|
||||
LAND,
|
||||
SHORE,
|
||||
RIVER,
|
||||
RIVER_SHORE,
|
||||
DRY_CHANNEL,
|
||||
OCEAN,
|
||||
VOID
|
||||
}
|
||||
+75
-1
@@ -28,6 +28,9 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
@@ -37,7 +40,9 @@ import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.HydrologyTypeHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
@@ -68,7 +73,7 @@ import java.util.Set;
|
||||
public class CommandFind implements DirectorExecutor {
|
||||
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
||||
public void biome(
|
||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look")
|
||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
||||
IrisBiome biome,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
@@ -116,6 +121,75 @@ public class CommandFind implements DirectorExecutor {
|
||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find an accepted hydrology feature")
|
||||
public void river(
|
||||
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class)
|
||||
String type,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine activeEngine = engine();
|
||||
VolmitSender commandSender = sender();
|
||||
Player target = player();
|
||||
if (activeEngine == null || commandSender == null || target == null) {
|
||||
if (commandSender != null) {
|
||||
commandSender.sendMessage(C.RED + "Run this command from an Iris world.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
||||
if (runtime == null) {
|
||||
commandSender.sendMessage(C.YELLOW + "Hydrology is not active in this world.");
|
||||
return;
|
||||
}
|
||||
HydrologyFeatureQuery query;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
} catch (IllegalArgumentException error) {
|
||||
commandSender.sendMessage(C.RED + error.getMessage());
|
||||
return;
|
||||
}
|
||||
Location origin = target.getLocation();
|
||||
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime.maximumFeatureSearchDistance(
|
||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
||||
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
||||
J.a(() -> {
|
||||
try {
|
||||
HydrologyFeatureRef feature = runtime.nearestFeature(
|
||||
query.types(),
|
||||
query.profileKey(),
|
||||
origin.getBlockX(),
|
||||
origin.getBlockZ(),
|
||||
maximumDistance,
|
||||
(int visited) -> sendStructureMessage(target, commandSender,
|
||||
C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...")
|
||||
).orElse(null);
|
||||
if (feature == null) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No accepted " + type + " hydrology feature found within "
|
||||
+ maximumDistance + " blocks.");
|
||||
return;
|
||||
}
|
||||
int worldY = feature.y() + activeEngine.getDimension().getMinHeight();
|
||||
String label = type + " hydrology feature";
|
||||
if (!teleport) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + worldY + ", " + feature.z() + ".");
|
||||
return;
|
||||
}
|
||||
Location destination = new Location(
|
||||
target.getWorld(), feature.x(), worldY, feature.z());
|
||||
prepareStructureTeleport(
|
||||
target, target.getWorld(), 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", descriptionKey = "iris.director.commandfind.director.find_structure_vanilla_key_like_minecraft_village_plains_minecraft_stronghold_imported_iris", sync = true)
|
||||
public void structure(
|
||||
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", descriptionKey = "iris.director.commandfind.param.structure_look_e_g_minecraft_village_plains_minecraft_stronghold_minecraft_ancient_city", customHandler = StructureHandler.class)
|
||||
|
||||
@@ -25,7 +25,10 @@ import art.arcane.iris.core.pack.PackValidationCache;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
@@ -90,6 +93,18 @@ public class CommandPack implements DirectorExecutor {
|
||||
persistValidationCache(packsRoot);
|
||||
}
|
||||
|
||||
@Director(description = "Package a validated dimension into a compressed format", descriptionKey = "iris.director.commandstudio.director.package_dimension_into_compressed_format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", descriptionKey = "iris.director.commandstudio.param.dimension_pack_compress", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_obfuscate_pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@Param(name = "minify", description = "Whether or not to minify the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_minify_pack", defaultValue = "true")
|
||||
boolean minify
|
||||
) {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Director(description = "Preview or apply unused-resource cleanup", descriptionKey = "iris.director.commandpack.director.preview_apply_unused_resource_cleanup", aliases = {"c"})
|
||||
public void cleanup(
|
||||
@Param(description = "The pack folder name to clean", descriptionKey = "iris.director.commandpack.param.pack_folder_name_clean")
|
||||
|
||||
@@ -420,18 +420,6 @@ public class CommandStudio implements DirectorExecutor {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
||||
}
|
||||
|
||||
@Director(description = "Package a dimension into a compressed format", descriptionKey = "iris.director.commandstudio.director.package_dimension_into_compressed_format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", descriptionKey = "iris.director.commandstudio.param.dimension_pack_compress", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_obfuscate_pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@Param(name = "minify", description = "Whether or not to minify the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_minify_pack", defaultValue = "true")
|
||||
boolean minify
|
||||
) {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
|
||||
public void profile(
|
||||
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
|
||||
+1
-1
@@ -318,7 +318,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File file = switch (type) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
|
||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
|
||||
+77
-5
@@ -2,7 +2,9 @@ package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisColumnField;
|
||||
import art.arcane.iris.api.terrain.IrisColumnQuery;
|
||||
import art.arcane.iris.api.terrain.IrisColumnSample;
|
||||
import art.arcane.iris.api.terrain.IrisColumnSink;
|
||||
import art.arcane.iris.api.terrain.IrisRiverState;
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
||||
import art.arcane.iris.api.terrain.IrisWorldInfo;
|
||||
@@ -13,6 +15,8 @@ import art.arcane.iris.core.service.terrain.IrisSampleLimits;
|
||||
import art.arcane.iris.core.service.terrain.IrisSurfaceClassifier;
|
||||
import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
@@ -101,13 +105,15 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
|
||||
try {
|
||||
int surface = engine.getHeight(blockX, blockZ);
|
||||
int fluid = engine.getDimension().getFluidHeight();
|
||||
HydrologyColumnSample hydrology = hydrologySample(engine, blockX, blockZ);
|
||||
int fluid = (int) Math.round(
|
||||
engine.getComplex().getRiverWaterSurfaceStream().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);
|
||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, hydrology);
|
||||
} catch (Throwable error) {
|
||||
reportQueryFault("surfaceKind", world, error);
|
||||
return IrisSurfaceKind.UNKNOWN;
|
||||
@@ -224,26 +230,75 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
|
||||
EnumSet<IrisColumnField> fields = query.fields();
|
||||
boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT);
|
||||
boolean wantNaturalHeight = fields.contains(IrisColumnField.NATURAL_HEIGHT);
|
||||
boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND);
|
||||
boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY);
|
||||
boolean wantRiverState = fields.contains(IrisColumnField.RIVER_STATE);
|
||||
boolean wantRiverDistance = fields.contains(IrisColumnField.RIVER_DISTANCE);
|
||||
boolean wantRiverFlow = fields.contains(IrisColumnField.RIVER_FLOW);
|
||||
boolean wantRiverWaterSurface = fields.contains(IrisColumnField.RIVER_WATER_SURFACE_Y);
|
||||
boolean wantRiver = wantKind || wantRiverState || wantRiverDistance || wantRiverFlow
|
||||
|| wantRiverWaterSurface;
|
||||
|
||||
try {
|
||||
int minHeight = engine.getMinHeight();
|
||||
int fluid = engine.getDimension().getFluidHeight();
|
||||
long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
|
||||
if (engine.isClosed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HydrologyColumnSample hydrology = wantRiver
|
||||
? hydrologySample(engine, blockX, blockZ)
|
||||
: null;
|
||||
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
||||
int fluid = wantRiver
|
||||
? (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ))
|
||||
: engine.getDimension().getFluidHeight();
|
||||
boolean needsBiome = wantBiome
|
||||
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
||||
IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null;
|
||||
IrisSurfaceKind kind = wantKind
|
||||
? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType())
|
||||
? IrisSurfaceClassifier.classify(
|
||||
surface,
|
||||
fluid,
|
||||
biome == null ? null : biome.getInferredType(),
|
||||
hydrology
|
||||
)
|
||||
: IrisSurfaceKind.UNKNOWN;
|
||||
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
|
||||
sink.accept(blockX, blockZ, wantHeight ? surface + minHeight : -1, kind, biomeKey);
|
||||
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)
|
||||
: IrisRiverState.NONE;
|
||||
double riverDistance = wantRiverDistance && riverPresent
|
||||
? engine.getComplex().getRiverDistanceStream().get(blockX, blockZ)
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||
int riverFlow = wantRiverFlow && riverPresent
|
||||
? (int) Math.round(engine.getComplex().getRiverFlowStream().get(blockX, blockZ))
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||
int riverWaterSurfaceY = wantRiverWaterSurface
|
||||
&& surfaceLayer != null
|
||||
&& surfaceLayer.connectedFluid()
|
||||
? fluid + minHeight
|
||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||
sink.accept(new IrisColumnSample(
|
||||
blockX,
|
||||
blockZ,
|
||||
wantHeight ? surface + minHeight : IrisColumnSample.UNAVAILABLE_HEIGHT,
|
||||
natural,
|
||||
kind,
|
||||
biomeKey,
|
||||
riverState,
|
||||
riverDistance,
|
||||
riverFlow,
|
||||
riverWaterSurfaceY
|
||||
));
|
||||
return true;
|
||||
});
|
||||
return visited == query.columnCount();
|
||||
@@ -253,6 +308,23 @@ 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static Optional<String> key(IrisBiome biome) {
|
||||
String loadKey = biome == null ? null : biome.getLoadKey();
|
||||
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
|
||||
|
||||
+28
@@ -1,6 +1,8 @@
|
||||
package art.arcane.iris.core.service.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
|
||||
public final class IrisSurfaceClassifier {
|
||||
@@ -22,4 +24,30 @@ public final class IrisSurfaceClassifier {
|
||||
|
||||
return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND;
|
||||
}
|
||||
|
||||
public static IrisSurfaceKind classify(
|
||||
int engineSurfaceHeight,
|
||||
int engineFluidHeight,
|
||||
InferredType inferredType,
|
||||
HydrologyColumnSample hydrology
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class IrisColumnSampleTest {
|
||||
@Test
|
||||
public void unavailableFieldsHaveUnambiguousSentinels() {
|
||||
IrisColumnSample sample = sample(
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT,
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT,
|
||||
IrisSurfaceKind.UNKNOWN,
|
||||
null,
|
||||
IrisRiverState.NONE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_FLOW,
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT
|
||||
);
|
||||
|
||||
assertFalse(sample.hasSurfaceHeight());
|
||||
assertFalse(sample.hasNaturalHeight());
|
||||
assertFalse(sample.hasSurfaceKind());
|
||||
assertFalse(sample.hasBiomeKey());
|
||||
assertFalse(sample.hasRiverState());
|
||||
assertFalse(sample.hasRiverDistance());
|
||||
assertFalse(sample.hasRiverFlow());
|
||||
assertFalse(sample.hasRiverWaterSurfaceY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeWorldHeightsRemainAvailableValues() {
|
||||
IrisColumnSample sample = sample(
|
||||
-1,
|
||||
-64,
|
||||
IrisSurfaceKind.DRY_CHANNEL,
|
||||
"test:river",
|
||||
IrisRiverState.DRY,
|
||||
0D,
|
||||
0,
|
||||
-1
|
||||
);
|
||||
|
||||
assertTrue(sample.hasSurfaceHeight());
|
||||
assertTrue(sample.hasNaturalHeight());
|
||||
assertTrue(sample.hasSurfaceKind());
|
||||
assertTrue(sample.hasBiomeKey());
|
||||
assertTrue(sample.hasRiverState());
|
||||
assertTrue(sample.hasRiverDistance());
|
||||
assertTrue(sample.hasRiverFlow());
|
||||
assertTrue(sample.hasRiverWaterSurfaceY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankBiomeKeysNormalizeToUnavailable() {
|
||||
IrisColumnSample sample = sample(
|
||||
64,
|
||||
65,
|
||||
IrisSurfaceKind.LAND,
|
||||
" ",
|
||||
IrisRiverState.NONE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_FLOW,
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT
|
||||
);
|
||||
|
||||
assertNull(sample.biomeKey());
|
||||
assertFalse(sample.hasBiomeKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theSinkReceivesTheTypedSample() {
|
||||
IrisColumnSample sample = sample(
|
||||
64,
|
||||
65,
|
||||
IrisSurfaceKind.RIVER,
|
||||
"test:river",
|
||||
IrisRiverState.WET,
|
||||
0.5D,
|
||||
3,
|
||||
67
|
||||
);
|
||||
AtomicReference<IrisColumnSample> received = new AtomicReference<>();
|
||||
IrisColumnSink sink = received::set;
|
||||
|
||||
sink.accept(sample);
|
||||
|
||||
assertSame(sample, received.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidHydrologyValuesAreRejected() {
|
||||
assertInvalid(Double.POSITIVE_INFINITY, 1);
|
||||
assertInvalid(-0.1D, 1);
|
||||
assertInvalid(0D, -2);
|
||||
}
|
||||
|
||||
private static void assertInvalid(double distance, int flow) {
|
||||
try {
|
||||
sample(
|
||||
64,
|
||||
65,
|
||||
IrisSurfaceKind.RIVER,
|
||||
"test:river",
|
||||
IrisRiverState.WET,
|
||||
distance,
|
||||
flow,
|
||||
67
|
||||
);
|
||||
fail("Expected invalid hydrology values to be rejected");
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertTrue(expected.getMessage().startsWith("river"));
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisColumnSample sample(
|
||||
int surfaceHeight,
|
||||
int naturalHeight,
|
||||
IrisSurfaceKind surfaceKind,
|
||||
String biomeKey,
|
||||
IrisRiverState riverState,
|
||||
double riverDistance,
|
||||
int riverFlow,
|
||||
int riverWaterSurfaceY
|
||||
) {
|
||||
return new IrisColumnSample(
|
||||
12,
|
||||
-7,
|
||||
surfaceHeight,
|
||||
naturalHeight,
|
||||
surfaceKind,
|
||||
biomeKey,
|
||||
riverState,
|
||||
riverDistance,
|
||||
riverFlow,
|
||||
riverWaterSurfaceY
|
||||
);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class CommandFindBiomeContractTest {
|
||||
@Test
|
||||
public void biomeLookupUsesReachableBiomeHandler() throws Exception {
|
||||
Method method = CommandFind.class.getMethod("biome", IrisBiome.class, boolean.class);
|
||||
Param parameter = method.getParameters()[0].getAnnotation(Param.class);
|
||||
|
||||
assertEquals(ReachableBiomeHandler.class, parameter.customHandler());
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -1,8 +1,10 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -13,22 +15,31 @@ import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CommandObjectFluidHeightTest {
|
||||
@Test
|
||||
public void absoluteObjectPlacementShiftsFluidHeightFromNegativeMinY() {
|
||||
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() {
|
||||
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);
|
||||
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);
|
||||
|
||||
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
||||
|
||||
assertEquals(63, placer.getFluidHeight());
|
||||
assertEquals(67, placer.getFluidHeight(12, -7));
|
||||
verify(riverHead).get(12, -7);
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -1,10 +1,12 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.director.compat.DirectorAnnotationCompatibility;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorNodeDescriptor;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorParameterDescriptor;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -52,8 +54,23 @@ public class CommandPackOptionalPackContractTest {
|
||||
assertFalse(CommandPack.wantsAllPacks("overworld"));
|
||||
}
|
||||
|
||||
private static java.lang.reflect.Method findMethod(String name) {
|
||||
for (java.lang.reflect.Method method : CommandPack.class.getDeclaredMethods()) {
|
||||
@Test
|
||||
public void packageCommandIsOwnedByPackNamespace() throws Exception {
|
||||
Method method = CommandPack.class.getDeclaredMethod(
|
||||
"pkg",
|
||||
IrisDimension.class,
|
||||
boolean.class,
|
||||
boolean.class
|
||||
);
|
||||
DirectorNodeDescriptor descriptor = DirectorAnnotationCompatibility.fromMethod(method).orElseThrow();
|
||||
assertTrue(descriptor.getAliases().contains("package"));
|
||||
for (Method studioMethod : CommandStudio.class.getDeclaredMethods()) {
|
||||
assertFalse("Studio must not retain the package command", studioMethod.getName().equals("pkg"));
|
||||
}
|
||||
}
|
||||
|
||||
private static Method findMethod(String name) {
|
||||
for (Method method : CommandPack.class.getDeclaredMethods()) {
|
||||
if (method.getName().equals(name)) {
|
||||
return method;
|
||||
}
|
||||
|
||||
+51
-3
@@ -2,8 +2,13 @@ package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisColumnField;
|
||||
import art.arcane.iris.api.terrain.IrisColumnQuery;
|
||||
import art.arcane.iris.api.terrain.IrisRiverState;
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
@@ -12,6 +17,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -82,9 +88,7 @@ public class IrisTerrainSVCTest {
|
||||
IrisTerrainSVC service = new IrisTerrainSVC();
|
||||
AtomicInteger sinkCalls = new AtomicInteger();
|
||||
|
||||
boolean answered = service.sampleColumns(null, SMALL,
|
||||
(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey)
|
||||
-> sinkCalls.incrementAndGet());
|
||||
boolean answered = service.sampleColumns(null, SMALL, sample -> sinkCalls.incrementAndGet());
|
||||
|
||||
assertFalse(answered);
|
||||
assertEquals(0, sinkCalls.get());
|
||||
@@ -97,4 +101,48 @@ public class IrisTerrainSVCTest {
|
||||
assertFalse(service.sampleColumns(null, null, null));
|
||||
assertFalse(service.sampleColumns(null, SMALL, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedHydrologyMapsToThePublicDiagnosticStates() {
|
||||
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(null));
|
||||
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(true)));
|
||||
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(false)));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
+107
-2
@@ -1,9 +1,15 @@
|
||||
package art.arcane.iris.core.service.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.object.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;
|
||||
@@ -53,9 +59,108 @@ public class IrisSurfaceClassifierTest {
|
||||
|
||||
IrisSurfaceKind expected = IrisSurfaceClassifier.classify(surface, FLUID, null);
|
||||
for (InferredType inferredType : InferredType.values()) {
|
||||
assertEquals("surface=" + surface + " type=" + inferredType,
|
||||
expected, IrisSurfaceClassifier.classify(surface, FLUID, inferredType));
|
||||
assertEquals(
|
||||
"surface=" + surface + " type=" + inferredType,
|
||||
expected,
|
||||
IrisSurfaceClassifier.classify(surface, FLUID, inferredType)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void voidClassificationWinsOverAcceptedRiverGeometry() {
|
||||
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
|
||||
0,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
||||
));
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user