mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
dwa
This commit is contained in:
@@ -255,19 +255,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
public static void msg(String string) {
|
||||
try {
|
||||
getSender().sendMessage(string);
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
Iris plugin = instance;
|
||||
String tag = plugin == null ? "" : plugin.getTag();
|
||||
ComponentLog.logMarkup(
|
||||
plugin,
|
||||
Logger.getLogger("Iris"),
|
||||
"[Iris] ",
|
||||
logPrefix(plugin),
|
||||
Level.INFO,
|
||||
tag + string,
|
||||
string,
|
||||
null);
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
Iris plugin = instance;
|
||||
String plainPrefix = ComponentText.legacy(logPrefix(plugin)).plain();
|
||||
Logger.getLogger("Iris").log(Level.INFO, plainPrefix + IrisLogging.clean(string));
|
||||
} catch (Throwable inner) {
|
||||
System.err.println("[Iris] Failed to emit log message: " + inner.getMessage());
|
||||
inner.printStackTrace(System.err);
|
||||
@@ -561,15 +562,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
private static void diagnostic(Level level, String message) {
|
||||
String line = IrisLogging.clean(message);
|
||||
Iris plugin = instance;
|
||||
ComponentLog.log(
|
||||
instance,
|
||||
plugin,
|
||||
Logger.getLogger("Iris"),
|
||||
"[Iris] ",
|
||||
logPrefix(plugin),
|
||||
level,
|
||||
ComponentText.literal(line),
|
||||
null);
|
||||
}
|
||||
|
||||
private static String logPrefix(Iris plugin) {
|
||||
return plugin == null ? "[Iris] " : plugin.getTag();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false when the bootstrap was aborted (unsupported server version); the caller
|
||||
* must bail out of onEnable without touching any further setup.
|
||||
|
||||
+13
-14
@@ -13,10 +13,8 @@ import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Core states a severity on every message it logs and the modded adapters honour it. On Bukkit the same
|
||||
* message used to become a coloured console line, which the server logs at INFO, so no core warning ever
|
||||
* appeared in a WARN-level scan of logs/latest.log - including the orphaned-world-storage reports the
|
||||
* bootstrap replays there specifically for operators to find.
|
||||
* Core states a severity on every message it logs and the modded adapters honour it. Bukkit must preserve
|
||||
* that severity through the shared component logger so diagnostics remain discoverable in logs/latest.log.
|
||||
*/
|
||||
public class IrisDiagnosticLogLevelTest {
|
||||
@Test
|
||||
@@ -26,25 +24,16 @@ public class IrisDiagnosticLogLevelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void informationalAndDebugMessagesStayOnTheConsolePath() {
|
||||
public void informationalAndDebugMessagesStayOnTheInformationalPath() {
|
||||
assertNull(Iris.diagnosticLevel(LogLevel.INFO));
|
||||
assertNull(Iris.diagnosticLevel(LogLevel.DEBUG));
|
||||
}
|
||||
|
||||
/**
|
||||
* Console sender output reaches the terminal but not the instance's logs/latest.log, which is the only
|
||||
* log most operators read after the fact. A handful of lifecycle lines go to the plugin logger instead.
|
||||
*/
|
||||
@Test
|
||||
public void lifecycleNoticesReachThePluginLoggerAtInfo() {
|
||||
assertEquals(Level.INFO, Iris.diagnosticLevel(LogLevel.NOTICE));
|
||||
}
|
||||
|
||||
/**
|
||||
* A warning raised by the adapter is the same kind of thing as a warning raised by core. Routing one
|
||||
* through the plugin logger and the other through the console sender makes the level depend on which
|
||||
* side of the SPI the call happened to be written on.
|
||||
*/
|
||||
@Test
|
||||
public void adapterSideWarningsCarryTheSameSeverityAsCoreWarnings() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java")).replace("\r\n", "\n");
|
||||
@@ -70,6 +59,16 @@ public class IrisDiagnosticLogLevelTest {
|
||||
assertTrue(diagnostic, diagnostic.contains("ComponentText.literal(line)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void informationalMessagesUseTheSharedComponentLogger() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java")).replace("\r\n", "\n");
|
||||
String message = method(source, "public static void msg(String string)");
|
||||
|
||||
assertTrue(message, message.contains("ComponentLog.logMarkup("));
|
||||
assertTrue(message, message.contains("logPrefix(plugin)"));
|
||||
assertFalse(message, message.contains("getSender().sendMessage"));
|
||||
}
|
||||
|
||||
private static String method(String source, String signature) {
|
||||
int start = source.indexOf(signature);
|
||||
assertTrue("method not found: " + signature, start >= 0);
|
||||
|
||||
@@ -111,10 +111,10 @@ final class PackRiverValidator {
|
||||
}
|
||||
|
||||
if (topology != null && terrain != null) {
|
||||
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
|
||||
WormEnvelope wormEnvelope = wormEnvelope(terrain);
|
||||
int cellSize = integerValue(topology, "cellSize", 512);
|
||||
if (Double.isFinite(meanderStrength) && meanderStrength > cellSize) {
|
||||
warnings.add(path + ".terrain.meanderStrength exceeds topology.cellSize; reaches may require large cache halos.");
|
||||
if (wormEnvelope.maximumOffset() > cellSize) {
|
||||
warnings.add(path + ".terrain.worms maxOffset exceeds topology.cellSize; reaches may require large cache halos.");
|
||||
}
|
||||
validateTopologyComplexity(packFolder, path, topology, terrain, errors);
|
||||
}
|
||||
@@ -144,8 +144,6 @@ final class PackRiverValidator {
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingNoiseWeight", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "flowAlignmentWeight", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "confluenceWeight", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "branchSoftCap", 1, 8, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "branchChildShrinkFactor", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainHeightWeight", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainSlopeWeight", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "oceanAttraction", 0D, 16D, errors);
|
||||
@@ -180,8 +178,6 @@ final class PackRiverValidator {
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelMouthBlend", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelFloorVariation", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelRoofVariation", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "meanderStrength", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "meanderSubdivisions", 1, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bedRoughness", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, terrain, "terminalMode", TERMINAL_MODES, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "terminalTaper", 8, 1024, errors);
|
||||
@@ -189,8 +185,8 @@ final class PackRiverValidator {
|
||||
validateNoiseChance(packFolder, terrain, "incision", path, errors);
|
||||
validateStyle(packFolder, terrain, "tunnelFloorStyle", path, errors);
|
||||
validateStyle(packFolder, terrain, "tunnelRoofStyle", path, errors);
|
||||
validateStyle(packFolder, terrain, "meanderStyle", path, errors);
|
||||
validateStyle(packFolder, terrain, "bedRoughnessStyle", path, errors);
|
||||
validateWorms(path, terrain, errors);
|
||||
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
|
||||
double maximumTunnelWidthMultiplier = styledRangeMaximum(
|
||||
packFolder,
|
||||
@@ -240,8 +236,7 @@ final class PackRiverValidator {
|
||||
int tileCells = integerValue(topology, "tileCells", 4);
|
||||
double siteJitter = doubleValue(topology, "siteJitter", 0.35D);
|
||||
int maxRouteReaches = integerValue(topology, "maxRouteReaches", 16);
|
||||
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
|
||||
int meanderSubdivisions = integerValue(terrain, "meanderSubdivisions", 8);
|
||||
WormEnvelope wormEnvelope = wormEnvelope(terrain);
|
||||
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
|
||||
double maximumBankWidth = doubleValue(terrain, "maxBankWidth", 4D);
|
||||
double maximumTunnelWidthMultiplier = styledRangeMaximum(
|
||||
@@ -256,8 +251,8 @@ final class PackRiverValidator {
|
||||
|| tileCells < 1 || tileCells > 64
|
||||
|| !Double.isFinite(siteJitter) || siteJitter < 0D || siteJitter > 0.49D
|
||||
|| maxRouteReaches < 1 || maxRouteReaches > 256
|
||||
|| !Double.isFinite(meanderStrength) || meanderStrength < 0D || meanderStrength > 1024D
|
||||
|| meanderSubdivisions < 1 || meanderSubdivisions > 64
|
||||
|| wormEnvelope.maximumOffset() < 0D || wormEnvelope.maximumOffset() > 1024D
|
||||
|| wormEnvelope.maximumSegments() < 1 || wormEnvelope.maximumSegments() > 64
|
||||
|| !Double.isFinite(maximumChannelWidth) || maximumChannelWidth < 1D || maximumChannelWidth > 2048D
|
||||
|| !Double.isFinite(maximumBankWidth) || maximumBankWidth < 0D || maximumBankWidth > 2048D
|
||||
|| !Double.isFinite(maximumTunnelWidthMultiplier)
|
||||
@@ -275,14 +270,187 @@ final class PackRiverValidator {
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
maximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
wormEnvelope.maximumOffset(),
|
||||
wormEnvelope.maximumSegments()
|
||||
);
|
||||
for (String violation : estimate.violations()) {
|
||||
errors.add(path + " exceeds the safe derived complexity budget. " + violation);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateWorms(String path, JSONObject terrain, List<String> errors) {
|
||||
Object rawWorms = terrain.opt("worms");
|
||||
if (!(rawWorms instanceof JSONArray worms)) {
|
||||
errors.add(path + ".worms must be an array with at least one Perlin-worm profile.");
|
||||
return;
|
||||
}
|
||||
if (worms.length() < 1) {
|
||||
errors.add(path + ".worms must contain at least one Perlin-worm profile.");
|
||||
return;
|
||||
}
|
||||
if (worms.length() > 16) {
|
||||
errors.add(path + ".worms must contain at most 16 root profiles.");
|
||||
}
|
||||
Set<String> ids = new HashSet<String>();
|
||||
Set<Long> seeds = new HashSet<Long>();
|
||||
int profileCount = validateWormArray(path + ".worms", worms, 1, ids, seeds, errors);
|
||||
if (profileCount > 128) {
|
||||
errors.add(path + ".worms hierarchy must contain at most 128 profiles.");
|
||||
}
|
||||
}
|
||||
|
||||
private static int validateWormArray(
|
||||
String path,
|
||||
JSONArray worms,
|
||||
int depth,
|
||||
Set<String> ids,
|
||||
Set<Long> seeds,
|
||||
List<String> errors
|
||||
) {
|
||||
if (worms.length() > 16) {
|
||||
errors.add(path + " must contain at most 16 profiles.");
|
||||
}
|
||||
double totalWeight = 0D;
|
||||
int profileCount = 0;
|
||||
for (int index = 0; index < worms.length(); index++) {
|
||||
JSONObject worm = worms.optJSONObject(index);
|
||||
String wormPath = path + "[" + index + "]";
|
||||
if (worm == null) {
|
||||
errors.add(wormPath + " must be an object.");
|
||||
continue;
|
||||
}
|
||||
profileCount++;
|
||||
validateWormId(wormPath, worm, ids, errors);
|
||||
long seed = validateWormSeed(wormPath, worm, errors);
|
||||
if (!seeds.add(seed)) {
|
||||
errors.add(wormPath + ".seed must be unique inside the worm hierarchy.");
|
||||
}
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "weight", 0.000001D, 1000000D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "wavelength", 8D, 16384D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "detailWavelength", 8D, 16384D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "tortuosity", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "detailTortuosity", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "maxOffset", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(wormPath, worm, "segments", 1, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "widthMultiplier", 0.125D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "bankMultiplier", 0.125D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "depthMultiplier", 0.125D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "bodyWavelength", 32D, 16384D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "bodyDetailWavelength", 32D, 16384D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "widthVariation", 0D, 0.875D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "bankVariation", 0D, 0.875D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "depthVariation", 0D, 0.875D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "roofVariation", 0D, 0.875D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(wormPath, worm, "branchCap", 1, 8, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "branchDecay", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "confluenceMultiplier", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "childChance", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "branchChildChance", 0D, 1D, errors);
|
||||
totalWeight += doubleValue(worm, "weight", 1D);
|
||||
Object rawChildren = worm.opt("children");
|
||||
if (rawChildren == null || rawChildren == JSONObject.NULL) {
|
||||
continue;
|
||||
}
|
||||
if (!(rawChildren instanceof JSONArray children)) {
|
||||
errors.add(wormPath + ".children must be an array.");
|
||||
continue;
|
||||
}
|
||||
if (children.length() > 0 && depth >= 4) {
|
||||
errors.add(wormPath + ".children exceeds the maximum hierarchy depth of 4.");
|
||||
continue;
|
||||
}
|
||||
profileCount += validateWormArray(
|
||||
wormPath + ".children",
|
||||
children,
|
||||
depth + 1,
|
||||
ids,
|
||||
seeds,
|
||||
errors
|
||||
);
|
||||
}
|
||||
if (worms.length() > 0 && (!Double.isFinite(totalWeight) || totalWeight <= 0D)) {
|
||||
errors.add(path + " total weight must be finite and positive.");
|
||||
}
|
||||
return profileCount;
|
||||
}
|
||||
|
||||
private static void validateWormId(String path, JSONObject worm, Set<String> ids, List<String> errors) {
|
||||
Object rawId = worm.opt("id");
|
||||
if (!(rawId instanceof String id) || !id.matches("[a-z0-9][a-z0-9_-]{0,63}")) {
|
||||
errors.add(path + ".id must use 1 to 64 lowercase letters, digits, underscores, or hyphens.");
|
||||
return;
|
||||
}
|
||||
if (!ids.add(id)) {
|
||||
errors.add(path + ".id must be unique inside the worm hierarchy.");
|
||||
}
|
||||
}
|
||||
|
||||
private static long validateWormSeed(String path, JSONObject worm, List<String> errors) {
|
||||
Object rawSeed = worm.opt("seed");
|
||||
if (rawSeed == null || rawSeed == JSONObject.NULL) {
|
||||
return 1L;
|
||||
}
|
||||
if (!(rawSeed instanceof Number number)
|
||||
|| !Double.isFinite(number.doubleValue())
|
||||
|| number.doubleValue() != StrictMath.rint(number.doubleValue())) {
|
||||
errors.add(path + ".seed must be an integer.");
|
||||
return 1L;
|
||||
}
|
||||
return number.longValue();
|
||||
}
|
||||
|
||||
private static WormEnvelope wormEnvelope(JSONObject terrain) {
|
||||
JSONArray worms = terrain.optJSONArray("worms");
|
||||
if (worms == null || worms.length() == 0) {
|
||||
return new WormEnvelope(320D, 48);
|
||||
}
|
||||
double maximumOffset = 0D;
|
||||
int maximumSegments = 1;
|
||||
WormEnvelope envelope = wormEnvelope(worms, maximumOffset, maximumSegments);
|
||||
maximumOffset = envelope.maximumOffset();
|
||||
maximumSegments = envelope.maximumSegments();
|
||||
return new WormEnvelope(maximumOffset, maximumSegments);
|
||||
}
|
||||
|
||||
private static WormEnvelope wormEnvelope(JSONArray worms, double maximumOffset, int maximumSegments) {
|
||||
double resolvedOffset = maximumOffset;
|
||||
int resolvedSegments = maximumSegments;
|
||||
for (int index = 0; index < worms.length(); index++) {
|
||||
JSONObject worm = worms.optJSONObject(index);
|
||||
if (worm == null) {
|
||||
continue;
|
||||
}
|
||||
resolvedOffset = Math.max(resolvedOffset, doubleValue(worm, "maxOffset", 320D));
|
||||
resolvedSegments = Math.max(resolvedSegments, integerValue(worm, "segments", 48));
|
||||
JSONArray children = worm.optJSONArray("children");
|
||||
if (children != null) {
|
||||
WormEnvelope childEnvelope = wormEnvelope(children, resolvedOffset, resolvedSegments);
|
||||
resolvedOffset = Math.max(resolvedOffset, childEnvelope.maximumOffset());
|
||||
resolvedSegments = Math.max(resolvedSegments, childEnvelope.maximumSegments());
|
||||
}
|
||||
}
|
||||
return new WormEnvelope(resolvedOffset, resolvedSegments);
|
||||
}
|
||||
|
||||
private static void validateCaves(File packFolder, String path, JSONObject caves,
|
||||
boolean forceGeneratedGrotto,
|
||||
List<String> errors, List<String> warnings) {
|
||||
@@ -961,6 +1129,9 @@ final class PackRiverValidator {
|
||||
return chance == null ? defaultValue : doubleValue(chance, "chance", defaultValue);
|
||||
}
|
||||
|
||||
private record WormEnvelope(double maximumOffset, int maximumSegments) {
|
||||
}
|
||||
|
||||
record Validation(List<String> errors, List<String> warnings) {
|
||||
Validation {
|
||||
errors = List.copyOf(errors);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
@@ -83,18 +86,10 @@ public class IrisRiverTerrain {
|
||||
@Desc("The maximum vertical roof variation in river tunnels.")
|
||||
private double tunnelRoofVariation = 3D;
|
||||
|
||||
@Desc("Warps the spacing and amplitude of varied local-normal sweeps, hooks, curls, and wandering bends while preserving graph endpoints.")
|
||||
private IrisGeneratorStyle meanderStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(512D);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1024)
|
||||
@Desc("The total endpoint-spline and shape-personality displacement envelope in blocks.")
|
||||
private double meanderStrength = 72D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("The number of straight segments used to flatten and resolve each meandering graph reach.")
|
||||
private int meanderSubdivisions = 8;
|
||||
@Required
|
||||
@ArrayType(min = 1, type = IrisRiverWorm.class)
|
||||
@Desc("Weighted root Perlin-worm families with inherited child styles for trunks and tributaries.")
|
||||
private KList<IrisRiverWorm> worms = new KList<IrisRiverWorm>();
|
||||
|
||||
@Desc("Modulates small river-bed height variation after the connected channel shape is solved.")
|
||||
private IrisGeneratorStyle bedRoughnessStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(96D);
|
||||
|
||||
@@ -92,16 +92,6 @@ public class IrisRiverTopology {
|
||||
@Desc("The deterministic attraction toward shared downstream nodes. Larger values form stronger tributary trees and confluences.")
|
||||
private double confluenceWeight = 0D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@Desc("The number of upstream children a graph node accepts before additional branches begin shrinking probabilistically.")
|
||||
private int branchSoftCap = 4;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The multiplicative survival factor for each child beyond branchSoftCap. Recursive generations remain unbounded by depth.")
|
||||
private double branchChildShrinkFactor = 0.35D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("The contribution of natural terrain height to downstream routing cost.")
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("One weighted Perlin-worm river shape and its channel proportions.")
|
||||
@Data
|
||||
public class IrisRiverWorm {
|
||||
@Required
|
||||
@Desc("Unique lowercase identifier for this root or child style.")
|
||||
private String id = "river";
|
||||
|
||||
@Desc("Stable salt for this Perlin field pair.")
|
||||
private long seed = 1L;
|
||||
|
||||
@MinNumber(0.000001)
|
||||
@MaxNumber(1000000)
|
||||
@Desc("Relative probability when selecting this root family or one child transition.")
|
||||
private double weight = 1D;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(16384)
|
||||
@Desc("Primary gradient-Perlin wavelength in blocks.")
|
||||
private double wavelength = 1024D;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(16384)
|
||||
@Desc("Secondary gradient-Perlin wavelength in blocks.")
|
||||
private double detailWavelength = 256D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Primary heading deviation as a fraction of 180 degrees.")
|
||||
private double tortuosity = 0.5D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Secondary heading deviation as a fraction of 180 degrees.")
|
||||
private double detailTortuosity = 0.15D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1024)
|
||||
@Desc("Maximum endpoint-bridged displacement from the reach chord in blocks.")
|
||||
private double maxOffset = 320D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("Number of deterministic Perlin-worm steps used to resolve the reach.")
|
||||
private int segments = 48;
|
||||
|
||||
@MinNumber(0.125)
|
||||
@MaxNumber(8)
|
||||
@Desc("Channel-width multiplier for reaches selecting this worm.")
|
||||
private double widthMultiplier = 1D;
|
||||
|
||||
@MinNumber(0.125)
|
||||
@MaxNumber(8)
|
||||
@Desc("Bank-width multiplier for reaches selecting this worm.")
|
||||
private double bankMultiplier = 1D;
|
||||
|
||||
@MinNumber(0.125)
|
||||
@MaxNumber(8)
|
||||
@Desc("Depth multiplier for reaches selecting this worm.")
|
||||
private double depthMultiplier = 1D;
|
||||
|
||||
@MinNumber(32)
|
||||
@MaxNumber(16384)
|
||||
@Desc("Primary world-space wavelength controlling longitudinal body swelling and pinching.")
|
||||
private double bodyWavelength = 512D;
|
||||
|
||||
@MinNumber(32)
|
||||
@MaxNumber(16384)
|
||||
@Desc("Detail wavelength adding smaller changes to the longitudinal body profile.")
|
||||
private double bodyDetailWavelength = 128D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.875)
|
||||
@Desc("Maximum proportional channel-width variation along this style's body.")
|
||||
private double widthVariation = 0D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.875)
|
||||
@Desc("Maximum proportional bank or basin-width variation along this style's body.")
|
||||
private double bankVariation = 0D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.875)
|
||||
@Desc("Maximum proportional bed-depth variation along this style's body.")
|
||||
private double depthVariation = 0D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.875)
|
||||
@Desc("Maximum downward variation of tunnel roof clearance without exceeding the authored cave headroom.")
|
||||
private double roofVariation = 0D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@Desc("Number of upstream children admitted before additional siblings decay probabilistically.")
|
||||
private int branchCap = 4;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Multiplicative survival probability for every sibling beyond branchCap.")
|
||||
private double branchDecay = 0.35D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("Multiplier applied to the dimension confluence attraction for this style.")
|
||||
private double confluenceMultiplier = 1D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Chance that an upstream continuation mutates from this style into one weighted child.")
|
||||
private double childChance = 0D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Additional child-mutation chance for each sibling slot beyond the primary branch.")
|
||||
private double branchChildChance = 0D;
|
||||
|
||||
@ArrayType(type = IrisRiverWorm.class)
|
||||
@Desc("Weighted descendant styles inherited by the complete upstream lineage after mutation.")
|
||||
private KList<IrisRiverWorm> children = new KList<IrisRiverWorm>();
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class RiverBodyProfile {
|
||||
private final double[] positions;
|
||||
private final double[] widths;
|
||||
private final double[] bankWidths;
|
||||
private final double[] depths;
|
||||
private final double[] roofScales;
|
||||
private final double maximumWidth;
|
||||
private final double maximumBankWidth;
|
||||
private final double maximumDepth;
|
||||
|
||||
public RiverBodyProfile(
|
||||
double[] positions,
|
||||
double[] widths,
|
||||
double[] bankWidths,
|
||||
double[] depths,
|
||||
double[] roofScales
|
||||
) {
|
||||
if (positions == null || widths == null || bankWidths == null || depths == null || roofScales == null
|
||||
|| positions.length < 2
|
||||
|| positions.length != widths.length
|
||||
|| positions.length != bankWidths.length
|
||||
|| positions.length != depths.length
|
||||
|| positions.length != roofScales.length) {
|
||||
throw new IllegalArgumentException("River body profiles require matching dimension samples");
|
||||
}
|
||||
this.positions = positions.clone();
|
||||
this.widths = widths.clone();
|
||||
this.bankWidths = bankWidths.clone();
|
||||
this.depths = depths.clone();
|
||||
this.roofScales = roofScales.clone();
|
||||
double resolvedMaximumWidth = 0D;
|
||||
double resolvedMaximumBankWidth = 0D;
|
||||
double resolvedMaximumDepth = 0D;
|
||||
for (int index = 0; index < this.positions.length; index++) {
|
||||
double position = this.positions[index];
|
||||
if (!Double.isFinite(position) || position < 0D || position > 1D
|
||||
|| index > 0 && position <= this.positions[index - 1]) {
|
||||
throw new IllegalArgumentException("River body profile positions must increase from zero to one");
|
||||
}
|
||||
requirePositive(this.widths[index], "width");
|
||||
requireNonNegative(this.bankWidths[index], "bank width");
|
||||
requirePositive(this.depths[index], "depth");
|
||||
requireUnitScale(this.roofScales[index], "roof scale");
|
||||
resolvedMaximumWidth = StrictMath.max(resolvedMaximumWidth, this.widths[index]);
|
||||
resolvedMaximumBankWidth = StrictMath.max(resolvedMaximumBankWidth, this.bankWidths[index]);
|
||||
resolvedMaximumDepth = StrictMath.max(resolvedMaximumDepth, this.depths[index]);
|
||||
}
|
||||
if (this.positions[0] != 0D || this.positions[this.positions.length - 1] != 1D) {
|
||||
throw new IllegalArgumentException("River body profile positions must include zero and one");
|
||||
}
|
||||
maximumWidth = resolvedMaximumWidth;
|
||||
maximumBankWidth = resolvedMaximumBankWidth;
|
||||
maximumDepth = resolvedMaximumDepth;
|
||||
}
|
||||
|
||||
public static RiverBodyProfile constant(double width, double bankWidth, double depth) {
|
||||
return new RiverBodyProfile(
|
||||
new double[]{0D, 1D},
|
||||
new double[]{width, width},
|
||||
new double[]{bankWidth, bankWidth},
|
||||
new double[]{depth, depth},
|
||||
new double[]{1D, 1D}
|
||||
);
|
||||
}
|
||||
|
||||
public double width(double alongReach) {
|
||||
return sample(widths, alongReach);
|
||||
}
|
||||
|
||||
public double bankWidth(double alongReach) {
|
||||
return sample(bankWidths, alongReach);
|
||||
}
|
||||
|
||||
public double depth(double alongReach) {
|
||||
return sample(depths, alongReach);
|
||||
}
|
||||
|
||||
public double roofScale(double alongReach) {
|
||||
return sample(roofScales, alongReach);
|
||||
}
|
||||
|
||||
public double maximumWidth() {
|
||||
return maximumWidth;
|
||||
}
|
||||
|
||||
public double maximumBankWidth() {
|
||||
return maximumBankWidth;
|
||||
}
|
||||
|
||||
public double maximumDepth() {
|
||||
return maximumDepth;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return positions.length;
|
||||
}
|
||||
|
||||
public double position(int index) {
|
||||
return positions[index];
|
||||
}
|
||||
|
||||
public double widthAtIndex(int index) {
|
||||
return widths[index];
|
||||
}
|
||||
|
||||
public double bankWidthAtIndex(int index) {
|
||||
return bankWidths[index];
|
||||
}
|
||||
|
||||
public double depthAtIndex(int index) {
|
||||
return depths[index];
|
||||
}
|
||||
|
||||
public double roofScaleAtIndex(int index) {
|
||||
return roofScales[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (!(object instanceof RiverBodyProfile profile)) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.equals(positions, profile.positions)
|
||||
&& Arrays.equals(widths, profile.widths)
|
||||
&& Arrays.equals(bankWidths, profile.bankWidths)
|
||||
&& Arrays.equals(depths, profile.depths)
|
||||
&& Arrays.equals(roofScales, profile.roofScales);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = Arrays.hashCode(positions);
|
||||
hash = 31 * hash + Arrays.hashCode(widths);
|
||||
hash = 31 * hash + Arrays.hashCode(bankWidths);
|
||||
hash = 31 * hash + Arrays.hashCode(depths);
|
||||
return 31 * hash + Arrays.hashCode(roofScales);
|
||||
}
|
||||
|
||||
private double sample(double[] values, double alongReach) {
|
||||
double position = StrictMath.max(0D, StrictMath.min(1D, alongReach));
|
||||
int index = Arrays.binarySearch(positions, position);
|
||||
if (index >= 0) {
|
||||
return values[index];
|
||||
}
|
||||
int upper = -index - 1;
|
||||
if (upper <= 0) {
|
||||
return values[0];
|
||||
}
|
||||
if (upper >= positions.length) {
|
||||
return values[values.length - 1];
|
||||
}
|
||||
int lower = upper - 1;
|
||||
double range = positions[upper] - positions[lower];
|
||||
double interpolation = range <= 0D ? 0D : (position - positions[lower]) / range;
|
||||
return values[lower] + (values[upper] - values[lower]) * interpolation;
|
||||
}
|
||||
|
||||
private static void requirePositive(double value, String name) {
|
||||
if (!Double.isFinite(value) || value <= 0D) {
|
||||
throw new IllegalArgumentException("River body profile " + name + " must be finite and positive");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireNonNegative(double value, String name) {
|
||||
if (!Double.isFinite(value) || value < 0D) {
|
||||
throw new IllegalArgumentException("River body profile " + name + " must be finite and non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireUnitScale(double value, String name) {
|
||||
if (!Double.isFinite(value) || value <= 0D || value > 1D) {
|
||||
throw new IllegalArgumentException("River body profile " + name + " must be greater than zero and at most one");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record RiverMeanderContext(
|
||||
RiverEdgeId reachId,
|
||||
double normalizedPosition,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
public RiverMeanderContext {
|
||||
Objects.requireNonNull(reachId);
|
||||
if (!Double.isFinite(normalizedPosition) || normalizedPosition < 0.0 || normalizedPosition > 1.0
|
||||
|| !Double.isFinite(x) || !Double.isFinite(z)) {
|
||||
throw new IllegalArgumentException("River meander context must be finite and normalized");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,21 +21,25 @@ public final class RiverNetwork {
|
||||
private static final long SOURCE_FLOOR_SALT = 0xD6E8FEB86659FD93L;
|
||||
private static final long REACH_SALT = 0x9B05688C2B3E6C1FL;
|
||||
private static final long DRY_SALT = 0x1F83D9ABFB41BD6BL;
|
||||
private static final long MEANDER_SALT = 0x5BE0CD19137E2179L;
|
||||
private static final long MEANDER_PERSONALITY_SALT = 0x243F6A8885A308D3L;
|
||||
private static final long MEANDER_AMPLITUDE_SALT = 0x13198A2E03707344L;
|
||||
private static final long MEANDER_PHASE_SALT = 0xD1310BA698DFB5ACL;
|
||||
private static final long MEANDER_SKEW_SALT = 0x2FFD72DBD01ADFB7L;
|
||||
private static final long MEANDER_CYCLE_SALT = 0x452821E638D01377L;
|
||||
private static final long MEANDER_FEATURE_A_SALT = 0xBE5466CF34E90C6CL;
|
||||
private static final long MEANDER_FEATURE_B_SALT = 0xC0AC29B7C97C50DDL;
|
||||
private static final long WORM_FAMILY_SALT = 0x5BE0CD19137E2179L;
|
||||
private static final long WORM_CHILD_GATE_SALT = 0x452821E638D01377L;
|
||||
private static final long WORM_CHILD_SELECTION_SALT = 0xBE5466CF34E90C6CL;
|
||||
private static final long WORM_PRIMARY_SALT = 0x243F6A8885A308D3L;
|
||||
private static final long WORM_DETAIL_SALT = 0x13198A2E03707344L;
|
||||
private static final long BODY_WIDTH_PRIMARY_SALT = 0xA4093822299F31D0L;
|
||||
private static final long BODY_WIDTH_DETAIL_SALT = 0x082EFA98EC4E6C89L;
|
||||
private static final long BODY_BANK_PRIMARY_SALT = 0x452821E638D01377L;
|
||||
private static final long BODY_BANK_DETAIL_SALT = 0xBE5466CF34E90C6CL;
|
||||
private static final long BODY_DEPTH_PRIMARY_SALT = 0xC0AC29B7C97C50DDL;
|
||||
private static final long BODY_DEPTH_DETAIL_SALT = 0x3F84D5B5B5470917L;
|
||||
private static final long BODY_ROOF_PRIMARY_SALT = 0xD1310BA698DFB5ACL;
|
||||
private static final long BODY_ROOF_DETAIL_SALT = 0x2FFD72DBD01ADFB7L;
|
||||
private static final long CONFLUENCE_SALT = 0x9E3779B97F4A7C15L;
|
||||
private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL;
|
||||
private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL;
|
||||
private static final int MEANDER_NOISE_SAMPLES = 12;
|
||||
private static final int WIDTH_PROFILE_SAMPLES = 12;
|
||||
private static final double TWO_PI = StrictMath.PI * 2D;
|
||||
private static final MeanderPersonality[] MEANDER_PERSONALITIES = MeanderPersonality.values();
|
||||
private static final int MINIMUM_BODY_PROFILE_SAMPLES = 12;
|
||||
private static final int MAXIMUM_BODY_PROFILE_SAMPLES = 32;
|
||||
private static final double PERLIN_NORMALIZATION = 1.4142135623730951D;
|
||||
|
||||
private final RiverNetworkOptions options;
|
||||
|
||||
@@ -208,7 +212,7 @@ public final class RiverNetwork {
|
||||
double routingScore = naturalHeight * options.terrainHeightWeight()
|
||||
+ routingNoise * options.routingNoiseWeight()
|
||||
+ finiteOrZero(terrainSample.routingCost());
|
||||
double drainageDistance = drainageDistance(id);
|
||||
double drainageDistance = drainageBasin(id).distance();
|
||||
double hydraulicHeight = ocean
|
||||
? options.hydraulicBaseHeight()
|
||||
: options.hydraulicBaseHeight()
|
||||
@@ -228,6 +232,10 @@ public final class RiverNetwork {
|
||||
}
|
||||
|
||||
private double drainageDistance(RiverNodeId id) {
|
||||
return drainageBasin(id).distance();
|
||||
}
|
||||
|
||||
private DrainageBasin drainageBasin(RiverNodeId id) {
|
||||
int basinCells = options.routingBasinCells();
|
||||
double nodeX = id.cellX() + 0.5D;
|
||||
double nodeZ = id.cellZ() + 0.5D;
|
||||
@@ -245,19 +253,21 @@ public final class RiverNetwork {
|
||||
long basinZ = (long) StrictMath.floor(nodeZ / basinCells);
|
||||
double jitterRadius = basinCells * 0.45D;
|
||||
double nearestDistance = Double.MAX_VALUE;
|
||||
DrainageBasinId nearestId = null;
|
||||
for (long candidateX = basinX - 1L; candidateX <= basinX + 1L; candidateX++) {
|
||||
for (long candidateZ = basinZ - 1L; candidateZ <= basinZ + 1L; candidateZ++) {
|
||||
double siteX = (candidateX + 0.5D) * basinCells
|
||||
+ centered(hash(candidateX, candidateZ, BASIN_X_SALT)) * jitterRadius;
|
||||
double siteZ = (candidateZ + 0.5D) * basinCells
|
||||
+ centered(hash(candidateX, candidateZ, BASIN_Z_SALT)) * jitterRadius;
|
||||
nearestDistance = StrictMath.min(
|
||||
nearestDistance,
|
||||
StrictMath.hypot(nodeX - siteX, nodeZ - siteZ)
|
||||
);
|
||||
double distance = StrictMath.hypot(nodeX - siteX, nodeZ - siteZ);
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
nearestId = new DrainageBasinId(candidateX, candidateZ);
|
||||
}
|
||||
}
|
||||
return nearestDistance;
|
||||
}
|
||||
return new DrainageBasin(nearestId, nearestDistance);
|
||||
}
|
||||
|
||||
private double smoothCellNoise(double x, double z, int scale, long salt) {
|
||||
@@ -305,7 +315,8 @@ public final class RiverNetwork {
|
||||
if (compareRank(neighbor, node) >= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!branchPermitted(node, neighbor, resolver)) {
|
||||
RiverWorm worm = resolver.worm(node, neighbor);
|
||||
if (!branchPermitted(node, neighbor, resolver, worm)) {
|
||||
continue;
|
||||
}
|
||||
RiverRoutingContext context = resolver.routingContext(node, neighbor);
|
||||
@@ -313,7 +324,8 @@ public final class RiverNetwork {
|
||||
double oceanAttraction = neighbor.ocean() ? options.oceanAttraction() : 0.0;
|
||||
double flowAlignmentCost = flowAlignmentCost(node, neighbor, resolver);
|
||||
double confluenceAttraction = unit(hash(neighbor.id(), CONFLUENCE_SALT))
|
||||
* options.confluenceWeight();
|
||||
* options.confluenceWeight()
|
||||
* worm.confluenceMultiplier();
|
||||
ranked.add(new RankedCandidate(
|
||||
neighbor,
|
||||
neighbor.routingScore() + routingCost + flowAlignmentCost
|
||||
@@ -331,15 +343,20 @@ public final class RiverNetwork {
|
||||
return List.copyOf(candidates);
|
||||
}
|
||||
|
||||
private boolean branchPermitted(RiverNode child, RiverNode parent, NodeResolver resolver) {
|
||||
private boolean branchPermitted(
|
||||
RiverNode child,
|
||||
RiverNode parent,
|
||||
NodeResolver resolver,
|
||||
RiverWorm worm
|
||||
) {
|
||||
RiverEdgeId childEdge = RiverEdgeId.of(child.id(), parent.id());
|
||||
int childSlot = resolver.branchSlot(parent, child);
|
||||
if (childSlot < options.branchSoftCap()) {
|
||||
if (childSlot < worm.branchCap()) {
|
||||
return true;
|
||||
}
|
||||
double survivalChance = 1D;
|
||||
for (int overflow = options.branchSoftCap(); overflow <= childSlot; overflow++) {
|
||||
survivalChance *= options.branchChildShrinkFactor();
|
||||
for (int overflow = worm.branchCap(); overflow <= childSlot; overflow++) {
|
||||
survivalChance *= worm.branchDecay();
|
||||
}
|
||||
return gate(hash(childEdge, BRANCH_GATE_SALT), survivalChance);
|
||||
}
|
||||
@@ -427,6 +444,7 @@ public final class RiverNetwork {
|
||||
from,
|
||||
to,
|
||||
resolver.routingContext(from, to),
|
||||
resolver.worm(from, to),
|
||||
resolver.terrain
|
||||
);
|
||||
accumulators.put(edgeId, accumulator);
|
||||
@@ -451,61 +469,65 @@ public final class RiverNetwork {
|
||||
}
|
||||
|
||||
private RiverPolyline createPolyline(
|
||||
RiverEdgeId id,
|
||||
RiverNode from,
|
||||
RiverNode to,
|
||||
NodeResolver resolver
|
||||
RiverWorm worm
|
||||
) {
|
||||
int pointCount = options.meanderSubdivisions() + 1;
|
||||
double[] baseX = new double[pointCount];
|
||||
double[] baseZ = new double[pointCount];
|
||||
int pointCount = worm.segments() + 1;
|
||||
double[] rawX = new double[pointCount];
|
||||
double[] rawZ = new double[pointCount];
|
||||
double deltaX = to.x() - from.x();
|
||||
double deltaZ = to.z() - from.z();
|
||||
double length = StrictMath.hypot(deltaX, deltaZ);
|
||||
double maximumOffset = StrictMath.min(options.meanderStrength(), length * 0.35);
|
||||
double directionX = length == 0D ? 1D : deltaX / length;
|
||||
double directionZ = length == 0D ? 0D : deltaZ / length;
|
||||
FlowTangent fromTangent = flowTangent(from, directionX, directionZ, resolver);
|
||||
FlowTangent toTangent = flowTangent(to, directionX, directionZ, resolver);
|
||||
for (int point = 0; point < pointCount; point++) {
|
||||
double t = (double) point / (pointCount - 1);
|
||||
double tSquared = t * t;
|
||||
double tCubed = tSquared * t;
|
||||
double fromWeight = 2D * tCubed - 3D * tSquared + 1D;
|
||||
double fromTangentWeight = tCubed - 2D * tSquared + t;
|
||||
double toWeight = -2D * tCubed + 3D * tSquared;
|
||||
double toTangentWeight = tCubed - tSquared;
|
||||
double curvedX = fromWeight * from.x()
|
||||
+ fromTangentWeight * fromTangent.x() * maximumOffset
|
||||
+ toWeight * to.x()
|
||||
+ toTangentWeight * toTangent.x() * maximumOffset;
|
||||
double curvedZ = fromWeight * from.z()
|
||||
+ fromTangentWeight * fromTangent.z() * maximumOffset
|
||||
+ toWeight * to.z()
|
||||
+ toTangentWeight * toTangent.z() * maximumOffset;
|
||||
baseX[point] = curvedX;
|
||||
baseZ[point] = curvedZ;
|
||||
rawX[0] = from.x();
|
||||
rawZ[0] = from.z();
|
||||
if (length <= 0D) {
|
||||
return new RiverPolyline(rawX, rawZ);
|
||||
}
|
||||
if (maximumOffset <= 0D) {
|
||||
return new RiverPolyline(baseX, baseZ);
|
||||
double baseHeading = StrictMath.atan2(deltaZ, deltaX);
|
||||
double stepLength = length / worm.segments();
|
||||
for (int point = 1; point < pointCount; point++) {
|
||||
double x = rawX[point - 1];
|
||||
double z = rawZ[point - 1];
|
||||
double primary = perlin(x, z, worm.wavelength(), worm.seed() ^ WORM_PRIMARY_SALT);
|
||||
double detail = perlin(x, z, worm.detailWavelength(), worm.seed() ^ WORM_DETAIL_SALT);
|
||||
double heading = baseHeading + StrictMath.PI * (
|
||||
primary * worm.tortuosity()
|
||||
+ detail * worm.detailTortuosity()
|
||||
);
|
||||
rawX[point] = x + StrictMath.cos(heading) * stepLength;
|
||||
rawZ[point] = z + StrictMath.sin(heading) * stepLength;
|
||||
}
|
||||
MeanderProfile profile = meanderProfile(id);
|
||||
double[] meanderNoise = meanderNoise(id, baseX, baseZ, resolver);
|
||||
double[] meanderSignal = meanderSignal(id, profile, meanderNoise);
|
||||
double[] x = new double[pointCount];
|
||||
double[] z = new double[pointCount];
|
||||
double rawDeltaX = rawX[pointCount - 1] - from.x();
|
||||
double rawDeltaZ = rawZ[pointCount - 1] - from.z();
|
||||
double maximumDisplacement = 0D;
|
||||
for (int point = 0; point < pointCount; point++) {
|
||||
double t = (double) point / (pointCount - 1);
|
||||
double tSquared = t * t;
|
||||
double envelope = 16D * tSquared * (1D - t) * (1D - t);
|
||||
double straightX = from.x() + deltaX * t;
|
||||
double straightZ = from.z() + deltaZ * t;
|
||||
double baseDisplacement = StrictMath.hypot(baseX[point] - straightX, baseZ[point] - straightZ);
|
||||
double availableOffset = StrictMath.max(0D, maximumOffset - baseDisplacement);
|
||||
double offset = availableOffset * 0.92D * envelope * meanderSignal[point];
|
||||
FlowTangent normal = localNormal(baseX, baseZ, point);
|
||||
x[point] = baseX[point] + normal.x() * offset;
|
||||
z[point] = baseZ[point] + normal.z() * offset;
|
||||
double rawBridgeX = rawX[point] - (from.x() + rawDeltaX * t);
|
||||
double rawBridgeZ = rawZ[point] - (from.z() + rawDeltaZ * t);
|
||||
x[point] = straightX + rawBridgeX * envelope;
|
||||
z[point] = straightZ + rawBridgeZ * envelope;
|
||||
maximumDisplacement = StrictMath.max(
|
||||
maximumDisplacement,
|
||||
StrictMath.hypot(x[point] - straightX, z[point] - straightZ)
|
||||
);
|
||||
}
|
||||
double maximumOffset = StrictMath.min(worm.maxOffset(), length * 0.35D);
|
||||
if (maximumDisplacement > maximumOffset && maximumDisplacement > 0D) {
|
||||
double scale = maximumOffset / maximumDisplacement;
|
||||
for (int point = 1; point < pointCount - 1; point++) {
|
||||
double t = (double) point / (pointCount - 1);
|
||||
double straightX = from.x() + deltaX * t;
|
||||
double straightZ = from.z() + deltaZ * t;
|
||||
x[point] = straightX + (x[point] - straightX) * scale;
|
||||
z[point] = straightZ + (z[point] - straightZ) * scale;
|
||||
}
|
||||
}
|
||||
x[0] = from.x();
|
||||
z[0] = from.z();
|
||||
@@ -514,222 +536,141 @@ public final class RiverNetwork {
|
||||
return new RiverPolyline(x, z);
|
||||
}
|
||||
|
||||
private double[] meanderNoise(
|
||||
RiverEdgeId id,
|
||||
double[] baseX,
|
||||
double[] baseZ,
|
||||
NodeResolver resolver
|
||||
private RiverWorm rootWormFor(RiverNodeId rootId) {
|
||||
return selectWeighted(options.worms(), hash(rootId, WORM_FAMILY_SALT));
|
||||
}
|
||||
|
||||
private RiverWorm childWormFor(
|
||||
RiverWorm parent,
|
||||
RiverNodeId parentId,
|
||||
RiverNodeId childId,
|
||||
int branchSlot
|
||||
) {
|
||||
int sampleCount = StrictMath.min(MEANDER_NOISE_SAMPLES, baseX.length);
|
||||
double[] knots = new double[sampleCount];
|
||||
for (int sample = 0; sample < sampleCount; sample++) {
|
||||
double t = (double) sample / (sampleCount - 1);
|
||||
double position = t * (baseX.length - 1);
|
||||
int lower = (int) StrictMath.floor(position);
|
||||
int upper = StrictMath.min(baseX.length - 1, lower + 1);
|
||||
double interpolation = position - lower;
|
||||
double curvedX = baseX[lower] + (baseX[upper] - baseX[lower]) * interpolation;
|
||||
double curvedZ = baseZ[lower] + (baseZ[upper] - baseZ[lower]) * interpolation;
|
||||
RiverMeanderContext context = new RiverMeanderContext(id, t, curvedX, curvedZ);
|
||||
double configuredNoise = resolver.terrain.meanderNoise(context);
|
||||
knots[sample] = Double.isFinite(configuredNoise)
|
||||
? StrictMath.max(-1D, StrictMath.min(1D, configuredNoise))
|
||||
: smoothEdgeNoise(id, t * 3D);
|
||||
if (parent.children().isEmpty()) {
|
||||
return parent;
|
||||
}
|
||||
double[] noise = new double[baseX.length];
|
||||
for (int point = 0; point < noise.length; point++) {
|
||||
double t = (double) point / (noise.length - 1);
|
||||
double position = t * (sampleCount - 1);
|
||||
int lower = (int) StrictMath.floor(position);
|
||||
int upper = StrictMath.min(sampleCount - 1, lower + 1);
|
||||
double interpolation = smoothStep(position - lower);
|
||||
noise[point] = knots[lower] + (knots[upper] - knots[lower]) * interpolation;
|
||||
double chance = StrictMath.min(
|
||||
1D,
|
||||
parent.childChance()
|
||||
+ StrictMath.min(7, branchSlot) * parent.branchChildChance()
|
||||
);
|
||||
RiverEdgeId edgeId = RiverEdgeId.of(childId, parentId);
|
||||
if (!gate(hash(edgeId, WORM_CHILD_GATE_SALT), chance)) {
|
||||
return parent;
|
||||
}
|
||||
return noise;
|
||||
return selectWeighted(parent.children(), hash(edgeId, WORM_CHILD_SELECTION_SALT));
|
||||
}
|
||||
|
||||
private MeanderProfile meanderProfile(RiverEdgeId id) {
|
||||
int personalityIndex = (int) StrictMath.floor(
|
||||
unit(hash(id, MEANDER_PERSONALITY_SALT)) * MEANDER_PERSONALITIES.length
|
||||
private RiverWorm selectWeighted(List<RiverWorm> worms, long selectionHash) {
|
||||
double totalWeight = 0D;
|
||||
for (RiverWorm worm : worms) {
|
||||
totalWeight += worm.weight();
|
||||
}
|
||||
double selection = unit(selectionHash) * totalWeight;
|
||||
double cumulative = 0D;
|
||||
for (RiverWorm worm : worms) {
|
||||
cumulative += worm.weight();
|
||||
if (selection < cumulative) {
|
||||
return worm;
|
||||
}
|
||||
}
|
||||
return worms.get(worms.size() - 1);
|
||||
}
|
||||
|
||||
private double perlin(double x, double z, double wavelength, long salt) {
|
||||
double scaledX = x / wavelength;
|
||||
double scaledZ = z / wavelength;
|
||||
long minimumX = (long) StrictMath.floor(scaledX);
|
||||
long minimumZ = (long) StrictMath.floor(scaledZ);
|
||||
double fractionX = scaledX - minimumX;
|
||||
double fractionZ = scaledZ - minimumZ;
|
||||
double fadeX = perlinFade(fractionX);
|
||||
double fadeZ = perlinFade(fractionZ);
|
||||
double northwest = perlinGradient(minimumX, minimumZ, fractionX, fractionZ, salt);
|
||||
double northeast = perlinGradient(minimumX + 1L, minimumZ, fractionX - 1D, fractionZ, salt);
|
||||
double southwest = perlinGradient(minimumX, minimumZ + 1L, fractionX, fractionZ - 1D, salt);
|
||||
double southeast = perlinGradient(
|
||||
minimumX + 1L,
|
||||
minimumZ + 1L,
|
||||
fractionX - 1D,
|
||||
fractionZ - 1D,
|
||||
salt
|
||||
);
|
||||
MeanderPersonality personality = MEANDER_PERSONALITIES[
|
||||
StrictMath.min(MEANDER_PERSONALITIES.length - 1, personalityIndex)
|
||||
];
|
||||
double amplitudeRoll = unit(hash(id, MEANDER_AMPLITUDE_SALT));
|
||||
double amplitude = personality == MeanderPersonality.QUIET
|
||||
? 0.16D + amplitudeRoll * 0.24D
|
||||
: 0.48D + amplitudeRoll * 0.52D;
|
||||
double maximumCycles = StrictMath.max(1D, StrictMath.min(8D, options.meanderSubdivisions() / 5D));
|
||||
double cycleRoll = unit(hash(id, MEANDER_CYCLE_SALT));
|
||||
double cycles = switch (personality) {
|
||||
case SWEEP, HOOK, QUIET -> 0.45D + cycleRoll * 0.65D;
|
||||
case S_CURVE -> 0.75D + cycleRoll * StrictMath.min(1.25D, maximumCycles);
|
||||
case OXBOW -> 1D + cycleRoll * StrictMath.min(1.5D, maximumCycles);
|
||||
case COIL -> StrictMath.min(maximumCycles, 2.25D + cycleRoll * 4.75D);
|
||||
case WANDER -> 0.65D + cycleRoll * StrictMath.min(2.35D, maximumCycles);
|
||||
case CHIRP -> StrictMath.min(maximumCycles, 1.5D + cycleRoll * 5.5D);
|
||||
};
|
||||
double handedness = (hash(id, MEANDER_SALT) & 1L) == 0L ? -1D : 1D;
|
||||
return new MeanderProfile(
|
||||
personality,
|
||||
amplitude,
|
||||
handedness,
|
||||
unit(hash(id, MEANDER_PHASE_SALT)) * TWO_PI,
|
||||
centered(hash(id, MEANDER_SKEW_SALT)) * 0.72D,
|
||||
cycles,
|
||||
unit(hash(id, MEANDER_FEATURE_A_SALT)),
|
||||
unit(hash(id, MEANDER_FEATURE_B_SALT))
|
||||
double north = northwest + (northeast - northwest) * fadeX;
|
||||
double south = southwest + (southeast - southwest) * fadeX;
|
||||
return StrictMath.max(
|
||||
-1D,
|
||||
StrictMath.min(1D, (north + (south - north) * fadeZ) * PERLIN_NORMALIZATION)
|
||||
);
|
||||
}
|
||||
|
||||
private double[] meanderSignal(RiverEdgeId id, MeanderProfile profile, double[] noise) {
|
||||
int subdivisions = noise.length - 1;
|
||||
for (int point = 0; point < noise.length; point++) {
|
||||
double t = (double) point / subdivisions;
|
||||
noise[point] = noise[point] * 0.78D + smoothEdgeNoise(id, t * 3.5D) * 0.22D;
|
||||
}
|
||||
smoothSignal(noise);
|
||||
double detailCycles = StrictMath.max(0.5D, profile.cycles() + profile.featureB() * 1.5D);
|
||||
double baseStep = TWO_PI * detailCycles / subdivisions;
|
||||
double phase = profile.phase() + profile.featureA() * StrictMath.PI;
|
||||
double[] signal = new double[noise.length];
|
||||
for (int point = 0; point < noise.length; point++) {
|
||||
double t = (double) point / subdivisions;
|
||||
if (point > 0) {
|
||||
double frequencyNoise = (noise[point - 1] + noise[point]) * 0.25D + 0.5D;
|
||||
phase += baseStep * (0.45D + frequencyNoise * 1.1D);
|
||||
}
|
||||
double warpedT = warpMeanderPosition(t, profile.skew());
|
||||
double macro = personalitySignal(profile, warpedT, noise[point]);
|
||||
double detailWeight = detailWeight(profile.personality());
|
||||
double detail = detailWeight <= 0D
|
||||
? 0D
|
||||
: StrictMath.sin(phase) * (0.65D + StrictMath.abs(noise[point]) * 0.35D);
|
||||
double noiseWeight = noiseWeight(profile.personality());
|
||||
double macroWeight = 1D - detailWeight - noiseWeight;
|
||||
signal[point] = clampSigned(profile.amplitude()
|
||||
* (macro * macroWeight + detail * detailWeight + noise[point] * noiseWeight));
|
||||
}
|
||||
smoothSignal(signal);
|
||||
return signal;
|
||||
}
|
||||
|
||||
private double personalitySignal(MeanderProfile profile, double t, double noise) {
|
||||
double handedness = profile.handedness();
|
||||
return switch (profile.personality()) {
|
||||
case SWEEP -> handedness * (0.68D + StrictMath.sin(StrictMath.PI * t) * 0.32D);
|
||||
case HOOK -> handedness * (0.12D + smoothStep(
|
||||
profile.featureA() < 0.5D ? t : 1D - t
|
||||
) * 0.88D);
|
||||
case S_CURVE -> handedness * StrictMath.sin(
|
||||
TWO_PI * profile.cycles() * t + profile.phase()
|
||||
);
|
||||
case COIL -> StrictMath.sin(TWO_PI * profile.cycles() * t + profile.phase())
|
||||
* (0.62D + StrictMath.sin(StrictMath.PI * t) * 0.38D);
|
||||
case WANDER -> clampSigned(
|
||||
noise * 0.82D
|
||||
+ StrictMath.sin(TWO_PI * profile.cycles() * t + profile.phase()) * 0.38D
|
||||
+ handedness * 0.12D
|
||||
);
|
||||
case OXBOW -> handedness * clampSigned(
|
||||
compactLobe(t, 0.18D + profile.featureA() * 0.2D, 0.24D + profile.featureB() * 0.12D)
|
||||
- compactLobe(t, 0.62D + profile.featureB() * 0.2D, 0.22D + profile.featureA() * 0.14D)
|
||||
* (0.55D + profile.featureA() * 0.45D)
|
||||
);
|
||||
case CHIRP -> StrictMath.sin(
|
||||
profile.phase() + TWO_PI * (0.45D * t + profile.cycles() * t * t)
|
||||
);
|
||||
case QUIET -> handedness * (0.72D + noise * 0.28D);
|
||||
private double perlinGradient(long latticeX, long latticeZ, double x, double z, long salt) {
|
||||
return switch ((int) (hash(latticeX, latticeZ, salt) & 7L)) {
|
||||
case 0 -> x;
|
||||
case 1 -> -x;
|
||||
case 2 -> z;
|
||||
case 3 -> -z;
|
||||
case 4 -> (x + z) / PERLIN_NORMALIZATION;
|
||||
case 5 -> (-x + z) / PERLIN_NORMALIZATION;
|
||||
case 6 -> (x - z) / PERLIN_NORMALIZATION;
|
||||
default -> (-x - z) / PERLIN_NORMALIZATION;
|
||||
};
|
||||
}
|
||||
|
||||
private double detailWeight(MeanderPersonality personality) {
|
||||
return switch (personality) {
|
||||
case SWEEP, HOOK, OXBOW, QUIET -> 0D;
|
||||
case S_CURVE -> 0.12D;
|
||||
case COIL, WANDER -> 0.08D;
|
||||
case CHIRP -> 0.1D;
|
||||
};
|
||||
private double perlinFade(double value) {
|
||||
double squared = value * value;
|
||||
double cubed = squared * value;
|
||||
return cubed * (value * (value * 6D - 15D) + 10D);
|
||||
}
|
||||
|
||||
private double noiseWeight(MeanderPersonality personality) {
|
||||
return switch (personality) {
|
||||
case SWEEP, S_CURVE, COIL, OXBOW, CHIRP -> 0.08D;
|
||||
case HOOK -> 0.1D;
|
||||
case WANDER -> 0.34D;
|
||||
case QUIET -> 0.14D;
|
||||
};
|
||||
}
|
||||
|
||||
private double warpMeanderPosition(double t, double skew) {
|
||||
return t + skew * t * (1D - t);
|
||||
}
|
||||
|
||||
private double compactLobe(double t, double center, double radius) {
|
||||
double distance = StrictMath.abs(t - center) / radius;
|
||||
if (distance >= 1D) {
|
||||
return 0D;
|
||||
}
|
||||
return smoothStep(1D - distance);
|
||||
}
|
||||
|
||||
private double smoothStep(double value) {
|
||||
double clamped = StrictMath.max(0D, StrictMath.min(1D, value));
|
||||
return clamped * clamped * (3D - 2D * clamped);
|
||||
}
|
||||
|
||||
private void smoothSignal(double[] signal) {
|
||||
if (signal.length < 3) {
|
||||
return;
|
||||
}
|
||||
double previousRaw = signal[0];
|
||||
double currentRaw = signal[1];
|
||||
for (int point = 1; point < signal.length - 1; point++) {
|
||||
double nextRaw = signal[point + 1];
|
||||
signal[point] = (previousRaw + currentRaw * 2D + nextRaw) * 0.25D;
|
||||
previousRaw = currentRaw;
|
||||
currentRaw = nextRaw;
|
||||
}
|
||||
}
|
||||
|
||||
private double clampSigned(double value) {
|
||||
return StrictMath.max(-1D, StrictMath.min(1D, value));
|
||||
}
|
||||
|
||||
private FlowTangent localNormal(double[] x, double[] z, int point) {
|
||||
int previous = StrictMath.max(0, point - 1);
|
||||
int next = StrictMath.min(x.length - 1, point + 1);
|
||||
double tangentX = x[next] - x[previous];
|
||||
double tangentZ = z[next] - z[previous];
|
||||
double tangentLength = StrictMath.hypot(tangentX, tangentZ);
|
||||
if (tangentLength <= 0.0000001D) {
|
||||
return new FlowTangent(0D, 0D);
|
||||
}
|
||||
return new FlowTangent(-tangentZ / tangentLength, tangentX / tangentLength);
|
||||
}
|
||||
|
||||
private FlowTangent flowTangent(
|
||||
RiverNode node,
|
||||
double fallbackX,
|
||||
double fallbackZ,
|
||||
NodeResolver resolver
|
||||
private double bodyMultiplier(
|
||||
ReachPosition position,
|
||||
RiverWorm worm,
|
||||
long primarySalt,
|
||||
long detailSalt,
|
||||
double variation
|
||||
) {
|
||||
FlowTangent preferred = resolver.flowTangent(node);
|
||||
double tangentX = preferred.x();
|
||||
double tangentZ = preferred.z();
|
||||
if (tangentX == 0D && tangentZ == 0D) {
|
||||
return new FlowTangent(fallbackX, fallbackZ);
|
||||
if (variation <= 0D) {
|
||||
return 1D;
|
||||
}
|
||||
double alignment = tangentX * fallbackX + tangentZ * fallbackZ;
|
||||
if (alignment < 0D) {
|
||||
tangentX = -tangentX;
|
||||
tangentZ = -tangentZ;
|
||||
alignment = -alignment;
|
||||
return StrictMath.max(
|
||||
0.125D,
|
||||
1D + bodyField(position, worm, primarySalt, detailSalt) * variation
|
||||
);
|
||||
}
|
||||
if (alignment < 0.15D) {
|
||||
return new FlowTangent(fallbackX, fallbackZ);
|
||||
|
||||
private double roofScale(ReachPosition position, RiverWorm worm) {
|
||||
if (worm.roofVariation() <= 0D) {
|
||||
return 1D;
|
||||
}
|
||||
return new FlowTangent(tangentX, tangentZ);
|
||||
double normalized = bodyField(
|
||||
position,
|
||||
worm,
|
||||
BODY_ROOF_PRIMARY_SALT,
|
||||
BODY_ROOF_DETAIL_SALT
|
||||
) * 0.5D + 0.5D;
|
||||
return StrictMath.max(0.125D, 1D - normalized * worm.roofVariation());
|
||||
}
|
||||
|
||||
private double bodyField(
|
||||
ReachPosition position,
|
||||
RiverWorm worm,
|
||||
long primarySalt,
|
||||
long detailSalt
|
||||
) {
|
||||
double primary = perlin(
|
||||
position.x(),
|
||||
position.z(),
|
||||
worm.bodyWavelength(),
|
||||
worm.seed() ^ primarySalt
|
||||
);
|
||||
double detail = perlin(
|
||||
position.x(),
|
||||
position.z(),
|
||||
worm.bodyDetailWavelength(),
|
||||
worm.seed() ^ detailSalt
|
||||
);
|
||||
return primary * 0.7D + detail * 0.3D;
|
||||
}
|
||||
|
||||
private FlowTangent resolveFlowTangent(RiverNode node, RiverTerrainSampler terrain) {
|
||||
@@ -771,16 +712,6 @@ public final class RiverNetwork {
|
||||
return options.flowAlignmentWeight() * (1D - StrictMath.min(1D, alignment));
|
||||
}
|
||||
|
||||
private double smoothEdgeNoise(RiverEdgeId id, double position) {
|
||||
int lower = (int) StrictMath.floor(position);
|
||||
int upper = lower + 1;
|
||||
double fraction = position - lower;
|
||||
double fade = fraction * fraction * (3.0 - 2.0 * fraction);
|
||||
double a = centered(mix(options.seed() ^ id.stableId() ^ MEANDER_SALT ^ lower * 0x9E3779B97F4A7C15L));
|
||||
double b = centered(mix(options.seed() ^ id.stableId() ^ MEANDER_SALT ^ upper * 0x9E3779B97F4A7C15L));
|
||||
return a + (b - a) * fade;
|
||||
}
|
||||
|
||||
private boolean intersects(
|
||||
RiverReach reach,
|
||||
long minimumX,
|
||||
@@ -812,8 +743,8 @@ public final class RiverNetwork {
|
||||
long maximumZ
|
||||
) {
|
||||
double length = StrictMath.hypot(to.x() - from.x(), to.z() - from.z());
|
||||
double maximumMeander = StrictMath.min(options.meanderStrength(), length * 0.35D);
|
||||
double padding = options.maximumReachRadius() + maximumMeander;
|
||||
double maximumWormOffset = StrictMath.min(options.maximumWormOffset(), length * 0.35D);
|
||||
double padding = options.maximumReachRadius() + maximumWormOffset;
|
||||
double reachMinimumX = StrictMath.min(from.x(), to.x()) - padding;
|
||||
double reachMaximumX = StrictMath.max(from.x(), to.x()) + padding;
|
||||
double reachMinimumZ = StrictMath.min(from.z(), to.z()) - padding;
|
||||
@@ -825,8 +756,8 @@ public final class RiverNetwork {
|
||||
private int geometryPaddingCells() {
|
||||
double maximumEdgeAxisDelta = options.cellSize() * (1D + options.siteJitter());
|
||||
double maximumEdgeLength = StrictMath.sqrt(2D) * maximumEdgeAxisDelta;
|
||||
double maximumMeander = StrictMath.min(options.meanderStrength(), maximumEdgeLength * 0.35D);
|
||||
double displacement = options.maximumReachRadius() + maximumMeander;
|
||||
double maximumWormOffset = StrictMath.min(options.maximumWormOffset(), maximumEdgeLength * 0.35D);
|
||||
double displacement = options.maximumReachRadius() + maximumWormOffset;
|
||||
return 1 + (int) StrictMath.ceil(displacement / options.cellSize());
|
||||
}
|
||||
|
||||
@@ -917,7 +848,12 @@ public final class RiverNetwork {
|
||||
private final Map<RiverEdgeId, RiverRoutingContext> routingContexts;
|
||||
private final Map<RiverNodeId, FlowTangent> flowTangents;
|
||||
private final Map<RiverEdgeId, Integer> branchSlots;
|
||||
private final Map<RiverNodeId, Double> styleDistances;
|
||||
private final Map<RiverNodeId, StyleParent> styleParents;
|
||||
private final Map<RiverNodeId, RiverWorm> styleWorms;
|
||||
private final Map<RiverEdgeId, Integer> styleBranchSlots;
|
||||
private final Map<RiverNodeId, Boolean> resolvedBranchParents;
|
||||
private final Map<RiverNodeId, Boolean> resolvedStyleBranchParents;
|
||||
|
||||
private NodeResolver(RiverTerrainSampler terrain) {
|
||||
this.terrain = terrain;
|
||||
@@ -932,7 +868,12 @@ public final class RiverNetwork {
|
||||
routingContexts = new HashMap<>();
|
||||
flowTangents = new HashMap<>();
|
||||
branchSlots = new HashMap<>();
|
||||
styleDistances = new HashMap<>();
|
||||
styleParents = new HashMap<>();
|
||||
styleWorms = new HashMap<>();
|
||||
styleBranchSlots = new HashMap<>();
|
||||
resolvedBranchParents = new HashMap<>();
|
||||
resolvedStyleBranchParents = new HashMap<>();
|
||||
}
|
||||
|
||||
private RiverNode resolve(RiverNodeId id) {
|
||||
@@ -1099,11 +1040,88 @@ public final class RiverNetwork {
|
||||
|
||||
private RiverRoutingContext routingContext(RiverNode from, RiverNode to) {
|
||||
RiverEdgeId edgeId = RiverEdgeId.of(from.id(), to.id());
|
||||
RiverWorm worm = worm(from, to);
|
||||
return routingContexts.computeIfAbsent(edgeId, ignored -> RiverRoutingContext.lazy(
|
||||
edgeId,
|
||||
from,
|
||||
to,
|
||||
() -> createPolyline(edgeId, from, to, this)));
|
||||
() -> createPolyline(from, to, worm)));
|
||||
}
|
||||
|
||||
private RiverWorm worm(RiverNode first, RiverNode second) {
|
||||
RiverNode child = compareRank(first, second) > 0 ? first : second;
|
||||
return styleWorm(child.id());
|
||||
}
|
||||
|
||||
private RiverWorm styleWorm(RiverNodeId id) {
|
||||
RiverWorm cached = styleWorms.get(id);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
StyleParent parent = styleParent(id);
|
||||
RiverWorm selected;
|
||||
if (parent.id() == null) {
|
||||
selected = rootWormFor(id);
|
||||
} else {
|
||||
RiverWorm parentWorm = styleWorm(parent.id());
|
||||
selected = childWormFor(
|
||||
parentWorm,
|
||||
parent.id(),
|
||||
id,
|
||||
styleBranchSlot(parent.id(), id)
|
||||
);
|
||||
}
|
||||
styleWorms.put(id, selected);
|
||||
return selected;
|
||||
}
|
||||
|
||||
private StyleParent styleParent(RiverNodeId id) {
|
||||
return styleParents.computeIfAbsent(id, nodeId -> {
|
||||
double nodeDistance = styleDistance(nodeId);
|
||||
RiverNodeId selected = null;
|
||||
double selectedDistance = Double.POSITIVE_INFINITY;
|
||||
for (RiverNodeId candidate : neighbors(nodeId)) {
|
||||
double candidateDistance = styleDistance(candidate);
|
||||
if (candidateDistance >= nodeDistance - 0.000000001D) {
|
||||
continue;
|
||||
}
|
||||
if (candidateDistance < selectedDistance
|
||||
|| candidateDistance == selectedDistance
|
||||
&& (selected == null || candidate.compareTo(selected) < 0)) {
|
||||
selected = candidate;
|
||||
selectedDistance = candidateDistance;
|
||||
}
|
||||
}
|
||||
return new StyleParent(selected);
|
||||
});
|
||||
}
|
||||
|
||||
private double styleDistance(RiverNodeId id) {
|
||||
return styleDistances.computeIfAbsent(id, RiverNetwork.this::drainageDistance);
|
||||
}
|
||||
|
||||
private int styleBranchSlot(RiverNodeId parentId, RiverNodeId childId) {
|
||||
if (!resolvedStyleBranchParents.containsKey(parentId)) {
|
||||
ArrayList<RiverNodeId> children = new ArrayList<>(8);
|
||||
for (RiverNodeId candidate : neighbors(parentId)) {
|
||||
StyleParent candidateParent = styleParent(candidate);
|
||||
if (parentId.equals(candidateParent.id())) {
|
||||
children.add(candidate);
|
||||
}
|
||||
}
|
||||
children.sort((first, second) -> {
|
||||
long firstPriority = hash(RiverEdgeId.of(first, parentId), BRANCH_SLOT_SALT);
|
||||
long secondPriority = hash(RiverEdgeId.of(second, parentId), BRANCH_SLOT_SALT);
|
||||
int priorityComparison = Long.compareUnsigned(firstPriority, secondPriority);
|
||||
return priorityComparison != 0 ? priorityComparison : first.compareTo(second);
|
||||
});
|
||||
for (int slot = 0; slot < children.size(); slot++) {
|
||||
RiverNodeId child = children.get(slot);
|
||||
styleBranchSlots.put(RiverEdgeId.of(child, parentId), slot);
|
||||
}
|
||||
resolvedStyleBranchParents.put(parentId, true);
|
||||
}
|
||||
return styleBranchSlots.getOrDefault(RiverEdgeId.of(childId, parentId), Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
private FlowTangent flowTangent(RiverNode node) {
|
||||
@@ -1122,27 +1140,13 @@ public final class RiverNetwork {
|
||||
private record FlowTangent(double x, double z) {
|
||||
}
|
||||
|
||||
private record MeanderProfile(
|
||||
MeanderPersonality personality,
|
||||
double amplitude,
|
||||
double handedness,
|
||||
double phase,
|
||||
double skew,
|
||||
double cycles,
|
||||
double featureA,
|
||||
double featureB
|
||||
) {
|
||||
private record DrainageBasinId(long x, long z) {
|
||||
}
|
||||
|
||||
private enum MeanderPersonality {
|
||||
QUIET,
|
||||
SWEEP,
|
||||
HOOK,
|
||||
S_CURVE,
|
||||
COIL,
|
||||
WANDER,
|
||||
OXBOW,
|
||||
CHIRP
|
||||
private record DrainageBasin(DrainageBasinId id, double distance) {
|
||||
}
|
||||
|
||||
private record StyleParent(RiverNodeId id) {
|
||||
}
|
||||
|
||||
private record SourceTileId(long tileX, long tileZ) {
|
||||
@@ -1156,6 +1160,7 @@ public final class RiverNetwork {
|
||||
private final RiverNode from;
|
||||
private final RiverNode to;
|
||||
private final RiverRoutingContext context;
|
||||
private final RiverWorm worm;
|
||||
private final RiverTerrainSampler terrain;
|
||||
private int wetFlow;
|
||||
private int dryFlow;
|
||||
@@ -1167,12 +1172,14 @@ public final class RiverNetwork {
|
||||
RiverNode from,
|
||||
RiverNode to,
|
||||
RiverRoutingContext context,
|
||||
RiverWorm worm,
|
||||
RiverTerrainSampler terrain
|
||||
) {
|
||||
this.id = id;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.context = context;
|
||||
this.worm = worm;
|
||||
this.terrain = terrain;
|
||||
}
|
||||
|
||||
@@ -1193,21 +1200,7 @@ public final class RiverNetwork {
|
||||
private RiverReach build() {
|
||||
int flow = wetFlow + dryFlow;
|
||||
int order = 1 + (31 - Integer.numberOfLeadingZeros(flow));
|
||||
double bankWidth = nonNegativeOrFallback(
|
||||
terrain.bankWidth(context, options.bankWidth()),
|
||||
options.bankWidth()
|
||||
);
|
||||
double baseDepth = positiveOrFallback(
|
||||
terrain.depth(context, options.depth()),
|
||||
options.depth()
|
||||
);
|
||||
RiverWidthProfile widthProfile = widthProfile(order);
|
||||
double width = widthProfile.maximum();
|
||||
bankWidth = StrictMath.min(options.maxBankWidth(), bankWidth);
|
||||
double depth = StrictMath.min(
|
||||
options.maxDepth(),
|
||||
baseDepth * (1.0 + options.orderDepthFactor() * (order - 1))
|
||||
);
|
||||
RiverBodyProfile bodyProfile = bodyProfile(order, worm);
|
||||
RiverRouteState state = wetFlow > 0 ? RiverRouteState.WET : RiverRouteState.DRY;
|
||||
return new RiverReach(
|
||||
id,
|
||||
@@ -1216,10 +1209,10 @@ public final class RiverNetwork {
|
||||
state,
|
||||
flow,
|
||||
order,
|
||||
width,
|
||||
widthProfile,
|
||||
bankWidth,
|
||||
depth,
|
||||
bodyProfile.maximumWidth(),
|
||||
bodyProfile.maximumBankWidth(),
|
||||
bodyProfile.maximumDepth(),
|
||||
bodyProfile,
|
||||
state == RiverRouteState.WET && to.ocean(),
|
||||
state == RiverRouteState.WET
|
||||
? terminalWetFlow == wetFlow
|
||||
@@ -1228,12 +1221,21 @@ public final class RiverNetwork {
|
||||
);
|
||||
}
|
||||
|
||||
private RiverWidthProfile widthProfile(int order) {
|
||||
private RiverBodyProfile bodyProfile(int order, RiverWorm worm) {
|
||||
RiverPolyline polyline = context.polyline();
|
||||
int sampleCount = Math.min(WIDTH_PROFILE_SAMPLES, Math.max(2, polyline.size()));
|
||||
double minimumWavelength = StrictMath.min(worm.bodyWavelength(), worm.bodyDetailWavelength());
|
||||
int resolvedSamples = 1 + (int) StrictMath.ceil(polyline.length() * 2D / minimumWavelength);
|
||||
int sampleCount = StrictMath.max(
|
||||
MINIMUM_BODY_PROFILE_SAMPLES,
|
||||
StrictMath.min(MAXIMUM_BODY_PROFILE_SAMPLES, resolvedSamples)
|
||||
);
|
||||
double[] positions = new double[sampleCount];
|
||||
double[] widths = new double[sampleCount];
|
||||
double orderScale = 1D + options.orderWidthFactor() * (order - 1);
|
||||
double[] bankWidths = new double[sampleCount];
|
||||
double[] depths = new double[sampleCount];
|
||||
double[] roofScales = new double[sampleCount];
|
||||
double widthOrderScale = 1D + options.orderWidthFactor() * (order - 1);
|
||||
double depthOrderScale = 1D + options.orderDepthFactor() * (order - 1);
|
||||
for (int index = 0; index < sampleCount; index++) {
|
||||
double alongReach = (double) index / (sampleCount - 1);
|
||||
ReachPosition position = positionAt(polyline, alongReach);
|
||||
@@ -1249,10 +1251,59 @@ public final class RiverNetwork {
|
||||
positions[index] = alongReach;
|
||||
widths[index] = StrictMath.min(
|
||||
options.maxChannelWidth(),
|
||||
StrictMath.max(1D, baseWidth * orderScale)
|
||||
StrictMath.max(
|
||||
1D,
|
||||
baseWidth
|
||||
* widthOrderScale
|
||||
* worm.widthMultiplier()
|
||||
* bodyMultiplier(
|
||||
position,
|
||||
worm,
|
||||
BODY_WIDTH_PRIMARY_SALT,
|
||||
BODY_WIDTH_DETAIL_SALT,
|
||||
worm.widthVariation()
|
||||
)
|
||||
)
|
||||
);
|
||||
double baseBankWidth = nonNegativeOrFallback(
|
||||
terrain.bankWidth(context, position.x(), position.z(), options.bankWidth()),
|
||||
options.bankWidth()
|
||||
);
|
||||
bankWidths[index] = StrictMath.min(
|
||||
options.maxBankWidth(),
|
||||
baseBankWidth
|
||||
* worm.bankMultiplier()
|
||||
* bodyMultiplier(
|
||||
position,
|
||||
worm,
|
||||
BODY_BANK_PRIMARY_SALT,
|
||||
BODY_BANK_DETAIL_SALT,
|
||||
worm.bankVariation()
|
||||
)
|
||||
);
|
||||
double baseDepth = positiveOrFallback(
|
||||
terrain.depth(context, position.x(), position.z(), options.depth()),
|
||||
options.depth()
|
||||
);
|
||||
depths[index] = StrictMath.min(
|
||||
options.maxDepth(),
|
||||
StrictMath.max(
|
||||
1D,
|
||||
baseDepth
|
||||
* depthOrderScale
|
||||
* worm.depthMultiplier()
|
||||
* bodyMultiplier(
|
||||
position,
|
||||
worm,
|
||||
BODY_DEPTH_PRIMARY_SALT,
|
||||
BODY_DEPTH_DETAIL_SALT,
|
||||
worm.depthVariation()
|
||||
)
|
||||
)
|
||||
);
|
||||
roofScales[index] = roofScale(position, worm);
|
||||
}
|
||||
return new RiverWidthProfile(positions, widths);
|
||||
return new RiverBodyProfile(positions, widths, bankWidths, depths, roofScales);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public record RiverNetworkOptions(
|
||||
long seed,
|
||||
int cellSize,
|
||||
@@ -21,8 +25,6 @@ public record RiverNetworkOptions(
|
||||
double routingNoiseWeight,
|
||||
double flowAlignmentWeight,
|
||||
double confluenceWeight,
|
||||
int branchSoftCap,
|
||||
double branchChildShrinkFactor,
|
||||
double oceanAttraction,
|
||||
double channelWidth,
|
||||
double bankWidth,
|
||||
@@ -33,8 +35,7 @@ public record RiverNetworkOptions(
|
||||
double orderWidthFactor,
|
||||
double orderDepthFactor,
|
||||
double maximumReachRadius,
|
||||
double meanderStrength,
|
||||
int meanderSubdivisions
|
||||
List<RiverWorm> worms
|
||||
) {
|
||||
public RiverNetworkOptions {
|
||||
requireRange(cellSize, 8, 4096, "cellSize");
|
||||
@@ -47,7 +48,6 @@ public record RiverNetworkOptions(
|
||||
requireRange(routingDeviationStrengthCells, 0D, 32D, "routingDeviationStrengthCells");
|
||||
requirePositive(routingPlateauHeight, "routingPlateauHeight");
|
||||
requireFinite(hydraulicBaseHeight, "hydraulicBaseHeight");
|
||||
requireRange(meanderSubdivisions, 1, 64, "meanderSubdivisions");
|
||||
requireProbability(siteJitter, "siteJitter");
|
||||
requireProbability(sourceChance, "sourceChance");
|
||||
requireProbability(reachChance, "reachChance");
|
||||
@@ -56,8 +56,6 @@ public record RiverNetworkOptions(
|
||||
requireFiniteNonNegative(routingNoiseWeight, "routingNoiseWeight");
|
||||
requireFiniteNonNegative(flowAlignmentWeight, "flowAlignmentWeight");
|
||||
requireFiniteNonNegative(confluenceWeight, "confluenceWeight");
|
||||
requireRange(branchSoftCap, 1, 8, "branchSoftCap");
|
||||
requireProbability(branchChildShrinkFactor, "branchChildShrinkFactor");
|
||||
requireFiniteNonNegative(oceanAttraction, "oceanAttraction");
|
||||
requirePositive(channelWidth, "channelWidth");
|
||||
requireFiniteNonNegative(bankWidth, "bankWidth");
|
||||
@@ -68,15 +66,27 @@ public record RiverNetworkOptions(
|
||||
requireFiniteNonNegative(orderWidthFactor, "orderWidthFactor");
|
||||
requireFiniteNonNegative(orderDepthFactor, "orderDepthFactor");
|
||||
requireFiniteNonNegative(maximumReachRadius, "maximumReachRadius");
|
||||
requireFiniteNonNegative(meanderStrength, "meanderStrength");
|
||||
if (worms == null || worms.isEmpty()) {
|
||||
throw new IllegalArgumentException("worms must contain at least one profile");
|
||||
}
|
||||
if (worms.size() > 16) {
|
||||
throw new IllegalArgumentException("worms must contain at most 16 root profiles");
|
||||
}
|
||||
Set<String> ids = new HashSet<String>();
|
||||
Set<Long> seeds = new HashSet<Long>();
|
||||
int wormCount = validateWormTree(worms, 1, ids, seeds);
|
||||
if (wormCount > 128) {
|
||||
throw new IllegalArgumentException("worm hierarchy must contain at most 128 profiles");
|
||||
}
|
||||
worms = List.copyOf(worms);
|
||||
RiverTopologyComplexity.requireSafe(
|
||||
cellSize,
|
||||
tileCells,
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
maximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
maximumWormOffset(worms),
|
||||
maximumWormSegments(worms)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,6 +94,66 @@ public record RiverNetworkOptions(
|
||||
return new Builder(seed);
|
||||
}
|
||||
|
||||
public double maximumWormOffset() {
|
||||
return maximumWormOffset(worms);
|
||||
}
|
||||
|
||||
public int maximumWormSegments() {
|
||||
return maximumWormSegments(worms);
|
||||
}
|
||||
|
||||
private static double maximumWormOffset(List<RiverWorm> worms) {
|
||||
double maximum = 0D;
|
||||
for (RiverWorm worm : worms) {
|
||||
if (worm == null) {
|
||||
throw new IllegalArgumentException("worms must not contain null profiles");
|
||||
}
|
||||
maximum = StrictMath.max(maximum, worm.maxOffset());
|
||||
maximum = StrictMath.max(maximum, maximumWormOffset(worm.children()));
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
private static int maximumWormSegments(List<RiverWorm> worms) {
|
||||
int maximum = 1;
|
||||
for (RiverWorm worm : worms) {
|
||||
if (worm == null) {
|
||||
throw new IllegalArgumentException("worms must not contain null profiles");
|
||||
}
|
||||
maximum = StrictMath.max(maximum, worm.segments());
|
||||
maximum = StrictMath.max(maximum, maximumWormSegments(worm.children()));
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
private static int validateWormTree(
|
||||
List<RiverWorm> worms,
|
||||
int depth,
|
||||
Set<String> ids,
|
||||
Set<Long> seeds
|
||||
) {
|
||||
if (depth > 4) {
|
||||
throw new IllegalArgumentException("worm hierarchy must be at most 4 profiles deep");
|
||||
}
|
||||
int count = 0;
|
||||
for (RiverWorm worm : worms) {
|
||||
if (worm == null) {
|
||||
throw new IllegalArgumentException("worm hierarchy must not contain null profiles");
|
||||
}
|
||||
if (!ids.add(worm.id())) {
|
||||
throw new IllegalArgumentException("worm ids must be unique: " + worm.id());
|
||||
}
|
||||
if (!seeds.add(worm.seed())) {
|
||||
throw new IllegalArgumentException("worm seeds must be unique: " + worm.seed());
|
||||
}
|
||||
count++;
|
||||
if (!worm.children().isEmpty()) {
|
||||
count += validateWormTree(worm.children(), depth + 1, ids, seeds);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void requireRange(int value, int minimum, int maximum, String name) {
|
||||
if (value < minimum || value > maximum) {
|
||||
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
|
||||
@@ -141,8 +211,6 @@ public record RiverNetworkOptions(
|
||||
private double routingNoiseWeight;
|
||||
private double flowAlignmentWeight;
|
||||
private double confluenceWeight;
|
||||
private int branchSoftCap;
|
||||
private double branchChildShrinkFactor;
|
||||
private double oceanAttraction;
|
||||
private double channelWidth;
|
||||
private double bankWidth;
|
||||
@@ -153,8 +221,7 @@ public record RiverNetworkOptions(
|
||||
private double orderWidthFactor;
|
||||
private double orderDepthFactor;
|
||||
private double maximumReachRadius;
|
||||
private double meanderStrength;
|
||||
private int meanderSubdivisions;
|
||||
private List<RiverWorm> worms;
|
||||
|
||||
private Builder(long seed) {
|
||||
this.seed = seed;
|
||||
@@ -177,8 +244,6 @@ public record RiverNetworkOptions(
|
||||
routingNoiseWeight = 24.0;
|
||||
flowAlignmentWeight = 0D;
|
||||
confluenceWeight = 0D;
|
||||
branchSoftCap = 4;
|
||||
branchChildShrinkFactor = 0.35D;
|
||||
oceanAttraction = 64.0;
|
||||
channelWidth = 10.0;
|
||||
bankWidth = 8.0;
|
||||
@@ -189,8 +254,32 @@ public record RiverNetworkOptions(
|
||||
orderWidthFactor = 0.35;
|
||||
orderDepthFactor = 0.2;
|
||||
maximumReachRadius = Double.NaN;
|
||||
meanderStrength = 40.0;
|
||||
meanderSubdivisions = 8;
|
||||
worms = List.of(new RiverWorm(
|
||||
"default",
|
||||
1L,
|
||||
1D,
|
||||
1024D,
|
||||
256D,
|
||||
0.5D,
|
||||
0.15D,
|
||||
40D,
|
||||
8,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
512D,
|
||||
128D,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
4,
|
||||
0.35D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
|
||||
public Builder cellSize(int value) {
|
||||
@@ -288,16 +377,6 @@ public record RiverNetworkOptions(
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder branchSoftCap(int value) {
|
||||
branchSoftCap = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder branchChildShrinkFactor(double value) {
|
||||
branchChildShrinkFactor = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder oceanAttraction(double value) {
|
||||
oceanAttraction = value;
|
||||
return this;
|
||||
@@ -348,13 +427,8 @@ public record RiverNetworkOptions(
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder meanderStrength(double value) {
|
||||
meanderStrength = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder meanderSubdivisions(int value) {
|
||||
meanderSubdivisions = value;
|
||||
public Builder worms(List<RiverWorm> value) {
|
||||
worms = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -383,8 +457,6 @@ public record RiverNetworkOptions(
|
||||
routingNoiseWeight,
|
||||
flowAlignmentWeight,
|
||||
confluenceWeight,
|
||||
branchSoftCap,
|
||||
branchChildShrinkFactor,
|
||||
oceanAttraction,
|
||||
channelWidth,
|
||||
bankWidth,
|
||||
@@ -395,8 +467,7 @@ public record RiverNetworkOptions(
|
||||
orderWidthFactor,
|
||||
orderDepthFactor,
|
||||
resolvedMaximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
worms
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ public record RiverReach(
|
||||
int flow,
|
||||
int order,
|
||||
double width,
|
||||
RiverWidthProfile widthProfile,
|
||||
double bankWidth,
|
||||
double depth,
|
||||
RiverBodyProfile bodyProfile,
|
||||
boolean mouth,
|
||||
boolean terminal,
|
||||
RiverPolyline polyline
|
||||
@@ -22,7 +22,7 @@ public record RiverReach(
|
||||
Objects.requireNonNull(from);
|
||||
Objects.requireNonNull(to);
|
||||
Objects.requireNonNull(state);
|
||||
Objects.requireNonNull(widthProfile);
|
||||
Objects.requireNonNull(bodyProfile);
|
||||
Objects.requireNonNull(polyline);
|
||||
if (state == RiverRouteState.SUPPRESSED) {
|
||||
throw new IllegalArgumentException("Suppressed routes cannot produce reaches");
|
||||
@@ -34,12 +34,26 @@ public record RiverReach(
|
||||
|| !Double.isFinite(depth) || depth <= 0.0) {
|
||||
throw new IllegalArgumentException("River reach dimensions must be finite and valid");
|
||||
}
|
||||
if (Double.compare(width, widthProfile.maximum()) != 0) {
|
||||
throw new IllegalArgumentException("River reach width must equal its profile maximum");
|
||||
if (Double.compare(width, bodyProfile.maximumWidth()) != 0
|
||||
|| Double.compare(bankWidth, bodyProfile.maximumBankWidth()) != 0
|
||||
|| Double.compare(depth, bodyProfile.maximumDepth()) != 0) {
|
||||
throw new IllegalArgumentException("River reach dimensions must equal their body-profile maxima");
|
||||
}
|
||||
}
|
||||
|
||||
public double widthAt(double alongReach) {
|
||||
return widthProfile.sample(alongReach);
|
||||
return bodyProfile.width(alongReach);
|
||||
}
|
||||
|
||||
public double bankWidthAt(double alongReach) {
|
||||
return bodyProfile.bankWidth(alongReach);
|
||||
}
|
||||
|
||||
public double depthAt(double alongReach) {
|
||||
return bodyProfile.depth(alongReach);
|
||||
}
|
||||
|
||||
public double roofScaleAt(double alongReach) {
|
||||
return bodyProfile.roofScale(alongReach);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,6 @@ public interface RiverTerrainSampler {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
default double meanderNoise(RiverMeanderContext context) {
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
default double flowNoise(double x, double z) {
|
||||
return Double.NaN;
|
||||
}
|
||||
@@ -75,10 +71,28 @@ public interface RiverTerrainSampler {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
default double bankWidth(
|
||||
RiverRoutingContext context,
|
||||
double x,
|
||||
double z,
|
||||
double fallback
|
||||
) {
|
||||
return bankWidth(context, fallback);
|
||||
}
|
||||
|
||||
default double depth(RiverRoutingContext context, double fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
default double depth(
|
||||
RiverRoutingContext context,
|
||||
double x,
|
||||
double z,
|
||||
double fallback
|
||||
) {
|
||||
return depth(context, fallback);
|
||||
}
|
||||
|
||||
default RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
||||
return RiverTerminalPolicy.INHERIT;
|
||||
}
|
||||
|
||||
@@ -210,9 +210,11 @@ public final class RiverTile {
|
||||
|
||||
double distance = StrictMath.sqrt(nearestDistanceSquared);
|
||||
double localWidth = nearestReach.widthAt(nearestAlongReach);
|
||||
double localBankWidth = nearestReach.bankWidthAt(nearestAlongReach);
|
||||
double localDepth = nearestReach.depthAt(nearestAlongReach);
|
||||
double channelRadius = localWidth * 0.5;
|
||||
RiverSection section = section(nearestReach, distance, channelRadius);
|
||||
double carveWeight = carveWeight(distance, channelRadius, nearestReach.bankWidth());
|
||||
double carveWeight = carveWeight(distance, channelRadius, localBankWidth);
|
||||
return new RiverSample(
|
||||
true,
|
||||
nearestReach.state(),
|
||||
@@ -223,8 +225,8 @@ public final class RiverTile {
|
||||
nearestReach.flow(),
|
||||
nearestReach.order(),
|
||||
localWidth,
|
||||
nearestReach.bankWidth(),
|
||||
nearestReach.depth(),
|
||||
localBankWidth,
|
||||
localDepth,
|
||||
nearestReach.terminal(),
|
||||
nearestReach.id()
|
||||
);
|
||||
@@ -325,7 +327,7 @@ public final class RiverTile {
|
||||
RiverPolyline polyline = reach.polyline();
|
||||
if (polyline.length() == 0D) {
|
||||
double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0));
|
||||
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidth() + additionalRadius;
|
||||
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D) + additionalRadius;
|
||||
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
|
||||
}
|
||||
double nearest = Double.POSITIVE_INFINITY;
|
||||
@@ -339,9 +341,9 @@ public final class RiverTile {
|
||||
}
|
||||
double deltaX = polyline.x(point + 1) - polyline.x(point);
|
||||
double deltaZ = polyline.z(point + 1) - polyline.z(point);
|
||||
for (int profileIndex = 0; profileIndex < reach.widthProfile().size() - 1; profileIndex++) {
|
||||
double profileStart = reach.widthProfile().position(profileIndex);
|
||||
double profileEnd = reach.widthProfile().position(profileIndex + 1);
|
||||
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
|
||||
double profileStart = reach.bodyProfile().position(profileIndex);
|
||||
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
|
||||
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
||||
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
||||
if (overlapStart > overlapEnd) {
|
||||
@@ -349,13 +351,16 @@ public final class RiverTile {
|
||||
}
|
||||
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
||||
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
||||
double widthSlope = (reach.widthProfile().width(profileIndex + 1)
|
||||
- reach.widthProfile().width(profileIndex)) / (profileEnd - profileStart);
|
||||
double radiusBase = (reach.widthProfile().width(profileIndex)
|
||||
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
|
||||
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
|
||||
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
|
||||
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
|
||||
+ reach.bankWidth()
|
||||
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
|
||||
+ bankSlope * (segmentStartAlong - profileStart)
|
||||
+ additionalRadius;
|
||||
double radiusSlope = widthSlope * segmentAlongSpan * 0.5D;
|
||||
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
|
||||
ClosestPoint candidate = coveringPoint(
|
||||
intervalStart,
|
||||
intervalEnd,
|
||||
@@ -394,7 +399,7 @@ public final class RiverTile {
|
||||
maximumX,
|
||||
maximumZ
|
||||
);
|
||||
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidth();
|
||||
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D);
|
||||
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
|
||||
}
|
||||
double nearest = Double.POSITIVE_INFINITY;
|
||||
@@ -410,9 +415,9 @@ public final class RiverTile {
|
||||
double startZ = polyline.z(point);
|
||||
double deltaX = polyline.x(point + 1) - startX;
|
||||
double deltaZ = polyline.z(point + 1) - startZ;
|
||||
for (int profileIndex = 0; profileIndex < reach.widthProfile().size() - 1; profileIndex++) {
|
||||
double profileStart = reach.widthProfile().position(profileIndex);
|
||||
double profileEnd = reach.widthProfile().position(profileIndex + 1);
|
||||
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
|
||||
double profileStart = reach.bodyProfile().position(profileIndex);
|
||||
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
|
||||
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
||||
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
||||
if (overlapStart > overlapEnd) {
|
||||
@@ -420,11 +425,15 @@ public final class RiverTile {
|
||||
}
|
||||
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
||||
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
||||
double widthSlope = (reach.widthProfile().width(profileIndex + 1)
|
||||
- reach.widthProfile().width(profileIndex)) / (profileEnd - profileStart);
|
||||
double radiusBase = (reach.widthProfile().width(profileIndex)
|
||||
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D + reach.bankWidth();
|
||||
double radiusSlope = widthSlope * segmentAlongSpan * 0.5D;
|
||||
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
|
||||
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
|
||||
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
|
||||
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
|
||||
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
|
||||
+ bankSlope * (segmentStartAlong - profileStart);
|
||||
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
|
||||
double cursor = intervalStart;
|
||||
do {
|
||||
double next = intervalEnd;
|
||||
|
||||
@@ -19,14 +19,12 @@ public final class RiverTopologyComplexity {
|
||||
double siteJitter,
|
||||
int maxRouteReaches,
|
||||
double maximumReachRadius,
|
||||
double meanderStrength,
|
||||
int meanderSubdivisions
|
||||
double maximumWormOffset,
|
||||
int maximumWormSegments
|
||||
) {
|
||||
double maximumEdgeAxisDelta = cellSize * (1D + siteJitter);
|
||||
double maximumEdgeLength = StrictMath.sqrt(2D) * maximumEdgeAxisDelta;
|
||||
double maximumMeander = StrictMath.min(meanderStrength, maximumEdgeLength * 0.35D);
|
||||
long geometryPaddingCells = 1L + ceilToLong(
|
||||
(maximumReachRadius + maximumMeander) / cellSize
|
||||
(maximumReachRadius + maximumWormOffset) / cellSize
|
||||
);
|
||||
long targetWindowAxis = saturatedAdd(tileCells, saturatedMultiply(2L, geometryPaddingCells));
|
||||
long sourceWindowAxis = saturatedAdd(
|
||||
@@ -36,7 +34,7 @@ public final class RiverTopologyComplexity {
|
||||
long sourceWindowCells = saturatedMultiply(sourceWindowAxis, sourceWindowAxis);
|
||||
long maximumRouteScanSteps = saturatedMultiply(sourceWindowCells, maxRouteReaches);
|
||||
double maximumSegmentSpan = maximumEdgeAxisDelta
|
||||
+ maximumMeander * 2D
|
||||
+ maximumWormOffset * 2D
|
||||
+ maximumReachRadius * 2D;
|
||||
long maximumSegmentBucketAxis = saturatedAdd(
|
||||
ceilToLong(maximumSegmentSpan / SPATIAL_BUCKET_SIZE),
|
||||
@@ -48,7 +46,7 @@ public final class RiverTopologyComplexity {
|
||||
);
|
||||
long maximumBucketWritesPerReach = saturatedMultiply(
|
||||
maximumSegmentBucketCount,
|
||||
meanderSubdivisions
|
||||
maximumWormSegments
|
||||
);
|
||||
return new Estimate(
|
||||
geometryPaddingCells,
|
||||
@@ -66,8 +64,8 @@ public final class RiverTopologyComplexity {
|
||||
double siteJitter,
|
||||
int maxRouteReaches,
|
||||
double maximumReachRadius,
|
||||
double meanderStrength,
|
||||
int meanderSubdivisions
|
||||
double maximumWormOffset,
|
||||
int maximumWormSegments
|
||||
) {
|
||||
Estimate estimate = estimate(
|
||||
cellSize,
|
||||
@@ -75,8 +73,8 @@ public final class RiverTopologyComplexity {
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
maximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
maximumWormOffset,
|
||||
maximumWormSegments
|
||||
);
|
||||
List<String> violations = estimate.violations();
|
||||
if (!violations.isEmpty()) {
|
||||
@@ -205,7 +203,7 @@ public final class RiverTopologyComplexity {
|
||||
+ " bucket writes for one reach (" + maximumSegmentBucketAxis
|
||||
+ " buckets per segment axis), above the safe limit of "
|
||||
+ MAXIMUM_BUCKET_WRITES_PER_REACH
|
||||
+ "; reduce channel width, bank width, orderWidthFactor, meanderStrength, or meanderSubdivisions.");
|
||||
+ "; reduce channel width, bank width, orderWidthFactor, worm maxOffset, or worm segments.");
|
||||
}
|
||||
return List.copyOf(violations);
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class RiverWidthProfile {
|
||||
private final double[] positions;
|
||||
private final double[] widths;
|
||||
private final double maximum;
|
||||
|
||||
public RiverWidthProfile(double[] positions, double[] widths) {
|
||||
if (positions == null || widths == null || positions.length < 2 || positions.length != widths.length) {
|
||||
throw new IllegalArgumentException("River width profiles require matching position and width samples");
|
||||
}
|
||||
this.positions = positions.clone();
|
||||
this.widths = widths.clone();
|
||||
double resolvedMaximum = 0D;
|
||||
for (int index = 0; index < this.positions.length; index++) {
|
||||
double position = this.positions[index];
|
||||
double width = this.widths[index];
|
||||
if (!Double.isFinite(position) || position < 0D || position > 1D
|
||||
|| (index > 0 && position <= this.positions[index - 1])) {
|
||||
throw new IllegalArgumentException("River width profile positions must increase from zero to one");
|
||||
}
|
||||
if (!Double.isFinite(width) || width <= 0D) {
|
||||
throw new IllegalArgumentException("River width profile widths must be finite and positive");
|
||||
}
|
||||
resolvedMaximum = Math.max(resolvedMaximum, width);
|
||||
}
|
||||
if (this.positions[0] != 0D || this.positions[this.positions.length - 1] != 1D) {
|
||||
throw new IllegalArgumentException("River width profile positions must include zero and one");
|
||||
}
|
||||
maximum = resolvedMaximum;
|
||||
}
|
||||
|
||||
public static RiverWidthProfile constant(double width) {
|
||||
return new RiverWidthProfile(new double[]{0D, 1D}, new double[]{width, width});
|
||||
}
|
||||
|
||||
public double sample(double alongReach) {
|
||||
double position = Math.max(0D, Math.min(1D, alongReach));
|
||||
int index = Arrays.binarySearch(positions, position);
|
||||
if (index >= 0) {
|
||||
return widths[index];
|
||||
}
|
||||
int upper = -index - 1;
|
||||
if (upper <= 0) {
|
||||
return widths[0];
|
||||
}
|
||||
if (upper >= positions.length) {
|
||||
return widths[widths.length - 1];
|
||||
}
|
||||
int lower = upper - 1;
|
||||
double range = positions[upper] - positions[lower];
|
||||
double interpolation = range <= 0D ? 0D : (position - positions[lower]) / range;
|
||||
return widths[lower] + (widths[upper] - widths[lower]) * interpolation;
|
||||
}
|
||||
|
||||
public double maximum() {
|
||||
return maximum;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return widths.length;
|
||||
}
|
||||
|
||||
public double position(int index) {
|
||||
return positions[index];
|
||||
}
|
||||
|
||||
public double width(int index) {
|
||||
return widths[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (!(object instanceof RiverWidthProfile profile)) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.equals(positions, profile.positions) && Arrays.equals(widths, profile.widths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * Arrays.hashCode(positions) + Arrays.hashCode(widths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record RiverWorm(
|
||||
String id,
|
||||
long seed,
|
||||
double weight,
|
||||
double wavelength,
|
||||
double detailWavelength,
|
||||
double tortuosity,
|
||||
double detailTortuosity,
|
||||
double maxOffset,
|
||||
int segments,
|
||||
double widthMultiplier,
|
||||
double bankMultiplier,
|
||||
double depthMultiplier,
|
||||
double bodyWavelength,
|
||||
double bodyDetailWavelength,
|
||||
double widthVariation,
|
||||
double bankVariation,
|
||||
double depthVariation,
|
||||
double roofVariation,
|
||||
int branchCap,
|
||||
double branchDecay,
|
||||
double confluenceMultiplier,
|
||||
double childChance,
|
||||
double branchChildChance,
|
||||
List<RiverWorm> children
|
||||
) {
|
||||
public RiverWorm {
|
||||
if (id == null || !id.matches("[a-z0-9][a-z0-9_-]{0,63}")) {
|
||||
throw new IllegalArgumentException("id must use 1 to 64 lowercase letters, digits, underscores, or hyphens");
|
||||
}
|
||||
requireRange(weight, 0.000001D, 1000000D, "weight");
|
||||
requireRange(wavelength, 8D, 16384D, "wavelength");
|
||||
requireRange(detailWavelength, 8D, 16384D, "detailWavelength");
|
||||
requireRange(tortuosity, 0D, 1D, "tortuosity");
|
||||
requireRange(detailTortuosity, 0D, 1D, "detailTortuosity");
|
||||
requireRange(maxOffset, 0D, 1024D, "maxOffset");
|
||||
if (segments < 1 || segments > 64) {
|
||||
throw new IllegalArgumentException("segments must be between 1 and 64");
|
||||
}
|
||||
requireRange(widthMultiplier, 0.125D, 8D, "widthMultiplier");
|
||||
requireRange(bankMultiplier, 0.125D, 8D, "bankMultiplier");
|
||||
requireRange(depthMultiplier, 0.125D, 8D, "depthMultiplier");
|
||||
requireRange(bodyWavelength, 32D, 16384D, "bodyWavelength");
|
||||
requireRange(bodyDetailWavelength, 32D, 16384D, "bodyDetailWavelength");
|
||||
requireRange(widthVariation, 0D, 0.875D, "widthVariation");
|
||||
requireRange(bankVariation, 0D, 0.875D, "bankVariation");
|
||||
requireRange(depthVariation, 0D, 0.875D, "depthVariation");
|
||||
requireRange(roofVariation, 0D, 0.875D, "roofVariation");
|
||||
if (branchCap < 1 || branchCap > 8) {
|
||||
throw new IllegalArgumentException("branchCap must be between 1 and 8");
|
||||
}
|
||||
requireRange(branchDecay, 0D, 1D, "branchDecay");
|
||||
requireRange(confluenceMultiplier, 0D, 8D, "confluenceMultiplier");
|
||||
requireRange(childChance, 0D, 1D, "childChance");
|
||||
requireRange(branchChildChance, 0D, 1D, "branchChildChance");
|
||||
if (children == null) {
|
||||
throw new IllegalArgumentException("children must not be null");
|
||||
}
|
||||
if (children.size() > 16) {
|
||||
throw new IllegalArgumentException("children must contain at most 16 profiles");
|
||||
}
|
||||
for (RiverWorm child : children) {
|
||||
if (child == null) {
|
||||
throw new IllegalArgumentException("children must not contain null profiles");
|
||||
}
|
||||
}
|
||||
children = List.copyOf(children);
|
||||
}
|
||||
|
||||
private static void requireRange(double value, double minimum, double maximum, String name) {
|
||||
if (!Double.isFinite(value) || value < minimum || value > maximum) {
|
||||
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,11 @@ import art.arcane.iris.engine.object.IrisRiverTerrain;
|
||||
import art.arcane.iris.engine.object.IrisRiverTopology;
|
||||
import art.arcane.iris.engine.object.IrisRiverWater;
|
||||
import art.arcane.iris.engine.object.IrisRiverWaterMode;
|
||||
import art.arcane.iris.engine.object.IrisRiverWorm;
|
||||
import art.arcane.iris.engine.object.IrisStyledRange;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.river.RiverAnchor;
|
||||
import art.arcane.iris.engine.river.RiverEdgeId;
|
||||
import art.arcane.iris.engine.river.RiverMeanderContext;
|
||||
import art.arcane.iris.engine.river.RiverNetworkOptions;
|
||||
import art.arcane.iris.engine.river.RiverPolyline;
|
||||
import art.arcane.iris.engine.river.RiverReach;
|
||||
@@ -35,6 +35,7 @@ import art.arcane.iris.engine.river.RiverTerminalPolicy;
|
||||
import art.arcane.iris.engine.river.RiverTile;
|
||||
import art.arcane.iris.engine.river.RiverTileCache;
|
||||
import art.arcane.iris.engine.river.RiverTopologyComplexity;
|
||||
import art.arcane.iris.engine.river.RiverWorm;
|
||||
import art.arcane.iris.util.project.interpolation.NoiseBounds;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
@@ -61,7 +62,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
private static final long WIDTH_NOISE_SALT = 0xBE5466CF34E90C6CL;
|
||||
private static final long BANK_NOISE_SALT = 0xC0AC29B7C97C50DDL;
|
||||
private static final long DEPTH_NOISE_SALT = 0x3F84D5B5B5470917L;
|
||||
private static final long MEANDER_NOISE_SALT = 0x9216D5D98979FB1BL;
|
||||
private static final long BED_NOISE_SALT = 0xD1310BA698DFB5ACL;
|
||||
private static final long BIOME_NOISE_SALT = 0x2FFD72DBD01ADFB7L;
|
||||
private static final long CAVE_ENTRY_NOISE_SALT = 0xB8E1AFED6A267E96L;
|
||||
@@ -98,7 +98,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
private final CNG widthNoise;
|
||||
private final CNG bankNoise;
|
||||
private final CNG depthNoise;
|
||||
private final CNG meanderNoise;
|
||||
private final CNG bedNoise;
|
||||
private final CNG biomeNoise;
|
||||
private final CNG caveEntryNoise;
|
||||
@@ -144,7 +143,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
widthNoise = noise(terrain.getChannelWidth(), WIDTH_NOISE_SALT);
|
||||
bankNoise = noise(terrain.getBankWidth(), BANK_NOISE_SALT);
|
||||
depthNoise = noise(terrain.getDepth(), DEPTH_NOISE_SALT);
|
||||
meanderNoise = noise(terrain.getMeanderStyle(), MEANDER_NOISE_SALT);
|
||||
bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT);
|
||||
biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT);
|
||||
caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_NOISE_SALT);
|
||||
@@ -256,7 +254,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
z
|
||||
);
|
||||
int ceilingY = shapedTunnelCeilingY(
|
||||
waterHeadY, caves.getDryHeadroom(), profile, roofOffset);
|
||||
waterHeadY,
|
||||
caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach()),
|
||||
profile,
|
||||
roofOffset
|
||||
);
|
||||
return new IrisRiverTunnelSample(column.river(), bedY, waterHeadY, ceilingY);
|
||||
}
|
||||
|
||||
@@ -349,7 +351,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
IrisBiome sampledBiome = naturalBiome.get(center.x(), center.z());
|
||||
double head = waterSurface(reach, alongReach, isNaturalOcean(sampledBiome));
|
||||
double centerNaturalHeight = naturalHeight.get(center.x(), center.z());
|
||||
double centerBedHeight = head - reach.depth() + bedRoughness(center.x(), center.z());
|
||||
double centerBedHeight = head - reach.depthAt(alongReach) + bedRoughness(center.x(), center.z());
|
||||
EffectiveRiverSettings settings = settingsFor(sampledRegion, sampledBiome);
|
||||
double maximumIncision = Math.max(0D, terrain.getMaxIncision() * settings.maxIncisionMultiplier());
|
||||
double cappedSurface = incisedHeight(centerNaturalHeight, centerBedHeight, 1D, maximumIncision);
|
||||
@@ -627,8 +629,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
.routingNoiseWeight(0D)
|
||||
.flowAlignmentWeight(topology.getFlowAlignmentWeight())
|
||||
.confluenceWeight(topology.getConfluenceWeight())
|
||||
.branchSoftCap(topology.getBranchSoftCap())
|
||||
.branchChildShrinkFactor(topology.getBranchChildShrinkFactor())
|
||||
.oceanAttraction(topology.getOceanAttraction())
|
||||
.channelWidth(mid(riverTerrain.getChannelWidth(), 12D))
|
||||
.bankWidth(mid(riverTerrain.getBankWidth(), 8D))
|
||||
@@ -639,11 +639,63 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
.orderWidthFactor(riverTerrain.getOrderWidthFactor())
|
||||
.orderDepthFactor(riverTerrain.getOrderDepthFactor())
|
||||
.maximumReachRadius(maximumReachRadius(topology, riverTerrain))
|
||||
.meanderStrength(riverTerrain.getMeanderStrength())
|
||||
.meanderSubdivisions(riverTerrain.getMeanderSubdivisions())
|
||||
.worms(worms(riverTerrain))
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<RiverWorm> worms(IrisRiverTerrain riverTerrain) {
|
||||
if (riverTerrain.getWorms() == null || riverTerrain.getWorms().isEmpty()) {
|
||||
throw new IllegalArgumentException("River terrain must configure at least one Perlin worm");
|
||||
}
|
||||
ArrayList<RiverWorm> worms = new ArrayList<>(riverTerrain.getWorms().size());
|
||||
for (IrisRiverWorm configured : riverTerrain.getWorms()) {
|
||||
if (configured == null) {
|
||||
throw new IllegalArgumentException("River terrain worms must not contain null entries");
|
||||
}
|
||||
worms.add(worm(configured));
|
||||
}
|
||||
return List.copyOf(worms);
|
||||
}
|
||||
|
||||
private RiverWorm worm(IrisRiverWorm configured) {
|
||||
if (configured.getChildren() == null) {
|
||||
throw new IllegalArgumentException("River worm children must be an array");
|
||||
}
|
||||
ArrayList<RiverWorm> children = new ArrayList<>(configured.getChildren().size());
|
||||
for (IrisRiverWorm child : configured.getChildren()) {
|
||||
if (child == null) {
|
||||
throw new IllegalArgumentException("River worm children must not contain null entries");
|
||||
}
|
||||
children.add(worm(child));
|
||||
}
|
||||
return new RiverWorm(
|
||||
configured.getId(),
|
||||
configured.getSeed(),
|
||||
configured.getWeight(),
|
||||
configured.getWavelength(),
|
||||
configured.getDetailWavelength(),
|
||||
configured.getTortuosity(),
|
||||
configured.getDetailTortuosity(),
|
||||
configured.getMaxOffset(),
|
||||
configured.getSegments(),
|
||||
configured.getWidthMultiplier(),
|
||||
configured.getBankMultiplier(),
|
||||
configured.getDepthMultiplier(),
|
||||
configured.getBodyWavelength(),
|
||||
configured.getBodyDetailWavelength(),
|
||||
configured.getWidthVariation(),
|
||||
configured.getBankVariation(),
|
||||
configured.getDepthVariation(),
|
||||
configured.getRoofVariation(),
|
||||
configured.getBranchCap(),
|
||||
configured.getBranchDecay(),
|
||||
configured.getConfluenceMultiplier(),
|
||||
configured.getChildChance(),
|
||||
configured.getBranchChildChance(),
|
||||
List.copyOf(children)
|
||||
);
|
||||
}
|
||||
|
||||
private void addTerminalCaveAnchors(
|
||||
RiverTile tile,
|
||||
int minimumX,
|
||||
@@ -1141,11 +1193,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
return routingCost(context.midpointX(), context.midpointZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
return meanderNoise.fitDouble(-1D, 1D, context.x(), context.z());
|
||||
}
|
||||
|
||||
@Override
|
||||
public double flowNoise(double x, double z) {
|
||||
return routingNoise.fitDouble(-1D, 1D, x, z);
|
||||
@@ -1183,6 +1230,18 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
* settings.bankWidthMultiplier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double bankWidth(RiverRoutingContext context, double x, double z, double fallback) {
|
||||
EffectiveRiverSettings settings = settingsAt(x, z);
|
||||
return styled(
|
||||
terrain.getBankWidth(),
|
||||
bankNoise,
|
||||
(int) StrictMath.round(x),
|
||||
(int) StrictMath.round(z),
|
||||
fallback
|
||||
) * settings.bankWidthMultiplier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double depth(RiverRoutingContext context, double fallback) {
|
||||
EffectiveRiverSettings settings = settingsAt(context.midpointX(), context.midpointZ());
|
||||
@@ -1199,6 +1258,22 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double depth(RiverRoutingContext context, double x, double z, double fallback) {
|
||||
EffectiveRiverSettings settings = settingsAt(x, z);
|
||||
double configuredDepth = styled(
|
||||
terrain.getDepth(),
|
||||
depthNoise,
|
||||
(int) StrictMath.round(x),
|
||||
(int) StrictMath.round(z),
|
||||
fallback
|
||||
) * settings.depthMultiplier();
|
||||
return Math.min(
|
||||
terrain.getMaxDepth(),
|
||||
Math.max(1D + terrain.getBedRoughness(), configuredDepth)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
||||
EffectiveRiverSettings settings = settingsAt(blockX, blockZ);
|
||||
|
||||
@@ -50,19 +50,19 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener {
|
||||
}
|
||||
|
||||
public void l(Object l) {
|
||||
IrisLogging.info("[" + getName() + "]: " + l);
|
||||
IrisLogging.info(String.valueOf(l));
|
||||
}
|
||||
|
||||
public void w(Object l) {
|
||||
IrisLogging.warn("[" + getName() + "]: " + l);
|
||||
IrisLogging.warn(String.valueOf(l));
|
||||
}
|
||||
|
||||
public void f(Object l) {
|
||||
IrisLogging.error("[" + getName() + "]: " + l);
|
||||
IrisLogging.error(String.valueOf(l));
|
||||
}
|
||||
|
||||
public void v(Object l) {
|
||||
IrisLogging.debug("[" + getName() + "]: " + l);
|
||||
IrisLogging.debug(String.valueOf(l));
|
||||
}
|
||||
|
||||
public void onEnable() {
|
||||
|
||||
@@ -25,6 +25,7 @@ public class PackRiverValidatorTest {
|
||||
"logicalHeight": 256,
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"biomes": {
|
||||
"channel": [],
|
||||
"bank": [],
|
||||
@@ -41,6 +42,212 @@ public class PackRiverValidatorTest {
|
||||
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsRequiredValidWormProfiles() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {
|
||||
"worms": [
|
||||
{
|
||||
"id": "floodplain_trunk",
|
||||
"seed": 17,
|
||||
"weight": 2.5,
|
||||
"wavelength": 1536,
|
||||
"detailWavelength": 192,
|
||||
"tortuosity": 0.65,
|
||||
"detailTortuosity": 0.2,
|
||||
"maxOffset": 420,
|
||||
"segments": 56,
|
||||
"widthMultiplier": 1.4,
|
||||
"bankMultiplier": 1.2,
|
||||
"depthMultiplier": 0.8,
|
||||
"bodyWavelength": 1400,
|
||||
"bodyDetailWavelength": 320,
|
||||
"widthVariation": 0.65,
|
||||
"bankVariation": 0.75,
|
||||
"depthVariation": 0.45,
|
||||
"roofVariation": 0.55,
|
||||
"branchCap": 3,
|
||||
"branchDecay": 0.25,
|
||||
"confluenceMultiplier": 1.5,
|
||||
"childChance": 0.2,
|
||||
"branchChildChance": 0.6,
|
||||
"children": [
|
||||
{
|
||||
"id": "floodplain_tributary",
|
||||
"seed": 29,
|
||||
"weight": 1,
|
||||
"wavelength": 640,
|
||||
"detailWavelength": 128,
|
||||
"tortuosity": 0.8,
|
||||
"detailTortuosity": 0.3,
|
||||
"maxOffset": 300,
|
||||
"segments": 48,
|
||||
"widthMultiplier": 0.7,
|
||||
"bankMultiplier": 0.9,
|
||||
"depthMultiplier": 1.3,
|
||||
"bodyWavelength": 180,
|
||||
"bodyDetailWavelength": 48,
|
||||
"widthVariation": 0.8,
|
||||
"bankVariation": 0.8,
|
||||
"depthVariation": 0.65,
|
||||
"roofVariation": 0.75,
|
||||
"branchCap": 2,
|
||||
"branchDecay": 0.1,
|
||||
"confluenceMultiplier": 0.75,
|
||||
"childChance": 0,
|
||||
"branchChildChance": 0,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackRiverValidator.Validation result = validate(pack);
|
||||
|
||||
assertTrue(result.errors().toString(), result.errors().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsInvalidWormHierarchyIdentifiersRangesAndChildren() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {
|
||||
"worms": [
|
||||
{
|
||||
"id": "trunk",
|
||||
"seed": 17,
|
||||
"bodyWavelength": 31,
|
||||
"bodyDetailWavelength": 16385,
|
||||
"widthVariation": -0.1,
|
||||
"bankVariation": 0.876,
|
||||
"depthVariation": -0.1,
|
||||
"roofVariation": 0.876,
|
||||
"branchCap": 0,
|
||||
"branchDecay": 2,
|
||||
"confluenceMultiplier": 9,
|
||||
"childChance": -0.1,
|
||||
"branchChildChance": 1.1,
|
||||
"children": "tributary"
|
||||
},
|
||||
{"id": "trunk", "seed": 29},
|
||||
{"id": "Bad ID", "seed": 31}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackRiverValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].bodyWavelength must be at least 32");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailWavelength must be at most 16384");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].widthVariation must be at least 0");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].bankVariation must be at most 0.875");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].depthVariation must be at least 0");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].roofVariation must be at most 0.875");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].branchCap must be at least 1");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].branchDecay must be at most 1");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].confluenceMultiplier must be at most 8");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].childChance must be at least 0");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].branchChildChance must be at most 1");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[0].children must be an array");
|
||||
assertContains(result.errors(), "rivers.terrain.worms[1].id must be unique inside the worm hierarchy");
|
||||
assertContains(result.errors(),
|
||||
"rivers.terrain.worms[2].id must use 1 to 64 lowercase letters, digits, underscores, or hyphens");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsWormHierarchyDepthAndProfileLimits() throws Exception {
|
||||
File excessiveDepth = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {
|
||||
"worms": [
|
||||
{
|
||||
"id": "level_1",
|
||||
"seed": 1,
|
||||
"children": [
|
||||
{
|
||||
"id": "level_2",
|
||||
"seed": 2,
|
||||
"children": [
|
||||
{
|
||||
"id": "level_3",
|
||||
"seed": 3,
|
||||
"children": [
|
||||
{
|
||||
"id": "level_4",
|
||||
"seed": 4,
|
||||
"children": [
|
||||
{"id": "level_5", "seed": 5}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
File excessiveRoots = packWithWorms(wormHierarchy(17, 0));
|
||||
File excessiveProfiles = packWithWorms(wormHierarchy(16, 8));
|
||||
|
||||
PackRiverValidator.Validation depthResult = validate(excessiveDepth);
|
||||
PackRiverValidator.Validation rootResult = validate(excessiveRoots);
|
||||
PackRiverValidator.Validation profileResult = validate(excessiveProfiles);
|
||||
|
||||
assertContains(depthResult.errors(), "children exceeds the maximum hierarchy depth of 4");
|
||||
assertContains(rootResult.errors(), "rivers.terrain.worms must contain at most 16 root profiles");
|
||||
assertContains(profileResult.errors(), "rivers.terrain.worms hierarchy must contain at most 128 profiles");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMissingAndEmptyWormLists() throws Exception {
|
||||
File missing = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {}
|
||||
}
|
||||
}
|
||||
""");
|
||||
File empty = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": []}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackRiverValidator.Validation missingResult = validate(missing);
|
||||
PackRiverValidator.Validation emptyResult = validate(empty);
|
||||
|
||||
assertContains(missingResult.errors(),
|
||||
"rivers.terrain.worms must be an array with at least one Perlin-worm profile");
|
||||
assertContains(emptyResult.errors(),
|
||||
"rivers.terrain.worms must contain at least one Perlin-worm profile");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNullEnabledNetworkSections() throws Exception {
|
||||
File pack = pack("""
|
||||
@@ -84,11 +291,10 @@ public class PackRiverValidatorTest {
|
||||
"routingPlateauHeight": 0,
|
||||
"flowAlignmentWeight": 1025,
|
||||
"confluenceWeight": -1,
|
||||
"branchSoftCap": 9,
|
||||
"branchChildShrinkFactor": 2,
|
||||
"routingStyle": {"zoom": 0}
|
||||
},
|
||||
"terrain": {
|
||||
"worms": [{"id": "river"}],
|
||||
"channelWidth": {"min": 40, "max": 12},
|
||||
"depth": {},
|
||||
"tunnelWidthMultiplier": {"min": 0.5, "max": 9},
|
||||
@@ -116,8 +322,6 @@ public class PackRiverValidatorTest {
|
||||
assertContains(result.errors(), "rivers.topology.routingPlateauHeight must be at least 1");
|
||||
assertContains(result.errors(), "rivers.topology.flowAlignmentWeight must be at most 1024");
|
||||
assertContains(result.errors(), "rivers.topology.confluenceWeight must be at least 0");
|
||||
assertContains(result.errors(), "rivers.topology.branchSoftCap must be at most 8");
|
||||
assertContains(result.errors(), "rivers.topology.branchChildShrinkFactor must be at most 1");
|
||||
assertContains(result.errors(), "rivers.topology.routingStyle.zoom must be at least");
|
||||
assertContains(result.errors(), "rivers.terrain.channelWidth.min must not exceed");
|
||||
assertContains(result.errors(), "rivers.terrain.depth must set min and max explicitly.");
|
||||
@@ -138,6 +342,7 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"water": {
|
||||
"mode": "TERRACED",
|
||||
"maximumPoolRise": 2,
|
||||
@@ -169,8 +374,7 @@ public class PackRiverValidatorTest {
|
||||
"channelWidth": {"min": 2048, "max": 2048},
|
||||
"bankWidth": {"min": 2048, "max": 2048},
|
||||
"orderWidthFactor": 8,
|
||||
"meanderStrength": 1024,
|
||||
"meanderSubdivisions": 64
|
||||
"worms": [{"id": "river", "maxOffset": 1024, "segments": 64}]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,6 +395,7 @@ public class PackRiverValidatorTest {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {
|
||||
"worms": [{"id": "river"}],
|
||||
"maxChannelWidth": 0,
|
||||
"maxBankWidth": -1,
|
||||
"maxDepth": 513
|
||||
@@ -214,6 +419,7 @@ public class PackRiverValidatorTest {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {
|
||||
"worms": [{"id": "river"}],
|
||||
"maxChannelWidth": 2048,
|
||||
"tunnelMouthBlend": 16
|
||||
}
|
||||
@@ -234,6 +440,7 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"biomes": {
|
||||
"channel": ["biome", "missing"],
|
||||
"bank": ["biome"]
|
||||
@@ -257,6 +464,7 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"biomes": {
|
||||
"channel": ["biome"],
|
||||
"bank": ["biome"]
|
||||
@@ -292,7 +500,10 @@ public class PackRiverValidatorTest {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"rivers": {"enabled": true}
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]}
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "regions/region.json", """
|
||||
@@ -326,7 +537,10 @@ public class PackRiverValidatorTest {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"rivers": {"enabled": true}
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]}
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "regions/region.json", """
|
||||
@@ -352,6 +566,7 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"caves": {
|
||||
"mode": "GENERATE_GROTTO",
|
||||
"throatRadius": 4,
|
||||
@@ -383,6 +598,7 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"caves": {
|
||||
"mode": "FLOOD_CLOSED_COMPONENT",
|
||||
"maximumPerReach": 0,
|
||||
@@ -406,7 +622,10 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"terminalMode": "SINKHOLE_GROTTO"},
|
||||
"terrain": {
|
||||
"worms": [{"id": "river"}],
|
||||
"terminalMode": "SINKHOLE_GROTTO"
|
||||
},
|
||||
"caves": {"mode": "SEALED"}
|
||||
}
|
||||
}
|
||||
@@ -424,7 +643,10 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"terminalMode": "SINKHOLE_GROTTO"},
|
||||
"terrain": {
|
||||
"worms": [{"id": "river"}],
|
||||
"terminalMode": "SINKHOLE_GROTTO"
|
||||
},
|
||||
"caves": {
|
||||
"mode": "GENERATE_GROTTO",
|
||||
"maximumPerReach": 0
|
||||
@@ -446,7 +668,10 @@ public class PackRiverValidatorTest {
|
||||
"carvingEnabled": false,
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"terminalMode": "SINKHOLE_GROTTO"},
|
||||
"terrain": {
|
||||
"worms": [{"id": "river"}],
|
||||
"terminalMode": "SINKHOLE_GROTTO"
|
||||
},
|
||||
"caves": {"mode": "GENERATE_GROTTO"}
|
||||
}
|
||||
}
|
||||
@@ -464,7 +689,10 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"terminalMode": "SINKHOLE_GROTTO"},
|
||||
"terrain": {
|
||||
"worms": [{"id": "river"}],
|
||||
"terminalMode": "SINKHOLE_GROTTO"
|
||||
},
|
||||
"caves": {
|
||||
"mode": "FLOOD_CLOSED_COMPONENT",
|
||||
"fallback": "SEALED",
|
||||
@@ -490,6 +718,7 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"caves": {"mode": "SEALED"}
|
||||
}
|
||||
}
|
||||
@@ -514,6 +743,7 @@ public class PackRiverValidatorTest {
|
||||
"carvingEnabled": false,
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"caves": {"mode": "GENERATE_GROTTO"}
|
||||
}
|
||||
}
|
||||
@@ -545,6 +775,7 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"topology": {
|
||||
"routingStyle": {"expression": "route"}
|
||||
}
|
||||
@@ -587,6 +818,7 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"topology": {
|
||||
"routingStyle": {"expression": "route"}
|
||||
}
|
||||
@@ -618,6 +850,7 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"topology": {
|
||||
"routingStyle": "snippet/style/first"
|
||||
}
|
||||
@@ -639,6 +872,7 @@ public class PackRiverValidatorTest {
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": [{"id": "river"}]},
|
||||
"topology": {
|
||||
"routingStyle": {"expression": "route"}
|
||||
}
|
||||
@@ -677,6 +911,50 @@ public class PackRiverValidatorTest {
|
||||
return pack;
|
||||
}
|
||||
|
||||
private File packWithWorms(String worms) throws Exception {
|
||||
return pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {"worms": %s}
|
||||
}
|
||||
}
|
||||
""".formatted(worms));
|
||||
}
|
||||
|
||||
private String wormHierarchy(int rootCount, int childrenPerRoot) {
|
||||
StringBuilder hierarchy = new StringBuilder("[");
|
||||
int seed = 1;
|
||||
for (int rootIndex = 0; rootIndex < rootCount; rootIndex++) {
|
||||
if (rootIndex > 0) {
|
||||
hierarchy.append(',');
|
||||
}
|
||||
hierarchy.append("{\"id\":\"root_")
|
||||
.append(rootIndex)
|
||||
.append("\",\"seed\":")
|
||||
.append(seed++);
|
||||
if (childrenPerRoot > 0) {
|
||||
hierarchy.append(",\"children\":[");
|
||||
for (int childIndex = 0; childIndex < childrenPerRoot; childIndex++) {
|
||||
if (childIndex > 0) {
|
||||
hierarchy.append(',');
|
||||
}
|
||||
hierarchy.append("{\"id\":\"child_")
|
||||
.append(rootIndex)
|
||||
.append('_')
|
||||
.append(childIndex)
|
||||
.append("\",\"seed\":")
|
||||
.append(seed++)
|
||||
.append('}');
|
||||
}
|
||||
hierarchy.append(']');
|
||||
}
|
||||
hierarchy.append('}');
|
||||
}
|
||||
return hierarchy.append(']').toString();
|
||||
}
|
||||
|
||||
private void assertContains(List<String> messages, String fragment) {
|
||||
assertTrue(messages.toString(), contains(messages, fragment));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ public class IrisRiverSchemaTest {
|
||||
JSONObject properties = schema.getJSONObject("properties");
|
||||
JSONObject topology = referencedProperties(definitions, properties.getJSONObject("topology"));
|
||||
JSONObject source = referencedProperties(definitions, topology.getJSONObject("source"));
|
||||
JSONObject terrain = referencedProperties(definitions, properties.getJSONObject("terrain"));
|
||||
JSONObject terrainDefinition = referencedDefinition(definitions, properties.getJSONObject("terrain"));
|
||||
JSONObject terrain = terrainDefinition.getJSONObject("properties");
|
||||
JSONObject worms = terrain.getJSONObject("worms");
|
||||
JSONObject worm = referencedProperties(definitions, worms.getJSONObject("items"));
|
||||
JSONObject water = referencedProperties(definitions, properties.getJSONObject("water"));
|
||||
JSONObject biomes = referencedProperties(definitions, properties.getJSONObject("biomes"));
|
||||
|
||||
@@ -46,6 +49,29 @@ public class IrisRiverSchemaTest {
|
||||
assertEquals(2048D, terrain.getJSONObject("maxChannelWidth").getDouble("maximum"), 0D);
|
||||
assertEquals(0D, terrain.getJSONObject("maxBankWidth").getDouble("minimum"), 0D);
|
||||
assertEquals(512D, terrain.getJSONObject("maxDepth").getDouble("maximum"), 0D);
|
||||
assertTrue(arrayContains(terrainDefinition.getJSONArray("required"), "worms"));
|
||||
assertEquals("array", worms.getString("type"));
|
||||
assertEquals(1, worms.getInt("minItems"));
|
||||
assertEquals(0.000001D, worm.getJSONObject("weight").getDouble("minimum"), 0D);
|
||||
assertEquals(16384D, worm.getJSONObject("wavelength").getDouble("maximum"), 0D);
|
||||
assertEquals(1D, worm.getJSONObject("tortuosity").getDouble("maximum"), 0D);
|
||||
assertEquals(1024D, worm.getJSONObject("maxOffset").getDouble("maximum"), 0D);
|
||||
assertEquals(64, worm.getJSONObject("segments").getInt("maximum"));
|
||||
assertEquals(0.125D, worm.getJSONObject("widthMultiplier").getDouble("minimum"), 0D);
|
||||
assertEquals(8D, worm.getJSONObject("bankMultiplier").getDouble("maximum"), 0D);
|
||||
assertEquals(8D, worm.getJSONObject("depthMultiplier").getDouble("maximum"), 0D);
|
||||
assertEquals(32D, worm.getJSONObject("bodyWavelength").getDouble("minimum"), 0D);
|
||||
assertEquals(16384D, worm.getJSONObject("bodyDetailWavelength").getDouble("maximum"), 0D);
|
||||
assertEquals(0.875D, worm.getJSONObject("widthVariation").getDouble("maximum"), 0D);
|
||||
assertEquals(0.875D, worm.getJSONObject("bankVariation").getDouble("maximum"), 0D);
|
||||
assertEquals(0.875D, worm.getJSONObject("depthVariation").getDouble("maximum"), 0D);
|
||||
assertEquals(0.875D, worm.getJSONObject("roofVariation").getDouble("maximum"), 0D);
|
||||
assertEquals(8, worm.getJSONObject("branchCap").getInt("maximum"));
|
||||
assertEquals(1D, worm.getJSONObject("branchDecay").getDouble("maximum"), 0D);
|
||||
assertEquals(8D, worm.getJSONObject("confluenceMultiplier").getDouble("maximum"), 0D);
|
||||
assertEquals(1D, worm.getJSONObject("childChance").getDouble("maximum"), 0D);
|
||||
assertEquals(1D, worm.getJSONObject("branchChildChance").getDouble("maximum"), 0D);
|
||||
assertEquals("array", worm.getJSONObject("children").getString("type"));
|
||||
assertEquals(List.of("SEA_LEVEL", "TERRACED"), enumValues(definitions, water.getJSONObject("mode")));
|
||||
assertEquals("array", biomes.getJSONObject("channel").getString("type"));
|
||||
assertEquals("#/definitions/erzbiomes",
|
||||
@@ -87,8 +113,21 @@ public class IrisRiverSchemaTest {
|
||||
}
|
||||
|
||||
private static JSONObject referencedProperties(JSONObject definitions, JSONObject reference) {
|
||||
return referencedDefinition(definitions, reference).getJSONObject("properties");
|
||||
}
|
||||
|
||||
private static JSONObject referencedDefinition(JSONObject definitions, JSONObject reference) {
|
||||
String key = reference.getString("$ref").substring("#/definitions/".length());
|
||||
return definitions.getJSONObject(key).getJSONObject("properties");
|
||||
return definitions.getJSONObject(key);
|
||||
}
|
||||
|
||||
private static boolean arrayContains(JSONArray values, String expected) {
|
||||
for (int index = 0; index < values.length(); index++) {
|
||||
if (expected.equals(values.getString(index))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<String> enumValues(JSONObject definitions, JSONObject reference) {
|
||||
|
||||
@@ -36,6 +36,7 @@ public class IrisRiverConfigurationTest {
|
||||
assertEquals(10D, dimension.getRivers().getTerrain().getMaxChannelWidth(), 0D);
|
||||
assertEquals(4D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D);
|
||||
assertEquals(10D, dimension.getRivers().getTerrain().getMaxDepth(), 0D);
|
||||
assertTrue(dimension.getRivers().getTerrain().getWorms().isEmpty());
|
||||
assertEquals(IrisRiverCaveMode.SEALED, dimension.getRivers().getCaves().getMode());
|
||||
assertEquals(IrisRiverCaveFallback.SEALED, dimension.getRivers().getCaves().getFallback());
|
||||
assertEquals(IrisRiverExistingFluidPolicy.REJECT,
|
||||
@@ -65,6 +66,27 @@ public class IrisRiverConfigurationTest {
|
||||
"maxBankWidth": 2.5,
|
||||
"maxDepth": 8,
|
||||
"maxIncision": 36,
|
||||
"worms": [
|
||||
{
|
||||
"seed": 73,
|
||||
"weight": 2.5,
|
||||
"wavelength": 1536,
|
||||
"detailWavelength": 192,
|
||||
"tortuosity": 0.65,
|
||||
"detailTortuosity": 0.2,
|
||||
"maxOffset": 420,
|
||||
"segments": 56,
|
||||
"widthMultiplier": 1.4,
|
||||
"bankMultiplier": 1.2,
|
||||
"depthMultiplier": 0.8,
|
||||
"bodyWavelength": 704,
|
||||
"bodyDetailWavelength": 88,
|
||||
"widthVariation": 0.75,
|
||||
"bankVariation": 0.65,
|
||||
"depthVariation": 0.55,
|
||||
"roofVariation": 0.45
|
||||
}
|
||||
],
|
||||
"terminalMode": "SUPPRESS"
|
||||
},
|
||||
"water": {"mode": "TERRACED", "poolLength": 80},
|
||||
@@ -111,6 +133,24 @@ public class IrisRiverConfigurationTest {
|
||||
assertEquals(2.5D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D);
|
||||
assertEquals(8D, dimension.getRivers().getTerrain().getMaxDepth(), 0D);
|
||||
assertEquals(36, dimension.getRivers().getTerrain().getMaxIncision());
|
||||
IrisRiverWorm worm = dimension.getRivers().getTerrain().getWorms().get(0);
|
||||
assertEquals(73L, worm.getSeed());
|
||||
assertEquals(2.5D, worm.getWeight(), 0D);
|
||||
assertEquals(1536D, worm.getWavelength(), 0D);
|
||||
assertEquals(192D, worm.getDetailWavelength(), 0D);
|
||||
assertEquals(0.65D, worm.getTortuosity(), 0D);
|
||||
assertEquals(0.2D, worm.getDetailTortuosity(), 0D);
|
||||
assertEquals(420D, worm.getMaxOffset(), 0D);
|
||||
assertEquals(56, worm.getSegments());
|
||||
assertEquals(1.4D, worm.getWidthMultiplier(), 0D);
|
||||
assertEquals(1.2D, worm.getBankMultiplier(), 0D);
|
||||
assertEquals(0.8D, worm.getDepthMultiplier(), 0D);
|
||||
assertEquals(704D, worm.getBodyWavelength(), 0D);
|
||||
assertEquals(88D, worm.getBodyDetailWavelength(), 0D);
|
||||
assertEquals(0.75D, worm.getWidthVariation(), 0D);
|
||||
assertEquals(0.65D, worm.getBankVariation(), 0D);
|
||||
assertEquals(0.55D, worm.getDepthVariation(), 0D);
|
||||
assertEquals(0.45D, worm.getRoofVariation(), 0D);
|
||||
assertEquals(IrisRiverTerminalMode.SUPPRESS,
|
||||
dimension.getRivers().getTerrain().getTerminalMode());
|
||||
assertEquals(IrisRiverWaterMode.TERRACED, dimension.getRivers().getWater().getMode());
|
||||
@@ -145,7 +185,11 @@ public class IrisRiverConfigurationTest {
|
||||
.setFloodedCaveBiomes(new KList<>("biome-grotto"));
|
||||
IrisDimension dimension = new IrisDimension()
|
||||
.setRegions(new KList<>("region"))
|
||||
.setRivers(new IrisRiverNetwork().setEnabled(true).setBiomes(dimensionBiomes));
|
||||
.setRivers(new IrisRiverNetwork()
|
||||
.setEnabled(true)
|
||||
.setTerrain(new IrisRiverTerrain()
|
||||
.setWorms(new KList<>(new IrisRiverWorm())))
|
||||
.setBiomes(dimensionBiomes));
|
||||
IrisRegion region = new IrisRegion()
|
||||
.setLandBiomes(new KList<>("natural"))
|
||||
.setRiverOverride(regionOverride);
|
||||
|
||||
@@ -21,8 +21,6 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class RiverNetworkTest {
|
||||
private static final double TWO_PI_FOR_TESTS = StrictMath.PI * 2D;
|
||||
|
||||
@Test
|
||||
public void mapsNegativeWorldCoordinatesWithFloorDivision() {
|
||||
RiverNetwork network = new RiverNetwork(options(1L).build());
|
||||
@@ -164,12 +162,25 @@ public class RiverNetworkTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recursiveBranchesApplyTheChildSoftCapAtEveryNode() {
|
||||
public void recursiveBranchesApplyTheProfileChildSoftCapAtEveryNode() {
|
||||
RiverWorm constrained = wormProfile(
|
||||
"constrained",
|
||||
720L,
|
||||
6,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
4,
|
||||
0D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
List.of()
|
||||
);
|
||||
RiverNetwork network = new RiverNetwork(options(720L)
|
||||
.requireOcean(false)
|
||||
.routingBasinCells(16)
|
||||
.branchSoftCap(4)
|
||||
.branchChildShrinkFactor(0D)
|
||||
.worms(List.of(constrained))
|
||||
.build());
|
||||
RiverTerrainSampler terrain = flatTerrain(false);
|
||||
HashMap<RiverNodeId, Integer> incoming = new HashMap<>();
|
||||
@@ -188,36 +199,29 @@ public class RiverNetworkTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void candidateRankingAndTracingDoNotMaterializeMeanderGeometry() {
|
||||
RiverNetwork network = new RiverNetwork(options(75L)
|
||||
public void candidateRankingAndTracingAreIndependentOfWormGeometry() {
|
||||
RiverNetwork straightNetwork = new RiverNetwork(options(75L)
|
||||
.requireOcean(false)
|
||||
.meanderSubdivisions(32)
|
||||
.worms(List.of(worm(1L, 0D, 0D, 0D, 1)))
|
||||
.build());
|
||||
RiverNetwork windingNetwork = new RiverNetwork(options(75L)
|
||||
.requireOcean(false)
|
||||
.worms(List.of(worm(2L, 0.8D, 0.2D, 32D, 32)))
|
||||
.build());
|
||||
int[] meanderSamples = new int[1];
|
||||
RiverTerrainSampler terrain = new TestTerrain(false) {
|
||||
@Override
|
||||
public double reachRoutingCost(RiverRoutingContext context) {
|
||||
return context.midpointX() + context.midpointZ();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
meanderSamples[0]++;
|
||||
return 0D;
|
||||
}
|
||||
};
|
||||
RiverNodeId source = new RiverNodeId(0L, 0L);
|
||||
|
||||
assertFalse(network.downstreamCandidates(new RiverNodeId(0L, 0L), terrain).isEmpty());
|
||||
assertEquals(0, meanderSamples[0]);
|
||||
assertFalse(network.trace(new RiverNodeId(0L, 0L), terrain).edges().isEmpty());
|
||||
assertEquals(0, meanderSamples[0]);
|
||||
|
||||
RiverTile tile = network.buildTile(0, 0, terrain);
|
||||
|
||||
assertFalse(tile.reaches().isEmpty());
|
||||
assertTrue(meanderSamples[0] > 0);
|
||||
assertEquals(0, meanderSamples[0] % 12);
|
||||
assertTrue(meanderSamples[0] / 12 <= 256);
|
||||
assertFalse(straightNetwork.downstreamCandidates(source, terrain).isEmpty());
|
||||
assertEquals(
|
||||
straightNetwork.downstreamCandidates(source, terrain),
|
||||
windingNetwork.downstreamCandidates(source, terrain)
|
||||
);
|
||||
assertEquals(straightNetwork.trace(source, terrain), windingNetwork.trace(source, terrain));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -616,10 +620,10 @@ public class RiverNetworkTest {
|
||||
);
|
||||
|
||||
assertEquals(List.of(
|
||||
3050685048971830506L,
|
||||
-3861364772874819248L,
|
||||
-8770785170128501517L,
|
||||
6698607697170340377L
|
||||
973031325888677478L,
|
||||
8932177974453767311L,
|
||||
-5189009084208004632L,
|
||||
6374264141259432071L
|
||||
), actual);
|
||||
}
|
||||
|
||||
@@ -659,9 +663,20 @@ public class RiverNetworkTest {
|
||||
.channelWidth(512D)
|
||||
.maxChannelWidth(512D)
|
||||
.bankWidth(0D)
|
||||
.branchChildShrinkFactor(1D)
|
||||
.meanderStrength(0D)
|
||||
.meanderSubdivisions(1)
|
||||
.worms(List.of(wormProfile(
|
||||
"straight",
|
||||
1L,
|
||||
1,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
8,
|
||||
1D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
List.of()
|
||||
)))
|
||||
.build());
|
||||
RiverTerrainSampler terrain = new TestTerrain(false) {
|
||||
@Override
|
||||
@@ -787,9 +802,9 @@ public class RiverNetworkTest {
|
||||
1,
|
||||
1,
|
||||
10.0,
|
||||
RiverWidthProfile.constant(10.0),
|
||||
5.0,
|
||||
3.0,
|
||||
RiverBodyProfile.constant(10.0, 5.0, 3.0),
|
||||
false,
|
||||
true,
|
||||
new RiverPolyline(new double[]{0.0, 100.0}, new double[]{0.0, 0.0})
|
||||
@@ -867,14 +882,9 @@ public class RiverNetworkTest {
|
||||
.maxChannelWidth(10D)
|
||||
.maxBankWidth(3D)
|
||||
.maxDepth(6D)
|
||||
.meanderStrength(0D)
|
||||
.worms(List.of(worm(1L, 0D, 0D, 0D, 1)))
|
||||
.build());
|
||||
RiverTerrainSampler styled = new TestTerrain(false) {
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double channelWidth(RiverRoutingContext context, double fallback) {
|
||||
return 20.0;
|
||||
@@ -906,12 +916,15 @@ public class RiverNetworkTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void channelWidthVariesInsideOneReachAndRemainsSpatiallyQueryable() {
|
||||
public void bodyAnatomyVariesInsideOneReachAndRemainsSpatiallyQueryable() {
|
||||
RiverNode from = node(0L, 0L, 0D, 0D);
|
||||
RiverNode to = node(1L, 0L, 100D, 0D);
|
||||
RiverWidthProfile profile = new RiverWidthProfile(
|
||||
RiverBodyProfile profile = new RiverBodyProfile(
|
||||
new double[]{0D, 0.5D, 1D},
|
||||
new double[]{1D, 7D, 1D}
|
||||
new double[]{1D, 7D, 1D},
|
||||
new double[]{1D, 9D, 1D},
|
||||
new double[]{2D, 8D, 2D},
|
||||
new double[]{1D, 0.35D, 1D}
|
||||
);
|
||||
RiverReach reach = new RiverReach(
|
||||
RiverEdgeId.of(from.id(), to.id()),
|
||||
@@ -921,31 +934,100 @@ public class RiverNetworkTest {
|
||||
1,
|
||||
1,
|
||||
7D,
|
||||
9D,
|
||||
8D,
|
||||
profile,
|
||||
0D,
|
||||
3D,
|
||||
false,
|
||||
false,
|
||||
new RiverPolyline(new double[]{0D, 100D}, new double[]{0D, 0D})
|
||||
);
|
||||
RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach));
|
||||
|
||||
RiverSample narrow = tile.sample(10D, 2D);
|
||||
RiverSample wide = tile.sample(50D, 2D);
|
||||
RiverSample narrow = tile.sample(10D, 7D);
|
||||
RiverSample swollen = tile.sample(50D, 7D);
|
||||
|
||||
assertFalse(narrow.present());
|
||||
assertTrue(wide.present());
|
||||
assertEquals(7D, wide.width(), 0D);
|
||||
assertEquals(RiverSection.CHANNEL, wide.section());
|
||||
assertTrue(swollen.present());
|
||||
assertEquals(7D, swollen.width(), 0D);
|
||||
assertEquals(9D, swollen.bankWidth(), 0D);
|
||||
assertEquals(8D, swollen.depth(), 0D);
|
||||
assertEquals(0.35D, reach.roofScaleAt(swollen.alongReach()), 0.0000001D);
|
||||
assertEquals(RiverSection.BANK, swollen.section());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void perlinWormBodyAnatomyVariesAllDimensionsDeterministically() {
|
||||
RiverWorm anatomy = wormProfile(
|
||||
"anatomy",
|
||||
917L,
|
||||
24,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
64D,
|
||||
32D,
|
||||
0.875D,
|
||||
0.875D,
|
||||
0.875D,
|
||||
0.875D,
|
||||
4,
|
||||
0.35D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
List.of()
|
||||
);
|
||||
RiverNetwork network = new RiverNetwork(options(917L)
|
||||
.requireOcean(false)
|
||||
.maxChannelWidth(64D)
|
||||
.maxBankWidth(64D)
|
||||
.maxDepth(64D)
|
||||
.maximumReachRadius(96D)
|
||||
.worms(List.of(anatomy))
|
||||
.build());
|
||||
RiverTerrainSampler terrain = flatTerrain(false);
|
||||
|
||||
RiverTile first = network.buildTile(0, 0, terrain);
|
||||
RiverTile second = network.buildTile(0, 0, terrain);
|
||||
|
||||
assertFalse(first.reaches().isEmpty());
|
||||
assertEquals(digest(first), digest(second));
|
||||
double minimumWidth = Double.POSITIVE_INFINITY;
|
||||
double maximumWidth = 0D;
|
||||
double minimumBank = Double.POSITIVE_INFINITY;
|
||||
double maximumBank = 0D;
|
||||
double minimumDepth = Double.POSITIVE_INFINITY;
|
||||
double maximumDepth = 0D;
|
||||
double minimumRoof = Double.POSITIVE_INFINITY;
|
||||
double maximumRoof = 0D;
|
||||
for (RiverReach reach : first.reaches()) {
|
||||
for (int index = 0; index < reach.bodyProfile().size(); index++) {
|
||||
minimumWidth = StrictMath.min(minimumWidth, reach.bodyProfile().widthAtIndex(index));
|
||||
maximumWidth = StrictMath.max(maximumWidth, reach.bodyProfile().widthAtIndex(index));
|
||||
minimumBank = StrictMath.min(minimumBank, reach.bodyProfile().bankWidthAtIndex(index));
|
||||
maximumBank = StrictMath.max(maximumBank, reach.bodyProfile().bankWidthAtIndex(index));
|
||||
minimumDepth = StrictMath.min(minimumDepth, reach.bodyProfile().depthAtIndex(index));
|
||||
maximumDepth = StrictMath.max(maximumDepth, reach.bodyProfile().depthAtIndex(index));
|
||||
minimumRoof = StrictMath.min(minimumRoof, reach.bodyProfile().roofScaleAtIndex(index));
|
||||
maximumRoof = StrictMath.max(maximumRoof, reach.bodyProfile().roofScaleAtIndex(index));
|
||||
}
|
||||
}
|
||||
assertTrue(maximumWidth - minimumWidth > 2D);
|
||||
assertTrue(maximumBank - minimumBank > 1.5D);
|
||||
assertTrue(maximumDepth - minimumDepth > 0.75D);
|
||||
assertTrue(maximumRoof - minimumRoof > 0.1D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void foldedReachSamplingFindsFartherCoveringWidthEnvelope() {
|
||||
RiverNode from = node(0L, 0L, 0D, 0D);
|
||||
RiverNode to = node(1L, 0L, 0D, 10D);
|
||||
RiverWidthProfile profile = new RiverWidthProfile(
|
||||
RiverBodyProfile profile = new RiverBodyProfile(
|
||||
new double[]{0D, 0.48D, 0.53D, 1D},
|
||||
new double[]{1D, 1D, 18D, 18D}
|
||||
new double[]{1D, 1D, 18D, 18D},
|
||||
new double[]{0D, 0D, 0D, 0D},
|
||||
new double[]{3D, 3D, 3D, 3D},
|
||||
new double[]{1D, 1D, 1D, 1D}
|
||||
);
|
||||
RiverReach reach = new RiverReach(
|
||||
RiverEdgeId.of(from.id(), to.id()),
|
||||
@@ -955,9 +1037,9 @@ public class RiverNetworkTest {
|
||||
1,
|
||||
1,
|
||||
18D,
|
||||
profile,
|
||||
0D,
|
||||
3D,
|
||||
profile,
|
||||
false,
|
||||
false,
|
||||
new RiverPolyline(
|
||||
@@ -978,15 +1060,12 @@ public class RiverNetworkTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reachFeasibilityReceivesTheFinalPinnedMeanderPolyline() {
|
||||
RiverNetwork network = new RiverNetwork(options(62L).meanderStrength(20D).build());
|
||||
boolean[] observedMeander = new boolean[1];
|
||||
public void reachFeasibilityReceivesTheFinalPinnedWormPolyline() {
|
||||
RiverNetwork network = new RiverNetwork(options(62L)
|
||||
.worms(List.of(worm(62L, 0.8D, 0.2D, 20D, 32)))
|
||||
.build());
|
||||
boolean[] observedWorm = new boolean[1];
|
||||
RiverTerrainSampler terrain = new TestTerrain(false) {
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
return 1D;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowsReach(RiverRoutingContext context) {
|
||||
RiverPolyline polyline = context.polyline();
|
||||
@@ -996,7 +1075,7 @@ public class RiverNetworkTest {
|
||||
double pointX = polyline.x(point) - context.from().x();
|
||||
double pointZ = polyline.z(point) - context.from().z();
|
||||
if (StrictMath.abs(pointX * deltaZ - pointZ * deltaX) > 0.000001D) {
|
||||
observedMeander[0] = true;
|
||||
observedWorm[0] = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1006,76 +1085,66 @@ public class RiverNetworkTest {
|
||||
|
||||
RiverNode downstream = network.downstream(new RiverNodeId(0L, 0L), terrain);
|
||||
|
||||
assertTrue(observedMeander[0]);
|
||||
assertTrue(observedWorm[0]);
|
||||
assertEquals(null, downstream);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void irisNoiseChangesTheSelectedPersonalityWithoutBreakingItsEnvelope() {
|
||||
double meanderStrength = 20D;
|
||||
RiverNetwork network = new RiverNetwork(options(63L)
|
||||
public void wormSeedChangesDeterministicGeometryWithoutBreakingItsEnvelope() {
|
||||
double maximumOffset = 20D;
|
||||
RiverNetwork firstNetwork = new RiverNetwork(options(63L)
|
||||
.siteJitter(0D)
|
||||
.requireOcean(false)
|
||||
.meanderStrength(meanderStrength)
|
||||
.meanderSubdivisions(32)
|
||||
.worms(List.of(worm(101L, 0.8D, 0.2D, maximumOffset, 32)))
|
||||
.build());
|
||||
RiverTerrainSampler quietTerrain = new TestTerrain(false) {
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
return 0D;
|
||||
}
|
||||
};
|
||||
RiverTerrainSampler noisyTerrain = new TestTerrain(false) {
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
return StrictMath.sin(context.normalizedPosition() * StrictMath.PI * 7D);
|
||||
}
|
||||
};
|
||||
RiverNetwork secondNetwork = new RiverNetwork(options(63L)
|
||||
.siteJitter(0D)
|
||||
.requireOcean(false)
|
||||
.worms(List.of(worm(202L, 0.8D, 0.2D, maximumOffset, 32)))
|
||||
.build());
|
||||
RiverTerrainSampler terrain = new TestTerrain(false);
|
||||
|
||||
RiverReach quietReach = network.buildTile(0, 0, quietTerrain).reaches().getFirst();
|
||||
RiverReach noisyReach = network.buildTile(0, 0, noisyTerrain).reaches().getFirst();
|
||||
assertEquals(quietReach.id(), noisyReach.id());
|
||||
RiverPolyline quietPolyline = quietReach.polyline();
|
||||
RiverPolyline noisyPolyline = noisyReach.polyline();
|
||||
double deltaX = noisyReach.to().x() - noisyReach.from().x();
|
||||
double deltaZ = noisyReach.to().z() - noisyReach.from().z();
|
||||
double chordLength = StrictMath.hypot(deltaX, deltaZ);
|
||||
boolean changed = false;
|
||||
for (int point = 1; point < noisyPolyline.size() - 1; point++) {
|
||||
double pointX = noisyPolyline.x(point) - noisyReach.from().x();
|
||||
double pointZ = noisyPolyline.z(point) - noisyReach.from().z();
|
||||
double signedOffset = (pointX * deltaZ - pointZ * deltaX) / chordLength;
|
||||
if (Double.doubleToLongBits(noisyPolyline.x(point)) != Double.doubleToLongBits(quietPolyline.x(point))
|
||||
|| Double.doubleToLongBits(noisyPolyline.z(point))
|
||||
!= Double.doubleToLongBits(quietPolyline.z(point))) {
|
||||
changed = true;
|
||||
}
|
||||
assertTrue(StrictMath.abs(signedOffset) <= meanderStrength + 0.0000001D);
|
||||
}
|
||||
RiverReach firstReach = firstNetwork.buildTile(0, 0, terrain).reaches().getFirst();
|
||||
RiverReach repeatedReach = firstNetwork.buildTile(0, 0, terrain).reaches().getFirst();
|
||||
RiverReach secondReach = secondNetwork.buildTile(0, 0, terrain).reaches().getFirst();
|
||||
|
||||
assertTrue(changed);
|
||||
assertEquals(noisyReach.from().x(), noisyPolyline.x(0), 0D);
|
||||
assertEquals(noisyReach.from().z(), noisyPolyline.z(0), 0D);
|
||||
int last = noisyPolyline.size() - 1;
|
||||
assertEquals(noisyReach.to().x(), noisyPolyline.x(last), 0D);
|
||||
assertEquals(noisyReach.to().z(), noisyPolyline.z(last), 0D);
|
||||
assertReachEquals(firstReach, repeatedReach);
|
||||
assertEquals(firstReach.id(), secondReach.id());
|
||||
assertTrue(polylinesDiffer(firstReach.polyline(), secondReach.polyline()));
|
||||
assertWormBounds(firstReach, maximumOffset);
|
||||
assertWormBounds(secondReach, maximumOffset);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reachesSpanBroadAsymmetricCompoundAndRestlessShapeFamilies() {
|
||||
double meanderStrength = 20D;
|
||||
public void weightedWormProfilesSelectDistinctConfigurableVariants() {
|
||||
RiverWorm gentle = new RiverWorm(
|
||||
"gentle", 301L, 1D, 1024D, 256D, 0.2D, 0.05D, 10D, 8, 0.5D, 0.5D, 0.5D,
|
||||
512D, 128D, 0D, 0D, 0D, 0D,
|
||||
4, 0.35D, 1D, 0D, 0D, List.of()
|
||||
);
|
||||
RiverWorm winding = new RiverWorm(
|
||||
"winding", 302L, 1D, 512D, 128D, 0.55D, 0.15D, 20D, 16, 1D, 1D, 1D,
|
||||
512D, 128D, 0D, 0D, 0D, 0D,
|
||||
4, 0.35D, 1D, 0D, 0D, List.of()
|
||||
);
|
||||
RiverWorm restless = new RiverWorm(
|
||||
"restless", 303L, 1D, 192D, 48D, 0.9D, 0.35D, 30D, 32, 2D, 2D, 2D,
|
||||
512D, 128D, 0D, 0D, 0D, 0D,
|
||||
4, 0.35D, 1D, 0D, 0D, List.of()
|
||||
);
|
||||
RiverNetwork network = new RiverNetwork(options(64L)
|
||||
.siteJitter(0D)
|
||||
.routingBasinCells(8)
|
||||
.requireOcean(false)
|
||||
.meanderStrength(meanderStrength)
|
||||
.meanderSubdivisions(48)
|
||||
.maxChannelWidth(32D)
|
||||
.maxBankWidth(32D)
|
||||
.maxDepth(32D)
|
||||
.orderWidthFactor(0D)
|
||||
.orderDepthFactor(0D)
|
||||
.maximumReachRadius(32D)
|
||||
.worms(List.of(gentle, winding, restless))
|
||||
.build());
|
||||
RiverTerrainSampler terrain = new TestTerrain(false) {
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
return 0D;
|
||||
}
|
||||
};
|
||||
RiverTerrainSampler terrain = new TestTerrain(false);
|
||||
|
||||
HashMap<RiverEdgeId, RiverReach> uniqueReaches = new HashMap<>();
|
||||
for (int tileX = -2; tileX <= 2; tileX++) {
|
||||
@@ -1085,39 +1154,217 @@ public class RiverNetworkTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
ArrayList<ReachShape> shapes = new ArrayList<>(uniqueReaches.size());
|
||||
Set<Integer> selectedPointCounts = new HashSet<>();
|
||||
HashMap<Integer, Double> maximumDisplacementRatios = new HashMap<>();
|
||||
for (RiverReach reach : uniqueReaches.values()) {
|
||||
shapes.add(shapeOf(reach, meanderStrength));
|
||||
}
|
||||
double minimumAmplitude = shapes.stream()
|
||||
.mapToDouble(ReachShape::amplitudeRatio)
|
||||
.min()
|
||||
.orElseThrow();
|
||||
double maximumAmplitude = shapes.stream()
|
||||
.mapToDouble(ReachShape::amplitudeRatio)
|
||||
.max()
|
||||
.orElseThrow();
|
||||
HashSet<Integer> dominantBands = new HashSet<>();
|
||||
HashSet<String> signatures = new HashSet<>();
|
||||
for (ReachShape shape : shapes) {
|
||||
dominantBands.add(shape.dominantBand());
|
||||
signatures.add(
|
||||
StrictMath.min(6, shape.crossings()) + ":"
|
||||
+ (int) StrictMath.floor(shape.centroid() * 5D) + ":"
|
||||
+ (int) StrictMath.floor(shape.amplitudeRatio() * 5D) + ":"
|
||||
+ shape.dominantBand()
|
||||
int pointCount = reach.polyline().size();
|
||||
selectedPointCounts.add(pointCount);
|
||||
RiverWorm selected = switch (pointCount) {
|
||||
case 9 -> gentle;
|
||||
case 17 -> winding;
|
||||
case 33 -> restless;
|
||||
default -> throw new AssertionError("Unexpected worm point count " + pointCount);
|
||||
};
|
||||
assertEquals(8D * selected.widthMultiplier(), reach.width(), 0.0000001D);
|
||||
assertEquals(6D * selected.bankMultiplier(), reach.bankWidth(), 0.0000001D);
|
||||
assertEquals(3D * selected.depthMultiplier(), reach.depth(), 0.0000001D);
|
||||
assertWormBounds(reach, selected.maxOffset());
|
||||
maximumDisplacementRatios.merge(
|
||||
pointCount,
|
||||
maximumWormDisplacement(reach) / selected.maxOffset(),
|
||||
StrictMath::max
|
||||
);
|
||||
}
|
||||
String diagnostics = shapes.toString();
|
||||
assertTrue(diagnostics, shapes.size() >= 32);
|
||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() <= 1));
|
||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() >= 2 && shape.crossings() <= 4));
|
||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() >= 5));
|
||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.centroid() < 0.43D));
|
||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.centroid() > 0.57D));
|
||||
assertTrue(diagnostics, maximumAmplitude - minimumAmplitude >= 0.25D);
|
||||
assertTrue(diagnostics, dominantBands.size() >= 3);
|
||||
assertTrue(diagnostics, signatures.size() >= 8);
|
||||
assertEquals(Set.of(9, 17, 33), selectedPointCounts);
|
||||
assertTrue(maximumDisplacementRatios.toString(), maximumDisplacementRatios.get(9) > 0.03D);
|
||||
assertTrue(maximumDisplacementRatios.toString(), maximumDisplacementRatios.get(17) > 0.18D);
|
||||
assertTrue(maximumDisplacementRatios.toString(), maximumDisplacementRatios.get(33) > 0.4D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forcedChildHierarchyControlsReachGeometryAndDimensionsDeterministically() {
|
||||
RiverWorm child = wormProfile(
|
||||
"child",
|
||||
402L,
|
||||
24,
|
||||
2D,
|
||||
1.5D,
|
||||
0.5D,
|
||||
8,
|
||||
1D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
List.of()
|
||||
);
|
||||
RiverWorm root = wormProfile(
|
||||
"root",
|
||||
401L,
|
||||
8,
|
||||
0.5D,
|
||||
0.5D,
|
||||
0.5D,
|
||||
8,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
0D,
|
||||
List.of(child)
|
||||
);
|
||||
RiverNetwork network = new RiverNetwork(options(68L)
|
||||
.siteJitter(0D)
|
||||
.requireOcean(false)
|
||||
.maxChannelWidth(32D)
|
||||
.maxBankWidth(32D)
|
||||
.maxDepth(32D)
|
||||
.orderWidthFactor(0D)
|
||||
.orderDepthFactor(0D)
|
||||
.maximumReachRadius(32D)
|
||||
.worms(List.of(root))
|
||||
.build());
|
||||
RiverTerrainSampler terrain = new TestTerrain(false);
|
||||
|
||||
RiverTile first = network.buildTile(0, 0, terrain);
|
||||
RiverTile second = network.buildTile(0, 0, terrain);
|
||||
|
||||
assertFalse(first.reaches().isEmpty());
|
||||
assertEquals(digest(first), digest(second));
|
||||
for (RiverReach reach : first.reaches()) {
|
||||
assertEquals(child.segments() + 1, reach.polyline().size());
|
||||
assertEquals(8D * child.widthMultiplier(), reach.width(), 0.0000001D);
|
||||
assertEquals(6D * child.bankMultiplier(), reach.bankWidth(), 0.0000001D);
|
||||
assertEquals(3D * child.depthMultiplier(), reach.depth(), 0.0000001D);
|
||||
assertWormBounds(reach, child.maxOffset());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wormHierarchyRejectsInvalidIdsDepthAndCounts() {
|
||||
assertThrows(IllegalArgumentException.class, () -> wormProfile(
|
||||
"Invalid",
|
||||
1L,
|
||||
8,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
512D,
|
||||
128D,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
4,
|
||||
0.35D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
List.of()
|
||||
));
|
||||
|
||||
RiverWorm duplicateChild = wormProfile(
|
||||
"duplicate", 2L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 0D, 0D, List.of()
|
||||
);
|
||||
RiverWorm duplicateRoot = wormProfile(
|
||||
"duplicate", 3L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 1D, 0D, List.of(duplicateChild)
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> options(10L).worms(List.of(duplicateRoot)).build()
|
||||
);
|
||||
|
||||
RiverWorm tooDeep = wormProfile(
|
||||
"depth-5", 5L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 0D, 0D, List.of()
|
||||
);
|
||||
for (int depth = 4; depth >= 1; depth--) {
|
||||
tooDeep = wormProfile(
|
||||
"depth-" + depth,
|
||||
depth,
|
||||
8,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
4,
|
||||
0.35D,
|
||||
1D,
|
||||
1D,
|
||||
0D,
|
||||
List.of(tooDeep)
|
||||
);
|
||||
}
|
||||
RiverWorm depthRoot = tooDeep;
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> options(10L).worms(List.of(depthRoot)).build()
|
||||
);
|
||||
|
||||
ArrayList<RiverWorm> roots = new ArrayList<>();
|
||||
for (int root = 0; root < 17; root++) {
|
||||
roots.add(wormProfile(
|
||||
"root-" + root, root, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 0D, 0D, List.of()
|
||||
));
|
||||
}
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).worms(roots).build());
|
||||
|
||||
ArrayList<RiverWorm> branches = new ArrayList<>();
|
||||
for (int branch = 0; branch < 8; branch++) {
|
||||
ArrayList<RiverWorm> leaves = new ArrayList<>();
|
||||
for (int leaf = 0; leaf < 16; leaf++) {
|
||||
leaves.add(wormProfile(
|
||||
"leaf-" + branch + "-" + leaf,
|
||||
branch * 16L + leaf,
|
||||
8,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
4,
|
||||
0.35D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
branches.add(wormProfile(
|
||||
"branch-" + branch,
|
||||
branch,
|
||||
8,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
4,
|
||||
0.35D,
|
||||
1D,
|
||||
1D,
|
||||
0D,
|
||||
leaves
|
||||
));
|
||||
}
|
||||
RiverWorm oversized = wormProfile(
|
||||
"oversized", 900L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 1D, 0D, branches
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> options(10L).worms(List.of(oversized)).build()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wormProfileRejectsInvalidBranchControls() {
|
||||
assertThrows(IllegalArgumentException.class, () -> wormProfile(
|
||||
"cap", 1L, 8, 1D, 1D, 1D, 0, 0.35D, 1D, 0D, 0D, List.of()
|
||||
));
|
||||
assertThrows(IllegalArgumentException.class, () -> wormProfile(
|
||||
"decay", 1L, 8, 1D, 1D, 1D, 4, 1.1D, 1D, 0D, 0D, List.of()
|
||||
));
|
||||
assertThrows(IllegalArgumentException.class, () -> wormProfile(
|
||||
"confluence", 1L, 8, 1D, 1D, 1D, 4, 0.35D, 8.1D, 0D, 0D, List.of()
|
||||
));
|
||||
assertThrows(IllegalArgumentException.class, () -> wormProfile(
|
||||
"child-chance", 1L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 1.1D, 0D, List.of()
|
||||
));
|
||||
assertThrows(IllegalArgumentException.class, () -> wormProfile(
|
||||
"sibling-chance", 1L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 0D, 1.1D, List.of()
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1194,8 +1441,6 @@ public class RiverNetworkTest {
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).routingPlateauHeight(0D).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).hydraulicBaseHeight(Double.NaN).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).confluenceWeight(-1D).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).branchSoftCap(0).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).branchChildShrinkFactor(1.1D).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).channelWidth(-1.0).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L)
|
||||
.tileCells(1)
|
||||
@@ -1255,8 +1500,7 @@ public class RiverNetworkTest {
|
||||
.siteJitter(0.49D)
|
||||
.maxRouteReaches(256)
|
||||
.maximumReachRadius(pathologicalRadius)
|
||||
.meanderStrength(1024D)
|
||||
.meanderSubdivisions(64)
|
||||
.worms(List.of(worm(1L, 1D, 1D, 1024D, 64)))
|
||||
.build()
|
||||
);
|
||||
assertTrue(failure.getMessage(), failure.getMessage().contains("source window"));
|
||||
@@ -1278,8 +1522,126 @@ public class RiverNetworkTest {
|
||||
.channelWidth(8.0)
|
||||
.bankWidth(6.0)
|
||||
.depth(3.0)
|
||||
.meanderStrength(12.0)
|
||||
.meanderSubdivisions(6);
|
||||
.worms(List.of(worm(1L, 0.5D, 0.15D, 12D, 6)));
|
||||
}
|
||||
|
||||
private static RiverWorm worm(
|
||||
long seed,
|
||||
double tortuosity,
|
||||
double detailTortuosity,
|
||||
double maximumOffset,
|
||||
int segments
|
||||
) {
|
||||
return new RiverWorm(
|
||||
"worm-" + Long.toUnsignedString(seed),
|
||||
seed,
|
||||
1D,
|
||||
1024D,
|
||||
256D,
|
||||
tortuosity,
|
||||
detailTortuosity,
|
||||
maximumOffset,
|
||||
segments,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
512D,
|
||||
128D,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
4,
|
||||
0.35D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private static RiverWorm wormProfile(
|
||||
String id,
|
||||
long seed,
|
||||
int segments,
|
||||
double widthMultiplier,
|
||||
double bankMultiplier,
|
||||
double depthMultiplier,
|
||||
int branchCap,
|
||||
double branchDecay,
|
||||
double confluenceMultiplier,
|
||||
double childChance,
|
||||
double branchChildChance,
|
||||
List<RiverWorm> children
|
||||
) {
|
||||
return wormProfile(
|
||||
id,
|
||||
seed,
|
||||
segments,
|
||||
widthMultiplier,
|
||||
bankMultiplier,
|
||||
depthMultiplier,
|
||||
512D,
|
||||
128D,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
branchCap,
|
||||
branchDecay,
|
||||
confluenceMultiplier,
|
||||
childChance,
|
||||
branchChildChance,
|
||||
children
|
||||
);
|
||||
}
|
||||
|
||||
private static RiverWorm wormProfile(
|
||||
String id,
|
||||
long seed,
|
||||
int segments,
|
||||
double widthMultiplier,
|
||||
double bankMultiplier,
|
||||
double depthMultiplier,
|
||||
double bodyWavelength,
|
||||
double bodyDetailWavelength,
|
||||
double widthVariation,
|
||||
double bankVariation,
|
||||
double depthVariation,
|
||||
double roofVariation,
|
||||
int branchCap,
|
||||
double branchDecay,
|
||||
double confluenceMultiplier,
|
||||
double childChance,
|
||||
double branchChildChance,
|
||||
List<RiverWorm> children
|
||||
) {
|
||||
return new RiverWorm(
|
||||
id,
|
||||
seed,
|
||||
1D,
|
||||
256D,
|
||||
64D,
|
||||
0.7D,
|
||||
0.2D,
|
||||
32D,
|
||||
segments,
|
||||
widthMultiplier,
|
||||
bankMultiplier,
|
||||
depthMultiplier,
|
||||
bodyWavelength,
|
||||
bodyDetailWavelength,
|
||||
widthVariation,
|
||||
bankVariation,
|
||||
depthVariation,
|
||||
roofVariation,
|
||||
branchCap,
|
||||
branchDecay,
|
||||
confluenceMultiplier,
|
||||
childChance,
|
||||
branchChildChance,
|
||||
children
|
||||
);
|
||||
}
|
||||
|
||||
private static RiverNode node(long cellX, long cellZ, double x, double z) {
|
||||
@@ -1305,9 +1667,9 @@ public class RiverNetworkTest {
|
||||
1,
|
||||
1,
|
||||
width,
|
||||
RiverWidthProfile.constant(width),
|
||||
bankWidth,
|
||||
3D,
|
||||
RiverBodyProfile.constant(width, bankWidth, 3D),
|
||||
false,
|
||||
false,
|
||||
new RiverPolyline(
|
||||
@@ -1347,55 +1709,45 @@ public class RiverNetworkTest {
|
||||
return hydraulicComparison != 0 ? hydraulicComparison : first.id().compareTo(second.id());
|
||||
}
|
||||
|
||||
private static ReachShape shapeOf(RiverReach reach, double meanderStrength) {
|
||||
private static void assertWormBounds(RiverReach reach, double maximumOffset) {
|
||||
assertTrue(maximumWormDisplacement(reach) <= maximumOffset + 0.0000001D);
|
||||
RiverPolyline polyline = reach.polyline();
|
||||
assertEquals(reach.from().x(), polyline.x(0), 0D);
|
||||
assertEquals(reach.from().z(), polyline.z(0), 0D);
|
||||
int last = polyline.size() - 1;
|
||||
assertEquals(reach.to().x(), polyline.x(last), 0D);
|
||||
assertEquals(reach.to().z(), polyline.z(last), 0D);
|
||||
}
|
||||
|
||||
private static double maximumWormDisplacement(RiverReach reach) {
|
||||
RiverPolyline polyline = reach.polyline();
|
||||
double deltaX = reach.to().x() - reach.from().x();
|
||||
double deltaZ = reach.to().z() - reach.from().z();
|
||||
double chordLength = StrictMath.hypot(deltaX, deltaZ);
|
||||
double[] offsets = new double[polyline.size()];
|
||||
double maximumOffset = 0D;
|
||||
double weightedPosition = 0D;
|
||||
double totalWeight = 0D;
|
||||
int crossings = 0;
|
||||
int previousSign = 0;
|
||||
double maximumDisplacement = 0D;
|
||||
for (int point = 0; point < polyline.size(); point++) {
|
||||
double pointX = polyline.x(point) - reach.from().x();
|
||||
double pointZ = polyline.z(point) - reach.from().z();
|
||||
double offset = (pointX * deltaZ - pointZ * deltaX) / chordLength;
|
||||
offsets[point] = offset;
|
||||
double absoluteOffset = StrictMath.abs(offset);
|
||||
maximumOffset = StrictMath.max(maximumOffset, absoluteOffset);
|
||||
double t = (double) point / (polyline.size() - 1);
|
||||
weightedPosition += t * absoluteOffset;
|
||||
totalWeight += absoluteOffset;
|
||||
int sign = absoluteOffset < meanderStrength * 0.05D ? 0 : offset < 0D ? -1 : 1;
|
||||
if (sign != 0 && previousSign != 0 && sign != previousSign) {
|
||||
crossings++;
|
||||
double straightX = reach.from().x() + deltaX * t;
|
||||
double straightZ = reach.from().z() + deltaZ * t;
|
||||
double displacement = StrictMath.hypot(
|
||||
polyline.x(point) - straightX,
|
||||
polyline.z(point) - straightZ
|
||||
);
|
||||
maximumDisplacement = StrictMath.max(maximumDisplacement, displacement);
|
||||
}
|
||||
if (sign != 0) {
|
||||
previousSign = sign;
|
||||
return maximumDisplacement;
|
||||
}
|
||||
assertTrue(absoluteOffset <= meanderStrength + 0.0000001D);
|
||||
|
||||
private static boolean polylinesDiffer(RiverPolyline first, RiverPolyline second) {
|
||||
if (first.size() != second.size()) {
|
||||
return true;
|
||||
}
|
||||
int dominantBand = 1;
|
||||
double dominantEnergy = -1D;
|
||||
int samples = offsets.length - 1;
|
||||
for (int frequency = 1; frequency <= 8; frequency++) {
|
||||
double cosine = 0D;
|
||||
double sine = 0D;
|
||||
for (int point = 0; point < samples; point++) {
|
||||
double phase = TWO_PI_FOR_TESTS * frequency * point / samples;
|
||||
cosine += offsets[point] * StrictMath.cos(phase);
|
||||
sine += offsets[point] * StrictMath.sin(phase);
|
||||
}
|
||||
double energy = cosine * cosine + sine * sine;
|
||||
if (energy > dominantEnergy) {
|
||||
dominantEnergy = energy;
|
||||
dominantBand = frequency;
|
||||
for (int point = 0; point < first.size(); point++) {
|
||||
if (Double.doubleToLongBits(first.x(point)) != Double.doubleToLongBits(second.x(point))
|
||||
|| Double.doubleToLongBits(first.z(point)) != Double.doubleToLongBits(second.z(point))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
double centroid = totalWeight <= 0.0000001D ? 0.5D : weightedPosition / totalWeight;
|
||||
return new ReachShape(crossings, maximumOffset / meanderStrength, centroid, dominantBand);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static long digest(RiverTile tile) {
|
||||
@@ -1408,9 +1760,12 @@ public class RiverNetworkTest {
|
||||
hash = RiverNetwork.mix(hash ^ reach.order());
|
||||
hash = RiverNetwork.mix(hash ^ reach.state().ordinal());
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.width()));
|
||||
for (int index = 0; index < reach.widthProfile().size(); index++) {
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.widthProfile().position(index)));
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.widthProfile().width(index)));
|
||||
for (int index = 0; index < reach.bodyProfile().size(); index++) {
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().position(index)));
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().widthAtIndex(index)));
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().bankWidthAtIndex(index)));
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().depthAtIndex(index)));
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().roofScaleAtIndex(index)));
|
||||
}
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bankWidth()));
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.depth()));
|
||||
@@ -1442,7 +1797,7 @@ public class RiverNetworkTest {
|
||||
assertEquals(first.flow(), second.flow());
|
||||
assertEquals(first.order(), second.order());
|
||||
assertEquals(first.width(), second.width(), 0.0);
|
||||
assertEquals(first.widthProfile(), second.widthProfile());
|
||||
assertEquals(first.bodyProfile(), second.bodyProfile());
|
||||
assertEquals(first.bankWidth(), second.bankWidth(), 0.0);
|
||||
assertEquals(first.depth(), second.depth(), 0.0);
|
||||
assertEquals(first.mouth(), second.mouth());
|
||||
@@ -1473,7 +1828,4 @@ public class RiverNetworkTest {
|
||||
return ocean && blockX >= 224;
|
||||
}
|
||||
}
|
||||
|
||||
private record ReachShape(int crossings, double amplitudeRatio, double centroid, int dominantBand) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,9 +295,9 @@ public class RiverTileCacheTest {
|
||||
1,
|
||||
1,
|
||||
8.0,
|
||||
RiverWidthProfile.constant(8.0),
|
||||
4.0,
|
||||
3.0,
|
||||
RiverBodyProfile.constant(8.0, 4.0, 3.0),
|
||||
false,
|
||||
false,
|
||||
new RiverPolyline(new double[]{8.0, 56.0}, new double[]{8.0, 8.0})
|
||||
|
||||
@@ -11,6 +11,7 @@ import art.arcane.iris.engine.object.IrisRiverOverride;
|
||||
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
|
||||
import art.arcane.iris.engine.object.IrisRiverTerminalMode;
|
||||
import art.arcane.iris.engine.object.IrisRiverWaterMode;
|
||||
import art.arcane.iris.engine.object.IrisRiverWorm;
|
||||
import art.arcane.iris.engine.object.IrisStyledRange;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.river.RiverEdgeId;
|
||||
@@ -20,14 +21,15 @@ import art.arcane.iris.engine.river.RiverPolyline;
|
||||
import art.arcane.iris.engine.river.RiverReach;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverAnchor;
|
||||
import art.arcane.iris.engine.river.RiverBodyProfile;
|
||||
import art.arcane.iris.engine.river.RiverRoutingContext;
|
||||
import art.arcane.iris.engine.river.RiverTerrainNodeSample;
|
||||
import art.arcane.iris.engine.river.RiverTopologyComplexity;
|
||||
import art.arcane.iris.engine.river.RiverTerrainSourceSample;
|
||||
import art.arcane.iris.engine.river.RiverWidthProfile;
|
||||
import art.arcane.iris.util.project.interpolation.NoiseBounds;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -793,9 +795,9 @@ public class IrisRiverRuntimeTest {
|
||||
1,
|
||||
1,
|
||||
4D,
|
||||
RiverWidthProfile.constant(4D),
|
||||
2D,
|
||||
4D,
|
||||
RiverBodyProfile.constant(4D, 2D, 4D),
|
||||
true,
|
||||
false,
|
||||
new RiverPolyline(new double[]{-600D, 0D}, new double[]{0D, 0D})
|
||||
@@ -1004,8 +1006,12 @@ public class IrisRiverRuntimeTest {
|
||||
.setBankWidth(range(12D))
|
||||
.setDepth(range(5D))
|
||||
.setMaxIncision(512)
|
||||
.setMeanderStrength(0D)
|
||||
.setMeanderStyle(flat())
|
||||
.setWorms(new KList<>(new IrisRiverWorm()
|
||||
.setSeed(1L)
|
||||
.setTortuosity(0D)
|
||||
.setDetailTortuosity(0D)
|
||||
.setMaxOffset(0D)
|
||||
.setSegments(1)))
|
||||
.setBedRoughness(0D)
|
||||
.setBedRoughnessStyle(flat())
|
||||
.setDryContinuationChance(1D);
|
||||
|
||||
Reference in New Issue
Block a user