mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-18 22:44:09 +00:00
Terrain-first hydrology
- Replace the river package with the hydrology planner, footprint compiler, cave containment and runtime - Add the hydrology pack schema, validator, probes and tests - Wire hydrology into IrisComplex, actuators, mantle, Vision and commands
This commit is contained in:
+73
-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,73 @@ 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
|
||||
).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)
|
||||
|
||||
+35
-25
@@ -15,12 +15,12 @@ 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;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
@@ -105,14 +105,15 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
|
||||
try {
|
||||
int surface = engine.getHeight(blockX, blockZ);
|
||||
IrisRiverSurfaceSample riverSurface = engine.getComplex().getRiverSurfaceStream().get(blockX, blockZ);
|
||||
int fluid = (int) Math.round(riverSurface.waterSurfaceY());
|
||||
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, riverSurface);
|
||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, hydrology);
|
||||
} catch (Throwable error) {
|
||||
reportQueryFault("surfaceKind", world, error);
|
||||
return IrisSurfaceKind.UNKNOWN;
|
||||
@@ -246,13 +247,13 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
return false;
|
||||
}
|
||||
|
||||
IrisRiverSurfaceSample riverSurface = wantRiver
|
||||
? engine.getComplex().getRiverSurfaceStream().get(blockX, blockZ)
|
||||
HydrologyColumnSample hydrology = wantRiver
|
||||
? hydrologySample(engine, blockX, blockZ)
|
||||
: null;
|
||||
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
||||
int fluid = riverSurface == null
|
||||
? engine.getDimension().getFluidHeight()
|
||||
: (int) Math.round(riverSurface.waterSurfaceY());
|
||||
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;
|
||||
@@ -261,26 +262,29 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
surface,
|
||||
fluid,
|
||||
biome == null ? null : biome.getInferredType(),
|
||||
riverSurface
|
||||
hydrology
|
||||
)
|
||||
: 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;
|
||||
boolean riverPresent = riverSurface != null && riverSurface.river().present();
|
||||
IrisRiverState riverState = wantRiverState && riverSurface != null
|
||||
? riverState(riverSurface)
|
||||
HydrologyColumnLayer surfaceLayer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
boolean riverPresent = surfaceLayer != null;
|
||||
IrisRiverState riverState = wantRiverState
|
||||
? riverState(hydrology)
|
||||
: IrisRiverState.NONE;
|
||||
double riverDistance = wantRiverDistance && riverPresent
|
||||
? riverSurface.river().distance()
|
||||
? engine.getComplex().getRiverDistanceStream().get(blockX, blockZ)
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||
int riverFlow = wantRiverFlow && riverPresent
|
||||
? riverSurface.river().flow()
|
||||
? (int) Math.round(engine.getComplex().getRiverFlowStream().get(blockX, blockZ))
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||
int riverWaterSurfaceY = wantRiverWaterSurface
|
||||
&& riverPresent
|
||||
&& riverSurface.river().state() == RiverRouteState.WET
|
||||
&& surfaceLayer != null
|
||||
&& surfaceLayer.connectedFluid()
|
||||
? fluid + minHeight
|
||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||
sink.accept(new IrisColumnSample(
|
||||
@@ -304,15 +308,21 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
}
|
||||
}
|
||||
|
||||
static IrisRiverState riverState(IrisRiverSurfaceSample surface) {
|
||||
if (!surface.river().present()) {
|
||||
static IrisRiverState riverState(HydrologyColumnSample hydrology) {
|
||||
HydrologyColumnLayer layer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
if (layer == null || !layer.channel()) {
|
||||
return IrisRiverState.NONE;
|
||||
}
|
||||
return switch (surface.river().state()) {
|
||||
case WET -> IrisRiverState.WET;
|
||||
case DRY -> IrisRiverState.DRY;
|
||||
case SUPPRESSED -> 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) {
|
||||
|
||||
+14
-17
@@ -1,10 +1,9 @@
|
||||
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;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
|
||||
public final class IrisSurfaceClassifier {
|
||||
private IrisSurfaceClassifier() {
|
||||
@@ -30,25 +29,23 @@ public final class IrisSurfaceClassifier {
|
||||
int engineSurfaceHeight,
|
||||
int engineFluidHeight,
|
||||
InferredType inferredType,
|
||||
IrisRiverSurfaceSample riverSurface
|
||||
HydrologyColumnSample hydrology
|
||||
) {
|
||||
if (engineSurfaceHeight <= 0) {
|
||||
return IrisSurfaceKind.VOID;
|
||||
}
|
||||
if (riverSurface != null && riverSurface.river().present() && !riverSurface.subterranean()) {
|
||||
if (riverSurface.river().state() == RiverRouteState.DRY) {
|
||||
return riverSurface.river().section() == RiverSection.DRY_CHANNEL
|
||||
? IrisSurfaceKind.DRY_CHANNEL
|
||||
: IrisSurfaceKind.LAND;
|
||||
HydrologyColumnLayer layer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
if (layer != null) {
|
||||
if (layer.shore()) {
|
||||
return IrisSurfaceKind.RIVER_SHORE;
|
||||
}
|
||||
if (riverSurface.river().state() == RiverRouteState.WET) {
|
||||
RiverSection section = riverSurface.river().section();
|
||||
if (section == RiverSection.BANK) {
|
||||
return IrisSurfaceKind.RIVER_SHORE;
|
||||
}
|
||||
if (section == RiverSection.CHANNEL || section == RiverSection.MOUTH) {
|
||||
return IrisSurfaceKind.RIVER;
|
||||
}
|
||||
if (layer.channel()) {
|
||||
return layer.connectedFluid() ? IrisSurfaceKind.RIVER : IrisSurfaceKind.DRY_CHANNEL;
|
||||
}
|
||||
if (layer.grading()) {
|
||||
return IrisSurfaceKind.LAND;
|
||||
}
|
||||
}
|
||||
return classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
||||
|
||||
+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());
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
|
||||
+42
-33
@@ -5,12 +5,10 @@ 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.river.RiverEdgeId;
|
||||
import art.arcane.iris.engine.river.RiverNodeId;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
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;
|
||||
@@ -19,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;
|
||||
@@ -104,36 +103,46 @@ public class IrisTerrainSVCTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverRouteStatesMapToThePublicDiagnosticStates() {
|
||||
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(
|
||||
new IrisRiverSurfaceSample(RiverSample.none(), 70D, 70D, 70D, false, false)
|
||||
));
|
||||
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(RiverRouteState.WET)));
|
||||
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(RiverRouteState.DRY)));
|
||||
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(river(RiverRouteState.SUPPRESSED)));
|
||||
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 IrisRiverSurfaceSample river(RiverRouteState state) {
|
||||
RiverSection section = switch (state) {
|
||||
case WET -> RiverSection.CHANNEL;
|
||||
case DRY -> RiverSection.DRY_CHANNEL;
|
||||
case SUPPRESSED -> RiverSection.NONE;
|
||||
};
|
||||
RiverSample sample = new RiverSample(
|
||||
true,
|
||||
state,
|
||||
section,
|
||||
0D,
|
||||
0.5D,
|
||||
1D,
|
||||
private static HydrologyColumnSample river(boolean connectedFluid) {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
1L,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
2L,
|
||||
3L,
|
||||
0,
|
||||
63,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
8D,
|
||||
4D,
|
||||
3D,
|
||||
false,
|
||||
RiverEdgeId.of(new RiverNodeId(0, 0), new RiverNodeId(1, 0))
|
||||
0,
|
||||
false
|
||||
);
|
||||
return new IrisRiverSurfaceSample(sample, 70D, 60D, 63D, false, state == RiverRouteState.WET);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
+64
-47
@@ -1,15 +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 art.arcane.iris.engine.river.RiverEdgeId;
|
||||
import art.arcane.iris.engine.river.RiverNodeId;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
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;
|
||||
@@ -59,91 +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 activeRiverGeometryOverridesGenericOceanAndLandKinds() {
|
||||
public void acceptedSurfaceRolesOverrideGenericOceanAndLandKinds() {
|
||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
63,
|
||||
InferredType.SEA,
|
||||
river(RiverRouteState.WET, RiverSection.CHANNEL)
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
63,
|
||||
InferredType.SEA,
|
||||
river(RiverRouteState.WET, RiverSection.MOUTH)
|
||||
hydrology(HydrologyFeatureType.MOUTH, true, false, true)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify(
|
||||
64,
|
||||
63,
|
||||
InferredType.SHORE,
|
||||
river(RiverRouteState.WET, RiverSection.BANK)
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, true, false)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
60,
|
||||
InferredType.LAND,
|
||||
river(RiverRouteState.DRY, RiverSection.DRY_CHANNEL)
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, false)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
60,
|
||||
InferredType.LAND,
|
||||
river(RiverRouteState.DRY, RiverSection.DRY_BANK)
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, false, false)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void voidClassificationWinsOverRiverGeometry() {
|
||||
for (RiverSection section : RiverSection.values()) {
|
||||
if (section == RiverSection.NONE) {
|
||||
continue;
|
||||
}
|
||||
RiverRouteState state = section == RiverSection.DRY_CHANNEL || section == RiverSection.DRY_BANK
|
||||
? RiverRouteState.DRY
|
||||
: RiverRouteState.WET;
|
||||
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
|
||||
0,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
river(state, section)
|
||||
));
|
||||
}
|
||||
public void voidClassificationWinsOverAcceptedRiverGeometry() {
|
||||
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
|
||||
0,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void suppressedRoutesDoNotCreatePublicRiverSurfaceKinds() {
|
||||
public void absentRejectedCandidatesDoNotCreatePublicRiverKinds() {
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
||||
64,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
river(RiverRouteState.SUPPRESSED, RiverSection.CHANNEL)
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
private static IrisRiverSurfaceSample river(RiverRouteState state, RiverSection section) {
|
||||
RiverSample sample = new RiverSample(
|
||||
true,
|
||||
state,
|
||||
section,
|
||||
0D,
|
||||
0.5D,
|
||||
1D,
|
||||
private static HydrologyColumnSample hydrology(
|
||||
HydrologyFeatureType type,
|
||||
boolean channel,
|
||||
boolean shore,
|
||||
boolean connectedFluid
|
||||
) {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
1L,
|
||||
type,
|
||||
2L,
|
||||
3L,
|
||||
0,
|
||||
63,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
8D,
|
||||
4D,
|
||||
3D,
|
||||
false,
|
||||
RiverEdgeId.of(new RiverNodeId(0, 0), new RiverNodeId(1, 0))
|
||||
0,
|
||||
false
|
||||
);
|
||||
double waterSurface = state == RiverRouteState.WET ? 63D : 60D;
|
||||
return new IrisRiverSurfaceSample(sample, 70D, 60D, waterSurface, false, state == RiverRouteState.WET);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,10 @@ dependencies {
|
||||
minecraft("com.mojang:minecraft:${minecraftVersion}")
|
||||
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
// registrySync and resourceLoader are LOAD-BEARING despite zero imports anywhere in Iris:
|
||||
// fabric-registry-sync-v0 delays registry freeze past mod init, which is what lets
|
||||
// IrisFabricBootstrap register the CHUNK_GENERATOR codec in
|
||||
|
||||
@@ -131,6 +131,10 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
||||
dependencies {
|
||||
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||
compileOnly(libs.spigot) {
|
||||
transitive = false
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -41,7 +42,7 @@ public final class MainWorldService {
|
||||
"region",
|
||||
"entities",
|
||||
"poi",
|
||||
"mantle",
|
||||
IrisEngineMantle.STORAGE_FOLDER_NAME,
|
||||
"dimensions/minecraft/overworld",
|
||||
"dimensions/minecraft/the_nether",
|
||||
"dimensions/minecraft/the_end",
|
||||
|
||||
+7
-1
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.Level;
|
||||
@@ -33,7 +34,12 @@ import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class ModdedDimensionStorage {
|
||||
private static final List<String> CHUNK_DATA_FOLDERS = List.of("region", "entities", "poi", "mantle");
|
||||
private static final List<String> CHUNK_DATA_FOLDERS = List.of(
|
||||
"region",
|
||||
"entities",
|
||||
"poi",
|
||||
IrisEngineMantle.STORAGE_FOLDER_NAME
|
||||
);
|
||||
|
||||
private ModdedDimensionStorage() {
|
||||
}
|
||||
|
||||
+1
-1
@@ -325,7 +325,7 @@ public final class IrisModdedCommands {
|
||||
return 0;
|
||||
}
|
||||
String target = downloadDisplayTarget(request);
|
||||
String downloadSource = request.pack() == null ? "direct ZIP URL" : "built-in beta release";
|
||||
String downloadSource = request.pack() == null ? "direct ZIP URL" : "built-in stable release";
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
fail(source, IrisLanguage.plain(
|
||||
|
||||
+44
-1
@@ -25,6 +25,8 @@ 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.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
@@ -62,6 +64,7 @@ final class ModdedCommandSuggestions {
|
||||
static final SuggestionProvider<CommandSourceStack> OBJECT_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> POI_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder);
|
||||
static final SuggestionProvider<CommandSourceStack> HYDROLOGY_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestHydrologyTypes(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
||||
|
||||
@@ -79,7 +82,7 @@ final class ModdedCommandSuggestions {
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder);
|
||||
return SharedSuggestionProvider.suggest(reachableBiomeKeys(engine), builder);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
warnTabFailure("biome keys", context.getSource(), e);
|
||||
@@ -87,6 +90,46 @@ final class ModdedCommandSuggestions {
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestHydrologyTypes(
|
||||
CommandContext<CommandSourceStack> context,
|
||||
SuggestionsBuilder builder
|
||||
) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
if (engine != null && engine.getComplex().getHydrologyRuntime() != null) {
|
||||
return SharedSuggestionProvider.suggest(
|
||||
engine.getComplex().getHydrologyRuntime().featureQueryKeys(), builder);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
warnTabFailure("hydrology types", context.getSource(), error);
|
||||
}
|
||||
return SharedSuggestionProvider.suggest(
|
||||
HydrologyFeatureQuery.suggestions(List.of()), builder);
|
||||
}
|
||||
|
||||
static Set<String> reachableBiomeKeys(Engine engine) {
|
||||
return reachableBiomeKeys(engine.getAllBiomes());
|
||||
}
|
||||
|
||||
static Set<String> reachableBiomeKeys(Iterable<IrisBiome> biomes) {
|
||||
Set<String> keys = new TreeSet<>();
|
||||
for (IrisBiome biome : biomes) {
|
||||
if (biome != null && biome.getLoadKey() != null && !biome.getLoadKey().isBlank()) {
|
||||
keys.add(biome.getLoadKey());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static boolean isReachableBiome(Engine engine, String biomeKey) {
|
||||
return isReachableBiome(engine.getAllBiomes(), biomeKey);
|
||||
}
|
||||
|
||||
static boolean isReachableBiome(Iterable<IrisBiome> biomes, String biomeKey) {
|
||||
return biomeKey != null && reachableBiomeKeys(biomes).contains(biomeKey.trim());
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestRegionKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
|
||||
+3
@@ -227,6 +227,9 @@ final class ModdedCommandTree {
|
||||
.then(Commands.literal("object")
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("river")
|
||||
.then(Commands.argument("type", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.HYDROLOGY_TYPES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoRiver(context.getSource(), StringArgumentType.getString(context, "type")))))
|
||||
.then(Commands.literal("unregistered")
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
ModdedUnregisteredStructures.print(context.getSource())))
|
||||
|
||||
+67
@@ -29,12 +29,16 @@ import art.arcane.iris.engine.framework.Locator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||
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;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
@@ -87,6 +91,10 @@ final class ModdedLocateCommands {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
|
||||
return 0;
|
||||
}
|
||||
if (!ModdedCommandSuggestions.isReachableBiome(engine, biome.getLoadKey())) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", biome.getLoadKey())));
|
||||
return 0;
|
||||
}
|
||||
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
|
||||
return 1;
|
||||
}
|
||||
@@ -137,6 +145,65 @@ final class ModdedLocateCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int gotoRiver(CommandSourceStack source, String type) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, "This command can only be used by a player.");
|
||||
return 0;
|
||||
}
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
||||
if (runtime == null) {
|
||||
IrisModdedCommands.fail(source, "Hydrology is not active in this dimension.");
|
||||
return 0;
|
||||
}
|
||||
HydrologyFeatureQuery query;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
} catch (IllegalArgumentException error) {
|
||||
IrisModdedCommands.fail(source, error.getMessage());
|
||||
return 0;
|
||||
}
|
||||
int originX = player.blockPosition().getX();
|
||||
int originZ = player.blockPosition().getZ();
|
||||
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
|
||||
MinecraftServer server = source.getServer();
|
||||
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
|
||||
MultiBurst.burst.completeValueAsync(() -> runtime.nearestFeature(
|
||||
query.types(), query.profileKey(), originX, originZ, maximumDistance).orElse(null))
|
||||
.whenComplete((HydrologyFeatureRef 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());
|
||||
return;
|
||||
}
|
||||
if (feature == null) {
|
||||
IrisModdedCommands.fail(source,
|
||||
"No accepted " + type + " hydrology feature found within "
|
||||
+ maximumDistance + " blocks.");
|
||||
return;
|
||||
}
|
||||
int worldY = feature.y() + engine.getDimension().getMinHeight() + 2;
|
||||
teleportToStructure(
|
||||
source,
|
||||
level,
|
||||
player,
|
||||
feature.x(),
|
||||
worldY,
|
||||
feature.z(),
|
||||
type + " hydrology feature"
|
||||
);
|
||||
}));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int gotoStructure(CommandSourceStack source, String keyRaw) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
|
||||
+4
-6
@@ -341,8 +341,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
markCompleted();
|
||||
listener.onChunkGenerated(x, z);
|
||||
cleanupMantleChunk(x, z);
|
||||
listener.onChunkCleaned(x, z);
|
||||
cleanupMantleChunksCoveredBy(x, z, listener);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
@@ -406,8 +405,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
onSuccess();
|
||||
markCompleted();
|
||||
listener.onChunkGenerated(x, z);
|
||||
cleanupMantleChunk(x, z);
|
||||
listener.onChunkCleaned(x, z);
|
||||
cleanupMantleChunksCoveredBy(x, z, listener);
|
||||
} finally {
|
||||
markFinished();
|
||||
semaphore.release();
|
||||
@@ -494,9 +492,9 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupMantleChunk(int x, int z) {
|
||||
private void cleanupMantleChunksCoveredBy(int x, int z, PregenListener listener) {
|
||||
try {
|
||||
engine.getMantle().forceCleanupChunk(x, z);
|
||||
engine.getMantle().cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned);
|
||||
} catch (Throwable e) {
|
||||
ModdedIrisLog.debug("Iris pregen mantle cleanup skipped for {},{}: {}", x, z, e.toString());
|
||||
}
|
||||
|
||||
+1
-1
@@ -301,7 +301,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
}
|
||||
|
||||
private void runUpdatePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
||||
void runUpdatePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
||||
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
int baseX = chunkX << 4;
|
||||
|
||||
+11
@@ -187,6 +187,17 @@ public class ModdedGenerationLeaseContractTest {
|
||||
assertTrue(logFailure.contains("cause.toString()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenReportsOnlyCleanupCallbacksFromCoveredTargets() throws IOException {
|
||||
String source = source("art/arcane/iris/modded/command/ModdedPregenMethod.java");
|
||||
String cleanup = method(source, "private void cleanupMantleChunksCoveredBy(");
|
||||
|
||||
assertTrue(source.contains("cleanupMantleChunksCoveredBy(x, z, listener);"));
|
||||
assertFalse(source.contains("cleanupMantleChunk(x, z);"));
|
||||
assertTrue(cleanup.contains("cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned)"));
|
||||
assertFalse(cleanup.contains("forceCleanupChunk"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidPregenBoundsFailBeforeRuntimeMutation() throws IOException {
|
||||
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
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;
|
||||
|
||||
public class IrisModdedBiomeCommandTest {
|
||||
@Test
|
||||
public void biomeKeysAreSortedAndLimitedToReachableBiomes() {
|
||||
KList<IrisBiome> reachable = new KList<>();
|
||||
IrisBiome river = new IrisBiome();
|
||||
river.setLoadKey("river");
|
||||
IrisBiome natural = new IrisBiome();
|
||||
natural.setLoadKey("natural");
|
||||
reachable.add(river);
|
||||
reachable.add(natural);
|
||||
assertEquals(
|
||||
List.of("natural", "river"),
|
||||
List.copyOf(ModdedCommandSuggestions.reachableBiomeKeys(reachable))
|
||||
);
|
||||
assertTrue(ModdedCommandSuggestions.isReachableBiome(reachable, "river"));
|
||||
assertFalse(ModdedCommandSuggestions.isReachableBiome(reachable, "unused"));
|
||||
}
|
||||
}
|
||||
+79
@@ -1,11 +1,27 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
import art.arcane.volmlib.util.matter.slices.UpdateMatter;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LiquidBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.material.FlowingFluid;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
@@ -15,8 +31,42 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedChunkUpdateServiceTest {
|
||||
@Test
|
||||
public void fallingFluidUpdateRetainsLevelEightStateAndRequestsPhysics() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.minHeight()).thenReturn(-64);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(world);
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
when(level.getMinY()).thenReturn(-64);
|
||||
when(level.getMaxY()).thenReturn(320);
|
||||
BlockPos position = new BlockPos(15, -59, 14);
|
||||
BlockState falling = Blocks.WATER.defaultBlockState().setValue(LiquidBlock.LEVEL, 8);
|
||||
when(level.getBlockState(position)).thenReturn(falling);
|
||||
UpdateRecordingMantleChunk chunk = new UpdateRecordingMantleChunk();
|
||||
|
||||
new ModdedChunkUpdateService().runUpdatePass(engine, level, 0, 0, chunk);
|
||||
|
||||
assertEquals(8, falling.getValue(LiquidBlock.LEVEL).intValue());
|
||||
if (falling.getFluidState().hasProperty(FlowingFluid.FALLING)) {
|
||||
assertTrue(falling.getFluidState().getValue(FlowingFluid.FALLING));
|
||||
}
|
||||
InOrder order = inOrder(level);
|
||||
order.verify(level).setBlock(eq(position), eq(Blocks.AIR.defaultBlockState()), anyInt());
|
||||
order.verify(level).setBlock(position, falling, Block.UPDATE_ALL);
|
||||
assertTrue(chunk.deleted());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scansWhenPlayersArePresent() {
|
||||
assertTrue(ModdedChunkUpdateService.hasUpdateTargets(true, false));
|
||||
@@ -84,6 +134,35 @@ public class ModdedChunkUpdateServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class UpdateRecordingMantleChunk extends MantleChunk<Matter> {
|
||||
private boolean deleted;
|
||||
|
||||
private UpdateRecordingMantleChunk() {
|
||||
super(1, 0, 0, emptyAdapter(), MantleHooks.NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void iterate(
|
||||
Class<T> type,
|
||||
Consumer4<Integer, Integer, Integer, T> iterator
|
||||
) {
|
||||
if (type == MatterUpdate.class) {
|
||||
iterator.accept(-1, 5, -2, type.cast(UpdateMatter.ON));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSlices(Class<?> type) {
|
||||
if (type == MatterUpdate.class) {
|
||||
deleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deleted() {
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static MantleDataAdapter<Matter> emptyAdapter() {
|
||||
return (MantleDataAdapter<Matter>) Proxy.newProxyInstance(
|
||||
|
||||
@@ -105,6 +105,10 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
||||
|
||||
dependencies {
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher:6.1.2')
|
||||
testRuntimeOnly('org.junit.vintage:junit-vintage-engine:6.1.2')
|
||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||
|
||||
@@ -23,10 +23,11 @@ import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.render.IrisRenderer;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateKind;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
@@ -125,6 +126,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
private VisionRenderController.Frame renderFrame;
|
||||
private JComboBox<RenderType> modeSelector;
|
||||
private JToggleButton gridToggle;
|
||||
private JToggleButton entitiesToggle;
|
||||
private JToggleButton followToggle;
|
||||
private RenderType currentType;
|
||||
private List<GuiMarker> players;
|
||||
@@ -142,6 +144,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
private double blocksPerPixel;
|
||||
private int paintCadenceFps;
|
||||
private boolean grid;
|
||||
private boolean entitiesVisible;
|
||||
private boolean follow;
|
||||
private boolean help;
|
||||
private boolean debug;
|
||||
@@ -160,6 +163,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
this.notifications = new LinkedHashMap<>();
|
||||
this.players = List.of();
|
||||
this.entities = List.of();
|
||||
this.entitiesVisible = false;
|
||||
this.currentType = RenderType.BIOME;
|
||||
this.blocksPerPixel = DEFAULT_BLOCKS_PER_PIXEL;
|
||||
this.contentRevision = 1L;
|
||||
@@ -265,6 +269,12 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
vision.gridToggle = createToggle(IrisLanguage.plain(DesktopUiMessages.VISION_GRID), vision.grid);
|
||||
vision.gridToggle.addActionListener(event -> vision.toggleGrid());
|
||||
trailing.add(vision.gridToggle);
|
||||
vision.entitiesToggle = createToggle(
|
||||
IrisLanguage.plain(DesktopUiMessages.VISION_ENTITIES),
|
||||
vision.entitiesVisible
|
||||
);
|
||||
vision.entitiesToggle.addActionListener(event -> vision.toggleEntities());
|
||||
trailing.add(vision.entitiesToggle);
|
||||
vision.followToggle = createToggle(IrisLanguage.plain(DesktopUiMessages.VISION_FOLLOW), vision.follow);
|
||||
vision.followToggle.addActionListener(event -> vision.toggleFollow());
|
||||
trailing.add(vision.followToggle);
|
||||
@@ -411,11 +421,13 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
}
|
||||
|
||||
private void renderMarkers(Graphics2D canvas) {
|
||||
for (GuiMarker marker : entities) {
|
||||
int screenX = (int) Math.round(worldToScreenX(marker.worldX()));
|
||||
int screenY = (int) Math.round(worldToScreenZ(marker.worldZ()));
|
||||
canvas.setColor(MOB_COLOR);
|
||||
canvas.fillRect(screenX - 2, screenY - 2, 5, 5);
|
||||
if (entitiesVisible) {
|
||||
for (GuiMarker marker : entities) {
|
||||
int screenX = (int) Math.round(worldToScreenX(marker.worldX()));
|
||||
int screenY = (int) Math.round(worldToScreenZ(marker.worldZ()));
|
||||
canvas.setColor(MOB_COLOR);
|
||||
canvas.fillRect(screenX - 2, screenY - 2, 5, 5);
|
||||
}
|
||||
}
|
||||
|
||||
for (GuiMarker marker : players) {
|
||||
@@ -430,7 +442,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
int labelWidth = canvas.getFontMetrics().stringWidth(marker.label());
|
||||
canvas.drawString(marker.label(), screenX - labelWidth / 2, screenY - 14);
|
||||
}
|
||||
if (detailedHover) {
|
||||
if (entitiesVisible && detailedHover) {
|
||||
renderNearestEntity(canvas);
|
||||
}
|
||||
}
|
||||
@@ -483,21 +495,40 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
}
|
||||
|
||||
private void renderRiverLegend(Graphics2D canvas) {
|
||||
RiverSection[] sections = RiverSection.values();
|
||||
HydrologyFeatureType[] types = HydrologyFeatureType.values();
|
||||
HydrologyCandidateKind[] candidateKinds = HydrologyCandidateKind.values();
|
||||
int lineHeight = 18;
|
||||
int width = 148;
|
||||
int height = sections.length * lineHeight + CARD_PADDING * 2;
|
||||
int width = 188;
|
||||
int height = (types.length + candidateKinds.length + 1) * lineHeight + CARD_PADDING * 2;
|
||||
int x = getWidth() - width - CARD_PADDING;
|
||||
int y = getHeight() - STATUS_HEIGHT - PROGRESS_HEIGHT - height - CARD_PADDING;
|
||||
drawCardBackground(canvas, x, y, width, height);
|
||||
canvas.setFont(BODY_FONT);
|
||||
for (int index = 0; index < sections.length; index++) {
|
||||
RiverSection section = sections[index];
|
||||
int rowY = y + CARD_PADDING + index * lineHeight;
|
||||
canvas.setColor(new Color(IrisRenderer.riverColor(section)));
|
||||
int headwaterY = y + CARD_PADDING;
|
||||
canvas.setColor(new Color(IrisRenderer.headwaterColor()));
|
||||
canvas.fillRoundRect(x + CARD_PADDING, headwaterY + 2, 12, 12, 4, 4);
|
||||
canvas.setColor(new Color(IrisRenderer.headwaterDirectionColor()));
|
||||
canvas.drawLine(x + CARD_PADDING + 3, headwaterY + 8, x + CARD_PADDING + 9, headwaterY + 8);
|
||||
canvas.drawLine(x + CARD_PADDING + 9, headwaterY + 8, x + CARD_PADDING + 6, headwaterY + 5);
|
||||
canvas.drawLine(x + CARD_PADDING + 9, headwaterY + 8, x + CARD_PADDING + 6, headwaterY + 11);
|
||||
canvas.setColor(TEXT_SECONDARY);
|
||||
canvas.drawString("headwater / source flow", x + CARD_PADDING + 20, headwaterY + 13);
|
||||
for (int index = 0; index < types.length; index++) {
|
||||
HydrologyFeatureType type = types[index];
|
||||
int rowY = y + CARD_PADDING + (index + 1) * lineHeight;
|
||||
canvas.setColor(new Color(IrisRenderer.hydrologyFeatureColor(type)));
|
||||
canvas.fillRoundRect(x + CARD_PADDING, rowY + 2, 12, 12, 4, 4);
|
||||
canvas.setColor(TEXT_SECONDARY);
|
||||
String label = section.name().toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||
String label = type.name().toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||
canvas.drawString(label, x + CARD_PADDING + 20, rowY + 13);
|
||||
}
|
||||
for (int index = 0; index < candidateKinds.length; index++) {
|
||||
HydrologyCandidateKind kind = candidateKinds[index];
|
||||
int rowY = y + CARD_PADDING + (types.length + index + 1) * lineHeight;
|
||||
canvas.setColor(new Color(IrisRenderer.hydrologyDiagnosticColor(kind)));
|
||||
canvas.fillRoundRect(x + CARD_PADDING, rowY + 2, 12, 12, 4, 4);
|
||||
canvas.setColor(TEXT_SECONDARY);
|
||||
String label = "projected " + kind.name().toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||
canvas.drawString(label, x + CARD_PADDING + 20, rowY + 13);
|
||||
}
|
||||
}
|
||||
@@ -783,12 +814,16 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
try {
|
||||
List<GuiMarker> nextPlayers = overlay.players();
|
||||
players = nextPlayers == null ? List.of() : List.copyOf(nextPlayers);
|
||||
overlay.requestEntities(next -> EventQueue.invokeLater(() -> {
|
||||
if (!closed) {
|
||||
entities = next == null ? List.of() : List.copyOf(next);
|
||||
repaint();
|
||||
}
|
||||
}));
|
||||
if (entitiesVisible) {
|
||||
overlay.requestEntities(next -> EventQueue.invokeLater(() -> {
|
||||
if (!closed && entitiesVisible) {
|
||||
entities = next == null ? List.of() : List.copyOf(next);
|
||||
repaint();
|
||||
}
|
||||
}));
|
||||
} else if (!entities.isEmpty()) {
|
||||
entities = List.of();
|
||||
}
|
||||
if (follow && !players.isEmpty()) {
|
||||
GuiMarker player = players.get(0);
|
||||
if (Math.abs(centerX - player.worldX()) > 0.5D || Math.abs(centerZ - player.worldZ()) > 0.5D) {
|
||||
@@ -852,6 +887,22 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
notifyUser(IrisLanguage.plain(grid ? DesktopUiMessages.VISION_GRID_ENABLED : DesktopUiMessages.VISION_GRID_DISABLED));
|
||||
}
|
||||
|
||||
private void toggleEntities() {
|
||||
entitiesVisible = !entitiesVisible;
|
||||
if (entitiesVisible) {
|
||||
refreshMarkers();
|
||||
} else {
|
||||
entities = List.of();
|
||||
repaint();
|
||||
}
|
||||
syncControls();
|
||||
notifyUser(IrisLanguage.plain(
|
||||
entitiesVisible
|
||||
? DesktopUiMessages.VISION_ENTITIES_ENABLED
|
||||
: DesktopUiMessages.VISION_ENTITIES_DISABLED
|
||||
));
|
||||
}
|
||||
|
||||
private void toggleFollow() {
|
||||
follow = !follow;
|
||||
if (follow && players.isEmpty()) {
|
||||
@@ -881,6 +932,9 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
||||
if (gridToggle != null) {
|
||||
gridToggle.setSelected(grid);
|
||||
}
|
||||
if (entitiesToggle != null) {
|
||||
entitiesToggle.setSelected(entitiesVisible);
|
||||
}
|
||||
if (followToggle != null) {
|
||||
followToggle.setSelected(follow);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.WorldSlotKey;
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
@@ -253,7 +254,9 @@ public final class BukkitWorldConfiguration {
|
||||
if (Files.isDirectory(worldRoot.resolve("iris"), LinkOption.NOFOLLOW_LINKS)) {
|
||||
return true;
|
||||
}
|
||||
for (String directory : new String[]{"region", "entities", "poi", "mantle"}) {
|
||||
for (String directory : new String[]{
|
||||
"region", "entities", "poi", IrisEngineMantle.STORAGE_FOLDER_NAME
|
||||
}) {
|
||||
if (hasEntries(worldRoot.resolve(directory))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.WorldSlotKey;
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
@@ -37,7 +38,9 @@ import java.util.stream.Stream;
|
||||
public final class HuskWorldQuarantine {
|
||||
private static final String IRIS_NAMESPACE = "iris";
|
||||
private static final Pattern MANAGED_KEY = Pattern.compile("[a-z0-9_-]+");
|
||||
private static final String[] CHUNK_DATA_DIRECTORIES = {"region", "entities", "poi", "mantle"};
|
||||
private static final String[] CHUNK_DATA_DIRECTORIES = {
|
||||
"region", "entities", "poi", IrisEngineMantle.STORAGE_FOLDER_NAME
|
||||
};
|
||||
private static final Set<String> OS_METADATA_FILES = Set.of(".DS_Store", "Thumbs.db", "desktop.ini");
|
||||
private static final String APPLE_DOUBLE_PREFIX = "._";
|
||||
private static final DateTimeFormatter STAMP = DateTimeFormatter
|
||||
|
||||
@@ -9,12 +9,15 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_TITLE = TextKey.of("iris.desktop.vision.title", "Iris Vision");
|
||||
public static final TextKey VISION_VIEW = TextKey.of("iris.desktop.vision.view", "View:");
|
||||
public static final TextKey VISION_GRID = TextKey.of("iris.desktop.vision.grid", "Grid");
|
||||
public static final TextKey VISION_ENTITIES = TextKey.of("iris.desktop.vision.entities", "Entities");
|
||||
public static final TextKey VISION_FOLLOW = TextKey.of("iris.desktop.vision.follow", "Follow");
|
||||
public static final TextKey VISION_REFRESHING = TextKey.of("iris.desktop.vision.refreshing", "Refreshing");
|
||||
public static final TextKey VISION_FPS = TextKey.of("iris.desktop.vision.fps", "{fps} FPS");
|
||||
public static final TextKey VISION_ZOOM_RESET = TextKey.of("iris.desktop.vision.zoom_reset", "Zoom reset");
|
||||
public static final TextKey VISION_GRID_ENABLED = TextKey.of("iris.desktop.vision.grid_enabled", "Grid enabled");
|
||||
public static final TextKey VISION_GRID_DISABLED = TextKey.of("iris.desktop.vision.grid_disabled", "Grid disabled");
|
||||
public static final TextKey VISION_ENTITIES_ENABLED = TextKey.of("iris.desktop.vision.entities_enabled", "Entities enabled");
|
||||
public static final TextKey VISION_ENTITIES_DISABLED = TextKey.of("iris.desktop.vision.entities_disabled", "Entities disabled");
|
||||
public static final TextKey VISION_FOLLOWING = TextKey.of("iris.desktop.vision.following", "Following {player}");
|
||||
public static final TextKey VISION_NO_PLAYER = TextKey.of("iris.desktop.vision.no_player", "No player in world");
|
||||
public static final TextKey VISION_FOLLOW_DISABLED = TextKey.of("iris.desktop.vision.follow_disabled", "Follow disabled");
|
||||
@@ -158,8 +161,9 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey IMAGEMAP_PREVIEW_STATUS = TextKey.of("iris.desktop.imagemap.preview_status", "X {x} Z {z} | {value} | {scale} blocks/pixel");
|
||||
|
||||
private static final List<MessageKey> KEYS = List.of(
|
||||
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_FOLLOW,
|
||||
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_ENTITIES, VISION_FOLLOW,
|
||||
VISION_REFRESHING, VISION_FPS, VISION_ZOOM_RESET, VISION_GRID_ENABLED, VISION_GRID_DISABLED,
|
||||
VISION_ENTITIES_ENABLED, VISION_ENTITIES_DISABLED,
|
||||
VISION_FOLLOWING, VISION_NO_PLAYER, VISION_FOLLOW_DISABLED,
|
||||
VISION_STATUS_LEFT, VISION_STATUS_RIGHT, VISION_ENTITY_POSITION, VISION_ENTITY_HEALTH,
|
||||
VISION_BLOCK_POSITION, VISION_CHUNK_POSITION, VISION_REGION_POSITION, VISION_BIOME_KEY,
|
||||
|
||||
@@ -388,7 +388,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
try {
|
||||
validCache = validateArchive(archivePath, spec);
|
||||
} catch (IOException exception) {
|
||||
feedback.accept("Cached Iris " + spec.key() + " beta archive is invalid; downloading a replacement.");
|
||||
feedback.accept("Cached Iris " + spec.key() + " release archive is invalid; downloading a replacement.");
|
||||
}
|
||||
}
|
||||
String cachedSource = metadata.getProperty("source", "");
|
||||
@@ -439,7 +439,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
copyLimited(input, output, options.maxArchiveBytes(), spec);
|
||||
}
|
||||
if (!validateArchive(temporary, spec)) {
|
||||
throw new IOException("Downloaded Iris " + spec.key() + " beta archive is invalid");
|
||||
throw new IOException("Downloaded Iris " + spec.key() + " release archive is invalid");
|
||||
}
|
||||
move(temporary, archivePath, true);
|
||||
} finally {
|
||||
@@ -452,11 +452,11 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
updated.setProperty("fetchedAt", Long.toString(options.clock().millis()));
|
||||
updated.setProperty("sha256", sha256(archivePath));
|
||||
storePropertiesAtomic(metadataPath, updated);
|
||||
feedback.accept("Downloaded the Iris " + spec.key() + " beta archive.");
|
||||
feedback.accept("Downloaded the Iris " + spec.key() + " release archive.");
|
||||
return new Archive(archivePath, updated.getProperty("sha256"));
|
||||
}
|
||||
close(response.body());
|
||||
IOException statusFailure = new IOException("Iris " + spec.key() + " beta download returned HTTP " + status);
|
||||
IOException statusFailure = new IOException("Iris " + spec.key() + " release download returned HTTP " + status);
|
||||
if (!retryableStatus(status)) {
|
||||
networkFailure = statusFailure;
|
||||
break;
|
||||
@@ -464,7 +464,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
networkFailure = statusFailure;
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Iris " + spec.key() + " beta download was interrupted", exception);
|
||||
throw new IOException("Iris " + spec.key() + " release download was interrupted", exception);
|
||||
} catch (IOException exception) {
|
||||
networkFailure = exception;
|
||||
}
|
||||
@@ -474,7 +474,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
}
|
||||
|
||||
if (cacheMatchesSource) {
|
||||
feedback.accept("Iris " + spec.key() + " beta download failed; using the validated cached archive.");
|
||||
feedback.accept("Iris " + spec.key() + " release download failed; using the validated cached archive.");
|
||||
return new Archive(archivePath, sha256(archivePath));
|
||||
}
|
||||
if (isManagedPackOutputUsable(marker, cacheRoot.getParent().getParent(), spec)) {
|
||||
@@ -482,8 +482,8 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
return new Archive(null, sourceSha);
|
||||
}
|
||||
throw networkFailure == null
|
||||
? new IOException("Iris " + spec.key() + " beta archive is unavailable and no valid cache exists")
|
||||
: new IOException("Iris " + spec.key() + " beta archive is unavailable and no valid cache exists", networkFailure);
|
||||
? new IOException("Iris " + spec.key() + " release archive is unavailable and no valid cache exists")
|
||||
: new IOException("Iris " + spec.key() + " release archive is unavailable and no valid cache exists", networkFailure);
|
||||
}
|
||||
|
||||
private static boolean isManagedPackOutputUsable(Properties marker, Path dataDirectory, PackSpec spec) {
|
||||
@@ -514,11 +514,11 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
while ((entry = zip.getNextEntry()) != null) {
|
||||
entries++;
|
||||
if (entries > MAX_ARCHIVE_ENTRIES) {
|
||||
throw new IOException("Iris " + spec.key() + " beta archive contains too many entries");
|
||||
throw new IOException("Iris " + spec.key() + " release archive contains too many entries");
|
||||
}
|
||||
String name = normalizedZipEntry(entry.getName());
|
||||
if (!paths.add(name)) {
|
||||
throw new IOException("Iris " + spec.key() + " beta archive contains duplicate path " + name);
|
||||
throw new IOException("Iris " + spec.key() + " release archive contains duplicate path " + name);
|
||||
}
|
||||
String requiredDimension = "dimensions/" + spec.requiredDimension() + ".json";
|
||||
if (name.equals(requiredDimension) || name.endsWith("/" + requiredDimension)) {
|
||||
@@ -530,7 +530,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
while ((read = zip.read(buffer)) >= 0) {
|
||||
expanded += read;
|
||||
if (expanded > MAX_EXPANDED_BYTES) {
|
||||
throw new IOException("Iris " + spec.key() + " beta archive expands beyond the safety limit");
|
||||
throw new IOException("Iris " + spec.key() + " release archive expands beyond the safety limit");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -542,7 +542,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
|
||||
private static Path extractArchive(Path archive, Path extractionRoot, PackSpec spec) throws IOException {
|
||||
if (archive == null) {
|
||||
throw new IOException("Cached Iris " + spec.key() + " beta archive is unavailable for required pack rebuild");
|
||||
throw new IOException("Cached Iris " + spec.key() + " release archive is unavailable for required pack rebuild");
|
||||
}
|
||||
long expanded = 0L;
|
||||
int entries = 0;
|
||||
@@ -551,12 +551,12 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
while ((entry = zip.getNextEntry()) != null) {
|
||||
entries++;
|
||||
if (entries > MAX_ARCHIVE_ENTRIES) {
|
||||
throw new IOException("Iris " + spec.key() + " beta archive contains too many entries");
|
||||
throw new IOException("Iris " + spec.key() + " release archive contains too many entries");
|
||||
}
|
||||
String name = normalizedZipEntry(entry.getName());
|
||||
Path output = extractionRoot.resolve(name).normalize();
|
||||
if (!output.startsWith(extractionRoot)) {
|
||||
throw new IOException("Unsafe path in Iris " + spec.key() + " beta archive: " + entry.getName());
|
||||
throw new IOException("Unsafe path in Iris " + spec.key() + " release archive: " + entry.getName());
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(output);
|
||||
@@ -568,7 +568,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
while ((read = zip.read(buffer)) >= 0) {
|
||||
expanded += read;
|
||||
if (expanded > MAX_EXPANDED_BYTES) {
|
||||
throw new IOException("Iris " + spec.key() + " beta archive expands beyond the safety limit");
|
||||
throw new IOException("Iris " + spec.key() + " release archive expands beyond the safety limit");
|
||||
}
|
||||
file.write(buffer, 0, read);
|
||||
}
|
||||
@@ -589,7 +589,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
}
|
||||
}
|
||||
if (candidates.size() != 1) {
|
||||
throw new IOException("Iris " + spec.key() + " beta archive has an invalid root layout");
|
||||
throw new IOException("Iris " + spec.key() + " release archive has an invalid root layout");
|
||||
}
|
||||
return candidates.getFirst();
|
||||
}
|
||||
@@ -613,7 +613,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
|
||||
private static void validatePackRoot(Path path, PackSpec spec) throws IOException {
|
||||
if (!isPackRoot(path, spec)) {
|
||||
throw new IOException("Iris " + spec.key() + " beta pack is missing dimensions/"
|
||||
throw new IOException("Iris " + spec.key() + " release pack is missing dimensions/"
|
||||
+ spec.requiredDimension() + ".json at " + path);
|
||||
}
|
||||
}
|
||||
@@ -822,7 +822,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
while ((read = input.read(buffer)) >= 0) {
|
||||
total += read;
|
||||
if (total > limit) {
|
||||
throw new IOException("Iris " + spec.key() + " beta archive exceeds the download size limit");
|
||||
throw new IOException("Iris " + spec.key() + " release archive exceeds the download size limit");
|
||||
}
|
||||
output.write(buffer, 0, read);
|
||||
}
|
||||
|
||||
@@ -63,10 +63,10 @@ import java.util.zip.ZipInputStream;
|
||||
public final class PackDownloader {
|
||||
private static final String DEFAULT_OVERWORLD_PACK = "overworld";
|
||||
private static final String DEFAULT_OVERWORLD_URL =
|
||||
"https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip";
|
||||
"https://github.com/IrisDimensions/overworld/releases/download/4002/overworld.zip";
|
||||
private static final String UNDERWORLD_PACK = "underworld";
|
||||
private static final String UNDERWORLD_URL =
|
||||
"https://github.com/IrisDimensions/underworld/releases/download/beta/underworld.zip";
|
||||
"https://github.com/IrisDimensions/underworld/releases/download/1005/underworld.zip";
|
||||
private static final List<String> BUILT_IN_PACK_KEYS = List.of(DEFAULT_OVERWORLD_PACK, UNDERWORLD_PACK);
|
||||
private static final Map<String, String> BUILT_IN_PACK_URLS = Map.of(
|
||||
DEFAULT_OVERWORLD_PACK, DEFAULT_OVERWORLD_URL,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -84,9 +84,10 @@ public final class PackValidator {
|
||||
packFolder, dimensionFiles, validateLiveRegistries);
|
||||
addDistinct(blockingErrors, imageMaps.errors());
|
||||
addDistinct(warnings, imageMaps.warnings());
|
||||
PackRiverValidator.Validation riverValidation = PackRiverValidator.validate(packFolder, dimensionFiles);
|
||||
addDistinct(blockingErrors, riverValidation.errors());
|
||||
addDistinct(warnings, riverValidation.warnings());
|
||||
PackHydrologyValidator.Validation hydrologyValidation =
|
||||
PackHydrologyValidator.validate(packFolder, dimensionFiles);
|
||||
addDistinct(blockingErrors, hydrologyValidation.errors());
|
||||
addDistinct(warnings, hydrologyValidation.warnings());
|
||||
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
|
||||
PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder);
|
||||
addDistinct(blockingErrors, lootIssues.errors());
|
||||
|
||||
@@ -481,8 +481,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
listener.onChunkFailed(x, z);
|
||||
} else {
|
||||
listener.onChunkGenerated(x, z);
|
||||
cleanupMantleChunk(x, z);
|
||||
listener.onChunkCleaned(x, z);
|
||||
cleanupMantleChunksCoveredBy(x, z, listener);
|
||||
onChunkCompleted(x, z, chunk);
|
||||
success = true;
|
||||
}
|
||||
@@ -692,11 +691,11 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupMantleChunk(int x, int z) {
|
||||
private void cleanupMantleChunksCoveredBy(int x, int z, PregenListener listener) {
|
||||
Engine engine = resolveMetricsEngine();
|
||||
if (engine != null) {
|
||||
try {
|
||||
engine.getMantle().forceCleanupChunk(x, z);
|
||||
engine.getMantle().cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
|
||||
@Accessors(fluent = true, chain = true)
|
||||
public class IrisCreator {
|
||||
private static final long WORLD_CREATE_TIMEOUT_SECONDS = 120L;
|
||||
private static final long INITIAL_SPAWN_TIMEOUT_SECONDS = 600L;
|
||||
private static final long WORLD_ENTRY_TELEPORT_TIMEOUT_SECONDS = 60L;
|
||||
private static final long ROLLBACK_PHASE_TIMEOUT_SECONDS = 120L;
|
||||
|
||||
@@ -619,7 +620,7 @@ public class IrisCreator {
|
||||
generator.getInitialSpawnReady(),
|
||||
"Initial spawn preparation future");
|
||||
try {
|
||||
initialSpawnReady.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
initialSpawnReady.get(INITIAL_SPAWN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException failure) {
|
||||
throw new TimeoutException("Initial spawn preparation timed out for world \""
|
||||
+ worldName + "\".");
|
||||
|
||||
@@ -22,7 +22,14 @@ import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyNaturalSample;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntimeContext;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.mantle.components.MantleHydrologyCaveVoxelView;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDecorationPart;
|
||||
@@ -31,20 +38,11 @@ import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisRiverCaves;
|
||||
import art.arcane.iris.engine.object.IrisRiverDeepPools;
|
||||
import art.arcane.iris.engine.object.IrisRiverNetwork;
|
||||
import art.arcane.iris.engine.object.IrisRiverOverride;
|
||||
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
|
||||
import art.arcane.iris.engine.object.IrisRiverWater;
|
||||
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
|
||||
import art.arcane.iris.engine.object.IrisHydrology;
|
||||
import art.arcane.iris.engine.object.IrisRiverHydrology;
|
||||
import art.arcane.iris.engine.object.IrisRiverProfile;
|
||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
@@ -67,36 +65,48 @@ import lombok.ToString;
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime", "imageMapRuntime"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime", "imageMapRuntime"})
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime"})
|
||||
public class IrisComplex implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
||||
private static final int HEIGHT_BOUNDS_GRID = 4;
|
||||
private static final Comparator<IrisInterpolator> INTERPOLATOR_ORDER = Comparator
|
||||
.comparing((IrisInterpolator interpolator) -> interpolator.getFunction().name())
|
||||
.thenComparingDouble(IrisInterpolator::getHorizontalScale);
|
||||
private static final Comparator<IrisGenerator> GENERATOR_ORDER = Comparator.comparing(
|
||||
IrisGenerator::getLoadKey,
|
||||
Comparator.nullsFirst(Comparator.naturalOrder())
|
||||
);
|
||||
private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
|
||||
InferredType.LAND,
|
||||
InferredType.CAVE,
|
||||
InferredType.SEA,
|
||||
InferredType.SHORE
|
||||
};
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ThreadLocal<GridBoundsCache> gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new);
|
||||
/**
|
||||
* Immutable snapshot of {@link #generators} taken once at the end of construction, in the exact
|
||||
* iteration order the map produces. The per-column height paths walk these arrays instead of
|
||||
* allocating map/set iterators, and the frozen order keeps the floating point accumulation order
|
||||
* identical to the map iteration it replaces. Mutating {@link #generators} after construction is
|
||||
* not reflected here.
|
||||
*/
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient IrisInterpolator[] frozenInterpolators;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient IrisGenerator[][] frozenGenerators;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> inferredBiomeStreams;
|
||||
private RNG rng;
|
||||
private double fluidHeight;
|
||||
private IrisData data;
|
||||
@@ -121,9 +131,7 @@ public class IrisComplex implements DataProvider {
|
||||
private ProceduralStream<Double> maxHeightStream;
|
||||
private ProceduralStream<Double> overlayStream;
|
||||
private ProceduralStream<Double> heightFluidStream;
|
||||
private ProceduralStream<Double> naturalSlopeStream;
|
||||
private ProceduralStream<Double> slopeStream;
|
||||
private ProceduralStream<IrisRiverSurfaceSample> riverSurfaceStream;
|
||||
private ProceduralStream<Double> riverDistanceStream;
|
||||
private ProceduralStream<Double> riverFlowStream;
|
||||
private ProceduralStream<Double> riverCarveWeightStream;
|
||||
@@ -138,13 +146,12 @@ public class IrisComplex implements DataProvider {
|
||||
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
|
||||
private ProceduralStream<PlatformBlockState> rockStream;
|
||||
private ProceduralStream<PlatformBlockState> fluidStream;
|
||||
private ProceduralStream<PlatformBlockState> riverFluidStream;
|
||||
private ProceduralStream<PlatformBlockState> riverDeepPoolFluidStream;
|
||||
private Map<String, ProceduralStream<PlatformBlockState>> hydrologyFluidStreams;
|
||||
private IrisBiome focusBiome;
|
||||
private IrisRegion focusRegion;
|
||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||
private Set<IrisBiome> generatorBiomes;
|
||||
private IrisRiverRuntime riverRuntime;
|
||||
private IrisHydrologyRuntime hydrologyRuntime;
|
||||
private transient IrisImageMapRuntime imageMapRuntime;
|
||||
// Copy-on-write: reads happen per column on every burst thread; the synchronizedMap
|
||||
// monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map
|
||||
@@ -170,6 +177,7 @@ public class IrisComplex implements DataProvider {
|
||||
focusBiome = engine.getFocus();
|
||||
focusRegion = engine.getFocusRegion();
|
||||
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
||||
KList<IrisRegion> preparedRegions = new KList<>();
|
||||
|
||||
if (focusBiome != null) {
|
||||
focusBiome = focusBiome.withInferredType(InferredType.LAND);
|
||||
@@ -178,7 +186,7 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
//@builder
|
||||
if (focusRegion != null) {
|
||||
prepareInferredBiomes(focusRegion);
|
||||
prepareInferredBiomes(focusRegion, preparedRegions);
|
||||
focusRegion.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
} else {
|
||||
engine.getDimension().getRegions().forEach(regionKey -> {
|
||||
@@ -186,23 +194,26 @@ public class IrisComplex implements DataProvider {
|
||||
if (region == null) {
|
||||
return;
|
||||
}
|
||||
prepareInferredBiomes(region);
|
||||
prepareInferredBiomes(region, preparedRegions);
|
||||
region.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
});
|
||||
for (IrisRegion region : imageMapRuntime.getMappedRegions()) {
|
||||
prepareInferredBiomes(region);
|
||||
prepareInferredBiomes(region, preparedRegions);
|
||||
region.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
}
|
||||
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
|
||||
}
|
||||
int interpolatorCount = generators.size();
|
||||
inferredBiomeStreams = compileInferredBiomeStreams(
|
||||
preparedRegions,
|
||||
(region, inferredType) -> compileInferredBiomeStream(engine, region, inferredType, emptyBiome)
|
||||
);
|
||||
GeneratorGroup[] generatorGroups = freezeGeneratorGroups(generators);
|
||||
int interpolatorCount = generatorGroups.length;
|
||||
frozenInterpolators = new IrisInterpolator[interpolatorCount];
|
||||
frozenGenerators = new IrisGenerator[interpolatorCount][];
|
||||
int frozenIndex = 0;
|
||||
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
|
||||
frozenInterpolators[frozenIndex] = entry.getKey();
|
||||
frozenGenerators[frozenIndex] = entry.getValue().toArray(new IrisGenerator[0]);
|
||||
frozenIndex++;
|
||||
for (int frozenIndex = 0; frozenIndex < generatorGroups.length; frozenIndex++) {
|
||||
frozenInterpolators[frozenIndex] = generatorGroups[frozenIndex].interpolator();
|
||||
frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators();
|
||||
}
|
||||
generatorBounds = buildGeneratorBounds(engine);
|
||||
KList<IrisShapedGeneratorStyle> overlayNoise = engine.getDimension().getOverlayNoise();
|
||||
@@ -221,29 +232,7 @@ public class IrisComplex implements DataProvider {
|
||||
.select(engine.getDimension().getRockPalette().getBlockData(data));
|
||||
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
||||
.select(engine.getDimension().getFluidPalette().getBlockData(data));
|
||||
IrisRiverNetwork configuredRivers = engine.getDimension().getRivers();
|
||||
IrisRiverWater configuredRiverWater = configuredRivers == null ? null : configuredRivers.getWater();
|
||||
riverFluidStream = configuredRivers != null && configuredRivers.isEnabled()
|
||||
? configuredFluidStream(
|
||||
Objects.requireNonNull(configuredRiverWater).getFluidPalette(),
|
||||
rng.nextParallelRNG(79),
|
||||
"River water"
|
||||
)
|
||||
: fluidStream;
|
||||
IrisRiverCaves configuredRiverCaves = configuredRivers == null ? null : configuredRivers.getCaves();
|
||||
IrisRiverDeepPools configuredDeepPools = configuredRiverCaves == null
|
||||
? null
|
||||
: configuredRiverCaves.getDeepPools();
|
||||
riverDeepPoolFluidStream = configuredRivers != null
|
||||
&& configuredRivers.isEnabled()
|
||||
&& configuredDeepPools != null
|
||||
&& configuredDeepPools.isEnabled()
|
||||
? configuredFluidStream(
|
||||
configuredDeepPools.getFluidPalette(),
|
||||
rng.nextParallelRNG(80),
|
||||
"River deep-pool"
|
||||
)
|
||||
: riverFluidStream;
|
||||
hydrologyFluidStreams = createHydrologyFluidStreams(engine.getDimension().getHydrology());
|
||||
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
||||
.zoom(engine.getDimension().getRegionZoom());
|
||||
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||
@@ -262,41 +251,22 @@ public class IrisComplex implements DataProvider {
|
||||
regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i),
|
||||
String.valueOf(i * 38445).hashCode() * 3245556666L));
|
||||
caveBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
.convert((r)
|
||||
-> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
|
||||
.zoom(engine.getDimension().getBiomeZoom())
|
||||
.zoom(r.getCaveBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(r.getCaveBiomes(), InferredType.CAVE))
|
||||
.onNull(emptyBiome)
|
||||
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize);
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
|
||||
.convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize);
|
||||
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
||||
landBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
.convert((r)
|
||||
-> engine.getDimension().getLandBiomeStyle().create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
|
||||
.zoom(engine.getDimension().getBiomeZoom())
|
||||
.zoom(engine.getDimension().getLandZoom())
|
||||
.zoom(r.getLandBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(r.getLandBiomes(), InferredType.LAND))
|
||||
).convertAware2D(ProceduralStream::get)
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
|
||||
.convertAware2D(ProceduralStream::get)
|
||||
.cache2D("landBiomeStream", engine, cacheSize);
|
||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||
seaBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
.convert((r)
|
||||
-> engine.getDimension().getSeaBiomeStyle().create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
|
||||
.zoom(engine.getDimension().getBiomeZoom())
|
||||
.zoom(engine.getDimension().getSeaZoom())
|
||||
.zoom(r.getSeaBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(r.getSeaBiomes(), InferredType.SEA))
|
||||
).convertAware2D(ProceduralStream::get)
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
|
||||
.convertAware2D(ProceduralStream::get)
|
||||
.cache2D("seaBiomeStream", engine, cacheSize);
|
||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||
shoreBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
.convert((r)
|
||||
-> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
|
||||
.zoom(engine.getDimension().getBiomeZoom())
|
||||
.zoom(r.getShoreBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(r.getShoreBiomes(), InferredType.SHORE))
|
||||
).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize);
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
|
||||
.convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize);
|
||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
|
||||
Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) :
|
||||
@@ -315,13 +285,10 @@ public class IrisComplex implements DataProvider {
|
||||
return mapped == null ? biome : mapped;
|
||||
})
|
||||
.cache2D("imageMappedBaseBiomeStream", engine, cacheSize);
|
||||
naturalHeightStream = ProceduralStream.of((x, z) -> {
|
||||
IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z);
|
||||
double proceduralHeight = getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
|
||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
||||
}, Interpolated.DOUBLE).cache2DDouble("naturalHeightStream", engine, cacheSize);
|
||||
naturalSlopeStream = naturalHeightStream.slope(3)
|
||||
.cache2DDouble("naturalSlopeStream", engine, cacheSize);
|
||||
naturalHeightStream = ProceduralStream.of(
|
||||
(x, z) -> sampleNaturalTerrainHeight(engine, x, z),
|
||||
Interpolated.DOUBLE
|
||||
).cache2DDouble("naturalHeightStream", engine, cacheSize);
|
||||
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||
b -> focusBiome))
|
||||
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
|
||||
@@ -332,46 +299,20 @@ public class IrisComplex implements DataProvider {
|
||||
: mapped;
|
||||
})
|
||||
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
|
||||
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
|
||||
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
|
||||
bridgeStream,
|
||||
focusBiome
|
||||
).cache2D("naturalOceanStream", engine, cacheSize);
|
||||
int riverFluidHeight = engine.getDimension().getRivers().getWater().getFluidHeight()
|
||||
- engine.getDimension().getMinHeight();
|
||||
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
|
||||
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
|
||||
if (hydrologyActive(configuredHydrology)) {
|
||||
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
|
||||
engine.getSeedManager().getBodies(),
|
||||
engine.getDimension().getRivers(),
|
||||
engine.getHeight(),
|
||||
engine.getDimension(),
|
||||
data,
|
||||
riverFluidHeight,
|
||||
(int) Math.round(fluidHeight),
|
||||
IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()),
|
||||
IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()),
|
||||
blockingRiverRoutingPossible(engine),
|
||||
variableMaxIncisionPossible(engine),
|
||||
biomeRiverOverridesPossible(focusBiome, generatorBiomes),
|
||||
(blockX, blockZ) -> naturalHeightBounds(engine, overlayNoise, blockX, blockZ),
|
||||
naturalHeightStream,
|
||||
naturalSlopeStream,
|
||||
naturalOceanStream,
|
||||
naturalTrueBiomeStream,
|
||||
regionStream
|
||||
(x, z, naturalHeight) -> sampleHydrologyNatural(engine, x, z, naturalHeight),
|
||||
(x, z) -> naturalHeightStream.getDouble(x, z),
|
||||
this::sampleNaturalOcean,
|
||||
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint)
|
||||
));
|
||||
riverSurfaceStream = ProceduralStream.of(
|
||||
(x, z) -> riverRuntime.sample(x, z),
|
||||
Interpolated.of(
|
||||
IrisRiverSurfaceSample::terrainHeight,
|
||||
value -> IrisRiverSurfaceSample.none(value, fluidHeight)
|
||||
)
|
||||
)
|
||||
.cache2D("riverSurfaceStream", engine, cacheSize);
|
||||
} else {
|
||||
riverSurfaceStream = naturalHeightStream.convert(
|
||||
value -> IrisRiverSurfaceSample.none(value, fluidHeight)
|
||||
)
|
||||
.cache2D("riverSurfaceStream-disabled", engine, cacheSize);
|
||||
}
|
||||
heightStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::terrainHeight)
|
||||
heightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z))
|
||||
.cache2DDouble("heightStream", engine, cacheSize);
|
||||
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.round();
|
||||
@@ -379,20 +320,18 @@ public class IrisComplex implements DataProvider {
|
||||
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
|
||||
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||
b -> focusBiome))
|
||||
.cache2D("trueBiomeStream-focus", engine, cacheSize) : riverSurfaceStream
|
||||
.convertAware2D((sample, x, z) -> resolveRiverSurfaceBiome(sample, x, z))
|
||||
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
|
||||
.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z))
|
||||
.cache2D("trueBiomeStream", engine, cacheSize);
|
||||
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||
.convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize);
|
||||
riverDistanceStream = riverSurfaceStream.convert(sample -> sample.river().present()
|
||||
? sample.river().distance()
|
||||
: Double.MAX_VALUE)
|
||||
riverDistanceStream = ProceduralStream.ofDouble(this::resolveHydrologyDistance)
|
||||
.cache2DDouble("riverDistanceStream", engine, cacheSize);
|
||||
riverFlowStream = riverSurfaceStream.convert(sample -> (double) sample.river().flow())
|
||||
riverFlowStream = ProceduralStream.ofDouble(this::resolveHydrologyFlow)
|
||||
.cache2DDouble("riverFlowStream", engine, cacheSize);
|
||||
riverCarveWeightStream = riverSurfaceStream.convert(sample -> sample.river().carveWeight())
|
||||
riverCarveWeightStream = ProceduralStream.ofDouble(this::resolveHydrologyCarveWeight)
|
||||
.cache2DDouble("riverCarveWeightStream", engine, cacheSize);
|
||||
riverWaterSurfaceStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::waterSurfaceY)
|
||||
riverWaterSurfaceStream = ProceduralStream.ofDouble(this::resolveHydrologyFluidSurface)
|
||||
.cache2DDouble("riverWaterSurfaceStream", engine, cacheSize);
|
||||
heightFluidStream = ProceduralStream.ofDouble((x, z) -> Math.max(
|
||||
heightStream.get(x, z),
|
||||
@@ -424,79 +363,204 @@ public class IrisComplex implements DataProvider {
|
||||
//@done
|
||||
}
|
||||
|
||||
private boolean blockingRiverRoutingPossible(Engine engine) {
|
||||
if (blocksRiverRouting(focusRegion == null ? null : focusRegion.getRiverOverride())
|
||||
|| blocksRiverRouting(focusBiome == null ? null : focusBiome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
||||
for (IrisRegion loadedRegion : regions) {
|
||||
if (loadedRegion != null && blocksRiverRouting(loadedRegion.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
KList<IrisBiome> biomes = engine.getDimension().getReachableBiomes(engine);
|
||||
for (IrisBiome biome : biomes) {
|
||||
if (biome != null && blocksRiverRouting(biome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean blocksRiverRouting(IrisRiverOverride override) {
|
||||
return override != null && override.getRoutingPolicy() == IrisRiverRoutingPolicy.BLOCK;
|
||||
}
|
||||
|
||||
private boolean variableMaxIncisionPossible(Engine engine) {
|
||||
if (changesMaxIncision(focusRegion == null ? null : focusRegion.getRiverOverride())
|
||||
|| changesMaxIncision(focusBiome == null ? null : focusBiome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
||||
for (IrisRegion loadedRegion : regions) {
|
||||
if (loadedRegion != null && changesMaxIncision(loadedRegion.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
KList<IrisBiome> biomes = engine.getDimension().getReachableBiomes(engine);
|
||||
for (IrisBiome biome : biomes) {
|
||||
if (biome != null && changesMaxIncision(biome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean changesMaxIncision(IrisRiverOverride override) {
|
||||
if (override == null || override.getMaxIncisionMultiplier() == null) {
|
||||
return false;
|
||||
}
|
||||
double multiplier = override.getMaxIncisionMultiplier();
|
||||
return Double.isFinite(multiplier) && Double.compare(Math.max(0D, multiplier), 1D) != 0;
|
||||
}
|
||||
|
||||
static boolean biomeRiverOverridesPossible(IrisBiome focusBiome, Iterable<IrisBiome> naturalBiomes) {
|
||||
if (focusBiome != null) {
|
||||
return focusBiome.getRiverOverride() != null;
|
||||
}
|
||||
for (IrisBiome biome : naturalBiomes) {
|
||||
if (biome != null && biome.getRiverOverride() != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ProceduralStream<Boolean> createNaturalOceanStream(
|
||||
static InferredType resolveNaturalInferredType(
|
||||
ProceduralStream<InferredType> bridgeStream,
|
||||
IrisBiome focusBiome
|
||||
IrisBiome focusBiome,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
if (focusBiome != null) {
|
||||
boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
|
||||
return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN);
|
||||
return focusBiome.getInferredType();
|
||||
}
|
||||
return bridgeStream.convert(type -> type == InferredType.SEA);
|
||||
return bridgeStream.get(x, z);
|
||||
}
|
||||
|
||||
private IrisHydrologyNaturalSample sampleHydrologyNatural(
|
||||
Engine engine,
|
||||
int x,
|
||||
int z,
|
||||
double naturalHeight
|
||||
) {
|
||||
InferredType inferredType = resolveNaturalInferredType(bridgeStream, focusBiome, x, z);
|
||||
IrisRegion region = regionStream.get(x, z);
|
||||
IrisBiome biome = focusBiome == null
|
||||
? sampleNaturalBiome(inferredType, region, naturalHeight, x, z)
|
||||
: focusBiome;
|
||||
return new IrisHydrologyNaturalSample(
|
||||
naturalHeight,
|
||||
inferredType == InferredType.SEA,
|
||||
biome,
|
||||
region
|
||||
);
|
||||
}
|
||||
|
||||
private boolean sampleNaturalOcean(int x, int z) {
|
||||
return resolveNaturalInferredType(bridgeStream, focusBiome, x, z) == InferredType.SEA;
|
||||
}
|
||||
|
||||
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
|
||||
double proceduralHeight = getHeight(engine, x, z, engine.getSeedManager().getHeight());
|
||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
||||
}
|
||||
|
||||
static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) {
|
||||
double deltaX = easternHeight - naturalHeight;
|
||||
double deltaZ = southernHeight - naturalHeight;
|
||||
return Math.sqrt(deltaX * deltaX + deltaZ * deltaZ);
|
||||
}
|
||||
|
||||
private IrisBiome sampleNaturalBiome(
|
||||
InferredType inferredType,
|
||||
IrisRegion region,
|
||||
double naturalHeight,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
if (mapped != null) {
|
||||
return mapped;
|
||||
}
|
||||
IrisBiome baseBiome = implode(sampleInferredBiome(region, inferredType, x, z), x, z);
|
||||
IrisBiome resolved = resolveNaturalSurfaceBiome(naturalHeight, baseBiome, region, x, z);
|
||||
return resolved == baseBiome ? baseBiome : implode(resolved, x, z);
|
||||
}
|
||||
|
||||
private IrisBiome resolveNaturalSurfaceBiome(
|
||||
double height,
|
||||
IrisBiome biome,
|
||||
IrisRegion region,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
if (biome == null || region == null) {
|
||||
return biome;
|
||||
}
|
||||
double shoreHeight = region.getShoreHeight(x, z);
|
||||
if (height >= fluidHeight - 1 && height <= fluidHeight + shoreHeight && !biome.isShore()) {
|
||||
return sampleInferredBiome(region, InferredType.SHORE, x, z);
|
||||
}
|
||||
if (height > fluidHeight + shoreHeight && !biome.isLand()) {
|
||||
return sampleInferredBiome(region, InferredType.LAND, x, z);
|
||||
}
|
||||
if (height < fluidHeight && !biome.isAquatic()) {
|
||||
return sampleInferredBiome(region, InferredType.SEA, x, z);
|
||||
}
|
||||
if (height == fluidHeight && !biome.isShore()) {
|
||||
return sampleInferredBiome(region, InferredType.SHORE, x, z);
|
||||
}
|
||||
return biome;
|
||||
}
|
||||
|
||||
private IrisBiome sampleInferredBiome(
|
||||
IrisRegion region,
|
||||
InferredType inferredType,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
return createInferredBiomeStream(region, inferredType).get(x, z);
|
||||
}
|
||||
|
||||
private ProceduralStream<IrisBiome> createInferredBiomeStream(
|
||||
IrisRegion region,
|
||||
InferredType inferredType
|
||||
) {
|
||||
return preparedInferredBiomeStream(inferredBiomeStreams, region, inferredType);
|
||||
}
|
||||
|
||||
private ProceduralStream<IrisBiome> compileInferredBiomeStream(
|
||||
Engine engine,
|
||||
IrisRegion region,
|
||||
InferredType inferredType,
|
||||
IrisBiome emptyBiome
|
||||
) {
|
||||
return switch (inferredType) {
|
||||
case CAVE -> engine.getDimension().getCaveBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
|
||||
.zoom(engine.getDimension().getBiomeZoom())
|
||||
.zoom(region.getCaveBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE))
|
||||
.onNull(emptyBiome);
|
||||
case LAND -> engine.getDimension().getLandBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
|
||||
.zoom(engine.getDimension().getBiomeZoom())
|
||||
.zoom(engine.getDimension().getLandZoom())
|
||||
.zoom(region.getLandBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(region.getLandBiomes(), InferredType.LAND));
|
||||
case SEA -> engine.getDimension().getSeaBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
|
||||
.zoom(engine.getDimension().getBiomeZoom())
|
||||
.zoom(engine.getDimension().getSeaZoom())
|
||||
.zoom(region.getSeaBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA));
|
||||
case SHORE -> engine.getDimension().getShoreBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
|
||||
.zoom(engine.getDimension().getBiomeZoom())
|
||||
.zoom(region.getShoreBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE));
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean hydrologyActive(IrisHydrology hydrology) {
|
||||
if (hydrology == null) {
|
||||
return false;
|
||||
}
|
||||
IrisRiverHydrology rivers = hydrology.getRivers();
|
||||
if (rivers != null && rivers.isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
for (IrisDeepFluidConfig deepFluid : hydrology.getDeepFluids()) {
|
||||
if (deepFluid != null
|
||||
&& deepFluid.getDensity() > 0D
|
||||
&& (deepFluid.isContainedPools() || deepFluid.isShortChannels())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Map<String, ProceduralStream<PlatformBlockState>> createHydrologyFluidStreams(IrisHydrology hydrology) {
|
||||
if (hydrology == null) {
|
||||
return Map.of();
|
||||
}
|
||||
LinkedHashMap<String, ProceduralStream<PlatformBlockState>> streams = new LinkedHashMap<>();
|
||||
IrisRiverHydrology rivers = Objects.requireNonNull(hydrology.getRivers(), "hydrology.rivers");
|
||||
int streamIndex = 0;
|
||||
for (IrisRiverProfile profile : rivers.getProfiles()) {
|
||||
if (profile == null) {
|
||||
throw new IllegalArgumentException("hydrology.rivers.profiles cannot contain null entries");
|
||||
}
|
||||
String profileKey = requireHydrologyKey(profile.getId(), "hydrology.rivers.profiles[].id");
|
||||
ProceduralStream<PlatformBlockState> stream = configuredFluidStream(
|
||||
profile.getFluidPalette(),
|
||||
rng.nextParallelRNG(7900 + streamIndex++),
|
||||
"hydrology river profile " + profileKey
|
||||
);
|
||||
if (streams.putIfAbsent(profileKey, stream) != null) {
|
||||
throw new IllegalArgumentException("Duplicate hydrology fluid profile: " + profileKey);
|
||||
}
|
||||
}
|
||||
for (IrisDeepFluidConfig deepFluid : hydrology.getDeepFluids()) {
|
||||
if (deepFluid == null) {
|
||||
throw new IllegalArgumentException("hydrology.deepFluids cannot contain null entries");
|
||||
}
|
||||
String profileKey = requireHydrologyKey(deepFluid.getId(), "hydrology.deepFluids[].id");
|
||||
ProceduralStream<PlatformBlockState> stream = configuredFluidStream(
|
||||
deepFluid.getFluidPalette(),
|
||||
rng.nextParallelRNG(8900 + streamIndex++),
|
||||
"hydrology deep-fluid profile " + profileKey
|
||||
);
|
||||
if (streams.putIfAbsent(profileKey, stream) != null) {
|
||||
throw new IllegalArgumentException("Duplicate hydrology fluid profile: " + profileKey);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(streams);
|
||||
}
|
||||
|
||||
private static String requireHydrologyKey(String key, String path) {
|
||||
if (key == null || key.isBlank()) {
|
||||
throw new IllegalArgumentException(path + " must not be blank");
|
||||
}
|
||||
return key.trim();
|
||||
}
|
||||
|
||||
private ProceduralStream<PlatformBlockState> configuredFluidStream(
|
||||
@@ -519,21 +583,81 @@ public class IrisComplex implements DataProvider {
|
||||
return palette.getLayerGenerator(fluidRng, data).stream().select(blocks);
|
||||
}
|
||||
|
||||
public PlatformBlockState resolveRiverCaveFluid(RiverCaveFluidKind fluidKind, double x, double z) {
|
||||
return switch (Objects.requireNonNull(fluidKind)) {
|
||||
case RIVER -> riverFluidStream.get(x, z);
|
||||
case DEEP_POOL -> riverDeepPoolFluidStream.get(x, z);
|
||||
};
|
||||
public PlatformBlockState resolveHydrologyFluid(String profileKey, double x, double z) {
|
||||
String key = Objects.requireNonNull(profileKey, "profileKey").trim();
|
||||
ProceduralStream<PlatformBlockState> stream = hydrologyFluidStreams.get(key);
|
||||
if (stream == null) {
|
||||
throw new IllegalArgumentException("Unknown hydrology fluid profile: " + key);
|
||||
}
|
||||
return stream.get(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState resolveSurfaceFluid(double x, double z) {
|
||||
IrisRiverSurfaceSample sample = riverSurfaceStream.get(x, z);
|
||||
if (sample.river().present() && sample.surfaceFluid()) {
|
||||
return riverFluidStream.get(x, z);
|
||||
HydrologyColumnLayer layer = surfaceFluidLayer(x, z);
|
||||
if (layer != null) {
|
||||
return resolveHydrologyFluid(layer.profileKey(), x, z);
|
||||
}
|
||||
return fluidStream.get(x, z);
|
||||
}
|
||||
|
||||
private double resolveHydrologyTerrainHeight(double x, double z) {
|
||||
HydrologyColumnSample sample = hydrologySample(x, z);
|
||||
return sample == null ? naturalHeightStream.get(x, z) : sample.terrainHeight();
|
||||
}
|
||||
|
||||
private double resolveHydrologyDistance(double x, double z) {
|
||||
HydrologyColumnLayer layer = surfaceLayer(x, z);
|
||||
if (layer == null) {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
if (layer.channel()) {
|
||||
return 0D;
|
||||
}
|
||||
return layer.shore() ? 1D : 2D;
|
||||
}
|
||||
|
||||
private double resolveHydrologyFlow(double x, double z) {
|
||||
HydrologyColumnLayer layer = surfaceFluidLayer(x, z);
|
||||
if (layer == null) {
|
||||
return 0D;
|
||||
}
|
||||
return layer.feature().flowDeltaX() == 0 && layer.feature().flowDeltaZ() == 0 ? 0D : 1D;
|
||||
}
|
||||
|
||||
private double resolveHydrologyCarveWeight(double x, double z) {
|
||||
HydrologyColumnLayer layer = surfaceLayer(x, z);
|
||||
if (layer == null) {
|
||||
return 0D;
|
||||
}
|
||||
if (layer.channel()) {
|
||||
return 1D;
|
||||
}
|
||||
return layer.shore() ? 0.5D : 0.25D;
|
||||
}
|
||||
|
||||
private double resolveHydrologyFluidSurface(double x, double z) {
|
||||
HydrologyColumnLayer layer = surfaceFluidLayer(x, z);
|
||||
return layer == null ? fluidHeight : layer.fluidHeadY();
|
||||
}
|
||||
|
||||
private HydrologyColumnSample hydrologySample(double x, double z) {
|
||||
return hydrologyRuntime == null ? null : hydrologyRuntime.sample(x, z).orElse(null);
|
||||
}
|
||||
|
||||
private HydrologyColumnLayer surfaceLayer(double x, double z) {
|
||||
HydrologyColumnSample sample = hydrologySample(x, z);
|
||||
return sample == null ? null : sample.primarySurfaceLayer().orElse(null);
|
||||
}
|
||||
|
||||
private HydrologyColumnLayer surfaceFluidLayer(double x, double z) {
|
||||
HydrologyColumnSample sample = hydrologySample(x, z);
|
||||
return sample == null ? null : sample.primarySurfaceFluidLayer().orElse(null);
|
||||
}
|
||||
|
||||
public boolean hasHydrologySurfaceFluid(int x, int z) {
|
||||
return surfaceFluidLayer(x, z) != null;
|
||||
}
|
||||
|
||||
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
|
||||
switch (type) {
|
||||
case CAVE:
|
||||
@@ -555,7 +679,7 @@ public class IrisComplex implements DataProvider {
|
||||
if (engine == null) {
|
||||
throw new IllegalArgumentException("Engine is required to sample procedural terrain height");
|
||||
}
|
||||
return getHeight(engine, null, worldX, worldZ, engine.getSeedManager().getHeight());
|
||||
return getHeight(engine, worldX, worldZ, engine.getSeedManager().getHeight());
|
||||
}
|
||||
|
||||
private IrisRegion findRegion(IrisBiome focus, Engine engine) {
|
||||
@@ -594,46 +718,29 @@ public class IrisComplex implements DataProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
private IrisBiome resolveRiverSurfaceBiome(IrisRiverSurfaceSample sample, double x, double z) {
|
||||
private IrisBiome resolveHydrologySurfaceBiome(double terrainHeight, double x, double z) {
|
||||
HydrologyColumnSample sample = hydrologySample(x, z);
|
||||
HydrologyColumnLayer layer = sample == null ? null : sample.primarySurfaceLayer().orElse(null);
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
if (mapped != null && (riverRuntime == null || !sample.river().present())) {
|
||||
if (mapped != null && layer == null) {
|
||||
return mapped;
|
||||
}
|
||||
if (riverRuntime != null) {
|
||||
if (sample.subterranean()) {
|
||||
return fixBiomeType(
|
||||
sample.naturalHeight(),
|
||||
baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight
|
||||
);
|
||||
if (layer != null) {
|
||||
String biomeKey = hydrologySurfaceBiomeKey(sample);
|
||||
IrisBiome biome = data.getBiomeLoader().load(biomeKey);
|
||||
if (biome == null) {
|
||||
throw new IllegalStateException("Hydrology biome does not exist: " + biomeKey);
|
||||
}
|
||||
IrisBiome riverBiome = riverRuntime.selectSurfaceBiome(sample, x, z);
|
||||
if (riverBiome != null) {
|
||||
return implode(riverBiome, x, z);
|
||||
}
|
||||
InferredType directFallback = directRiverFallback(sample.river());
|
||||
if (directFallback != null) {
|
||||
IrisBiome baseBiome = baseBiomeStream.get(x, z);
|
||||
return implode(baseBiome.withInferredType(directFallback), x, z);
|
||||
}
|
||||
if (sample.river().present()
|
||||
&& sample.river().state() == RiverRouteState.WET
|
||||
&& sample.river().section() == RiverSection.BANK) {
|
||||
return fixBiomeType(
|
||||
sample.terrainHeight(),
|
||||
baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
sample.waterSurfaceY()
|
||||
);
|
||||
if (layer.biomeKey() == null) {
|
||||
return biome;
|
||||
}
|
||||
InferredType inferredType = layer.shore()
|
||||
? InferredType.SHORE
|
||||
: layer.connectedFluid() ? InferredType.SEA : InferredType.LAND;
|
||||
return implode(biome.withInferredType(inferredType), x, z);
|
||||
}
|
||||
return fixBiomeType(
|
||||
sample.terrainHeight(),
|
||||
terrainHeight,
|
||||
baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
@@ -642,17 +749,14 @@ public class IrisComplex implements DataProvider {
|
||||
);
|
||||
}
|
||||
|
||||
static InferredType directRiverFallback(RiverSample river) {
|
||||
if (!river.present()) {
|
||||
return null;
|
||||
static String hydrologySurfaceBiomeKey(HydrologyColumnSample sample) {
|
||||
Objects.requireNonNull(sample, "sample");
|
||||
HydrologyColumnLayer layer = sample.primarySurfaceLayer().orElse(null);
|
||||
if (layer == null) {
|
||||
return sample.parentBiomeKey();
|
||||
}
|
||||
if (river.state() == RiverRouteState.DRY) {
|
||||
return InferredType.LAND;
|
||||
}
|
||||
return switch (river.section()) {
|
||||
case CHANNEL, MOUTH -> InferredType.SEA;
|
||||
default -> null;
|
||||
};
|
||||
String biomeKey = layer.biomeKey();
|
||||
return biomeKey == null ? sample.parentBiomeKey() : biomeKey;
|
||||
}
|
||||
|
||||
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
|
||||
@@ -713,13 +817,7 @@ public class IrisComplex implements DataProvider {
|
||||
double hi = sampledBounds.max();
|
||||
double lo = sampledBounds.min();
|
||||
|
||||
double d = 0;
|
||||
|
||||
for (IrisGenerator i : generators) {
|
||||
d += M.lerp(lo, hi, i.getHeight(x, z, seed + 239945));
|
||||
}
|
||||
|
||||
return d / generators.length;
|
||||
return averageGeneratorHeights(generators, lo, hi, x, z, seed + 239945);
|
||||
}
|
||||
|
||||
private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) {
|
||||
@@ -742,6 +840,22 @@ public class IrisComplex implements DataProvider {
|
||||
return new NoiseBounds(boundsLow(b00), boundsHigh(b00));
|
||||
}
|
||||
|
||||
if (fz == 0D) {
|
||||
long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz);
|
||||
return new NoiseBounds(
|
||||
biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b00), boundsLow(b10), fx, fz),
|
||||
biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b00), boundsHigh(b10), fx, fz)
|
||||
);
|
||||
}
|
||||
|
||||
if (fx == 0D) {
|
||||
long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid);
|
||||
return new NoiseBounds(
|
||||
biLerp(boundsLow(b00), boundsLow(b00), boundsLow(b01), boundsLow(b01), fx, fz),
|
||||
biLerp(boundsHigh(b00), boundsHigh(b00), boundsHigh(b01), boundsHigh(b01), fx, fz)
|
||||
);
|
||||
}
|
||||
|
||||
long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz);
|
||||
long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid);
|
||||
long b11 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz + grid);
|
||||
@@ -835,15 +949,54 @@ public class IrisComplex implements DataProvider {
|
||||
);
|
||||
}
|
||||
|
||||
private double getHeight(Engine engine, IrisBiome b, double x, double z, long seed) {
|
||||
private double getHeight(Engine engine, double x, double z, long seed) {
|
||||
return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0);
|
||||
}
|
||||
|
||||
private void prepareInferredBiomes(IrisRegion region) {
|
||||
private void prepareInferredBiomes(IrisRegion region, KList<IrisRegion> preparedRegions) {
|
||||
loadInferredBiomes(region.getLandBiomes(), InferredType.LAND);
|
||||
loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE);
|
||||
loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA);
|
||||
loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE);
|
||||
preparedRegions.add(region);
|
||||
}
|
||||
|
||||
static Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compileInferredBiomeStreams(
|
||||
Iterable<IrisRegion> regions,
|
||||
BiFunction<IrisRegion, InferredType, ProceduralStream<IrisBiome>> compiler
|
||||
) {
|
||||
IdentityHashMap<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compiled = new IdentityHashMap<>();
|
||||
for (IrisRegion region : regions) {
|
||||
if (compiled.containsKey(region)) {
|
||||
continue;
|
||||
}
|
||||
EnumMap<InferredType, ProceduralStream<IrisBiome>> regionStreams = new EnumMap<>(InferredType.class);
|
||||
for (InferredType inferredType : INFERRED_BIOME_PREPARATION_ORDER) {
|
||||
regionStreams.put(inferredType, Objects.requireNonNull(compiler.apply(region, inferredType)));
|
||||
}
|
||||
compiled.put(region, Collections.unmodifiableMap(regionStreams));
|
||||
}
|
||||
return Collections.unmodifiableMap(compiled);
|
||||
}
|
||||
|
||||
static ProceduralStream<IrisBiome> preparedInferredBiomeStream(
|
||||
Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> streams,
|
||||
IrisRegion region,
|
||||
InferredType inferredType
|
||||
) {
|
||||
Map<InferredType, ProceduralStream<IrisBiome>> regionStreams = streams.get(region);
|
||||
if (regionStreams == null) {
|
||||
String regionKey = region == null || region.getLoadKey() == null || region.getLoadKey().isBlank()
|
||||
? "<unkeyed>"
|
||||
: region.getLoadKey();
|
||||
throw new IllegalStateException("Inferred-biome streams were not prepared for region '"
|
||||
+ regionKey + "'.");
|
||||
}
|
||||
ProceduralStream<IrisBiome> stream = regionStreams.get(inferredType);
|
||||
if (stream == null) {
|
||||
throw new IllegalStateException("Inferred-biome stream was not prepared for type " + inferredType + ".");
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
private KList<IrisBiome> loadInferredBiomes(KList<String> keys, InferredType type) {
|
||||
@@ -863,6 +1016,37 @@ public class IrisComplex implements DataProvider {
|
||||
generators.computeIfAbsent(cachedGenerator.getInterpolator(), (k) -> new HashSet<>()).add(cachedGenerator);
|
||||
}
|
||||
|
||||
static GeneratorGroup[] freezeGeneratorGroups(Map<IrisInterpolator, Set<IrisGenerator>> generators) {
|
||||
GeneratorGroup[] groups = new GeneratorGroup[generators.size()];
|
||||
int groupIndex = 0;
|
||||
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
|
||||
IrisGenerator[] groupGenerators = entry.getValue().toArray(new IrisGenerator[0]);
|
||||
Arrays.sort(groupGenerators, GENERATOR_ORDER);
|
||||
groups[groupIndex] = new GeneratorGroup(entry.getKey(), groupGenerators);
|
||||
groupIndex++;
|
||||
}
|
||||
Arrays.sort(groups, Comparator.comparing(GeneratorGroup::interpolator, INTERPOLATOR_ORDER));
|
||||
return groups;
|
||||
}
|
||||
|
||||
static double averageGeneratorHeights(
|
||||
IrisGenerator[] generators,
|
||||
double low,
|
||||
double high,
|
||||
double x,
|
||||
double z,
|
||||
long seed
|
||||
) {
|
||||
if (generators.length == 0) {
|
||||
return 0D;
|
||||
}
|
||||
double height = 0D;
|
||||
for (IrisGenerator generator : generators) {
|
||||
height += M.lerp(low, high, generator.getHeight(x, z, seed));
|
||||
}
|
||||
return height / generators.length;
|
||||
}
|
||||
|
||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> buildGeneratorBounds(Engine engine) {
|
||||
Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> bounds = new HashMap<>();
|
||||
KList<IrisBiome> allBiomes = new KList<>(generatorBiomes);
|
||||
@@ -1265,9 +1449,12 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
record GeneratorGroup(IrisInterpolator interpolator, IrisGenerator[] generators) {
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (riverRuntime != null) {
|
||||
riverRuntime.close();
|
||||
if (hydrologyRuntime != null) {
|
||||
hydrologyRuntime.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import art.arcane.iris.engine.mantle.MantlePass;
|
||||
import art.arcane.iris.engine.mantle.components.MantleCarvingComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleRiverHydrologyComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleHydrologyComponent;
|
||||
import art.arcane.iris.engine.mantle.components.IrisStructureComponent;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -75,6 +75,7 @@ import java.util.function.Supplier;
|
||||
@EqualsAndHashCode(exclude = "engine")
|
||||
@ToString(exclude = "engine")
|
||||
public class IrisEngineMantle implements EngineMantle {
|
||||
public static final String STORAGE_FOLDER_NAME = "mantle-hydrology";
|
||||
private static final boolean BUKKIT_PRESENT = IrisMatterSupport.isBukkitPresent();
|
||||
private final Engine engine;
|
||||
private final Mantle<Matter> mantle;
|
||||
@@ -90,7 +91,7 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
this.mantle = createMantle(engine);
|
||||
components = new KMap<>();
|
||||
registerComponent(new MantleCarvingComponent(this));
|
||||
registerComponent(new MantleRiverHydrologyComponent(this));
|
||||
registerComponent(new MantleHydrologyComponent(this));
|
||||
object = new MantleObjectComponent(this);
|
||||
registerComponent(object);
|
||||
registerComponent(new MantleFloatingObjectComponent(this));
|
||||
@@ -126,7 +127,7 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
List<MantleComponent> pass = passes.get(i);
|
||||
int passBlockRadius = pass.stream()
|
||||
.filter(MantleComponent::isEnabled)
|
||||
.mapToInt(MantleComponent::getRadius)
|
||||
.mapToInt(MantleComponent::getOutputRadius)
|
||||
.max()
|
||||
.orElse(0);
|
||||
int passInputRadius = pass.stream()
|
||||
@@ -180,19 +181,15 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
disabled.addIfMissing(ReservedFlag.CARVED);
|
||||
}
|
||||
if (disabled.contains(ReservedFlag.CARVED)
|
||||
|| !isRiverHydrologyEnabled(getDimension())) {
|
||||
|| !isHydrologyEnabled(getDimension())) {
|
||||
disabled.addIfMissing(ReservedFlag.RIVER_HYDROLOGY);
|
||||
}
|
||||
return Set.copyOf(disabled);
|
||||
});
|
||||
}
|
||||
|
||||
static boolean isRiverHydrologyEnabled(IrisDimension dimension) {
|
||||
return MantleRiverHydrologyComponent.isEnabledFor(dimension);
|
||||
}
|
||||
|
||||
static boolean isRiverCaveHydrologyEnabled(IrisDimension dimension) {
|
||||
return MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension);
|
||||
static boolean isHydrologyEnabled(IrisDimension dimension) {
|
||||
return MantleHydrologyComponent.isEnabledFor(dimension);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -202,7 +199,7 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
|
||||
private static Mantle<Matter> createMantle(Engine engine) {
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
File dataFolder = new File(engine.getWorld().worldFolder(), "mantle");
|
||||
File dataFolder = new File(engine.getWorld().worldFolder(), STORAGE_FOLDER_NAME);
|
||||
int worldHeight = engine.getTarget().getHeight();
|
||||
MantleDataAdapter<Matter> adapter = createDataAdapter(engine::getData);
|
||||
MantleHooks hooks = createHooks(EnginePanic.scoped("world " + engine.getWorld().name()));
|
||||
|
||||
@@ -14,7 +14,6 @@ import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.project.interpolation.NoiseBounds;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -112,13 +111,14 @@ public class UpperDimensionContext implements DataProvider {
|
||||
registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators);
|
||||
}
|
||||
|
||||
IrisComplex.GeneratorGroup[] generatorGroups = IrisComplex.freezeGeneratorGroups(generators);
|
||||
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
|
||||
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
|
||||
for (IrisComplex.GeneratorGroup group : generatorGroups) {
|
||||
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
|
||||
for (IrisBiome biome : allBiomes) {
|
||||
double min = 0D;
|
||||
double max = 0D;
|
||||
for (IrisGenerator gen : entry.getValue()) {
|
||||
for (IrisGenerator gen : group.generators()) {
|
||||
String key = gen.getLoadKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
@@ -128,7 +128,7 @@ public class UpperDimensionContext implements DataProvider {
|
||||
}
|
||||
interpolatorBounds.put(biome, new NoiseBounds(min, max));
|
||||
}
|
||||
generatorBounds.put(entry.getKey(), interpolatorBounds);
|
||||
generatorBounds.put(group.interpolator(), interpolatorBounds);
|
||||
}
|
||||
|
||||
ProceduralStream<Double> regionStyleStream = upperDim.getRegionStyle()
|
||||
@@ -204,10 +204,10 @@ public class UpperDimensionContext implements DataProvider {
|
||||
return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z);
|
||||
}
|
||||
double interpolatedHeight = 0;
|
||||
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
|
||||
IrisInterpolator interpolator = entry.getKey();
|
||||
Set<IrisGenerator> gens = entry.getValue();
|
||||
if (gens.isEmpty()) {
|
||||
for (IrisComplex.GeneratorGroup group : generatorGroups) {
|
||||
IrisInterpolator interpolator = group.interpolator();
|
||||
IrisGenerator[] groupGenerators = group.generators();
|
||||
if (groupGenerators.length == 0) {
|
||||
continue;
|
||||
}
|
||||
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
|
||||
@@ -223,7 +223,7 @@ public class UpperDimensionContext implements DataProvider {
|
||||
}
|
||||
double bMin = 0D;
|
||||
double bMax = 0D;
|
||||
for (IrisGenerator gen : gens) {
|
||||
for (IrisGenerator gen : groupGenerators) {
|
||||
String key = gen.getLoadKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
@@ -239,11 +239,14 @@ public class UpperDimensionContext implements DataProvider {
|
||||
});
|
||||
double hi = sampledBounds.max();
|
||||
double lo = sampledBounds.min();
|
||||
double d = 0;
|
||||
for (IrisGenerator gen : gens) {
|
||||
d += M.lerp(lo, hi, gen.getHeight(x, z, heightSeed + 239945));
|
||||
}
|
||||
interpolatedHeight += d / gens.size();
|
||||
interpolatedHeight += IrisComplex.averageGeneratorHeights(
|
||||
groupGenerators,
|
||||
lo,
|
||||
hi,
|
||||
x,
|
||||
z,
|
||||
heightSeed + 239945
|
||||
);
|
||||
}
|
||||
double proceduralHeight = Math.max(
|
||||
Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package art.arcane.iris.engine.actuator;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
final class HydrologyFluidLayerSelector {
|
||||
private HydrologyFluidLayerSelector() {
|
||||
}
|
||||
|
||||
static PlatformBlockState select(
|
||||
KList<PlatformBlockState> seaLayers,
|
||||
int depth,
|
||||
PlatformBlockState fluid,
|
||||
boolean hydrologyOwned
|
||||
) {
|
||||
if (!hydrologyOwned && seaLayers != null && seaLayers.hasIndex(depth)) {
|
||||
return seaLayers.get(depth);
|
||||
}
|
||||
return fluid;
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,10 @@ import art.arcane.iris.engine.decorator.IrisSurfaceDecorator;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||
import art.arcane.iris.engine.framework.EngineDecorator;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
@@ -52,7 +53,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
@Getter
|
||||
private final EngineDecorator seaFloorDecorator;
|
||||
@Getter
|
||||
private final EngineDecorator shoreLineDecorator;
|
||||
private final IrisShoreLineDecorator shoreLineDecorator;
|
||||
private final boolean shouldRay;
|
||||
|
||||
public IrisDecorantActuator(Engine engine) {
|
||||
@@ -66,10 +67,82 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
seaFloorDecorator = new IrisSeaFloorDecorator(getEngine());
|
||||
}
|
||||
|
||||
static boolean shouldDecorateShoreline(IrisRiverSurfaceSample sample, int height) {
|
||||
return !sample.subterranean()
|
||||
&& height == Math.round(sample.waterSurfaceY())
|
||||
&& (!sample.river().present() || sample.river().state() != RiverRouteState.DRY);
|
||||
static ShorelineDecorationMode shorelineDecorationMode(
|
||||
HydrologyColumnSample sample,
|
||||
int height,
|
||||
int fluidHeight
|
||||
) {
|
||||
HydrologyColumnLayer surfaceLayer = sample == null
|
||||
? null
|
||||
: sample.primarySurfaceLayer().orElse(null);
|
||||
if (surfaceLayer != null) {
|
||||
return surfaceLayer.shore()
|
||||
? ShorelineDecorationMode.ACCEPTED
|
||||
: ShorelineDecorationMode.NONE;
|
||||
}
|
||||
return height == fluidHeight
|
||||
? ShorelineDecorationMode.LEGACY
|
||||
: ShorelineDecorationMode.NONE;
|
||||
}
|
||||
|
||||
static boolean hasConnectedSurfaceWater(
|
||||
Hunk<PlatformBlockState> output,
|
||||
int x,
|
||||
int z,
|
||||
int height,
|
||||
int fluidHeight
|
||||
) {
|
||||
return height < fluidHeight
|
||||
&& height + 1 < output.getHeight()
|
||||
&& IrisSurfaceDecorator.hasConnectedWater(output, x, height + 1, z);
|
||||
}
|
||||
|
||||
static boolean hasConnectedWaterColumn(
|
||||
Hunk<PlatformBlockState> output,
|
||||
int x,
|
||||
int z,
|
||||
int lowerY,
|
||||
int upperY
|
||||
) {
|
||||
int boundedLowerY = Math.max(0, lowerY);
|
||||
int boundedUpperY = Math.min(output.getHeight() - 1, upperY);
|
||||
if (boundedLowerY > boundedUpperY) {
|
||||
return false;
|
||||
}
|
||||
for (int y = boundedLowerY; y <= boundedUpperY; y++) {
|
||||
if (!IrisSurfaceDecorator.hasConnectedWater(output, x, y, z)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void restoreUnsupportedAquaticPlacement(
|
||||
Hunk<PlatformBlockState> output,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
PlatformBlockState original
|
||||
) {
|
||||
PlatformBlockState placed = output.get(x, y, z);
|
||||
if (placed != original && IrisSurfaceDecorator.isAquaticPlacement(placed)) {
|
||||
output.set(x, y, z, original);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isFallingWaterfallThroat(HydrologyColumnSample sample) {
|
||||
if (sample == null) {
|
||||
return false;
|
||||
}
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if ((layer.feature().type() == HydrologyFeatureType.WATERFALL
|
||||
|| layer.feature().type() == HydrologyFeatureType.SINKHOLE)
|
||||
&& layer.channel()
|
||||
&& layer.fallingFluid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
@@ -80,7 +153,6 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
}
|
||||
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
|
||||
for (int i = 0; i < output.getWidth(); i++) {
|
||||
int height;
|
||||
int realX = Math.round(x + i);
|
||||
@@ -94,25 +166,57 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
height = context.getRoundedHeight(i, j);
|
||||
biome = context.getBiome().get(i, j);
|
||||
cave = shouldRay ? context.getCave().get(i, j) : null;
|
||||
IrisRiverSurfaceSample riverSurface = getComplex().getRiverSurfaceStream().get(realX, realZ);
|
||||
int surfaceFluidHeight = (int) Math.round(riverSurface.waterSurfaceY());
|
||||
HydrologyColumnSample hydrology = getComplex().getHydrologyRuntime() == null
|
||||
? null
|
||||
: getComplex().getHydrologyRuntime().sample(realX, realZ).orElse(null);
|
||||
if (isFallingWaterfallThroat(hydrology)) {
|
||||
continue;
|
||||
}
|
||||
int surfaceFluidHeight = (int) Math.round(
|
||||
getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
|
||||
|
||||
if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j))
|
||||
&& height + 1 < output.getHeight() && B.isFluid(output.get(i, height + 1, j))) {
|
||||
if (PREDICATE_SOLID.test(output.get(i, height, j))
|
||||
&& hasConnectedSurfaceWater(output, i, j, height, surfaceFluidHeight)) {
|
||||
int seaSurfaceY = surfaceFluidHeight + 1;
|
||||
PlatformBlockState seaSurfaceOriginal = seaSurfaceY < output.getHeight()
|
||||
? output.get(i, seaSurfaceY, j)
|
||||
: null;
|
||||
getSeaSurfaceDecorator().decorate(i, j,
|
||||
realX, Math.round(i + 1), Math.round(x + i - 1),
|
||||
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
|
||||
output, biome, surfaceFluidHeight, getEngine().getHeight());
|
||||
getSeaFloorDecorator().decorate(i, j,
|
||||
realX, realZ, output, biome, height + 1,
|
||||
surfaceFluidHeight + 1);
|
||||
if (seaSurfaceY < output.getHeight()) {
|
||||
restoreUnsupportedAquaticPlacement(
|
||||
output, i, seaSurfaceY, j, seaSurfaceOriginal);
|
||||
}
|
||||
if (hasConnectedWaterColumn(output, i, j, height + 1, surfaceFluidHeight)) {
|
||||
getSeaFloorDecorator().decorate(i, j,
|
||||
realX, realZ, output, biome, height + 1,
|
||||
surfaceFluidHeight + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDecorateShoreline(riverSurface, height)) {
|
||||
ShorelineDecorationMode shorelineMode = shorelineDecorationMode(
|
||||
hydrology,
|
||||
height,
|
||||
surfaceFluidHeight
|
||||
);
|
||||
if (shorelineMode == ShorelineDecorationMode.ACCEPTED) {
|
||||
getShoreLineDecorator().decorateAcceptedShore(
|
||||
i,
|
||||
j,
|
||||
realX,
|
||||
realZ,
|
||||
output,
|
||||
biome,
|
||||
height,
|
||||
getEngine().getHeight()
|
||||
);
|
||||
} else if (shorelineMode == ShorelineDecorationMode.LEGACY) {
|
||||
getShoreLineDecorator().decorate(i, j,
|
||||
realX, Math.round(x + i + 1), Math.round(x + i - 1),
|
||||
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
|
||||
@@ -150,4 +254,10 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
private boolean shouldRayDecorate() {
|
||||
return false; // TODO CAVES
|
||||
}
|
||||
|
||||
enum ShorelineDecorationMode {
|
||||
NONE,
|
||||
LEGACY,
|
||||
ACCEPTED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisOreGenerator;
|
||||
@@ -117,7 +119,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
}
|
||||
|
||||
int topY = Math.min(hf, chunkHeight - 1);
|
||||
PlatformBlockState fluid = complex.resolveSurfaceFluid(realX, realZ);
|
||||
HydrologyColumnSample hydrology = complex.getHydrologyRuntime() == null
|
||||
? null
|
||||
: complex.getHydrologyRuntime().sample(realX, realZ).orElse(null);
|
||||
HydrologyColumnLayer hydrologyFluid = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceFluidLayer().orElse(null);
|
||||
PlatformBlockState fluid = hydrologyFluid == null
|
||||
? complex.resolveSurfaceFluid(realX, realZ)
|
||||
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
|
||||
PlatformBlockState rock = rockCache.get(xf, zf);
|
||||
PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
|
||||
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
|
||||
@@ -159,15 +169,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
|
||||
if (i > he && i <= hf) {
|
||||
int fdepth = hf - i;
|
||||
if (fblocks == null) {
|
||||
if (hydrologyFluid == null && fblocks == null) {
|
||||
fblocks = biome.generateSeaLayers(realX, realZ, localRng, hf - he, data);
|
||||
}
|
||||
|
||||
if (fblocks.hasIndex(fdepth)) {
|
||||
h.setRaw(xf, i, zf, fblocks.get(fdepth));
|
||||
} else {
|
||||
h.setRaw(xf, i, zf, fluid);
|
||||
}
|
||||
h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select(
|
||||
fblocks,
|
||||
fdepth,
|
||||
fluid,
|
||||
hydrologyFluid != null
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,32 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
return;
|
||||
}
|
||||
|
||||
place(x, z, realX, realZ, data, biome, height, max);
|
||||
}
|
||||
|
||||
public void decorateAcceptedShore(
|
||||
int x,
|
||||
int z,
|
||||
int realX,
|
||||
int realZ,
|
||||
Hunk<PlatformBlockState> data,
|
||||
IrisBiome biome,
|
||||
int height,
|
||||
int max
|
||||
) {
|
||||
place(x, z, realX, realZ, data, biome, height, max);
|
||||
}
|
||||
|
||||
private void place(
|
||||
int x,
|
||||
int z,
|
||||
int realX,
|
||||
int realZ,
|
||||
Hunk<PlatformBlockState> data,
|
||||
IrisBiome biome,
|
||||
int height,
|
||||
int max
|
||||
) {
|
||||
RNG rng = getRNG(realX, realZ);
|
||||
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
||||
|
||||
@@ -71,6 +97,8 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisSurfaceDecorator.AquaticPlacementSnapshot aquaticSnapshot = IrisSurfaceDecorator.captureAquaticPlacement(
|
||||
decorator, getData(), data, x, z, height, max);
|
||||
if (!decorator.isStacking()) {
|
||||
int targetY = height + 1;
|
||||
if (targetY >= data.getHeight()
|
||||
@@ -80,6 +108,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
|
||||
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
|
||||
data.set(x, targetY, z, block);
|
||||
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -100,6 +129,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
|
||||
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
|
||||
data.set(x, targetY, z, block);
|
||||
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -123,5 +153,6 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
}
|
||||
data.set(x, targetY, z, block);
|
||||
}
|
||||
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
|
||||
package art.arcane.iris.engine.decorator;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDecorationPart;
|
||||
import art.arcane.iris.engine.object.IrisDecorator;
|
||||
import art.arcane.iris.engine.object.IrisProceduralBlocks;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
@@ -64,6 +66,9 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
}
|
||||
|
||||
boolean underwater = isUnderwater(inferredType, height, fluidHeight);
|
||||
if (underwater && !hasFluidAbove(data, x, height, z)) {
|
||||
return;
|
||||
}
|
||||
boolean caveSkipFluid = skipsFluid(inferredType);
|
||||
RNG rng = getRNG(realX, realZ);
|
||||
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
||||
@@ -72,6 +77,8 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
return;
|
||||
}
|
||||
|
||||
AquaticPlacementSnapshot aquaticSnapshot = captureAquaticPlacement(
|
||||
decorator, getData(), data, x, z, height, max);
|
||||
if (decorator.isStacking()) {
|
||||
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
|
||||
opts.reset();
|
||||
@@ -79,11 +86,13 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
opts.fluidHeight = fluidHeight;
|
||||
opts.caveSkipFluid = caveSkipFluid;
|
||||
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts);
|
||||
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||
return;
|
||||
}
|
||||
|
||||
DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
|
||||
data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
|
||||
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||
}
|
||||
|
||||
static boolean isUnderwater(InferredType inferredType, int height, int fluidHeight) {
|
||||
@@ -93,4 +102,148 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
static boolean skipsFluid(InferredType inferredType) {
|
||||
return inferredType == InferredType.CAVE;
|
||||
}
|
||||
|
||||
static boolean hasFluidAbove(Hunk<PlatformBlockState> data, int x, int height, int z) {
|
||||
return height + 1 < data.getHeight() && data.get(x, height + 1, z).isFluid();
|
||||
}
|
||||
|
||||
public static boolean isAquaticPlacement(PlatformBlockState state) {
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
if (state.isWaterLogged()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String material = IrisProceduralBlocks.materialKey(state);
|
||||
if (material.equals("minecraft:seagrass")
|
||||
|| material.equals("minecraft:tall_seagrass")
|
||||
|| material.equals("minecraft:kelp")
|
||||
|| material.equals("minecraft:kelp_plant")
|
||||
|| material.equals("minecraft:sea_pickle")) {
|
||||
return true;
|
||||
}
|
||||
return material.startsWith("minecraft:")
|
||||
&& material.contains("_coral")
|
||||
&& !material.contains(":dead_");
|
||||
}
|
||||
|
||||
public static boolean hasConnectedWater(Hunk<PlatformBlockState> data, int x, int y, int z) {
|
||||
if (y < 0 || y >= data.getHeight() || !isWater(data.get(x, y, z))) {
|
||||
return false;
|
||||
}
|
||||
return isWaterNeighbor(data, x - 1, y, z)
|
||||
|| isWaterNeighbor(data, x + 1, y, z)
|
||||
|| isWaterNeighbor(data, x, y, z - 1)
|
||||
|| isWaterNeighbor(data, x, y, z + 1);
|
||||
}
|
||||
|
||||
static AquaticPlacementSnapshot captureAquaticPlacement(
|
||||
IrisDecorator decorator,
|
||||
IrisData irisData,
|
||||
Hunk<PlatformBlockState> data,
|
||||
int x,
|
||||
int z,
|
||||
int height,
|
||||
int max
|
||||
) {
|
||||
if (!mayPlaceAquatic(decorator, irisData)) {
|
||||
return AquaticPlacementSnapshot.EMPTY;
|
||||
}
|
||||
|
||||
int highestOffset = decorator.isStacking()
|
||||
? maximumStackHeight(decorator, max)
|
||||
: 2;
|
||||
int lowerY = Math.max(0, height);
|
||||
int upperY = Math.min(data.getHeight() - 1, height + highestOffset);
|
||||
if (lowerY > upperY) {
|
||||
return AquaticPlacementSnapshot.EMPTY;
|
||||
}
|
||||
|
||||
PlatformBlockState[] originals = new PlatformBlockState[upperY - lowerY + 1];
|
||||
boolean[] connectedWater = new boolean[originals.length];
|
||||
for (int i = 0; i < originals.length; i++) {
|
||||
int y = lowerY + i;
|
||||
originals[i] = data.get(x, y, z);
|
||||
connectedWater[i] = hasConnectedWater(data, x, y, z);
|
||||
}
|
||||
return new AquaticPlacementSnapshot(lowerY, originals, connectedWater);
|
||||
}
|
||||
|
||||
private static boolean mayPlaceAquatic(IrisDecorator decorator, IrisData irisData) {
|
||||
PlatformBlockState[] palette = decorator.getBlockDataArray(irisData);
|
||||
if (palette != null) {
|
||||
for (PlatformBlockState state : palette) {
|
||||
if (isAquaticPlacement(state)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
PlatformBlockState[] topPalette = decorator.getBlockDataTopsArray(irisData);
|
||||
if (topPalette != null) {
|
||||
for (PlatformBlockState state : topPalette) {
|
||||
if (isAquaticPlacement(state)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return decorator.getForceBlock() != null
|
||||
&& isAquaticPlacement(decorator.getForceBlock().getBlockData(irisData));
|
||||
}
|
||||
|
||||
private static int maximumStackHeight(IrisDecorator decorator, int max) {
|
||||
if (decorator.isScaleStack()) {
|
||||
return Math.max(1, decorator.getAbsoluteMaxStack());
|
||||
}
|
||||
return Math.max(1, Math.min(Math.max(1, max), decorator.getStackMax()));
|
||||
}
|
||||
|
||||
private static boolean isWaterNeighbor(Hunk<PlatformBlockState> data, int x, int y, int z) {
|
||||
if (x < 0 || x >= data.getWidth() || z < 0 || z >= data.getDepth()) {
|
||||
return false;
|
||||
}
|
||||
return isWater(data.get(x, y, z));
|
||||
}
|
||||
|
||||
private static boolean isWater(PlatformBlockState state) {
|
||||
return state != null && state.isWater();
|
||||
}
|
||||
|
||||
static final class AquaticPlacementSnapshot {
|
||||
private static final AquaticPlacementSnapshot EMPTY = new AquaticPlacementSnapshot(
|
||||
0, new PlatformBlockState[0], new boolean[0]);
|
||||
|
||||
private final int lowerY;
|
||||
private final PlatformBlockState[] originals;
|
||||
private final boolean[] connectedWater;
|
||||
|
||||
private AquaticPlacementSnapshot(
|
||||
int lowerY,
|
||||
PlatformBlockState[] originals,
|
||||
boolean[] connectedWater
|
||||
) {
|
||||
this.lowerY = lowerY;
|
||||
this.originals = originals;
|
||||
this.connectedWater = connectedWater;
|
||||
}
|
||||
|
||||
void restoreIfUnsupported(Hunk<PlatformBlockState> data, int x, int z) {
|
||||
if (!containsUnsupportedAquatic(data, x, z)) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < originals.length; i++) {
|
||||
data.set(x, lowerY + i, z, originals[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsUnsupportedAquatic(Hunk<PlatformBlockState> data, int x, int z) {
|
||||
for (int i = 0; i < originals.length; i++) {
|
||||
PlatformBlockState placed = data.get(x, lowerY + i, z);
|
||||
if (placed != originals[i] && isAquaticPlacement(placed) && !connectedWater[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveStorage;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -246,7 +246,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
|
||||
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(
|
||||
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(
|
||||
getMantle().getMantle(), x, y, z);
|
||||
if (hydrology != null && !hydrology.floodedBiomeKey().isEmpty()) {
|
||||
IrisBiome biome = getData().getBiomeLoader().load(hydrology.floodedBiomeKey());
|
||||
@@ -455,6 +455,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
default PlacedObject getObjectPlacement(int x, int y, int z, MantleChunk<Matter> chunk) {
|
||||
String objectAt = chunk.get(x & 15, y, z & 15, String.class);
|
||||
return resolveObjectPlacementMarker(x, z, objectAt);
|
||||
}
|
||||
|
||||
default PlacedObject resolveObjectPlacementMarker(int x, int z, @Nullable String objectAt) {
|
||||
if (objectAt == null || objectAt.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
@@ -562,7 +566,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return;
|
||||
}
|
||||
if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) {
|
||||
getMantle().cleanupChunk(x, z);
|
||||
getMantle().cleanupChunksCoveredBy(
|
||||
x,
|
||||
z,
|
||||
false,
|
||||
EngineMantle.ChunkCleanupCallback.NONE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.mantle.MantleComponent;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
public interface EnginePlatformHooks {
|
||||
@@ -70,4 +71,8 @@ public interface EnginePlatformHooks {
|
||||
default boolean shouldBypassMantleStages(Engine engine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean shouldGenerateMantleComponent(Engine engine, MantleComponent component) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +328,11 @@ public final class HintedLocator<T> implements Locator<T> {
|
||||
}
|
||||
|
||||
if (!surfaceRegions.isEmpty()) {
|
||||
return SearchPlan.of(surfaceCoarse(complex, surfaceRegions, landHosts, seaHosts, shoreHosts), BIOME_STRIDE_CHUNKS, null);
|
||||
return SearchPlan.of(
|
||||
surfaceCoarse(complex, surfaceRegions, landHosts, seaHosts, shoreHosts),
|
||||
BIOME_STRIDE_CHUNKS,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (!caveRegions.isEmpty()) {
|
||||
@@ -341,6 +345,11 @@ public final class HintedLocator<T> implements Locator<T> {
|
||||
return SearchPlan.of(coarse, BIOME_STRIDE_CHUNKS, caveExact);
|
||||
}
|
||||
|
||||
for (IrisBiome reachableBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||
if (reachableBiome != null && biomeKey.equals(reachableBiome.getLoadKey())) {
|
||||
return SearchPlan.unpruned();
|
||||
}
|
||||
}
|
||||
return SearchPlan.impossible();
|
||||
}
|
||||
|
||||
@@ -576,6 +585,36 @@ public final class HintedLocator<T> implements Locator<T> {
|
||||
return false;
|
||||
}
|
||||
|
||||
private KList<IrisBiome> closure(String rootKey) {
|
||||
KList<IrisBiome> biomes = new KList<>();
|
||||
if (rootKey == null) {
|
||||
return biomes;
|
||||
}
|
||||
|
||||
KSet<String> visited = new KSet<>();
|
||||
ArrayDeque<String> queue = new ArrayDeque<>();
|
||||
queue.add(rootKey);
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
String key = queue.poll();
|
||||
if (!visited.add(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IrisBiome biome = load(key);
|
||||
if (biome == null) {
|
||||
continue;
|
||||
}
|
||||
biomes.add(biome);
|
||||
for (String child : biome.getChildren()) {
|
||||
if (child != null && !visited.contains(child)) {
|
||||
queue.add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private boolean closurePlacesObject(String rootKey, String objectKey) {
|
||||
if (rootKey == null || objectKey == null) {
|
||||
return false;
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.framework;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.nms.container.BlockPos;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
@@ -53,10 +54,22 @@ public interface Locator<T> {
|
||||
}
|
||||
|
||||
static Locator<IrisBiome> surfaceBiome(String loadKey) {
|
||||
Locator<IrisBiome> exact = (e, c) -> e.getSurfaceBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
|
||||
Locator<IrisBiome> exact = (e, c) -> chunkContainsSurfaceBiome(e, c, loadKey);
|
||||
return new HintedLocator<>(exact, (engine) -> HintedLocator.biomePlan(engine, loadKey));
|
||||
}
|
||||
|
||||
static boolean chunkContainsSurfaceBiome(Engine engine, Position2 chunk, String loadKey) {
|
||||
int minimumX = chunk.getX() << 4;
|
||||
int minimumZ = chunk.getZ() << 4;
|
||||
IrisBiome center = engine.getSurfaceBiome(minimumX + 8, minimumZ + 8);
|
||||
if (center != null && loadKey.equals(center.getLoadKey())) {
|
||||
return true;
|
||||
}
|
||||
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
|
||||
return hydrology != null
|
||||
&& hydrology.hasAcceptedSurfaceBiomeInChunk(loadKey, chunk.getX(), chunk.getZ());
|
||||
}
|
||||
|
||||
static Locator<art.arcane.iris.engine.object.IrisStructure> structure(String key) {
|
||||
return (e, c) -> IrisStructureLocator.startsInChunk(e, key, c.getX(), c.getZ());
|
||||
}
|
||||
|
||||
+7
-7
@@ -3,8 +3,8 @@ package art.arcane.iris.engine.framework;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisStructureAnchorMode;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveStorage;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
|
||||
@@ -288,7 +288,7 @@ public final class StructureCaveAnchorResolver {
|
||||
int mantleY,
|
||||
int blockZ
|
||||
) {
|
||||
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||
HydrologyCaveCell hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||
MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ);
|
||||
return acceptsAnchorFluid(
|
||||
placement.isUnderwater(),
|
||||
@@ -310,7 +310,7 @@ public final class StructureCaveAnchorResolver {
|
||||
static boolean acceptsAnchorFluid(
|
||||
boolean underwater,
|
||||
MatterCavern cavern,
|
||||
RiverCaveHydrology hydrology,
|
||||
HydrologyCaveCell hydrology,
|
||||
int mantleY,
|
||||
int defaultLavaHeight
|
||||
) {
|
||||
@@ -332,12 +332,12 @@ public final class StructureCaveAnchorResolver {
|
||||
private static MatterCavern cavernAt(Engine engine, int blockX, int mantleY, int blockZ) {
|
||||
MatterCavern baseline = engine.getMantle().getMantle()
|
||||
.get(blockX, mantleY, blockZ, MatterCavern.class);
|
||||
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||
HydrologyCaveCell hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||
return hydrology == null ? baseline : hydrology.asCavern();
|
||||
}
|
||||
|
||||
private static RiverCaveHydrology hydrologyAt(Engine engine, int blockX, int mantleY, int blockZ) {
|
||||
return RiverCaveHydrologyStorage.getIfPresent(
|
||||
private static HydrologyCaveCell hydrologyAt(Engine engine, int blockX, int mantleY, int blockZ) {
|
||||
return HydrologyCaveStorage.getIfPresent(
|
||||
engine.getMantle().getMantle(), blockX, mantleY, blockZ);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,21 +20,29 @@ package art.arcane.iris.engine.framework.render;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateKind;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticCandidate;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticRenderSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyRenderSample;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.BooleanSupplier;
|
||||
@@ -43,11 +51,22 @@ public final class IrisRenderer {
|
||||
private static final int BLUE = new Color(45, 91, 156).getRGB();
|
||||
private static final int YELLOW = new Color(211, 164, 67).getRGB();
|
||||
private static final int GREEN = new Color(78, 137, 83).getRGB();
|
||||
private static final int HEADWATER = new Color(99, 184, 230).getRGB();
|
||||
private static final int HEADWATER_DIRECTION = new Color(13, 38, 55).getRGB();
|
||||
private static final int RIVER_CHANNEL = new Color(48, 112, 190).getRGB();
|
||||
private static final int RIVER_RIFFLE = new Color(96, 166, 214).getRGB();
|
||||
private static final int RIVER_CASCADE = new Color(135, 204, 232).getRGB();
|
||||
private static final int SUBTERRANEAN_CHANNEL = new Color(102, 86, 196).getRGB();
|
||||
private static final int RIDGE_TUNNEL = new Color(120, 99, 174).getRGB();
|
||||
private static final int COASTAL_GROTTO = new Color(51, 174, 174).getRGB();
|
||||
private static final int INLAND_GROTTO = new Color(116, 76, 150).getRGB();
|
||||
private static final int RIVER_SINKHOLE = new Color(176, 146, 232).getRGB();
|
||||
private static final int RIVER_MOUTH = new Color(54, 164, 205).getRGB();
|
||||
private static final int RIVER_BANK = new Color(92, 146, 78).getRGB();
|
||||
private static final int DRY_CHANNEL = new Color(171, 128, 68).getRGB();
|
||||
private static final int DRY_BANK = new Color(132, 105, 62).getRGB();
|
||||
private static final int RIVER_WATERFALL = new Color(190, 236, 255).getRGB();
|
||||
private static final int CONFIRMED_DEEP_LAVA = new Color(194, 50, 22).getRGB();
|
||||
private static final int PROJECTED_SOURCE = new Color(225, 75, 178).getRGB();
|
||||
private static final int PROJECTED_OUTLET = new Color(235, 145, 54).getRGB();
|
||||
private static final int PROJECTED_DEEP_FLUID = new Color(175, 70, 118).getRGB();
|
||||
private static final int NO_RIVER = new Color(28, 31, 38).getRGB();
|
||||
private static final int DEEP_WATER = new Color(20, 48, 92).getRGB();
|
||||
private static final int SHALLOW_WATER = new Color(50, 112, 154).getRGB();
|
||||
@@ -150,15 +169,53 @@ public final class IrisRenderer {
|
||||
return image;
|
||||
}
|
||||
|
||||
public static int riverColor(RiverSection section) {
|
||||
Objects.requireNonNull(section, "section");
|
||||
return switch (section) {
|
||||
case CHANNEL -> RIVER_CHANNEL;
|
||||
public static int waterfallColor() {
|
||||
return RIVER_WATERFALL;
|
||||
}
|
||||
|
||||
public static int surfaceWaterColor() {
|
||||
return RIVER_CHANNEL;
|
||||
}
|
||||
|
||||
public static int subterraneanColor() {
|
||||
return SUBTERRANEAN_CHANNEL;
|
||||
}
|
||||
|
||||
public static int confirmedDeepLavaColor() {
|
||||
return CONFIRMED_DEEP_LAVA;
|
||||
}
|
||||
|
||||
public static int hydrologyFeatureColor(HydrologyFeatureType type) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
return switch (type) {
|
||||
case SURFACE_POOL -> RIVER_CHANNEL;
|
||||
case RIFFLE -> RIVER_RIFFLE;
|
||||
case CASCADE -> RIVER_CASCADE;
|
||||
case WATERFALL -> RIVER_WATERFALL;
|
||||
case SINKHOLE -> RIVER_SINKHOLE;
|
||||
case RIDGE_BORE -> RIDGE_TUNNEL;
|
||||
case UNDERGROUND_POOL, UNDERGROUND_DROP -> SUBTERRANEAN_CHANNEL;
|
||||
case COASTAL_GROTTO -> COASTAL_GROTTO;
|
||||
case INLAND_GROTTO -> INLAND_GROTTO;
|
||||
case MOUTH -> RIVER_MOUTH;
|
||||
case BANK -> RIVER_BANK;
|
||||
case DRY_CHANNEL -> DRY_CHANNEL;
|
||||
case DRY_BANK -> DRY_BANK;
|
||||
case NONE -> NO_RIVER;
|
||||
case DEEP_POOL, DEEP_CHANNEL -> CONFIRMED_DEEP_LAVA;
|
||||
};
|
||||
}
|
||||
|
||||
public static int headwaterColor() {
|
||||
return HEADWATER;
|
||||
}
|
||||
|
||||
public static int headwaterDirectionColor() {
|
||||
return HEADWATER_DIRECTION;
|
||||
}
|
||||
|
||||
public static int hydrologyDiagnosticColor(HydrologyCandidateKind kind) {
|
||||
Objects.requireNonNull(kind, "kind");
|
||||
return switch (kind) {
|
||||
case SOURCE -> PROJECTED_SOURCE;
|
||||
case OUTLET -> PROJECTED_OUTLET;
|
||||
case DEEP_FLUID -> PROJECTED_DEEP_FLUID;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -277,10 +334,11 @@ public final class IrisRenderer {
|
||||
BooleanSupplier cancelled,
|
||||
boolean composite
|
||||
) {
|
||||
IrisRiverRuntime runtime = complex.getRiverRuntime();
|
||||
IrisHydrologyRuntime runtime = complex.getHydrologyRuntime();
|
||||
if (runtime == null) {
|
||||
return;
|
||||
}
|
||||
Map<Long, HydrologyFeatureRef> headwaters = new LinkedHashMap<>();
|
||||
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
|
||||
int blockPixels = Integer.highestOneBit(maximumPixels);
|
||||
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
|
||||
@@ -295,6 +353,8 @@ public final class IrisRenderer {
|
||||
runtime,
|
||||
cancelled,
|
||||
composite,
|
||||
!composite,
|
||||
headwaters,
|
||||
pixelX,
|
||||
pixelZ,
|
||||
Math.min(blockPixels, resolution - pixelX),
|
||||
@@ -302,6 +362,7 @@ public final class IrisRenderer {
|
||||
);
|
||||
}
|
||||
}
|
||||
renderHeadwaterDirections(pixels, resolution, startX, startZ, step, headwaters);
|
||||
}
|
||||
|
||||
private static void renderRiverBlock(
|
||||
@@ -310,58 +371,163 @@ public final class IrisRenderer {
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
IrisRiverRuntime runtime,
|
||||
IrisHydrologyRuntime runtime,
|
||||
BooleanSupplier cancelled,
|
||||
boolean composite,
|
||||
boolean diagnostics,
|
||||
Map<Long, HydrologyFeatureRef> headwaters,
|
||||
int pixelX,
|
||||
int pixelZ,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
checkCancelled(cancelled);
|
||||
RiverSample sample = runtime.sampleFootprint(
|
||||
HydrologyRenderSample sample = runtime.sampleRenderFootprint(
|
||||
startX + pixelX * step,
|
||||
startZ + pixelZ * step,
|
||||
startX + (pixelX + width) * step,
|
||||
startZ + (pixelZ + height) * step
|
||||
);
|
||||
if (!sample.present()) {
|
||||
HydrologyDiagnosticRenderSample diagnosticSample = diagnostics
|
||||
? runtime.sampleDiagnosticFootprint(
|
||||
startX + pixelX * step,
|
||||
startZ + pixelZ * step,
|
||||
startX + (pixelX + width) * step,
|
||||
startZ + (pixelZ + height) * step
|
||||
)
|
||||
: null;
|
||||
collectHeadwaters(sample, headwaters);
|
||||
if (!sample.present() && (diagnosticSample == null || !diagnosticSample.present())) {
|
||||
return;
|
||||
}
|
||||
if (width == 1 && height == 1) {
|
||||
int index = pixelZ * resolution + pixelX;
|
||||
int color = riverColor(sample.section());
|
||||
pixels[index] = composite ? riverCompositeColor(pixels[index], sample.section(), color) : color;
|
||||
int color = sample.present()
|
||||
? hydrologyColor(sample)
|
||||
: hydrologyDiagnosticColor(diagnosticSample);
|
||||
pixels[index] = composite
|
||||
? blend(pixels[index], color, 0.72D)
|
||||
: color;
|
||||
return;
|
||||
}
|
||||
int leftWidth = Math.max(1, width / 2);
|
||||
int rightWidth = width - leftWidth;
|
||||
int topHeight = Math.max(1, height / 2);
|
||||
int bottomHeight = height - topHeight;
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, diagnostics,
|
||||
headwaters,
|
||||
pixelX, pixelZ, leftWidth, topHeight);
|
||||
if (rightWidth > 0) {
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, diagnostics,
|
||||
headwaters,
|
||||
pixelX + leftWidth, pixelZ, rightWidth, topHeight);
|
||||
}
|
||||
if (bottomHeight > 0) {
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, diagnostics,
|
||||
headwaters,
|
||||
pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
|
||||
if (rightWidth > 0) {
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, diagnostics,
|
||||
headwaters,
|
||||
pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int riverCompositeColor(int base, RiverSection section, int river) {
|
||||
return switch (section) {
|
||||
case CHANNEL, MOUTH -> river;
|
||||
case BANK -> blend(base, river, 0.68D);
|
||||
case DRY_CHANNEL -> blend(base, river, 0.88D);
|
||||
case DRY_BANK -> blend(base, river, 0.58D);
|
||||
case NONE -> base;
|
||||
};
|
||||
private static void collectHeadwaters(
|
||||
HydrologyRenderSample sample,
|
||||
Map<Long, HydrologyFeatureRef> headwaters
|
||||
) {
|
||||
for (HydrologyFeatureRef feature : sample.features()) {
|
||||
if (!feature.source()) {
|
||||
continue;
|
||||
}
|
||||
HydrologyFeatureRef existing = headwaters.putIfAbsent(feature.id(), feature);
|
||||
if (existing != null && !existing.equals(feature)) {
|
||||
throw new IllegalStateException("Hydrology headwater feature id collision in renderer footprint.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderHeadwaterDirections(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
Map<Long, HydrologyFeatureRef> headwaters
|
||||
) {
|
||||
if (resolution < 5 || headwaters.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ArrayList<HydrologyFeatureRef> ordered = new ArrayList<>(headwaters.values());
|
||||
ordered.sort(Comparator.comparingLong(HydrologyFeatureRef::id));
|
||||
for (HydrologyFeatureRef feature : ordered) {
|
||||
int pixelX = worldToPixel(feature.x(), startX, step, resolution);
|
||||
int pixelZ = worldToPixel(feature.z(), startZ, step, resolution);
|
||||
drawHeadwaterDirection(
|
||||
pixels,
|
||||
resolution,
|
||||
pixelX,
|
||||
pixelZ,
|
||||
feature.flowDeltaX(),
|
||||
feature.flowDeltaZ()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static int worldToPixel(int coordinate, double start, double step, int resolution) {
|
||||
int pixel = (int) StrictMath.floor((coordinate + 0.5D - start) / step);
|
||||
int markerRadius = 2;
|
||||
return Math.max(markerRadius, Math.min(resolution - markerRadius - 1, pixel));
|
||||
}
|
||||
|
||||
private static void drawHeadwaterDirection(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
int centerX,
|
||||
int centerZ,
|
||||
int flowX,
|
||||
int flowZ
|
||||
) {
|
||||
int markerRadius = Math.min(2, (resolution - 1) / 2);
|
||||
for (int deltaZ = -markerRadius; deltaZ <= markerRadius; deltaZ++) {
|
||||
for (int deltaX = -markerRadius; deltaX <= markerRadius; deltaX++) {
|
||||
setPixel(pixels, resolution, centerX + deltaX, centerZ + deltaZ, HEADWATER);
|
||||
}
|
||||
}
|
||||
if (flowX == 0 && flowZ == 0) {
|
||||
setPixel(pixels, resolution, centerX, centerZ, HEADWATER_DIRECTION);
|
||||
return;
|
||||
}
|
||||
setPixel(pixels, resolution, centerX - flowX * 2, centerZ - flowZ * 2, HEADWATER_DIRECTION);
|
||||
setPixel(pixels, resolution, centerX - flowX, centerZ - flowZ, HEADWATER_DIRECTION);
|
||||
setPixel(pixels, resolution, centerX, centerZ, HEADWATER_DIRECTION);
|
||||
setPixel(pixels, resolution, centerX + flowX, centerZ + flowZ, HEADWATER_DIRECTION);
|
||||
int tipX = centerX + flowX * 2;
|
||||
int tipZ = centerZ + flowZ * 2;
|
||||
setPixel(pixels, resolution, tipX, tipZ, HEADWATER_DIRECTION);
|
||||
setPixel(
|
||||
pixels,
|
||||
resolution,
|
||||
tipX - flowX - flowZ,
|
||||
tipZ - flowZ + flowX,
|
||||
HEADWATER_DIRECTION
|
||||
);
|
||||
setPixel(
|
||||
pixels,
|
||||
resolution,
|
||||
tipX - flowX + flowZ,
|
||||
tipZ - flowZ - flowX,
|
||||
HEADWATER_DIRECTION
|
||||
);
|
||||
}
|
||||
|
||||
private static void setPixel(int[] pixels, int resolution, int x, int z, int color) {
|
||||
if (x < 0 || x >= resolution || z < 0 || z >= resolution) {
|
||||
return;
|
||||
}
|
||||
pixels[z * resolution + x] = color;
|
||||
}
|
||||
|
||||
private PixelShader biomeShader(ProceduralStream<IrisBiome> stream, RenderType currentType) {
|
||||
@@ -399,19 +565,47 @@ public final class IrisRenderer {
|
||||
}
|
||||
|
||||
private int riverColor(IrisComplex complex, double x, double z, double step) {
|
||||
IrisRiverRuntime runtime = complex.getRiverRuntime();
|
||||
IrisHydrologyRuntime runtime = complex.getHydrologyRuntime();
|
||||
if (runtime == null) {
|
||||
return riverColor(RiverSection.NONE);
|
||||
return NO_RIVER;
|
||||
}
|
||||
double endX = x + step;
|
||||
double endZ = z + step;
|
||||
RiverSample sample = runtime.sampleFootprint(
|
||||
HydrologyRenderSample sample = runtime.sampleRenderFootprint(
|
||||
StrictMath.min(x, endX),
|
||||
StrictMath.min(z, endZ),
|
||||
StrictMath.max(x, endX),
|
||||
StrictMath.max(z, endZ)
|
||||
);
|
||||
return riverColor(sample.section());
|
||||
if (sample.present()) {
|
||||
return hydrologyColor(sample);
|
||||
}
|
||||
return hydrologyDiagnosticColor(runtime.sampleDiagnosticFootprint(
|
||||
StrictMath.min(x, endX),
|
||||
StrictMath.min(z, endZ),
|
||||
StrictMath.max(x, endX),
|
||||
StrictMath.max(z, endZ)
|
||||
));
|
||||
}
|
||||
|
||||
static int hydrologyColor(HydrologyRenderSample sample) {
|
||||
Objects.requireNonNull(sample, "sample");
|
||||
HydrologyFeatureRef feature = sample.primaryFeature().orElse(null);
|
||||
if (feature == null) {
|
||||
return NO_RIVER;
|
||||
}
|
||||
if (feature.source()) {
|
||||
return HEADWATER;
|
||||
}
|
||||
return hydrologyFeatureColor(feature.type());
|
||||
}
|
||||
|
||||
static int hydrologyDiagnosticColor(HydrologyDiagnosticRenderSample sample) {
|
||||
Objects.requireNonNull(sample, "sample");
|
||||
HydrologyDiagnosticCandidate candidate = sample.candidates().isEmpty()
|
||||
? null
|
||||
: sample.candidates().getFirst();
|
||||
return candidate == null ? NO_RIVER : hydrologyDiagnosticColor(candidate.kind());
|
||||
}
|
||||
|
||||
private int continentColor(double x, double z) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record DrainageEdge(
|
||||
long id,
|
||||
long upstreamNodeId,
|
||||
long downstreamNodeId,
|
||||
long outletId,
|
||||
double cost,
|
||||
int contributingSurfaceSources,
|
||||
int contributingUndergroundSources,
|
||||
List<HydrologyPoint> centerline
|
||||
) {
|
||||
public DrainageEdge {
|
||||
if (upstreamNodeId == downstreamNodeId) {
|
||||
throw new IllegalArgumentException("A drainage edge cannot point to the same node.");
|
||||
}
|
||||
if (!Double.isFinite(cost) || cost <= 0D) {
|
||||
throw new IllegalArgumentException("Drainage edge cost must be finite and positive.");
|
||||
}
|
||||
if (contributingSurfaceSources < 0 || contributingUndergroundSources < 0) {
|
||||
throw new IllegalArgumentException("Contributing source counts cannot be negative.");
|
||||
}
|
||||
Objects.requireNonNull(centerline, "centerline");
|
||||
centerline = List.copyOf(centerline);
|
||||
if (centerline.size() < 2) {
|
||||
throw new IllegalArgumentException("A drainage edge requires at least two centerline points.");
|
||||
}
|
||||
}
|
||||
|
||||
public int totalContributingSources() {
|
||||
return contributingSurfaceSources + contributingUndergroundSources;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record DrainageNode(
|
||||
long id,
|
||||
int x,
|
||||
int z,
|
||||
HydrologyTerrainSample terrain,
|
||||
double potential,
|
||||
long outletId
|
||||
) {
|
||||
public DrainageNode {
|
||||
Objects.requireNonNull(terrain, "terrain");
|
||||
if (!Double.isFinite(potential) || potential < 0D) {
|
||||
throw new IllegalArgumentException("Drainage potential must be finite and non-negative.");
|
||||
}
|
||||
if (terrain.ocean()) {
|
||||
throw new IllegalArgumentException("Drainage nodes must remain landward of the first ocean column.");
|
||||
}
|
||||
}
|
||||
|
||||
public HydrologyPoint naturalPoint() {
|
||||
return new HydrologyPoint(x, terrain.naturalHeight(), z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydraulicSegment(
|
||||
long id,
|
||||
long courseId,
|
||||
HydrologyFeatureType type,
|
||||
int upstreamHeadY,
|
||||
int downstreamHeadY,
|
||||
int width,
|
||||
int depth,
|
||||
boolean fallingFluid,
|
||||
boolean receivingPool,
|
||||
List<HydrologyPoint> centerline
|
||||
) {
|
||||
public HydraulicSegment {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(centerline, "centerline");
|
||||
centerline = List.copyOf(centerline);
|
||||
if (centerline.isEmpty()) {
|
||||
throw new IllegalArgumentException("A hydraulic segment requires a centerline.");
|
||||
}
|
||||
if (upstreamHeadY < downstreamHeadY) {
|
||||
throw new IllegalArgumentException("Hydraulic heads cannot rise downstream.");
|
||||
}
|
||||
if (width <= 0 || depth <= 0) {
|
||||
throw new IllegalArgumentException("Hydraulic segment width and depth must be positive.");
|
||||
}
|
||||
int drop = upstreamHeadY - downstreamHeadY;
|
||||
if (fallingFluid && drop == 0) {
|
||||
throw new IllegalArgumentException("Falling fluid requires a positive head transition.");
|
||||
}
|
||||
if (drop > 0 && !type.isDrop()) {
|
||||
throw new IllegalArgumentException("A positive head transition requires a drop segment type.");
|
||||
}
|
||||
if (drop == 0 && type.isDrop()) {
|
||||
throw new IllegalArgumentException("A drop segment type requires a positive head transition.");
|
||||
}
|
||||
if (drop > 0 && !fallingFluid) {
|
||||
if (centerline.getFirst().y() != upstreamHeadY || centerline.getLast().y() != downstreamHeadY) {
|
||||
throw new IllegalArgumentException("A graded head transition must span its declared heads.");
|
||||
}
|
||||
int previousHead = upstreamHeadY;
|
||||
for (HydrologyPoint point : centerline) {
|
||||
if (point.y() > previousHead || point.y() < downstreamHeadY) {
|
||||
throw new IllegalArgumentException("A graded head transition must descend monotonically.");
|
||||
}
|
||||
previousHead = point.y();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int drop() {
|
||||
return upstreamHeadY - downstreamHeadY;
|
||||
}
|
||||
|
||||
public HydrologyPoint start() {
|
||||
return centerline.getFirst();
|
||||
}
|
||||
|
||||
public HydrologyPoint end() {
|
||||
return centerline.getLast();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public enum HydrologyCandidateKind {
|
||||
SOURCE,
|
||||
OUTLET,
|
||||
DEEP_FLUID
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public enum HydrologyCandidateRejection {
|
||||
NO_LEGAL_OUTLET,
|
||||
NO_DRAINAGE_PATH,
|
||||
ROUTE_LIMIT,
|
||||
COURSE_TOO_SHORT,
|
||||
SOURCE_QUOTA,
|
||||
SOURCE_SPACING,
|
||||
OUTLET_LIMIT,
|
||||
RIDGE_CONTAINMENT,
|
||||
OUTLET_LEVEL,
|
||||
CAVE_CONTAINMENT,
|
||||
VOLUME_LIMIT,
|
||||
SURFACE_HEAD_RANGE,
|
||||
SURFACE_HEAD_DISTRIBUTION,
|
||||
SURFACE_RIDGE_TUNNEL_LIMIT,
|
||||
SURFACE_EXPOSURE,
|
||||
SURFACE_DROP_UNSUPPORTED,
|
||||
SURFACE_CORRIDOR_UNSUPPORTED,
|
||||
SURFACE_TRANSITION_UNSUPPORTED,
|
||||
SURFACE_SEGMENT_UNSUPPORTED,
|
||||
SURFACE_SINKHOLE_CLEARANCE,
|
||||
SURFACE_SHAPE_UNSUPPORTED
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+13
@@ -0,0 +1,13 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HydrologyCaveVoxelViewFactory {
|
||||
CaveVoxelView create(PlannedSurface plannedSurface);
|
||||
|
||||
@FunctionalInterface
|
||||
interface PlannedSurface {
|
||||
int resolve(int x, int z, int naturalHeight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydrologyColumnLayer(
|
||||
HydrologyFeatureRef feature,
|
||||
int bedY,
|
||||
int fluidHeadY,
|
||||
int ceilingY,
|
||||
boolean channel,
|
||||
boolean shore,
|
||||
boolean grading,
|
||||
boolean connectedFluid,
|
||||
boolean fallingFluid,
|
||||
boolean receivingPool,
|
||||
boolean terrainOwned,
|
||||
boolean fluidOwned,
|
||||
boolean oceanApron,
|
||||
String profileKey,
|
||||
String surfaceBiomeKey,
|
||||
String mouthBiomeKey,
|
||||
String shoreBiomeKey,
|
||||
String dryBiomeKey,
|
||||
String floodedCaveBiomeKey
|
||||
) {
|
||||
public HydrologyColumnLayer {
|
||||
Objects.requireNonNull(feature, "feature");
|
||||
profileKey = requireKey(profileKey, "profileKey");
|
||||
surfaceBiomeKey = requireKey(surfaceBiomeKey, "surfaceBiomeKey");
|
||||
mouthBiomeKey = requireKey(mouthBiomeKey, "mouthBiomeKey");
|
||||
shoreBiomeKey = requireKey(shoreBiomeKey, "shoreBiomeKey");
|
||||
dryBiomeKey = requireKey(dryBiomeKey, "dryBiomeKey");
|
||||
floodedCaveBiomeKey = requireKey(floodedCaveBiomeKey, "floodedCaveBiomeKey");
|
||||
if (channel && bedY > fluidHeadY) {
|
||||
throw new IllegalArgumentException("A channel bed cannot be above its fluid head.");
|
||||
}
|
||||
if (ceilingY < fluidHeadY) {
|
||||
throw new IllegalArgumentException("A hydrology carve ceiling cannot be below its fluid head.");
|
||||
}
|
||||
if (!feature.type().isUnderground() && !feature.type().isDeepFluid() && ceilingY != fluidHeadY) {
|
||||
throw new IllegalArgumentException("Surface hydrology ceilings must equal the fluid head.");
|
||||
}
|
||||
if (fluidOwned && !channel) {
|
||||
throw new IllegalArgumentException("Owned fluid requires a channel footprint.");
|
||||
}
|
||||
if (fallingFluid && !connectedFluid) {
|
||||
throw new IllegalArgumentException("Falling fluid must be connected.");
|
||||
}
|
||||
if (receivingPool && !connectedFluid) {
|
||||
throw new IllegalArgumentException("A receiving pool must be connected.");
|
||||
}
|
||||
if (oceanApron && (terrainOwned || fluidOwned || grading || shore)) {
|
||||
throw new IllegalArgumentException("An ocean apron cannot own river writes or grading.");
|
||||
}
|
||||
}
|
||||
|
||||
public String biomeKey() {
|
||||
if (grading && !channel && !shore) {
|
||||
return null;
|
||||
}
|
||||
if (shore) {
|
||||
return shoreBiomeKey;
|
||||
}
|
||||
if (feature.type() == HydrologyFeatureType.MOUTH || feature.type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
return mouthBiomeKey;
|
||||
}
|
||||
if (feature.type().isUnderground() || feature.type().isDeepFluid()) {
|
||||
return floodedCaveBiomeKey;
|
||||
}
|
||||
return surfaceBiomeKey;
|
||||
}
|
||||
|
||||
public boolean publishesSurfaceFluid() {
|
||||
return feature.type().isSurface()
|
||||
&& !oceanApron
|
||||
&& channel
|
||||
&& fluidOwned
|
||||
&& (terrainOwned || fallingFluid)
|
||||
&& (feature.type().isDrop() || fallingFluid || receivingPool)
|
||||
&& fluidHeadY > bedY;
|
||||
}
|
||||
|
||||
private static String requireKey(String value, String name) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(name + " must not be blank.");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public record HydrologyColumnSample(
|
||||
int x,
|
||||
int z,
|
||||
int naturalHeight,
|
||||
int seaLevel,
|
||||
boolean ocean,
|
||||
String parentBiomeKey,
|
||||
List<HydrologyColumnLayer> layers
|
||||
) {
|
||||
private static final Comparator<HydrologyColumnLayer> LAYER_ORDER = Comparator
|
||||
.comparingInt((HydrologyColumnLayer layer) -> layer.feature().type().renderPriority())
|
||||
.thenComparing(Comparator.comparingInt(HydrologyColumnLayer::fluidHeadY).reversed())
|
||||
.thenComparingLong((HydrologyColumnLayer layer) -> layer.feature().id());
|
||||
private static final Comparator<HydrologyColumnLayer> SURFACE_LAYER_ORDER = Comparator
|
||||
.comparingInt(HydrologyColumnSample::surfaceRolePriority)
|
||||
.thenComparingInt((HydrologyColumnLayer layer) -> layer.feature().type().renderPriority())
|
||||
.thenComparing(Comparator.comparingInt(HydrologyColumnLayer::fluidHeadY).reversed())
|
||||
.thenComparingInt(HydrologyColumnLayer::bedY)
|
||||
.thenComparingLong((HydrologyColumnLayer layer) -> layer.feature().id());
|
||||
|
||||
public HydrologyColumnSample {
|
||||
if (parentBiomeKey == null || parentBiomeKey.isBlank()) {
|
||||
throw new IllegalArgumentException("parentBiomeKey must not be blank.");
|
||||
}
|
||||
parentBiomeKey = parentBiomeKey.trim();
|
||||
Objects.requireNonNull(layers, "layers");
|
||||
if (layers.size() < 2) {
|
||||
layers = List.copyOf(layers);
|
||||
} else {
|
||||
ArrayList<HydrologyColumnLayer> ordered = new ArrayList<>(layers);
|
||||
ordered.sort(LAYER_ORDER);
|
||||
layers = List.copyOf(ordered);
|
||||
}
|
||||
if (ocean) {
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.terrainOwned() || layer.fluidOwned() || layer.grading() || layer.shore()) {
|
||||
throw new IllegalArgumentException("Ocean columns cannot contain river-owned writes or grading.");
|
||||
}
|
||||
if (layer.fluidHeadY() > seaLevel) {
|
||||
throw new IllegalArgumentException("Ocean column fluid cannot be elevated above sea level.");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (naturalHeight <= seaLevel) {
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.feature().type().isSurface()
|
||||
&& !layer.oceanApron()
|
||||
&& (layer.terrainOwned() || layer.fluidOwned() || layer.grading() || layer.shore())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Naturally submerged columns cannot contain owned surface hydrology writes."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.feature().type().isSurface()
|
||||
&& !layer.oceanApron()
|
||||
&& layer.channel()
|
||||
&& layer.fluidOwned()
|
||||
&& layer.fluidHeadY() >= naturalHeight) {
|
||||
throw new IllegalArgumentException("Owned surface fluid must remain below natural terrain.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean present() {
|
||||
return !layers.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasFeature(HydrologyFeatureType type) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.feature().type() == type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasConnectedFluid() {
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.connectedFluid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnLayer> primaryLayer() {
|
||||
return layers.isEmpty() ? Optional.empty() : Optional.of(layers.getFirst());
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnLayer> primarySurfaceLayer() {
|
||||
return selectSurfaceLayer(false);
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnLayer> primarySurfaceFluidLayer() {
|
||||
return selectSurfaceLayer(true);
|
||||
}
|
||||
|
||||
public Optional<SurfacePublicationCell> surfacePublicationCellAt(int y) {
|
||||
if (y <= terrainHeight()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
SurfacePublicationCell selected = null;
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (!layer.publishesSurfaceFluid() || y <= layer.bedY() || y > layer.fluidHeadY()) {
|
||||
continue;
|
||||
}
|
||||
HydrologyCaveAction action = layer.fallingFluid() && y < layer.fluidHeadY()
|
||||
? HydrologyCaveAction.FALLING_FLUID
|
||||
: HydrologyCaveAction.WET_SOURCE;
|
||||
SurfacePublicationCell candidate = new SurfacePublicationCell(layer, action);
|
||||
if (selected == null || surfaceActionPriority(action) < surfaceActionPriority(selected.action())) {
|
||||
selected = candidate;
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(selected);
|
||||
}
|
||||
|
||||
public int terrainHeight() {
|
||||
Optional<HydrologyColumnLayer> layer = primarySurfaceLayer();
|
||||
if (layer.isEmpty()) {
|
||||
return naturalHeight;
|
||||
}
|
||||
HydrologyColumnLayer primary = layer.get();
|
||||
if (primary.channel()) {
|
||||
return primary.bedY();
|
||||
}
|
||||
int terrainHeight = primary.bedY();
|
||||
for (HydrologyColumnLayer candidate : layers) {
|
||||
if (candidate.feature().type().isSurface()
|
||||
&& candidate.terrainOwned()
|
||||
&& !candidate.channel()) {
|
||||
terrainHeight = Math.max(terrainHeight, candidate.bedY());
|
||||
}
|
||||
}
|
||||
return terrainHeight;
|
||||
}
|
||||
|
||||
public HydrologyRenderSample renderSample() {
|
||||
ArrayList<HydrologyFeatureRef> features = new ArrayList<>(layers.size());
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
features.add(layer.feature());
|
||||
}
|
||||
return new HydrologyRenderSample(x, z, features);
|
||||
}
|
||||
|
||||
private Optional<HydrologyColumnLayer> selectSurfaceLayer(boolean fluidOnly) {
|
||||
HydrologyColumnLayer selected = null;
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.oceanApron() || !layer.feature().type().isSurface()) {
|
||||
continue;
|
||||
}
|
||||
if (fluidOnly) {
|
||||
if (!layer.channel() || !layer.connectedFluid() || !layer.fluidOwned()) {
|
||||
continue;
|
||||
}
|
||||
} else if (!layer.terrainOwned()) {
|
||||
continue;
|
||||
}
|
||||
if (selected == null || SURFACE_LAYER_ORDER.compare(layer, selected) < 0) {
|
||||
selected = layer;
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(selected);
|
||||
}
|
||||
|
||||
private static int surfaceRolePriority(HydrologyColumnLayer layer) {
|
||||
if (layer.channel()) {
|
||||
return 0;
|
||||
}
|
||||
return layer.shore() ? 1 : 2;
|
||||
}
|
||||
|
||||
private static int surfaceActionPriority(HydrologyCaveAction action) {
|
||||
return action == HydrologyCaveAction.WET_SOURCE ? 0 : 1;
|
||||
}
|
||||
|
||||
public record SurfacePublicationCell(
|
||||
HydrologyColumnLayer layer,
|
||||
HydrologyCaveAction action
|
||||
) {
|
||||
public SurfacePublicationCell {
|
||||
Objects.requireNonNull(layer, "layer");
|
||||
if (action != HydrologyCaveAction.WET_SOURCE
|
||||
&& action != HydrologyCaveAction.FALLING_FLUID) {
|
||||
throw new IllegalArgumentException("Surface publication cells must contain fluid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveConflictPolicy;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
|
||||
final class HydrologyCrossTileCaveAdmission {
|
||||
private static final Comparator<Claim> CLAIM_PRIORITY = Comparator
|
||||
.comparing((Claim claim) -> claim.plan().source(), HydrologyCaveConflictPolicy.sourcePriority())
|
||||
.thenComparing(Claim::profileKey);
|
||||
private static final Comparator<RankedClaim> BLOCKER_PRIORITY = Comparator
|
||||
.comparingInt(RankedClaim::ownerRank)
|
||||
.thenComparing(RankedClaim::claim, CLAIM_PRIORITY)
|
||||
.thenComparing(RankedClaim::ownerKey);
|
||||
|
||||
private HydrologyCrossTileCaveAdmission() {
|
||||
}
|
||||
|
||||
static Result admit(
|
||||
int currentOwnerRank,
|
||||
Collection<Claim> currentClaims,
|
||||
Collection<RankedClaim> blockers
|
||||
) {
|
||||
if (currentOwnerRank < 0) {
|
||||
throw new IllegalArgumentException("currentOwnerRank cannot be negative");
|
||||
}
|
||||
ArrayList<Claim> orderedClaims = new ArrayList<>(Objects.requireNonNull(currentClaims, "currentClaims"));
|
||||
orderedClaims.sort(CLAIM_PRIORITY);
|
||||
ArrayList<RankedClaim> orderedBlockers = new ArrayList<>(Objects.requireNonNull(blockers, "blockers"));
|
||||
orderedBlockers.sort(BLOCKER_PRIORITY);
|
||||
for (RankedClaim blocker : orderedBlockers) {
|
||||
if (blocker.ownerRank() >= currentOwnerRank) {
|
||||
throw new IllegalArgumentException("Cross-tile blockers must have a lower owner color rank");
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<Rejection> rejections = new ArrayList<>();
|
||||
for (Claim claim : orderedClaims) {
|
||||
RankedClaim winner = null;
|
||||
for (RankedClaim blocker : orderedBlockers) {
|
||||
if (claim.courseId() == blocker.claim().courseId()) {
|
||||
throw new IllegalArgumentException("A cave course cannot belong to multiple color-ranked owners");
|
||||
}
|
||||
if (!conflicts(claim, blocker.claim())) {
|
||||
continue;
|
||||
}
|
||||
if (winner == null || BLOCKER_PRIORITY.compare(blocker, winner) < 0) {
|
||||
winner = blocker;
|
||||
}
|
||||
}
|
||||
if (winner != null) {
|
||||
rejections.add(new Rejection(claim, winner));
|
||||
}
|
||||
}
|
||||
return new Result(rejections);
|
||||
}
|
||||
|
||||
private static boolean conflicts(Claim left, Claim right) {
|
||||
return HydrologyCaveConflictPolicy.hasIncompatibleOverlap(
|
||||
left.profileKey(),
|
||||
left.plan().actions(),
|
||||
right.profileKey(),
|
||||
right.plan().actions()
|
||||
);
|
||||
}
|
||||
|
||||
record Claim(String profileKey, HydrologyCavePlan plan) {
|
||||
Claim {
|
||||
if (profileKey == null || profileKey.isBlank()) {
|
||||
throw new IllegalArgumentException("profileKey must not be blank");
|
||||
}
|
||||
profileKey = profileKey.trim();
|
||||
Objects.requireNonNull(plan, "plan");
|
||||
if (!plan.accepted()) {
|
||||
throw new IllegalArgumentException("Cross-tile admission requires accepted cave plans");
|
||||
}
|
||||
}
|
||||
|
||||
long courseId() {
|
||||
return plan.source().sourceId();
|
||||
}
|
||||
}
|
||||
|
||||
record RankedClaim(HydrologyTileKey ownerKey, int ownerRank, Claim claim) {
|
||||
RankedClaim {
|
||||
Objects.requireNonNull(ownerKey, "ownerKey");
|
||||
if (ownerRank < 0) {
|
||||
throw new IllegalArgumentException("ownerRank cannot be negative");
|
||||
}
|
||||
Objects.requireNonNull(claim, "claim");
|
||||
}
|
||||
}
|
||||
|
||||
record Result(List<Rejection> rejections) {
|
||||
Result {
|
||||
rejections = List.copyOf(Objects.requireNonNull(rejections, "rejections"));
|
||||
}
|
||||
|
||||
Map<Long, Long> loserCourseIdsToWinnerSourceIds() {
|
||||
TreeMap<Long, Long> ordered = new TreeMap<>();
|
||||
for (Rejection rejection : rejections) {
|
||||
ordered.put(rejection.loser().courseId(), rejection.winnerSourceId());
|
||||
}
|
||||
return Collections.unmodifiableMap(new LinkedHashMap<>(ordered));
|
||||
}
|
||||
|
||||
Rejection rejection(Claim claim) {
|
||||
Objects.requireNonNull(claim, "claim");
|
||||
for (Rejection rejection : rejections) {
|
||||
if (rejection.loser().equals(claim)) {
|
||||
return rejection;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
record Rejection(Claim loser, RankedClaim winner) {
|
||||
Rejection {
|
||||
Objects.requireNonNull(loser, "loser");
|
||||
Objects.requireNonNull(winner, "winner");
|
||||
}
|
||||
|
||||
long winnerSourceId() {
|
||||
return winner.claim().plan().source().sourceId();
|
||||
}
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
final class HydrologyCrossTileSurfaceAdmission {
|
||||
private HydrologyCrossTileSurfaceAdmission() {
|
||||
}
|
||||
|
||||
static Result admit(
|
||||
List<Claim> currentClaims,
|
||||
List<RankedClaim> blockers,
|
||||
int minimumSeparation
|
||||
) {
|
||||
ArrayList<Rejection> rejections = new ArrayList<>();
|
||||
for (Claim current : currentClaims) {
|
||||
for (RankedClaim ranked : blockers) {
|
||||
Claim blocker = ranked.claim();
|
||||
if (!conflicts(current, blocker, minimumSeparation)) {
|
||||
continue;
|
||||
}
|
||||
rejections.add(new Rejection(current, blocker.courseId()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new Result(List.copyOf(rejections));
|
||||
}
|
||||
|
||||
private static boolean conflicts(Claim first, Claim second, int minimumSeparation) {
|
||||
if (first.outletId() == second.outletId()) {
|
||||
return true;
|
||||
}
|
||||
long minimumSeparationSquared = (long) minimumSeparation * minimumSeparation;
|
||||
if (first.centerline().getFirst().distanceSquared2D(second.centerline().getFirst())
|
||||
< minimumSeparationSquared) {
|
||||
return true;
|
||||
}
|
||||
if (first.reachesOutlet() && second.reachesOutlet()
|
||||
&& first.terminal().distanceSquared2D(second.terminal())
|
||||
< minimumSeparationSquared) {
|
||||
return true;
|
||||
}
|
||||
double clearance = Math.max(4D, (first.maximumWidth() + second.maximumWidth()) / 2D + 4D);
|
||||
return centerlinesWithin(first.centerline(), second.centerline(), clearance);
|
||||
}
|
||||
|
||||
private static boolean centerlinesWithin(
|
||||
List<HydrologyPoint> first,
|
||||
List<HydrologyPoint> second,
|
||||
double clearance
|
||||
) {
|
||||
double clearanceSquared = clearance * clearance;
|
||||
for (int firstIndex = 1; firstIndex < first.size(); firstIndex++) {
|
||||
HydrologyPoint firstStart = first.get(firstIndex - 1);
|
||||
HydrologyPoint firstEnd = first.get(firstIndex);
|
||||
for (int secondIndex = 1; secondIndex < second.size(); secondIndex++) {
|
||||
HydrologyPoint secondStart = second.get(secondIndex - 1);
|
||||
HydrologyPoint secondEnd = second.get(secondIndex);
|
||||
if (segmentDistanceSquared(firstStart, firstEnd, secondStart, secondEnd) <= clearanceSquared) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static double segmentDistanceSquared(
|
||||
HydrologyPoint firstStart,
|
||||
HydrologyPoint firstEnd,
|
||||
HydrologyPoint secondStart,
|
||||
HydrologyPoint secondEnd
|
||||
) {
|
||||
if (segmentsIntersect(firstStart, firstEnd, secondStart, secondEnd)) {
|
||||
return 0D;
|
||||
}
|
||||
return Math.min(
|
||||
Math.min(pointSegmentDistanceSquared(firstStart, secondStart, secondEnd),
|
||||
pointSegmentDistanceSquared(firstEnd, secondStart, secondEnd)),
|
||||
Math.min(pointSegmentDistanceSquared(secondStart, firstStart, firstEnd),
|
||||
pointSegmentDistanceSquared(secondEnd, firstStart, firstEnd))
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean segmentsIntersect(
|
||||
HydrologyPoint firstStart,
|
||||
HydrologyPoint firstEnd,
|
||||
HydrologyPoint secondStart,
|
||||
HydrologyPoint secondEnd
|
||||
) {
|
||||
long firstA = orientation(firstStart, firstEnd, secondStart);
|
||||
long firstB = orientation(firstStart, firstEnd, secondEnd);
|
||||
long secondA = orientation(secondStart, secondEnd, firstStart);
|
||||
long secondB = orientation(secondStart, secondEnd, firstEnd);
|
||||
return Long.signum(firstA) != Long.signum(firstB)
|
||||
&& Long.signum(secondA) != Long.signum(secondB);
|
||||
}
|
||||
|
||||
private static long orientation(HydrologyPoint start, HydrologyPoint end, HydrologyPoint point) {
|
||||
long firstX = end.x() - (long) start.x();
|
||||
long firstZ = end.z() - (long) start.z();
|
||||
long secondX = point.x() - (long) start.x();
|
||||
long secondZ = point.z() - (long) start.z();
|
||||
return firstX * secondZ - firstZ * secondX;
|
||||
}
|
||||
|
||||
private static double pointSegmentDistanceSquared(
|
||||
HydrologyPoint point,
|
||||
HydrologyPoint start,
|
||||
HydrologyPoint end
|
||||
) {
|
||||
double deltaX = end.x() - start.x();
|
||||
double deltaZ = end.z() - start.z();
|
||||
double lengthSquared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
if (lengthSquared == 0D) {
|
||||
return point.distanceSquared2D(start);
|
||||
}
|
||||
double progress = ((point.x() - start.x()) * deltaX + (point.z() - start.z()) * deltaZ)
|
||||
/ lengthSquared;
|
||||
progress = Math.max(0D, Math.min(1D, progress));
|
||||
double closestX = start.x() + deltaX * progress;
|
||||
double closestZ = start.z() + deltaZ * progress;
|
||||
double pointX = point.x() - closestX;
|
||||
double pointZ = point.z() - closestZ;
|
||||
return pointX * pointX + pointZ * pointZ;
|
||||
}
|
||||
|
||||
record Claim(
|
||||
long courseId,
|
||||
long outletId,
|
||||
HydrologyPoint terminal,
|
||||
boolean reachesOutlet,
|
||||
int maximumWidth,
|
||||
List<HydrologyPoint> centerline
|
||||
) {
|
||||
Claim {
|
||||
Objects.requireNonNull(terminal, "terminal");
|
||||
centerline = List.copyOf(Objects.requireNonNull(centerline, "centerline"));
|
||||
if (maximumWidth < 1 || centerline.size() < 2) {
|
||||
throw new IllegalArgumentException("Surface claims require a positive width and centerline.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record RankedClaim(HydrologyTileKey ownerKey, int ownerRank, Claim claim) {
|
||||
RankedClaim {
|
||||
Objects.requireNonNull(ownerKey, "ownerKey");
|
||||
Objects.requireNonNull(claim, "claim");
|
||||
}
|
||||
}
|
||||
|
||||
record Rejection(Claim loser, long winnerCourseId) {
|
||||
Rejection {
|
||||
Objects.requireNonNull(loser, "loser");
|
||||
}
|
||||
}
|
||||
|
||||
record Result(List<Rejection> rejections) {
|
||||
Result {
|
||||
rejections = List.copyOf(Objects.requireNonNull(rejections, "rejections"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydrologyDiagnosticCandidate(
|
||||
long id,
|
||||
HydrologyCandidateKind kind,
|
||||
HydrologyFeatureType projectedType,
|
||||
HydrologyPoint point,
|
||||
HydrologyCandidateRejection rejection
|
||||
) {
|
||||
public HydrologyDiagnosticCandidate {
|
||||
Objects.requireNonNull(kind, "kind");
|
||||
Objects.requireNonNull(projectedType, "projectedType");
|
||||
Objects.requireNonNull(point, "point");
|
||||
Objects.requireNonNull(rejection, "rejection");
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydrologyDiagnosticRenderSample(
|
||||
int x,
|
||||
int z,
|
||||
List<HydrologyDiagnosticCandidate> candidates
|
||||
) {
|
||||
private static final Comparator<HydrologyDiagnosticCandidate> CANDIDATE_ORDER = Comparator
|
||||
.comparing(HydrologyDiagnosticCandidate::kind)
|
||||
.thenComparing(HydrologyDiagnosticCandidate::rejection)
|
||||
.thenComparingLong(HydrologyDiagnosticCandidate::id);
|
||||
|
||||
public HydrologyDiagnosticRenderSample {
|
||||
Objects.requireNonNull(candidates, "candidates");
|
||||
ArrayList<HydrologyDiagnosticCandidate> ordered = new ArrayList<>(candidates);
|
||||
ordered.sort(CANDIDATE_ORDER);
|
||||
candidates = List.copyOf(ordered);
|
||||
}
|
||||
|
||||
public boolean present() {
|
||||
return !candidates.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public record HydrologyFeatureQuery(Set<HydrologyFeatureType> types, String profileKey) {
|
||||
private static final Set<HydrologyFeatureType> SURFACE = Set.of(
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
HydrologyFeatureType.RIFFLE,
|
||||
HydrologyFeatureType.CASCADE,
|
||||
HydrologyFeatureType.WATERFALL,
|
||||
HydrologyFeatureType.MOUTH
|
||||
);
|
||||
private static final Set<HydrologyFeatureType> UNDERGROUND = Set.of(
|
||||
HydrologyFeatureType.RIDGE_BORE,
|
||||
HydrologyFeatureType.UNDERGROUND_POOL,
|
||||
HydrologyFeatureType.UNDERGROUND_DROP,
|
||||
HydrologyFeatureType.SINKHOLE
|
||||
);
|
||||
private static final Set<HydrologyFeatureType> GROTTOS = Set.of(
|
||||
HydrologyFeatureType.COASTAL_GROTTO,
|
||||
HydrologyFeatureType.INLAND_GROTTO
|
||||
);
|
||||
private static final Set<HydrologyFeatureType> DEEP = Set.of(
|
||||
HydrologyFeatureType.DEEP_POOL,
|
||||
HydrologyFeatureType.DEEP_CHANNEL
|
||||
);
|
||||
private static final Map<String, Set<HydrologyFeatureType>> BUILT_INS = builtIns();
|
||||
|
||||
public HydrologyFeatureQuery {
|
||||
types = Set.copyOf(types);
|
||||
if (types.isEmpty()) {
|
||||
throw new IllegalArgumentException("Hydrology feature query requires at least one feature type.");
|
||||
}
|
||||
profileKey = profileKey == null || profileKey.isBlank() ? null : profileKey.trim();
|
||||
}
|
||||
|
||||
public static HydrologyFeatureQuery parse(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Hydrology feature type must not be blank.");
|
||||
}
|
||||
String normalized = value.trim();
|
||||
if (normalized.regionMatches(true, 0, "type=", 0, 5)) {
|
||||
normalized = normalized.substring(5).trim();
|
||||
}
|
||||
String key = normalizeKeyword(normalized);
|
||||
Set<HydrologyFeatureType> builtInTypes = BUILT_INS.get(key);
|
||||
return builtInTypes == null
|
||||
? new HydrologyFeatureQuery(DEEP, normalized)
|
||||
: new HydrologyFeatureQuery(builtInTypes, null);
|
||||
}
|
||||
|
||||
public static List<String> suggestions(Collection<String> deepFluidIds) {
|
||||
LinkedHashSet<String> suggestions = new LinkedHashSet<>(BUILT_INS.keySet());
|
||||
TreeSet<String> orderedIds = new TreeSet<>();
|
||||
if (deepFluidIds != null) {
|
||||
for (String id : deepFluidIds) {
|
||||
if (id != null && !id.isBlank() && !isReservedKeyword(id)) {
|
||||
orderedIds.add(id.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
suggestions.addAll(orderedIds);
|
||||
return List.copyOf(suggestions);
|
||||
}
|
||||
|
||||
public static boolean isReservedKeyword(String value) {
|
||||
return value != null && !value.isBlank() && BUILT_INS.containsKey(normalizeKeyword(value.trim()));
|
||||
}
|
||||
|
||||
private static String normalizeKeyword(String value) {
|
||||
return value.toLowerCase(Locale.ROOT).replace('-', '_');
|
||||
}
|
||||
|
||||
private static Map<String, Set<HydrologyFeatureType>> builtIns() {
|
||||
LinkedHashMap<String, Set<HydrologyFeatureType>> builtIns = new LinkedHashMap<>();
|
||||
builtIns.put("surface", SURFACE);
|
||||
builtIns.put("waterfall", Set.of(HydrologyFeatureType.WATERFALL));
|
||||
builtIns.put("sinkhole", Set.of(HydrologyFeatureType.SINKHOLE));
|
||||
builtIns.put("underground", UNDERGROUND);
|
||||
builtIns.put("grotto", GROTTOS);
|
||||
builtIns.put("coastal_grotto", Set.of(HydrologyFeatureType.COASTAL_GROTTO));
|
||||
builtIns.put("inland_grotto", Set.of(HydrologyFeatureType.INLAND_GROTTO));
|
||||
builtIns.put("mouth", Set.of(HydrologyFeatureType.MOUTH));
|
||||
builtIns.put("ridge_tunnel", Set.of(HydrologyFeatureType.RIDGE_BORE));
|
||||
builtIns.put("deep", DEEP);
|
||||
return Collections.unmodifiableMap(builtIns);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydrologyFeatureRef(
|
||||
long id,
|
||||
HydrologyFeatureType type,
|
||||
long courseId,
|
||||
long segmentId,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
int flowDeltaX,
|
||||
int flowDeltaZ,
|
||||
boolean source
|
||||
) {
|
||||
public HydrologyFeatureRef {
|
||||
Objects.requireNonNull(type, "type");
|
||||
if (flowDeltaX < -1 || flowDeltaX > 1 || flowDeltaZ < -1 || flowDeltaZ > 1) {
|
||||
throw new IllegalArgumentException("Flow direction components must be between -1 and 1.");
|
||||
}
|
||||
}
|
||||
|
||||
public HydrologyPoint point() {
|
||||
return new HydrologyPoint(x, y, z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public enum HydrologyFeatureType {
|
||||
SURFACE_POOL,
|
||||
RIFFLE,
|
||||
CASCADE,
|
||||
WATERFALL,
|
||||
RIDGE_BORE,
|
||||
UNDERGROUND_POOL,
|
||||
UNDERGROUND_DROP,
|
||||
SINKHOLE,
|
||||
COASTAL_GROTTO,
|
||||
INLAND_GROTTO,
|
||||
MOUTH,
|
||||
DEEP_POOL,
|
||||
DEEP_CHANNEL;
|
||||
|
||||
public boolean isSurface() {
|
||||
return switch (this) {
|
||||
case SURFACE_POOL, RIFFLE, CASCADE, WATERFALL, MOUTH -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean isUnderground() {
|
||||
return switch (this) {
|
||||
case RIDGE_BORE, UNDERGROUND_POOL, UNDERGROUND_DROP, SINKHOLE, COASTAL_GROTTO, INLAND_GROTTO -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean isGrotto() {
|
||||
return this == COASTAL_GROTTO || this == INLAND_GROTTO;
|
||||
}
|
||||
|
||||
public boolean isDeepFluid() {
|
||||
return this == DEEP_POOL || this == DEEP_CHANNEL;
|
||||
}
|
||||
|
||||
public boolean isDrop() {
|
||||
return this == RIFFLE || this == CASCADE || this == WATERFALL
|
||||
|| this == UNDERGROUND_DROP || this == SINKHOLE;
|
||||
}
|
||||
|
||||
int renderPriority() {
|
||||
return switch (this) {
|
||||
case WATERFALL -> 0;
|
||||
case SINKHOLE -> 1;
|
||||
case CASCADE -> 2;
|
||||
case RIFFLE -> 3;
|
||||
case COASTAL_GROTTO -> 4;
|
||||
case INLAND_GROTTO -> 5;
|
||||
case MOUTH -> 6;
|
||||
case RIDGE_BORE -> 7;
|
||||
case UNDERGROUND_DROP -> 8;
|
||||
case UNDERGROUND_POOL -> 9;
|
||||
case DEEP_CHANNEL -> 10;
|
||||
case DEEP_POOL -> 11;
|
||||
case SURFACE_POOL -> 12;
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HydrologyGeometrySampler {
|
||||
int sample(Request request);
|
||||
|
||||
default int sample(
|
||||
Field field,
|
||||
String profileKey,
|
||||
int x,
|
||||
int z,
|
||||
int minimum,
|
||||
int maximum
|
||||
) {
|
||||
return sample(field, profileKey, x, z, 0L, minimum, maximum);
|
||||
}
|
||||
|
||||
default int sample(
|
||||
Field field,
|
||||
String profileKey,
|
||||
int x,
|
||||
int z,
|
||||
long stableId,
|
||||
int minimum,
|
||||
int maximum
|
||||
) {
|
||||
Request request = new Request(field, profileKey, x, z, stableId, minimum, maximum);
|
||||
int value = sample(request);
|
||||
if (value < minimum || value > maximum) {
|
||||
throw new IllegalStateException(
|
||||
"Hydrology geometry sampler returned " + value + " outside " + minimum + ".." + maximum
|
||||
+ " for " + field + " at " + x + "," + z + "."
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static HydrologyGeometrySampler deterministic(
|
||||
long worldSeed,
|
||||
HydrologyTerrainSampler terrainSampler
|
||||
) {
|
||||
Objects.requireNonNull(terrainSampler, "terrainSampler");
|
||||
return request -> {
|
||||
if (request.field() == Field.UNDERGROUND_FLUID_LEVEL) {
|
||||
HydrologyTerrainSample terrain = Objects.requireNonNull(
|
||||
terrainSampler.sample(request.x(), request.z()),
|
||||
"Hydrology terrain sampler returned null at " + request.x() + "," + request.z()
|
||||
);
|
||||
return Math.max(request.minimum(), Math.min(request.maximum(), terrain.caveFluidY()));
|
||||
}
|
||||
if (request.field() == Field.SURFACE_WIDTH
|
||||
|| request.field() == Field.SURFACE_DEPTH
|
||||
|| request.field() == Field.SURFACE_INSET
|
||||
|| request.field() == Field.UNDERGROUND_WIDTH
|
||||
|| request.field() == Field.UNDERGROUND_DEPTH) {
|
||||
return request.minimum();
|
||||
}
|
||||
long stable = switch (request.field()) {
|
||||
case SURFACE_BLEND_WIDTH -> HydrologyHash.mix(
|
||||
request.stableId(),
|
||||
request.x(),
|
||||
request.z(),
|
||||
0x424c454e44L
|
||||
);
|
||||
case TARGET_POOL_LENGTH -> HydrologyHash.mix(
|
||||
worldSeed,
|
||||
request.stableId(),
|
||||
0x504f4f4cL
|
||||
);
|
||||
case UNDERGROUND_HEADROOM -> HydrologyHash.mix(
|
||||
request.stableId(),
|
||||
request.x(),
|
||||
request.z(),
|
||||
0x48454144L
|
||||
);
|
||||
case DEEP_FLUID_HEIGHT -> HydrologyHash.mix(request.stableId(), 0x484549474854L);
|
||||
default -> throw new IllegalStateException("Missing deterministic geometry rule for " + request.field() + ".");
|
||||
};
|
||||
return HydrologyHash.between(stable, request.minimum(), request.maximum());
|
||||
};
|
||||
}
|
||||
|
||||
enum Field {
|
||||
SURFACE_WIDTH,
|
||||
SURFACE_DEPTH,
|
||||
SURFACE_INSET,
|
||||
SURFACE_BLEND_WIDTH,
|
||||
TARGET_POOL_LENGTH,
|
||||
UNDERGROUND_FLUID_LEVEL,
|
||||
UNDERGROUND_WIDTH,
|
||||
UNDERGROUND_DEPTH,
|
||||
UNDERGROUND_HEADROOM,
|
||||
DEEP_FLUID_HEIGHT
|
||||
}
|
||||
|
||||
record Request(
|
||||
Field field,
|
||||
String profileKey,
|
||||
int x,
|
||||
int z,
|
||||
long stableId,
|
||||
int minimum,
|
||||
int maximum
|
||||
) {
|
||||
public Request {
|
||||
field = Objects.requireNonNull(field, "field");
|
||||
profileKey = profileKey == null ? "" : profileKey;
|
||||
if (minimum > maximum) {
|
||||
throw new IllegalArgumentException("Hydrology geometry sample bounds are reversed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
final class HydrologyHash {
|
||||
private HydrologyHash() {
|
||||
}
|
||||
|
||||
static long mix(long seed, long... values) {
|
||||
long mixed = avalanche(seed ^ 0x9e3779b97f4a7c15L);
|
||||
for (long value : values) {
|
||||
mixed = avalanche(mixed ^ avalanche(value + 0x9e3779b97f4a7c15L));
|
||||
}
|
||||
return mixed;
|
||||
}
|
||||
|
||||
static long text(String value) {
|
||||
long hash = 0xcbf29ce484222325L;
|
||||
for (int index = 0; index < value.length(); index++) {
|
||||
hash ^= value.charAt(index);
|
||||
hash *= 0x100000001b3L;
|
||||
}
|
||||
return avalanche(hash);
|
||||
}
|
||||
|
||||
static double unit(long value) {
|
||||
return (avalanche(value) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
static int between(long value, int minimum, int maximum) {
|
||||
if (minimum > maximum) {
|
||||
throw new IllegalArgumentException("minimum cannot exceed maximum.");
|
||||
}
|
||||
long span = (long) maximum - minimum + 1L;
|
||||
return minimum + (int) Long.remainderUnsigned(avalanche(value), span);
|
||||
}
|
||||
|
||||
private static long avalanche(long value) {
|
||||
value ^= value >>> 30;
|
||||
value *= 0xbf58476d1ce4e5b9L;
|
||||
value ^= value >>> 27;
|
||||
value *= 0x94d049bb133111ebL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public interface HydrologyNaturalTerrainSampler extends HydrologyRoutingTerrainSampler {
|
||||
HydrologyTerrainSample sampleBasis(int blockX, int blockZ);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class HydrologyObservedPlannedSurface implements HydrologyCaveVoxelViewFactory.PlannedSurface {
|
||||
private final HydrologyCaveVoxelViewFactory.PlannedSurface delegate;
|
||||
private final Long2ObjectOpenHashMap<ArrayList<Observation>> observations;
|
||||
|
||||
public HydrologyObservedPlannedSurface(HydrologyCaveVoxelViewFactory.PlannedSurface delegate) {
|
||||
this.delegate = Objects.requireNonNull(delegate);
|
||||
this.observations = new Long2ObjectOpenHashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int resolve(int x, int z, int naturalHeight) {
|
||||
int resolvedHeight = delegate.resolve(x, z, naturalHeight);
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
ArrayList<Observation> column = observations.get(key);
|
||||
if (column == null) {
|
||||
column = new ArrayList<>(1);
|
||||
observations.put(key, column);
|
||||
}
|
||||
for (Observation observation : column) {
|
||||
if (observation.naturalHeight() == naturalHeight) {
|
||||
return resolvedHeight;
|
||||
}
|
||||
}
|
||||
column.add(new Observation(x, z, naturalHeight, resolvedHeight));
|
||||
return resolvedHeight;
|
||||
}
|
||||
|
||||
public List<Observation> observationsAt(int x, int z) {
|
||||
ArrayList<Observation> column = observations.get(RiverFootprint.pack(x, z));
|
||||
return column == null ? List.of() : List.copyOf(column);
|
||||
}
|
||||
|
||||
public record Observation(
|
||||
int x,
|
||||
int z,
|
||||
int naturalHeight,
|
||||
int resolvedHeight
|
||||
) {
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
final class HydrologyOceanBoundaryRefiner {
|
||||
private HydrologyOceanBoundaryRefiner() {
|
||||
}
|
||||
|
||||
static Result refine(
|
||||
List<HydrologyPoint> crossing,
|
||||
HydrologyTerrainSampler terrainSampler,
|
||||
HydrologyRoutingTerrainSampler routingSampler
|
||||
) {
|
||||
Objects.requireNonNull(crossing, "crossing");
|
||||
Objects.requireNonNull(terrainSampler, "terrainSampler");
|
||||
Objects.requireNonNull(routingSampler, "routingSampler");
|
||||
if (crossing.size() < 2) {
|
||||
return null;
|
||||
}
|
||||
HydrologyPoint previous = crossing.getFirst();
|
||||
for (int index = 1; index < crossing.size(); index++) {
|
||||
HydrologyPoint point = crossing.get(index);
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification classification =
|
||||
routingSampler.classifyNatural(point.x(), point.z());
|
||||
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.UNAVAILABLE) {
|
||||
return null;
|
||||
}
|
||||
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN) {
|
||||
HydrologyTerrainSample landwardTerrain = terrainSampler.sample(previous.x(), previous.z());
|
||||
if (landwardTerrain == null || landwardTerrain.ocean()) {
|
||||
return null;
|
||||
}
|
||||
return new Result(previous, point, landwardTerrain);
|
||||
}
|
||||
previous = point;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
record Result(
|
||||
HydrologyPoint landwardPoint,
|
||||
HydrologyPoint oceanPoint,
|
||||
HydrologyTerrainSample landwardTerrain
|
||||
) {
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,522 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record HydrologyPlannerSettings(
|
||||
int seaLevel,
|
||||
Routing routing,
|
||||
Surface surface,
|
||||
Hydraulics hydraulics,
|
||||
Underground underground,
|
||||
Outlets outlets,
|
||||
Geometry geometry,
|
||||
List<DeepFluid> deepFluids
|
||||
) {
|
||||
private static final long PLAN_FORMAT_REVISION = 5L;
|
||||
private static final int MAXIMUM_CROSS_TILE_COLOR_PERIOD = 4;
|
||||
|
||||
public HydrologyPlannerSettings {
|
||||
if (seaLevel < -2048 || seaLevel > 2048) {
|
||||
throw new IllegalArgumentException("seaLevel is outside the supported planning range.");
|
||||
}
|
||||
if (routing == null || surface == null || hydraulics == null || underground == null
|
||||
|| outlets == null || geometry == null) {
|
||||
throw new IllegalArgumentException("Hydrology planner settings cannot contain null sections.");
|
||||
}
|
||||
deepFluids = deepFluids == null ? List.of() : List.copyOf(deepFluids);
|
||||
int publicationRadius = publicationRadius(routing, surface, underground, outlets, geometry, deepFluids);
|
||||
if (crossTileColorPeriod(publicationRadius, routing.tileSize()) > MAXIMUM_CROSS_TILE_COLOR_PERIOD) {
|
||||
throw new IllegalArgumentException("Hydrology publication envelope exceeds the bounded cross-tile admission period.");
|
||||
}
|
||||
}
|
||||
|
||||
public static HydrologyPlannerSettings defaults() {
|
||||
Source surfaceSources = new Source(true, 0.5D, 88, 0, 1, 384);
|
||||
Source undergroundSources = new Source(true, 0.25D, Integer.MIN_VALUE, 0, 1, 512);
|
||||
return new HydrologyPlannerSettings(
|
||||
63,
|
||||
new Routing(2048, 64, 8, 8192, 8192, new Branching(384, 192), 1.5D, 24D, 2D, 0.2D),
|
||||
new Surface(true, surfaceSources, 4, 8, 1, 4, 3, 7, 6, 2, 10, 24, true, 192, 10),
|
||||
new Hydraulics(80, 180, 1, 7, 24, 8),
|
||||
new Underground(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true),
|
||||
new Outlets(
|
||||
true,
|
||||
new Grotto(true, 18, 8, 8, 32768),
|
||||
new Grotto(true, 18, 8, 8, 32768),
|
||||
false,
|
||||
12,
|
||||
64,
|
||||
8,
|
||||
12
|
||||
),
|
||||
Geometry.defaults(),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
public long fingerprint() {
|
||||
long hash = HydrologyHash.mix(
|
||||
0x485944524f4c4f47L,
|
||||
PLAN_FORMAT_REVISION,
|
||||
routing.hashCode(),
|
||||
surface.hashCode(),
|
||||
hydraulics.hashCode(),
|
||||
underground.hashCode(),
|
||||
outlets.hashCode(),
|
||||
geometry.hashCode()
|
||||
);
|
||||
for (DeepFluid deepFluid : deepFluids) {
|
||||
hash = HydrologyHash.mix(hash, deepFluid.hashCode(), HydrologyHash.text(deepFluid.id()));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
public int publicationRadius() {
|
||||
return publicationRadius(routing, surface, underground, outlets, geometry, deepFluids);
|
||||
}
|
||||
|
||||
int crossTileColorPeriod() {
|
||||
return crossTileColorPeriod(publicationRadius(), routing.tileSize());
|
||||
}
|
||||
|
||||
private static int publicationRadius(
|
||||
Routing routing,
|
||||
Surface surface,
|
||||
Underground underground,
|
||||
Outlets outlets,
|
||||
Geometry geometry,
|
||||
List<DeepFluid> deepFluids
|
||||
) {
|
||||
int alignedHalo = Math.floorDiv(
|
||||
Math.min(
|
||||
routing.maximumRouteLength(),
|
||||
Math.multiplyExact(routing.sampleSpacing(), 2)
|
||||
),
|
||||
routing.sampleSpacing()
|
||||
) * routing.sampleSpacing();
|
||||
int radius = 0;
|
||||
int routeDisplacement = Math.multiplyExact(routing.sampleSpacing(), 3);
|
||||
if (surface.enabled() && surface.sources().enabled()) {
|
||||
int surfaceRadius = (int) StrictMath.ceil(
|
||||
surface.maximumWidth() / 2D + surface.shoreWidth() + surface.maximumTerrainBlendWidth()
|
||||
);
|
||||
surfaceRadius = Math.max(
|
||||
surfaceRadius,
|
||||
(int) StrictMath.ceil(geometry.drops().basinWidth(
|
||||
geometry.drops().flowWidth(surface.maximumWidth())
|
||||
) / 2D + surface.shoreWidth() + surface.maximumTerrainBlendWidth())
|
||||
);
|
||||
surfaceRadius = Math.max(surfaceRadius, outlets.coastalGrotto().horizontalRadius());
|
||||
surfaceRadius = Math.max(surfaceRadius, outlets.inlandGrotto().horizontalRadius());
|
||||
radius = Math.max(radius, Math.addExact(alignedHalo, Math.addExact(surfaceRadius, routeDisplacement)));
|
||||
}
|
||||
if (underground.enabled() && underground.sources().enabled()) {
|
||||
int undergroundRadius = (int) StrictMath.ceil(underground.maximumWidth() / 2D);
|
||||
undergroundRadius = Math.max(
|
||||
undergroundRadius,
|
||||
(int) StrictMath.ceil(geometry.drops().basinWidth(
|
||||
geometry.drops().flowWidth(underground.maximumWidth())
|
||||
) / 2D)
|
||||
);
|
||||
undergroundRadius = Math.max(undergroundRadius, outlets.coastalGrotto().horizontalRadius());
|
||||
undergroundRadius = Math.max(undergroundRadius, outlets.inlandGrotto().horizontalRadius());
|
||||
radius = Math.max(radius, Math.addExact(alignedHalo, Math.addExact(undergroundRadius, routeDisplacement)));
|
||||
}
|
||||
for (DeepFluid deepFluid : deepFluids) {
|
||||
if (!deepFluid.enabled() || deepFluid.maximumPerTile() == 0) {
|
||||
continue;
|
||||
}
|
||||
int poolReach = deepFluid.containedPools() ? deepFluid.maximumHorizontalRadius() : 0;
|
||||
int channelReach = deepFluid.shortChannels()
|
||||
? Math.addExact(deepFluid.maximumChannelLength(), (int) StrictMath.ceil(deepFluid.channelWidth() / 2D))
|
||||
: 0;
|
||||
radius = Math.max(radius, Math.max(poolReach, channelReach));
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private static int crossTileColorPeriod(int publicationRadius, int tileSize) {
|
||||
long actionReach = Math.addExact((long) publicationRadius, 1L);
|
||||
return Math.toIntExact(Math.addExact(Math.floorDiv(Math.multiplyExact(2L, actionReach), tileSize), 2L));
|
||||
}
|
||||
|
||||
public record Routing(
|
||||
int tileSize,
|
||||
int sampleSpacing,
|
||||
int refinementSpacing,
|
||||
int maximumRouteNodes,
|
||||
int maximumRouteLength,
|
||||
Branching branching,
|
||||
double valleyPreference,
|
||||
double uphillPenalty,
|
||||
double slopePenalty,
|
||||
double confluenceAttraction
|
||||
) {
|
||||
public Routing {
|
||||
if (tileSize < 32 || sampleSpacing < 4 || refinementSpacing < 1 || branching == null
|
||||
|| tileSize % sampleSpacing != 0 || sampleSpacing % refinementSpacing != 0) {
|
||||
throw new IllegalArgumentException("Routing sizes must form an exact bounded tile lattice.");
|
||||
}
|
||||
int latticeWidth = tileSize / sampleSpacing + 1;
|
||||
int halo = Math.min(maximumRouteLength, Math.multiplyExact(sampleSpacing, 2));
|
||||
int alignedHalo = Math.floorDiv(halo, sampleSpacing) * sampleSpacing;
|
||||
int expandedWidth = latticeWidth + alignedHalo * 2 / sampleSpacing;
|
||||
long latticeNodes = (long) expandedWidth * expandedWidth;
|
||||
if (maximumRouteNodes < latticeNodes || maximumRouteNodes > 1_000_000 || maximumRouteLength < 1) {
|
||||
throw new IllegalArgumentException("maximumRouteNodes must contain the complete lattice and remain bounded.");
|
||||
}
|
||||
if (branching.minimumSurfaceCourseLength() > maximumRouteLength
|
||||
|| branching.minimumUndergroundCourseLength() > maximumRouteLength) {
|
||||
throw new IllegalArgumentException("Minimum course lengths cannot exceed maximum route length.");
|
||||
}
|
||||
requireFiniteNonNegative(valleyPreference, "valleyPreference");
|
||||
requireFiniteNonNegative(uphillPenalty, "uphillPenalty");
|
||||
requireFiniteNonNegative(slopePenalty, "slopePenalty");
|
||||
requireFiniteNonNegative(confluenceAttraction, "confluenceAttraction");
|
||||
}
|
||||
}
|
||||
|
||||
public record Branching(
|
||||
int minimumSurfaceCourseLength,
|
||||
int minimumUndergroundCourseLength
|
||||
) {
|
||||
public Branching {
|
||||
if (minimumSurfaceCourseLength < 0 || minimumSurfaceCourseLength > 32_768
|
||||
|| minimumUndergroundCourseLength < 0 || minimumUndergroundCourseLength > 32_768) {
|
||||
throw new IllegalArgumentException("Hydrology branching lengths are invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
public int minimumCourseLength(boolean surface) {
|
||||
return surface ? minimumSurfaceCourseLength : minimumUndergroundCourseLength;
|
||||
}
|
||||
}
|
||||
|
||||
public record Source(
|
||||
boolean enabled,
|
||||
double density,
|
||||
int minimumElevation,
|
||||
int minimumPerTile,
|
||||
int maximumPerTile,
|
||||
int minimumSpacing
|
||||
) {
|
||||
public Source {
|
||||
if (!Double.isFinite(density) || density < 0D || density > 64D) {
|
||||
throw new IllegalArgumentException("Source density must be between 0 and 64 expected sources per tile.");
|
||||
}
|
||||
if (minimumPerTile < 0 || maximumPerTile < minimumPerTile || maximumPerTile > 4096) {
|
||||
throw new IllegalArgumentException("Source tile quotas are invalid.");
|
||||
}
|
||||
if (minimumSpacing < 0) {
|
||||
throw new IllegalArgumentException("Source spacing cannot be negative.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record Surface(
|
||||
boolean enabled,
|
||||
Source sources,
|
||||
int minimumWidth,
|
||||
int maximumWidth,
|
||||
int minimumDepth,
|
||||
int maximumDepth,
|
||||
int minimumSurfaceInset,
|
||||
int maximumSurfaceInset,
|
||||
int maximumIncision,
|
||||
double shoreWidth,
|
||||
int minimumTerrainBlendWidth,
|
||||
int maximumTerrainBlendWidth,
|
||||
boolean ridgeTunnelsEnabled,
|
||||
int maximumRidgeTunnelLength,
|
||||
int ridgeTunnelHeadroom
|
||||
) {
|
||||
public Surface {
|
||||
if (sources == null) {
|
||||
throw new IllegalArgumentException("Surface source settings are required.");
|
||||
}
|
||||
requireRange(minimumWidth, maximumWidth, 1, 256, "surface width");
|
||||
requireRange(minimumDepth, maximumDepth, 1, 64, "surface depth");
|
||||
requireRange(minimumSurfaceInset, maximumSurfaceInset, 1, 64, "surface inset");
|
||||
if (maximumIncision < 0 || !Double.isFinite(shoreWidth) || shoreWidth < 0D || shoreWidth > 32D) {
|
||||
throw new IllegalArgumentException("Surface incision and shore width are invalid.");
|
||||
}
|
||||
requireRange(minimumTerrainBlendWidth, maximumTerrainBlendWidth, 0, 128, "terrain blend width");
|
||||
if (maximumRidgeTunnelLength < 0 || ridgeTunnelHeadroom < 0) {
|
||||
throw new IllegalArgumentException("Ridge tunnel bounds cannot be negative.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record Hydraulics(
|
||||
int minimumTargetPoolLength,
|
||||
int maximumTargetPoolLength,
|
||||
int riffleDrop,
|
||||
int maximumGradualDrop,
|
||||
int maximumGradualLength,
|
||||
int waterfallMinimumDrop
|
||||
) {
|
||||
public Hydraulics {
|
||||
requireRange(minimumTargetPoolLength, maximumTargetPoolLength, 1, 4096, "target pool length");
|
||||
if (riffleDrop < 0 || maximumGradualDrop < riffleDrop
|
||||
|| maximumGradualLength < Math.max(1, maximumGradualDrop)
|
||||
|| waterfallMinimumDrop != maximumGradualDrop + 1
|
||||
|| minimumTargetPoolLength < maximumGradualLength) {
|
||||
throw new IllegalArgumentException("Hydraulic transition thresholds are inconsistent.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record Underground(
|
||||
boolean enabled,
|
||||
Source sources,
|
||||
int minimumFluidY,
|
||||
int maximumFluidY,
|
||||
int minimumWidth,
|
||||
int maximumWidth,
|
||||
int minimumDepth,
|
||||
int maximumDepth,
|
||||
int minimumHeadroom,
|
||||
int maximumHeadroom,
|
||||
boolean connectToExistingCaves
|
||||
) {
|
||||
public Underground {
|
||||
if (sources == null || minimumFluidY > maximumFluidY) {
|
||||
throw new IllegalArgumentException("Underground source and height settings are invalid.");
|
||||
}
|
||||
requireRange(minimumWidth, maximumWidth, 1, 256, "underground width");
|
||||
requireRange(minimumDepth, maximumDepth, 1, 64, "underground depth");
|
||||
requireRange(minimumHeadroom, maximumHeadroom, 1, 128, "underground headroom");
|
||||
}
|
||||
}
|
||||
|
||||
public record Outlets(
|
||||
boolean oceanEnabled,
|
||||
Grotto coastalGrotto,
|
||||
Grotto inlandGrotto,
|
||||
boolean surfaceSinkholesEnabled,
|
||||
int coastalCliffMinimumHeight,
|
||||
int mouthLevelingDistance,
|
||||
int maximumOceanApron,
|
||||
int maximumPerTile
|
||||
) {
|
||||
public Outlets {
|
||||
if (coastalGrotto == null || inlandGrotto == null) {
|
||||
throw new IllegalArgumentException("Coastal and inland grotto settings are required.");
|
||||
}
|
||||
if (surfaceSinkholesEnabled && !inlandGrotto.enabled()) {
|
||||
throw new IllegalArgumentException("Surface sinkholes require inland grotto outlets.");
|
||||
}
|
||||
if (coastalCliffMinimumHeight < 0 || maximumOceanApron < 0 || maximumOceanApron > 64
|
||||
|| mouthLevelingDistance < 0 || maximumPerTile < 1 || maximumPerTile > 256) {
|
||||
throw new IllegalArgumentException("Outlet bounds are invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record Grotto(
|
||||
boolean enabled,
|
||||
int horizontalRadius,
|
||||
int verticalRadius,
|
||||
int headroom,
|
||||
int maximumVolume
|
||||
) {
|
||||
public Grotto {
|
||||
if (horizontalRadius < 1 || horizontalRadius > 128 || verticalRadius < 1 || verticalRadius > 128
|
||||
|| headroom < 1 || headroom > 128 || maximumVolume < 1 || maximumVolume > 1_048_576) {
|
||||
throw new IllegalArgumentException("Grotto bounds are invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record Geometry(
|
||||
Meanders meanders,
|
||||
ChannelShape surface,
|
||||
ChannelShape underground,
|
||||
ChannelShape grottos,
|
||||
Drops drops
|
||||
) {
|
||||
public Geometry {
|
||||
if (meanders == null || surface == null || underground == null || grottos == null || drops == null) {
|
||||
throw new IllegalArgumentException("Hydrology geometry settings cannot contain null sections.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Geometry defaults() {
|
||||
ChannelShape channel = new ChannelShape(2.4D, 0.28D, 0.24D, 11);
|
||||
return new Geometry(
|
||||
new Meanders(64, 12, 0.34D, 0.42D, 0.48D, 1, 82D),
|
||||
channel,
|
||||
channel,
|
||||
channel,
|
||||
new Drops(2, 1.4D, 2, 0.45D, 2, 1.8D, 8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record Meanders(
|
||||
int primaryWavelength,
|
||||
int detailWavelength,
|
||||
double primaryStrength,
|
||||
double detailStrength,
|
||||
double maximumOffsetRatio,
|
||||
int smoothingPasses,
|
||||
double maximumTurnDegrees
|
||||
) {
|
||||
public Meanders {
|
||||
if (primaryWavelength < 8 || primaryWavelength > 512
|
||||
|| detailWavelength < 4 || detailWavelength > 128
|
||||
|| detailWavelength > primaryWavelength
|
||||
|| smoothingPasses < 0 || smoothingPasses > 4
|
||||
|| !Double.isFinite(primaryStrength) || primaryStrength < 0D || primaryStrength > 2D
|
||||
|| !Double.isFinite(detailStrength) || detailStrength < 0D || detailStrength > 2D
|
||||
|| !Double.isFinite(maximumOffsetRatio) || maximumOffsetRatio < 0D || maximumOffsetRatio > 1D
|
||||
|| !Double.isFinite(maximumTurnDegrees)
|
||||
|| maximumTurnDegrees < 10D || maximumTurnDegrees > 150D) {
|
||||
throw new IllegalArgumentException("Hydrology meander geometry is invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record ChannelShape(
|
||||
double bedRoundness,
|
||||
double bedRoughness,
|
||||
double wallRoughness,
|
||||
int roughnessWavelength
|
||||
) {
|
||||
public ChannelShape {
|
||||
if (!Double.isFinite(bedRoundness) || bedRoundness < 1D || bedRoundness > 6D
|
||||
|| !Double.isFinite(bedRoughness) || bedRoughness < 0D || bedRoughness > 1D
|
||||
|| !Double.isFinite(wallRoughness) || wallRoughness < 0D || wallRoughness > 1D
|
||||
|| roughnessWavelength < 3 || roughnessWavelength > 128) {
|
||||
throw new IllegalArgumentException("Hydrology channel shape is invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record Drops(
|
||||
int cascadeRunPerBlock,
|
||||
double cascadeExponent,
|
||||
int maximumCascadeStep,
|
||||
double flowWidthRatio,
|
||||
int maximumFlowDepth,
|
||||
double basinWidthRatio,
|
||||
int maximumBasinDepth
|
||||
) {
|
||||
public Drops {
|
||||
if (cascadeRunPerBlock < 1 || cascadeRunPerBlock > 16
|
||||
|| !Double.isFinite(cascadeExponent) || cascadeExponent < 0.25D || cascadeExponent > 6D
|
||||
|| maximumCascadeStep < 1 || maximumCascadeStep > 4
|
||||
|| !Double.isFinite(flowWidthRatio) || flowWidthRatio < 0.25D || flowWidthRatio > 1D
|
||||
|| maximumFlowDepth < 1 || maximumFlowDepth > 16
|
||||
|| !Double.isFinite(basinWidthRatio) || basinWidthRatio < 1D || basinWidthRatio > 4D
|
||||
|| maximumBasinDepth < maximumFlowDepth || maximumBasinDepth > 32) {
|
||||
throw new IllegalArgumentException("Hydrology drop geometry is invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
public int flowWidth(int channelWidth) {
|
||||
return Math.max(1, Math.min(channelWidth, (int) StrictMath.ceil(channelWidth * flowWidthRatio)));
|
||||
}
|
||||
|
||||
public int stepLimit(HydrologyFeatureType type) {
|
||||
return type.isSurface() ? 1 : maximumCascadeStep;
|
||||
}
|
||||
|
||||
public int flowDepth(int channelDepth) {
|
||||
return Math.max(1, Math.min(channelDepth, maximumFlowDepth));
|
||||
}
|
||||
|
||||
public int basinWidth(int flowWidth) {
|
||||
return Math.max(flowWidth, (int) StrictMath.ceil(flowWidth * basinWidthRatio));
|
||||
}
|
||||
|
||||
public int basinDepth(int flowDepth, int drop) {
|
||||
int erodedDepth = Math.addExact(flowDepth, Math.max(1, (int) StrictMath.ceil(StrictMath.sqrt(drop))));
|
||||
return Math.max(flowDepth, Math.min(maximumBasinDepth, erodedDepth));
|
||||
}
|
||||
}
|
||||
|
||||
public record DeepFluid(
|
||||
String id,
|
||||
boolean enabled,
|
||||
double density,
|
||||
int spacing,
|
||||
int minimumY,
|
||||
int maximumY,
|
||||
int minimumHorizontalRadius,
|
||||
int maximumHorizontalRadius,
|
||||
int minimumVerticalRadius,
|
||||
int maximumVerticalRadius,
|
||||
int minimumChannelLength,
|
||||
int maximumChannelLength,
|
||||
int channelWidth,
|
||||
int channelDepth,
|
||||
int headroom,
|
||||
int maximumVolume,
|
||||
int maximumPerTile,
|
||||
boolean containedPools,
|
||||
boolean shortChannels
|
||||
) {
|
||||
public DeepFluid {
|
||||
if (id == null || id.isBlank()) {
|
||||
throw new IllegalArgumentException("Deep-fluid id must not be blank.");
|
||||
}
|
||||
id = id.trim();
|
||||
if (!Double.isFinite(density) || density < 0D || density > 64D || spacing < 16 || minimumY > maximumY) {
|
||||
throw new IllegalArgumentException("Deep-fluid density or height range is invalid.");
|
||||
}
|
||||
requireRange(minimumHorizontalRadius, maximumHorizontalRadius, 1, 128, "deep-fluid horizontal radius");
|
||||
requireRange(minimumVerticalRadius, maximumVerticalRadius, 1, 128, "deep-fluid vertical radius");
|
||||
requireRange(minimumChannelLength, maximumChannelLength, 0, 32_768, "deep-fluid channel length");
|
||||
if (channelWidth < 1 || channelWidth > 256 || channelDepth < 1 || channelDepth > 64 || headroom < 1
|
||||
|| maximumVolume < 64 || maximumVolume > 1_048_576
|
||||
|| maximumPerTile < 0 || maximumPerTile > 1024) {
|
||||
throw new IllegalArgumentException("Deep-fluid channel bounds are invalid.");
|
||||
}
|
||||
long poolEnvelope = ellipsoidVolume(
|
||||
maximumHorizontalRadius,
|
||||
Math.max(channelDepth, maximumVerticalRadius),
|
||||
headroom
|
||||
);
|
||||
if (enabled && containedPools && poolEnvelope > maximumVolume) {
|
||||
throw new IllegalArgumentException("Deep-fluid maximumVolume cannot contain its configured pool ellipsoid.");
|
||||
}
|
||||
if (enabled && !containedPools && !shortChannels) {
|
||||
throw new IllegalArgumentException("An enabled deep fluid requires a pool or channel.");
|
||||
}
|
||||
if (enabled && shortChannels && minimumChannelLength < 1) {
|
||||
throw new IllegalArgumentException("Enabled deep-fluid channels require a positive minimum length.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireRange(int minimum, int maximum, int lower, int upper, String name) {
|
||||
if (minimum < lower || maximum < minimum || maximum > upper) {
|
||||
throw new IllegalArgumentException(name + " range is invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireFiniteNonNegative(double value, String name) {
|
||||
if (!Double.isFinite(value) || value < 0D) {
|
||||
throw new IllegalArgumentException(name + " must be finite and non-negative.");
|
||||
}
|
||||
}
|
||||
|
||||
static long ellipsoidVolume(int radius, int lowerExtent, int upperExtent) {
|
||||
long volume = 0L;
|
||||
for (int deltaZ = -radius; deltaZ <= radius; deltaZ++) {
|
||||
for (int deltaX = -radius; deltaX <= radius; deltaX++) {
|
||||
double distance = StrictMath.hypot(deltaX, deltaZ);
|
||||
if (distance > radius + 0.25D) {
|
||||
continue;
|
||||
}
|
||||
double normalized = Math.min(1D, distance / Math.max(1D, radius));
|
||||
double scale = StrictMath.sqrt(Math.max(0D, 1D - normalized * normalized));
|
||||
int lower = (int) StrictMath.ceil(lowerExtent * scale);
|
||||
int upper = (int) StrictMath.floor(upperExtent * scale);
|
||||
volume += lower + upper + 1L;
|
||||
}
|
||||
}
|
||||
return volume;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public record HydrologyPoint(int x, int y, int z) {
|
||||
public long packedColumn() {
|
||||
return RiverFootprint.pack(x, z);
|
||||
}
|
||||
|
||||
public long distanceSquared2D(HydrologyPoint other) {
|
||||
long deltaX = (long) x - other.x;
|
||||
long deltaZ = (long) z - other.z;
|
||||
return deltaX * deltaX + deltaZ * deltaZ;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public record HydrologyRenderSample(int x, int z, List<HydrologyFeatureRef> features) {
|
||||
private static final Comparator<HydrologyFeatureRef> FEATURE_ORDER = Comparator
|
||||
.comparingInt((HydrologyFeatureRef feature) -> feature.type().renderPriority())
|
||||
.thenComparingLong(HydrologyFeatureRef::id);
|
||||
|
||||
public HydrologyRenderSample {
|
||||
Objects.requireNonNull(features, "features");
|
||||
ArrayList<HydrologyFeatureRef> ordered = new ArrayList<>(features);
|
||||
ordered.sort(FEATURE_ORDER);
|
||||
Map<Long, HydrologyFeatureRef> unique = new LinkedHashMap<>();
|
||||
for (HydrologyFeatureRef feature : ordered) {
|
||||
unique.putIfAbsent(feature.id(), feature);
|
||||
}
|
||||
features = List.copyOf(unique.values());
|
||||
}
|
||||
|
||||
public boolean present() {
|
||||
return !features.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasFeature(HydrologyFeatureType type) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
for (HydrologyFeatureRef feature : features) {
|
||||
if (feature.type() == type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Optional<HydrologyFeatureRef> primaryFeature() {
|
||||
return features.isEmpty() ? Optional.empty() : Optional.of(features.getFirst());
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public interface HydrologyRoutingTerrainSampler {
|
||||
HydrologyTerrainSample[] sampleGrid(GridRequest request);
|
||||
|
||||
NaturalClassification classifyNatural(int blockX, int blockZ);
|
||||
|
||||
enum NaturalClassification {
|
||||
LAND,
|
||||
OCEAN,
|
||||
UNAVAILABLE
|
||||
}
|
||||
|
||||
record GridRequest(int minimumX, int minimumZ, int width, int spacing) {
|
||||
public GridRequest {
|
||||
if (width < 1 || spacing < 1) {
|
||||
throw new IllegalArgumentException("Hydrology routing grid dimensions must be positive");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
final class HydrologyTerrainCaveVoxelView implements CaveVoxelView {
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> samples;
|
||||
private final int seaLevel;
|
||||
private final int minimumY;
|
||||
private final int maximumY;
|
||||
|
||||
HydrologyTerrainCaveVoxelView(HydrologyTerrainSampler sampler, int seaLevel, int worldHeight) {
|
||||
this(sampler, seaLevel, 0, worldHeight);
|
||||
}
|
||||
|
||||
HydrologyTerrainCaveVoxelView(
|
||||
HydrologyTerrainSampler sampler,
|
||||
int seaLevel,
|
||||
int minimumY,
|
||||
int maximumY
|
||||
) {
|
||||
this(sampler, seaLevel, minimumY, maximumY, new Long2ObjectOpenHashMap<>());
|
||||
}
|
||||
|
||||
private HydrologyTerrainCaveVoxelView(
|
||||
HydrologyTerrainSampler sampler,
|
||||
int seaLevel,
|
||||
int minimumY,
|
||||
int maximumY,
|
||||
Long2ObjectOpenHashMap<HydrologyTerrainSample> samples
|
||||
) {
|
||||
this.sampler = Objects.requireNonNull(sampler);
|
||||
this.samples = Objects.requireNonNull(samples);
|
||||
this.seaLevel = seaLevel;
|
||||
if (maximumY - minimumY < 3) {
|
||||
throw new IllegalArgumentException("The cave view requires at least three vertical blocks");
|
||||
}
|
||||
this.minimumY = minimumY;
|
||||
this.maximumY = maximumY;
|
||||
}
|
||||
|
||||
HydrologyTerrainCaveVoxelView withSampleCache(
|
||||
Long2ObjectOpenHashMap<HydrologyTerrainSample> sampleCache
|
||||
) {
|
||||
return new HydrologyTerrainCaveVoxelView(
|
||||
sampler,
|
||||
seaLevel,
|
||||
minimumY,
|
||||
maximumY,
|
||||
sampleCache
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInWorld(CavePosition position) {
|
||||
return position.y() > minimumY && position.y() < maximumY - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxel voxelAt(CavePosition position) {
|
||||
HydrologyTerrainSample terrain = sample(position.x(), position.z());
|
||||
if (position.y() <= terrain.naturalHeight()) {
|
||||
return CaveVoxel.SOLID;
|
||||
}
|
||||
if (terrain.ocean() && position.y() <= seaLevel) {
|
||||
return CaveVoxel.COMPATIBLE_FLUID;
|
||||
}
|
||||
return CaveVoxel.CAVE_AIR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpenToSurface(CavePosition position) {
|
||||
return isAboveTerrainSurface(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAboveTerrainSurface(CavePosition position) {
|
||||
HydrologyTerrainSample terrain = sample(position.x(), position.z());
|
||||
return position.y() > terrain.naturalHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAboveTerrainSurface(int x, int z, int requestedMinimumY, int requestedMaximumY) {
|
||||
int firstY = Math.max(requestedMinimumY, minimumY + 1);
|
||||
int lastY = Math.min(requestedMaximumY, maximumY - 2);
|
||||
return firstY <= lastY && lastY > sample(x, z).naturalHeight();
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z) {
|
||||
long key = (long) x << 32 ^ z & 0xffffffffL;
|
||||
HydrologyTerrainSample cached = samples.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
HydrologyTerrainSample sampled = Objects.requireNonNull(
|
||||
sampler.sample(x, z),
|
||||
"Hydrology terrain sampler returned null at " + x + "," + z
|
||||
);
|
||||
samples.put(key, sampled);
|
||||
return sampled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record HydrologyTerrainSample(
|
||||
int naturalHeight,
|
||||
double slope,
|
||||
boolean ocean,
|
||||
boolean caveAvailable,
|
||||
int caveFloorY,
|
||||
int caveFluidY,
|
||||
boolean transitAllowed,
|
||||
boolean outletAllowed,
|
||||
boolean surfaceSourceAllowed,
|
||||
boolean surfaceSourceRequired,
|
||||
boolean undergroundSourceAllowed,
|
||||
boolean undergroundSourceRequired,
|
||||
double routingCost,
|
||||
double surfaceSourceWeight,
|
||||
double undergroundSourceWeight,
|
||||
double widthMultiplier,
|
||||
double depthMultiplier,
|
||||
double incisionMultiplier,
|
||||
double routingMultiplier,
|
||||
String parentBiomeKey,
|
||||
String surfaceBiomeKey,
|
||||
String mouthBiomeKey,
|
||||
String shoreBiomeKey,
|
||||
String dryBiomeKey,
|
||||
String floodedCaveBiomeKey,
|
||||
List<String> preferredProfileKeys
|
||||
) {
|
||||
public HydrologyTerrainSample {
|
||||
requireFiniteNonNegative(slope, "slope");
|
||||
requireFiniteNonNegative(routingCost, "routingCost");
|
||||
requireFiniteNonNegative(surfaceSourceWeight, "surfaceSourceWeight");
|
||||
requireFiniteNonNegative(undergroundSourceWeight, "undergroundSourceWeight");
|
||||
requireFinitePositive(widthMultiplier, "widthMultiplier");
|
||||
requireFinitePositive(depthMultiplier, "depthMultiplier");
|
||||
requireFiniteNonNegative(incisionMultiplier, "incisionMultiplier");
|
||||
requireFiniteNonNegative(routingMultiplier, "routingMultiplier");
|
||||
if (caveAvailable && (caveFluidY <= caveFloorY || caveFluidY >= naturalHeight)) {
|
||||
throw new IllegalArgumentException("Available cave fluid must be above its floor and below natural terrain.");
|
||||
}
|
||||
parentBiomeKey = normalizeKey(parentBiomeKey, "parent");
|
||||
surfaceBiomeKey = normalizeKey(surfaceBiomeKey, parentBiomeKey);
|
||||
mouthBiomeKey = normalizeKey(mouthBiomeKey, surfaceBiomeKey);
|
||||
shoreBiomeKey = normalizeKey(shoreBiomeKey, parentBiomeKey);
|
||||
dryBiomeKey = normalizeKey(dryBiomeKey, parentBiomeKey);
|
||||
floodedCaveBiomeKey = normalizeKey(floodedCaveBiomeKey, surfaceBiomeKey);
|
||||
preferredProfileKeys = normalizeProfiles(preferredProfileKeys);
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSample openLand(int naturalHeight, double slope, String parentBiomeKey) {
|
||||
return new HydrologyTerrainSample(
|
||||
naturalHeight,
|
||||
slope,
|
||||
false,
|
||||
false,
|
||||
naturalHeight - 32,
|
||||
naturalHeight - 30,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
List.of("default")
|
||||
);
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSample ocean(int naturalHeight, String parentBiomeKey) {
|
||||
return new HydrologyTerrainSample(
|
||||
naturalHeight,
|
||||
0D,
|
||||
true,
|
||||
false,
|
||||
naturalHeight - 32,
|
||||
naturalHeight - 30,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
parentBiomeKey,
|
||||
List.of("default")
|
||||
);
|
||||
}
|
||||
|
||||
public HydrologyTerrainSample withSlope(double replacementSlope) {
|
||||
return new HydrologyTerrainSample(
|
||||
naturalHeight,
|
||||
replacementSlope,
|
||||
ocean,
|
||||
caveAvailable,
|
||||
caveFloorY,
|
||||
caveFluidY,
|
||||
transitAllowed,
|
||||
outletAllowed,
|
||||
surfaceSourceAllowed,
|
||||
surfaceSourceRequired,
|
||||
undergroundSourceAllowed,
|
||||
undergroundSourceRequired,
|
||||
routingCost,
|
||||
surfaceSourceWeight,
|
||||
undergroundSourceWeight,
|
||||
widthMultiplier,
|
||||
depthMultiplier,
|
||||
incisionMultiplier,
|
||||
routingMultiplier,
|
||||
parentBiomeKey,
|
||||
surfaceBiomeKey,
|
||||
mouthBiomeKey,
|
||||
shoreBiomeKey,
|
||||
dryBiomeKey,
|
||||
floodedCaveBiomeKey,
|
||||
preferredProfileKeys
|
||||
);
|
||||
}
|
||||
|
||||
private static List<String> normalizeProfiles(List<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return List.of("default");
|
||||
}
|
||||
if (profilesNormalized(values)) {
|
||||
return List.copyOf(values);
|
||||
}
|
||||
List<String> normalized = values.stream()
|
||||
.filter((String value) -> value != null && !value.isBlank())
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.sorted()
|
||||
.toList();
|
||||
return normalized.isEmpty() ? List.of("default") : normalized;
|
||||
}
|
||||
|
||||
private static boolean profilesNormalized(List<String> values) {
|
||||
String previous = null;
|
||||
for (String value : values) {
|
||||
if (value == null
|
||||
|| value.isBlank()
|
||||
|| !value.equals(value.trim())
|
||||
|| previous != null && previous.compareTo(value) >= 0) {
|
||||
return false;
|
||||
}
|
||||
previous = value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String normalizeKey(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value.trim();
|
||||
}
|
||||
|
||||
private static void requireFiniteNonNegative(double value, String name) {
|
||||
if (!Double.isFinite(value) || value < 0D) {
|
||||
throw new IllegalArgumentException(name + " must be finite and non-negative.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireFinitePositive(double value, String name) {
|
||||
if (!Double.isFinite(value) || value <= 0D) {
|
||||
throw new IllegalArgumentException(name + " must be finite and positive.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HydrologyTerrainSampler {
|
||||
HydrologyTerrainSample sample(int blockX, int blockZ);
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public final class HydrologyTile {
|
||||
private final HydrologyTileKey key;
|
||||
private final long worldSeed;
|
||||
private final long settingsFingerprint;
|
||||
private final int tileSize;
|
||||
private final List<DrainageNode> nodes;
|
||||
private final List<DrainageEdge> edges;
|
||||
private final List<RiverOutlet> outlets;
|
||||
private final List<RiverCourse> courses;
|
||||
private final List<HydrologyCavePlan> cavePlans;
|
||||
private final List<HydrologyDiagnosticCandidate> diagnosticCandidates;
|
||||
private final RiverFootprint footprint;
|
||||
private final Map<Long, DrainageNode> nodesById;
|
||||
private final Map<Long, RiverOutlet> outletsById;
|
||||
private final Map<Long, HydrologyCavePlan> cavePlansByCourseId;
|
||||
private final List<HydrologyFeatureRef> features;
|
||||
|
||||
public HydrologyTile(
|
||||
HydrologyTileKey key,
|
||||
long worldSeed,
|
||||
long settingsFingerprint,
|
||||
int tileSize,
|
||||
List<DrainageNode> nodes,
|
||||
List<DrainageEdge> edges,
|
||||
List<RiverOutlet> outlets,
|
||||
List<RiverCourse> courses,
|
||||
List<HydrologyCavePlan> cavePlans,
|
||||
List<HydrologyDiagnosticCandidate> diagnosticCandidates,
|
||||
RiverFootprint footprint
|
||||
) {
|
||||
this.key = Objects.requireNonNull(key, "key");
|
||||
if (tileSize <= 0) {
|
||||
throw new IllegalArgumentException("tileSize must be positive.");
|
||||
}
|
||||
this.worldSeed = worldSeed;
|
||||
this.settingsFingerprint = settingsFingerprint;
|
||||
this.tileSize = tileSize;
|
||||
this.nodes = sortedCopy(nodes, Comparator.comparingLong(DrainageNode::id));
|
||||
this.edges = sortedCopy(edges, Comparator.comparingLong(DrainageEdge::id));
|
||||
this.outlets = sortedCopy(outlets, Comparator.comparingLong(RiverOutlet::id));
|
||||
this.courses = sortedCopy(courses, Comparator.comparingLong(RiverCourse::id));
|
||||
this.cavePlans = sortedCopy(
|
||||
cavePlans,
|
||||
Comparator.comparingLong((HydrologyCavePlan plan) -> plan.source().sourceId())
|
||||
);
|
||||
this.diagnosticCandidates = sortedCopy(
|
||||
diagnosticCandidates,
|
||||
Comparator.comparingLong(HydrologyDiagnosticCandidate::id)
|
||||
);
|
||||
this.footprint = Objects.requireNonNull(footprint, "footprint");
|
||||
this.nodesById = indexNodes(this.nodes);
|
||||
this.outletsById = indexOutlets(this.outlets);
|
||||
this.cavePlansByCourseId = indexCavePlans(this.cavePlans);
|
||||
validateGraph();
|
||||
validateCourses();
|
||||
this.features = collectFeatures(footprint);
|
||||
}
|
||||
|
||||
public HydrologyTileKey key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public long worldSeed() {
|
||||
return worldSeed;
|
||||
}
|
||||
|
||||
public long settingsFingerprint() {
|
||||
return settingsFingerprint;
|
||||
}
|
||||
|
||||
public int tileSize() {
|
||||
return tileSize;
|
||||
}
|
||||
|
||||
public List<DrainageNode> nodes() {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public List<DrainageEdge> edges() {
|
||||
return edges;
|
||||
}
|
||||
|
||||
public List<RiverOutlet> outlets() {
|
||||
return outlets;
|
||||
}
|
||||
|
||||
public List<RiverCourse> courses() {
|
||||
return courses;
|
||||
}
|
||||
|
||||
public List<HydrologyCavePlan> cavePlans() {
|
||||
return cavePlans;
|
||||
}
|
||||
|
||||
public List<HydrologyDiagnosticCandidate> diagnosticCandidates() {
|
||||
return diagnosticCandidates;
|
||||
}
|
||||
|
||||
public RiverFootprint footprint() {
|
||||
return footprint;
|
||||
}
|
||||
|
||||
public Optional<DrainageNode> node(long id) {
|
||||
return Optional.ofNullable(nodesById.get(id));
|
||||
}
|
||||
|
||||
public Optional<RiverOutlet> outlet(long id) {
|
||||
return Optional.ofNullable(outletsById.get(id));
|
||||
}
|
||||
|
||||
public Optional<HydrologyCavePlan> cavePlan(long courseId) {
|
||||
return Optional.ofNullable(cavePlansByCourseId.get(courseId));
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnSample> columnAt(int x, int z) {
|
||||
return footprint.sample(x, z);
|
||||
}
|
||||
|
||||
public HydrologyRenderSample renderAt(int x, int z) {
|
||||
return footprint.renderSample(x, z);
|
||||
}
|
||||
|
||||
public HydrologyDiagnosticRenderSample diagnosticRenderAt(int x, int z, int maximumDistance) {
|
||||
if (maximumDistance < 0) {
|
||||
throw new IllegalArgumentException("maximumDistance cannot be negative.");
|
||||
}
|
||||
long maximumDistanceSquared = (long) maximumDistance * maximumDistance;
|
||||
ArrayList<HydrologyDiagnosticCandidate> selected = new ArrayList<>();
|
||||
for (HydrologyDiagnosticCandidate candidate : diagnosticCandidates) {
|
||||
long distance = candidate.point().distanceSquared2D(new HydrologyPoint(x, candidate.point().y(), z));
|
||||
if (distance <= maximumDistanceSquared) {
|
||||
selected.add(candidate);
|
||||
}
|
||||
}
|
||||
return new HydrologyDiagnosticRenderSample(x, z, selected);
|
||||
}
|
||||
|
||||
public List<HydrologyFeatureRef> features() {
|
||||
return features;
|
||||
}
|
||||
|
||||
public Optional<HydrologyFeatureRef> nearestFeature(
|
||||
HydrologyFeatureType type,
|
||||
int x,
|
||||
int z,
|
||||
int maximumDistance
|
||||
) {
|
||||
return nearestFeature(Set.of(Objects.requireNonNull(type, "type")), null, x, z, maximumDistance);
|
||||
}
|
||||
|
||||
public Optional<HydrologyFeatureRef> nearestFeature(
|
||||
Set<HydrologyFeatureType> types,
|
||||
String profileKey,
|
||||
int x,
|
||||
int z,
|
||||
int maximumDistance
|
||||
) {
|
||||
Objects.requireNonNull(types, "types");
|
||||
if (maximumDistance < 0) {
|
||||
throw new IllegalArgumentException("maximumDistance cannot be negative.");
|
||||
}
|
||||
long maximumDistanceSquared = (long) maximumDistance * maximumDistance;
|
||||
HydrologyFeatureRef nearest = null;
|
||||
long nearestDistance = Long.MAX_VALUE;
|
||||
for (RiverCourse course : courses) {
|
||||
if (profileKey != null && !profileKey.isBlank() && !profileKey.equals(course.profileKey())) {
|
||||
continue;
|
||||
}
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (!types.contains(segment.type())) {
|
||||
continue;
|
||||
}
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
long deltaX = (long) point.x() - x;
|
||||
long deltaZ = (long) point.z() - z;
|
||||
long distance = deltaX * deltaX + deltaZ * deltaZ;
|
||||
if (distance > maximumDistanceSquared || distance > nearestDistance) {
|
||||
continue;
|
||||
}
|
||||
HydrologyFeatureRef feature = publishedCenterlineFeature(segment, point);
|
||||
if (feature == null) {
|
||||
continue;
|
||||
}
|
||||
if (distance < nearestDistance || nearest == null || feature.id() < nearest.id()) {
|
||||
nearest = feature;
|
||||
nearestDistance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(nearest);
|
||||
}
|
||||
|
||||
public boolean acyclic() {
|
||||
for (DrainageEdge edge : edges) {
|
||||
DrainageNode upstream = nodesById.get(edge.upstreamNodeId());
|
||||
DrainageNode downstream = nodesById.get(edge.downstreamNodeId());
|
||||
if (upstream == null || downstream == null || downstream.potential() >= upstream.potential()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void validateGraph() {
|
||||
if (!acyclic()) {
|
||||
throw new IllegalArgumentException("Drainage edges must strictly lower potential.");
|
||||
}
|
||||
for (DrainageNode node : nodes) {
|
||||
if (!outletsById.containsKey(node.outletId())) {
|
||||
throw new IllegalArgumentException("Every drainage node must resolve to an accepted outlet.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCourses() {
|
||||
LinkedHashMap<Long, RiverCourse> coursesById = new LinkedHashMap<>();
|
||||
for (RiverCourse course : courses) {
|
||||
if (coursesById.put(course.id(), course) != null) {
|
||||
throw new IllegalArgumentException("Duplicate river course id: " + course.id());
|
||||
}
|
||||
if (!course.hydraulicallyNonRising()) {
|
||||
throw new IllegalArgumentException("Course heads must be non-rising downstream.");
|
||||
}
|
||||
if (course.sourceNodeId().isPresent() && !nodesById.containsKey(course.sourceNodeId().getAsLong())) {
|
||||
throw new IllegalArgumentException("Course source is not part of the accepted drainage graph.");
|
||||
}
|
||||
if (course.outletId().isPresent() && !outletsById.containsKey(course.outletId().getAsLong())) {
|
||||
throw new IllegalArgumentException("Course outlet is not accepted by this tile.");
|
||||
}
|
||||
if (isCaveBearing(course) != cavePlansByCourseId.containsKey(course.id())) {
|
||||
throw new IllegalArgumentException("Every cave-bearing course must have one accepted containment plan.");
|
||||
}
|
||||
}
|
||||
validateFootprintCourses(coursesById);
|
||||
}
|
||||
|
||||
private void validateFootprintCourses(Map<Long, RiverCourse> coursesById) {
|
||||
LinkedHashMap<Long, Map<CavePosition, HydrologyCaveAction>> expectedActions = new LinkedHashMap<>();
|
||||
for (HydrologyColumnSample sample : footprint.columns().values()) {
|
||||
ArrayList<Long> caveCourseIds = new ArrayList<>();
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
long courseId = layer.feature().courseId();
|
||||
if (!coursesById.containsKey(courseId)) {
|
||||
throw new IllegalArgumentException("Footprint references an unaccepted river course: " + courseId);
|
||||
}
|
||||
if (layer.oceanApron() || !layer.channel() || !layer.terrainOwned()
|
||||
|| !isCaveLayer(layer)) {
|
||||
continue;
|
||||
}
|
||||
Map<CavePosition, HydrologyCaveAction> courseActions = expectedActions.computeIfAbsent(
|
||||
courseId,
|
||||
(Long ignored) -> new HashMap<>()
|
||||
);
|
||||
if (!caveCourseIds.contains(courseId)) {
|
||||
caveCourseIds.add(courseId);
|
||||
}
|
||||
for (int y = layer.bedY() + 1; y <= layer.ceilingY(); y++) {
|
||||
CavePosition position = new CavePosition(sample.x(), y, sample.z());
|
||||
HydrologyCaveAction action = actionAt(layer, y);
|
||||
HydrologyCaveAction existing = courseActions.get(position);
|
||||
if (existing == null || actionPriority(action) < actionPriority(existing)) {
|
||||
courseActions.put(position, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
composeSurfaceActions(sample, caveCourseIds, coursesById, expectedActions);
|
||||
}
|
||||
HashSet<Long> validatedCourseIds = new HashSet<>();
|
||||
for (RiverCourse course : courses) {
|
||||
if (!isCaveBearing(course)) {
|
||||
continue;
|
||||
}
|
||||
if (!validatedCourseIds.add(course.id())) {
|
||||
continue;
|
||||
}
|
||||
List<RiverCourse> sharedCourses = sharedOutletCaveCourses(course);
|
||||
HashMap<CavePosition, HydrologyCaveAction> expected = new HashMap<>();
|
||||
HashMap<CavePosition, HydrologyCaveAction> actual = new HashMap<>();
|
||||
for (RiverCourse sharedCourse : sharedCourses) {
|
||||
validatedCourseIds.add(sharedCourse.id());
|
||||
mergeCaveActions(
|
||||
expected,
|
||||
expectedActions.getOrDefault(sharedCourse.id(), Map.of())
|
||||
);
|
||||
HydrologyCavePlan plan = cavePlansByCourseId.get(sharedCourse.id());
|
||||
plan.forEachAction((CavePosition position, HydrologyCaveAction action) -> {
|
||||
if (action != HydrologyCaveAction.SEAL_GUARD) {
|
||||
mergeCaveAction(actual, position, action);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!actual.equals(expected)) {
|
||||
throw caveFootprintMismatch(course.id(), expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<RiverCourse> sharedOutletCaveCourses(RiverCourse course) {
|
||||
if (course.outletId().isEmpty()) {
|
||||
return List.of(course);
|
||||
}
|
||||
long outletId = course.outletId().getAsLong();
|
||||
ArrayList<RiverCourse> shared = new ArrayList<>();
|
||||
for (RiverCourse candidate : courses) {
|
||||
if (isCaveBearing(candidate)
|
||||
&& candidate.outletId().isPresent()
|
||||
&& candidate.outletId().getAsLong() == outletId
|
||||
&& candidate.profileKey().equals(course.profileKey())) {
|
||||
shared.add(candidate);
|
||||
}
|
||||
}
|
||||
return List.copyOf(shared);
|
||||
}
|
||||
|
||||
private void mergeCaveActions(
|
||||
Map<CavePosition, HydrologyCaveAction> target,
|
||||
Map<CavePosition, HydrologyCaveAction> source
|
||||
) {
|
||||
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : source.entrySet()) {
|
||||
mergeCaveAction(target, entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeCaveAction(
|
||||
Map<CavePosition, HydrologyCaveAction> actions,
|
||||
CavePosition position,
|
||||
HydrologyCaveAction action
|
||||
) {
|
||||
HydrologyCaveAction existing = actions.get(position);
|
||||
if (existing == null || actionPriority(action) < actionPriority(existing)) {
|
||||
actions.put(position, action);
|
||||
}
|
||||
}
|
||||
|
||||
private IllegalArgumentException caveFootprintMismatch(
|
||||
long courseId,
|
||||
Map<CavePosition, HydrologyCaveAction> expected,
|
||||
Map<CavePosition, HydrologyCaveAction> actual
|
||||
) {
|
||||
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : expected.entrySet()) {
|
||||
HydrologyCaveAction actualAction = actual.get(entry.getKey());
|
||||
if (actualAction != entry.getValue()) {
|
||||
return new IllegalArgumentException("Cave footprint differs from its accepted containment plan: "
|
||||
+ courseId + " at " + entry.getKey() + ", footprint=" + entry.getValue()
|
||||
+ ", plan=" + actualAction + ".");
|
||||
}
|
||||
}
|
||||
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : actual.entrySet()) {
|
||||
if (!expected.containsKey(entry.getKey())) {
|
||||
return new IllegalArgumentException("Cave footprint differs from its accepted containment plan: "
|
||||
+ courseId + " has extra plan action " + entry.getValue() + " at " + entry.getKey() + ".");
|
||||
}
|
||||
}
|
||||
return new IllegalArgumentException("Cave footprint differs from its accepted containment plan: "
|
||||
+ courseId + ".");
|
||||
}
|
||||
|
||||
private void composeSurfaceActions(
|
||||
HydrologyColumnSample sample,
|
||||
List<Long> caveCourseIds,
|
||||
Map<Long, RiverCourse> coursesById,
|
||||
Map<Long, Map<CavePosition, HydrologyCaveAction>> expectedActions
|
||||
) {
|
||||
if (caveCourseIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int minimumY = Integer.MAX_VALUE;
|
||||
int maximumY = Integer.MIN_VALUE;
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (!layer.publishesSurfaceFluid()) {
|
||||
continue;
|
||||
}
|
||||
minimumY = Math.min(minimumY, layer.bedY() + 1);
|
||||
maximumY = Math.max(maximumY, layer.fluidHeadY());
|
||||
}
|
||||
for (int y = minimumY; y <= maximumY; y++) {
|
||||
HydrologyColumnSample.SurfacePublicationCell surfaceCell = sample
|
||||
.surfacePublicationCellAt(y)
|
||||
.orElse(null);
|
||||
if (surfaceCell == null) {
|
||||
continue;
|
||||
}
|
||||
CavePosition position = new CavePosition(sample.x(), y, sample.z());
|
||||
for (long courseId : caveCourseIds) {
|
||||
Map<CavePosition, HydrologyCaveAction> courseActions = expectedActions.get(courseId);
|
||||
if (!courseActions.containsKey(position)) {
|
||||
continue;
|
||||
}
|
||||
RiverCourse course = coursesById.get(courseId);
|
||||
if (!course.profileKey().equals(surfaceCell.layer().profileKey())) {
|
||||
throw new IllegalArgumentException("Surface and cave hydrology profiles conflict at "
|
||||
+ position + ".");
|
||||
}
|
||||
courseActions.put(position, surfaceCell.action());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCaveBearing(RiverCourse course) {
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (segment.type().isUnderground() || segment.type().isDeepFluid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCaveLayer(HydrologyColumnLayer layer) {
|
||||
return layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid();
|
||||
}
|
||||
|
||||
private HydrologyCaveAction actionAt(HydrologyColumnLayer layer, int y) {
|
||||
if (y > layer.fluidHeadY()) {
|
||||
return HydrologyCaveAction.DRY_AIR;
|
||||
}
|
||||
if (layer.fallingFluid() && y < layer.fluidHeadY()) {
|
||||
return HydrologyCaveAction.FALLING_FLUID;
|
||||
}
|
||||
return HydrologyCaveAction.WET_SOURCE;
|
||||
}
|
||||
|
||||
private int actionPriority(HydrologyCaveAction action) {
|
||||
return switch (action) {
|
||||
case WET_SOURCE -> 0;
|
||||
case FALLING_FLUID -> 1;
|
||||
case DRY_AIR -> 2;
|
||||
case SEAL_GUARD -> 3;
|
||||
};
|
||||
}
|
||||
|
||||
private static Map<Long, DrainageNode> indexNodes(List<DrainageNode> values) {
|
||||
LinkedHashMap<Long, DrainageNode> indexed = new LinkedHashMap<>();
|
||||
for (DrainageNode value : values) {
|
||||
if (indexed.put(value.id(), value) != null) {
|
||||
throw new IllegalArgumentException("Duplicate drainage node id: " + value.id());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(indexed);
|
||||
}
|
||||
|
||||
private static Map<Long, RiverOutlet> indexOutlets(List<RiverOutlet> values) {
|
||||
LinkedHashMap<Long, RiverOutlet> indexed = new LinkedHashMap<>();
|
||||
for (RiverOutlet value : values) {
|
||||
if (indexed.put(value.id(), value) != null) {
|
||||
throw new IllegalArgumentException("Duplicate river outlet id: " + value.id());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(indexed);
|
||||
}
|
||||
|
||||
private static Map<Long, HydrologyCavePlan> indexCavePlans(List<HydrologyCavePlan> values) {
|
||||
LinkedHashMap<Long, HydrologyCavePlan> indexed = new LinkedHashMap<>();
|
||||
for (HydrologyCavePlan value : values) {
|
||||
if (!value.accepted()) {
|
||||
throw new IllegalArgumentException("Hydrology tiles cannot contain rejected cave plans.");
|
||||
}
|
||||
long courseId = value.source().sourceId();
|
||||
if (indexed.put(courseId, value) != null) {
|
||||
throw new IllegalArgumentException("Duplicate cave plan for course: " + courseId);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(indexed);
|
||||
}
|
||||
|
||||
private HydrologyFeatureRef publishedCenterlineFeature(
|
||||
HydraulicSegment segment,
|
||||
HydrologyPoint point
|
||||
) {
|
||||
HydrologyColumnSample column = footprint.columns().get(RiverFootprint.pack(point.x(), point.z()));
|
||||
if (column == null) {
|
||||
return null;
|
||||
}
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
HydrologyFeatureRef feature = layer.feature();
|
||||
if (layer.channel()
|
||||
&& feature.segmentId() == segment.id()
|
||||
&& feature.type() == segment.type()) {
|
||||
if (feature.x() == point.x() && feature.z() == point.z()) {
|
||||
return feature;
|
||||
}
|
||||
return new HydrologyFeatureRef(
|
||||
feature.id(),
|
||||
feature.type(),
|
||||
feature.courseId(),
|
||||
feature.segmentId(),
|
||||
point.x(),
|
||||
layer.fluidHeadY(),
|
||||
point.z(),
|
||||
feature.flowDeltaX(),
|
||||
feature.flowDeltaZ(),
|
||||
feature.source()
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<HydrologyFeatureRef> collectFeatures(RiverFootprint footprint) {
|
||||
HashMap<Long, HydrologyColumnLayer> representatives = new HashMap<>();
|
||||
for (HydrologyColumnSample column : footprint.columns().values()) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
long segmentId = layer.feature().segmentId();
|
||||
HydrologyColumnLayer existing = representatives.get(segmentId);
|
||||
if (existing == null || featureOrder(layer, existing) < 0) {
|
||||
representatives.put(segmentId, layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
ArrayList<HydrologyFeatureRef> ordered = new ArrayList<>(representatives.size());
|
||||
for (HydrologyColumnLayer layer : representatives.values()) {
|
||||
ordered.add(layer.feature());
|
||||
}
|
||||
ordered.sort(Comparator.comparingInt((HydrologyFeatureRef feature) -> feature.type().renderPriority())
|
||||
.thenComparingLong(HydrologyFeatureRef::id));
|
||||
return List.copyOf(ordered);
|
||||
}
|
||||
|
||||
private static int featureOrder(HydrologyColumnLayer first, HydrologyColumnLayer second) {
|
||||
int role = Integer.compare(featureRole(first), featureRole(second));
|
||||
if (role != 0) {
|
||||
return role;
|
||||
}
|
||||
HydrologyFeatureRef firstFeature = first.feature();
|
||||
HydrologyFeatureRef secondFeature = second.feature();
|
||||
int y = Integer.compare(firstFeature.y(), secondFeature.y());
|
||||
if (y != 0) {
|
||||
return y;
|
||||
}
|
||||
int x = Integer.compare(firstFeature.x(), secondFeature.x());
|
||||
return x != 0 ? x : Integer.compare(firstFeature.z(), secondFeature.z());
|
||||
}
|
||||
|
||||
private static int featureRole(HydrologyColumnLayer layer) {
|
||||
if (layer.feature().source()) {
|
||||
return 0;
|
||||
}
|
||||
if (layer.fallingFluid()) {
|
||||
return 1;
|
||||
}
|
||||
if (layer.receivingPool()) {
|
||||
return 2;
|
||||
}
|
||||
if (layer.channel() && !layer.oceanApron()) {
|
||||
return 3;
|
||||
}
|
||||
if (layer.channel()) {
|
||||
return 4;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
private static <T> List<T> sortedCopy(List<T> values, Comparator<T> comparator) {
|
||||
Objects.requireNonNull(values, "values");
|
||||
ArrayList<T> ordered = new ArrayList<>(values);
|
||||
ordered.sort(comparator);
|
||||
return List.copyOf(ordered);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof HydrologyTile tile)) {
|
||||
return false;
|
||||
}
|
||||
return worldSeed == tile.worldSeed
|
||||
&& settingsFingerprint == tile.settingsFingerprint
|
||||
&& tileSize == tile.tileSize
|
||||
&& key.equals(tile.key)
|
||||
&& nodes.equals(tile.nodes)
|
||||
&& edges.equals(tile.edges)
|
||||
&& outlets.equals(tile.outlets)
|
||||
&& courses.equals(tile.courses)
|
||||
&& cavePlans.equals(tile.cavePlans)
|
||||
&& diagnosticCandidates.equals(tile.diagnosticCandidates)
|
||||
&& footprint.equals(tile.footprint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(
|
||||
key,
|
||||
worldSeed,
|
||||
settingsFingerprint,
|
||||
tileSize,
|
||||
nodes,
|
||||
edges,
|
||||
outlets,
|
||||
courses,
|
||||
cavePlans,
|
||||
diagnosticCandidates,
|
||||
footprint
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HydrologyTile[key=" + key + ", courses=" + courses.size() + ", candidates="
|
||||
+ diagnosticCandidates.size() + ", columns=" + footprint.size() + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public final class HydrologyTileCache implements AutoCloseable {
|
||||
private static final int DEFAULT_MAXIMUM_ENTRIES = 64;
|
||||
private static final int MAXIMUM_COMPOSED_CHUNKS = 256;
|
||||
private static final int CHUNK_SIZE = 16;
|
||||
private static final int CHUNK_COLUMN_COUNT = CHUNK_SIZE * CHUNK_SIZE;
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final int maximumEntries;
|
||||
private final Cache<HydrologyTileKey, HydrologyTile> tiles;
|
||||
private final Cache<Long, ChunkColumns> composedChunks;
|
||||
private final AtomicLong cacheEpoch;
|
||||
private final ThreadLocal<LocalChunkColumns> localChunkColumns;
|
||||
|
||||
public HydrologyTileCache(HydrologyPlanner planner) {
|
||||
this(planner, DEFAULT_MAXIMUM_ENTRIES);
|
||||
}
|
||||
|
||||
public HydrologyTileCache(HydrologyPlanner planner, int maximumEntries) {
|
||||
this.planner = Objects.requireNonNull(planner, "planner");
|
||||
if (maximumEntries < 1) {
|
||||
throw new IllegalArgumentException("maximumEntries must be positive.");
|
||||
}
|
||||
this.maximumEntries = maximumEntries;
|
||||
this.tiles = Caffeine.newBuilder()
|
||||
.maximumSize(maximumEntries)
|
||||
.build();
|
||||
this.composedChunks = Caffeine.newBuilder()
|
||||
.maximumSize(MAXIMUM_COMPOSED_CHUNKS)
|
||||
.build();
|
||||
this.cacheEpoch = new AtomicLong();
|
||||
this.localChunkColumns = new ThreadLocal<>();
|
||||
}
|
||||
|
||||
public HydrologyTile get(HydrologyTileKey key) {
|
||||
Objects.requireNonNull(key, "key");
|
||||
return tiles.get(key, planner::plan);
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
|
||||
return chunkColumns(blockX, blockZ).columnAt(blockX, blockZ);
|
||||
}
|
||||
|
||||
public void prepareChunkColumns(int blockX, int blockZ) {
|
||||
chunkColumns(blockX, blockZ);
|
||||
}
|
||||
|
||||
public HydrologyRenderSample renderAt(int blockX, int blockZ) {
|
||||
return columnAt(blockX, blockZ)
|
||||
.map(HydrologyColumnSample::renderSample)
|
||||
.orElseGet(() -> new HydrologyRenderSample(blockX, blockZ, List.of()));
|
||||
}
|
||||
|
||||
public void invalidate(HydrologyTileKey key) {
|
||||
Objects.requireNonNull(key, "key");
|
||||
clear();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
cacheEpoch.incrementAndGet();
|
||||
planner.clearOwnerDrafts();
|
||||
tiles.invalidateAll();
|
||||
composedChunks.invalidateAll();
|
||||
localChunkColumns.remove();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
tiles.cleanUp();
|
||||
return Math.toIntExact(tiles.estimatedSize());
|
||||
}
|
||||
|
||||
public int maximumEntries() {
|
||||
return maximumEntries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
clear();
|
||||
}
|
||||
|
||||
private ChunkColumns chunkColumns(int blockX, int blockZ) {
|
||||
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
|
||||
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
|
||||
long epoch = cacheEpoch.get();
|
||||
LocalChunkColumns local = localChunkColumns.get();
|
||||
if (local != null && local.epoch() == epoch
|
||||
&& local.columns().chunkX() == chunkX
|
||||
&& local.columns().chunkZ() == chunkZ) {
|
||||
return local.columns();
|
||||
}
|
||||
long packedChunk = RiverFootprint.pack(chunkX, chunkZ);
|
||||
ChunkColumns columns = composedChunks.get(
|
||||
packedChunk,
|
||||
ignored -> composeChunkColumns(chunkX, chunkZ)
|
||||
);
|
||||
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
|
||||
return columns;
|
||||
}
|
||||
|
||||
private ChunkColumns composeChunkColumns(int chunkX, int chunkZ) {
|
||||
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
|
||||
int minimumBlockZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
|
||||
int tileSize = planner.settings().routing().tileSize();
|
||||
int publicationRadius = planner.settings().publicationRadius();
|
||||
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
|
||||
int maximumTileX = tileCoordinate((long) minimumBlockX + CHUNK_SIZE - 1L + publicationRadius, tileSize);
|
||||
int minimumTileZ = tileCoordinate((long) minimumBlockZ - publicationRadius, tileSize);
|
||||
int maximumTileZ = tileCoordinate((long) minimumBlockZ + CHUNK_SIZE - 1L + publicationRadius, tileSize);
|
||||
ArrayList<HydrologyTileKey> relevantKeys = new ArrayList<>(
|
||||
Math.multiplyExact(maximumTileX - minimumTileX + 1, maximumTileZ - minimumTileZ + 1)
|
||||
);
|
||||
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
|
||||
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
|
||||
relevantKeys.add(new HydrologyTileKey(tileX, tileZ));
|
||||
}
|
||||
}
|
||||
List<HydrologyTile> relevantTiles = loadTiles(relevantKeys);
|
||||
HydrologyColumnSample[] columns = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
|
||||
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
|
||||
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
|
||||
int blockX = minimumBlockX + localX;
|
||||
int blockZ = minimumBlockZ + localZ;
|
||||
columns[localZ * CHUNK_SIZE + localX] = composeColumn(
|
||||
blockX,
|
||||
blockZ,
|
||||
relevantKeys,
|
||||
relevantTiles
|
||||
)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
return new ChunkColumns(chunkX, chunkZ, columns);
|
||||
}
|
||||
|
||||
private List<HydrologyTile> loadTiles(List<HydrologyTileKey> keys) {
|
||||
if (keys.size() < 2
|
||||
|| !IrisPlatforms.isBound()) {
|
||||
ArrayList<HydrologyTile> loaded = new ArrayList<>(keys.size());
|
||||
for (HydrologyTileKey key : keys) {
|
||||
loaded.add(get(key));
|
||||
}
|
||||
return List.copyOf(loaded);
|
||||
}
|
||||
ArrayList<CompletableFuture<HydrologyTile>> futures = new ArrayList<>(keys.size());
|
||||
for (HydrologyTileKey key : keys) {
|
||||
futures.add(CompletableFuture.supplyAsync(() -> get(key), MultiBurst.burst));
|
||||
}
|
||||
ArrayList<HydrologyTile> loaded = new ArrayList<>(keys.size());
|
||||
for (CompletableFuture<HydrologyTile> future : futures) {
|
||||
loaded.add(awaitTile(future));
|
||||
}
|
||||
return List.copyOf(loaded);
|
||||
}
|
||||
|
||||
private static HydrologyTile awaitTile(CompletableFuture<HydrologyTile> future) {
|
||||
try {
|
||||
return future.join();
|
||||
} catch (CompletionException failure) {
|
||||
Throwable cause = failure.getCause();
|
||||
if (cause instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (cause instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException("Hydrology tile planning failed.", cause);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<HydrologyColumnSample> composeColumn(
|
||||
int blockX,
|
||||
int blockZ,
|
||||
List<HydrologyTileKey> relevantKeys,
|
||||
List<HydrologyTile> relevantTiles
|
||||
) {
|
||||
ArrayList<HydrologyColumnLayer> layers = new ArrayList<>();
|
||||
HydrologyColumnSample template = null;
|
||||
int tileSize = planner.settings().routing().tileSize();
|
||||
int publicationRadius = planner.settings().publicationRadius();
|
||||
int minimumTileX = tileCoordinate((long) blockX - publicationRadius, tileSize);
|
||||
int maximumTileX = tileCoordinate((long) blockX + publicationRadius, tileSize);
|
||||
int minimumTileZ = tileCoordinate((long) blockZ - publicationRadius, tileSize);
|
||||
int maximumTileZ = tileCoordinate((long) blockZ + publicationRadius, tileSize);
|
||||
for (int tileIndex = 0; tileIndex < relevantTiles.size(); tileIndex++) {
|
||||
HydrologyTileKey key = relevantKeys.get(tileIndex);
|
||||
if (key.tileX() < minimumTileX || key.tileX() > maximumTileX
|
||||
|| key.tileZ() < minimumTileZ || key.tileZ() > maximumTileZ) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTile tile = relevantTiles.get(tileIndex);
|
||||
Optional<HydrologyColumnSample> candidate = tile.columnAt(blockX, blockZ);
|
||||
if (candidate.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
HydrologyColumnSample sample = candidate.get();
|
||||
if (template == null) {
|
||||
template = sample;
|
||||
} else {
|
||||
requireMatchingTerrain(template, sample);
|
||||
}
|
||||
layers.addAll(sample.layers());
|
||||
}
|
||||
if (template == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
LinkedHashMap<Long, HydrologyColumnLayer> unique = new LinkedHashMap<>();
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
HydrologyColumnLayer existing = unique.putIfAbsent(layer.feature().id(), layer);
|
||||
if (existing != null && !existing.equals(layer)) {
|
||||
throw new IllegalStateException("Hydrology feature id collision at " + blockX + "," + blockZ + ".");
|
||||
}
|
||||
}
|
||||
return Optional.of(new HydrologyColumnSample(
|
||||
blockX,
|
||||
blockZ,
|
||||
template.naturalHeight(),
|
||||
template.seaLevel(),
|
||||
template.ocean(),
|
||||
template.parentBiomeKey(),
|
||||
new ArrayList<>(unique.values())
|
||||
));
|
||||
}
|
||||
|
||||
private static int tileCoordinate(long blockCoordinate, int tileSize) {
|
||||
return Math.toIntExact(Math.floorDiv(blockCoordinate, tileSize));
|
||||
}
|
||||
|
||||
private static void requireMatchingTerrain(
|
||||
HydrologyColumnSample expected,
|
||||
HydrologyColumnSample actual
|
||||
) {
|
||||
if (expected.naturalHeight() != actual.naturalHeight()
|
||||
|| expected.seaLevel() != actual.seaLevel()
|
||||
|| expected.ocean() != actual.ocean()
|
||||
|| !expected.parentBiomeKey().equals(actual.parentBiomeKey())) {
|
||||
throw new IllegalStateException(
|
||||
"Hydrology plans disagree on terrain metadata at " + expected.x() + "," + expected.z() + "."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private record ChunkColumns(int chunkX, int chunkZ, HydrologyColumnSample[] columns) {
|
||||
private Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
|
||||
int localX = Math.floorMod(blockX, CHUNK_SIZE);
|
||||
int localZ = Math.floorMod(blockZ, CHUNK_SIZE);
|
||||
return Optional.ofNullable(columns[localZ * CHUNK_SIZE + localX]);
|
||||
}
|
||||
}
|
||||
|
||||
private record LocalChunkColumns(long epoch, ChunkColumns columns) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public record HydrologyTileKey(int tileX, int tileZ) implements Comparable<HydrologyTileKey> {
|
||||
public static HydrologyTileKey fromBlock(int blockX, int blockZ, int tileSize) {
|
||||
if (tileSize <= 0) {
|
||||
throw new IllegalArgumentException("Tile size must be positive.");
|
||||
}
|
||||
return new HydrologyTileKey(Math.floorDiv(blockX, tileSize), Math.floorDiv(blockZ, tileSize));
|
||||
}
|
||||
|
||||
public int minimumBlockX(int tileSize) {
|
||||
return Math.multiplyExact(tileX, tileSize);
|
||||
}
|
||||
|
||||
public int minimumBlockZ(int tileSize) {
|
||||
return Math.multiplyExact(tileZ, tileSize);
|
||||
}
|
||||
|
||||
public boolean contains(int blockX, int blockZ, int tileSize) {
|
||||
int minimumX = minimumBlockX(tileSize);
|
||||
int minimumZ = minimumBlockZ(tileSize);
|
||||
return blockX >= minimumX && blockX < minimumX + tileSize
|
||||
&& blockZ >= minimumZ && blockZ < minimumZ + tileSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(HydrologyTileKey other) {
|
||||
int xComparison = Integer.compare(tileX, other.tileX);
|
||||
return xComparison != 0 ? xComparison : Integer.compare(tileZ, other.tileZ);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
public record RiverCourse(
|
||||
long id,
|
||||
RiverCourseType type,
|
||||
OptionalLong sourceNodeId,
|
||||
OptionalLong outletId,
|
||||
String profileKey,
|
||||
int discharge,
|
||||
List<DrainageEdge> drainageEdges,
|
||||
List<HydraulicSegment> segments
|
||||
) {
|
||||
public RiverCourse {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(sourceNodeId, "sourceNodeId");
|
||||
Objects.requireNonNull(outletId, "outletId");
|
||||
Objects.requireNonNull(drainageEdges, "drainageEdges");
|
||||
Objects.requireNonNull(segments, "segments");
|
||||
if (profileKey == null || profileKey.isBlank()) {
|
||||
throw new IllegalArgumentException("profileKey must not be blank.");
|
||||
}
|
||||
profileKey = profileKey.trim();
|
||||
if (discharge <= 0) {
|
||||
throw new IllegalArgumentException("Course discharge must be positive.");
|
||||
}
|
||||
drainageEdges = List.copyOf(drainageEdges);
|
||||
segments = List.copyOf(segments);
|
||||
if (segments.isEmpty()) {
|
||||
throw new IllegalArgumentException("A river course requires at least one hydraulic segment.");
|
||||
}
|
||||
for (HydraulicSegment segment : segments) {
|
||||
if (segment.courseId() != id) {
|
||||
throw new IllegalArgumentException("Every segment must belong to its containing course.");
|
||||
}
|
||||
}
|
||||
validateSinkholeContinuation(type, segments);
|
||||
if (type == RiverCourseType.DEEP_FLUID && (sourceNodeId.isPresent() || outletId.isPresent())) {
|
||||
throw new IllegalArgumentException("Independent deep-fluid courses cannot reference drainage nodes or river outlets.");
|
||||
}
|
||||
if (type != RiverCourseType.DEEP_FLUID && (sourceNodeId.isEmpty() || outletId.isEmpty())) {
|
||||
throw new IllegalArgumentException("Surface and underground courses require a source and outlet.");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hydraulicallyNonRising() {
|
||||
int previousHead = Integer.MAX_VALUE;
|
||||
for (HydraulicSegment segment : segments) {
|
||||
if (segment.upstreamHeadY() > previousHead || segment.downstreamHeadY() > segment.upstreamHeadY()) {
|
||||
return false;
|
||||
}
|
||||
previousHead = segment.downstreamHeadY();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean surfaceSinkholeContinuation() {
|
||||
return type == RiverCourseType.SURFACE
|
||||
&& segments.size() >= 2
|
||||
&& segments.get(segments.size() - 2).type() == HydrologyFeatureType.SINKHOLE
|
||||
&& segments.getLast().type() == HydrologyFeatureType.INLAND_GROTTO;
|
||||
}
|
||||
|
||||
private static void validateSinkholeContinuation(
|
||||
RiverCourseType courseType,
|
||||
List<HydraulicSegment> segments
|
||||
) {
|
||||
int sinkholeCount = 0;
|
||||
for (HydraulicSegment segment : segments) {
|
||||
if (segment.type() == HydrologyFeatureType.SINKHOLE) {
|
||||
sinkholeCount++;
|
||||
}
|
||||
}
|
||||
boolean terminalInlandGrotto = courseType == RiverCourseType.SURFACE
|
||||
&& segments.getLast().type() == HydrologyFeatureType.INLAND_GROTTO;
|
||||
if (!terminalInlandGrotto) {
|
||||
if (sinkholeCount > 0) {
|
||||
throw new IllegalArgumentException("A sinkhole must link a surface course to its terminal inland grotto.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (sinkholeCount != 1 || segments.size() < 3) {
|
||||
throw new IllegalArgumentException("A surface inland grotto requires one explicit sinkhole link and surface lip.");
|
||||
}
|
||||
HydraulicSegment lip = segments.get(segments.size() - 3);
|
||||
HydraulicSegment sinkhole = segments.get(segments.size() - 2);
|
||||
HydraulicSegment grotto = segments.getLast();
|
||||
if (!lip.type().isSurface() && lip.type() != HydrologyFeatureType.RIDGE_BORE
|
||||
|| sinkhole.type() != HydrologyFeatureType.SINKHOLE
|
||||
|| sinkhole.drop() <= 0
|
||||
|| !sinkhole.receivingPool()
|
||||
|| lip.downstreamHeadY() != sinkhole.upstreamHeadY()
|
||||
|| sinkhole.downstreamHeadY() != grotto.upstreamHeadY()
|
||||
|| grotto.upstreamHeadY() != grotto.downstreamHeadY()
|
||||
|| lip.end().y() != sinkhole.upstreamHeadY()
|
||||
|| sinkhole.start().y() != sinkhole.upstreamHeadY()
|
||||
|| sinkhole.end().y() != sinkhole.downstreamHeadY()
|
||||
|| grotto.start().y() != grotto.upstreamHeadY()
|
||||
|| lip.end().distanceSquared2D(sinkhole.start()) != 0L
|
||||
|| sinkhole.end().distanceSquared2D(grotto.start()) != 0L) {
|
||||
throw new IllegalArgumentException("A surface sinkhole requires a connected lip, graded descent, and receiving grotto.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
public enum RiverCourseType {
|
||||
SURFACE,
|
||||
UNDERGROUND,
|
||||
DEEP_FLUID
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public final class RiverFootprint {
|
||||
private final Map<Long, HydrologyColumnSample> columns;
|
||||
|
||||
public RiverFootprint(Map<Long, HydrologyColumnSample> columns) {
|
||||
if (columns == null) {
|
||||
throw new IllegalArgumentException("columns must not be null.");
|
||||
}
|
||||
TreeMap<Long, HydrologyColumnSample> ordered = new TreeMap<>();
|
||||
for (Map.Entry<Long, HydrologyColumnSample> entry : columns.entrySet()) {
|
||||
HydrologyColumnSample sample = entry.getValue();
|
||||
if (sample == null || entry.getKey() != pack(sample.x(), sample.z())) {
|
||||
throw new IllegalArgumentException("Footprint keys must match their column coordinates.");
|
||||
}
|
||||
ordered.put(entry.getKey(), sample);
|
||||
}
|
||||
Long2ObjectLinkedOpenHashMap<HydrologyColumnSample> indexed =
|
||||
new Long2ObjectLinkedOpenHashMap<>(ordered.size());
|
||||
for (Map.Entry<Long, HydrologyColumnSample> entry : ordered.entrySet()) {
|
||||
indexed.put(entry.getKey().longValue(), entry.getValue());
|
||||
}
|
||||
columns = Collections.unmodifiableMap(indexed);
|
||||
this.columns = columns;
|
||||
}
|
||||
|
||||
public static RiverFootprint empty() {
|
||||
return new RiverFootprint(Map.of());
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnSample> sample(int x, int z) {
|
||||
return Optional.ofNullable(columns.get(pack(x, z)));
|
||||
}
|
||||
|
||||
public HydrologyRenderSample renderSample(int x, int z) {
|
||||
HydrologyColumnSample sample = columns.get(pack(x, z));
|
||||
return sample == null ? new HydrologyRenderSample(x, z, List.of()) : sample.renderSample();
|
||||
}
|
||||
|
||||
public Map<Long, HydrologyColumnSample> columns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
public List<HydrologyColumnSample> columnsIn(int minimumX, int minimumZ, int maximumX, int maximumZ) {
|
||||
ArrayList<HydrologyColumnSample> selected = new ArrayList<>();
|
||||
long width = (long) maximumX - minimumX;
|
||||
long height = (long) maximumZ - minimumZ;
|
||||
if (width <= 0L || height <= 0L) {
|
||||
return List.of();
|
||||
}
|
||||
long area = width > Long.MAX_VALUE / height ? Long.MAX_VALUE : width * height;
|
||||
if (area < columns.size()) {
|
||||
for (long z = minimumZ; z < maximumZ; z++) {
|
||||
for (long x = minimumX; x < maximumX; x++) {
|
||||
HydrologyColumnSample sample = columns.get(pack((int) x, (int) z));
|
||||
if (sample != null) {
|
||||
selected.add(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(selected);
|
||||
}
|
||||
for (HydrologyColumnSample sample : columns.values()) {
|
||||
if (sample.x() >= minimumX && sample.x() < maximumX
|
||||
&& sample.z() >= minimumZ && sample.z() < maximumZ) {
|
||||
selected.add(sample);
|
||||
}
|
||||
}
|
||||
return List.copyOf(selected);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return columns.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return columns.isEmpty();
|
||||
}
|
||||
|
||||
public static long pack(int x, int z) {
|
||||
return ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
}
|
||||
|
||||
public static int unpackX(long packed) {
|
||||
return (int) (packed >> 32);
|
||||
}
|
||||
|
||||
public static int unpackZ(long packed) {
|
||||
return (int) packed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof RiverFootprint footprint && columns.equals(footprint.columns);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return columns.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RiverFootprint[columns=" + columns.size() + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record RiverOutlet(
|
||||
long id,
|
||||
HydrologyFeatureType type,
|
||||
long drainageNodeId,
|
||||
HydrologyPoint landwardPoint,
|
||||
HydrologyPoint connectionPoint,
|
||||
int seaLevel,
|
||||
boolean directOcean
|
||||
) {
|
||||
public RiverOutlet {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(landwardPoint, "landwardPoint");
|
||||
Objects.requireNonNull(connectionPoint, "connectionPoint");
|
||||
if (type != HydrologyFeatureType.MOUTH
|
||||
&& type != HydrologyFeatureType.COASTAL_GROTTO
|
||||
&& type != HydrologyFeatureType.INLAND_GROTTO) {
|
||||
throw new IllegalArgumentException("Unsupported outlet feature type: " + type);
|
||||
}
|
||||
if (directOcean && type == HydrologyFeatureType.INLAND_GROTTO) {
|
||||
throw new IllegalArgumentException("An inland grotto cannot be a direct ocean outlet.");
|
||||
}
|
||||
if (directOcean && connectionPoint.y() != seaLevel) {
|
||||
throw new IllegalArgumentException("A direct ocean connection must be at sea level.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
public record CavePosition(int x, int y, int z) {
|
||||
public CavePosition offset(int dx, int dy, int dz) {
|
||||
return new CavePosition(x + dx, y + dy, z + dz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 0x811C9DC5;
|
||||
hash = (hash ^ x) * 0x01000193;
|
||||
hash = (hash ^ y) * 0x01000193;
|
||||
hash = (hash ^ z) * 0x01000193;
|
||||
hash ^= hash >>> 16;
|
||||
hash *= 0x7FEB352D;
|
||||
hash ^= hash >>> 15;
|
||||
hash *= 0x846CA68B;
|
||||
return hash ^ hash >>> 16;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public final class CavePositionIndex {
|
||||
private static final int DEFAULT_CAPACITY = 16;
|
||||
private static final int MAXIMUM_CAPACITY = 1 << 30;
|
||||
|
||||
private int[] xCoordinates;
|
||||
private int[] yCoordinates;
|
||||
private int[] zCoordinates;
|
||||
private boolean[] occupied;
|
||||
private int mask;
|
||||
private int resizeThreshold;
|
||||
private int size;
|
||||
|
||||
public CavePositionIndex() {
|
||||
initialize(DEFAULT_CAPACITY);
|
||||
}
|
||||
|
||||
public CavePositionIndex(int expectedSize) {
|
||||
initialize(capacityFor(expectedSize));
|
||||
}
|
||||
|
||||
public static CavePositionIndex copyOf(Collection<CavePosition> positions) {
|
||||
CavePositionIndex index = new CavePositionIndex(positions.size());
|
||||
for (CavePosition position : positions) {
|
||||
index.add(position.x(), position.y(), position.z());
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
public boolean add(int x, int y, int z) {
|
||||
int slot = findSlot(x, y, z);
|
||||
if (occupied[slot]) {
|
||||
return false;
|
||||
}
|
||||
occupied[slot] = true;
|
||||
xCoordinates[slot] = x;
|
||||
yCoordinates[slot] = y;
|
||||
zCoordinates[slot] = z;
|
||||
size++;
|
||||
if (size > resizeThreshold) {
|
||||
resize();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean contains(int x, int y, int z) {
|
||||
return occupied[findSlot(x, y, z)];
|
||||
}
|
||||
|
||||
private void initialize(int capacity) {
|
||||
xCoordinates = new int[capacity];
|
||||
yCoordinates = new int[capacity];
|
||||
zCoordinates = new int[capacity];
|
||||
occupied = new boolean[capacity];
|
||||
mask = capacity - 1;
|
||||
resizeThreshold = capacity - (capacity >>> 2);
|
||||
size = 0;
|
||||
}
|
||||
|
||||
private int findSlot(int x, int y, int z) {
|
||||
int slot = hash(x, y, z) & mask;
|
||||
while (occupied[slot]
|
||||
&& (xCoordinates[slot] != x || yCoordinates[slot] != y || zCoordinates[slot] != z)) {
|
||||
slot = slot + 1 & mask;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
private void resize() {
|
||||
if (occupied.length == MAXIMUM_CAPACITY) {
|
||||
resizeThreshold = Integer.MAX_VALUE;
|
||||
return;
|
||||
}
|
||||
int[] previousXCoordinates = xCoordinates;
|
||||
int[] previousYCoordinates = yCoordinates;
|
||||
int[] previousZCoordinates = zCoordinates;
|
||||
boolean[] previousOccupied = occupied;
|
||||
initialize(occupied.length << 1);
|
||||
for (int index = 0; index < previousOccupied.length; index++) {
|
||||
if (previousOccupied[index]) {
|
||||
add(previousXCoordinates[index], previousYCoordinates[index], previousZCoordinates[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int capacityFor(int expectedSize) {
|
||||
if (expectedSize < 0) {
|
||||
throw new IllegalArgumentException("Expected position count must not be negative.");
|
||||
}
|
||||
long requiredCapacity = Math.max(DEFAULT_CAPACITY, ((long) expectedSize * 4L + 2L) / 3L);
|
||||
if (requiredCapacity >= MAXIMUM_CAPACITY) {
|
||||
return MAXIMUM_CAPACITY;
|
||||
}
|
||||
return Integer.highestOneBit((int) requiredCapacity - 1) << 1;
|
||||
}
|
||||
|
||||
private static int hash(int x, int y, int z) {
|
||||
int hash = 0x811C9DC5;
|
||||
hash = (hash ^ x) * 0x01000193;
|
||||
hash = (hash ^ y) * 0x01000193;
|
||||
hash = (hash ^ z) * 0x01000193;
|
||||
hash ^= hash >>> 16;
|
||||
hash *= 0x7FEB352D;
|
||||
hash ^= hash >>> 15;
|
||||
hash *= 0x846CA68B;
|
||||
return hash ^ hash >>> 16;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
public enum CaveVoxel {
|
||||
SOLID,
|
||||
CAVE_AIR,
|
||||
COMPATIBLE_FLUID,
|
||||
LAVA,
|
||||
INCOMPATIBLE_FLUID,
|
||||
UNCONDITIONAL
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
public interface CaveVoxelView {
|
||||
boolean isInWorld(CavePosition position);
|
||||
|
||||
CaveVoxel voxelAt(CavePosition position);
|
||||
|
||||
boolean isOpenToSurface(CavePosition position);
|
||||
|
||||
boolean isAboveTerrainSurface(CavePosition position);
|
||||
|
||||
default boolean hasAboveTerrainSurface(int x, int z, int minimumY, int maximumY) {
|
||||
if (minimumY > maximumY) {
|
||||
return false;
|
||||
}
|
||||
for (int y = minimumY; ; y++) {
|
||||
CavePosition position = new CavePosition(x, y, z);
|
||||
if (isInWorld(position) && isAboveTerrainSurface(position)) {
|
||||
return true;
|
||||
}
|
||||
if (y == maximumY) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
public enum HydrologyCaveAction {
|
||||
WET_SOURCE,
|
||||
FALLING_FLUID,
|
||||
DRY_AIR,
|
||||
SEAL_GUARD
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public record HydrologyCaveCandidate(
|
||||
HydrologyCaveSource source,
|
||||
String profileKey,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
boolean allowDryCaveConnections,
|
||||
Map<CavePosition, HydrologyCaveAction> actions,
|
||||
Set<CavePosition> intentionalOpenings
|
||||
) {
|
||||
public HydrologyCaveCandidate {
|
||||
Objects.requireNonNull(source);
|
||||
if (profileKey == null || profileKey.isBlank()) {
|
||||
throw new IllegalArgumentException("profileKey must not be blank");
|
||||
}
|
||||
profileKey = profileKey.trim();
|
||||
Objects.requireNonNull(settings);
|
||||
actions = Collections.unmodifiableMap(new LinkedHashMap<>(Objects.requireNonNull(actions)));
|
||||
intentionalOpenings = Collections.unmodifiableSet(
|
||||
new LinkedHashSet<>(Objects.requireNonNull(intentionalOpenings))
|
||||
);
|
||||
if (actions.isEmpty()) {
|
||||
throw new IllegalArgumentException("A cave candidate requires at least one planned action");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class HydrologyCaveCell {
|
||||
private static final String DEFAULT_FLUID_PROFILE_KEY = "default";
|
||||
private static final byte LIQUID_FLUID = 1;
|
||||
private static final byte LIQUID_FORCED_AIR = 3;
|
||||
|
||||
private final HydrologyCaveAction action;
|
||||
private final String fluidProfileKey;
|
||||
private final String floodedBiomeKey;
|
||||
private final MatterCavern cavern;
|
||||
|
||||
public HydrologyCaveCell(
|
||||
HydrologyCaveAction action,
|
||||
String fluidProfileKey,
|
||||
String floodedBiomeKey
|
||||
) {
|
||||
this.action = Objects.requireNonNull(action);
|
||||
if (fluidProfileKey == null || fluidProfileKey.isBlank()) {
|
||||
throw new IllegalArgumentException("fluidProfileKey must not be blank");
|
||||
}
|
||||
this.fluidProfileKey = fluidProfileKey.trim();
|
||||
this.floodedBiomeKey = floodedBiomeKey == null ? "" : floodedBiomeKey.trim();
|
||||
this.cavern = switch (action) {
|
||||
case WET_SOURCE, FALLING_FLUID -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FLUID);
|
||||
case DRY_AIR -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FORCED_AIR);
|
||||
case SEAL_GUARD -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static HydrologyCaveCell of(HydrologyCaveAction action) {
|
||||
return new HydrologyCaveCell(action, DEFAULT_FLUID_PROFILE_KEY, "");
|
||||
}
|
||||
|
||||
public static HydrologyCaveCell of(HydrologyCaveAction action, String fluidProfileKey) {
|
||||
return new HydrologyCaveCell(action, fluidProfileKey, "");
|
||||
}
|
||||
|
||||
public boolean carves() {
|
||||
return action != HydrologyCaveAction.SEAL_GUARD;
|
||||
}
|
||||
|
||||
public boolean isWet() {
|
||||
return action == HydrologyCaveAction.WET_SOURCE || action == HydrologyCaveAction.FALLING_FLUID;
|
||||
}
|
||||
|
||||
public boolean isFalling() {
|
||||
return action == HydrologyCaveAction.FALLING_FLUID;
|
||||
}
|
||||
|
||||
public boolean protectsPlacement() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public MatterCavern asCavern() {
|
||||
return cavern;
|
||||
}
|
||||
|
||||
public HydrologyCaveAction action() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public String floodedBiomeKey() {
|
||||
return floodedBiomeKey;
|
||||
}
|
||||
|
||||
public String fluidProfileKey() {
|
||||
return fluidProfileKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (!(object instanceof HydrologyCaveCell hydrology)) {
|
||||
return false;
|
||||
}
|
||||
return action == hydrology.action
|
||||
&& fluidProfileKey.equals(hydrology.fluidProfileKey)
|
||||
&& floodedBiomeKey.equals(hydrology.floodedBiomeKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (31 * ((31 * action.hashCode()) + fluidProfileKey.hashCode())) + floodedBiomeKey.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HydrologyCaveCell[action=" + action + ", fluidProfileKey=" + fluidProfileKey
|
||||
+ ", floodedBiomeKey=" + floodedBiomeKey + "]";
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class HydrologyCaveConflictPolicy {
|
||||
private static final Comparator<HydrologyCaveSource> SOURCE_PRIORITY = Comparator
|
||||
.comparingInt(HydrologyCaveSource::waterHeadY)
|
||||
.reversed()
|
||||
.thenComparingLong(HydrologyCaveSource::sourceId)
|
||||
.thenComparingInt(source -> source.entry().x())
|
||||
.thenComparingInt(source -> source.entry().y())
|
||||
.thenComparingInt(source -> source.entry().z())
|
||||
.thenComparingInt(source -> source.target().x())
|
||||
.thenComparingInt(source -> source.target().y())
|
||||
.thenComparingInt(source -> source.target().z())
|
||||
.thenComparing(HydrologyCaveSource::mode);
|
||||
|
||||
private HydrologyCaveConflictPolicy() {
|
||||
}
|
||||
|
||||
public static Comparator<HydrologyCaveSource> sourcePriority() {
|
||||
return SOURCE_PRIORITY;
|
||||
}
|
||||
|
||||
public static int compareSources(HydrologyCaveSource left, HydrologyCaveSource right) {
|
||||
return SOURCE_PRIORITY.compare(
|
||||
Objects.requireNonNull(left, "left"),
|
||||
Objects.requireNonNull(right, "right")
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean hasIncompatibleOverlap(
|
||||
String leftProfileKey,
|
||||
Map<CavePosition, HydrologyCaveAction> leftActions,
|
||||
String rightProfileKey,
|
||||
Map<CavePosition, HydrologyCaveAction> rightActions
|
||||
) {
|
||||
String normalizedLeftProfileKey = requireProfileKey(leftProfileKey, "leftProfileKey");
|
||||
String normalizedRightProfileKey = requireProfileKey(rightProfileKey, "rightProfileKey");
|
||||
Map<CavePosition, HydrologyCaveAction> normalizedLeftActions = Objects.requireNonNull(
|
||||
leftActions,
|
||||
"leftActions"
|
||||
);
|
||||
Map<CavePosition, HydrologyCaveAction> normalizedRightActions = Objects.requireNonNull(
|
||||
rightActions,
|
||||
"rightActions"
|
||||
);
|
||||
if (normalizedLeftActions.size() > normalizedRightActions.size()) {
|
||||
return hasIncompatibleOverlap(
|
||||
normalizedRightProfileKey,
|
||||
normalizedRightActions,
|
||||
normalizedLeftProfileKey,
|
||||
normalizedLeftActions
|
||||
);
|
||||
}
|
||||
boolean sameProfile = normalizedLeftProfileKey.equals(normalizedRightProfileKey);
|
||||
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : normalizedLeftActions.entrySet()) {
|
||||
CavePosition position = Objects.requireNonNull(entry.getKey(), "left action position");
|
||||
if (!normalizedRightActions.containsKey(position)) {
|
||||
continue;
|
||||
}
|
||||
HydrologyCaveAction leftAction = Objects.requireNonNull(entry.getValue(), "left action");
|
||||
HydrologyCaveAction rightAction = Objects.requireNonNull(
|
||||
normalizedRightActions.get(position),
|
||||
"right action"
|
||||
);
|
||||
if (!sameProfile || leftAction != rightAction) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String requireProfileKey(String value, String name) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(name + " must not be blank");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
+1849
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
public enum HydrologyCaveFluidPolicy {
|
||||
REJECT_EXISTING,
|
||||
ALLOW_COMPATIBLE,
|
||||
REPLACE_CONTAINED
|
||||
}
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RiverCaveGrottoShape {
|
||||
RiverCaveGrottoShape ELLIPSOID = (source, settings, dx, dy, dz) -> {
|
||||
public interface HydrologyCaveGrottoShape {
|
||||
HydrologyCaveGrottoShape ELLIPSOID = (source, settings, dx, dy, dz) -> {
|
||||
double horizontalRadius = settings.grottoHorizontalRadius();
|
||||
double verticalRadius = settings.grottoVerticalRadius();
|
||||
double normalized = ((double) dx * dx / (horizontalRadius * horizontalRadius))
|
||||
@@ -12,8 +12,8 @@ public interface RiverCaveGrottoShape {
|
||||
};
|
||||
|
||||
boolean contains(
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
int offsetX,
|
||||
int offsetY,
|
||||
int offsetZ
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
public enum RiverCaveMode {
|
||||
public enum HydrologyCaveMode {
|
||||
CLOSED_COMPONENT,
|
||||
GENERATED_GROTTO,
|
||||
GROTTO_OR_CLOSED_COMPONENT,
|
||||
@@ -0,0 +1,692 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.AbstractCollection;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.AbstractSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
public record HydrologyCavePlan(
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCaveRejection rejection,
|
||||
Map<CavePosition, HydrologyCaveAction> actions,
|
||||
Map<CavePosition, CaveVoxelPrecondition> baselinePreconditions,
|
||||
OptionalLong arbitrationWinnerSourceId
|
||||
) {
|
||||
private static final int ACTION_MASK = 0x03;
|
||||
private static final int ACTION_PRESENT = 0x04;
|
||||
private static final int VOXEL_SHIFT = 3;
|
||||
private static final int VOXEL_MASK = 0x38;
|
||||
private static final int OPEN_TO_SURFACE = 0x40;
|
||||
private static final HydrologyCaveAction[] ACTIONS = HydrologyCaveAction.values();
|
||||
private static final CaveVoxel[] VOXELS = CaveVoxel.values();
|
||||
private static final CaveVoxelPrecondition[][] PRECONDITIONS = createPreconditions();
|
||||
|
||||
public HydrologyCavePlan {
|
||||
Objects.requireNonNull(source);
|
||||
Objects.requireNonNull(rejection);
|
||||
Map<CavePosition, HydrologyCaveAction> requestedActions = Objects.requireNonNull(actions);
|
||||
Map<CavePosition, CaveVoxelPrecondition> requestedPreconditions =
|
||||
Objects.requireNonNull(baselinePreconditions);
|
||||
Objects.requireNonNull(arbitrationWinnerSourceId);
|
||||
if (rejection != HydrologyCaveRejection.NONE
|
||||
&& (!requestedActions.isEmpty() || !requestedPreconditions.isEmpty())) {
|
||||
throw new IllegalArgumentException("Rejected cave plans cannot contain mutations or preconditions");
|
||||
}
|
||||
if (!requestedPreconditions.keySet().containsAll(requestedActions.keySet())) {
|
||||
throw new IllegalArgumentException("Every cave action requires a baseline precondition");
|
||||
}
|
||||
if (rejection != HydrologyCaveRejection.OVERLAPPING_SOURCE && arbitrationWinnerSourceId.isPresent()) {
|
||||
throw new IllegalArgumentException("Only overlap rejections can name an arbitration winner");
|
||||
}
|
||||
if (requestedPreconditions.isEmpty()) {
|
||||
actions = Map.of();
|
||||
baselinePreconditions = Map.of();
|
||||
} else {
|
||||
LinkedHashMap<CavePosition, Byte> packed = new LinkedHashMap<>(requestedPreconditions.size());
|
||||
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : requestedActions.entrySet()) {
|
||||
CaveVoxelPrecondition precondition = requestedPreconditions.get(entry.getKey());
|
||||
packed.put(entry.getKey(), encode(entry.getValue(), precondition));
|
||||
}
|
||||
for (Map.Entry<CavePosition, CaveVoxelPrecondition> entry : requestedPreconditions.entrySet()) {
|
||||
packed.putIfAbsent(entry.getKey(), encode(null, entry.getValue()));
|
||||
}
|
||||
Map<CavePosition, Byte> immutablePacked = Collections.unmodifiableMap(packed);
|
||||
PositionSpatialIndex spatialIndex = new PositionSpatialIndex(immutablePacked);
|
||||
actions = new ActionMap(immutablePacked, requestedActions.size(), spatialIndex);
|
||||
baselinePreconditions = new PreconditionMap(
|
||||
immutablePacked,
|
||||
List.copyOf(requestedPreconditions.keySet()),
|
||||
spatialIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean accepted() {
|
||||
return rejection == HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
public void forEachAction(BiConsumer<CavePosition, HydrologyCaveAction> consumer) {
|
||||
actions.forEach(consumer);
|
||||
}
|
||||
|
||||
public void forEachPrecondition(BiConsumer<CavePosition, CaveVoxelPrecondition> consumer) {
|
||||
baselinePreconditions.forEach(consumer);
|
||||
}
|
||||
|
||||
public boolean allPreconditions(BiPredicate<CavePosition, CaveVoxelPrecondition> predicate) {
|
||||
Objects.requireNonNull(predicate);
|
||||
for (CavePosition position : baselinePreconditions.keySet()) {
|
||||
if (!predicate.test(position, baselinePreconditions.get(position))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean intersectsActions(
|
||||
int minimumX,
|
||||
int minimumZ,
|
||||
int maximumX,
|
||||
int maximumZ
|
||||
) {
|
||||
if (maximumX <= minimumX || maximumZ <= minimumZ || !(actions instanceof ActionMap actionMap)) {
|
||||
return false;
|
||||
}
|
||||
return actionMap.intersects(minimumX, minimumZ, maximumX, maximumZ);
|
||||
}
|
||||
|
||||
public void forEachActionIn(
|
||||
int minimumX,
|
||||
int minimumZ,
|
||||
int maximumX,
|
||||
int maximumZ,
|
||||
BiConsumer<CavePosition, HydrologyCaveAction> consumer
|
||||
) {
|
||||
Objects.requireNonNull(consumer);
|
||||
if (maximumX <= minimumX || maximumZ <= minimumZ || !(actions instanceof ActionMap actionMap)) {
|
||||
return;
|
||||
}
|
||||
actionMap.forEachIn(minimumX, minimumZ, maximumX, maximumZ, consumer);
|
||||
}
|
||||
|
||||
public boolean allPreconditionsIn(
|
||||
int minimumX,
|
||||
int minimumZ,
|
||||
int maximumX,
|
||||
int maximumZ,
|
||||
BiPredicate<CavePosition, CaveVoxelPrecondition> predicate
|
||||
) {
|
||||
Objects.requireNonNull(predicate);
|
||||
if (maximumX <= minimumX || maximumZ <= minimumZ
|
||||
|| !(baselinePreconditions instanceof PreconditionMap preconditionMap)) {
|
||||
return true;
|
||||
}
|
||||
return preconditionMap.allIn(minimumX, minimumZ, maximumX, maximumZ, predicate);
|
||||
}
|
||||
|
||||
private static byte encode(
|
||||
HydrologyCaveAction action,
|
||||
CaveVoxelPrecondition precondition
|
||||
) {
|
||||
Objects.requireNonNull(precondition);
|
||||
int packed = precondition.voxel().ordinal() << VOXEL_SHIFT;
|
||||
if (precondition.openToSurface()) {
|
||||
packed |= OPEN_TO_SURFACE;
|
||||
}
|
||||
if (action != null) {
|
||||
packed |= ACTION_PRESENT | action.ordinal();
|
||||
}
|
||||
return (byte) packed;
|
||||
}
|
||||
|
||||
private static boolean hasAction(byte packed) {
|
||||
return (packed & ACTION_PRESENT) != 0;
|
||||
}
|
||||
|
||||
private static HydrologyCaveAction decodeAction(byte packed) {
|
||||
return ACTIONS[packed & ACTION_MASK];
|
||||
}
|
||||
|
||||
private static CaveVoxelPrecondition decodePrecondition(byte packed) {
|
||||
int unsigned = Byte.toUnsignedInt(packed);
|
||||
int voxel = unsigned & VOXEL_MASK;
|
||||
int open = unsigned & OPEN_TO_SURFACE;
|
||||
return PRECONDITIONS[voxel >>> VOXEL_SHIFT][open == 0 ? 0 : 1];
|
||||
}
|
||||
|
||||
private static CaveVoxelPrecondition[][] createPreconditions() {
|
||||
if (ACTIONS.length > ACTION_MASK + 1) {
|
||||
throw new IllegalStateException("Hydrology cave action packing capacity exceeded");
|
||||
}
|
||||
if (VOXELS.length > (VOXEL_MASK >>> VOXEL_SHIFT) + 1) {
|
||||
throw new IllegalStateException("Hydrology cave voxel packing capacity exceeded");
|
||||
}
|
||||
CaveVoxelPrecondition[][] preconditions = new CaveVoxelPrecondition[VOXELS.length][2];
|
||||
for (CaveVoxel voxel : VOXELS) {
|
||||
preconditions[voxel.ordinal()][0] = new CaveVoxelPrecondition(voxel, false);
|
||||
preconditions[voxel.ordinal()][1] = new CaveVoxelPrecondition(voxel, true);
|
||||
}
|
||||
return preconditions;
|
||||
}
|
||||
|
||||
private static final class ActionMap extends AbstractMap<CavePosition, HydrologyCaveAction> {
|
||||
private final Map<CavePosition, Byte> packed;
|
||||
private final int size;
|
||||
private final PositionSpatialIndex spatialIndex;
|
||||
|
||||
private ActionMap(Map<CavePosition, Byte> packed, int size, PositionSpatialIndex spatialIndex) {
|
||||
this.packed = packed;
|
||||
this.size = size;
|
||||
this.spatialIndex = spatialIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyCaveAction get(Object key) {
|
||||
Byte value = packed.get(key);
|
||||
return value == null || !hasAction(value) ? null : decodeAction(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
Byte value = packed.get(key);
|
||||
return value != null && hasAction(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<CavePosition> keySet() {
|
||||
return new ActionKeySet(packed, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<CavePosition, HydrologyCaveAction>> entrySet() {
|
||||
return new ActionEntrySet(packed, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<HydrologyCaveAction> values() {
|
||||
return new ActionValues(packed, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(BiConsumer<? super CavePosition, ? super HydrologyCaveAction> consumer) {
|
||||
Objects.requireNonNull(consumer);
|
||||
for (Map.Entry<CavePosition, Byte> entry : packed.entrySet()) {
|
||||
if (hasAction(entry.getValue())) {
|
||||
consumer.accept(entry.getKey(), decodeAction(entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean intersects(int minimumX, int minimumZ, int maximumX, int maximumZ) {
|
||||
return spatialIndex.anyIn(minimumX, minimumZ, maximumX, maximumZ,
|
||||
(CavePosition position) -> hasAction(packed.get(position)));
|
||||
}
|
||||
|
||||
private void forEachIn(
|
||||
int minimumX,
|
||||
int minimumZ,
|
||||
int maximumX,
|
||||
int maximumZ,
|
||||
BiConsumer<CavePosition, HydrologyCaveAction> consumer
|
||||
) {
|
||||
spatialIndex.forEachIn(minimumX, minimumZ, maximumX, maximumZ, (CavePosition position) -> {
|
||||
byte value = packed.get(position);
|
||||
if (hasAction(value)) {
|
||||
consumer.accept(position, decodeAction(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PreconditionMap extends AbstractMap<CavePosition, CaveVoxelPrecondition> {
|
||||
private final Map<CavePosition, Byte> packed;
|
||||
private final List<CavePosition> order;
|
||||
private final PositionSpatialIndex spatialIndex;
|
||||
|
||||
private PreconditionMap(
|
||||
Map<CavePosition, Byte> packed,
|
||||
List<CavePosition> order,
|
||||
PositionSpatialIndex spatialIndex
|
||||
) {
|
||||
this.packed = packed;
|
||||
this.order = order;
|
||||
this.spatialIndex = spatialIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxelPrecondition get(Object key) {
|
||||
Byte value = packed.get(key);
|
||||
return value == null ? null : decodePrecondition(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return packed.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return order.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<CavePosition> keySet() {
|
||||
return new PreconditionKeySet(packed, order);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<CavePosition, CaveVoxelPrecondition>> entrySet() {
|
||||
return new PreconditionEntrySet(packed, order);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<CaveVoxelPrecondition> values() {
|
||||
return new PreconditionValues(packed, order);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(BiConsumer<? super CavePosition, ? super CaveVoxelPrecondition> consumer) {
|
||||
Objects.requireNonNull(consumer);
|
||||
for (CavePosition position : order) {
|
||||
consumer.accept(position, decodePrecondition(packed.get(position)));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean allIn(
|
||||
int minimumX,
|
||||
int minimumZ,
|
||||
int maximumX,
|
||||
int maximumZ,
|
||||
BiPredicate<CavePosition, CaveVoxelPrecondition> predicate
|
||||
) {
|
||||
return spatialIndex.allIn(minimumX, minimumZ, maximumX, maximumZ,
|
||||
(CavePosition position) -> predicate.test(position, decodePrecondition(packed.get(position))));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PositionSpatialIndex {
|
||||
private static final int CHUNK_SHIFT = 4;
|
||||
|
||||
private final Map<Long, List<CavePosition>> positionsByChunk;
|
||||
|
||||
private PositionSpatialIndex(Map<CavePosition, Byte> packed) {
|
||||
HashMap<Long, ArrayList<CavePosition>> mutable = new HashMap<>();
|
||||
for (CavePosition position : packed.keySet()) {
|
||||
long chunkKey = packChunk(position.x() >> CHUNK_SHIFT, position.z() >> CHUNK_SHIFT);
|
||||
mutable.computeIfAbsent(chunkKey, ignored -> new ArrayList<>()).add(position);
|
||||
}
|
||||
HashMap<Long, List<CavePosition>> immutable = HashMap.newHashMap(mutable.size());
|
||||
for (Map.Entry<Long, ArrayList<CavePosition>> entry : mutable.entrySet()) {
|
||||
immutable.put(entry.getKey(), List.copyOf(entry.getValue()));
|
||||
}
|
||||
positionsByChunk = Map.copyOf(immutable);
|
||||
}
|
||||
|
||||
private boolean anyIn(
|
||||
int minimumX,
|
||||
int minimumZ,
|
||||
int maximumX,
|
||||
int maximumZ,
|
||||
java.util.function.Predicate<CavePosition> predicate
|
||||
) {
|
||||
int minimumChunkX = minimumX >> CHUNK_SHIFT;
|
||||
int maximumChunkX = maximumX - 1 >> CHUNK_SHIFT;
|
||||
int minimumChunkZ = minimumZ >> CHUNK_SHIFT;
|
||||
int maximumChunkZ = maximumZ - 1 >> CHUNK_SHIFT;
|
||||
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
|
||||
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
|
||||
List<CavePosition> positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
|
||||
if (positions == null) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition position : positions) {
|
||||
if (inside(position, minimumX, minimumZ, maximumX, maximumZ)
|
||||
&& predicate.test(position)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean allIn(
|
||||
int minimumX,
|
||||
int minimumZ,
|
||||
int maximumX,
|
||||
int maximumZ,
|
||||
java.util.function.Predicate<CavePosition> predicate
|
||||
) {
|
||||
int minimumChunkX = minimumX >> CHUNK_SHIFT;
|
||||
int maximumChunkX = maximumX - 1 >> CHUNK_SHIFT;
|
||||
int minimumChunkZ = minimumZ >> CHUNK_SHIFT;
|
||||
int maximumChunkZ = maximumZ - 1 >> CHUNK_SHIFT;
|
||||
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
|
||||
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
|
||||
List<CavePosition> positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
|
||||
if (positions == null) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition position : positions) {
|
||||
if (inside(position, minimumX, minimumZ, maximumX, maximumZ)
|
||||
&& !predicate.test(position)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void forEachIn(
|
||||
int minimumX,
|
||||
int minimumZ,
|
||||
int maximumX,
|
||||
int maximumZ,
|
||||
java.util.function.Consumer<CavePosition> consumer
|
||||
) {
|
||||
int minimumChunkX = minimumX >> CHUNK_SHIFT;
|
||||
int maximumChunkX = maximumX - 1 >> CHUNK_SHIFT;
|
||||
int minimumChunkZ = minimumZ >> CHUNK_SHIFT;
|
||||
int maximumChunkZ = maximumZ - 1 >> CHUNK_SHIFT;
|
||||
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
|
||||
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
|
||||
List<CavePosition> positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
|
||||
if (positions == null) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition position : positions) {
|
||||
if (inside(position, minimumX, minimumZ, maximumX, maximumZ)) {
|
||||
consumer.accept(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean inside(
|
||||
CavePosition position,
|
||||
int minimumX,
|
||||
int minimumZ,
|
||||
int maximumX,
|
||||
int maximumZ
|
||||
) {
|
||||
return position.x() >= minimumX && position.x() < maximumX
|
||||
&& position.z() >= minimumZ && position.z() < maximumZ;
|
||||
}
|
||||
|
||||
private static long packChunk(int chunkX, int chunkZ) {
|
||||
return (long) chunkX << 32 ^ chunkZ & 0xffffffffL;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ActionKeySet extends AbstractSet<CavePosition> {
|
||||
private final Map<CavePosition, Byte> packed;
|
||||
private final int size;
|
||||
|
||||
private ActionKeySet(Map<CavePosition, Byte> packed, int size) {
|
||||
this.packed = packed;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<CavePosition> iterator() {
|
||||
return new ActionKeyIterator(packed.entrySet().iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object value) {
|
||||
Byte packedValue = packed.get(value);
|
||||
return packedValue != null && hasAction(packedValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ActionEntrySet extends AbstractSet<Entry<CavePosition, HydrologyCaveAction>> {
|
||||
private final Map<CavePosition, Byte> packed;
|
||||
private final int size;
|
||||
|
||||
private ActionEntrySet(Map<CavePosition, Byte> packed, int size) {
|
||||
this.packed = packed;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Entry<CavePosition, HydrologyCaveAction>> iterator() {
|
||||
return new ActionEntryIterator(packed.entrySet().iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PreconditionEntrySet
|
||||
extends AbstractSet<Entry<CavePosition, CaveVoxelPrecondition>> {
|
||||
private final Map<CavePosition, Byte> packed;
|
||||
private final List<CavePosition> order;
|
||||
|
||||
private PreconditionEntrySet(Map<CavePosition, Byte> packed, List<CavePosition> order) {
|
||||
this.packed = packed;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Entry<CavePosition, CaveVoxelPrecondition>> iterator() {
|
||||
return new PreconditionEntryIterator(packed, order.iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return order.size();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PreconditionKeySet extends AbstractSet<CavePosition> {
|
||||
private final Map<CavePosition, Byte> packed;
|
||||
private final List<CavePosition> order;
|
||||
|
||||
private PreconditionKeySet(Map<CavePosition, Byte> packed, List<CavePosition> order) {
|
||||
this.packed = packed;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<CavePosition> iterator() {
|
||||
return order.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return order.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object value) {
|
||||
return packed.containsKey(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ActionValues extends AbstractCollection<HydrologyCaveAction> {
|
||||
private final Map<CavePosition, Byte> packed;
|
||||
private final int size;
|
||||
|
||||
private ActionValues(Map<CavePosition, Byte> packed, int size) {
|
||||
this.packed = packed;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<HydrologyCaveAction> iterator() {
|
||||
return new ActionValueIterator(packed.entrySet().iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PreconditionValues extends AbstractCollection<CaveVoxelPrecondition> {
|
||||
private final Map<CavePosition, Byte> packed;
|
||||
private final List<CavePosition> order;
|
||||
|
||||
private PreconditionValues(Map<CavePosition, Byte> packed, List<CavePosition> order) {
|
||||
this.packed = packed;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<CaveVoxelPrecondition> iterator() {
|
||||
return new PreconditionValueIterator(packed, order.iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return order.size();
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class FilteredActionIterator<T> implements Iterator<T> {
|
||||
private final Iterator<Map.Entry<CavePosition, Byte>> entries;
|
||||
private Map.Entry<CavePosition, Byte> next;
|
||||
|
||||
private FilteredActionIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
advance();
|
||||
return next != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T next() {
|
||||
advance();
|
||||
if (next == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
Map.Entry<CavePosition, Byte> selected = next;
|
||||
next = null;
|
||||
return map(selected);
|
||||
}
|
||||
|
||||
protected abstract T map(Map.Entry<CavePosition, Byte> entry);
|
||||
|
||||
private void advance() {
|
||||
while (next == null && entries.hasNext()) {
|
||||
Map.Entry<CavePosition, Byte> candidate = entries.next();
|
||||
if (hasAction(candidate.getValue())) {
|
||||
next = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ActionKeyIterator extends FilteredActionIterator<CavePosition> {
|
||||
private ActionKeyIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
|
||||
super(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CavePosition map(Map.Entry<CavePosition, Byte> entry) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ActionEntryIterator
|
||||
extends FilteredActionIterator<Entry<CavePosition, HydrologyCaveAction>> {
|
||||
private ActionEntryIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
|
||||
super(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry<CavePosition, HydrologyCaveAction> map(Map.Entry<CavePosition, Byte> entry) {
|
||||
return Map.entry(entry.getKey(), decodeAction(entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ActionValueIterator extends FilteredActionIterator<HydrologyCaveAction> {
|
||||
private ActionValueIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
|
||||
super(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HydrologyCaveAction map(Map.Entry<CavePosition, Byte> entry) {
|
||||
return decodeAction(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PreconditionEntryIterator
|
||||
implements Iterator<Entry<CavePosition, CaveVoxelPrecondition>> {
|
||||
private final Map<CavePosition, Byte> packed;
|
||||
private final Iterator<CavePosition> positions;
|
||||
|
||||
private PreconditionEntryIterator(
|
||||
Map<CavePosition, Byte> packed,
|
||||
Iterator<CavePosition> positions
|
||||
) {
|
||||
this.packed = packed;
|
||||
this.positions = positions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return positions.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<CavePosition, CaveVoxelPrecondition> next() {
|
||||
CavePosition position = positions.next();
|
||||
return Map.entry(position, decodePrecondition(packed.get(position)));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PreconditionValueIterator implements Iterator<CaveVoxelPrecondition> {
|
||||
private final Map<CavePosition, Byte> packed;
|
||||
private final Iterator<CavePosition> positions;
|
||||
|
||||
private PreconditionValueIterator(
|
||||
Map<CavePosition, Byte> packed,
|
||||
Iterator<CavePosition> positions
|
||||
) {
|
||||
this.packed = packed;
|
||||
this.positions = positions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return positions.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxelPrecondition next() {
|
||||
return decodePrecondition(packed.get(positions.next()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-11
@@ -1,8 +1,8 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record RiverCavePlannerSettings(
|
||||
public record HydrologyCavePlannerSettings(
|
||||
int maxHorizontalRadius,
|
||||
int maxDepth,
|
||||
int maxFloodVolume,
|
||||
@@ -11,12 +11,14 @@ public record RiverCavePlannerSettings(
|
||||
int grottoHorizontalRadius,
|
||||
int grottoVerticalRadius,
|
||||
int dryHeadroom,
|
||||
RiverCaveFluidPolicy existingFluidPolicy,
|
||||
RiverCaveGrottoShape grottoShape,
|
||||
HydrologyCaveFluidPolicy existingFluidPolicy,
|
||||
HydrologyCaveGrottoShape grottoShape,
|
||||
int maxClosedComponentHorizontalRadius,
|
||||
int maxClosedComponentDepth
|
||||
) {
|
||||
public RiverCavePlannerSettings(
|
||||
public static final int MAXIMUM_PLANNED_MUTATIONS = 262_144;
|
||||
|
||||
public HydrologyCavePlannerSettings(
|
||||
int maxHorizontalRadius,
|
||||
int maxDepth,
|
||||
int maxFloodVolume,
|
||||
@@ -25,8 +27,8 @@ public record RiverCavePlannerSettings(
|
||||
int grottoHorizontalRadius,
|
||||
int grottoVerticalRadius,
|
||||
int dryHeadroom,
|
||||
RiverCaveFluidPolicy existingFluidPolicy,
|
||||
RiverCaveGrottoShape grottoShape
|
||||
HydrologyCaveFluidPolicy existingFluidPolicy,
|
||||
HydrologyCaveGrottoShape grottoShape
|
||||
) {
|
||||
this(
|
||||
maxHorizontalRadius,
|
||||
@@ -44,14 +46,14 @@ public record RiverCavePlannerSettings(
|
||||
);
|
||||
}
|
||||
|
||||
public RiverCavePlannerSettings(
|
||||
public HydrologyCavePlannerSettings(
|
||||
int maxHorizontalRadius,
|
||||
int maxDepth,
|
||||
int maxFloodVolume,
|
||||
int maxThroatLength,
|
||||
int grottoHorizontalRadius,
|
||||
int grottoVerticalRadius,
|
||||
RiverCaveFluidPolicy existingFluidPolicy
|
||||
HydrologyCaveFluidPolicy existingFluidPolicy
|
||||
) {
|
||||
this(
|
||||
maxHorizontalRadius,
|
||||
@@ -63,11 +65,11 @@ public record RiverCavePlannerSettings(
|
||||
grottoVerticalRadius,
|
||||
0,
|
||||
existingFluidPolicy,
|
||||
RiverCaveGrottoShape.ELLIPSOID
|
||||
HydrologyCaveGrottoShape.ELLIPSOID
|
||||
);
|
||||
}
|
||||
|
||||
public RiverCavePlannerSettings {
|
||||
public HydrologyCavePlannerSettings {
|
||||
Objects.requireNonNull(existingFluidPolicy);
|
||||
Objects.requireNonNull(grottoShape);
|
||||
if (maxHorizontalRadius < 1) {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user