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) {
|
public static void msg(String string) {
|
||||||
try {
|
|
||||||
getSender().sendMessage(string);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
try {
|
try {
|
||||||
Iris plugin = instance;
|
Iris plugin = instance;
|
||||||
String tag = plugin == null ? "" : plugin.getTag();
|
|
||||||
ComponentLog.logMarkup(
|
ComponentLog.logMarkup(
|
||||||
plugin,
|
plugin,
|
||||||
Logger.getLogger("Iris"),
|
Logger.getLogger("Iris"),
|
||||||
"[Iris] ",
|
logPrefix(plugin),
|
||||||
Level.INFO,
|
Level.INFO,
|
||||||
tag + string,
|
string,
|
||||||
null);
|
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) {
|
} catch (Throwable inner) {
|
||||||
System.err.println("[Iris] Failed to emit log message: " + inner.getMessage());
|
System.err.println("[Iris] Failed to emit log message: " + inner.getMessage());
|
||||||
inner.printStackTrace(System.err);
|
inner.printStackTrace(System.err);
|
||||||
@@ -561,15 +562,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
|
|
||||||
private static void diagnostic(Level level, String message) {
|
private static void diagnostic(Level level, String message) {
|
||||||
String line = IrisLogging.clean(message);
|
String line = IrisLogging.clean(message);
|
||||||
|
Iris plugin = instance;
|
||||||
ComponentLog.log(
|
ComponentLog.log(
|
||||||
instance,
|
plugin,
|
||||||
Logger.getLogger("Iris"),
|
Logger.getLogger("Iris"),
|
||||||
"[Iris] ",
|
logPrefix(plugin),
|
||||||
level,
|
level,
|
||||||
ComponentText.literal(line),
|
ComponentText.literal(line),
|
||||||
null);
|
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
|
* @return false when the bootstrap was aborted (unsupported server version); the caller
|
||||||
* must bail out of onEnable without touching any further setup.
|
* 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;
|
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
|
* Core states a severity on every message it logs and the modded adapters honour it. Bukkit must preserve
|
||||||
* message used to become a coloured console line, which the server logs at INFO, so no core warning ever
|
* that severity through the shared component logger so diagnostics remain discoverable in logs/latest.log.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
public class IrisDiagnosticLogLevelTest {
|
public class IrisDiagnosticLogLevelTest {
|
||||||
@Test
|
@Test
|
||||||
@@ -26,25 +24,16 @@ public class IrisDiagnosticLogLevelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void informationalAndDebugMessagesStayOnTheConsolePath() {
|
public void informationalAndDebugMessagesStayOnTheInformationalPath() {
|
||||||
assertNull(Iris.diagnosticLevel(LogLevel.INFO));
|
assertNull(Iris.diagnosticLevel(LogLevel.INFO));
|
||||||
assertNull(Iris.diagnosticLevel(LogLevel.DEBUG));
|
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
|
@Test
|
||||||
public void lifecycleNoticesReachThePluginLoggerAtInfo() {
|
public void lifecycleNoticesReachThePluginLoggerAtInfo() {
|
||||||
assertEquals(Level.INFO, Iris.diagnosticLevel(LogLevel.NOTICE));
|
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
|
@Test
|
||||||
public void adapterSideWarningsCarryTheSameSeverityAsCoreWarnings() throws Exception {
|
public void adapterSideWarningsCarryTheSameSeverityAsCoreWarnings() throws Exception {
|
||||||
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java")).replace("\r\n", "\n");
|
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)"));
|
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) {
|
private static String method(String source, String signature) {
|
||||||
int start = source.indexOf(signature);
|
int start = source.indexOf(signature);
|
||||||
assertTrue("method not found: " + signature, start >= 0);
|
assertTrue("method not found: " + signature, start >= 0);
|
||||||
|
|||||||
@@ -111,10 +111,10 @@ final class PackRiverValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (topology != null && terrain != null) {
|
if (topology != null && terrain != null) {
|
||||||
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
|
WormEnvelope wormEnvelope = wormEnvelope(terrain);
|
||||||
int cellSize = integerValue(topology, "cellSize", 512);
|
int cellSize = integerValue(topology, "cellSize", 512);
|
||||||
if (Double.isFinite(meanderStrength) && meanderStrength > cellSize) {
|
if (wormEnvelope.maximumOffset() > cellSize) {
|
||||||
warnings.add(path + ".terrain.meanderStrength exceeds topology.cellSize; reaches may require large cache halos.");
|
warnings.add(path + ".terrain.worms maxOffset exceeds topology.cellSize; reaches may require large cache halos.");
|
||||||
}
|
}
|
||||||
validateTopologyComplexity(packFolder, path, topology, terrain, errors);
|
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, "routingNoiseWeight", 0D, 1024D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "flowAlignmentWeight", 0D, 1024D, errors);
|
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "flowAlignmentWeight", 0D, 1024D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "confluenceWeight", 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, "terrainHeightWeight", 0D, 16D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainSlopeWeight", 0D, 16D, errors);
|
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainSlopeWeight", 0D, 16D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "oceanAttraction", 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, "tunnelMouthBlend", 0D, 16D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelFloorVariation", 0D, 8D, errors);
|
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelFloorVariation", 0D, 8D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelRoofVariation", 0D, 16D, 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.validateOptionalDoubleRange(path, terrain, "bedRoughness", 0D, 8D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalEnum(path, terrain, "terminalMode", TERMINAL_MODES, errors);
|
PackJsonFieldChecks.validateOptionalEnum(path, terrain, "terminalMode", TERMINAL_MODES, errors);
|
||||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "terminalTaper", 8, 1024, errors);
|
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "terminalTaper", 8, 1024, errors);
|
||||||
@@ -189,8 +185,8 @@ final class PackRiverValidator {
|
|||||||
validateNoiseChance(packFolder, terrain, "incision", path, errors);
|
validateNoiseChance(packFolder, terrain, "incision", path, errors);
|
||||||
validateStyle(packFolder, terrain, "tunnelFloorStyle", path, errors);
|
validateStyle(packFolder, terrain, "tunnelFloorStyle", path, errors);
|
||||||
validateStyle(packFolder, terrain, "tunnelRoofStyle", path, errors);
|
validateStyle(packFolder, terrain, "tunnelRoofStyle", path, errors);
|
||||||
validateStyle(packFolder, terrain, "meanderStyle", path, errors);
|
|
||||||
validateStyle(packFolder, terrain, "bedRoughnessStyle", path, errors);
|
validateStyle(packFolder, terrain, "bedRoughnessStyle", path, errors);
|
||||||
|
validateWorms(path, terrain, errors);
|
||||||
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
|
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
|
||||||
double maximumTunnelWidthMultiplier = styledRangeMaximum(
|
double maximumTunnelWidthMultiplier = styledRangeMaximum(
|
||||||
packFolder,
|
packFolder,
|
||||||
@@ -240,8 +236,7 @@ final class PackRiverValidator {
|
|||||||
int tileCells = integerValue(topology, "tileCells", 4);
|
int tileCells = integerValue(topology, "tileCells", 4);
|
||||||
double siteJitter = doubleValue(topology, "siteJitter", 0.35D);
|
double siteJitter = doubleValue(topology, "siteJitter", 0.35D);
|
||||||
int maxRouteReaches = integerValue(topology, "maxRouteReaches", 16);
|
int maxRouteReaches = integerValue(topology, "maxRouteReaches", 16);
|
||||||
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
|
WormEnvelope wormEnvelope = wormEnvelope(terrain);
|
||||||
int meanderSubdivisions = integerValue(terrain, "meanderSubdivisions", 8);
|
|
||||||
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
|
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
|
||||||
double maximumBankWidth = doubleValue(terrain, "maxBankWidth", 4D);
|
double maximumBankWidth = doubleValue(terrain, "maxBankWidth", 4D);
|
||||||
double maximumTunnelWidthMultiplier = styledRangeMaximum(
|
double maximumTunnelWidthMultiplier = styledRangeMaximum(
|
||||||
@@ -256,8 +251,8 @@ final class PackRiverValidator {
|
|||||||
|| tileCells < 1 || tileCells > 64
|
|| tileCells < 1 || tileCells > 64
|
||||||
|| !Double.isFinite(siteJitter) || siteJitter < 0D || siteJitter > 0.49D
|
|| !Double.isFinite(siteJitter) || siteJitter < 0D || siteJitter > 0.49D
|
||||||
|| maxRouteReaches < 1 || maxRouteReaches > 256
|
|| maxRouteReaches < 1 || maxRouteReaches > 256
|
||||||
|| !Double.isFinite(meanderStrength) || meanderStrength < 0D || meanderStrength > 1024D
|
|| wormEnvelope.maximumOffset() < 0D || wormEnvelope.maximumOffset() > 1024D
|
||||||
|| meanderSubdivisions < 1 || meanderSubdivisions > 64
|
|| wormEnvelope.maximumSegments() < 1 || wormEnvelope.maximumSegments() > 64
|
||||||
|| !Double.isFinite(maximumChannelWidth) || maximumChannelWidth < 1D || maximumChannelWidth > 2048D
|
|| !Double.isFinite(maximumChannelWidth) || maximumChannelWidth < 1D || maximumChannelWidth > 2048D
|
||||||
|| !Double.isFinite(maximumBankWidth) || maximumBankWidth < 0D || maximumBankWidth > 2048D
|
|| !Double.isFinite(maximumBankWidth) || maximumBankWidth < 0D || maximumBankWidth > 2048D
|
||||||
|| !Double.isFinite(maximumTunnelWidthMultiplier)
|
|| !Double.isFinite(maximumTunnelWidthMultiplier)
|
||||||
@@ -275,14 +270,187 @@ final class PackRiverValidator {
|
|||||||
siteJitter,
|
siteJitter,
|
||||||
maxRouteReaches,
|
maxRouteReaches,
|
||||||
maximumReachRadius,
|
maximumReachRadius,
|
||||||
meanderStrength,
|
wormEnvelope.maximumOffset(),
|
||||||
meanderSubdivisions
|
wormEnvelope.maximumSegments()
|
||||||
);
|
);
|
||||||
for (String violation : estimate.violations()) {
|
for (String violation : estimate.violations()) {
|
||||||
errors.add(path + " exceeds the safe derived complexity budget. " + violation);
|
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,
|
private static void validateCaves(File packFolder, String path, JSONObject caves,
|
||||||
boolean forceGeneratedGrotto,
|
boolean forceGeneratedGrotto,
|
||||||
List<String> errors, List<String> warnings) {
|
List<String> errors, List<String> warnings) {
|
||||||
@@ -961,6 +1129,9 @@ final class PackRiverValidator {
|
|||||||
return chance == null ? defaultValue : doubleValue(chance, "chance", defaultValue);
|
return chance == null ? defaultValue : doubleValue(chance, "chance", defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record WormEnvelope(double maximumOffset, int maximumSegments) {
|
||||||
|
}
|
||||||
|
|
||||||
record Validation(List<String> errors, List<String> warnings) {
|
record Validation(List<String> errors, List<String> warnings) {
|
||||||
Validation {
|
Validation {
|
||||||
errors = List.copyOf(errors);
|
errors = List.copyOf(errors);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package art.arcane.iris.engine.object;
|
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.Desc;
|
||||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
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.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
@@ -83,18 +86,10 @@ public class IrisRiverTerrain {
|
|||||||
@Desc("The maximum vertical roof variation in river tunnels.")
|
@Desc("The maximum vertical roof variation in river tunnels.")
|
||||||
private double tunnelRoofVariation = 3D;
|
private double tunnelRoofVariation = 3D;
|
||||||
|
|
||||||
@Desc("Warps the spacing and amplitude of varied local-normal sweeps, hooks, curls, and wandering bends while preserving graph endpoints.")
|
@Required
|
||||||
private IrisGeneratorStyle meanderStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(512D);
|
@ArrayType(min = 1, type = IrisRiverWorm.class)
|
||||||
|
@Desc("Weighted root Perlin-worm families with inherited child styles for trunks and tributaries.")
|
||||||
@MinNumber(0)
|
private KList<IrisRiverWorm> worms = new KList<IrisRiverWorm>();
|
||||||
@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;
|
|
||||||
|
|
||||||
@Desc("Modulates small river-bed height variation after the connected channel shape is solved.")
|
@Desc("Modulates small river-bed height variation after the connected channel shape is solved.")
|
||||||
private IrisGeneratorStyle bedRoughnessStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(96D);
|
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.")
|
@Desc("The deterministic attraction toward shared downstream nodes. Larger values form stronger tributary trees and confluences.")
|
||||||
private double confluenceWeight = 0D;
|
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)
|
@MinNumber(0)
|
||||||
@MaxNumber(16)
|
@MaxNumber(16)
|
||||||
@Desc("The contribution of natural terrain height to downstream routing cost.")
|
@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 SOURCE_FLOOR_SALT = 0xD6E8FEB86659FD93L;
|
||||||
private static final long REACH_SALT = 0x9B05688C2B3E6C1FL;
|
private static final long REACH_SALT = 0x9B05688C2B3E6C1FL;
|
||||||
private static final long DRY_SALT = 0x1F83D9ABFB41BD6BL;
|
private static final long DRY_SALT = 0x1F83D9ABFB41BD6BL;
|
||||||
private static final long MEANDER_SALT = 0x5BE0CD19137E2179L;
|
private static final long WORM_FAMILY_SALT = 0x5BE0CD19137E2179L;
|
||||||
private static final long MEANDER_PERSONALITY_SALT = 0x243F6A8885A308D3L;
|
private static final long WORM_CHILD_GATE_SALT = 0x452821E638D01377L;
|
||||||
private static final long MEANDER_AMPLITUDE_SALT = 0x13198A2E03707344L;
|
private static final long WORM_CHILD_SELECTION_SALT = 0xBE5466CF34E90C6CL;
|
||||||
private static final long MEANDER_PHASE_SALT = 0xD1310BA698DFB5ACL;
|
private static final long WORM_PRIMARY_SALT = 0x243F6A8885A308D3L;
|
||||||
private static final long MEANDER_SKEW_SALT = 0x2FFD72DBD01ADFB7L;
|
private static final long WORM_DETAIL_SALT = 0x13198A2E03707344L;
|
||||||
private static final long MEANDER_CYCLE_SALT = 0x452821E638D01377L;
|
private static final long BODY_WIDTH_PRIMARY_SALT = 0xA4093822299F31D0L;
|
||||||
private static final long MEANDER_FEATURE_A_SALT = 0xBE5466CF34E90C6CL;
|
private static final long BODY_WIDTH_DETAIL_SALT = 0x082EFA98EC4E6C89L;
|
||||||
private static final long MEANDER_FEATURE_B_SALT = 0xC0AC29B7C97C50DDL;
|
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 CONFLUENCE_SALT = 0x9E3779B97F4A7C15L;
|
||||||
private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL;
|
private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL;
|
||||||
private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL;
|
private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL;
|
||||||
private static final int MEANDER_NOISE_SAMPLES = 12;
|
private static final int MINIMUM_BODY_PROFILE_SAMPLES = 12;
|
||||||
private static final int WIDTH_PROFILE_SAMPLES = 12;
|
private static final int MAXIMUM_BODY_PROFILE_SAMPLES = 32;
|
||||||
private static final double TWO_PI = StrictMath.PI * 2D;
|
private static final double PERLIN_NORMALIZATION = 1.4142135623730951D;
|
||||||
private static final MeanderPersonality[] MEANDER_PERSONALITIES = MeanderPersonality.values();
|
|
||||||
|
|
||||||
private final RiverNetworkOptions options;
|
private final RiverNetworkOptions options;
|
||||||
|
|
||||||
@@ -208,7 +212,7 @@ public final class RiverNetwork {
|
|||||||
double routingScore = naturalHeight * options.terrainHeightWeight()
|
double routingScore = naturalHeight * options.terrainHeightWeight()
|
||||||
+ routingNoise * options.routingNoiseWeight()
|
+ routingNoise * options.routingNoiseWeight()
|
||||||
+ finiteOrZero(terrainSample.routingCost());
|
+ finiteOrZero(terrainSample.routingCost());
|
||||||
double drainageDistance = drainageDistance(id);
|
double drainageDistance = drainageBasin(id).distance();
|
||||||
double hydraulicHeight = ocean
|
double hydraulicHeight = ocean
|
||||||
? options.hydraulicBaseHeight()
|
? options.hydraulicBaseHeight()
|
||||||
: options.hydraulicBaseHeight()
|
: options.hydraulicBaseHeight()
|
||||||
@@ -228,6 +232,10 @@ public final class RiverNetwork {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double drainageDistance(RiverNodeId id) {
|
private double drainageDistance(RiverNodeId id) {
|
||||||
|
return drainageBasin(id).distance();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DrainageBasin drainageBasin(RiverNodeId id) {
|
||||||
int basinCells = options.routingBasinCells();
|
int basinCells = options.routingBasinCells();
|
||||||
double nodeX = id.cellX() + 0.5D;
|
double nodeX = id.cellX() + 0.5D;
|
||||||
double nodeZ = id.cellZ() + 0.5D;
|
double nodeZ = id.cellZ() + 0.5D;
|
||||||
@@ -245,19 +253,21 @@ public final class RiverNetwork {
|
|||||||
long basinZ = (long) StrictMath.floor(nodeZ / basinCells);
|
long basinZ = (long) StrictMath.floor(nodeZ / basinCells);
|
||||||
double jitterRadius = basinCells * 0.45D;
|
double jitterRadius = basinCells * 0.45D;
|
||||||
double nearestDistance = Double.MAX_VALUE;
|
double nearestDistance = Double.MAX_VALUE;
|
||||||
|
DrainageBasinId nearestId = null;
|
||||||
for (long candidateX = basinX - 1L; candidateX <= basinX + 1L; candidateX++) {
|
for (long candidateX = basinX - 1L; candidateX <= basinX + 1L; candidateX++) {
|
||||||
for (long candidateZ = basinZ - 1L; candidateZ <= basinZ + 1L; candidateZ++) {
|
for (long candidateZ = basinZ - 1L; candidateZ <= basinZ + 1L; candidateZ++) {
|
||||||
double siteX = (candidateX + 0.5D) * basinCells
|
double siteX = (candidateX + 0.5D) * basinCells
|
||||||
+ centered(hash(candidateX, candidateZ, BASIN_X_SALT)) * jitterRadius;
|
+ centered(hash(candidateX, candidateZ, BASIN_X_SALT)) * jitterRadius;
|
||||||
double siteZ = (candidateZ + 0.5D) * basinCells
|
double siteZ = (candidateZ + 0.5D) * basinCells
|
||||||
+ centered(hash(candidateX, candidateZ, BASIN_Z_SALT)) * jitterRadius;
|
+ centered(hash(candidateX, candidateZ, BASIN_Z_SALT)) * jitterRadius;
|
||||||
nearestDistance = StrictMath.min(
|
double distance = StrictMath.hypot(nodeX - siteX, nodeZ - siteZ);
|
||||||
nearestDistance,
|
if (distance < nearestDistance) {
|
||||||
StrictMath.hypot(nodeX - siteX, nodeZ - siteZ)
|
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) {
|
private double smoothCellNoise(double x, double z, int scale, long salt) {
|
||||||
@@ -305,7 +315,8 @@ public final class RiverNetwork {
|
|||||||
if (compareRank(neighbor, node) >= 0) {
|
if (compareRank(neighbor, node) >= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!branchPermitted(node, neighbor, resolver)) {
|
RiverWorm worm = resolver.worm(node, neighbor);
|
||||||
|
if (!branchPermitted(node, neighbor, resolver, worm)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
RiverRoutingContext context = resolver.routingContext(node, neighbor);
|
RiverRoutingContext context = resolver.routingContext(node, neighbor);
|
||||||
@@ -313,7 +324,8 @@ public final class RiverNetwork {
|
|||||||
double oceanAttraction = neighbor.ocean() ? options.oceanAttraction() : 0.0;
|
double oceanAttraction = neighbor.ocean() ? options.oceanAttraction() : 0.0;
|
||||||
double flowAlignmentCost = flowAlignmentCost(node, neighbor, resolver);
|
double flowAlignmentCost = flowAlignmentCost(node, neighbor, resolver);
|
||||||
double confluenceAttraction = unit(hash(neighbor.id(), CONFLUENCE_SALT))
|
double confluenceAttraction = unit(hash(neighbor.id(), CONFLUENCE_SALT))
|
||||||
* options.confluenceWeight();
|
* options.confluenceWeight()
|
||||||
|
* worm.confluenceMultiplier();
|
||||||
ranked.add(new RankedCandidate(
|
ranked.add(new RankedCandidate(
|
||||||
neighbor,
|
neighbor,
|
||||||
neighbor.routingScore() + routingCost + flowAlignmentCost
|
neighbor.routingScore() + routingCost + flowAlignmentCost
|
||||||
@@ -331,15 +343,20 @@ public final class RiverNetwork {
|
|||||||
return List.copyOf(candidates);
|
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());
|
RiverEdgeId childEdge = RiverEdgeId.of(child.id(), parent.id());
|
||||||
int childSlot = resolver.branchSlot(parent, child);
|
int childSlot = resolver.branchSlot(parent, child);
|
||||||
if (childSlot < options.branchSoftCap()) {
|
if (childSlot < worm.branchCap()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
double survivalChance = 1D;
|
double survivalChance = 1D;
|
||||||
for (int overflow = options.branchSoftCap(); overflow <= childSlot; overflow++) {
|
for (int overflow = worm.branchCap(); overflow <= childSlot; overflow++) {
|
||||||
survivalChance *= options.branchChildShrinkFactor();
|
survivalChance *= worm.branchDecay();
|
||||||
}
|
}
|
||||||
return gate(hash(childEdge, BRANCH_GATE_SALT), survivalChance);
|
return gate(hash(childEdge, BRANCH_GATE_SALT), survivalChance);
|
||||||
}
|
}
|
||||||
@@ -427,6 +444,7 @@ public final class RiverNetwork {
|
|||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
resolver.routingContext(from, to),
|
resolver.routingContext(from, to),
|
||||||
|
resolver.worm(from, to),
|
||||||
resolver.terrain
|
resolver.terrain
|
||||||
);
|
);
|
||||||
accumulators.put(edgeId, accumulator);
|
accumulators.put(edgeId, accumulator);
|
||||||
@@ -451,61 +469,65 @@ public final class RiverNetwork {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private RiverPolyline createPolyline(
|
private RiverPolyline createPolyline(
|
||||||
RiverEdgeId id,
|
|
||||||
RiverNode from,
|
RiverNode from,
|
||||||
RiverNode to,
|
RiverNode to,
|
||||||
NodeResolver resolver
|
RiverWorm worm
|
||||||
) {
|
) {
|
||||||
int pointCount = options.meanderSubdivisions() + 1;
|
int pointCount = worm.segments() + 1;
|
||||||
double[] baseX = new double[pointCount];
|
double[] rawX = new double[pointCount];
|
||||||
double[] baseZ = new double[pointCount];
|
double[] rawZ = new double[pointCount];
|
||||||
double deltaX = to.x() - from.x();
|
double deltaX = to.x() - from.x();
|
||||||
double deltaZ = to.z() - from.z();
|
double deltaZ = to.z() - from.z();
|
||||||
double length = StrictMath.hypot(deltaX, deltaZ);
|
double length = StrictMath.hypot(deltaX, deltaZ);
|
||||||
double maximumOffset = StrictMath.min(options.meanderStrength(), length * 0.35);
|
rawX[0] = from.x();
|
||||||
double directionX = length == 0D ? 1D : deltaX / length;
|
rawZ[0] = from.z();
|
||||||
double directionZ = length == 0D ? 0D : deltaZ / length;
|
if (length <= 0D) {
|
||||||
FlowTangent fromTangent = flowTangent(from, directionX, directionZ, resolver);
|
return new RiverPolyline(rawX, rawZ);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
if (maximumOffset <= 0D) {
|
double baseHeading = StrictMath.atan2(deltaZ, deltaX);
|
||||||
return new RiverPolyline(baseX, baseZ);
|
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[] x = new double[pointCount];
|
||||||
double[] z = 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++) {
|
for (int point = 0; point < pointCount; point++) {
|
||||||
double t = (double) point / (pointCount - 1);
|
double t = (double) point / (pointCount - 1);
|
||||||
double tSquared = t * t;
|
double tSquared = t * t;
|
||||||
double envelope = 16D * tSquared * (1D - t) * (1D - t);
|
double envelope = 16D * tSquared * (1D - t) * (1D - t);
|
||||||
double straightX = from.x() + deltaX * t;
|
double straightX = from.x() + deltaX * t;
|
||||||
double straightZ = from.z() + deltaZ * t;
|
double straightZ = from.z() + deltaZ * t;
|
||||||
double baseDisplacement = StrictMath.hypot(baseX[point] - straightX, baseZ[point] - straightZ);
|
double rawBridgeX = rawX[point] - (from.x() + rawDeltaX * t);
|
||||||
double availableOffset = StrictMath.max(0D, maximumOffset - baseDisplacement);
|
double rawBridgeZ = rawZ[point] - (from.z() + rawDeltaZ * t);
|
||||||
double offset = availableOffset * 0.92D * envelope * meanderSignal[point];
|
x[point] = straightX + rawBridgeX * envelope;
|
||||||
FlowTangent normal = localNormal(baseX, baseZ, point);
|
z[point] = straightZ + rawBridgeZ * envelope;
|
||||||
x[point] = baseX[point] + normal.x() * offset;
|
maximumDisplacement = StrictMath.max(
|
||||||
z[point] = baseZ[point] + normal.z() * offset;
|
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();
|
x[0] = from.x();
|
||||||
z[0] = from.z();
|
z[0] = from.z();
|
||||||
@@ -514,222 +536,141 @@ public final class RiverNetwork {
|
|||||||
return new RiverPolyline(x, z);
|
return new RiverPolyline(x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double[] meanderNoise(
|
private RiverWorm rootWormFor(RiverNodeId rootId) {
|
||||||
RiverEdgeId id,
|
return selectWeighted(options.worms(), hash(rootId, WORM_FAMILY_SALT));
|
||||||
double[] baseX,
|
}
|
||||||
double[] baseZ,
|
|
||||||
NodeResolver resolver
|
private RiverWorm childWormFor(
|
||||||
|
RiverWorm parent,
|
||||||
|
RiverNodeId parentId,
|
||||||
|
RiverNodeId childId,
|
||||||
|
int branchSlot
|
||||||
) {
|
) {
|
||||||
int sampleCount = StrictMath.min(MEANDER_NOISE_SAMPLES, baseX.length);
|
if (parent.children().isEmpty()) {
|
||||||
double[] knots = new double[sampleCount];
|
return parent;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
double[] noise = new double[baseX.length];
|
double chance = StrictMath.min(
|
||||||
for (int point = 0; point < noise.length; point++) {
|
1D,
|
||||||
double t = (double) point / (noise.length - 1);
|
parent.childChance()
|
||||||
double position = t * (sampleCount - 1);
|
+ StrictMath.min(7, branchSlot) * parent.branchChildChance()
|
||||||
int lower = (int) StrictMath.floor(position);
|
);
|
||||||
int upper = StrictMath.min(sampleCount - 1, lower + 1);
|
RiverEdgeId edgeId = RiverEdgeId.of(childId, parentId);
|
||||||
double interpolation = smoothStep(position - lower);
|
if (!gate(hash(edgeId, WORM_CHILD_GATE_SALT), chance)) {
|
||||||
noise[point] = knots[lower] + (knots[upper] - knots[lower]) * interpolation;
|
return parent;
|
||||||
}
|
}
|
||||||
return noise;
|
return selectWeighted(parent.children(), hash(edgeId, WORM_CHILD_SELECTION_SALT));
|
||||||
}
|
}
|
||||||
|
|
||||||
private MeanderProfile meanderProfile(RiverEdgeId id) {
|
private RiverWorm selectWeighted(List<RiverWorm> worms, long selectionHash) {
|
||||||
int personalityIndex = (int) StrictMath.floor(
|
double totalWeight = 0D;
|
||||||
unit(hash(id, MEANDER_PERSONALITY_SALT)) * MEANDER_PERSONALITIES.length
|
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[
|
double north = northwest + (northeast - northwest) * fadeX;
|
||||||
StrictMath.min(MEANDER_PERSONALITIES.length - 1, personalityIndex)
|
double south = southwest + (southeast - southwest) * fadeX;
|
||||||
];
|
return StrictMath.max(
|
||||||
double amplitudeRoll = unit(hash(id, MEANDER_AMPLITUDE_SALT));
|
-1D,
|
||||||
double amplitude = personality == MeanderPersonality.QUIET
|
StrictMath.min(1D, (north + (south - north) * fadeZ) * PERLIN_NORMALIZATION)
|
||||||
? 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))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double[] meanderSignal(RiverEdgeId id, MeanderProfile profile, double[] noise) {
|
private double perlinGradient(long latticeX, long latticeZ, double x, double z, long salt) {
|
||||||
int subdivisions = noise.length - 1;
|
return switch ((int) (hash(latticeX, latticeZ, salt) & 7L)) {
|
||||||
for (int point = 0; point < noise.length; point++) {
|
case 0 -> x;
|
||||||
double t = (double) point / subdivisions;
|
case 1 -> -x;
|
||||||
noise[point] = noise[point] * 0.78D + smoothEdgeNoise(id, t * 3.5D) * 0.22D;
|
case 2 -> z;
|
||||||
}
|
case 3 -> -z;
|
||||||
smoothSignal(noise);
|
case 4 -> (x + z) / PERLIN_NORMALIZATION;
|
||||||
double detailCycles = StrictMath.max(0.5D, profile.cycles() + profile.featureB() * 1.5D);
|
case 5 -> (-x + z) / PERLIN_NORMALIZATION;
|
||||||
double baseStep = TWO_PI * detailCycles / subdivisions;
|
case 6 -> (x - z) / PERLIN_NORMALIZATION;
|
||||||
double phase = profile.phase() + profile.featureA() * StrictMath.PI;
|
default -> (-x - z) / PERLIN_NORMALIZATION;
|
||||||
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 detailWeight(MeanderPersonality personality) {
|
private double perlinFade(double value) {
|
||||||
return switch (personality) {
|
double squared = value * value;
|
||||||
case SWEEP, HOOK, OXBOW, QUIET -> 0D;
|
double cubed = squared * value;
|
||||||
case S_CURVE -> 0.12D;
|
return cubed * (value * (value * 6D - 15D) + 10D);
|
||||||
case COIL, WANDER -> 0.08D;
|
|
||||||
case CHIRP -> 0.1D;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double noiseWeight(MeanderPersonality personality) {
|
private double bodyMultiplier(
|
||||||
return switch (personality) {
|
ReachPosition position,
|
||||||
case SWEEP, S_CURVE, COIL, OXBOW, CHIRP -> 0.08D;
|
RiverWorm worm,
|
||||||
case HOOK -> 0.1D;
|
long primarySalt,
|
||||||
case WANDER -> 0.34D;
|
long detailSalt,
|
||||||
case QUIET -> 0.14D;
|
double variation
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
) {
|
) {
|
||||||
FlowTangent preferred = resolver.flowTangent(node);
|
if (variation <= 0D) {
|
||||||
double tangentX = preferred.x();
|
return 1D;
|
||||||
double tangentZ = preferred.z();
|
|
||||||
if (tangentX == 0D && tangentZ == 0D) {
|
|
||||||
return new FlowTangent(fallbackX, fallbackZ);
|
|
||||||
}
|
}
|
||||||
double alignment = tangentX * fallbackX + tangentZ * fallbackZ;
|
return StrictMath.max(
|
||||||
if (alignment < 0D) {
|
0.125D,
|
||||||
tangentX = -tangentX;
|
1D + bodyField(position, worm, primarySalt, detailSalt) * variation
|
||||||
tangentZ = -tangentZ;
|
);
|
||||||
alignment = -alignment;
|
|
||||||
}
|
}
|
||||||
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) {
|
private FlowTangent resolveFlowTangent(RiverNode node, RiverTerrainSampler terrain) {
|
||||||
@@ -771,16 +712,6 @@ public final class RiverNetwork {
|
|||||||
return options.flowAlignmentWeight() * (1D - StrictMath.min(1D, alignment));
|
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(
|
private boolean intersects(
|
||||||
RiverReach reach,
|
RiverReach reach,
|
||||||
long minimumX,
|
long minimumX,
|
||||||
@@ -812,8 +743,8 @@ public final class RiverNetwork {
|
|||||||
long maximumZ
|
long maximumZ
|
||||||
) {
|
) {
|
||||||
double length = StrictMath.hypot(to.x() - from.x(), to.z() - from.z());
|
double length = StrictMath.hypot(to.x() - from.x(), to.z() - from.z());
|
||||||
double maximumMeander = StrictMath.min(options.meanderStrength(), length * 0.35D);
|
double maximumWormOffset = StrictMath.min(options.maximumWormOffset(), length * 0.35D);
|
||||||
double padding = options.maximumReachRadius() + maximumMeander;
|
double padding = options.maximumReachRadius() + maximumWormOffset;
|
||||||
double reachMinimumX = StrictMath.min(from.x(), to.x()) - padding;
|
double reachMinimumX = StrictMath.min(from.x(), to.x()) - padding;
|
||||||
double reachMaximumX = StrictMath.max(from.x(), to.x()) + padding;
|
double reachMaximumX = StrictMath.max(from.x(), to.x()) + padding;
|
||||||
double reachMinimumZ = StrictMath.min(from.z(), to.z()) - padding;
|
double reachMinimumZ = StrictMath.min(from.z(), to.z()) - padding;
|
||||||
@@ -825,8 +756,8 @@ public final class RiverNetwork {
|
|||||||
private int geometryPaddingCells() {
|
private int geometryPaddingCells() {
|
||||||
double maximumEdgeAxisDelta = options.cellSize() * (1D + options.siteJitter());
|
double maximumEdgeAxisDelta = options.cellSize() * (1D + options.siteJitter());
|
||||||
double maximumEdgeLength = StrictMath.sqrt(2D) * maximumEdgeAxisDelta;
|
double maximumEdgeLength = StrictMath.sqrt(2D) * maximumEdgeAxisDelta;
|
||||||
double maximumMeander = StrictMath.min(options.meanderStrength(), maximumEdgeLength * 0.35D);
|
double maximumWormOffset = StrictMath.min(options.maximumWormOffset(), maximumEdgeLength * 0.35D);
|
||||||
double displacement = options.maximumReachRadius() + maximumMeander;
|
double displacement = options.maximumReachRadius() + maximumWormOffset;
|
||||||
return 1 + (int) StrictMath.ceil(displacement / options.cellSize());
|
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<RiverEdgeId, RiverRoutingContext> routingContexts;
|
||||||
private final Map<RiverNodeId, FlowTangent> flowTangents;
|
private final Map<RiverNodeId, FlowTangent> flowTangents;
|
||||||
private final Map<RiverEdgeId, Integer> branchSlots;
|
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> resolvedBranchParents;
|
||||||
|
private final Map<RiverNodeId, Boolean> resolvedStyleBranchParents;
|
||||||
|
|
||||||
private NodeResolver(RiverTerrainSampler terrain) {
|
private NodeResolver(RiverTerrainSampler terrain) {
|
||||||
this.terrain = terrain;
|
this.terrain = terrain;
|
||||||
@@ -932,7 +868,12 @@ public final class RiverNetwork {
|
|||||||
routingContexts = new HashMap<>();
|
routingContexts = new HashMap<>();
|
||||||
flowTangents = new HashMap<>();
|
flowTangents = new HashMap<>();
|
||||||
branchSlots = new HashMap<>();
|
branchSlots = new HashMap<>();
|
||||||
|
styleDistances = new HashMap<>();
|
||||||
|
styleParents = new HashMap<>();
|
||||||
|
styleWorms = new HashMap<>();
|
||||||
|
styleBranchSlots = new HashMap<>();
|
||||||
resolvedBranchParents = new HashMap<>();
|
resolvedBranchParents = new HashMap<>();
|
||||||
|
resolvedStyleBranchParents = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private RiverNode resolve(RiverNodeId id) {
|
private RiverNode resolve(RiverNodeId id) {
|
||||||
@@ -1099,11 +1040,88 @@ public final class RiverNetwork {
|
|||||||
|
|
||||||
private RiverRoutingContext routingContext(RiverNode from, RiverNode to) {
|
private RiverRoutingContext routingContext(RiverNode from, RiverNode to) {
|
||||||
RiverEdgeId edgeId = RiverEdgeId.of(from.id(), to.id());
|
RiverEdgeId edgeId = RiverEdgeId.of(from.id(), to.id());
|
||||||
|
RiverWorm worm = worm(from, to);
|
||||||
return routingContexts.computeIfAbsent(edgeId, ignored -> RiverRoutingContext.lazy(
|
return routingContexts.computeIfAbsent(edgeId, ignored -> RiverRoutingContext.lazy(
|
||||||
edgeId,
|
edgeId,
|
||||||
from,
|
from,
|
||||||
to,
|
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) {
|
private FlowTangent flowTangent(RiverNode node) {
|
||||||
@@ -1122,27 +1140,13 @@ public final class RiverNetwork {
|
|||||||
private record FlowTangent(double x, double z) {
|
private record FlowTangent(double x, double z) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record MeanderProfile(
|
private record DrainageBasinId(long x, long z) {
|
||||||
MeanderPersonality personality,
|
|
||||||
double amplitude,
|
|
||||||
double handedness,
|
|
||||||
double phase,
|
|
||||||
double skew,
|
|
||||||
double cycles,
|
|
||||||
double featureA,
|
|
||||||
double featureB
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum MeanderPersonality {
|
private record DrainageBasin(DrainageBasinId id, double distance) {
|
||||||
QUIET,
|
}
|
||||||
SWEEP,
|
|
||||||
HOOK,
|
private record StyleParent(RiverNodeId id) {
|
||||||
S_CURVE,
|
|
||||||
COIL,
|
|
||||||
WANDER,
|
|
||||||
OXBOW,
|
|
||||||
CHIRP
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private record SourceTileId(long tileX, long tileZ) {
|
private record SourceTileId(long tileX, long tileZ) {
|
||||||
@@ -1156,6 +1160,7 @@ public final class RiverNetwork {
|
|||||||
private final RiverNode from;
|
private final RiverNode from;
|
||||||
private final RiverNode to;
|
private final RiverNode to;
|
||||||
private final RiverRoutingContext context;
|
private final RiverRoutingContext context;
|
||||||
|
private final RiverWorm worm;
|
||||||
private final RiverTerrainSampler terrain;
|
private final RiverTerrainSampler terrain;
|
||||||
private int wetFlow;
|
private int wetFlow;
|
||||||
private int dryFlow;
|
private int dryFlow;
|
||||||
@@ -1167,12 +1172,14 @@ public final class RiverNetwork {
|
|||||||
RiverNode from,
|
RiverNode from,
|
||||||
RiverNode to,
|
RiverNode to,
|
||||||
RiverRoutingContext context,
|
RiverRoutingContext context,
|
||||||
|
RiverWorm worm,
|
||||||
RiverTerrainSampler terrain
|
RiverTerrainSampler terrain
|
||||||
) {
|
) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.from = from;
|
this.from = from;
|
||||||
this.to = to;
|
this.to = to;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.worm = worm;
|
||||||
this.terrain = terrain;
|
this.terrain = terrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1193,21 +1200,7 @@ public final class RiverNetwork {
|
|||||||
private RiverReach build() {
|
private RiverReach build() {
|
||||||
int flow = wetFlow + dryFlow;
|
int flow = wetFlow + dryFlow;
|
||||||
int order = 1 + (31 - Integer.numberOfLeadingZeros(flow));
|
int order = 1 + (31 - Integer.numberOfLeadingZeros(flow));
|
||||||
double bankWidth = nonNegativeOrFallback(
|
RiverBodyProfile bodyProfile = bodyProfile(order, worm);
|
||||||
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))
|
|
||||||
);
|
|
||||||
RiverRouteState state = wetFlow > 0 ? RiverRouteState.WET : RiverRouteState.DRY;
|
RiverRouteState state = wetFlow > 0 ? RiverRouteState.WET : RiverRouteState.DRY;
|
||||||
return new RiverReach(
|
return new RiverReach(
|
||||||
id,
|
id,
|
||||||
@@ -1216,10 +1209,10 @@ public final class RiverNetwork {
|
|||||||
state,
|
state,
|
||||||
flow,
|
flow,
|
||||||
order,
|
order,
|
||||||
width,
|
bodyProfile.maximumWidth(),
|
||||||
widthProfile,
|
bodyProfile.maximumBankWidth(),
|
||||||
bankWidth,
|
bodyProfile.maximumDepth(),
|
||||||
depth,
|
bodyProfile,
|
||||||
state == RiverRouteState.WET && to.ocean(),
|
state == RiverRouteState.WET && to.ocean(),
|
||||||
state == RiverRouteState.WET
|
state == RiverRouteState.WET
|
||||||
? terminalWetFlow == wetFlow
|
? 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();
|
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[] positions = new double[sampleCount];
|
||||||
double[] widths = 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++) {
|
for (int index = 0; index < sampleCount; index++) {
|
||||||
double alongReach = (double) index / (sampleCount - 1);
|
double alongReach = (double) index / (sampleCount - 1);
|
||||||
ReachPosition position = positionAt(polyline, alongReach);
|
ReachPosition position = positionAt(polyline, alongReach);
|
||||||
@@ -1249,10 +1251,59 @@ public final class RiverNetwork {
|
|||||||
positions[index] = alongReach;
|
positions[index] = alongReach;
|
||||||
widths[index] = StrictMath.min(
|
widths[index] = StrictMath.min(
|
||||||
options.maxChannelWidth(),
|
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;
|
package art.arcane.iris.engine.river;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public record RiverNetworkOptions(
|
public record RiverNetworkOptions(
|
||||||
long seed,
|
long seed,
|
||||||
int cellSize,
|
int cellSize,
|
||||||
@@ -21,8 +25,6 @@ public record RiverNetworkOptions(
|
|||||||
double routingNoiseWeight,
|
double routingNoiseWeight,
|
||||||
double flowAlignmentWeight,
|
double flowAlignmentWeight,
|
||||||
double confluenceWeight,
|
double confluenceWeight,
|
||||||
int branchSoftCap,
|
|
||||||
double branchChildShrinkFactor,
|
|
||||||
double oceanAttraction,
|
double oceanAttraction,
|
||||||
double channelWidth,
|
double channelWidth,
|
||||||
double bankWidth,
|
double bankWidth,
|
||||||
@@ -33,8 +35,7 @@ public record RiverNetworkOptions(
|
|||||||
double orderWidthFactor,
|
double orderWidthFactor,
|
||||||
double orderDepthFactor,
|
double orderDepthFactor,
|
||||||
double maximumReachRadius,
|
double maximumReachRadius,
|
||||||
double meanderStrength,
|
List<RiverWorm> worms
|
||||||
int meanderSubdivisions
|
|
||||||
) {
|
) {
|
||||||
public RiverNetworkOptions {
|
public RiverNetworkOptions {
|
||||||
requireRange(cellSize, 8, 4096, "cellSize");
|
requireRange(cellSize, 8, 4096, "cellSize");
|
||||||
@@ -47,7 +48,6 @@ public record RiverNetworkOptions(
|
|||||||
requireRange(routingDeviationStrengthCells, 0D, 32D, "routingDeviationStrengthCells");
|
requireRange(routingDeviationStrengthCells, 0D, 32D, "routingDeviationStrengthCells");
|
||||||
requirePositive(routingPlateauHeight, "routingPlateauHeight");
|
requirePositive(routingPlateauHeight, "routingPlateauHeight");
|
||||||
requireFinite(hydraulicBaseHeight, "hydraulicBaseHeight");
|
requireFinite(hydraulicBaseHeight, "hydraulicBaseHeight");
|
||||||
requireRange(meanderSubdivisions, 1, 64, "meanderSubdivisions");
|
|
||||||
requireProbability(siteJitter, "siteJitter");
|
requireProbability(siteJitter, "siteJitter");
|
||||||
requireProbability(sourceChance, "sourceChance");
|
requireProbability(sourceChance, "sourceChance");
|
||||||
requireProbability(reachChance, "reachChance");
|
requireProbability(reachChance, "reachChance");
|
||||||
@@ -56,8 +56,6 @@ public record RiverNetworkOptions(
|
|||||||
requireFiniteNonNegative(routingNoiseWeight, "routingNoiseWeight");
|
requireFiniteNonNegative(routingNoiseWeight, "routingNoiseWeight");
|
||||||
requireFiniteNonNegative(flowAlignmentWeight, "flowAlignmentWeight");
|
requireFiniteNonNegative(flowAlignmentWeight, "flowAlignmentWeight");
|
||||||
requireFiniteNonNegative(confluenceWeight, "confluenceWeight");
|
requireFiniteNonNegative(confluenceWeight, "confluenceWeight");
|
||||||
requireRange(branchSoftCap, 1, 8, "branchSoftCap");
|
|
||||||
requireProbability(branchChildShrinkFactor, "branchChildShrinkFactor");
|
|
||||||
requireFiniteNonNegative(oceanAttraction, "oceanAttraction");
|
requireFiniteNonNegative(oceanAttraction, "oceanAttraction");
|
||||||
requirePositive(channelWidth, "channelWidth");
|
requirePositive(channelWidth, "channelWidth");
|
||||||
requireFiniteNonNegative(bankWidth, "bankWidth");
|
requireFiniteNonNegative(bankWidth, "bankWidth");
|
||||||
@@ -68,15 +66,27 @@ public record RiverNetworkOptions(
|
|||||||
requireFiniteNonNegative(orderWidthFactor, "orderWidthFactor");
|
requireFiniteNonNegative(orderWidthFactor, "orderWidthFactor");
|
||||||
requireFiniteNonNegative(orderDepthFactor, "orderDepthFactor");
|
requireFiniteNonNegative(orderDepthFactor, "orderDepthFactor");
|
||||||
requireFiniteNonNegative(maximumReachRadius, "maximumReachRadius");
|
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(
|
RiverTopologyComplexity.requireSafe(
|
||||||
cellSize,
|
cellSize,
|
||||||
tileCells,
|
tileCells,
|
||||||
siteJitter,
|
siteJitter,
|
||||||
maxRouteReaches,
|
maxRouteReaches,
|
||||||
maximumReachRadius,
|
maximumReachRadius,
|
||||||
meanderStrength,
|
maximumWormOffset(worms),
|
||||||
meanderSubdivisions
|
maximumWormSegments(worms)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +94,66 @@ public record RiverNetworkOptions(
|
|||||||
return new Builder(seed);
|
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) {
|
private static void requireRange(int value, int minimum, int maximum, String name) {
|
||||||
if (value < minimum || value > maximum) {
|
if (value < minimum || value > maximum) {
|
||||||
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
|
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
|
||||||
@@ -141,8 +211,6 @@ public record RiverNetworkOptions(
|
|||||||
private double routingNoiseWeight;
|
private double routingNoiseWeight;
|
||||||
private double flowAlignmentWeight;
|
private double flowAlignmentWeight;
|
||||||
private double confluenceWeight;
|
private double confluenceWeight;
|
||||||
private int branchSoftCap;
|
|
||||||
private double branchChildShrinkFactor;
|
|
||||||
private double oceanAttraction;
|
private double oceanAttraction;
|
||||||
private double channelWidth;
|
private double channelWidth;
|
||||||
private double bankWidth;
|
private double bankWidth;
|
||||||
@@ -153,8 +221,7 @@ public record RiverNetworkOptions(
|
|||||||
private double orderWidthFactor;
|
private double orderWidthFactor;
|
||||||
private double orderDepthFactor;
|
private double orderDepthFactor;
|
||||||
private double maximumReachRadius;
|
private double maximumReachRadius;
|
||||||
private double meanderStrength;
|
private List<RiverWorm> worms;
|
||||||
private int meanderSubdivisions;
|
|
||||||
|
|
||||||
private Builder(long seed) {
|
private Builder(long seed) {
|
||||||
this.seed = seed;
|
this.seed = seed;
|
||||||
@@ -177,8 +244,6 @@ public record RiverNetworkOptions(
|
|||||||
routingNoiseWeight = 24.0;
|
routingNoiseWeight = 24.0;
|
||||||
flowAlignmentWeight = 0D;
|
flowAlignmentWeight = 0D;
|
||||||
confluenceWeight = 0D;
|
confluenceWeight = 0D;
|
||||||
branchSoftCap = 4;
|
|
||||||
branchChildShrinkFactor = 0.35D;
|
|
||||||
oceanAttraction = 64.0;
|
oceanAttraction = 64.0;
|
||||||
channelWidth = 10.0;
|
channelWidth = 10.0;
|
||||||
bankWidth = 8.0;
|
bankWidth = 8.0;
|
||||||
@@ -189,8 +254,32 @@ public record RiverNetworkOptions(
|
|||||||
orderWidthFactor = 0.35;
|
orderWidthFactor = 0.35;
|
||||||
orderDepthFactor = 0.2;
|
orderDepthFactor = 0.2;
|
||||||
maximumReachRadius = Double.NaN;
|
maximumReachRadius = Double.NaN;
|
||||||
meanderStrength = 40.0;
|
worms = List.of(new RiverWorm(
|
||||||
meanderSubdivisions = 8;
|
"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) {
|
public Builder cellSize(int value) {
|
||||||
@@ -288,16 +377,6 @@ public record RiverNetworkOptions(
|
|||||||
return this;
|
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) {
|
public Builder oceanAttraction(double value) {
|
||||||
oceanAttraction = value;
|
oceanAttraction = value;
|
||||||
return this;
|
return this;
|
||||||
@@ -348,13 +427,8 @@ public record RiverNetworkOptions(
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder meanderStrength(double value) {
|
public Builder worms(List<RiverWorm> value) {
|
||||||
meanderStrength = value;
|
worms = value;
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder meanderSubdivisions(int value) {
|
|
||||||
meanderSubdivisions = value;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,8 +457,6 @@ public record RiverNetworkOptions(
|
|||||||
routingNoiseWeight,
|
routingNoiseWeight,
|
||||||
flowAlignmentWeight,
|
flowAlignmentWeight,
|
||||||
confluenceWeight,
|
confluenceWeight,
|
||||||
branchSoftCap,
|
|
||||||
branchChildShrinkFactor,
|
|
||||||
oceanAttraction,
|
oceanAttraction,
|
||||||
channelWidth,
|
channelWidth,
|
||||||
bankWidth,
|
bankWidth,
|
||||||
@@ -395,8 +467,7 @@ public record RiverNetworkOptions(
|
|||||||
orderWidthFactor,
|
orderWidthFactor,
|
||||||
orderDepthFactor,
|
orderDepthFactor,
|
||||||
resolvedMaximumReachRadius,
|
resolvedMaximumReachRadius,
|
||||||
meanderStrength,
|
worms
|
||||||
meanderSubdivisions
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ public record RiverReach(
|
|||||||
int flow,
|
int flow,
|
||||||
int order,
|
int order,
|
||||||
double width,
|
double width,
|
||||||
RiverWidthProfile widthProfile,
|
|
||||||
double bankWidth,
|
double bankWidth,
|
||||||
double depth,
|
double depth,
|
||||||
|
RiverBodyProfile bodyProfile,
|
||||||
boolean mouth,
|
boolean mouth,
|
||||||
boolean terminal,
|
boolean terminal,
|
||||||
RiverPolyline polyline
|
RiverPolyline polyline
|
||||||
@@ -22,7 +22,7 @@ public record RiverReach(
|
|||||||
Objects.requireNonNull(from);
|
Objects.requireNonNull(from);
|
||||||
Objects.requireNonNull(to);
|
Objects.requireNonNull(to);
|
||||||
Objects.requireNonNull(state);
|
Objects.requireNonNull(state);
|
||||||
Objects.requireNonNull(widthProfile);
|
Objects.requireNonNull(bodyProfile);
|
||||||
Objects.requireNonNull(polyline);
|
Objects.requireNonNull(polyline);
|
||||||
if (state == RiverRouteState.SUPPRESSED) {
|
if (state == RiverRouteState.SUPPRESSED) {
|
||||||
throw new IllegalArgumentException("Suppressed routes cannot produce reaches");
|
throw new IllegalArgumentException("Suppressed routes cannot produce reaches");
|
||||||
@@ -34,12 +34,26 @@ public record RiverReach(
|
|||||||
|| !Double.isFinite(depth) || depth <= 0.0) {
|
|| !Double.isFinite(depth) || depth <= 0.0) {
|
||||||
throw new IllegalArgumentException("River reach dimensions must be finite and valid");
|
throw new IllegalArgumentException("River reach dimensions must be finite and valid");
|
||||||
}
|
}
|
||||||
if (Double.compare(width, widthProfile.maximum()) != 0) {
|
if (Double.compare(width, bodyProfile.maximumWidth()) != 0
|
||||||
throw new IllegalArgumentException("River reach width must equal its profile maximum");
|
|| 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) {
|
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;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
default double meanderNoise(RiverMeanderContext context) {
|
|
||||||
return Double.NaN;
|
|
||||||
}
|
|
||||||
|
|
||||||
default double flowNoise(double x, double z) {
|
default double flowNoise(double x, double z) {
|
||||||
return Double.NaN;
|
return Double.NaN;
|
||||||
}
|
}
|
||||||
@@ -75,10 +71,28 @@ public interface RiverTerrainSampler {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default double bankWidth(
|
||||||
|
RiverRoutingContext context,
|
||||||
|
double x,
|
||||||
|
double z,
|
||||||
|
double fallback
|
||||||
|
) {
|
||||||
|
return bankWidth(context, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
default double depth(RiverRoutingContext context, double fallback) {
|
default double depth(RiverRoutingContext context, double fallback) {
|
||||||
return 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) {
|
default RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
||||||
return RiverTerminalPolicy.INHERIT;
|
return RiverTerminalPolicy.INHERIT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,9 +210,11 @@ public final class RiverTile {
|
|||||||
|
|
||||||
double distance = StrictMath.sqrt(nearestDistanceSquared);
|
double distance = StrictMath.sqrt(nearestDistanceSquared);
|
||||||
double localWidth = nearestReach.widthAt(nearestAlongReach);
|
double localWidth = nearestReach.widthAt(nearestAlongReach);
|
||||||
|
double localBankWidth = nearestReach.bankWidthAt(nearestAlongReach);
|
||||||
|
double localDepth = nearestReach.depthAt(nearestAlongReach);
|
||||||
double channelRadius = localWidth * 0.5;
|
double channelRadius = localWidth * 0.5;
|
||||||
RiverSection section = section(nearestReach, distance, channelRadius);
|
RiverSection section = section(nearestReach, distance, channelRadius);
|
||||||
double carveWeight = carveWeight(distance, channelRadius, nearestReach.bankWidth());
|
double carveWeight = carveWeight(distance, channelRadius, localBankWidth);
|
||||||
return new RiverSample(
|
return new RiverSample(
|
||||||
true,
|
true,
|
||||||
nearestReach.state(),
|
nearestReach.state(),
|
||||||
@@ -223,8 +225,8 @@ public final class RiverTile {
|
|||||||
nearestReach.flow(),
|
nearestReach.flow(),
|
||||||
nearestReach.order(),
|
nearestReach.order(),
|
||||||
localWidth,
|
localWidth,
|
||||||
nearestReach.bankWidth(),
|
localBankWidth,
|
||||||
nearestReach.depth(),
|
localDepth,
|
||||||
nearestReach.terminal(),
|
nearestReach.terminal(),
|
||||||
nearestReach.id()
|
nearestReach.id()
|
||||||
);
|
);
|
||||||
@@ -325,7 +327,7 @@ public final class RiverTile {
|
|||||||
RiverPolyline polyline = reach.polyline();
|
RiverPolyline polyline = reach.polyline();
|
||||||
if (polyline.length() == 0D) {
|
if (polyline.length() == 0D) {
|
||||||
double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0));
|
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;
|
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
|
||||||
}
|
}
|
||||||
double nearest = Double.POSITIVE_INFINITY;
|
double nearest = Double.POSITIVE_INFINITY;
|
||||||
@@ -339,9 +341,9 @@ public final class RiverTile {
|
|||||||
}
|
}
|
||||||
double deltaX = polyline.x(point + 1) - polyline.x(point);
|
double deltaX = polyline.x(point + 1) - polyline.x(point);
|
||||||
double deltaZ = polyline.z(point + 1) - polyline.z(point);
|
double deltaZ = polyline.z(point + 1) - polyline.z(point);
|
||||||
for (int profileIndex = 0; profileIndex < reach.widthProfile().size() - 1; profileIndex++) {
|
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
|
||||||
double profileStart = reach.widthProfile().position(profileIndex);
|
double profileStart = reach.bodyProfile().position(profileIndex);
|
||||||
double profileEnd = reach.widthProfile().position(profileIndex + 1);
|
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
|
||||||
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
||||||
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
||||||
if (overlapStart > overlapEnd) {
|
if (overlapStart > overlapEnd) {
|
||||||
@@ -349,13 +351,16 @@ public final class RiverTile {
|
|||||||
}
|
}
|
||||||
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
||||||
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
||||||
double widthSlope = (reach.widthProfile().width(profileIndex + 1)
|
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
|
||||||
- reach.widthProfile().width(profileIndex)) / (profileEnd - profileStart);
|
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||||
double radiusBase = (reach.widthProfile().width(profileIndex)
|
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
|
||||||
|
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||||
|
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
|
||||||
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
|
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
|
||||||
+ reach.bankWidth()
|
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
|
||||||
|
+ bankSlope * (segmentStartAlong - profileStart)
|
||||||
+ additionalRadius;
|
+ additionalRadius;
|
||||||
double radiusSlope = widthSlope * segmentAlongSpan * 0.5D;
|
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
|
||||||
ClosestPoint candidate = coveringPoint(
|
ClosestPoint candidate = coveringPoint(
|
||||||
intervalStart,
|
intervalStart,
|
||||||
intervalEnd,
|
intervalEnd,
|
||||||
@@ -394,7 +399,7 @@ public final class RiverTile {
|
|||||||
maximumX,
|
maximumX,
|
||||||
maximumZ
|
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;
|
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
|
||||||
}
|
}
|
||||||
double nearest = Double.POSITIVE_INFINITY;
|
double nearest = Double.POSITIVE_INFINITY;
|
||||||
@@ -410,9 +415,9 @@ public final class RiverTile {
|
|||||||
double startZ = polyline.z(point);
|
double startZ = polyline.z(point);
|
||||||
double deltaX = polyline.x(point + 1) - startX;
|
double deltaX = polyline.x(point + 1) - startX;
|
||||||
double deltaZ = polyline.z(point + 1) - startZ;
|
double deltaZ = polyline.z(point + 1) - startZ;
|
||||||
for (int profileIndex = 0; profileIndex < reach.widthProfile().size() - 1; profileIndex++) {
|
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
|
||||||
double profileStart = reach.widthProfile().position(profileIndex);
|
double profileStart = reach.bodyProfile().position(profileIndex);
|
||||||
double profileEnd = reach.widthProfile().position(profileIndex + 1);
|
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
|
||||||
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
||||||
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
||||||
if (overlapStart > overlapEnd) {
|
if (overlapStart > overlapEnd) {
|
||||||
@@ -420,11 +425,15 @@ public final class RiverTile {
|
|||||||
}
|
}
|
||||||
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
||||||
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
||||||
double widthSlope = (reach.widthProfile().width(profileIndex + 1)
|
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
|
||||||
- reach.widthProfile().width(profileIndex)) / (profileEnd - profileStart);
|
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||||
double radiusBase = (reach.widthProfile().width(profileIndex)
|
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
|
||||||
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D + reach.bankWidth();
|
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||||
double radiusSlope = widthSlope * segmentAlongSpan * 0.5D;
|
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;
|
double cursor = intervalStart;
|
||||||
do {
|
do {
|
||||||
double next = intervalEnd;
|
double next = intervalEnd;
|
||||||
|
|||||||
@@ -19,14 +19,12 @@ public final class RiverTopologyComplexity {
|
|||||||
double siteJitter,
|
double siteJitter,
|
||||||
int maxRouteReaches,
|
int maxRouteReaches,
|
||||||
double maximumReachRadius,
|
double maximumReachRadius,
|
||||||
double meanderStrength,
|
double maximumWormOffset,
|
||||||
int meanderSubdivisions
|
int maximumWormSegments
|
||||||
) {
|
) {
|
||||||
double maximumEdgeAxisDelta = cellSize * (1D + siteJitter);
|
double maximumEdgeAxisDelta = cellSize * (1D + siteJitter);
|
||||||
double maximumEdgeLength = StrictMath.sqrt(2D) * maximumEdgeAxisDelta;
|
|
||||||
double maximumMeander = StrictMath.min(meanderStrength, maximumEdgeLength * 0.35D);
|
|
||||||
long geometryPaddingCells = 1L + ceilToLong(
|
long geometryPaddingCells = 1L + ceilToLong(
|
||||||
(maximumReachRadius + maximumMeander) / cellSize
|
(maximumReachRadius + maximumWormOffset) / cellSize
|
||||||
);
|
);
|
||||||
long targetWindowAxis = saturatedAdd(tileCells, saturatedMultiply(2L, geometryPaddingCells));
|
long targetWindowAxis = saturatedAdd(tileCells, saturatedMultiply(2L, geometryPaddingCells));
|
||||||
long sourceWindowAxis = saturatedAdd(
|
long sourceWindowAxis = saturatedAdd(
|
||||||
@@ -36,7 +34,7 @@ public final class RiverTopologyComplexity {
|
|||||||
long sourceWindowCells = saturatedMultiply(sourceWindowAxis, sourceWindowAxis);
|
long sourceWindowCells = saturatedMultiply(sourceWindowAxis, sourceWindowAxis);
|
||||||
long maximumRouteScanSteps = saturatedMultiply(sourceWindowCells, maxRouteReaches);
|
long maximumRouteScanSteps = saturatedMultiply(sourceWindowCells, maxRouteReaches);
|
||||||
double maximumSegmentSpan = maximumEdgeAxisDelta
|
double maximumSegmentSpan = maximumEdgeAxisDelta
|
||||||
+ maximumMeander * 2D
|
+ maximumWormOffset * 2D
|
||||||
+ maximumReachRadius * 2D;
|
+ maximumReachRadius * 2D;
|
||||||
long maximumSegmentBucketAxis = saturatedAdd(
|
long maximumSegmentBucketAxis = saturatedAdd(
|
||||||
ceilToLong(maximumSegmentSpan / SPATIAL_BUCKET_SIZE),
|
ceilToLong(maximumSegmentSpan / SPATIAL_BUCKET_SIZE),
|
||||||
@@ -48,7 +46,7 @@ public final class RiverTopologyComplexity {
|
|||||||
);
|
);
|
||||||
long maximumBucketWritesPerReach = saturatedMultiply(
|
long maximumBucketWritesPerReach = saturatedMultiply(
|
||||||
maximumSegmentBucketCount,
|
maximumSegmentBucketCount,
|
||||||
meanderSubdivisions
|
maximumWormSegments
|
||||||
);
|
);
|
||||||
return new Estimate(
|
return new Estimate(
|
||||||
geometryPaddingCells,
|
geometryPaddingCells,
|
||||||
@@ -66,8 +64,8 @@ public final class RiverTopologyComplexity {
|
|||||||
double siteJitter,
|
double siteJitter,
|
||||||
int maxRouteReaches,
|
int maxRouteReaches,
|
||||||
double maximumReachRadius,
|
double maximumReachRadius,
|
||||||
double meanderStrength,
|
double maximumWormOffset,
|
||||||
int meanderSubdivisions
|
int maximumWormSegments
|
||||||
) {
|
) {
|
||||||
Estimate estimate = estimate(
|
Estimate estimate = estimate(
|
||||||
cellSize,
|
cellSize,
|
||||||
@@ -75,8 +73,8 @@ public final class RiverTopologyComplexity {
|
|||||||
siteJitter,
|
siteJitter,
|
||||||
maxRouteReaches,
|
maxRouteReaches,
|
||||||
maximumReachRadius,
|
maximumReachRadius,
|
||||||
meanderStrength,
|
maximumWormOffset,
|
||||||
meanderSubdivisions
|
maximumWormSegments
|
||||||
);
|
);
|
||||||
List<String> violations = estimate.violations();
|
List<String> violations = estimate.violations();
|
||||||
if (!violations.isEmpty()) {
|
if (!violations.isEmpty()) {
|
||||||
@@ -205,7 +203,7 @@ public final class RiverTopologyComplexity {
|
|||||||
+ " bucket writes for one reach (" + maximumSegmentBucketAxis
|
+ " bucket writes for one reach (" + maximumSegmentBucketAxis
|
||||||
+ " buckets per segment axis), above the safe limit of "
|
+ " buckets per segment axis), above the safe limit of "
|
||||||
+ MAXIMUM_BUCKET_WRITES_PER_REACH
|
+ 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);
|
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.IrisRiverTopology;
|
||||||
import art.arcane.iris.engine.object.IrisRiverWater;
|
import art.arcane.iris.engine.object.IrisRiverWater;
|
||||||
import art.arcane.iris.engine.object.IrisRiverWaterMode;
|
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.IrisStyledRange;
|
||||||
import art.arcane.iris.engine.object.NoiseStyle;
|
import art.arcane.iris.engine.object.NoiseStyle;
|
||||||
import art.arcane.iris.engine.river.RiverAnchor;
|
import art.arcane.iris.engine.river.RiverAnchor;
|
||||||
import art.arcane.iris.engine.river.RiverEdgeId;
|
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.RiverNetworkOptions;
|
||||||
import art.arcane.iris.engine.river.RiverPolyline;
|
import art.arcane.iris.engine.river.RiverPolyline;
|
||||||
import art.arcane.iris.engine.river.RiverReach;
|
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.RiverTile;
|
||||||
import art.arcane.iris.engine.river.RiverTileCache;
|
import art.arcane.iris.engine.river.RiverTileCache;
|
||||||
import art.arcane.iris.engine.river.RiverTopologyComplexity;
|
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.interpolation.NoiseBounds;
|
||||||
import art.arcane.iris.util.project.noise.CNG;
|
import art.arcane.iris.util.project.noise.CNG;
|
||||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
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 WIDTH_NOISE_SALT = 0xBE5466CF34E90C6CL;
|
||||||
private static final long BANK_NOISE_SALT = 0xC0AC29B7C97C50DDL;
|
private static final long BANK_NOISE_SALT = 0xC0AC29B7C97C50DDL;
|
||||||
private static final long DEPTH_NOISE_SALT = 0x3F84D5B5B5470917L;
|
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 BED_NOISE_SALT = 0xD1310BA698DFB5ACL;
|
||||||
private static final long BIOME_NOISE_SALT = 0x2FFD72DBD01ADFB7L;
|
private static final long BIOME_NOISE_SALT = 0x2FFD72DBD01ADFB7L;
|
||||||
private static final long CAVE_ENTRY_NOISE_SALT = 0xB8E1AFED6A267E96L;
|
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 widthNoise;
|
||||||
private final CNG bankNoise;
|
private final CNG bankNoise;
|
||||||
private final CNG depthNoise;
|
private final CNG depthNoise;
|
||||||
private final CNG meanderNoise;
|
|
||||||
private final CNG bedNoise;
|
private final CNG bedNoise;
|
||||||
private final CNG biomeNoise;
|
private final CNG biomeNoise;
|
||||||
private final CNG caveEntryNoise;
|
private final CNG caveEntryNoise;
|
||||||
@@ -144,7 +143,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
widthNoise = noise(terrain.getChannelWidth(), WIDTH_NOISE_SALT);
|
widthNoise = noise(terrain.getChannelWidth(), WIDTH_NOISE_SALT);
|
||||||
bankNoise = noise(terrain.getBankWidth(), BANK_NOISE_SALT);
|
bankNoise = noise(terrain.getBankWidth(), BANK_NOISE_SALT);
|
||||||
depthNoise = noise(terrain.getDepth(), DEPTH_NOISE_SALT);
|
depthNoise = noise(terrain.getDepth(), DEPTH_NOISE_SALT);
|
||||||
meanderNoise = noise(terrain.getMeanderStyle(), MEANDER_NOISE_SALT);
|
|
||||||
bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT);
|
bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT);
|
||||||
biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT);
|
biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT);
|
||||||
caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_NOISE_SALT);
|
caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_NOISE_SALT);
|
||||||
@@ -256,7 +254,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
z
|
z
|
||||||
);
|
);
|
||||||
int ceilingY = shapedTunnelCeilingY(
|
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);
|
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());
|
IrisBiome sampledBiome = naturalBiome.get(center.x(), center.z());
|
||||||
double head = waterSurface(reach, alongReach, isNaturalOcean(sampledBiome));
|
double head = waterSurface(reach, alongReach, isNaturalOcean(sampledBiome));
|
||||||
double centerNaturalHeight = naturalHeight.get(center.x(), center.z());
|
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);
|
EffectiveRiverSettings settings = settingsFor(sampledRegion, sampledBiome);
|
||||||
double maximumIncision = Math.max(0D, terrain.getMaxIncision() * settings.maxIncisionMultiplier());
|
double maximumIncision = Math.max(0D, terrain.getMaxIncision() * settings.maxIncisionMultiplier());
|
||||||
double cappedSurface = incisedHeight(centerNaturalHeight, centerBedHeight, 1D, maximumIncision);
|
double cappedSurface = incisedHeight(centerNaturalHeight, centerBedHeight, 1D, maximumIncision);
|
||||||
@@ -627,8 +629,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
.routingNoiseWeight(0D)
|
.routingNoiseWeight(0D)
|
||||||
.flowAlignmentWeight(topology.getFlowAlignmentWeight())
|
.flowAlignmentWeight(topology.getFlowAlignmentWeight())
|
||||||
.confluenceWeight(topology.getConfluenceWeight())
|
.confluenceWeight(topology.getConfluenceWeight())
|
||||||
.branchSoftCap(topology.getBranchSoftCap())
|
|
||||||
.branchChildShrinkFactor(topology.getBranchChildShrinkFactor())
|
|
||||||
.oceanAttraction(topology.getOceanAttraction())
|
.oceanAttraction(topology.getOceanAttraction())
|
||||||
.channelWidth(mid(riverTerrain.getChannelWidth(), 12D))
|
.channelWidth(mid(riverTerrain.getChannelWidth(), 12D))
|
||||||
.bankWidth(mid(riverTerrain.getBankWidth(), 8D))
|
.bankWidth(mid(riverTerrain.getBankWidth(), 8D))
|
||||||
@@ -639,11 +639,63 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
.orderWidthFactor(riverTerrain.getOrderWidthFactor())
|
.orderWidthFactor(riverTerrain.getOrderWidthFactor())
|
||||||
.orderDepthFactor(riverTerrain.getOrderDepthFactor())
|
.orderDepthFactor(riverTerrain.getOrderDepthFactor())
|
||||||
.maximumReachRadius(maximumReachRadius(topology, riverTerrain))
|
.maximumReachRadius(maximumReachRadius(topology, riverTerrain))
|
||||||
.meanderStrength(riverTerrain.getMeanderStrength())
|
.worms(worms(riverTerrain))
|
||||||
.meanderSubdivisions(riverTerrain.getMeanderSubdivisions())
|
|
||||||
.build();
|
.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(
|
private void addTerminalCaveAnchors(
|
||||||
RiverTile tile,
|
RiverTile tile,
|
||||||
int minimumX,
|
int minimumX,
|
||||||
@@ -1141,11 +1193,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
return routingCost(context.midpointX(), context.midpointZ());
|
return routingCost(context.midpointX(), context.midpointZ());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public double meanderNoise(RiverMeanderContext context) {
|
|
||||||
return meanderNoise.fitDouble(-1D, 1D, context.x(), context.z());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public double flowNoise(double x, double z) {
|
public double flowNoise(double x, double z) {
|
||||||
return routingNoise.fitDouble(-1D, 1D, x, z);
|
return routingNoise.fitDouble(-1D, 1D, x, z);
|
||||||
@@ -1183,6 +1230,18 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
* settings.bankWidthMultiplier();
|
* 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
|
@Override
|
||||||
public double depth(RiverRoutingContext context, double fallback) {
|
public double depth(RiverRoutingContext context, double fallback) {
|
||||||
EffectiveRiverSettings settings = settingsAt(context.midpointX(), context.midpointZ());
|
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
|
@Override
|
||||||
public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
||||||
EffectiveRiverSettings settings = settingsAt(blockX, blockZ);
|
EffectiveRiverSettings settings = settingsAt(blockX, blockZ);
|
||||||
|
|||||||
@@ -50,19 +50,19 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void l(Object l) {
|
public void l(Object l) {
|
||||||
IrisLogging.info("[" + getName() + "]: " + l);
|
IrisLogging.info(String.valueOf(l));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void w(Object l) {
|
public void w(Object l) {
|
||||||
IrisLogging.warn("[" + getName() + "]: " + l);
|
IrisLogging.warn(String.valueOf(l));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void f(Object l) {
|
public void f(Object l) {
|
||||||
IrisLogging.error("[" + getName() + "]: " + l);
|
IrisLogging.error(String.valueOf(l));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void v(Object l) {
|
public void v(Object l) {
|
||||||
IrisLogging.debug("[" + getName() + "]: " + l);
|
IrisLogging.debug(String.valueOf(l));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public class PackRiverValidatorTest {
|
|||||||
"logicalHeight": 256,
|
"logicalHeight": 256,
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"biomes": {
|
"biomes": {
|
||||||
"channel": [],
|
"channel": [],
|
||||||
"bank": [],
|
"bank": [],
|
||||||
@@ -41,6 +42,212 @@ public class PackRiverValidatorTest {
|
|||||||
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
|
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
|
@Test
|
||||||
public void rejectsNullEnabledNetworkSections() throws Exception {
|
public void rejectsNullEnabledNetworkSections() throws Exception {
|
||||||
File pack = pack("""
|
File pack = pack("""
|
||||||
@@ -84,11 +291,10 @@ public class PackRiverValidatorTest {
|
|||||||
"routingPlateauHeight": 0,
|
"routingPlateauHeight": 0,
|
||||||
"flowAlignmentWeight": 1025,
|
"flowAlignmentWeight": 1025,
|
||||||
"confluenceWeight": -1,
|
"confluenceWeight": -1,
|
||||||
"branchSoftCap": 9,
|
|
||||||
"branchChildShrinkFactor": 2,
|
|
||||||
"routingStyle": {"zoom": 0}
|
"routingStyle": {"zoom": 0}
|
||||||
},
|
},
|
||||||
"terrain": {
|
"terrain": {
|
||||||
|
"worms": [{"id": "river"}],
|
||||||
"channelWidth": {"min": 40, "max": 12},
|
"channelWidth": {"min": 40, "max": 12},
|
||||||
"depth": {},
|
"depth": {},
|
||||||
"tunnelWidthMultiplier": {"min": 0.5, "max": 9},
|
"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.routingPlateauHeight must be at least 1");
|
||||||
assertContains(result.errors(), "rivers.topology.flowAlignmentWeight must be at most 1024");
|
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.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.topology.routingStyle.zoom must be at least");
|
||||||
assertContains(result.errors(), "rivers.terrain.channelWidth.min must not exceed");
|
assertContains(result.errors(), "rivers.terrain.channelWidth.min must not exceed");
|
||||||
assertContains(result.errors(), "rivers.terrain.depth must set min and max explicitly.");
|
assertContains(result.errors(), "rivers.terrain.depth must set min and max explicitly.");
|
||||||
@@ -138,6 +342,7 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"water": {
|
"water": {
|
||||||
"mode": "TERRACED",
|
"mode": "TERRACED",
|
||||||
"maximumPoolRise": 2,
|
"maximumPoolRise": 2,
|
||||||
@@ -169,8 +374,7 @@ public class PackRiverValidatorTest {
|
|||||||
"channelWidth": {"min": 2048, "max": 2048},
|
"channelWidth": {"min": 2048, "max": 2048},
|
||||||
"bankWidth": {"min": 2048, "max": 2048},
|
"bankWidth": {"min": 2048, "max": 2048},
|
||||||
"orderWidthFactor": 8,
|
"orderWidthFactor": 8,
|
||||||
"meanderStrength": 1024,
|
"worms": [{"id": "river", "maxOffset": 1024, "segments": 64}]
|
||||||
"meanderSubdivisions": 64
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,6 +395,7 @@ public class PackRiverValidatorTest {
|
|||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"terrain": {
|
"terrain": {
|
||||||
|
"worms": [{"id": "river"}],
|
||||||
"maxChannelWidth": 0,
|
"maxChannelWidth": 0,
|
||||||
"maxBankWidth": -1,
|
"maxBankWidth": -1,
|
||||||
"maxDepth": 513
|
"maxDepth": 513
|
||||||
@@ -214,6 +419,7 @@ public class PackRiverValidatorTest {
|
|||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"terrain": {
|
"terrain": {
|
||||||
|
"worms": [{"id": "river"}],
|
||||||
"maxChannelWidth": 2048,
|
"maxChannelWidth": 2048,
|
||||||
"tunnelMouthBlend": 16
|
"tunnelMouthBlend": 16
|
||||||
}
|
}
|
||||||
@@ -234,6 +440,7 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"biomes": {
|
"biomes": {
|
||||||
"channel": ["biome", "missing"],
|
"channel": ["biome", "missing"],
|
||||||
"bank": ["biome"]
|
"bank": ["biome"]
|
||||||
@@ -257,6 +464,7 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"biomes": {
|
"biomes": {
|
||||||
"channel": ["biome"],
|
"channel": ["biome"],
|
||||||
"bank": ["biome"]
|
"bank": ["biome"]
|
||||||
@@ -292,7 +500,10 @@ public class PackRiverValidatorTest {
|
|||||||
File pack = pack("""
|
File pack = pack("""
|
||||||
{
|
{
|
||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {"enabled": true}
|
"rivers": {
|
||||||
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
write(pack, "regions/region.json", """
|
write(pack, "regions/region.json", """
|
||||||
@@ -326,7 +537,10 @@ public class PackRiverValidatorTest {
|
|||||||
File pack = pack("""
|
File pack = pack("""
|
||||||
{
|
{
|
||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {"enabled": true}
|
"rivers": {
|
||||||
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
write(pack, "regions/region.json", """
|
write(pack, "regions/region.json", """
|
||||||
@@ -352,6 +566,7 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"caves": {
|
"caves": {
|
||||||
"mode": "GENERATE_GROTTO",
|
"mode": "GENERATE_GROTTO",
|
||||||
"throatRadius": 4,
|
"throatRadius": 4,
|
||||||
@@ -383,6 +598,7 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"caves": {
|
"caves": {
|
||||||
"mode": "FLOOD_CLOSED_COMPONENT",
|
"mode": "FLOOD_CLOSED_COMPONENT",
|
||||||
"maximumPerReach": 0,
|
"maximumPerReach": 0,
|
||||||
@@ -406,7 +622,10 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"terrain": {"terminalMode": "SINKHOLE_GROTTO"},
|
"terrain": {
|
||||||
|
"worms": [{"id": "river"}],
|
||||||
|
"terminalMode": "SINKHOLE_GROTTO"
|
||||||
|
},
|
||||||
"caves": {"mode": "SEALED"}
|
"caves": {"mode": "SEALED"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,7 +643,10 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"terrain": {"terminalMode": "SINKHOLE_GROTTO"},
|
"terrain": {
|
||||||
|
"worms": [{"id": "river"}],
|
||||||
|
"terminalMode": "SINKHOLE_GROTTO"
|
||||||
|
},
|
||||||
"caves": {
|
"caves": {
|
||||||
"mode": "GENERATE_GROTTO",
|
"mode": "GENERATE_GROTTO",
|
||||||
"maximumPerReach": 0
|
"maximumPerReach": 0
|
||||||
@@ -446,7 +668,10 @@ public class PackRiverValidatorTest {
|
|||||||
"carvingEnabled": false,
|
"carvingEnabled": false,
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"terrain": {"terminalMode": "SINKHOLE_GROTTO"},
|
"terrain": {
|
||||||
|
"worms": [{"id": "river"}],
|
||||||
|
"terminalMode": "SINKHOLE_GROTTO"
|
||||||
|
},
|
||||||
"caves": {"mode": "GENERATE_GROTTO"}
|
"caves": {"mode": "GENERATE_GROTTO"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -464,7 +689,10 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"terrain": {"terminalMode": "SINKHOLE_GROTTO"},
|
"terrain": {
|
||||||
|
"worms": [{"id": "river"}],
|
||||||
|
"terminalMode": "SINKHOLE_GROTTO"
|
||||||
|
},
|
||||||
"caves": {
|
"caves": {
|
||||||
"mode": "FLOOD_CLOSED_COMPONENT",
|
"mode": "FLOOD_CLOSED_COMPONENT",
|
||||||
"fallback": "SEALED",
|
"fallback": "SEALED",
|
||||||
@@ -490,6 +718,7 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"caves": {"mode": "SEALED"}
|
"caves": {"mode": "SEALED"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,6 +743,7 @@ public class PackRiverValidatorTest {
|
|||||||
"carvingEnabled": false,
|
"carvingEnabled": false,
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"caves": {"mode": "GENERATE_GROTTO"}
|
"caves": {"mode": "GENERATE_GROTTO"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -545,6 +775,7 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"topology": {
|
"topology": {
|
||||||
"routingStyle": {"expression": "route"}
|
"routingStyle": {"expression": "route"}
|
||||||
}
|
}
|
||||||
@@ -587,6 +818,7 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"topology": {
|
"topology": {
|
||||||
"routingStyle": {"expression": "route"}
|
"routingStyle": {"expression": "route"}
|
||||||
}
|
}
|
||||||
@@ -618,6 +850,7 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"topology": {
|
"topology": {
|
||||||
"routingStyle": "snippet/style/first"
|
"routingStyle": "snippet/style/first"
|
||||||
}
|
}
|
||||||
@@ -639,6 +872,7 @@ public class PackRiverValidatorTest {
|
|||||||
"regions": ["region"],
|
"regions": ["region"],
|
||||||
"rivers": {
|
"rivers": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
"topology": {
|
"topology": {
|
||||||
"routingStyle": {"expression": "route"}
|
"routingStyle": {"expression": "route"}
|
||||||
}
|
}
|
||||||
@@ -677,6 +911,50 @@ public class PackRiverValidatorTest {
|
|||||||
return pack;
|
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) {
|
private void assertContains(List<String> messages, String fragment) {
|
||||||
assertTrue(messages.toString(), contains(messages, fragment));
|
assertTrue(messages.toString(), contains(messages, fragment));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ public class IrisRiverSchemaTest {
|
|||||||
JSONObject properties = schema.getJSONObject("properties");
|
JSONObject properties = schema.getJSONObject("properties");
|
||||||
JSONObject topology = referencedProperties(definitions, properties.getJSONObject("topology"));
|
JSONObject topology = referencedProperties(definitions, properties.getJSONObject("topology"));
|
||||||
JSONObject source = referencedProperties(definitions, topology.getJSONObject("source"));
|
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 water = referencedProperties(definitions, properties.getJSONObject("water"));
|
||||||
JSONObject biomes = referencedProperties(definitions, properties.getJSONObject("biomes"));
|
JSONObject biomes = referencedProperties(definitions, properties.getJSONObject("biomes"));
|
||||||
|
|
||||||
@@ -46,6 +49,29 @@ public class IrisRiverSchemaTest {
|
|||||||
assertEquals(2048D, terrain.getJSONObject("maxChannelWidth").getDouble("maximum"), 0D);
|
assertEquals(2048D, terrain.getJSONObject("maxChannelWidth").getDouble("maximum"), 0D);
|
||||||
assertEquals(0D, terrain.getJSONObject("maxBankWidth").getDouble("minimum"), 0D);
|
assertEquals(0D, terrain.getJSONObject("maxBankWidth").getDouble("minimum"), 0D);
|
||||||
assertEquals(512D, terrain.getJSONObject("maxDepth").getDouble("maximum"), 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(List.of("SEA_LEVEL", "TERRACED"), enumValues(definitions, water.getJSONObject("mode")));
|
||||||
assertEquals("array", biomes.getJSONObject("channel").getString("type"));
|
assertEquals("array", biomes.getJSONObject("channel").getString("type"));
|
||||||
assertEquals("#/definitions/erzbiomes",
|
assertEquals("#/definitions/erzbiomes",
|
||||||
@@ -87,8 +113,21 @@ public class IrisRiverSchemaTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static JSONObject referencedProperties(JSONObject definitions, JSONObject reference) {
|
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());
|
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) {
|
private static List<String> enumValues(JSONObject definitions, JSONObject reference) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class IrisRiverConfigurationTest {
|
|||||||
assertEquals(10D, dimension.getRivers().getTerrain().getMaxChannelWidth(), 0D);
|
assertEquals(10D, dimension.getRivers().getTerrain().getMaxChannelWidth(), 0D);
|
||||||
assertEquals(4D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D);
|
assertEquals(4D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D);
|
||||||
assertEquals(10D, dimension.getRivers().getTerrain().getMaxDepth(), 0D);
|
assertEquals(10D, dimension.getRivers().getTerrain().getMaxDepth(), 0D);
|
||||||
|
assertTrue(dimension.getRivers().getTerrain().getWorms().isEmpty());
|
||||||
assertEquals(IrisRiverCaveMode.SEALED, dimension.getRivers().getCaves().getMode());
|
assertEquals(IrisRiverCaveMode.SEALED, dimension.getRivers().getCaves().getMode());
|
||||||
assertEquals(IrisRiverCaveFallback.SEALED, dimension.getRivers().getCaves().getFallback());
|
assertEquals(IrisRiverCaveFallback.SEALED, dimension.getRivers().getCaves().getFallback());
|
||||||
assertEquals(IrisRiverExistingFluidPolicy.REJECT,
|
assertEquals(IrisRiverExistingFluidPolicy.REJECT,
|
||||||
@@ -65,6 +66,27 @@ public class IrisRiverConfigurationTest {
|
|||||||
"maxBankWidth": 2.5,
|
"maxBankWidth": 2.5,
|
||||||
"maxDepth": 8,
|
"maxDepth": 8,
|
||||||
"maxIncision": 36,
|
"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"
|
"terminalMode": "SUPPRESS"
|
||||||
},
|
},
|
||||||
"water": {"mode": "TERRACED", "poolLength": 80},
|
"water": {"mode": "TERRACED", "poolLength": 80},
|
||||||
@@ -111,6 +133,24 @@ public class IrisRiverConfigurationTest {
|
|||||||
assertEquals(2.5D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D);
|
assertEquals(2.5D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D);
|
||||||
assertEquals(8D, dimension.getRivers().getTerrain().getMaxDepth(), 0D);
|
assertEquals(8D, dimension.getRivers().getTerrain().getMaxDepth(), 0D);
|
||||||
assertEquals(36, dimension.getRivers().getTerrain().getMaxIncision());
|
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,
|
assertEquals(IrisRiverTerminalMode.SUPPRESS,
|
||||||
dimension.getRivers().getTerrain().getTerminalMode());
|
dimension.getRivers().getTerrain().getTerminalMode());
|
||||||
assertEquals(IrisRiverWaterMode.TERRACED, dimension.getRivers().getWater().getMode());
|
assertEquals(IrisRiverWaterMode.TERRACED, dimension.getRivers().getWater().getMode());
|
||||||
@@ -145,7 +185,11 @@ public class IrisRiverConfigurationTest {
|
|||||||
.setFloodedCaveBiomes(new KList<>("biome-grotto"));
|
.setFloodedCaveBiomes(new KList<>("biome-grotto"));
|
||||||
IrisDimension dimension = new IrisDimension()
|
IrisDimension dimension = new IrisDimension()
|
||||||
.setRegions(new KList<>("region"))
|
.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()
|
IrisRegion region = new IrisRegion()
|
||||||
.setLandBiomes(new KList<>("natural"))
|
.setLandBiomes(new KList<>("natural"))
|
||||||
.setRiverOverride(regionOverride);
|
.setRiverOverride(regionOverride);
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class RiverNetworkTest {
|
public class RiverNetworkTest {
|
||||||
private static final double TWO_PI_FOR_TESTS = StrictMath.PI * 2D;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void mapsNegativeWorldCoordinatesWithFloorDivision() {
|
public void mapsNegativeWorldCoordinatesWithFloorDivision() {
|
||||||
RiverNetwork network = new RiverNetwork(options(1L).build());
|
RiverNetwork network = new RiverNetwork(options(1L).build());
|
||||||
@@ -164,12 +162,25 @@ public class RiverNetworkTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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)
|
RiverNetwork network = new RiverNetwork(options(720L)
|
||||||
.requireOcean(false)
|
.requireOcean(false)
|
||||||
.routingBasinCells(16)
|
.routingBasinCells(16)
|
||||||
.branchSoftCap(4)
|
.worms(List.of(constrained))
|
||||||
.branchChildShrinkFactor(0D)
|
|
||||||
.build());
|
.build());
|
||||||
RiverTerrainSampler terrain = flatTerrain(false);
|
RiverTerrainSampler terrain = flatTerrain(false);
|
||||||
HashMap<RiverNodeId, Integer> incoming = new HashMap<>();
|
HashMap<RiverNodeId, Integer> incoming = new HashMap<>();
|
||||||
@@ -188,36 +199,29 @@ public class RiverNetworkTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void candidateRankingAndTracingDoNotMaterializeMeanderGeometry() {
|
public void candidateRankingAndTracingAreIndependentOfWormGeometry() {
|
||||||
RiverNetwork network = new RiverNetwork(options(75L)
|
RiverNetwork straightNetwork = new RiverNetwork(options(75L)
|
||||||
.requireOcean(false)
|
.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());
|
.build());
|
||||||
int[] meanderSamples = new int[1];
|
|
||||||
RiverTerrainSampler terrain = new TestTerrain(false) {
|
RiverTerrainSampler terrain = new TestTerrain(false) {
|
||||||
@Override
|
@Override
|
||||||
public double reachRoutingCost(RiverRoutingContext context) {
|
public double reachRoutingCost(RiverRoutingContext context) {
|
||||||
return context.midpointX() + context.midpointZ();
|
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());
|
assertFalse(straightNetwork.downstreamCandidates(source, terrain).isEmpty());
|
||||||
assertEquals(0, meanderSamples[0]);
|
assertEquals(
|
||||||
assertFalse(network.trace(new RiverNodeId(0L, 0L), terrain).edges().isEmpty());
|
straightNetwork.downstreamCandidates(source, terrain),
|
||||||
assertEquals(0, meanderSamples[0]);
|
windingNetwork.downstreamCandidates(source, terrain)
|
||||||
|
);
|
||||||
RiverTile tile = network.buildTile(0, 0, terrain);
|
assertEquals(straightNetwork.trace(source, terrain), windingNetwork.trace(source, terrain));
|
||||||
|
|
||||||
assertFalse(tile.reaches().isEmpty());
|
|
||||||
assertTrue(meanderSamples[0] > 0);
|
|
||||||
assertEquals(0, meanderSamples[0] % 12);
|
|
||||||
assertTrue(meanderSamples[0] / 12 <= 256);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -616,10 +620,10 @@ public class RiverNetworkTest {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assertEquals(List.of(
|
assertEquals(List.of(
|
||||||
3050685048971830506L,
|
973031325888677478L,
|
||||||
-3861364772874819248L,
|
8932177974453767311L,
|
||||||
-8770785170128501517L,
|
-5189009084208004632L,
|
||||||
6698607697170340377L
|
6374264141259432071L
|
||||||
), actual);
|
), actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,9 +663,20 @@ public class RiverNetworkTest {
|
|||||||
.channelWidth(512D)
|
.channelWidth(512D)
|
||||||
.maxChannelWidth(512D)
|
.maxChannelWidth(512D)
|
||||||
.bankWidth(0D)
|
.bankWidth(0D)
|
||||||
.branchChildShrinkFactor(1D)
|
.worms(List.of(wormProfile(
|
||||||
.meanderStrength(0D)
|
"straight",
|
||||||
.meanderSubdivisions(1)
|
1L,
|
||||||
|
1,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
8,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
0D,
|
||||||
|
0D,
|
||||||
|
List.of()
|
||||||
|
)))
|
||||||
.build());
|
.build());
|
||||||
RiverTerrainSampler terrain = new TestTerrain(false) {
|
RiverTerrainSampler terrain = new TestTerrain(false) {
|
||||||
@Override
|
@Override
|
||||||
@@ -787,9 +802,9 @@ public class RiverNetworkTest {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10.0,
|
10.0,
|
||||||
RiverWidthProfile.constant(10.0),
|
|
||||||
5.0,
|
5.0,
|
||||||
3.0,
|
3.0,
|
||||||
|
RiverBodyProfile.constant(10.0, 5.0, 3.0),
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
new RiverPolyline(new double[]{0.0, 100.0}, new double[]{0.0, 0.0})
|
new RiverPolyline(new double[]{0.0, 100.0}, new double[]{0.0, 0.0})
|
||||||
@@ -867,14 +882,9 @@ public class RiverNetworkTest {
|
|||||||
.maxChannelWidth(10D)
|
.maxChannelWidth(10D)
|
||||||
.maxBankWidth(3D)
|
.maxBankWidth(3D)
|
||||||
.maxDepth(6D)
|
.maxDepth(6D)
|
||||||
.meanderStrength(0D)
|
.worms(List.of(worm(1L, 0D, 0D, 0D, 1)))
|
||||||
.build());
|
.build());
|
||||||
RiverTerrainSampler styled = new TestTerrain(false) {
|
RiverTerrainSampler styled = new TestTerrain(false) {
|
||||||
@Override
|
|
||||||
public double meanderNoise(RiverMeanderContext context) {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public double channelWidth(RiverRoutingContext context, double fallback) {
|
public double channelWidth(RiverRoutingContext context, double fallback) {
|
||||||
return 20.0;
|
return 20.0;
|
||||||
@@ -906,12 +916,15 @@ public class RiverNetworkTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void channelWidthVariesInsideOneReachAndRemainsSpatiallyQueryable() {
|
public void bodyAnatomyVariesInsideOneReachAndRemainsSpatiallyQueryable() {
|
||||||
RiverNode from = node(0L, 0L, 0D, 0D);
|
RiverNode from = node(0L, 0L, 0D, 0D);
|
||||||
RiverNode to = node(1L, 0L, 100D, 0D);
|
RiverNode to = node(1L, 0L, 100D, 0D);
|
||||||
RiverWidthProfile profile = new RiverWidthProfile(
|
RiverBodyProfile profile = new RiverBodyProfile(
|
||||||
new double[]{0D, 0.5D, 1D},
|
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(
|
RiverReach reach = new RiverReach(
|
||||||
RiverEdgeId.of(from.id(), to.id()),
|
RiverEdgeId.of(from.id(), to.id()),
|
||||||
@@ -921,31 +934,100 @@ public class RiverNetworkTest {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
7D,
|
7D,
|
||||||
|
9D,
|
||||||
|
8D,
|
||||||
profile,
|
profile,
|
||||||
0D,
|
|
||||||
3D,
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
new RiverPolyline(new double[]{0D, 100D}, new double[]{0D, 0D})
|
new RiverPolyline(new double[]{0D, 100D}, new double[]{0D, 0D})
|
||||||
);
|
);
|
||||||
RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach));
|
RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach));
|
||||||
|
|
||||||
RiverSample narrow = tile.sample(10D, 2D);
|
RiverSample narrow = tile.sample(10D, 7D);
|
||||||
RiverSample wide = tile.sample(50D, 2D);
|
RiverSample swollen = tile.sample(50D, 7D);
|
||||||
|
|
||||||
assertFalse(narrow.present());
|
assertFalse(narrow.present());
|
||||||
assertTrue(wide.present());
|
assertTrue(swollen.present());
|
||||||
assertEquals(7D, wide.width(), 0D);
|
assertEquals(7D, swollen.width(), 0D);
|
||||||
assertEquals(RiverSection.CHANNEL, wide.section());
|
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
|
@Test
|
||||||
public void foldedReachSamplingFindsFartherCoveringWidthEnvelope() {
|
public void foldedReachSamplingFindsFartherCoveringWidthEnvelope() {
|
||||||
RiverNode from = node(0L, 0L, 0D, 0D);
|
RiverNode from = node(0L, 0L, 0D, 0D);
|
||||||
RiverNode to = node(1L, 0L, 0D, 10D);
|
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[]{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(
|
RiverReach reach = new RiverReach(
|
||||||
RiverEdgeId.of(from.id(), to.id()),
|
RiverEdgeId.of(from.id(), to.id()),
|
||||||
@@ -955,9 +1037,9 @@ public class RiverNetworkTest {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
18D,
|
18D,
|
||||||
profile,
|
|
||||||
0D,
|
0D,
|
||||||
3D,
|
3D,
|
||||||
|
profile,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
new RiverPolyline(
|
new RiverPolyline(
|
||||||
@@ -978,15 +1060,12 @@ public class RiverNetworkTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void reachFeasibilityReceivesTheFinalPinnedMeanderPolyline() {
|
public void reachFeasibilityReceivesTheFinalPinnedWormPolyline() {
|
||||||
RiverNetwork network = new RiverNetwork(options(62L).meanderStrength(20D).build());
|
RiverNetwork network = new RiverNetwork(options(62L)
|
||||||
boolean[] observedMeander = new boolean[1];
|
.worms(List.of(worm(62L, 0.8D, 0.2D, 20D, 32)))
|
||||||
|
.build());
|
||||||
|
boolean[] observedWorm = new boolean[1];
|
||||||
RiverTerrainSampler terrain = new TestTerrain(false) {
|
RiverTerrainSampler terrain = new TestTerrain(false) {
|
||||||
@Override
|
|
||||||
public double meanderNoise(RiverMeanderContext context) {
|
|
||||||
return 1D;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean allowsReach(RiverRoutingContext context) {
|
public boolean allowsReach(RiverRoutingContext context) {
|
||||||
RiverPolyline polyline = context.polyline();
|
RiverPolyline polyline = context.polyline();
|
||||||
@@ -996,7 +1075,7 @@ public class RiverNetworkTest {
|
|||||||
double pointX = polyline.x(point) - context.from().x();
|
double pointX = polyline.x(point) - context.from().x();
|
||||||
double pointZ = polyline.z(point) - context.from().z();
|
double pointZ = polyline.z(point) - context.from().z();
|
||||||
if (StrictMath.abs(pointX * deltaZ - pointZ * deltaX) > 0.000001D) {
|
if (StrictMath.abs(pointX * deltaZ - pointZ * deltaX) > 0.000001D) {
|
||||||
observedMeander[0] = true;
|
observedWorm[0] = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1006,76 +1085,66 @@ public class RiverNetworkTest {
|
|||||||
|
|
||||||
RiverNode downstream = network.downstream(new RiverNodeId(0L, 0L), terrain);
|
RiverNode downstream = network.downstream(new RiverNodeId(0L, 0L), terrain);
|
||||||
|
|
||||||
assertTrue(observedMeander[0]);
|
assertTrue(observedWorm[0]);
|
||||||
assertEquals(null, downstream);
|
assertEquals(null, downstream);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void irisNoiseChangesTheSelectedPersonalityWithoutBreakingItsEnvelope() {
|
public void wormSeedChangesDeterministicGeometryWithoutBreakingItsEnvelope() {
|
||||||
double meanderStrength = 20D;
|
double maximumOffset = 20D;
|
||||||
RiverNetwork network = new RiverNetwork(options(63L)
|
RiverNetwork firstNetwork = new RiverNetwork(options(63L)
|
||||||
.siteJitter(0D)
|
.siteJitter(0D)
|
||||||
.requireOcean(false)
|
.requireOcean(false)
|
||||||
.meanderStrength(meanderStrength)
|
.worms(List.of(worm(101L, 0.8D, 0.2D, maximumOffset, 32)))
|
||||||
.meanderSubdivisions(32)
|
|
||||||
.build());
|
.build());
|
||||||
RiverTerrainSampler quietTerrain = new TestTerrain(false) {
|
RiverNetwork secondNetwork = new RiverNetwork(options(63L)
|
||||||
@Override
|
.siteJitter(0D)
|
||||||
public double meanderNoise(RiverMeanderContext context) {
|
.requireOcean(false)
|
||||||
return 0D;
|
.worms(List.of(worm(202L, 0.8D, 0.2D, maximumOffset, 32)))
|
||||||
}
|
.build());
|
||||||
};
|
RiverTerrainSampler terrain = new TestTerrain(false);
|
||||||
RiverTerrainSampler noisyTerrain = new TestTerrain(false) {
|
|
||||||
@Override
|
|
||||||
public double meanderNoise(RiverMeanderContext context) {
|
|
||||||
return StrictMath.sin(context.normalizedPosition() * StrictMath.PI * 7D);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RiverReach quietReach = network.buildTile(0, 0, quietTerrain).reaches().getFirst();
|
RiverReach firstReach = firstNetwork.buildTile(0, 0, terrain).reaches().getFirst();
|
||||||
RiverReach noisyReach = network.buildTile(0, 0, noisyTerrain).reaches().getFirst();
|
RiverReach repeatedReach = firstNetwork.buildTile(0, 0, terrain).reaches().getFirst();
|
||||||
assertEquals(quietReach.id(), noisyReach.id());
|
RiverReach secondReach = secondNetwork.buildTile(0, 0, terrain).reaches().getFirst();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(changed);
|
assertReachEquals(firstReach, repeatedReach);
|
||||||
assertEquals(noisyReach.from().x(), noisyPolyline.x(0), 0D);
|
assertEquals(firstReach.id(), secondReach.id());
|
||||||
assertEquals(noisyReach.from().z(), noisyPolyline.z(0), 0D);
|
assertTrue(polylinesDiffer(firstReach.polyline(), secondReach.polyline()));
|
||||||
int last = noisyPolyline.size() - 1;
|
assertWormBounds(firstReach, maximumOffset);
|
||||||
assertEquals(noisyReach.to().x(), noisyPolyline.x(last), 0D);
|
assertWormBounds(secondReach, maximumOffset);
|
||||||
assertEquals(noisyReach.to().z(), noisyPolyline.z(last), 0D);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void reachesSpanBroadAsymmetricCompoundAndRestlessShapeFamilies() {
|
public void weightedWormProfilesSelectDistinctConfigurableVariants() {
|
||||||
double meanderStrength = 20D;
|
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)
|
RiverNetwork network = new RiverNetwork(options(64L)
|
||||||
.siteJitter(0D)
|
.siteJitter(0D)
|
||||||
|
.routingBasinCells(8)
|
||||||
.requireOcean(false)
|
.requireOcean(false)
|
||||||
.meanderStrength(meanderStrength)
|
.maxChannelWidth(32D)
|
||||||
.meanderSubdivisions(48)
|
.maxBankWidth(32D)
|
||||||
|
.maxDepth(32D)
|
||||||
|
.orderWidthFactor(0D)
|
||||||
|
.orderDepthFactor(0D)
|
||||||
|
.maximumReachRadius(32D)
|
||||||
|
.worms(List.of(gentle, winding, restless))
|
||||||
.build());
|
.build());
|
||||||
RiverTerrainSampler terrain = new TestTerrain(false) {
|
RiverTerrainSampler terrain = new TestTerrain(false);
|
||||||
@Override
|
|
||||||
public double meanderNoise(RiverMeanderContext context) {
|
|
||||||
return 0D;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
HashMap<RiverEdgeId, RiverReach> uniqueReaches = new HashMap<>();
|
HashMap<RiverEdgeId, RiverReach> uniqueReaches = new HashMap<>();
|
||||||
for (int tileX = -2; tileX <= 2; tileX++) {
|
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()) {
|
for (RiverReach reach : uniqueReaches.values()) {
|
||||||
shapes.add(shapeOf(reach, meanderStrength));
|
int pointCount = reach.polyline().size();
|
||||||
}
|
selectedPointCounts.add(pointCount);
|
||||||
double minimumAmplitude = shapes.stream()
|
RiverWorm selected = switch (pointCount) {
|
||||||
.mapToDouble(ReachShape::amplitudeRatio)
|
case 9 -> gentle;
|
||||||
.min()
|
case 17 -> winding;
|
||||||
.orElseThrow();
|
case 33 -> restless;
|
||||||
double maximumAmplitude = shapes.stream()
|
default -> throw new AssertionError("Unexpected worm point count " + pointCount);
|
||||||
.mapToDouble(ReachShape::amplitudeRatio)
|
};
|
||||||
.max()
|
assertEquals(8D * selected.widthMultiplier(), reach.width(), 0.0000001D);
|
||||||
.orElseThrow();
|
assertEquals(6D * selected.bankMultiplier(), reach.bankWidth(), 0.0000001D);
|
||||||
HashSet<Integer> dominantBands = new HashSet<>();
|
assertEquals(3D * selected.depthMultiplier(), reach.depth(), 0.0000001D);
|
||||||
HashSet<String> signatures = new HashSet<>();
|
assertWormBounds(reach, selected.maxOffset());
|
||||||
for (ReachShape shape : shapes) {
|
maximumDisplacementRatios.merge(
|
||||||
dominantBands.add(shape.dominantBand());
|
pointCount,
|
||||||
signatures.add(
|
maximumWormDisplacement(reach) / selected.maxOffset(),
|
||||||
StrictMath.min(6, shape.crossings()) + ":"
|
StrictMath::max
|
||||||
+ (int) StrictMath.floor(shape.centroid() * 5D) + ":"
|
|
||||||
+ (int) StrictMath.floor(shape.amplitudeRatio() * 5D) + ":"
|
|
||||||
+ shape.dominantBand()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
String diagnostics = shapes.toString();
|
assertEquals(Set.of(9, 17, 33), selectedPointCounts);
|
||||||
assertTrue(diagnostics, shapes.size() >= 32);
|
assertTrue(maximumDisplacementRatios.toString(), maximumDisplacementRatios.get(9) > 0.03D);
|
||||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() <= 1));
|
assertTrue(maximumDisplacementRatios.toString(), maximumDisplacementRatios.get(17) > 0.18D);
|
||||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() >= 2 && shape.crossings() <= 4));
|
assertTrue(maximumDisplacementRatios.toString(), maximumDisplacementRatios.get(33) > 0.4D);
|
||||||
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));
|
@Test
|
||||||
assertTrue(diagnostics, maximumAmplitude - minimumAmplitude >= 0.25D);
|
public void forcedChildHierarchyControlsReachGeometryAndDimensionsDeterministically() {
|
||||||
assertTrue(diagnostics, dominantBands.size() >= 3);
|
RiverWorm child = wormProfile(
|
||||||
assertTrue(diagnostics, signatures.size() >= 8);
|
"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
|
@Test
|
||||||
@@ -1194,8 +1441,6 @@ public class RiverNetworkTest {
|
|||||||
assertThrows(IllegalArgumentException.class, () -> options(10L).routingPlateauHeight(0D).build());
|
assertThrows(IllegalArgumentException.class, () -> options(10L).routingPlateauHeight(0D).build());
|
||||||
assertThrows(IllegalArgumentException.class, () -> options(10L).hydraulicBaseHeight(Double.NaN).build());
|
assertThrows(IllegalArgumentException.class, () -> options(10L).hydraulicBaseHeight(Double.NaN).build());
|
||||||
assertThrows(IllegalArgumentException.class, () -> options(10L).confluenceWeight(-1D).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).channelWidth(-1.0).build());
|
||||||
assertThrows(IllegalArgumentException.class, () -> options(10L)
|
assertThrows(IllegalArgumentException.class, () -> options(10L)
|
||||||
.tileCells(1)
|
.tileCells(1)
|
||||||
@@ -1255,8 +1500,7 @@ public class RiverNetworkTest {
|
|||||||
.siteJitter(0.49D)
|
.siteJitter(0.49D)
|
||||||
.maxRouteReaches(256)
|
.maxRouteReaches(256)
|
||||||
.maximumReachRadius(pathologicalRadius)
|
.maximumReachRadius(pathologicalRadius)
|
||||||
.meanderStrength(1024D)
|
.worms(List.of(worm(1L, 1D, 1D, 1024D, 64)))
|
||||||
.meanderSubdivisions(64)
|
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
assertTrue(failure.getMessage(), failure.getMessage().contains("source window"));
|
assertTrue(failure.getMessage(), failure.getMessage().contains("source window"));
|
||||||
@@ -1278,8 +1522,126 @@ public class RiverNetworkTest {
|
|||||||
.channelWidth(8.0)
|
.channelWidth(8.0)
|
||||||
.bankWidth(6.0)
|
.bankWidth(6.0)
|
||||||
.depth(3.0)
|
.depth(3.0)
|
||||||
.meanderStrength(12.0)
|
.worms(List.of(worm(1L, 0.5D, 0.15D, 12D, 6)));
|
||||||
.meanderSubdivisions(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) {
|
private static RiverNode node(long cellX, long cellZ, double x, double z) {
|
||||||
@@ -1305,9 +1667,9 @@ public class RiverNetworkTest {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
width,
|
width,
|
||||||
RiverWidthProfile.constant(width),
|
|
||||||
bankWidth,
|
bankWidth,
|
||||||
3D,
|
3D,
|
||||||
|
RiverBodyProfile.constant(width, bankWidth, 3D),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
new RiverPolyline(
|
new RiverPolyline(
|
||||||
@@ -1347,55 +1709,45 @@ public class RiverNetworkTest {
|
|||||||
return hydraulicComparison != 0 ? hydraulicComparison : first.id().compareTo(second.id());
|
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();
|
RiverPolyline polyline = reach.polyline();
|
||||||
double deltaX = reach.to().x() - reach.from().x();
|
double deltaX = reach.to().x() - reach.from().x();
|
||||||
double deltaZ = reach.to().z() - reach.from().z();
|
double deltaZ = reach.to().z() - reach.from().z();
|
||||||
double chordLength = StrictMath.hypot(deltaX, deltaZ);
|
double maximumDisplacement = 0D;
|
||||||
double[] offsets = new double[polyline.size()];
|
|
||||||
double maximumOffset = 0D;
|
|
||||||
double weightedPosition = 0D;
|
|
||||||
double totalWeight = 0D;
|
|
||||||
int crossings = 0;
|
|
||||||
int previousSign = 0;
|
|
||||||
for (int point = 0; point < polyline.size(); point++) {
|
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);
|
double t = (double) point / (polyline.size() - 1);
|
||||||
weightedPosition += t * absoluteOffset;
|
double straightX = reach.from().x() + deltaX * t;
|
||||||
totalWeight += absoluteOffset;
|
double straightZ = reach.from().z() + deltaZ * t;
|
||||||
int sign = absoluteOffset < meanderStrength * 0.05D ? 0 : offset < 0D ? -1 : 1;
|
double displacement = StrictMath.hypot(
|
||||||
if (sign != 0 && previousSign != 0 && sign != previousSign) {
|
polyline.x(point) - straightX,
|
||||||
crossings++;
|
polyline.z(point) - straightZ
|
||||||
|
);
|
||||||
|
maximumDisplacement = StrictMath.max(maximumDisplacement, displacement);
|
||||||
}
|
}
|
||||||
if (sign != 0) {
|
return maximumDisplacement;
|
||||||
previousSign = sign;
|
|
||||||
}
|
}
|
||||||
assertTrue(absoluteOffset <= meanderStrength + 0.0000001D);
|
|
||||||
|
private static boolean polylinesDiffer(RiverPolyline first, RiverPolyline second) {
|
||||||
|
if (first.size() != second.size()) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
int dominantBand = 1;
|
for (int point = 0; point < first.size(); point++) {
|
||||||
double dominantEnergy = -1D;
|
if (Double.doubleToLongBits(first.x(point)) != Double.doubleToLongBits(second.x(point))
|
||||||
int samples = offsets.length - 1;
|
|| Double.doubleToLongBits(first.z(point)) != Double.doubleToLongBits(second.z(point))) {
|
||||||
for (int frequency = 1; frequency <= 8; frequency++) {
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
double centroid = totalWeight <= 0.0000001D ? 0.5D : weightedPosition / totalWeight;
|
return false;
|
||||||
return new ReachShape(crossings, maximumOffset / meanderStrength, centroid, dominantBand);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long digest(RiverTile tile) {
|
private static long digest(RiverTile tile) {
|
||||||
@@ -1408,9 +1760,12 @@ public class RiverNetworkTest {
|
|||||||
hash = RiverNetwork.mix(hash ^ reach.order());
|
hash = RiverNetwork.mix(hash ^ reach.order());
|
||||||
hash = RiverNetwork.mix(hash ^ reach.state().ordinal());
|
hash = RiverNetwork.mix(hash ^ reach.state().ordinal());
|
||||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.width()));
|
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.width()));
|
||||||
for (int index = 0; index < reach.widthProfile().size(); index++) {
|
for (int index = 0; index < reach.bodyProfile().size(); index++) {
|
||||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.widthProfile().position(index)));
|
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().position(index)));
|
||||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.widthProfile().width(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.bankWidth()));
|
||||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.depth()));
|
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.depth()));
|
||||||
@@ -1442,7 +1797,7 @@ public class RiverNetworkTest {
|
|||||||
assertEquals(first.flow(), second.flow());
|
assertEquals(first.flow(), second.flow());
|
||||||
assertEquals(first.order(), second.order());
|
assertEquals(first.order(), second.order());
|
||||||
assertEquals(first.width(), second.width(), 0.0);
|
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.bankWidth(), second.bankWidth(), 0.0);
|
||||||
assertEquals(first.depth(), second.depth(), 0.0);
|
assertEquals(first.depth(), second.depth(), 0.0);
|
||||||
assertEquals(first.mouth(), second.mouth());
|
assertEquals(first.mouth(), second.mouth());
|
||||||
@@ -1473,7 +1828,4 @@ public class RiverNetworkTest {
|
|||||||
return ocean && blockX >= 224;
|
return ocean && blockX >= 224;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ReachShape(int crossings, double amplitudeRatio, double centroid, int dominantBand) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,9 +295,9 @@ public class RiverTileCacheTest {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
8.0,
|
8.0,
|
||||||
RiverWidthProfile.constant(8.0),
|
|
||||||
4.0,
|
4.0,
|
||||||
3.0,
|
3.0,
|
||||||
|
RiverBodyProfile.constant(8.0, 4.0, 3.0),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
new RiverPolyline(new double[]{8.0, 56.0}, new double[]{8.0, 8.0})
|
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.IrisRiverRoutingPolicy;
|
||||||
import art.arcane.iris.engine.object.IrisRiverTerminalMode;
|
import art.arcane.iris.engine.object.IrisRiverTerminalMode;
|
||||||
import art.arcane.iris.engine.object.IrisRiverWaterMode;
|
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.IrisStyledRange;
|
||||||
import art.arcane.iris.engine.object.NoiseStyle;
|
import art.arcane.iris.engine.object.NoiseStyle;
|
||||||
import art.arcane.iris.engine.river.RiverEdgeId;
|
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.RiverReach;
|
||||||
import art.arcane.iris.engine.river.RiverRouteState;
|
import art.arcane.iris.engine.river.RiverRouteState;
|
||||||
import art.arcane.iris.engine.river.RiverAnchor;
|
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.RiverRoutingContext;
|
||||||
import art.arcane.iris.engine.river.RiverTerrainNodeSample;
|
import art.arcane.iris.engine.river.RiverTerrainNodeSample;
|
||||||
import art.arcane.iris.engine.river.RiverTopologyComplexity;
|
import art.arcane.iris.engine.river.RiverTopologyComplexity;
|
||||||
import art.arcane.iris.engine.river.RiverTerrainSourceSample;
|
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.interpolation.NoiseBounds;
|
||||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -793,9 +795,9 @@ public class IrisRiverRuntimeTest {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
4D,
|
4D,
|
||||||
RiverWidthProfile.constant(4D),
|
|
||||||
2D,
|
2D,
|
||||||
4D,
|
4D,
|
||||||
|
RiverBodyProfile.constant(4D, 2D, 4D),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
new RiverPolyline(new double[]{-600D, 0D}, new double[]{0D, 0D})
|
new RiverPolyline(new double[]{-600D, 0D}, new double[]{0D, 0D})
|
||||||
@@ -1004,8 +1006,12 @@ public class IrisRiverRuntimeTest {
|
|||||||
.setBankWidth(range(12D))
|
.setBankWidth(range(12D))
|
||||||
.setDepth(range(5D))
|
.setDepth(range(5D))
|
||||||
.setMaxIncision(512)
|
.setMaxIncision(512)
|
||||||
.setMeanderStrength(0D)
|
.setWorms(new KList<>(new IrisRiverWorm()
|
||||||
.setMeanderStyle(flat())
|
.setSeed(1L)
|
||||||
|
.setTortuosity(0D)
|
||||||
|
.setDetailTortuosity(0D)
|
||||||
|
.setMaxOffset(0D)
|
||||||
|
.setSegments(1)))
|
||||||
.setBedRoughness(0D)
|
.setBedRoughness(0D)
|
||||||
.setBedRoughnessStyle(flat())
|
.setBedRoughnessStyle(flat())
|
||||||
.setDryContinuationChance(1D);
|
.setDryContinuationChance(1D);
|
||||||
|
|||||||
Reference in New Issue
Block a user