Way too much

This commit is contained in:
Brian Neumann-Fopiano
2026-08-25 12:52:12 -04:00
parent 2797a5bf22
commit 07cc76614f
131 changed files with 6826 additions and 1094 deletions
+148 -6
View File
@@ -86,12 +86,11 @@ dependencies {
implementation(volmLibCoordinate) {
transitive = false
}
implementation(libs.gson)
implementation(libs.lru)
implementation(libs.caffeine)
implementation(libs.paralithic)
// Dynamically Loaded
slim(libs.gson)
slim(libs.lru)
slim(libs.caffeine)
slim(libs.paralithic)
slim(libs.paperlib)
slim(libs.adventure.api)
slim(libs.adventure.minimessage)
@@ -154,6 +153,8 @@ slimJar {
]
relocate('com.dfsek.paralithic', "${lib}.paralithic")
relocate('com.google.gson', "${lib}.gson")
relocate('com.googlecode.concurrentlinkedhashmap', "${lib}.lru")
relocate('io.papermc.lib', "${lib}.paper")
relocate('net.kyori', "${lib}.kyori")
relocate('org.bstats', "${lib}.metrics")
@@ -285,9 +286,149 @@ List<String> supersededVolmLibPackages = [
'art/arcane/volmlib/util/director/visual/**',
'art/arcane/volmlib/util/value/**',
'art/arcane/volmlib/util/api/**',
'art/arcane/volmlib/util/entity/**'
'art/arcane/volmlib/util/entity/**',
'art/arcane/volmlib/util/config/**',
'art/arcane/volmlib/util/reflect/**',
'art/arcane/volmlib/util/documentation/**',
'art/arcane/iris/core/report/**',
'art/arcane/iris/util/common/inventorygui/**',
'art/arcane/iris/util/common/board/**',
'art/arcane/volmlib/integration/VaultEconomy*.class'
]
List<String> unusedVolmLibClasses = [
'art/arcane/volmlib/util/nbt/io/ParseException',
'art/arcane/volmlib/util/nbt/io/SNBTDeserializer',
'art/arcane/volmlib/util/nbt/io/SNBTParser',
'art/arcane/volmlib/util/nbt/io/SNBTSerializer',
'art/arcane/volmlib/util/nbt/io/SNBTUtil',
'art/arcane/volmlib/util/nbt/io/SNBTWriter',
'art/arcane/volmlib/util/nbt/io/StringPointer',
'art/arcane/volmlib/util/io/StringDeserializer',
'art/arcane/volmlib/util/io/StringSerializer',
'art/arcane/volmlib/util/json/EnumType',
'art/arcane/volmlib/util/json/HTTP',
'art/arcane/volmlib/util/json/HTTPTokener',
'art/arcane/volmlib/util/json/JSONML',
'art/arcane/volmlib/util/json/JSONStringer',
'art/arcane/volmlib/util/json/JSONWriter',
'art/arcane/volmlib/util/json/XML',
'art/arcane/volmlib/util/json/XMLTokener',
'art/arcane/volmlib/util/hud/HudBid',
'art/arcane/volmlib/util/hud/HudBidder',
'art/arcane/volmlib/util/hud/HudLocalLedger',
'art/arcane/volmlib/util/hud/HudTitleClaim',
'art/arcane/volmlib/util/hud/HudTitleService',
'art/arcane/volmlib/util/scheduling/GroupedExecutor',
'art/arcane/volmlib/util/scheduling/SchedulerRuntime',
'art/arcane/volmlib/util/scheduling/SchedulerUtils',
'art/arcane/volmlib/util/scheduling/TaskExecutor',
'art/arcane/volmlib/util/parallel/StreamUtilsSupport',
'art/arcane/volmlib/util/parallel/SyncExecutorSupport',
'art/arcane/volmlib/util/cache/ByteBitCache',
'art/arcane/volmlib/util/cache/DataBitCache',
'art/arcane/volmlib/util/cache/FloatBitCache',
'art/arcane/volmlib/util/cache/IntBitCache',
'art/arcane/volmlib/util/cache/ShortBitCache',
'art/arcane/volmlib/util/cache/UByteBitCache',
'art/arcane/volmlib/util/data/ChunkCache',
'art/arcane/volmlib/util/data/ComplexCache',
'art/arcane/volmlib/util/data/IrisBiomeStorage',
'art/arcane/volmlib/util/data/NibbleArray',
'art/arcane/volmlib/util/data/NibbleDataPalette',
'art/arcane/volmlib/util/data/Recycler',
'art/arcane/volmlib/util/data/VanillaBiomeMap',
'art/arcane/volmlib/util/data/Writable',
'art/arcane/volmlib/util/data/base/ComplexCacheBase',
'art/arcane/volmlib/util/director/DirectorNodeBase',
'art/arcane/volmlib/util/director/DirectorParameterBase',
'art/arcane/volmlib/util/director/theme/DirectorThemes',
'art/arcane/volmlib/util/interpolation/InterpolationMethod',
'art/arcane/volmlib/util/interpolation/IrisInterpolation',
'art/arcane/volmlib/util/math/INode',
'art/arcane/volmlib/util/math/IrisMathHelper',
'art/arcane/volmlib/util/math/KochanekBartelsInterpolation',
'art/arcane/volmlib/util/math/PathInterpolation',
'art/arcane/volmlib/util/math/Spiral',
'art/arcane/volmlib/util/board/BoardManager',
'art/arcane/volmlib/util/board/BoardUpdateTask',
'art/arcane/volmlib/util/bukkit/Placeholders',
'art/arcane/volmlib/util/hunk/HunkFactory',
'art/arcane/volmlib/util/hunk/storage/ArrayHunk',
'art/arcane/volmlib/util/hunk/storage/AtomicDoubleHunk',
'art/arcane/volmlib/util/hunk/storage/AtomicIntegerHunk',
'art/arcane/volmlib/util/hunk/storage/AtomicLongHunk',
'art/arcane/volmlib/util/hunk/storage/SynchronizedArrayHunk',
'art/arcane/volmlib/util/hunk/view/BiomeForceBridge',
'art/arcane/volmlib/util/hunk/view/BiomeGridForceSupport',
'art/arcane/volmlib/util/hunk/view/BiomeGridHunkHolder',
'art/arcane/volmlib/util/hunk/view/BiomeGridHunkView',
'art/arcane/volmlib/util/hunk/view/ChunkDataHunkHolder',
'art/arcane/volmlib/util/hunk/view/RotatedXHunkView',
'art/arcane/volmlib/util/hunk/view/RotatedYHunkView',
'art/arcane/volmlib/util/hunk/view/RotatedZHunkView',
'art/arcane/volmlib/util/exceptions/MissingDimensionException',
'art/arcane/volmlib/util/function/Supplier2',
'art/arcane/volmlib/util/function/Supplier3',
'art/arcane/volmlib/util/hud/HudPriority',
'art/arcane/volmlib/util/interpolation/InterpolationMethod3D',
'art/arcane/volmlib/util/interpolation/InterpolationType',
'art/arcane/volmlib/util/inventorygui/UIRainbowDecorator',
'art/arcane/volmlib/util/io/Converter',
'art/arcane/volmlib/util/matter/MatterPlacer',
'art/arcane/volmlib/util/plugin/SplashScreenSupport',
'art/arcane/volmlib/util/scheduling/Contained',
'art/arcane/volmlib/util/scheduling/IrisLock',
'art/arcane/volmlib/util/scheduling/QueueExecutor',
'art/arcane/volmlib/util/scheduling/S',
'art/arcane/volmlib/util/scheduling/SBase',
'art/arcane/volmlib/util/scheduling/SlidingWindowRateLimiter',
'art/arcane/volmlib/util/scheduling/Switch',
'art/arcane/volmlib/util/scheduling/ThreadMonitor',
'art/arcane/volmlib/util/cache/ByteCache',
'art/arcane/volmlib/util/cache/ChunkCache2DSimple',
'art/arcane/volmlib/util/cache/IntCache',
'art/arcane/volmlib/util/cache/ShortCache',
'art/arcane/volmlib/util/cache/UByteCache',
'art/arcane/volmlib/util/cache/WorldCache2DSimple',
'art/arcane/volmlib/util/data/CuboidException',
'art/arcane/volmlib/util/data/DUTF',
'art/arcane/volmlib/util/data/Heafty',
'art/arcane/volmlib/util/data/InvertedBiomeGrid',
'art/arcane/volmlib/util/data/Shrinkwrap',
'art/arcane/volmlib/util/data/WeightMap',
'art/arcane/volmlib/util/data/WeightedRandom',
'art/arcane/volmlib/util/director/compat/BukkitDirectorContext',
'art/arcane/volmlib/util/director/handlers/base/DummyHandlerBase',
'art/arcane/volmlib/util/director/handlers/base/OptionalWorldHandlerBase',
'art/arcane/volmlib/util/parallel/BurstedHunk',
'art/arcane/volmlib/util/parallel/GridLockSupport',
'art/arcane/volmlib/util/parallel/NoopGridLockSupport',
'art/arcane/volmlib/util/bukkit/ChunkPositionSet',
'art/arcane/volmlib/util/bukkit/Events',
'art/arcane/volmlib/util/json/SingleCollectionTypeFactory',
'art/arcane/volmlib/util/collection/StateList',
'art/arcane/iris/core/pack/IrisPack',
'art/arcane/iris/core/pack/IrisPackRepository',
'art/arcane/iris/core/jobs/DownloadJob',
'art/arcane/iris/core/jobs/JobCollection',
'art/arcane/iris/core/jobs/ParallelQueueJob',
'art/arcane/iris/core/jobs/QueueJob',
'art/arcane/iris/engine/framework/placer/WorldObjectPlacer',
'art/arcane/iris/engine/object/IPostBlockAccess',
'art/arcane/iris/engine/object/IrisPotionEffect',
'art/arcane/iris/engine/platform/DummyChunkGenerator',
'art/arcane/iris/util/common/math/VectorMath',
'art/arcane/iris/util/common/parallel/GridLock',
'art/arcane/iris/util/common/reflect/W',
'art/arcane/iris/util/slimjar/BuildConstants',
'art/arcane/iris/util/slimjar/relocation/meta/AttributeMetaMediator',
'art/arcane/iris/util/slimjar/relocation/meta/AttributeMetaMediatorFactory'
]
List<String> unusedVolmLibClassEntries = unusedVolmLibClasses.collectMany { String classPath ->
[classPath + '.class', classPath + '$*.class']
}
// Annotation-only artifacts pulled in transitively by Gson and Caffeine. Their types appear solely
// in annotation attributes, which the JVM skips silently when the type is absent, so nothing loads
// or links against them at runtime.
@@ -315,6 +456,7 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow
relocate('io.github.slimjar', "${lib}.slimjar")
exclude('modules/loader-agent.isolated-jar')
exclude(supersededVolmLibPackages)
exclude(unusedVolmLibClassEntries)
exclude(annotationOnlyArtifacts)
exclude(dependencyBuildMetadata)
from(embeddedAgentJar.map { it.archiveFile }) {
@@ -39,7 +39,6 @@ import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
@@ -90,6 +89,8 @@ public class ServerConfigurator {
private static final Object DATAPACK_INSTALL_LOCK = new Object();
private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace";
private static final String COMPILER_INPUT_FINGERPRINT_CACHE = "datapack-compiler-input-fingerprint";
static final String POST_COMPILE_RESTART_WARNING = "Iris installed updated datapack registry entries; "
+ "restart the server before creating worlds or opening Studios.";
private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024;
private static volatile boolean loadedDatapackRuntimeReady;
private static volatile String loadedDatapackCompilerInputFingerprint = "";
@@ -419,13 +420,18 @@ public class ServerConfigurator {
&& !reusableRuntimeFingerprint(
loadedDatapackCompilerInputFingerprint,
current);
boolean loadedRegistryRestartRequired = fullInstall
&& loadedCompilerInputsChanged
&& currentRegistryRequiresRestart();
if (!current.isEmpty() && current.equals(cached)) {
IrisLogging.debug("Data packs unchanged, skipping install.");
DatapackInstallResult result = fullInstall && loadedCompilerInputsChanged
DatapackInstallResult result = loadedRegistryRestartRequired
? DatapackInstallResult.restartRequiredResult()
: resultForUnchangedFingerprint(fullInstall, reapply);
if (result.restartRequired()) {
requireDatapackRestart();
} else if (result.succeeded()) {
loadedDatapackCompilerInputFingerprint = current;
}
reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart);
return result;
@@ -435,7 +441,7 @@ public class ServerConfigurator {
resolveDataFixer(),
fullInstall,
reapply);
if (fullInstall && loadedCompilerInputsChanged && result.succeeded()) {
if (loadedRegistryRestartRequired && result.succeeded()) {
result = DatapackInstallResult.restartRequiredResult();
}
if (result.restartRequired()) {
@@ -444,6 +450,7 @@ public class ServerConfigurator {
reportTiming(timingConsumer, "datapack_compile_publish", compileStart);
if (result.succeeded() && !result.restartRequired()) {
writeCompilerInputFingerprintCache(cacheFile.toPath(), current);
loadedDatapackCompilerInputFingerprint = current;
}
reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart);
return result;
@@ -456,6 +463,22 @@ public class ServerConfigurator {
IrisWorldStorage.levelRoot().toPath());
}
private static boolean currentRegistryRequiresRestart() {
try {
Map<String, String> currentRequirements = IrisDatapackCompiler.computeRegistryRequirements(
collectCompilerPackRoots(),
resolveDataFixer());
return runtimeRequiresRegistryRestart(
loadedDatapackRegistryRequirements,
currentRequirements);
} catch (IOException | RuntimeException exception) {
IrisLogging.reportError(
"Unable to compare loaded Iris datapack registry requirements.",
exception);
return true;
}
}
private static List<IrisGeneratorBinding> collectConfiguredLevelStemBindings() throws IOException {
File levelRoot = IrisWorldStorage.levelRoot();
String levelId = levelRoot.getName();
@@ -545,6 +568,19 @@ public class ServerConfigurator {
return true;
}
static boolean runtimeRequiresRegistryRestart(
Map<String, String> loadedRequirements,
Map<String, String> currentRequirements
) {
if (currentRequirements == null) {
return true;
}
if (currentRequirements.isEmpty()) {
return false;
}
return !loadedRegistrySatisfies(loadedRequirements, currentRequirements);
}
static boolean reusableRuntimeFingerprint(String loadedFingerprint, String currentFingerprint) {
return loadedFingerprint != null
&& !loadedFingerprint.isBlank()
@@ -961,31 +997,29 @@ public class ServerConfigurator {
private static boolean verifyDataPacksPost() {
try (Stream<IrisData> stream = allPacks()) {
boolean bad = stream
.map(data -> {
IrisLogging.debug("Checking Pack: " + data.getDataFolder().getPath());
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
return loader.loadAll(loader.getPossibleKeys())
.stream()
.filter(Objects::nonNull)
.map(ServerConfigurator::verifyDataPackInstalled)
.toList()
.contains(false);
})
.toList()
.contains(true);
if (!bad) {
return false;
}
return verifyDataPacksPost(stream);
}
}
static boolean verifyDataPacksPost(Stream<IrisData> packs) {
boolean bad = Objects.requireNonNull(packs, "Iris packs")
.map(data -> {
IrisLogging.debug("Checking Pack: " + data.getDataFolder().getPath());
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
return loader.loadAll(loader.getPossibleKeys())
.stream()
.filter(Objects::nonNull)
.map(dimension -> verifyDataPackInstalled(dimension, false))
.toList()
.contains(false);
})
.toList()
.contains(true);
if (!bad) {
return false;
}
if (INMS.get().supportsDataPacks()) {
// Three sentences, no rules: a separator carries the record's severity too, so a box drawn
// out of equals signs became three more [SEVERE] lines saying nothing.
IrisLogging.error(C.ITALIC + "You need to restart your server to properly generate custom biomes.");
IrisLogging.error(C.ITALIC + "By continuing, Iris will use backup biomes in place of the custom biomes.");
IrisLogging.error(C.UNDERLINE + "Restart the server before generating.");
IrisLogging.warn(POST_COMPILE_RESTART_WARNING);
for (Player i : Bukkit.getOnlinePlayers()) {
if (i.isOp() || i.hasPermission("iris.all")) {
@@ -1054,6 +1088,10 @@ public class ServerConfigurator {
}
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
return verifyDataPackInstalled(dimension, true);
}
private static boolean verifyDataPackInstalled(IrisDimension dimension, boolean reportRuntimeFailure) {
KSet<String> keys = new KSet<>();
boolean warn = false;
@@ -1082,17 +1120,21 @@ public class ServerConfigurator {
Object o = INMS.get().getCustomBiomeBaseFor(i);
if (o == null) {
IrisLogging.warn("The Biome " + i + " is not registered on the server.");
if (reportRuntimeFailure) {
IrisLogging.warn("The Biome " + i + " is not registered on the server.");
}
warn = true;
}
}
if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) {
IrisLogging.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server.");
if (reportRuntimeFailure) {
IrisLogging.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server.");
}
warn = true;
}
if (warn) {
if (warn && reportRuntimeFailure) {
IrisLogging.error("The Pack " + key + " is INCAPABLE of generating custom biomes");
IrisLogging.error("If not done automatically, restart your server before generating with this pack!");
}
@@ -31,6 +31,7 @@ import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -63,7 +64,7 @@ public final class GuiHost {
default void unregisterHotloadHook(Runnable onHotload) {
}
default GuiOverlay overlayFor(Engine engine) {
default GuiOverlay overlayFor(Engine engine, UUID openerId) {
return null;
}
}
@@ -73,6 +73,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
public final class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener {
private static final long serialVersionUID = 2094606939770332040L;
@@ -107,6 +108,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
private static final double KEYBOARD_ZOOM_FACTOR = 1.189207115002721D;
private final JFrame hostFrame;
private final UUID openerId;
private final VisionRenderController controller;
private final Runnable hotloadHook;
private final Timer resizeTimer;
@@ -147,11 +149,12 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
private boolean controlsUpdating;
private boolean closed;
private VisionGUI(JFrame hostFrame, Engine engine) {
private VisionGUI(JFrame hostFrame, Engine engine, UUID openerId) {
this.hostFrame = Objects.requireNonNull(hostFrame, "hostFrame");
this.engine = Objects.requireNonNull(engine, "engine");
this.openerId = openerId;
this.renderer = new IrisRenderer(engine);
this.overlay = GuiHost.get().overlayFor(engine);
this.overlay = GuiHost.get().overlayFor(engine, openerId);
this.controller = new VisionRenderController(this::repaint);
this.hotloadHook = () -> EventQueue.invokeLater(this::refreshContent);
this.notifications = new LinkedHashMap<>();
@@ -201,14 +204,14 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
notificationTimer.start();
}
public static void launch(Engine engine) {
EventQueue.invokeLater(() -> createAndShowGUI(engine));
public static void launch(Engine engine, UUID openerId) {
EventQueue.invokeLater(() -> createAndShowGUI(engine, openerId));
}
private static void createAndShowGUI(Engine engine) {
private static void createAndShowGUI(Engine engine, UUID openerId) {
JFrame frame = new JFrame(IrisLanguage.plain(DesktopUiMessages.VISION_TITLE));
GuiHost.prepareFrame(frame);
VisionGUI vision = new VisionGUI(frame, engine);
VisionGUI vision = new VisionGUI(frame, engine, openerId);
frame.getContentPane().setBackground(BACKGROUND);
frame.setLayout(new BorderLayout());
frame.add(buildToolbar(vision), BorderLayout.NORTH);
@@ -739,7 +742,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
}
engine = reacquired;
renderer = new IrisRenderer(reacquired);
overlay = GuiHost.get().overlayFor(reacquired);
overlay = GuiHost.get().overlayFor(reacquired, openerId);
contentRevision++;
captureHeightRange();
return true;
@@ -758,7 +761,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
return;
}
renderer = new IrisRenderer(engine);
overlay = GuiHost.get().overlayFor(engine);
overlay = GuiHost.get().overlayFor(engine, openerId);
contentRevision++;
captureHeightRange();
requestRender();
@@ -56,6 +56,10 @@ public final class IrisLanguage {
private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+");
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
private static final MessageCatalog CATALOG = IrisMessages.catalog();
private static final List<String> BUKKIT_MESSAGE_IDS = CATALOG.ids().stream()
.filter(id -> !id.startsWith("iris.modded."))
.sorted()
.toList();
private static final LocalizationManager MANAGER = new LocalizationManager(
LocalizationCandidate.english(CATALOG, PluralSelector.oneOther())
);
@@ -369,8 +373,11 @@ public final class IrisLanguage {
}
}
private static LocaleOverlay parseOverlay(String source, String locale, String raw) {
static LocaleOverlay parseOverlay(String source, String locale, String raw) {
JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw);
if (parsed.isJsonArray()) {
return parseCompactBukkitOverlay(source, locale, parsed.getAsJsonArray());
}
if (!parsed.isJsonObject()) {
throw new IllegalArgumentException("Locale source is not a JSON object: " + source);
}
@@ -398,6 +405,49 @@ public final class IrisLanguage {
return builder.build();
}
private static LocaleOverlay parseCompactBukkitOverlay(
String source,
String locale,
JsonArray root
) {
if (root.size() != 2 || !root.get(0).isJsonPrimitive() || !root.get(1).isJsonArray()) {
throw new IllegalArgumentException("Compact locale source is invalid: " + source);
}
if (!locale.equals(normalizeLocale(root.get(0).getAsString()))) {
throw new IllegalArgumentException("Locale source declares a different locale than its file: " + source);
}
JsonArray messages = root.get(1).getAsJsonArray();
if (messages.size() != BUKKIT_MESSAGE_IDS.size()) {
throw new IllegalArgumentException("Compact locale message count is invalid: " + source);
}
LocaleOverlay.Builder builder = LocaleOverlay.builder(source, locale);
for (int i = 0; i < messages.size(); i++) {
JsonElement value = messages.get(i);
if (value == null || value.isJsonNull()) {
continue;
}
appendCompactMessage(builder, BUKKIT_MESSAGE_IDS.get(i), value);
}
return builder.build();
}
private static void appendCompactMessage(
LocaleOverlay.Builder builder,
String key,
JsonElement value
) {
MessageKey definition = CATALOG.key(key);
if (value.isJsonObject() && definition instanceof PluralKey) {
builder.plural(key, readPlural(key, value.getAsJsonObject()));
} else if (value.isJsonArray()) {
builder.lines(key, readLines(key, value.getAsJsonArray()));
} else if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
builder.text(key, value.getAsString());
} else {
throw new IllegalArgumentException("Compact locale value has an invalid shape: " + key);
}
}
private static void appendMessages(LocaleOverlay.Builder builder, JsonObject object, String prefix) {
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
@@ -13,7 +13,7 @@ import java.util.List;
import java.util.Set;
final class PackRiverValidator {
private static final Set<String> WATER_MODES = Set.of("SEA_LEVEL", "TERRACED");
private static final Set<String> WATER_MODES = Set.of("FIXED", "TERRACED");
private static final Set<String> TERMINAL_MODES = Set.of("SUPPRESS", "DRY_CHANNEL", "SINKHOLE_GROTTO");
private static final Set<String> ROUTING_POLICIES = Set.of("ALLOW", "AVOID", "BLOCK");
private static final Set<String> CAVE_MODES = Set.of(
@@ -91,7 +91,7 @@ final class PackRiverValidator {
validateTerrain(packFolder, path + ".terrain", terrain, errors, warnings);
}
if (water != null) {
validateWater(path + ".water", water, errors);
validateWater(path + ".water", water, context.dimension(), errors);
}
if (biomes != null) {
validateBiomePools(
@@ -107,7 +107,15 @@ final class PackRiverValidator {
boolean sinkholeTerminal = terrain != null
&& "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL"));
if (caves != null) {
validateCaves(packFolder, path + ".caves", caves, sinkholeTerminal, errors, warnings);
validateCaves(
packFolder,
path + ".caves",
caves,
context.dimension(),
sinkholeTerminal,
errors,
warnings
);
}
if (topology != null && terrain != null) {
@@ -168,6 +176,7 @@ final class PackRiverValidator {
validateStyledRange(packFolder, terrain, "bankWidth", path, 0D, 2048D, errors, warnings);
validateStyledRange(packFolder, terrain, "depth", path, 1D, 512D, errors, warnings);
validateStyledRange(packFolder, terrain, "tunnelWidthMultiplier", path, 1D, 8D, errors, warnings);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "channelRadiusBonus", 0D, 64D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxChannelWidth", 1D, 2048D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxBankWidth", 0D, 2048D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxDepth", 1D, 512D, errors);
@@ -211,18 +220,50 @@ final class PackRiverValidator {
}
}
private static void validateWater(String path, JSONObject water, List<String> errors) {
private static void validateWater(
String path,
JSONObject water,
JSONObject dimension,
List<String> errors
) {
PackJsonFieldChecks.validateOptionalEnum(path, water, "mode", WATER_MODES, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "poolLength", 8, 4096, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "maximumPoolRise", 0, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "dropHeight", 1, 32, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "fluidHeight", -2048, 2048, errors);
validateFluidPalette(path, water, errors);
String mode = stringValue(water, "mode", "SEA_LEVEL");
String mode = stringValue(water, "mode", "FIXED");
int fluidHeight = integerValue(water, "fluidHeight", 63);
int maximumPoolRise = integerValue(water, "maximumPoolRise", 4);
int dropHeight = integerValue(water, "dropHeight", 1);
if ("TERRACED".equals(mode) && dropHeight > maximumPoolRise) {
errors.add(path + ".dropHeight must not exceed maximumPoolRise in TERRACED mode.");
}
JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight");
int minimumHeight = dimensionHeight == null ? -64 : integerValue(dimensionHeight, "min", -64);
int maximumHeight = dimensionHeight == null ? 320 : integerValue(dimensionHeight, "max", 320);
if (fluidHeight < minimumHeight || fluidHeight > maximumHeight) {
errors.add(path + ".fluidHeight must remain inside dimensionHeight.");
}
if ("TERRACED".equals(mode) && fluidHeight + maximumPoolRise > maximumHeight) {
errors.add(path + ".fluidHeight plus maximumPoolRise must remain inside dimensionHeight.");
}
}
private static void validateFluidPalette(String path, JSONObject water, List<String> errors) {
if (!water.has("fluidPalette")) {
return;
}
Object rawPalette = water.opt("fluidPalette");
if (!(rawPalette instanceof JSONObject palette)) {
errors.add(path + ".fluidPalette must be an object.");
return;
}
Object rawBlocks = palette.opt("palette");
if (!(rawBlocks instanceof JSONArray blocks) || blocks.length() < 1) {
errors.add(path + ".fluidPalette.palette must contain at least one fluid block.");
}
}
private static void validateTopologyComplexity(
@@ -345,9 +386,11 @@ final class PackRiverValidator {
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "depthMultiplier", 0.125D, 8D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "bodyWavelength", 32D, 16384D, errors);
wormPath, worm, "bodyWavelength", 8D, 16384D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "bodyDetailWavelength", 32D, 16384D, errors);
wormPath, worm, "bodyDetailWavelength", 8D, 16384D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "bodyDetailInfluence", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "widthVariation", 0D, 0.875D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
@@ -452,6 +495,7 @@ final class PackRiverValidator {
}
private static void validateCaves(File packFolder, String path, JSONObject caves,
JSONObject dimension,
boolean forceGeneratedGrotto,
List<String> errors, List<String> warnings) {
PackJsonFieldChecks.validateOptionalEnum(path, caves, "mode", CAVE_MODES, errors);
@@ -473,6 +517,19 @@ final class PackRiverValidator {
validateNoiseChance(packFolder, caves, "entry", path, errors);
validateStyle(packFolder, caves, "grottoShapeStyle", path, errors);
validateStyle(packFolder, caves, "grottoWarpStyle", path, errors);
JSONObject deepPools = caves.has("deepPools")
? requireObject(caves, "deepPools", path + ".deepPools", errors)
: null;
if (deepPools != null) {
validateDeepPools(
packFolder,
path + ".deepPools",
deepPools,
dimension,
errors,
warnings
);
}
String mode = stringValue(caves, "mode", "SEALED");
if ("SEALED".equals(mode) && !forceGeneratedGrotto) {
@@ -505,6 +562,86 @@ final class PackRiverValidator {
}
}
private static void validateDeepPools(
File packFolder,
String path,
JSONObject deepPools,
JSONObject dimension,
List<String> errors,
List<String> warnings
) {
PackJsonFieldChecks.validateOptionalBoolean(path, deepPools, "enabled", errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "minimumSpacing", 16, 4096, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "maximumPerReach", 0, 16, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "minimumFluidY", -2048, 2048, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "maximumFluidY", -2048, 2048, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "searchRadius", 0, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "searchAttempts", 1, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "horizontalRadius", 2, 128, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "verticalRadius", 2, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "dryHeadroom", 1, 63, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
path, deepPools, "shapeVariation", 0D, 0.75D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
path, deepPools, "warpStrength", 0D, 64D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "maximumVolume", 64, 1048576, errors);
validateNoiseChance(packFolder, deepPools, "reach", path, errors);
validateStyle(packFolder, deepPools, "shapeStyle", path, errors);
validateStyle(packFolder, deepPools, "warpStyle", path, errors);
validateFluidPalette(path, deepPools, errors);
int minimumFluidY = integerValue(deepPools, "minimumFluidY", -224);
int maximumFluidY = integerValue(deepPools, "maximumFluidY", -104);
int searchRadius = integerValue(deepPools, "searchRadius", 16);
int horizontalRadius = integerValue(deepPools, "horizontalRadius", 18);
int verticalRadius = integerValue(deepPools, "verticalRadius", 8);
int dryHeadroom = integerValue(deepPools, "dryHeadroom", 4);
int maximumVolume = integerValue(deepPools, "maximumVolume", 32768);
if (minimumFluidY > maximumFluidY) {
errors.add(path + ".minimumFluidY must not exceed maximumFluidY.");
}
if (dryHeadroom >= verticalRadius) {
errors.add(path + ".dryHeadroom must be smaller than verticalRadius.");
}
if (searchRadius + horizontalRadius > 128) {
errors.add(path + ".searchRadius plus horizontalRadius must not exceed 128 blocks.");
}
long minimumVolume = grottoVolume(horizontalRadius, verticalRadius);
if (minimumVolume > maximumVolume) {
errors.add(path + ".maximumVolume must be at least " + minimumVolume
+ " to contain the base deep-pool chamber.");
}
if (!booleanValue(deepPools, "enabled", false)) {
return;
}
JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight");
int minimumHeight = dimensionHeight == null ? -64 : integerValue(dimensionHeight, "min", -64);
int maximumHeight = dimensionHeight == null ? 320 : integerValue(dimensionHeight, "max", 320);
int lowestBoundaryY = minimumFluidY - (verticalRadius * 2 - dryHeadroom) - 1;
int highestBoundaryY = maximumFluidY + dryHeadroom + 1;
if (lowestBoundaryY <= minimumHeight || highestBoundaryY >= maximumHeight) {
errors.add(path + " fluid range and chamber envelope must remain inside dimensionHeight.");
}
int maximumPerReach = integerValue(deepPools, "maximumPerReach", 1);
double reachChance = noiseChanceValue(deepPools, "reach", 1D / 3D);
if (maximumPerReach == 0 || reachChance == 0D) {
warnings.add(path + " is enabled but its reach gate cannot accept any pools.");
}
}
private static void validateGrotto(String path, JSONObject caves, List<String> errors) {
int throatRadius = integerValue(caves, "throatRadius", 2);
int dryHeadroom = integerValue(caves, "dryHeadroom", 4);
@@ -65,7 +65,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger();
private static final int ADAPTIVE_SLOW_REQUEST_STEP = 3;
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L;
private static final long CLOSE_DRAIN_WARNING_SECONDS = 60L;
private static final long FLUSH_TIMEOUT_SECONDS = 120L;
private final World world;
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
@@ -773,17 +773,13 @@ public class AsyncPregenMethod implements PregeneratorMethod {
// A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk.
boolean interrupted = Thread.interrupted();
try {
boolean drained = false;
try {
drained = semaphore.tryAcquire(threads, CLOSE_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
interrupted = true;
}
if (!drained) {
IrisLogging.warn("Async pregen close did not drain in " + CLOSE_DRAIN_TIMEOUT_SECONDS
+ "s, continuing degraded. " + metricsSnapshot());
}
interrupted |= awaitDrain(
semaphore,
threads,
CLOSE_DRAIN_WARNING_SECONDS,
TimeUnit.SECONDS,
() -> IrisLogging.warn("Async pregen is still draining outstanding chunks. " + metricsSnapshot())
);
flushAllRemainingChunks();
executor.shutdown();
@@ -797,6 +793,26 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
}
static boolean awaitDrain(
Semaphore semaphore,
int permits,
long warningInterval,
TimeUnit timeUnit,
Runnable onWait
) {
boolean interrupted = false;
while (true) {
try {
if (semaphore.tryAcquire(permits, warningInterval, timeUnit)) {
return interrupted;
}
onWait.run();
} catch (InterruptedException e) {
interrupted = true;
}
}
}
private boolean isCancelled() {
return closing.get() || Thread.currentThread().isInterrupted();
}
@@ -84,19 +84,35 @@ public class IrisProject {
long seed,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
) throws IrisException {
return open(sender, seed, openKind, onDone, System.nanoTime());
}
public CompletableFuture<StudioOpenCoordinator.StudioOpenResult> open(
VolmitSender sender,
long seed,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone,
long requestedAtNanos
) throws IrisException {
if (isOpen()) {
return close().thenCompose(ignored -> openInternal(sender, seed, openKind, onDone));
return close().thenCompose(ignored -> openInternal(
sender,
seed,
openKind,
onDone,
requestedAtNanos));
}
return openInternal(sender, seed, openKind, onDone);
return openInternal(sender, seed, openKind, onDone, requestedAtNanos);
}
private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> openInternal(
VolmitSender sender,
long seed,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
AtomicReference<String> stage = new AtomicReference<>("Queued");
AtomicReference<Double> progress = new AtomicReference<>(0.01D);
@@ -114,7 +130,8 @@ public class IrisProject {
}
progress.set(Math.max(0D, Math.min(0.99D, update.progress())));
},
onDone
onDone,
requestedAtNanos
)
);
StudioOpenProgressReporter.startStudioOpenReporter(sender, stage, progress, complete, failed);
@@ -14,6 +14,7 @@ import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender;
@@ -98,16 +99,11 @@ public final class StudioOpenCoordinator {
public CompletableFuture<Boolean> teleportPlayerToProject(
IrisProject project,
Player player,
AtomicBoolean admission,
long deadlineNanos
Player player
) {
if (project == null || player == null) {
return CompletableFuture.completedFuture(false);
}
AtomicBoolean activeAdmission = Objects.requireNonNull(
admission,
"Studio teleport admission");
PlatformChunkGenerator provider = project.getActiveProvider();
if (provider == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
@@ -123,10 +119,6 @@ public final class StudioOpenCoordinator {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio entry point could not be resolved."));
}
if (System.nanoTime() >= deadlineNanos
|| !activeAdmission.compareAndSet(true, false)) {
return studioTeleportDeadlineFailure("native teleport delegation");
}
CompletableFuture<Boolean> teleport = project.getActiveOpenKind() == StudioOpenKind.STANDARD
? WorldRuntimeControlService.get().teleportInMode(player, entry, GameMode.SPECTATOR)
: WorldRuntimeControlService.get().teleport(player, entry);
@@ -134,25 +126,12 @@ public final class StudioOpenCoordinator {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio native teleport returned no completion future."));
}
long remainingNanos = deadlineNanos - System.nanoTime();
if (remainingNanos <= 0L) {
teleport.completeExceptionally(new TimeoutException(
"Studio teleport deadline expired before native teleport completion."));
return teleport;
}
teleport.orTimeout(remainingNanos, TimeUnit.NANOSECONDS);
return teleport;
}
private <T> CompletableFuture<T> studioTeleportDeadlineFailure(String stageName) {
return CompletableFuture.failedFuture(new TimeoutException(
"Studio teleport deadline expired before " + stageName + "."));
}
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
World world = null;
PlatformChunkGenerator provider = null;
CompletableFuture<Boolean> nativeTeleportFuture = null;
try {
long openStart = System.nanoTime();
long t = openStart;
@@ -181,6 +160,10 @@ public final class StudioOpenCoordinator {
if (provider == null) {
throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\".");
}
World entryWorld = world;
PlatformChunkGenerator entryProvider = provider;
CompletableFuture<Void> entryBootstrap = J.afut(
() -> endStudioEntryBootstrap(entryWorld, entryProvider));
updateStage(request, "apply_world_rules", 0.72D);
final World rulesWorld = world;
@@ -204,9 +187,15 @@ public final class StudioOpenCoordinator {
t = logStudioPhase(request, "resolve_entry_anchor", t, openStart);
updateStage(request, "prepare_structure_rings", 0.79D);
endStudioEntryBootstrap(world, provider);
entryBootstrap.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
t = logStudioPhase(request, "prepare_structure_rings", t, openStart);
updateStage(request, "prepare_generation_caches", 0.88D);
if (entryProvider.getEngine() instanceof IrisEngine irisEngine) {
irisEngine.awaitGenerationCacheWarm();
}
t = logStudioPhase(request, "prepare_generation_caches", t, openStart);
Location entryLocation = entryAnchor;
if (request.openKind().teleportThroughStandardEntry()
@@ -219,7 +208,7 @@ public final class StudioOpenCoordinator {
}
Boolean teleported;
try {
nativeTeleportFuture = WorldRuntimeControlService.get().teleportInMode(
CompletableFuture<Boolean> nativeTeleportFuture = WorldRuntimeControlService.get().teleportInMode(
player,
entryLocation,
GameMode.SPECTATOR);
@@ -227,15 +216,20 @@ public final class StudioOpenCoordinator {
throw new IllegalStateException(
"Studio native teleport returned no completion future.");
}
teleported = nativeTeleportFuture.get(60L, TimeUnit.SECONDS);
} catch (TimeoutException e) {
nativeTeleportFuture.completeExceptionally(e);
throw new IllegalStateException("Studio teleport timed out — destination region may still be generating.");
teleported = nativeTeleportFuture.get();
} catch (ExecutionException e) {
Throwable failure = unwrapFailure(e);
throw new IllegalStateException("Studio teleport failed.", failure);
}
if (!Boolean.TRUE.equals(teleported)) {
throw new IllegalStateException("Studio teleport did not complete successfully.");
}
t = logStudioPhase(request, "teleport_standard_entry", t, openStart);
IrisLogging.info("Studio player %s arrived in %dms: dimension=%s world=%s",
player.getName(),
elapsedMillis(request.requestedAtNanos()),
request.dimensionKey(),
world.getName());
}
updateStage(request, "finalize_open", 1.00D);
@@ -861,10 +855,14 @@ public final class StudioOpenCoordinator {
StudioOpenKind openKind,
boolean retainOnFailure,
Consumer<StudioOpenProgress> progressConsumer,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
public StudioOpenRequest {
openKind = Objects.requireNonNull(openKind, "Studio open kind");
if (requestedAtNanos <= 0L) {
throw new IllegalArgumentException("Studio request time must be positive.");
}
}
public static StudioOpenRequest studioProject(
@@ -874,6 +872,25 @@ public final class StudioOpenCoordinator {
StudioOpenKind openKind,
Consumer<StudioOpenProgress> progressConsumer,
Consumer<World> onDone
) {
return studioProject(
project,
sender,
seed,
openKind,
progressConsumer,
onDone,
System.nanoTime());
}
public static StudioOpenRequest studioProject(
IrisProject project,
VolmitSender sender,
long seed,
StudioOpenKind openKind,
Consumer<StudioOpenProgress> progressConsumer,
Consumer<World> onDone,
long requestedAtNanos
) {
String playerName = sender != null && sender.isPlayer() && sender.player() != null ? sender.player().getName() : null;
return new StudioOpenRequest(
@@ -886,7 +903,8 @@ public final class StudioOpenCoordinator {
openKind,
false,
progressConsumer,
onDone
onDone,
requestedAtNanos
);
}
}
@@ -10,6 +10,7 @@ import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
@@ -41,10 +42,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public final class WorldRuntimeControlService {
private static final int STUDIO_ENTRY_VIEW_DISTANCE = 2;
private static final int MAX_SAFE_ENTRY_HORIZONTAL_RADIUS = 15;
private static final int MAX_SAFE_ENTRY_VERTICAL_SEARCH = 64;
private static final int SAFE_ENTRY_UPWARD_ALLOWANCE = 32;
private static final double BLOCK_CENTER = 0.5D;
private static final double COLLISION_EPSILON = 0.000001D;
private static final Method PLAYER_GET_VIEW_DISTANCE_METHOD = findPlayerMethod("getViewDistance");
private static final Method PLAYER_SET_VIEW_DISTANCE_METHOD = findPlayerMethod("setViewDistance", int.class);
private static final Set<Material> UNSAFE_ENTRY_MATERIALS = Set.of(
Material.CACTUS,
Material.CAMPFIRE,
@@ -302,7 +306,7 @@ public final class WorldRuntimeControlService {
CompletableFuture<Location> future = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
future.complete(findTopSafeLocation(world, source));
future.complete(findTopSafeLocationWithTicket(world, source, chunkX, chunkZ));
} catch (Throwable t) {
future.completeExceptionally(t);
}
@@ -314,6 +318,22 @@ public final class WorldRuntimeControlService {
return future;
}
private static Location findTopSafeLocationWithTicket(
World world,
Location source,
int chunkX,
int chunkZ
) {
boolean ticketAdded = world.addPluginChunkTicket(chunkX, chunkZ, BukkitPlatform.plugin());
try {
return findTopSafeLocation(world, source);
} finally {
if (ticketAdded) {
world.removePluginChunkTicket(chunkX, chunkZ, BukkitPlatform.plugin());
}
}
}
public CompletableFuture<Boolean> teleport(Player player, Location location) {
return scheduleTeleport(player, location, null);
}
@@ -350,6 +370,7 @@ public final class WorldRuntimeControlService {
CompletableFuture<Boolean> future = new CompletableFuture<>();
GameModeRestore modeRestore = new GameModeRestore(player);
PlayerViewDistanceRestore viewDistanceRestore = new PlayerViewDistanceRestore(player);
AtomicReference<CompletableFuture<Boolean>> activeTeleport = new AtomicReference<>();
future.whenComplete((success, failure) -> {
if (Boolean.TRUE.equals(success)) {
@@ -359,6 +380,7 @@ public final class WorldRuntimeControlService {
if (teleport != null && !teleport.isDone()) {
teleport.cancel(false);
}
viewDistanceRestore.restore();
modeRestore.restore();
});
boolean scheduled = J.runEntity(player, () -> {
@@ -368,7 +390,9 @@ public final class WorldRuntimeControlService {
}
if (gameMode != null) {
modeRestore.apply(gameMode);
viewDistanceRestore.apply();
if (future.isDone()) {
viewDistanceRestore.restore();
modeRestore.restore();
return;
}
@@ -393,6 +417,7 @@ public final class WorldRuntimeControlService {
if (Boolean.TRUE.equals(success)) {
if (future.complete(true)) {
viewDistanceRestore.restore();
J.runEntity(player, () -> IrisServices.get(BoardSVC.class).updatePlayer(player));
}
return;
@@ -479,6 +504,7 @@ public final class WorldRuntimeControlService {
z,
minimumFloorY,
maximumFloorY,
source.getBlockY() - 1,
yaw,
pitch
);
@@ -498,13 +524,14 @@ public final class WorldRuntimeControlService {
int z,
int minimumFloorY,
int maximumFloorY,
int preferredFloorY,
float yaw,
float pitch
) {
int highestY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
int startingFloorY = Math.max(minimumFloorY, Math.min(maximumFloorY, highestY));
int lowestFloorY = Math.max(minimumFloorY, startingFloorY - MAX_SAFE_ENTRY_VERTICAL_SEARCH + 1);
for (int floorY = startingFloorY; floorY >= lowestFloorY; floorY--) {
int preferredMaximumY = Math.min(maximumFloorY, preferredFloorY + SAFE_ENTRY_UPWARD_ALLOWANCE);
int startingFloorY = Math.max(minimumFloorY, Math.min(preferredMaximumY, highestY));
for (int floorY = startingFloorY; floorY >= minimumFloorY; floorY--) {
Block floor = world.getBlockAt(x, floorY, z);
if (!isSafeFloor(floor)) {
continue;
@@ -777,6 +804,14 @@ public final class WorldRuntimeControlService {
return method.invoke(instance);
}
private static Method findPlayerMethod(String methodName, Class<?>... parameterTypes) {
try {
return Player.class.getMethod(methodName, parameterTypes);
} catch (NoSuchMethodException ignored) {
return null;
}
}
@FunctionalInterface
interface TeleportExecutor {
CompletableFuture<Boolean> teleport(Player player, Location location);
@@ -822,4 +857,56 @@ public final class WorldRuntimeControlService {
}
}
}
private static final class PlayerViewDistanceRestore {
private final Player player;
private final AtomicBoolean changed;
private final AtomicBoolean restored;
private int previousViewDistance;
private PlayerViewDistanceRestore(Player player) {
this.player = player;
changed = new AtomicBoolean(false);
restored = new AtomicBoolean(false);
}
private void apply() {
if (PLAYER_GET_VIEW_DISTANCE_METHOD == null || PLAYER_SET_VIEW_DISTANCE_METHOD == null) {
return;
}
try {
Object currentValue = PLAYER_GET_VIEW_DISTANCE_METHOD.invoke(player);
if (!(currentValue instanceof Number)) {
return;
}
int currentViewDistance = ((Number) currentValue).intValue();
if (currentViewDistance <= STUDIO_ENTRY_VIEW_DISTANCE) {
return;
}
PLAYER_SET_VIEW_DISTANCE_METHOD.invoke(player, STUDIO_ENTRY_VIEW_DISTANCE);
previousViewDistance = currentViewDistance;
changed.set(true);
} catch (ReflectiveOperationException failure) {
IrisLogging.reportError("Failed to apply a temporary player view distance for a studio teleport.", failure);
}
}
private void restore() {
if (!changed.get() || !restored.compareAndSet(false, true)) {
return;
}
Runnable restoration = () -> {
try {
PLAYER_SET_VIEW_DISTANCE_METHOD.invoke(player, previousViewDistance);
} catch (ReflectiveOperationException failure) {
IrisLogging.reportError("Failed to restore a player's view distance after a studio teleport.", failure);
}
};
if (!J.runEntity(player, restoration)) {
IrisLogging.reportError(
"Failed to restore a player's view distance after a studio teleport.",
new IllegalStateException("The player entity scheduler rejected the view-distance restoration."));
}
}
}
}
@@ -83,7 +83,6 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Pattern;
@@ -95,7 +94,6 @@ import art.arcane.volmlib.util.localization.MessageArgument;
public class StudioSVC implements IrisService {
public static final String WORKSPACE_NAME = "packs";
private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L;
private static final long STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS = 10L;
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
private static final AtomicCache<Integer> counter = new AtomicCache<>();
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
@@ -478,24 +476,14 @@ public class StudioSVC implements IrisService {
public CompletableFuture<Boolean> teleportToActiveProject(Player player) {
Player target = Objects.requireNonNull(player, "Studio teleport player");
AtomicBoolean admission = new AtomicBoolean(true);
long deadlineNanos = System.nanoTime()
+ TimeUnit.SECONDS.toNanos(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS);
CompletableFuture<Boolean> transition = studioTransitions.submit(() -> {
return studioTransitions.submit(() -> {
IrisProject project = activeProject;
if (project == null || !project.isOpen()) {
return CompletableFuture.failedFuture(new IllegalStateException(
"No active Studio project is available for teleport."));
}
return StudioOpenCoordinator.get().teleportPlayerToProject(
project,
target,
admission,
deadlineNanos);
return StudioOpenCoordinator.get().teleportPlayerToProject(project, target);
});
transition.orTimeout(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
transition.whenComplete((ignored, failure) -> admission.set(false));
return transition;
}
public void open(VolmitSender sender, String dimm) {
@@ -565,13 +553,20 @@ public class StudioSVC implements IrisService {
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
) throws IrisException {
long requestedAtNanos = System.nanoTime();
if (reportPackAdmissionFailure(sender, dimm) != null) {
return;
}
StudioOpenCoordinator.StudioOpenKind requiredOpenKind = Objects.requireNonNull(
openKind,
"Studio open kind");
studioTransitions.submit(() -> replaceActiveProject(sender, seed, dimm, requiredOpenKind, onDone))
studioTransitions.submit(() -> replaceActiveProject(
sender,
seed,
dimm,
requiredOpenKind,
onDone,
requestedAtNanos))
.whenComplete((ignored, throwable) -> {
if (throwable == null) {
return;
@@ -591,6 +586,7 @@ public class StudioSVC implements IrisService {
Runnable beforeOpen,
Consumer<World> onDone
) {
long requestedAtNanos = System.nanoTime();
BrokenPackException failure = reportPackAdmissionFailure(sender, dimension);
if (failure != null) {
return CompletableFuture.failedFuture(failure);
@@ -601,7 +597,8 @@ public class StudioSVC implements IrisService {
dimension,
Objects.requireNonNull(openKind, "Studio open kind"),
Objects.requireNonNull(beforeOpen, "Studio before-open callback"),
Objects.requireNonNull(onDone, "Studio open completion callback")));
Objects.requireNonNull(onDone, "Studio open completion callback"),
requestedAtNanos));
}
private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> replaceActiveProjectTracked(
@@ -610,7 +607,8 @@ public class StudioSVC implements IrisService {
String dimension,
StudioOpenCoordinator.StudioOpenKind openKind,
Runnable beforeOpen,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
return closeActiveProject().thenCompose(closeResult -> {
if (closeResult == null) {
@@ -621,7 +619,13 @@ public class StudioSVC implements IrisService {
return CompletableFuture.failedFuture(closeResult.failureCause());
}
beforeOpen.run();
return beginStudioOpenTracked(sender, seed, dimension, openKind, onDone);
return beginStudioOpenTracked(
sender,
seed,
dimension,
openKind,
onDone,
requestedAtNanos);
});
}
@@ -630,13 +634,14 @@ public class StudioSVC implements IrisService {
long seed,
String dimension,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
activeProject = project;
CompletableFuture<StudioOpenCoordinator.StudioOpenResult> opening;
try {
opening = project.open(sender, seed, openKind, onDone);
opening = project.open(sender, seed, openKind, onDone, requestedAtNanos);
} catch (IrisException exception) {
if (activeProject == project) {
activeProject = null;
@@ -663,7 +668,8 @@ public class StudioSVC implements IrisService {
long seed,
String dimension,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> {
if (closeThrowable != null) {
@@ -691,7 +697,13 @@ public class StudioSVC implements IrisService {
}
return true;
}).thenCompose(closed -> closed
? beginStudioOpen(sender, seed, dimension, openKind, onDone)
? beginStudioOpen(
sender,
seed,
dimension,
openKind,
onDone,
requestedAtNanos)
: CompletableFuture.completedFuture(null));
}
@@ -700,7 +712,8 @@ public class StudioSVC implements IrisService {
long seed,
String dimension,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
activeProject = project;
@@ -710,7 +723,8 @@ public class StudioSVC implements IrisService {
sender,
seed,
openKind,
onDone);
onDone,
requestedAtNanos);
} catch (IrisException e) {
if (activeProject == project) {
activeProject = null;
@@ -306,6 +306,9 @@ public class IrisCreator {
world = J.sfut(() -> INMS.get().createWorldAsync(wc, request))
.thenCompose(Function.identity())
.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (!studio && !benchmark) {
awaitInitialSpawnPreparation(access, name);
}
} catch (Throwable e) {
done.set(true);
cancelRepeatingTask(createProgressTask);
@@ -603,6 +606,21 @@ public class IrisCreator {
return taskId;
}
static void awaitInitialSpawnPreparation(
PlatformChunkGenerator generator,
String worldName
) throws InterruptedException, ExecutionException, TimeoutException {
CompletableFuture<Void> initialSpawnReady = Objects.requireNonNull(
generator.getInitialSpawnReady(),
"Initial spawn preparation future");
try {
initialSpawnReady.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (TimeoutException failure) {
throw new TimeoutException("Initial spawn preparation timed out for world \""
+ worldName + "\".");
}
}
private AtomicInteger startPregenProgressReporter(
AtomicDouble progress,
AtomicBoolean done,
@@ -29,10 +29,14 @@ import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.object.IrisRiverDeepPools;
import art.arcane.iris.engine.object.IrisRiverNetwork;
import art.arcane.iris.engine.object.IrisRiverOverride;
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
import art.arcane.iris.engine.object.IrisRiverWaterMode;
import art.arcane.iris.engine.object.IrisRiverWater;
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext;
@@ -40,6 +44,7 @@ import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
@@ -66,6 +71,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@@ -132,6 +138,8 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
private ProceduralStream<PlatformBlockState> rockStream;
private ProceduralStream<PlatformBlockState> fluidStream;
private ProceduralStream<PlatformBlockState> riverFluidStream;
private ProceduralStream<PlatformBlockState> riverDeepPoolFluidStream;
private IrisBiome focusBiome;
private IrisRegion focusRegion;
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
@@ -213,6 +221,29 @@ public class IrisComplex implements DataProvider {
.select(engine.getDimension().getRockPalette().getBlockData(data));
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
.select(engine.getDimension().getFluidPalette().getBlockData(data));
IrisRiverNetwork configuredRivers = engine.getDimension().getRivers();
IrisRiverWater configuredRiverWater = configuredRivers == null ? null : configuredRivers.getWater();
riverFluidStream = configuredRivers != null && configuredRivers.isEnabled()
? configuredFluidStream(
Objects.requireNonNull(configuredRiverWater).getFluidPalette(),
rng.nextParallelRNG(79),
"River water"
)
: fluidStream;
IrisRiverCaves configuredRiverCaves = configuredRivers == null ? null : configuredRivers.getCaves();
IrisRiverDeepPools configuredDeepPools = configuredRiverCaves == null
? null
: configuredRiverCaves.getDeepPools();
riverDeepPoolFluidStream = configuredRivers != null
&& configuredRivers.isEnabled()
&& configuredDeepPools != null
&& configuredDeepPools.isEnabled()
? configuredFluidStream(
configuredDeepPools.getFluidPalette(),
rng.nextParallelRNG(80),
"River deep-pool"
)
: riverFluidStream;
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
.zoom(engine.getDimension().getRegionZoom());
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
@@ -303,16 +334,16 @@ public class IrisComplex implements DataProvider {
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
naturalHeightStream,
bridgeStream,
focusBiome,
fluidHeight,
engine.getDimension().getRivers().getWater().getMode()
focusBiome
).cache2D("naturalOceanStream", engine, cacheSize);
int riverFluidHeight = engine.getDimension().getRivers().getWater().getFluidHeight()
- engine.getDimension().getMinHeight();
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
engine.getSeedManager().getBodies(),
engine.getDimension().getRivers(),
data,
riverFluidHeight,
(int) Math.round(fluidHeight),
IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()),
IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()),
@@ -458,23 +489,49 @@ public class IrisComplex implements DataProvider {
}
static ProceduralStream<Boolean> createNaturalOceanStream(
ProceduralStream<Double> naturalHeightStream,
ProceduralStream<InferredType> bridgeStream,
IrisBiome focusBiome,
double fluidHeight,
IrisRiverWaterMode waterMode
IrisBiome focusBiome
) {
if (focusBiome != null) {
boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN);
}
if (waterMode == IrisRiverWaterMode.SEA_LEVEL) {
return bridgeStream.convert(type -> type == InferredType.SEA);
return bridgeStream.convert(type -> type == InferredType.SEA);
}
private ProceduralStream<PlatformBlockState> configuredFluidStream(
IrisMaterialPalette palette,
RNG fluidRng,
String configurationName
) {
Objects.requireNonNull(palette, configurationName + " fluidPalette must be configured");
KList<PlatformBlockState> blocks = palette.getBlockData(data);
if (blocks.isEmpty()) {
throw new IllegalArgumentException(
configurationName + " fluidPalette must resolve at least one fluid block");
}
return ProceduralStream.of(
(x, z) -> naturalHeightStream.getDouble(x, z) < fluidHeight - 1D,
Interpolated.BOOLEAN
);
for (PlatformBlockState block : blocks) {
if (block == null || !block.isFluid()) {
throw new IllegalArgumentException(
configurationName + " fluidPalette may contain only fluid blocks");
}
}
return palette.getLayerGenerator(fluidRng, data).stream().select(blocks);
}
public PlatformBlockState resolveRiverCaveFluid(RiverCaveFluidKind fluidKind, double x, double z) {
return switch (Objects.requireNonNull(fluidKind)) {
case RIVER -> riverFluidStream.get(x, z);
case DEEP_POOL -> riverDeepPoolFluidStream.get(x, z);
};
}
public PlatformBlockState resolveSurfaceFluid(double x, double z) {
IrisRiverSurfaceSample sample = riverSurfaceStream.get(x, z);
if (sample.river().present() && sample.surfaceFluid()) {
return riverFluidStream.get(x, z);
}
return fluidStream.get(x, z);
}
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
@@ -71,6 +71,7 @@ import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -93,6 +94,7 @@ public class IrisEngine implements Engine {
private final ChronoLatch perSecondLatch;
private final ChronoLatch perSecondBudLatch;
private final EngineMetrics metrics;
private final CompletableFuture<Void> generationCacheWarm;
private final boolean studio;
private final AtomicRollingSequence wallClock;
@Getter(AccessLevel.NONE)
@@ -186,6 +188,7 @@ public class IrisEngine implements Engine {
bud = new AtomicInteger(0);
buds = new AtomicInteger(0);
metrics = new EngineMetrics(32);
generationCacheWarm = new CompletableFuture<>();
cleanLatch = new ChronoLatch(10000);
generatedLast = new AtomicInteger(0);
perSecond = new AtomicDouble(0);
@@ -232,16 +235,17 @@ public class IrisEngine implements Engine {
runtimeBuilder.publishRuntime(initialRuntime, null);
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
_t0 = M.ms();
phaseStartedAt = System.nanoTime();
if (requiredMode.warmGenerationCaches()) {
GenerationCacheWarmer.warm(this);
if (requiredMode.studio()) {
startGenerationCacheWarm(phaseStartedAt);
} else {
warmGenerationCaches(phaseStartedAt);
}
} else {
generationCacheWarm.complete(null);
logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
}
IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - _t0) + "ms");
logStudioInitializationPhase(
"generation_cache_warm",
phaseStartedAt,
!requiredMode.warmGenerationCaches());
EngineTickRegistry.registerTicking(this);
} catch (Throwable e) {
shutdownSequence.cleanupFailedConstruction(e);
@@ -250,6 +254,21 @@ public class IrisEngine implements Engine {
IrisLogging.debug("Engine Initialized " + getCacheID());
}
public void awaitGenerationCacheWarm() {
if (generationCacheWarm.isDone() && !generationCacheWarm.isCompletedExceptionally()) {
return;
}
try {
generationCacheWarm.join();
} catch (CompletionException failure) {
throw new IllegalStateException("Iris generation caches did not become ready.", failure.getCause());
}
}
public boolean isGenerationCacheWarmPending() {
return !generationCacheWarm.isDone();
}
private void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) {
if (!studio) {
return;
@@ -262,6 +281,32 @@ public class IrisEngine implements Engine {
Boolean.toString(skipped));
}
private void startGenerationCacheWarm(long phaseStartedAtNanos) {
if (!backgroundTasks.scheduleTrackedTask(() -> warmGenerationCaches(phaseStartedAtNanos))) {
throw new IllegalStateException("Iris background task admission closed before generation cache warming.");
}
}
private void warmGenerationCaches(long phaseStartedAtNanos) {
long startedAtMillis = M.ms();
try {
GenerationCacheWarmer.warm(this);
generationCacheWarm.complete(null);
} catch (Throwable failure) {
generationCacheWarm.completeExceptionally(failure);
if (failure instanceof Error error) {
throw error;
}
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new IllegalStateException("Generation cache warming failed.", failure);
} finally {
IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - startedAtMillis) + "ms");
logStudioInitializationPhase("generation_cache_warm", phaseStartedAtNanos, false);
}
}
private void verifySeed() {
if (getEngineData().getSeed() != null && getEngineData().getSeed() != target.getWorld().getRawWorldSeed()) {
target.getWorld().setRawWorldSeed(getEngineData().getSeed());
@@ -323,6 +368,7 @@ public class IrisEngine implements Engine {
@Override
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
awaitGenerationCacheWarm();
try (GenerationSessionLease lease = acquireGenerationLease("matter_generate");
IrisContext.Scope ignored = IrisContext.open(this, lease.sessionId(), context)) {
IrisComplex activeComplex = getComplex();
@@ -574,6 +620,7 @@ public class IrisEngine implements Engine {
@BlockCoordinates
@Override
public void generate(int x, int z, Hunk<PlatformBlockState> vblocks, Hunk<PlatformBiome> vbiomes, boolean multicore) throws WrongEngineBroException {
awaitGenerationCacheWarm();
try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate");
IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) {
getEngineData().getStatistics().generatedChunk();
@@ -48,7 +48,6 @@ import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@EqualsAndHashCode(callSuper = true)
@Data
@@ -78,7 +77,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
final WorldBlockDropRouter blockDropRouter = new WorldBlockDropRouter(this);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup(this);
final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup();
private boolean looperStopped;
private volatile boolean cleanupServiceStopped;
volatile int entityCount = 0;
@@ -193,10 +192,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
};
}
AtomicBoolean ignoreTeleport() {
return ignoreTP;
}
@Override
public void onTick() {
@@ -18,87 +18,53 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.common.scheduling.jobs.QueueJob;
import art.arcane.volmlib.util.collection.KList;
import io.papermc.lib.PaperLib;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* Cancels a teleport, loads the destination chunks off the main thread and then replays the teleport
* on the thread that owns the player. The replay sets the manager's ignore flag so the reissued
* teleport is not intercepted again.
*/
final class WorldTeleportWarmup {
private final IrisWorldManager manager;
WorldTeleportWarmup(IrisWorldManager manager) {
this.manager = manager;
}
void teleportAsync(PlayerTeleportEvent e) {
Location destination = e.getTo();
if (destination == null) {
return;
}
Player player = e.getPlayer();
PlayerTeleportEvent.TeleportCause cause = e.getCause();
e.setCancelled(true);
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), manager.managedTask(
"bukkit_world_manager_teleport",
() -> {
manager.ignoreTeleport().set(true);
e.getPlayer().teleport(e.getTo(), e.getCause());
manager.ignoreTeleport().set(false);
})));
CompletableFuture<Boolean> teleport;
try {
teleport = BukkitPlatform.teleportAsync(
player,
destination.clone(),
cause);
} catch (Throwable failure) {
reportFailure(player, destination, failure);
return;
}
if (teleport == null) {
reportFailure(player, destination, new IllegalStateException(
"Async teleport returned no completion future."));
return;
}
teleport.whenComplete((success, failure) -> {
if (failure != null) {
reportFailure(player, destination, failure);
} else if (!Boolean.TRUE.equals(success)) {
reportFailure(player, destination, new IllegalStateException(
"Async teleport did not complete successfully."));
}
});
}
private void warmupAreaAsync(Player player, Location to, Runnable r) {
J.a(manager.managedTask("bukkit_world_manager_teleport_warmup", () -> {
int viewDistance = 2;
KList<Future<Chunk>> futures = new KList<>();
for (int i = -viewDistance; i <= viewDistance; i++) {
for (int j = -viewDistance; j <= viewDistance; j++) {
int finalJ = j;
int finalI = i;
if (to.getWorld().isChunkLoaded((to.getBlockX() >> 4) + i, (to.getBlockZ() >> 4) + j)) {
futures.add(CompletableFuture.completedFuture(null));
continue;
}
futures.add(MultiBurst.burst.completeValue(()
-> PaperLib.getChunkAtAsync(to.getWorld(),
(to.getBlockX() >> 4) + finalI,
(to.getBlockZ() >> 4) + finalJ,
true, false).get()));
}
}
new QueueJob<Future<Chunk>>() {
@Override
public void execute(Future<Chunk> chunkFuture) {
try {
chunkFuture.get();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
IrisLogging.debug("Chunk warmup interrupted while loading async teleport chunk.");
} catch (ExecutionException ex) {
IrisLogging.reportError(ex);
}
}
@Override
public String getName() {
return IrisLanguage.text(RuntimeUiMessages.JOB_LOADING_CHUNKS);
}
}.queue(futures).execute(new VolmitSender(player), true, r);
}));
private void reportFailure(Player player, Location destination, Throwable failure) {
IrisLogging.error("Async teleport into Iris world failed for " + player.getName()
+ " at " + destination.getBlockX() + ", " + destination.getBlockY() + ", "
+ destination.getBlockZ() + ".");
IrisLogging.reportError(failure);
}
}
@@ -102,7 +102,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
}
if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j))
&& height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, j))) {
&& height + 1 < output.getHeight() && B.isFluid(output.get(i, height + 1, j))) {
getSeaSurfaceDecorator().decorate(i, j,
realX, Math.round(i + 1), Math.round(x + i - 1),
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
@@ -92,7 +92,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
boolean hideOres = dimension.isHideOresForHiddenOre();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
ChunkedDataCache<PlatformBlockState> fluidCache = context.getFluid();
ChunkedDataCache<PlatformBlockState> rockCache = context.getRock();
int realX = xf + x;
UpperDimensionContext upperContext = getEngine().getUpperContext();
@@ -118,7 +117,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
}
int topY = Math.min(hf, chunkHeight - 1);
PlatformBlockState fluid = fluidCache.get(xf, zf);
PlatformBlockState fluid = complex.resolveSurfaceFluid(realX, realZ);
PlatformBlockState rock = rockCache.get(xf, zf);
PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
@@ -26,12 +26,16 @@ import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.event.world.WorldSaveEvent;
@@ -45,7 +49,6 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
private boolean listenerRegistered;
private boolean closeRequested;
private int taskId;
protected AtomicBoolean ignoreTP = new AtomicBoolean(false);
public EngineAssignedWorldManager() {
super(null, null);
@@ -126,6 +129,29 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
});
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void on(PlayerTeleportEvent e) {
if (!BukkitPlatform.isPaperServer()
|| !getEngine().isStudio()
|| e.getCause() != PlayerTeleportEvent.TeleportCause.COMMAND) {
return;
}
Location destination = e.getTo();
World targetWorld = BukkitWorldBinding.world(getTarget().getWorld());
if (destination == null || targetWorld == null || !targetWorld.equals(destination.getWorld())) {
return;
}
int chunkX = destination.getBlockX() >> 4;
int chunkZ = destination.getBlockZ() >> 4;
if (targetWorld.isChunkLoaded(chunkX, chunkZ)) {
return;
}
runManagerTask("bukkit_world_manager_teleport_event", () -> teleportAsync(e));
}
@EventHandler
public void on(ChunkLoadEvent e) {
runManagerTask("bukkit_world_manager_chunk_load", () -> {
@@ -38,6 +38,19 @@ public interface MantleComponent extends Comparable<MantleComponent> {
return 0;
}
default int getInputRadius(
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius,
ChunkContext context
) {
return getInputRadius();
}
default boolean isInputGenerationLazy() {
return false;
}
default IrisData getData() {
return getEngineMantle().getData();
}
@@ -66,9 +66,21 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
private final AtomicReferenceArray<MantleChunk<Matter>> window;
public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) {
this(engineMantle, mantle, x, z, radius, radius * 2, multicore);
}
public MantleWriter(
EngineMantle engineMantle,
Mantle<Matter> mantle,
int x,
int z,
int prefetchRadius,
int accessRadius,
boolean multicore
) {
this.engineMantle = engineMantle;
this.mantle = mantle;
this.radius = radius * 2;
this.radius = accessRadius;
this.x = x;
this.z = z;
// Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the
@@ -78,7 +90,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
final boolean foliaMaintenance = J.isFolia()
&& WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity());
final int parallelism = foliaMaintenance ? 1 : (multicore ? Runtime.getRuntime().availableProcessors() / 2 : 4);
final int parallelism = resolvePrefetchParallelism(
foliaMaintenance,
multicore,
Runtime.getRuntime().availableProcessors());
if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) {
IrisLogging.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + ".");
}
@@ -86,10 +101,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
// prefetch must release the permits already pinned into the window here.
try {
mantle.getChunks(
x - radius,
x + radius,
z - radius,
z + radius,
x - prefetchRadius,
x + prefetchRadius,
z - prefetchRadius,
z + prefetchRadius,
parallelism,
this::storePrefetchedChunk
);
@@ -99,6 +114,16 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
}
}
static int resolvePrefetchParallelism(boolean foliaMaintenance, boolean multicore, int availableProcessors) {
if (foliaMaintenance) {
return 1;
}
if (!multicore) {
return 4;
}
return Math.max(1, availableProcessors / 2);
}
private static Set<IrisPosition> getBallooned(Set<IrisPosition> vset, double radius) {
Set<IrisPosition> returnset = new HashSet<>();
int ceilrad = (int) Math.ceil(radius);
@@ -4,6 +4,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -43,18 +44,31 @@ public interface MatterGenerator {
return;
}
int writeRadius = getRadius();
MatterGenerationPlan generationPlan = resolveGenerationPlan(x, z, context);
MatterPassPlan[] passPlans = generationPlan.passPlans();
if (passPlans.length == 0) {
return;
}
int prefetchRadius = passPlans[0].passChunkRadius();
LongOpenHashSet partialChunks = new LongOpenHashSet();
try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, multicore)) {
try (MantleWriter writer = new MantleWriter(
getEngine().getMantle(),
getMantle(),
x,
z,
prefetchRadius,
generationPlan.writerAccessRadius(),
multicore)) {
// Every task launched below writes through this writer. They must all be finished before
// close() releases the cached chunks, even when a pass throws, or detached pool threads
// write into released chunks.
List<MatterComponentTask> outstandingTasks = null;
try {
for (MantlePass pass : getComponents()) {
int passRadius = pass.passChunkRadius();
for (MatterPassPlan passPlan : passPlans) {
MantlePass pass = passPlan.pass();
int passRadius = passPlan.passChunkRadius();
List<MantleComponent> passComponents = pass.components();
MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()];
int[] componentPassRadii = new int[passComponents.size()];
@@ -63,7 +77,7 @@ public interface MatterGenerator {
if (component.isEnabled()) {
// A component must cover its own reach plus every later pass' reach, or a
// later pass reads this component's data from chunks it never wrote.
int componentReach = component.getRadius() + pass.downstreamBlockRadius();
int componentReach = component.getRadius() + passPlan.downstreamBlockRadius();
componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0;
enabledComponents[enabledComponentCount++] = component;
}
@@ -112,7 +126,8 @@ public interface MatterGenerator {
MantleChunk<Matter> chunk = writer.acquireChunk(passX, passZ);
if (chunk == null) {
throw new IllegalStateException("Mantle pass chunk " + passX + "," + passZ
+ " is outside the writer prepared at " + x + "," + z + " with radius " + writeRadius);
+ " is outside the writer prepared at " + x + "," + z
+ " with radius " + generationPlan.writerAccessRadius());
}
if (chunk.isFlagged(MantleFlag.PLANNED)) {
@@ -174,8 +189,9 @@ public interface MatterGenerator {
}
}
for (int i = -getRealRadius(); i <= getRealRadius(); i++) {
for (int j = -getRealRadius(); j <= getRealRadius(); j++) {
int realRadius = passPlans[passPlans.length - 1].passChunkRadius();
for (int i = -realRadius; i <= realRadius; i++) {
for (int j = -realRadius; j <= realRadius; j++) {
int realX = x + i;
int realZ = z + j;
long realKey = chunkKey(realX, realZ);
@@ -195,6 +211,45 @@ public interface MatterGenerator {
return (((long) x) << 32) ^ (z & 0xffffffffL);
}
private MatterGenerationPlan resolveGenerationPlan(int x, int z, ChunkContext context) {
List<MantlePass> passes = getComponents();
MatterPassPlan[] plans = new MatterPassPlan[passes.size()];
int accessDownstreamBlockRadius = 0;
int generationDownstreamBlockRadius = 0;
for (int passIndex = passes.size() - 1; passIndex >= 0; passIndex--) {
MantlePass pass = passes.get(passIndex);
int passBlockRadius = 0;
int passAccessInputRadius = 0;
int passGenerationInputRadius = 0;
for (MantleComponent component : pass.components()) {
if (!component.isEnabled()) {
continue;
}
int componentRadius = component.getRadius();
passBlockRadius = Math.max(passBlockRadius, componentRadius);
int componentReach = componentRadius + generationDownstreamBlockRadius;
int invocationChunkRadius = componentReach > 0
? Math.ceilDiv(componentReach, 16)
: 0;
int componentInputRadius = component.getInputRadius(x, z, invocationChunkRadius, context);
passAccessInputRadius = Math.max(passAccessInputRadius, componentInputRadius);
if (!component.isInputGenerationLazy()) {
passGenerationInputRadius = Math.max(passGenerationInputRadius, componentInputRadius);
}
}
int accessInvocationRadius = accessDownstreamBlockRadius + passBlockRadius;
int generationInvocationRadius = generationDownstreamBlockRadius + passBlockRadius;
int passChunkRadius = generationInvocationRadius > 0 ? Math.ceilDiv(generationInvocationRadius, 16) : 0;
plans[passIndex] = new MatterPassPlan(pass, passChunkRadius, generationDownstreamBlockRadius);
accessDownstreamBlockRadius = accessInvocationRadius + passAccessInputRadius;
generationDownstreamBlockRadius = generationInvocationRadius + passGenerationInputRadius;
}
int writerChunkRadius = accessDownstreamBlockRadius > 0
? Math.ceilDiv(accessDownstreamBlockRadius, 16)
: 0;
return new MatterGenerationPlan(plans, writerChunkRadius * 2);
}
private MatterComponentTask runComponentAsync(
MantleChunk<Matter> chunk,
MantleComponent component,
@@ -217,11 +272,21 @@ public interface MatterGenerator {
try {
if (DISPATCHER.ownsCurrentThread()) {
completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context);
completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context, IrisContext.get());
return new MatterComponentTask(key, future, null);
}
Future<?> submission = DISPATCHER.submit(() -> completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context));
IrisContext callerContext = IrisContext.get();
Future<?> submission = DISPATCHER.submit(() -> completeComponentTask(
future,
key,
chunk,
component,
writer,
chunkX,
chunkZ,
context,
callerContext));
return new MatterComponentTask(key, future, submission);
} catch (Throwable throwable) {
IN_FLIGHT_COMPONENTS.remove(key, future);
@@ -329,10 +394,11 @@ public interface MatterGenerator {
MantleWriter writer,
int chunkX,
int chunkZ,
ChunkContext context
ChunkContext context,
IrisContext callerContext
) {
try {
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext);
future.complete(null);
} catch (Throwable throwable) {
future.completeExceptionally(throwable);
@@ -342,6 +408,28 @@ public interface MatterGenerator {
}
}
private void runComponentWithContext(
MantleChunk<Matter> chunk,
MantleComponent component,
MantleWriter writer,
int chunkX,
int chunkZ,
ChunkContext context,
IrisContext callerContext
) {
if (callerContext == null) {
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
return;
}
try (IrisContext.Scope ignored = IrisContext.open(
callerContext.getEngine(),
callerContext.getGenerationSessionId(),
callerContext.getChunkContext())) {
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
}
}
private void runComponentInline(
MantleChunk<Matter> chunk,
MantleComponent component,
@@ -360,6 +448,12 @@ public interface MatterGenerator {
record MatterComponentTask(MatterTaskKey key, CompletableFuture<Void> future, Future<?> submission) {
}
record MatterPassPlan(MantlePass pass, int passChunkRadius, int downstreamBlockRadius) {
}
record MatterGenerationPlan(MatterPassPlan[] passPlans, int writerAccessRadius) {
}
final class MatterTaskKey {
private final Mantle<Matter> mantle;
private final int chunkX;
@@ -27,6 +27,7 @@ final class CaveCarveScratch {
final int[] fluidMaxY = new int[256];
final int[] surfaceBreakFloorY = new int[256];
final boolean[] surfaceBreakColumn = new boolean[256];
final boolean[] surfaceCeilingColumn = new boolean[256];
final double[] columnThreshold = new double[256];
final double[] passThreshold = new double[256];
final double[] fullWeights = new double[256];
@@ -62,6 +63,7 @@ final class CaveCarveScratch {
int activeModulesY = Integer.MIN_VALUE;
int activeModuleCount;
double[] verticalEdgeFade = new double[0];
double[] surfaceClosureThreshold = new double[0];
MatterCavern[] matterByY = new MatterCavern[0];
Matter[] sectionMatter = new Matter[0];
MatterSlice<?>[] sectionSlices = new MatterSlice<?>[0];
@@ -20,13 +20,15 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
private final CNG warpY;
private final CNG warpZ;
private final double warpStrength;
private final double boundaryVariation;
ConfiguredRiverGrottoShape(
long seed,
IrisData data,
IrisGeneratorStyle shapeStyle,
IrisGeneratorStyle warpStyle,
double warpStrength
double warpStrength,
double boundaryVariation
) {
IrisGeneratorStyle resolvedShape = shapeStyle == null
? new IrisGeneratorStyle(NoiseStyle.FLAT)
@@ -39,6 +41,7 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data);
warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data);
this.warpStrength = Math.max(0D, warpStrength);
this.boundaryVariation = Math.max(0D, Math.min(0.75D, boundaryVariation));
}
@Override
@@ -60,7 +63,7 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius))
+ (warpedY * warpedY / (verticalRadius * verticalRadius))
+ (warpedZ * warpedZ / (horizontalRadius * horizontalRadius));
double boundary = shape.fitDouble(-0.2D, 0.2D, worldX, worldY, worldZ);
double boundary = shape.fitDouble(-boundaryVariation, boundaryVariation, worldX, worldY, worldZ);
return normalized <= 1D + boundary;
}
}
@@ -48,6 +48,8 @@ public class IrisCaveCarver3D {
private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8;
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D;
private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D;
private static final int SURFACE_CEILING_FADE_DEPTH = 12;
private static final double SURFACE_CEILING_SOLID_EPSILON = 0.000001D;
private final Engine engine;
private final IrisData data;
@@ -250,9 +252,11 @@ public class IrisCaveCarver3D {
int[] fluidMaxY = scratch.fluidMaxY;
int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY;
boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn;
boolean[] surfaceCeilingColumn = scratch.surfaceCeilingColumn;
double[] columnThreshold = scratch.columnThreshold;
double[] clampedWeights = scratch.clampedColumnWeights;
double[] verticalEdgeFade = prepareVerticalEdgeFadeTable(scratch, minY, maxY);
prepareSurfaceClosureThresholdTable(scratch, minY, maxY);
MatterCavern[] matterByY = prepareMatterByYTable(scratch, minY, maxY);
prepareSectionCaches(scratch, minY, maxY);
@@ -284,7 +288,8 @@ public class IrisCaveCarver3D {
} else {
columnSurfaceY = engine.getHeight(x, z);
}
int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurfaceY - surfaceClearance));
int unclampedClearanceTopY = columnSurfaceY - surfaceClearance;
int clearanceTopY = Math.min(maxY, Math.max(minY, unclampedClearanceTopY));
boolean breakColumn = allowSurfaceBreak
&& surfaceBreakDensity.noiseFastSigned2D(x, z) >= surfaceBreakNoiseThreshold;
int columnTopY = breakColumn
@@ -297,6 +302,7 @@ public class IrisCaveCarver3D {
: Integer.MIN_VALUE;
surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth);
surfaceBreakColumn[index] = breakColumn;
surfaceCeilingColumn[index] = !breakColumn && unclampedClearanceTopY <= maxY;
columnThreshold[index] = (thresholdDensity == null
? constantThreshold
: thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)) - thresholdBias;
@@ -487,6 +493,7 @@ public class IrisCaveCarver3D {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[y - minY];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, columnIndex, y, minY);
planeThresholdLimit[planeCount] = localThreshold * normalizationFactor;
planeCount++;
}
@@ -514,7 +521,7 @@ public class IrisCaveCarver3D {
}
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
@@ -531,7 +538,7 @@ public class IrisCaveCarver3D {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
@@ -624,6 +631,7 @@ public class IrisCaveCarver3D {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[y - minY];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, columnIndex, y, minY);
planeThresholdLimit[planeCount] = localThreshold * normalizationFactor;
planeCount++;
}
@@ -662,7 +670,7 @@ public class IrisCaveCarver3D {
}
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
@@ -679,7 +687,7 @@ public class IrisCaveCarver3D {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
@@ -822,6 +830,7 @@ public class IrisCaveCarver3D {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[fadeIndex];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, index, yy, minY);
if (density > localThreshold) {
continue;
}
@@ -830,7 +839,7 @@ public class IrisCaveCarver3D {
int localZ = tileLocalZ[columnIndex];
int worldX = x0 + localX;
int worldZ = z0 + localZ;
MatterCavern matter = resolveMatter(verticalMatter, worldX, yy, worldZ,
MatterCavern matter = resolveMatter(scratch, verticalMatter, worldX, yy, worldZ,
index, fluidMaxY, localThreshold);
if (skipExistingCarved) {
if (cavernSlice.get(localX, localY, localZ) == null) {
@@ -897,23 +906,23 @@ public class IrisCaveCarver3D {
double threshold = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty);
for (int y = minY; y <= columnTopY; y += sampleStep) {
double localThreshold = threshold;
if (breakColumn && y >= breakFloorY) {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[y - minY];
if (sampleDensityOptimized(scratch, x, y, z) > localThreshold) {
continue;
}
double density = sampleDensityOptimized(scratch, x, y, z);
int carveMaxY = Math.min(columnTopY, y + sampleStep - 1);
for (int yy = y; yy <= carveMaxY; yy++) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
continue;
}
double localThreshold = threshold;
if (breakColumn && yy >= breakFloorY) {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[yy - minY];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, index, yy, minY);
if (density > localThreshold) {
continue;
}
MatterCavern verticalMatter = matterByY[yy - minY];
MatterCavern matter = resolveMatter(verticalMatter, x, yy, z,
MatterCavern matter = resolveMatter(scratch, verticalMatter, x, yy, z,
index, fluidMaxY, localThreshold);
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, PowerOfTwoCoordinates.floorDivPow2(yy, 4));
int localY = yy & 15;
@@ -2191,57 +2200,61 @@ public class IrisCaveCarver3D {
return matterByY;
}
private MatterCavern resolveMatter(MatterCavern verticalMatter, int x, int y, int z,
private MatterCavern resolveMatter(CaveCarveScratch scratch, MatterCavern verticalMatter, int x, int y, int z,
int columnIndex, int[] fluidMaxY, double localThreshold) {
if (verticalMatter != carveLava
&& y <= fluidMaxY[columnIndex]
&& isAquiferCandidate(x, y, z, localThreshold)) {
&& isAquiferCandidate(scratch, x, y, z, localThreshold)) {
return carveFluid;
}
return verticalMatter;
}
private boolean isAquiferCandidate(int x, int y, int z, double localThreshold) {
private boolean isAquiferCandidate(CaveCarveScratch scratch, int x, int y, int z, double localThreshold) {
double depthFactor = Math.max(0D, Math.min(1.5D, (fluidHeight - y) / 48D));
double cutoff = 0.35D + (depthFactor * 0.2D);
if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) {
return false;
}
return !fluidRequiresFloor || hasAquiferCupSupport(x, y, z, localThreshold);
return !fluidRequiresFloor || hasAquiferCupSupport(scratch, x, y, z, localThreshold);
}
private boolean hasAquiferCupSupport(int x, int y, int z, double threshold) {
private boolean isAquiferCandidate(int x, int y, int z, double localThreshold) {
return isAquiferCandidate(scratchCache.get(), x, y, z, localThreshold);
}
private boolean hasAquiferCupSupport(CaveCarveScratch scratch, int x, int y, int z, double threshold) {
int floorY = Math.max(0, y - 1);
int deepFloorY = Math.max(0, y - 2);
int aboveY = Math.min(aquiferCeilingY, y + 1);
if (!isDensitySolid(x, floorY, z, threshold)) {
if (!isDensitySolid(scratch, x, floorY, z, threshold)) {
return false;
}
if (!isDensitySolid(x, deepFloorY, z, threshold - 0.05D)) {
if (!isDensitySolid(scratch, x, deepFloorY, z, threshold - 0.05D)) {
return false;
}
int support = 0;
if (isDensitySolid(x + 1, y, z, threshold)) {
if (isDensitySolid(scratch, x + 1, y, z, threshold)) {
support++;
}
if (isDensitySolid(x - 1, y, z, threshold)) {
if (isDensitySolid(scratch, x - 1, y, z, threshold)) {
support++;
}
if (isDensitySolid(x, y, z + 1, threshold)) {
if (isDensitySolid(scratch, x, y, z + 1, threshold)) {
support++;
}
if (isDensitySolid(x, y, z - 1, threshold)) {
if (isDensitySolid(scratch, x, y, z - 1, threshold)) {
support++;
}
if (isDensitySolid(x, aboveY, z, threshold)) {
if (isDensitySolid(scratch, x, aboveY, z, threshold)) {
support++;
}
return support >= 4;
}
private boolean isDensitySolid(int x, int y, int z, double threshold) {
return sampleDensityOptimized(x, y, z) > threshold;
private boolean isDensitySolid(CaveCarveScratch scratch, int x, int y, int z, double threshold) {
return sampleDensityOptimized(scratch, x, y, z) > threshold;
}
private void writeCavern(MatterSlice<MatterCavern> cavernSlice, int localX, int y, int localZ,
@@ -2288,6 +2301,52 @@ public class IrisCaveCarver3D {
return (value * 2D) - 1D;
}
private double applySurfaceCeilingFade(
CaveCarveScratch scratch,
double threshold,
int columnIndex,
int y,
int minY
) {
if (!scratch.surfaceCeilingColumn[columnIndex]) {
return threshold;
}
int ceilingDistance = scratch.columnMaxY[columnIndex] - y;
if (ceilingDistance < 0 || ceilingDistance >= SURFACE_CEILING_FADE_DEPTH) {
return threshold;
}
double closureThreshold = scratch.surfaceClosureThreshold[y - minY];
if (threshold <= closureThreshold) {
return threshold;
}
double progress = ceilingDistance / (double) SURFACE_CEILING_FADE_DEPTH;
double smooth = progress * progress * (3D - (2D * progress));
return closureThreshold + ((threshold - closureThreshold) * smooth);
}
private void prepareSurfaceClosureThresholdTable(CaveCarveScratch scratch, int minY, int maxY) {
int size = Math.max(0, maxY - minY + 1);
if (scratch.surfaceClosureThreshold.length < size) {
scratch.surfaceClosureThreshold = new double[size];
}
double baseMinimum = -Math.abs(baseWeight) - Math.abs(detailWeight);
for (int y = minY; y <= maxY; y++) {
double minimumDensity = baseMinimum;
for (CaveFieldModuleState module : modules) {
if (y < module.minY || y > module.maxY) {
continue;
}
minimumDensity += Math.min(module.minContribution, module.maxContribution);
}
scratch.surfaceClosureThreshold[y - minY] =
(minimumDensity * inverseNormalization) - SURFACE_CEILING_SOLID_EPSILON;
}
}
private double[] prepareVerticalEdgeFadeTable(CaveCarveScratch scratch, int minY, int maxY) {
int size = Math.max(0, maxY - minY + 1);
if (scratch.verticalEdgeFade.length < size) {
@@ -474,7 +474,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights);
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldSurfaceHeights,
fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights,
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
@@ -500,7 +500,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
}
private void fillFieldFluidPresence(
ProceduralStream<PlatformBlockState> stream,
IrisComplex complex,
int startX,
int startZ,
double[] surfaceHeights,
@@ -511,7 +511,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
target[fieldIndex] = B.isFluid(stream.get(worldX, startZ + fieldZ))
target[fieldIndex] = B.isFluid(complex.resolveSurfaceFluid(worldX, startZ + fieldZ))
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
}
}
@@ -3,7 +3,7 @@ package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.river.cave.CavePosition;
import art.arcane.iris.engine.river.cave.CaveVoxel;
import art.arcane.iris.engine.river.cave.CaveVoxelView;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
@@ -15,8 +15,10 @@ import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Objects;
import java.util.function.BiConsumer;
final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView {
private static final int CLOSED_COLUMN = Integer.MAX_VALUE;
@@ -26,6 +28,9 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
private final int worldHeight;
private final Function2<Integer, Integer, Integer> surfaceHeight;
private final Function2<Integer, Integer, PlatformBlockState> compatibleFluid;
private final RiverCaveFluidKind planningFluidKind;
private final BiConsumer<Integer, Integer> chunkLoader;
private final LongOpenHashSet loadedChunks;
private final Long2IntOpenHashMap openFloorCache;
private final Long2IntOpenHashMap surfaceHeightCache;
@@ -33,12 +38,17 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
Mantle<Matter> mantle,
int worldHeight,
Function2<Integer, Integer, Integer> surfaceHeight,
Function2<Integer, Integer, PlatformBlockState> compatibleFluid
Function2<Integer, Integer, PlatformBlockState> compatibleFluid,
RiverCaveFluidKind planningFluidKind,
BiConsumer<Integer, Integer> chunkLoader
) {
this.mantle = Objects.requireNonNull(mantle);
this.worldHeight = worldHeight;
this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
this.compatibleFluid = Objects.requireNonNull(compatibleFluid);
this.planningFluidKind = Objects.requireNonNull(planningFluidKind);
this.chunkLoader = Objects.requireNonNull(chunkLoader);
loadedChunks = new LongOpenHashSet();
openFloorCache = new Long2IntOpenHashMap();
openFloorCache.defaultReturnValue(CACHE_MISS);
surfaceHeightCache = new Long2IntOpenHashMap();
@@ -52,10 +62,18 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
@Override
public CaveVoxel voxelAt(CavePosition position) {
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
if (hydrology != null && hydrology.fluidKind() != planningFluidKind) {
return CaveVoxel.INCOMPATIBLE_FLUID;
}
MatterCavern cavern = dataIfPresent(position, MatterCavern.class);
if (cavern != null) {
if (cavern.isLava()) {
return CaveVoxel.LAVA;
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
return expected != null
&& IrisProceduralBlocks.materialKey(expected).endsWith(":lava")
? CaveVoxel.COMPATIBLE_FLUID
: CaveVoxel.LAVA;
}
if (cavern.getLiquid() == 1) {
return CaveVoxel.COMPATIBLE_FLUID;
@@ -71,13 +89,13 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
if (!block.isFluid()) {
return CaveVoxel.SOLID;
}
if (IrisProceduralBlocks.materialKey(block).endsWith(":lava")) {
return CaveVoxel.LAVA;
}
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
return expected != null
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))
? CaveVoxel.COMPATIBLE_FLUID
if (expected != null
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))) {
return CaveVoxel.COMPATIBLE_FLUID;
}
return IrisProceduralBlocks.materialKey(block).endsWith(":lava")
? CaveVoxel.LAVA
: CaveVoxel.INCOMPATIBLE_FLUID;
}
@@ -99,9 +117,8 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
}
@Override
public RiverCaveAction riverActionAt(CavePosition position) {
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
return hydrology == null ? null : hydrology.action();
public RiverCaveHydrology riverHydrologyAt(CavePosition position) {
return dataIfPresent(position, RiverCaveHydrology.class);
}
private int resolveOpenFloor(int x, int z) {
@@ -131,6 +148,10 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
private <T> T dataIfPresent(CavePosition position, Class<T> type) {
int chunkX = position.x() >> 4;
int chunkZ = position.z() >> 4;
long chunkKey = Mantle.key(chunkX, chunkZ);
if (loadedChunks.add(chunkKey)) {
chunkLoader.accept(chunkX, chunkZ);
}
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5));
if (plate == null || plate.isClosed()) {
return null;
@@ -1,17 +1,21 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisRiverCaveFallback;
import art.arcane.iris.engine.object.IrisRiverCaveMode;
import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.object.IrisRiverDeepPools;
import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRiverNetwork;
import art.arcane.iris.engine.river.RiverAnchor;
import art.arcane.iris.engine.river.RiverNetwork;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection;
@@ -22,6 +26,7 @@ import art.arcane.iris.engine.river.cave.CaveVoxelPrecondition;
import art.arcane.iris.engine.river.cave.CaveVoxelView;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.river.cave.RiverCaveMode;
@@ -35,8 +40,13 @@ import art.arcane.iris.engine.river.runtime.IrisRiverTunnelSample;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.matter.Matter;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -47,6 +57,8 @@ import java.util.Set;
@ComponentFlag(ReservedFlag.RIVER_HYDROLOGY)
public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
private static final long CANDIDATE_SALT = 0x6A09E667F3BCC909L;
private static final long DEEP_POOL_CANDIDATE_SALT = 0xBB67AE8584CAA73BL;
private static final long DEEP_POOL_POSITION_SALT = 0x3C6EF372FE94F82BL;
static final int PRIORITY = 1;
private static final int[] FALLBACK_X = {0, 1, -1, 0, 0};
private static final int[] FALLBACK_Z = {0, 0, 0, 1, -1};
@@ -73,6 +85,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
return PREREQUISITES;
}
@Override
public boolean isInputGenerationLazy() {
return true;
}
@Override
public int getInputRadius() {
if (!getDimension().isCarvingEnabled()
@@ -87,6 +104,122 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
return inputRadius(runtime.caveSettings(), tunnelHalo(runtime));
}
@Override
public int getInputRadius(
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius,
ChunkContext context
) {
if (!getDimension().isCarvingEnabled()
|| getDimension().getRivers() == null
|| !getDimension().getRivers().isEnabled()) {
return 0;
}
if (context == null || context.getComplex() == null) {
return getInputRadius();
}
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
if (runtime == null) {
return getInputRadius();
}
IrisRiverCaves caves = runtime.caveSettings();
int tunnelRadius = tunnelHalo(runtime);
int radius = hasRiverFootprint(
runtime,
targetChunkX,
targetChunkZ,
invocationChunkRadius,
tunnelRadius
) ? tunnelRadius : 0;
if (caves.getMode() != IrisRiverCaveMode.SEALED
&& caves.getMaximumPerReach() > 0) {
radius = Math.max(radius, hasAcceptedCaveAnchor(
runtime,
caves,
targetChunkX,
targetChunkZ,
invocationChunkRadius
) ? planningHalo(caves) : 0);
}
IrisRiverDeepPools deepPools = caves.getDeepPools();
if (deepPools != null
&& deepPools.isEnabled()
&& deepPools.getMaximumPerReach() > 0) {
radius = Math.max(radius, hasAcceptedDeepPoolAnchor(
runtime,
deepPools,
targetChunkX,
targetChunkZ,
invocationChunkRadius
) ? deepPoolPlanningHalo(deepPools) : 0);
}
return radius;
}
private static boolean hasRiverFootprint(
IrisRiverRuntime runtime,
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius,
int tunnelRadius
) {
return runtime.hasRiverFootprint(
((targetChunkX - invocationChunkRadius) << 4) - tunnelRadius,
((targetChunkZ - invocationChunkRadius) << 4) - tunnelRadius,
((targetChunkX + invocationChunkRadius + 1) << 4) + tunnelRadius,
((targetChunkZ + invocationChunkRadius + 1) << 4) + tunnelRadius
);
}
private static boolean hasAcceptedCaveAnchor(
IrisRiverRuntime runtime,
IrisRiverCaves caves,
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius
) {
int candidateHalo = candidateHalo(caves);
List<RiverAnchor> anchors = runtime.candidateAnchors(
((targetChunkX - invocationChunkRadius) << 4) - candidateHalo,
((targetChunkZ - invocationChunkRadius) << 4) - candidateHalo,
((targetChunkX + invocationChunkRadius + 1) << 4) + candidateHalo,
((targetChunkZ + invocationChunkRadius + 1) << 4) + candidateHalo,
caves.getMinimumSpacing(),
CANDIDATE_SALT
);
for (RiverAnchor anchor : anchors) {
if (runtime.acceptsCaveAnchor(anchor)) {
return true;
}
}
return false;
}
private static boolean hasAcceptedDeepPoolAnchor(
IrisRiverRuntime runtime,
IrisRiverDeepPools deepPools,
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius
) {
int candidateHalo = deepPoolCandidateHalo(deepPools);
List<RiverAnchor> anchors = runtime.candidateAnchors(
((targetChunkX - invocationChunkRadius) << 4) - candidateHalo,
((targetChunkZ - invocationChunkRadius) << 4) - candidateHalo,
((targetChunkX + invocationChunkRadius + 1) << 4) + candidateHalo,
((targetChunkZ + invocationChunkRadius + 1) << 4) + candidateHalo,
deepPools.getMinimumSpacing(),
DEEP_POOL_CANDIDATE_SALT
);
for (RiverAnchor anchor : anchors) {
if (runtime.acceptsDeepPoolAnchor(anchor)) {
return true;
}
}
return false;
}
@Override
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
@@ -96,11 +229,21 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
publishTunnels(writer, context, runtime, chunkX, chunkZ);
IrisRiverCaves caves = runtime.caveSettings();
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
return;
if (caves.getMode() != IrisRiverCaveMode.SEALED && caves.getMaximumPerReach() > 0) {
publishCaveConnections(writer, context, runtime, caves, chunkX, chunkZ);
}
publishDeepPools(writer, context, runtime, caves.getDeepPools(), chunkX, chunkZ);
}
MantleRiverCaveVoxelView view = createView(writer, context);
private void publishCaveConnections(
MantleWriter writer,
ChunkContext context,
IrisRiverRuntime runtime,
IrisRiverCaves caves,
int chunkX,
int chunkZ
) {
MantleRiverCaveVoxelView view = createView(writer, context, RiverCaveFluidKind.RIVER);
int candidateHalo = candidateHalo(caves);
int minimumX = (chunkX << 4) - candidateHalo;
int minimumZ = (chunkZ << 4) - candidateHalo;
@@ -146,11 +289,202 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
}
RiverCavePlanningResult result = planner.planAll(view, sources, settings);
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
MantleRiverCaveVoxelView revalidationView = createView(
writer,
context,
RiverCaveFluidKind.RIVER
);
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
return;
}
publishLocal(writer, chunkX, chunkZ, result, floodedBiomes);
publishLocal(
writer,
chunkX,
chunkZ,
result,
floodedBiomes,
RiverCaveFluidKind.RIVER
);
}
private void publishDeepPools(
MantleWriter writer,
ChunkContext context,
IrisRiverRuntime runtime,
IrisRiverDeepPools deepPools,
int chunkX,
int chunkZ
) {
if (deepPools == null || !deepPools.isEnabled() || deepPools.getMaximumPerReach() <= 0) {
return;
}
MantleRiverCaveVoxelView view = createView(
writer,
context,
RiverCaveFluidKind.DEEP_POOL
);
int candidateHalo = deepPoolCandidateHalo(deepPools);
int minimumX = (chunkX << 4) - candidateHalo;
int minimumZ = (chunkZ << 4) - candidateHalo;
int maximumX = ((chunkX + 1) << 4) + candidateHalo;
int maximumZ = ((chunkZ + 1) << 4) + candidateHalo;
List<RiverAnchor> anchors = runtime.candidateAnchors(
minimumX,
minimumZ,
maximumX,
maximumZ,
deepPools.getMinimumSpacing(),
DEEP_POOL_CANDIDATE_SALT
);
if (anchors.isEmpty()) {
return;
}
RiverCavePlannerSettings settings = deepPoolPlannerSettings(deepPools, seed(), getData());
List<RiverCaveSource> sources = new ArrayList<>();
Map<Long, String> floodedBiomes = new HashMap<>();
for (RiverAnchor anchor : anchors) {
if (!runtime.acceptsDeepPoolAnchor(anchor)) {
continue;
}
RiverCaveSource source = deepPoolSourceFor(
view,
deepPools,
anchor,
getDimension().getMinHeight(),
seed()
);
if (source == null) {
continue;
}
sources.add(source);
floodedBiomes.put(source.sourceId(), runtime.selectFloodedCaveBiome(anchor));
}
if (sources.isEmpty()) {
return;
}
RiverCavePlanningResult result = planner.planAll(view, sources, settings);
MantleRiverCaveVoxelView revalidationView = createView(
writer,
context,
RiverCaveFluidKind.DEEP_POOL
);
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
return;
}
publishLocal(
writer,
chunkX,
chunkZ,
result,
floodedBiomes,
RiverCaveFluidKind.DEEP_POOL
);
}
static RiverCavePlannerSettings deepPoolPlannerSettings(
IrisRiverDeepPools deepPools,
long seed,
IrisData data
) {
int verticalDepth = deepPools.getVerticalRadius() * 2 - deepPools.getDryHeadroom() + 1;
int proofRadius = (int) StrictMath.ceil(
StrictMath.sqrt(2D) * deepPools.getHorizontalRadius()
) + 1;
return new RiverCavePlannerSettings(
proofRadius,
verticalDepth,
deepPools.getMaximumVolume(),
deepPools.getVerticalRadius(),
1,
deepPools.getHorizontalRadius(),
deepPools.getVerticalRadius(),
deepPools.getDryHeadroom(),
RiverCaveFluidPolicy.REJECT_EXISTING,
new ConfiguredRiverGrottoShape(
seed ^ DEEP_POOL_POSITION_SALT,
data,
deepPools.getShapeStyle(),
deepPools.getWarpStyle(),
deepPools.getWarpStrength(),
deepPools.getShapeVariation()
),
proofRadius,
verticalDepth
);
}
static RiverCaveSource deepPoolSourceFor(
CaveVoxelView view,
IrisRiverDeepPools deepPools,
RiverAnchor anchor,
int worldMinimumY,
long seed
) {
int minimumHead = deepPools.getMinimumFluidY() - worldMinimumY;
int maximumHead = deepPools.getMaximumFluidY() - worldMinimumY;
int headRange = maximumHead - minimumHead + 1;
if (headRange <= 0) {
return null;
}
int searchRadius = deepPools.getSearchRadius();
int searchWidth = searchRadius * 2 + 1;
int targetDepth = Math.max(
1,
deepPools.getVerticalRadius() - deepPools.getDryHeadroom()
);
for (int attempt = 0; attempt < deepPools.getSearchAttempts(); attempt++) {
long hash = RiverNetwork.mix(
seed
^ anchor.stableId()
^ DEEP_POOL_POSITION_SALT
^ (long) attempt * 0x9E3779B97F4A7C15L
);
int offsetX = searchRadius == 0
? 0
: Math.floorMod((int) hash, searchWidth) - searchRadius;
int offsetZ = searchRadius == 0
? 0
: Math.floorMod((int) (hash >>> 32), searchWidth) - searchRadius;
if ((long) offsetX * offsetX + (long) offsetZ * offsetZ
> (long) searchRadius * searchRadius) {
continue;
}
int x = (int) StrictMath.floor(anchor.x()) + offsetX;
int z = (int) StrictMath.floor(anchor.z()) + offsetZ;
int startOffset = (int) StrictMath.floor(unit(hash) * headRange);
for (int scanned = 0; scanned < headRange; scanned++) {
int headY = maximumHead - Math.floorMod(startOffset + scanned, headRange);
CavePosition floor = new CavePosition(x, headY, z);
CavePosition above = new CavePosition(x, headY + 1, z);
CavePosition target = new CavePosition(x, headY - targetDepth, z);
if (!view.isInWorld(target)
|| !view.isInWorld(above)
|| view.voxelAt(floor) != CaveVoxel.SOLID
|| view.voxelAt(above) != CaveVoxel.CAVE_AIR
|| view.isOpenToSurface(above)) {
continue;
}
long sourceId = RiverNetwork.mix(
anchor.stableId()
^ BlockPosition.toLong(x, headY, z)
^ DEEP_POOL_POSITION_SALT
);
return new RiverCaveSource(
sourceId,
floor,
target,
headY,
RiverCaveMode.DEEP_POOL
);
}
}
return null;
}
private static double unit(long hash) {
return (hash >>> 11) * 0x1.0p-53;
}
@Override
@@ -176,9 +510,15 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
return false;
}
IrisRiverCaves caves = dimension.getRivers().getCaves();
return caves != null
&& caves.getMode() != IrisRiverCaveMode.SEALED
if (caves == null) {
return false;
}
boolean caveConnections = caves.getMode() != IrisRiverCaveMode.SEALED
&& caves.getMaximumPerReach() > 0;
IrisRiverDeepPools deepPools = caves.getDeepPools();
return caveConnections || deepPools != null
&& deepPools.isEnabled()
&& deepPools.getMaximumPerReach() > 0;
}
static int planningHalo(IrisRiverCaves caves) {
@@ -186,16 +526,33 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
}
static int inputRadius(IrisRiverCaves caves, int tunnelRadius) {
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
return tunnelRadius;
int radius = tunnelRadius;
if (caves.getMode() != IrisRiverCaveMode.SEALED && caves.getMaximumPerReach() > 0) {
radius = Math.max(radius, planningHalo(caves));
}
return Math.max(tunnelRadius, planningHalo(caves));
IrisRiverDeepPools deepPools = caves.getDeepPools();
if (deepPools != null && deepPools.isEnabled() && deepPools.getMaximumPerReach() > 0) {
radius = Math.max(radius, deepPoolPlanningHalo(deepPools));
}
return radius;
}
static int candidateHalo(IrisRiverCaves caves) {
return cavePublicationRadius(caves) * 3;
}
static int deepPoolPlanningHalo(IrisRiverDeepPools deepPools) {
return deepPoolPublicationRadius(deepPools) * 4;
}
static int deepPoolCandidateHalo(IrisRiverDeepPools deepPools) {
return deepPoolPublicationRadius(deepPools) * 3;
}
static int deepPoolPublicationRadius(IrisRiverDeepPools deepPools) {
return deepPools.getSearchRadius() + deepPools.getHorizontalRadius() + 1;
}
static int cavePublicationRadius(IrisRiverCaves caves) {
int generatedRadius = generatedGrottoPublicationRadius(caves);
return switch (caves.getMode()) {
@@ -296,13 +653,13 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
boolean changed;
do {
changed = false;
Set<CavePosition> candidateActions = mergeActions(containedColumns).keySet();
Long2ObjectOpenHashMap<TunnelColumn> candidateColumns = indexColumns(containedColumns);
for (int index = containedColumns.size() - 1; index >= 0; index--) {
TunnelColumn column = containedColumns.get(index);
if (!isTunnelColumnContained(
view,
column,
candidateActions,
candidateColumns,
dryHeadroom,
surfaceSampler
)) {
@@ -348,7 +705,10 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
));
}
}
return new TunnelPlan(Map.copyOf(actions), Map.copyOf(preconditions));
return new TunnelPlan(
Collections.unmodifiableMap(actions),
Collections.unmodifiableMap(preconditions)
);
}
private static TunnelColumn createTunnelColumn(
@@ -360,8 +720,9 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
if (sample == null) {
return null;
}
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
for (int y = sample.bedY() + 1; y <= sample.ceilingY(); y++) {
int minimumY = sample.bedY() + 1;
int maximumY = sample.ceilingY();
for (int y = minimumY; y <= maximumY; y++) {
CavePosition position = new CavePosition(x, y, z);
RiverCaveAction action = y <= sample.waterHeadY()
? RiverCaveAction.WET_SOURCE
@@ -371,22 +732,24 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
&& !matchesPublishedAction(view, position, action))) {
return null;
}
actions.put(position, action);
}
return actions.isEmpty() ? null : new TunnelColumn(actions);
return minimumY > maximumY
? null
: new TunnelColumn(x, z, minimumY, sample.waterHeadY(), maximumY);
}
private static boolean isTunnelColumnContained(
CaveVoxelView view,
TunnelColumn column,
Set<CavePosition> candidateActions,
Long2ObjectOpenHashMap<TunnelColumn> candidateColumns,
int dryHeadroom,
SurfaceSampler surfaceSampler
) {
for (CavePosition position : column.actions().keySet()) {
for (int y = column.minimumY(); y <= column.maximumY(); y++) {
CavePosition position = new CavePosition(column.x(), y, column.z());
for (int[] offset : NEIGHBORS) {
CavePosition neighbor = offset(position, offset);
if (candidateActions.contains(neighbor)) {
if (containsAction(candidateColumns, neighbor)) {
continue;
}
if (!view.isInWorld(neighbor)) {
@@ -417,7 +780,9 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
SurfaceSampler surfaceSampler
) {
IrisRiverSurfaceSample sample = surfaceSampler.sample(position.x(), position.z());
if (!isWetChannelBed(sample) || sample.subterranean()) {
if (!sample.river().present()
|| sample.river().state() != RiverRouteState.WET
|| sample.subterranean()) {
return false;
}
int bedY = (int) Math.round(sample.terrainHeight());
@@ -428,18 +793,48 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
private static Map<CavePosition, RiverCaveAction> mergeActions(List<TunnelColumn> columns) {
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
for (TunnelColumn column : columns) {
actions.putAll(column.actions());
for (int y = column.minimumY(); y <= column.maximumY(); y++) {
actions.put(
new CavePosition(column.x(), y, column.z()),
y <= column.waterHeadY()
? RiverCaveAction.WET_SOURCE
: RiverCaveAction.DRY_AIR
);
}
}
return actions;
}
private static Long2ObjectOpenHashMap<TunnelColumn> indexColumns(List<TunnelColumn> columns) {
Long2ObjectOpenHashMap<TunnelColumn> indexed = new Long2ObjectOpenHashMap<>(columns.size());
for (TunnelColumn column : columns) {
indexed.put(Cache.key(column.x(), column.z()), column);
}
return indexed;
}
private static boolean containsAction(
Long2ObjectOpenHashMap<TunnelColumn> columns,
CavePosition position
) {
TunnelColumn column = columns.get(Cache.key(position.x(), position.z()));
return column != null
&& position.y() >= column.minimumY()
&& position.y() <= column.maximumY();
}
private static boolean matchesPublishedAction(
CaveVoxelView view,
CavePosition position,
RiverCaveAction action
) {
return view instanceof TunnelVoxelView tunnelView
&& tunnelView.riverActionAt(position) == action;
if (!(view instanceof TunnelVoxelView tunnelView)) {
return false;
}
RiverCaveHydrology hydrology = tunnelView.riverHydrologyAt(position);
return hydrology != null
&& hydrology.action() == action
&& hydrology.fluidKind() == RiverCaveFluidKind.RIVER;
}
private static CavePosition offset(CavePosition position, int[] offset) {
@@ -450,15 +845,39 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
);
}
private MantleRiverCaveVoxelView createView(MantleWriter writer, ChunkContext context) {
private MantleRiverCaveVoxelView createView(
MantleWriter writer,
ChunkContext context,
RiverCaveFluidKind fluidKind
) {
return new MantleRiverCaveVoxelView(
writer.getMantle(),
writer.getMantle().getWorldHeight(),
(x, z) -> context.getComplex().getRoundedHeighteightStream().get(x, z),
(x, z) -> context.getComplex().getFluidStream().get(x, z)
(x, z) -> context.getComplex().resolveRiverCaveFluid(fluidKind, x, z),
fluidKind,
(chunkX, chunkZ) -> generateCarvingInput(writer, context, chunkX, chunkZ)
);
}
private void generateCarvingInput(
MantleWriter writer,
ChunkContext context,
int chunkX,
int chunkZ
) {
MantleComponent carving = getEngineMantle().getRegisteredComponents().get(ReservedFlag.CARVED);
if (carving == null || !carving.isEnabled()) {
throw new IllegalStateException("River hydrology requires the carving component");
}
MantleChunk<Matter> chunk = writer.acquireChunk(chunkX, chunkZ);
if (chunk == null) {
throw new IllegalStateException("River hydrology read exceeded the prepared mantle radius at "
+ chunkX + "," + chunkZ);
}
chunk.raiseFlagSuspend(ReservedFlag.CARVED, () -> carving.generateLayer(writer, chunkX, chunkZ, context));
}
private void publishTunnels(
MantleWriter writer,
ChunkContext context,
@@ -467,7 +886,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
int chunkZ
) {
for (int attempt = 0; attempt < 2; attempt++) {
MantleRiverCaveVoxelView view = createView(writer, context);
MantleRiverCaveVoxelView view = createView(
writer,
context,
RiverCaveFluidKind.RIVER
);
TunnelPlan plan = planTunnels(
view,
chunkX,
@@ -478,7 +901,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
runtime::sampleTunnel,
runtime::sample
);
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
MantleRiverCaveVoxelView revalidationView = createView(
writer,
context,
RiverCaveFluidKind.RIVER
);
if (preconditionsHold(revalidationView, plan.preconditions())) {
publishTunnelLocal(writer, chunkX, chunkZ, plan);
return;
@@ -505,7 +932,8 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
data,
caves.getGrottoShapeStyle(),
caves.getGrottoWarpStyle(),
caves.getGrottoWarpStrength()
caves.getGrottoWarpStrength(),
0.2D
),
caves.getMaxFloodRadius(),
caves.getMaxFloodDepth()
@@ -673,7 +1101,8 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
int chunkX,
int chunkZ,
RiverCavePlanningResult result,
Map<Long, String> floodedBiomes
Map<Long, String> floodedBiomes,
RiverCaveFluidKind fluidKind
) {
Map<CavePosition, RiverCaveSource> owners = actionOwners(result);
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(result.actions().entrySet());
@@ -692,7 +1121,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
position.x(),
position.y(),
position.z(),
new RiverCaveHydrology(entry.getValue(), biome)
new RiverCaveHydrology(entry.getValue(), biome, fluidKind)
);
}
}
@@ -708,7 +1137,12 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) {
CavePosition position = entry.getKey();
if (owns(chunkX, chunkZ, position)) {
writer.setData(position.x(), position.y(), position.z(), RiverCaveHydrology.of(entry.getValue()));
writer.setData(
position.x(),
position.y(),
position.z(),
RiverCaveHydrology.of(entry.getValue(), RiverCaveFluidKind.RIVER)
);
}
}
}
@@ -749,7 +1183,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
}
interface TunnelVoxelView extends CaveVoxelView {
RiverCaveAction riverActionAt(CavePosition position);
RiverCaveHydrology riverHydrologyAt(CavePosition position);
}
record TunnelPlan(
@@ -761,6 +1195,12 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
}
}
private record TunnelColumn(Map<CavePosition, RiverCaveAction> actions) {
private record TunnelColumn(
int x,
int z,
int minimumY,
int waterHeadY,
int maximumY
) {
}
}
@@ -57,7 +57,11 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
if (shouldBypassMantleStages()) {
return;
}
generateMatter(x >> 4, z >> 4, m, c);
generateMatter(
x >> 4,
z >> 4,
m || getEngine().isStudio(),
c);
};
EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c);
EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c);
@@ -117,7 +117,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
columnMasks,
upperSurfaceHeights,
worldHeightSpan,
caveLavaHeight
caveLavaHeight,
chunkBlockX,
chunkBlockZ
);
CarveResolver carveResolver = new CarveResolver(resolutionContext);
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
@@ -166,8 +168,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
});
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
processColumnFromMask(
output,
mantleChunk,
@@ -178,8 +178,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
z,
resolverState,
caveBiomeCache,
customBiomeCache,
context.getFluid().get(localX, localZ)
customBiomeCache
);
}
@@ -279,7 +278,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
return switch (hydrology.action()) {
case WET_SOURCE -> fluid;
case FALLING_WATER -> fallingFluidState(fluid);
case FALLING_FLUID -> fallingFluidState(fluid);
case DRY_AIR -> air;
case SEAL_GUARD -> normalizeWaterlogging(current, null);
};
@@ -369,9 +368,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return;
}
PlatformBlockState fluid = isFluidIntent(cavern)
? context.chunkContext().getFluid().get(localX, localZ)
: null;
PlatformBlockState fluid = null;
if (isFluidIntent(cavern)) {
fluid = hydrology == null
? context.chunkContext().getFluid().get(localX, localZ)
: getComplex().resolveRiverCaveFluid(
hydrology.fluidKind(),
context.chunkBlockX() + localX,
context.chunkBlockZ() + localZ
);
}
if (hydrology != null) {
context.output().setRaw(localX, y, localZ,
resolveHydrologyState(hydrology, current, fluid, AIR));
@@ -395,7 +401,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
CarveColumnMask[] columnMasks,
int[] upperSurfaceHeights,
int worldHeightSpan,
int caveLavaHeight
int caveLavaHeight,
int chunkBlockX,
int chunkBlockZ
) {
}
@@ -572,8 +580,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int chunkZ,
IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache,
PlatformBlockState columnFluid
Map<String, IrisBiome> customBiomeCache
) {
if (columnMask == null || columnMask.isEmpty()) {
return;
@@ -601,7 +608,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
} else {
if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache, columnFluid);
caveBiomeCache, customBiomeCache);
}
zone = new CaveZone();
zone.setFloor(y);
@@ -614,7 +621,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache, columnFluid);
caveBiomeCache, customBiomeCache);
}
}
@@ -767,8 +774,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
CaveZone zone, int rx, int rz, int xx, int zz,
IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache,
PlatformBlockState columnFluid) {
Map<String, IrisBiome> customBiomeCache) {
int maxY = output.getHeight();
if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) {
@@ -793,7 +799,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
if (floorBiome == null && ceilingBiome == null) {
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
return;
}
@@ -865,7 +871,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
}
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
}
private void normalizeCaveZoneWaterlogging(
@@ -874,7 +880,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
CaveZone zone,
int localX,
int localZ,
PlatformBlockState columnFluid
int worldX,
int worldZ
) {
int minimumY = Math.max(0, zone.floor - 1);
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
@@ -887,6 +894,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
MatterCavern baseline = dataIfPresent(
mantleChunk, localX, y, localZ, MatterCavern.class);
PlatformBlockState current = output.getRaw(localX, y, localZ);
PlatformBlockState columnFluid = getComplex().resolveRiverCaveFluid(
hydrology.fluidKind(),
worldX,
worldZ
);
PlatformBlockState normalized = normalizeHydrologyWaterlogging(
current,
baseline,
@@ -10,7 +10,10 @@ import lombok.experimental.Accessors;
@Desc("A two-dimensional image-map coordinate")
@Data
public class IrisImageMapOrigin {
@Desc("X coordinate in world blocks for origin, or source pixels for sourceOrigin")
private double x = 0D;
@Desc("Z coordinate in world blocks for origin, or source image Y pixels for sourceOrigin")
private double z = 0D;
public IrisImageMapOrigin(double x, double z) {
@@ -25,6 +25,11 @@ import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.common.math.IrisVector;
import art.arcane.iris.util.project.interpolation.Interpolation3D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
*/
@@ -133,6 +138,7 @@ final class IrisObjectTransforms {
VectorMap<PlatformBlockState> b = new VectorMap<>();
IrisPosition min = self.getAABB().min();
IrisPosition max = self.getAABB().max();
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) {
@@ -146,7 +152,7 @@ final class IrisObjectTransforms {
return 1;
}) >= 0.5) {
b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z));
b.put(new IrisBlockVector(x, y, z), nearestBlockData(v, nearestBlocks, x, y, z));
} else {
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
}
@@ -169,6 +175,7 @@ final class IrisObjectTransforms {
VectorMap<PlatformBlockState> b = new VectorMap<>();
IrisPosition min = self.getAABB().min();
IrisPosition max = self.getAABB().max();
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) {
@@ -182,7 +189,7 @@ final class IrisObjectTransforms {
return 1;
}) >= 0.5) {
b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z));
b.put(new IrisBlockVector(x, y, z), nearestBlockData(v, nearestBlocks, x, y, z));
} else {
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
}
@@ -209,6 +216,7 @@ final class IrisObjectTransforms {
VectorMap<PlatformBlockState> b = new VectorMap<>();
IrisPosition min = self.getAABB().min();
IrisPosition max = self.getAABB().max();
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) {
@@ -222,7 +230,7 @@ final class IrisObjectTransforms {
return 1;
}, tension, bias) >= 0.5) {
b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z));
b.put(new IrisBlockVector(x, y, z), nearestBlockData(v, nearestBlocks, x, y, z));
} else {
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
}
@@ -238,36 +246,124 @@ final class IrisObjectTransforms {
}
}
private static PlatformBlockState nearestBlockData(IrisObject self, int x, int y, int z) {
private static PlatformBlockState nearestBlockData(VectorMap<PlatformBlockState> blocks,
NearestBlockIndex nearestBlocks,
int x, int y, int z) {
IrisBlockVector vv = new IrisBlockVector(x, y, z);
self.readLock.lock();
try {
PlatformBlockState r = self.blocks.get(vv);
PlatformBlockState direct = blocks.get(vv);
if (!B.isAir(direct)) {
return direct;
}
return nearestBlocks.nearest(x, y, z, direct);
}
if (!B.isAir(r)) {
return r;
static final class NearestBlockIndex {
private static final Comparator<NearestBlock> X_ORDER = Comparator
.comparingInt(NearestBlock::x)
.thenComparingInt(NearestBlock::rank);
private static final Comparator<NearestBlock> Y_ORDER = Comparator
.comparingInt(NearestBlock::y)
.thenComparingInt(NearestBlock::rank);
private static final Comparator<NearestBlock> Z_ORDER = Comparator
.comparingInt(NearestBlock::z)
.thenComparingInt(NearestBlock::rank);
private final NearestNode root;
private PlatformBlockState bestState;
private double bestDistance;
private int bestRank;
private NearestBlockIndex(NearestNode root) {
this.root = root;
}
static NearestBlockIndex create(VectorMap<PlatformBlockState> blocks) {
List<NearestBlock> points = new ArrayList<>(blocks.size());
VectorMap<PlatformBlockState>.Cursor cursor = blocks.cursor();
int rank = 0;
while (cursor.next()) {
PlatformBlockState state = cursor.value();
if (!B.isAir(state)) {
IrisBlockVector position = cursor.key();
points.add(new NearestBlock(position.getBlockX(), position.getBlockY(), position.getBlockZ(),
rank, state));
}
rank++;
}
double d = Double.MAX_VALUE;
NearestBlock[] pointArray = points.toArray(new NearestBlock[0]);
return new NearestBlockIndex(build(pointArray, 0, pointArray.length, 0));
}
for (var entry : self.blocks) {
PlatformBlockState dat = entry.getValue();
if (B.isAir(dat)) {
continue;
}
double dx = entry.getKey().distanceSquared(vv);
if (dx < d) {
d = dx;
r = dat;
}
PlatformBlockState nearest(int x, int y, int z, PlatformBlockState fallback) {
if (root == null) {
return fallback;
}
return r;
} finally {
self.readLock.unlock();
bestState = fallback;
bestDistance = Double.MAX_VALUE;
bestRank = Integer.MAX_VALUE;
search(root, x, y, z);
return bestState;
}
private static NearestNode build(NearestBlock[] points, int from, int to, int depth) {
if (from >= to) {
return null;
}
int axis = depth % 3;
Arrays.sort(points, from, to, comparator(axis));
int middle = (from + to) >>> 1;
return new NearestNode(
points[middle],
axis,
build(points, from, middle, depth + 1),
build(points, middle + 1, to, depth + 1)
);
}
private static Comparator<NearestBlock> comparator(int axis) {
return switch (axis) {
case 0 -> X_ORDER;
case 1 -> Y_ORDER;
default -> Z_ORDER;
};
}
private void search(NearestNode node, int x, int y, int z) {
if (node == null) {
return;
}
NearestBlock point = node.point();
double xDistance = point.x() - x;
double yDistance = point.y() - y;
double zDistance = point.z() - z;
double distance = (xDistance * xDistance) + (yDistance * yDistance) + (zDistance * zDistance);
if (distance < bestDistance || (distance == bestDistance && point.rank() < bestRank)) {
bestState = point.state();
bestDistance = distance;
bestRank = point.rank();
}
double axisDistance = switch (node.axis()) {
case 0 -> x - point.x();
case 1 -> y - point.y();
default -> z - point.z();
};
NearestNode near = axisDistance <= 0D ? node.lower() : node.upper();
NearestNode far = axisDistance <= 0D ? node.upper() : node.lower();
search(near, x, y, z);
if ((axisDistance * axisDistance) <= bestDistance) {
search(far, x, y, z);
}
}
}
private record NearestNode(NearestBlock point, int axis, NearestNode lower, NearestNode upper) {
}
private record NearestBlock(int x, int y, int z, int rank, PlatformBlockState state) {
}
}
@@ -97,4 +97,7 @@ public class IrisRiverCaves {
@Desc("The policy for fluid already present in a candidate contained cave body.")
private IrisRiverExistingFluidPolicy existingFluidPolicy = IrisRiverExistingFluidPolicy.REJECT;
@Desc("Sparse, independently filled cave-floor pools generated at deep river-network anchors.")
private IrisRiverDeepPools deepPools = new IrisRiverDeepPools();
}
@@ -0,0 +1,92 @@
package art.arcane.iris.engine.object;
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 lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Controls sparse, river-anchored fluid pools attached to deep cave floors.")
@Data
public class IrisRiverDeepPools {
@Desc("Enables independently configured deep cave pools along eligible wet river reaches.")
private boolean enabled = false;
@Desc("Selects complete wet river reaches that may host deep pools.")
private IrisRiverNoiseChance reach = new IrisRiverNoiseChance()
.setChance(1D / 3D)
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(4096D))
.setInfluence(0.08D);
@MinNumber(16)
@MaxNumber(4096)
@Desc("The minimum distance in blocks between deep-pool candidates.")
private int minimumSpacing = 768;
@MinNumber(0)
@MaxNumber(16)
@Desc("The maximum accepted deep pools on one river reach.")
private int maximumPerReach = 1;
@MinNumber(-2048)
@MaxNumber(2048)
@Desc("The lowest absolute world Y considered for the pool fluid surface.")
private int minimumFluidY = -224;
@MinNumber(-2048)
@MaxNumber(2048)
@Desc("The highest absolute world Y considered for the pool fluid surface.")
private int maximumFluidY = -104;
@MinNumber(0)
@MaxNumber(256)
@Desc("The horizontal distance searched from a river anchor for a contained cave floor.")
private int searchRadius = 16;
@MinNumber(1)
@MaxNumber(64)
@Desc("The number of deterministic nearby columns tested for a contained cave floor.")
private int searchAttempts = 12;
@MinNumber(2)
@MaxNumber(128)
@Desc("The horizontal radius of the generated deep-pool chamber.")
private int horizontalRadius = 18;
@MinNumber(2)
@MaxNumber(64)
@Desc("The vertical radius of the generated deep-pool chamber.")
private int verticalRadius = 8;
@MinNumber(1)
@MaxNumber(63)
@Desc("The dry chamber height retained above the deep-pool fluid surface.")
private int dryHeadroom = 4;
@Desc("Noise shaping the deep-pool chamber boundary.")
private IrisGeneratorStyle shapeStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(12D);
@MinNumber(0)
@MaxNumber(0.75)
@Desc("The proportional noise displacement applied to the deep-pool chamber boundary.")
private double shapeVariation = 0.5D;
@Desc("Noise warping the deep-pool chamber coordinate field.")
private IrisGeneratorStyle warpStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(24D);
@MinNumber(0)
@MaxNumber(64)
@Desc("The maximum coordinate warp applied to the deep-pool chamber in blocks.")
private double warpStrength = 6D;
@MinNumber(64)
@MaxNumber(1048576)
@Desc("The greatest generated deep-pool chamber volume that may be transactionally published.")
private int maximumVolume = 32768;
@Desc("The fluid palette used only by accepted deep pools.")
private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("lava");
}
@@ -24,6 +24,11 @@ public class IrisRiverTerrain {
@Desc("The wet-bed depth below the local water surface, or dry-channel depth below natural terrain, in blocks.")
private IrisStyledRange depth = range(2D, 7D, NoiseStyle.IRIS, 768D);
@MinNumber(0)
@MaxNumber(64)
@Desc("The radius added to every channel after worm, regional, local, and stream-order shaping.")
private double channelRadiusBonus = 0D;
@MinNumber(1)
@MaxNumber(2048)
@Desc("The final wet-channel width cap after region, biome, and stream-order scaling.")
@@ -64,7 +69,7 @@ public class IrisRiverTerrain {
@MinNumber(0)
@MaxNumber(16)
@Desc("The extra lateral tunnel-mouth blend carved on each side where a surface river enters or exits solid terrain.")
@Desc("The longitudinal transition length and maximum lateral and roof flare where a surface river enters or exits solid terrain.")
private double tunnelMouthBlend = 2D;
@Desc("Noise modulating the submerged floor of river tunnels.")
@@ -13,7 +13,15 @@ import lombok.experimental.Accessors;
@Data
public class IrisRiverWater {
@Desc("The strategy used to determine river water-surface height.")
private IrisRiverWaterMode mode = IrisRiverWaterMode.SEA_LEVEL;
private IrisRiverWaterMode mode = IrisRiverWaterMode.FIXED;
@MinNumber(-2048)
@MaxNumber(2048)
@Desc("The base river fluid surface in absolute world Y, independent of the dimension ocean height.")
private int fluidHeight = 63;
@Desc("The river fluid palette used by surface channels, contained tunnels, grottos, and waterfall throats.")
private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("water");
@MinNumber(8)
@MaxNumber(4096)
@@ -22,7 +30,7 @@ public class IrisRiverWater {
@MinNumber(0)
@MaxNumber(64)
@Desc("The greatest river water height permitted above the dimension fluid height.")
@Desc("The greatest terraced river height permitted above fluidHeight.")
private int maximumPoolRise = 4;
@MinNumber(1)
@@ -4,8 +4,8 @@ import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects how a river determines its surface fluid height.")
public enum IrisRiverWaterMode {
@Desc("Use the dimension fluid height for every wet river reach.")
SEA_LEVEL,
@Desc("Use the river water configuration's fixed fluid height for every wet reach.")
FIXED,
@Desc("Use flat pools connected by controlled vertical drops.")
TERRACED
@@ -72,16 +72,21 @@ public class IrisRiverWorm {
@Desc("Depth multiplier for reaches selecting this worm.")
private double depthMultiplier = 1D;
@MinNumber(32)
@MinNumber(8)
@MaxNumber(16384)
@Desc("Primary world-space wavelength controlling longitudinal body swelling and pinching.")
private double bodyWavelength = 512D;
@MinNumber(32)
@MinNumber(8)
@MaxNumber(16384)
@Desc("Detail wavelength adding smaller changes to the longitudinal body profile.")
private double bodyDetailWavelength = 128D;
@MinNumber(0)
@MaxNumber(1)
@Desc("Share of the longitudinal body field supplied by bodyDetailWavelength; the remainder uses bodyWavelength.")
private double bodyDetailInfluence = 0.3D;
@MinNumber(0)
@MaxNumber(0.875)
@Desc("Maximum proportional channel-width variation along this style's body.")
@@ -121,6 +121,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private final AtomicInteger a = new AtomicInteger(0);
private volatile long lastChunkGenTime = 0L;
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
private final CompletableFuture<Void> initialSpawnReady = new CompletableFuture<>();
private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>();
private final AtomicReference<CompletableFuture<Void>> closeFuture = new AtomicReference<>();
private volatile Engine engine;
@@ -194,11 +195,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
engine.getPlatformHooks().applyWorldBoundary(engine);
IrisLogging.debug("Injected Iris Biome Source into " + world.getName());
if (!studio) {
J.s(() -> updateSpawnLocation(world), 1);
J.sfut(() -> updateSpawnLocation(world), 1)
.whenComplete((ignored, failure) -> {
if (failure != null) {
initialSpawnReady.completeExceptionally(failure);
}
});
} else {
initialSpawnReady.complete(null);
}
} catch (Throwable e) {
initializationFailure = e;
spawnChunks.completeExceptionally(e);
initialSpawnReady.completeExceptionally(e);
IrisLogging.reportError(e);
IrisLogging.error("Failed to initialize Iris generator for " + world.getName());
if (e instanceof RuntimeException runtimeException) {
@@ -231,16 +240,58 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
private void updateSpawnLocation(World world) {
Location initialSpawn = getInitialSpawnLocation(world);
int chunkX = initialSpawn.getBlockX() >> 4;
int chunkZ = initialSpawn.getBlockZ() >> 4;
CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true);
if (chunkFuture == null) {
return;
}
try {
Location initialSpawn = getInitialSpawnLocation(world);
int chunkX = initialSpawn.getBlockX() >> 4;
int chunkZ = initialSpawn.getBlockZ() >> 4;
CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true);
if (chunkFuture == null) {
initialSpawnReady.completeExceptionally(new IllegalStateException(
"Initial spawn chunk request returned no completion future for world \""
+ world.getName() + "\"."));
return;
}
chunkFuture.thenAccept(chunk ->
J.runRegion(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> applySpawnLocation(chunk.getWorld(), initialSpawn)));
chunkFuture.whenComplete((chunk, failure) -> {
try {
if (failure != null) {
initialSpawnReady.completeExceptionally(failure);
return;
}
if (chunk == null) {
throw new IllegalStateException(
"Initial spawn chunk request completed without a chunk for world \""
+ world.getName() + "\".");
}
J.runRegionFuture(
chunk.getWorld(),
chunk.getX(),
chunk.getZ(),
() -> completeSpawnLocation(chunk.getWorld(), initialSpawn))
.whenComplete((ignored, scheduleFailure) -> {
if (scheduleFailure != null) {
initialSpawnReady.completeExceptionally(scheduleFailure);
}
});
} catch (Throwable callbackFailure) {
initialSpawnReady.completeExceptionally(new IllegalStateException(
"Initial spawn preparation failed for world \""
+ world.getName() + "\".",
callbackFailure));
}
});
} catch (Throwable failure) {
initialSpawnReady.completeExceptionally(failure);
}
}
private void completeSpawnLocation(World world, Location initialSpawn) {
try {
applySpawnLocation(world, initialSpawn);
initialSpawnReady.complete(null);
} catch (Throwable failure) {
initialSpawnReady.completeExceptionally(failure);
}
}
private void applySpawnLocation(World world, Location initialSpawn) {
@@ -255,10 +306,27 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2;
int y = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn) + 1));
int y = resolveInitialSpawnY(world, initialSpawn, minY, maxY);
world.setSpawnLocation(new Location(world, initialSpawn.getX(), y, initialSpawn.getZ(), initialSpawn.getYaw(), initialSpawn.getPitch()));
}
private int resolveInitialSpawnY(World world, Location initialSpawn, int minY, int maxY) {
Engine activeEngine = engine;
if (activeEngine != null && activeEngine.getComplex() != null && activeEngine.getComplex().getHeightStream() != null) {
int generatedY = activeEngine.getMinHeight()
+ activeEngine.getComplex().getHeightStream().get(initialSpawn.getX(), initialSpawn.getZ()).intValue()
+ 1;
return Math.max(minY, Math.min(maxY, generatedY));
}
return Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn) + 1));
}
@Override
public CompletableFuture<Void> getInitialSpawnReady() {
return initialSpawnReady;
}
@SuppressWarnings("unchecked")
private CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
try {
@@ -377,6 +445,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
this.hotloader = shouldRunStudioHotload(studio, closing, jigsawStudioActive) ? new Looper() {
@Override
protected long loop() {
Engine activeEngine = engine;
if (activeEngine instanceof IrisEngine irisEngine
&& irisEngine.isGenerationCacheWarmPending()) {
return HOTLOAD_LOOP_DELAY_MS;
}
if (shouldThrottleHotload()) {
return HOTLOAD_MAINTENANCE_DELAY_MS;
}
@@ -76,4 +76,8 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
}
CompletableFuture<Integer> getSpawnChunks();
default CompletableFuture<Void> getInitialSpawnReady() {
return CompletableFuture.completedFuture(null);
}
}
@@ -119,6 +119,15 @@ public final class RiverBodyProfile {
return roofScales[index];
}
public int intervalIndex(double alongReach) {
double position = StrictMath.max(0D, StrictMath.min(1D, alongReach));
int index = Arrays.binarySearch(positions, position);
if (index >= 0) {
return StrictMath.min(index, positions.length - 2);
}
return StrictMath.max(0, StrictMath.min(-index - 2, positions.length - 2));
}
@Override
public boolean equals(Object object) {
if (this == object) {
@@ -38,7 +38,7 @@ public final class RiverNetwork {
private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL;
private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL;
private static final int MINIMUM_BODY_PROFILE_SAMPLES = 12;
private static final int MAXIMUM_BODY_PROFILE_SAMPLES = 32;
private static final int MAXIMUM_BODY_PROFILE_SAMPLES = 512;
private static final double PERLIN_NORMALIZATION = 1.4142135623730951D;
private final RiverNetworkOptions options;
@@ -670,7 +670,8 @@ public final class RiverNetwork {
worm.bodyDetailWavelength(),
worm.seed() ^ detailSalt
);
return primary * 0.7D + detail * 0.3D;
double detailInfluence = worm.bodyDetailInfluence();
return primary * (1D - detailInfluence) + detail * detailInfluence;
}
private FlowTangent resolveFlowTangent(RiverNode node, RiverTerrainSampler terrain) {
@@ -1263,6 +1264,7 @@ public final class RiverNetwork {
BODY_WIDTH_DETAIL_SALT,
worm.widthVariation()
)
+ options.channelRadiusBonus() * 2D
)
);
double baseBankWidth = nonNegativeOrFallback(
@@ -29,6 +29,7 @@ public record RiverNetworkOptions(
double channelWidth,
double bankWidth,
double depth,
double channelRadiusBonus,
double maxChannelWidth,
double maxBankWidth,
double maxDepth,
@@ -60,6 +61,7 @@ public record RiverNetworkOptions(
requirePositive(channelWidth, "channelWidth");
requireFiniteNonNegative(bankWidth, "bankWidth");
requirePositive(depth, "depth");
requireFiniteNonNegative(channelRadiusBonus, "channelRadiusBonus");
requirePositive(maxChannelWidth, "maxChannelWidth");
requireFiniteNonNegative(maxBankWidth, "maxBankWidth");
requirePositive(maxDepth, "maxDepth");
@@ -215,6 +217,7 @@ public record RiverNetworkOptions(
private double channelWidth;
private double bankWidth;
private double depth;
private double channelRadiusBonus;
private double maxChannelWidth;
private double maxBankWidth;
private double maxDepth;
@@ -269,6 +272,7 @@ public record RiverNetworkOptions(
1D,
512D,
128D,
0.3D,
0D,
0D,
0D,
@@ -397,6 +401,11 @@ public record RiverNetworkOptions(
return this;
}
public Builder channelRadiusBonus(double value) {
channelRadiusBonus = value;
return this;
}
public Builder maxChannelWidth(double value) {
maxChannelWidth = value;
return this;
@@ -461,6 +470,7 @@ public record RiverNetworkOptions(
channelWidth,
bankWidth,
depth,
channelRadiusBonus,
maxChannelWidth,
maxBankWidth,
maxDepth,
@@ -325,25 +325,35 @@ public final class RiverTile {
double additionalRadius
) {
RiverPolyline polyline = reach.polyline();
if (polyline.length() == 0D) {
RiverBodyProfile bodyProfile = reach.bodyProfile();
double polylineLength = polyline.length();
if (polylineLength == 0D) {
double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0));
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D) + additionalRadius;
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
}
double nearest = Double.POSITIVE_INFINITY;
double nearestAlong = 0.0;
for (int point = 0; point < polyline.size() - 1; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polyline.length();
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polyline.length();
int pointLimit = polyline.size() - 1;
int profileLimit = bodyProfile.size() - 1;
for (int point = 0; point < pointLimit; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength;
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength;
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
if (segmentAlongSpan == 0D) {
continue;
}
double deltaX = polyline.x(point + 1) - polyline.x(point);
double deltaZ = polyline.z(point + 1) - polyline.z(point);
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
double profileStart = reach.bodyProfile().position(profileIndex);
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
double startX = polyline.x(point);
double startZ = polyline.z(point);
double deltaX = polyline.x(point + 1) - startX;
double deltaZ = polyline.z(point + 1) - startZ;
int firstProfileIndex = bodyProfile.intervalIndex(segmentStartAlong);
for (int profileIndex = firstProfileIndex;
profileIndex < profileLimit
&& bodyProfile.position(profileIndex) <= segmentEndAlong;
profileIndex++) {
double profileStart = bodyProfile.position(profileIndex);
double profileEnd = bodyProfile.position(profileIndex + 1);
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
if (overlapStart > overlapEnd) {
@@ -351,13 +361,16 @@ public final class RiverTile {
}
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
double profileSpan = profileEnd - profileStart;
double profileWidth = bodyProfile.widthAtIndex(profileIndex);
double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex);
double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1)
- profileWidth) / profileSpan;
double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1)
- profileBankWidth) / profileSpan;
double radiusBase = (profileWidth
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
+ profileBankWidth
+ bankSlope * (segmentStartAlong - profileStart)
+ additionalRadius;
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
@@ -365,9 +378,9 @@ public final class RiverTile {
intervalStart,
intervalEnd,
deltaX,
polyline.x(point) - x,
startX - x,
deltaZ,
polyline.z(point) - z,
startZ - z,
radiusSlope,
radiusBase,
segmentStartAlong,
@@ -390,7 +403,9 @@ public final class RiverTile {
double maximumZ
) {
RiverPolyline polyline = reach.polyline();
if (polyline.length() == 0D) {
RiverBodyProfile bodyProfile = reach.bodyProfile();
double polylineLength = polyline.length();
if (polylineLength == 0D) {
double distanceSquared = pointRectangleDistanceSquared(
polyline.x(0),
polyline.z(0),
@@ -404,9 +419,11 @@ public final class RiverTile {
}
double nearest = Double.POSITIVE_INFINITY;
double nearestAlong = 0.0;
for (int point = 0; point < polyline.size() - 1; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polyline.length();
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polyline.length();
int pointLimit = polyline.size() - 1;
int profileLimit = bodyProfile.size() - 1;
for (int point = 0; point < pointLimit; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength;
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength;
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
if (segmentAlongSpan == 0D) {
continue;
@@ -415,9 +432,13 @@ public final class RiverTile {
double startZ = polyline.z(point);
double deltaX = polyline.x(point + 1) - startX;
double deltaZ = polyline.z(point + 1) - startZ;
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
double profileStart = reach.bodyProfile().position(profileIndex);
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
int firstProfileIndex = bodyProfile.intervalIndex(segmentStartAlong);
for (int profileIndex = firstProfileIndex;
profileIndex < profileLimit
&& bodyProfile.position(profileIndex) <= segmentEndAlong;
profileIndex++) {
double profileStart = bodyProfile.position(profileIndex);
double profileEnd = bodyProfile.position(profileIndex + 1);
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
if (overlapStart > overlapEnd) {
@@ -425,13 +446,16 @@ public final class RiverTile {
}
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
double profileSpan = profileEnd - profileStart;
double profileWidth = bodyProfile.widthAtIndex(profileIndex);
double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex);
double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1)
- profileWidth) / profileSpan;
double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1)
- profileBankWidth) / profileSpan;
double radiusBase = (profileWidth
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
+ profileBankWidth
+ bankSlope * (segmentStartAlong - profileStart);
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
double cursor = intervalStart;
@@ -626,8 +650,7 @@ public final class RiverTile {
}
private List<RiverReach> indexedReaches(double x, double z) {
List<RiverReach> indexed = spatialIndex.get(bucketKey(bucket(x), bucket(z)));
return indexed == null ? List.of() : indexed;
return spatialIndex.getOrDefault(bucketKey(bucket(x), bucket(z)), List.of());
}
private List<RiverReach> indexedReaches(
@@ -641,12 +664,15 @@ public final class RiverTile {
int maximumBucketX = bucket(StrictMath.nextDown(queryMaximumX));
int minimumBucketZ = bucket(queryMinimumZ);
int maximumBucketZ = bucket(StrictMath.nextDown(queryMaximumZ));
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
return spatialIndex.getOrDefault(
bucketKey(minimumBucketX, minimumBucketZ),
List.of()
);
}
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(bucketX, bucketZ));
if (bucketReaches != null) {
indexed.addAll(bucketReaches);
}
indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of()));
}
}
return List.copyOf(indexed);
@@ -662,26 +688,13 @@ public final class RiverTile {
int maximumBucketX = bucket(queryMaximumX);
int minimumBucketZ = bucket(queryMinimumZ);
int maximumBucketZ = bucket(queryMaximumZ);
if (spatialIndex.isEmpty()) {
return List.of();
}
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(minimumBucketX, minimumBucketZ));
return bucketReaches == null ? List.of() : bucketReaches;
}
long bucketWidth = (long) maximumBucketX - minimumBucketX + 1L;
long bucketDepth = (long) maximumBucketZ - minimumBucketZ + 1L;
if (bucketWidth > spatialIndex.size() / bucketDepth
|| bucketWidth * bucketDepth >= spatialIndex.size()) {
return reaches;
return indexedReaches(queryMinimumX, queryMinimumZ);
}
LinkedHashSet<RiverReach> indexed = new LinkedHashSet<>();
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(bucketX, bucketZ));
if (bucketReaches != null) {
indexed.addAll(bucketReaches);
}
indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of()));
}
}
return List.copyOf(indexed);
@@ -17,6 +17,7 @@ public record RiverWorm(
double depthMultiplier,
double bodyWavelength,
double bodyDetailWavelength,
double bodyDetailInfluence,
double widthVariation,
double bankVariation,
double depthVariation,
@@ -44,8 +45,9 @@ public record RiverWorm(
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(bodyWavelength, 8D, 16384D, "bodyWavelength");
requireRange(bodyDetailWavelength, 8D, 16384D, "bodyDetailWavelength");
requireRange(bodyDetailInfluence, 0D, 1D, "bodyDetailInfluence");
requireRange(widthVariation, 0D, 0.875D, "widthVariation");
requireRange(bankVariation, 0D, 0.875D, "bankVariation");
requireRange(depthVariation, 0D, 0.875D, "depthVariation");
@@ -2,7 +2,7 @@ package art.arcane.iris.engine.river.cave;
public enum RiverCaveAction {
WET_SOURCE,
FALLING_WATER,
FALLING_FLUID,
DRY_AIR,
SEAL_GUARD
}
@@ -70,6 +70,7 @@ public final class RiverCaveContainmentPlanner {
settings,
throat.positions()
);
case DEEP_POOL -> planDeepPool(view, source, settings, throat.positions());
};
}
@@ -302,6 +303,111 @@ public final class RiverCaveContainmentPlanner {
return accepted(view, source, actions);
}
private RiverCavePlan planDeepPool(
CaveVoxelView view,
RiverCaveSource source,
RiverCavePlannerSettings settings,
List<CavePosition> throat
) {
GrottoResult grotto = buildGrotto(source, settings);
if (grotto.rejection() != RiverCaveRejection.NONE) {
return rejected(source, grotto.rejection());
}
Set<CavePosition> chamber = grotto.positions();
Set<CavePosition> carve = new HashSet<>(chamber.size() + throat.size());
carve.addAll(chamber);
carve.addAll(throat);
RiverCaveRejection carveRejection = validateDeepPoolCarve(view, source, settings, carve);
if (carveRejection != RiverCaveRejection.NONE) {
return rejected(source, carveRejection);
}
BoundaryResult boundary = validateDeepPoolBoundary(view, source, settings, carve);
if (boundary.rejection() != RiverCaveRejection.NONE) {
return rejected(source, boundary.rejection());
}
Map<CavePosition, RiverCaveAction> actions = new HashMap<>();
addChamberActions(actions, chamber, source.waterHeadY());
addThroatActions(actions, throat, source);
for (CavePosition position : boundary.sealGuards()) {
actions.put(position, RiverCaveAction.SEAL_GUARD);
}
return accepted(view, source, actions);
}
private RiverCaveRejection validateDeepPoolCarve(
CaveVoxelView view,
RiverCaveSource source,
RiverCavePlannerSettings settings,
Set<CavePosition> carve
) {
for (CavePosition position : carve) {
if (!view.isInWorld(position)) {
return RiverCaveRejection.WORLD_BOUNDARY;
}
RiverCaveRejection boundsRejection = validateBounds(source, settings, position);
if (boundsRejection != RiverCaveRejection.NONE) {
return boundsRejection;
}
CaveVoxel voxel = voxelAt(view, position);
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
if (hazard != RiverCaveRejection.NONE) {
return hazard;
}
if (voxel == CaveVoxel.SOLID) {
continue;
}
if (position.y() > source.waterHeadY()
&& voxel == CaveVoxel.CAVE_AIR
&& !view.isOpenToSurface(position)) {
continue;
}
return RiverCaveRejection.GROTTO_INTERSECTION;
}
return RiverCaveRejection.NONE;
}
private BoundaryResult validateDeepPoolBoundary(
CaveVoxelView view,
RiverCaveSource source,
RiverCavePlannerSettings settings,
Set<CavePosition> carve
) {
Set<CavePosition> guards = new HashSet<>();
for (CavePosition position : carve) {
for (CavePosition direction : DIRECTIONS) {
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
if (carve.contains(neighbor)) {
continue;
}
if (!view.isInWorld(neighbor)) {
return BoundaryResult.rejected(RiverCaveRejection.WORLD_BOUNDARY);
}
RiverCaveRejection boundsRejection = validateBounds(source, settings, neighbor);
if (boundsRejection != RiverCaveRejection.NONE) {
return BoundaryResult.rejected(boundsRejection);
}
CaveVoxel voxel = voxelAt(view, neighbor);
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
if (hazard != RiverCaveRejection.NONE) {
return BoundaryResult.rejected(hazard);
}
if (voxel == CaveVoxel.SOLID) {
guards.add(neighbor);
continue;
}
if (neighbor.y() > source.waterHeadY()
&& voxel == CaveVoxel.CAVE_AIR
&& !view.isOpenToSurface(neighbor)) {
continue;
}
return BoundaryResult.rejected(RiverCaveRejection.GROTTO_SHELL_OPEN);
}
}
return BoundaryResult.accepted(guards);
}
private ComponentResult resolveClosedComponent(
CaveVoxelView view,
RiverCaveSource source,
@@ -732,7 +838,7 @@ public final class RiverCaveContainmentPlanner {
action = RiverCaveAction.WET_SOURCE;
} else if (source.mode() == RiverCaveMode.WATERFALL_POOL
|| source.mode() == RiverCaveMode.GENERATED_GROTTO) {
action = RiverCaveAction.FALLING_WATER;
action = RiverCaveAction.FALLING_FLUID;
} else {
action = RiverCaveAction.DRY_AIR;
}
@@ -0,0 +1,6 @@
package art.arcane.iris.engine.river.cave;
public enum RiverCaveFluidKind {
RIVER,
DEEP_POOL
}
@@ -11,20 +11,30 @@ public final class RiverCaveHydrology {
private final RiverCaveAction action;
private final String floodedBiomeKey;
private final RiverCaveFluidKind fluidKind;
private final MatterCavern cavern;
public RiverCaveHydrology(RiverCaveAction action, String floodedBiomeKey) {
public RiverCaveHydrology(
RiverCaveAction action,
String floodedBiomeKey,
RiverCaveFluidKind fluidKind
) {
this.action = Objects.requireNonNull(action);
this.floodedBiomeKey = floodedBiomeKey == null ? "" : floodedBiomeKey.trim();
this.fluidKind = Objects.requireNonNull(fluidKind);
this.cavern = switch (action) {
case WET_SOURCE, FALLING_WATER -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FLUID);
case WET_SOURCE, FALLING_FLUID -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FLUID);
case DRY_AIR -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FORCED_AIR);
case SEAL_GUARD -> null;
};
}
public static RiverCaveHydrology of(RiverCaveAction action) {
return new RiverCaveHydrology(action, "");
return new RiverCaveHydrology(action, "", RiverCaveFluidKind.RIVER);
}
public static RiverCaveHydrology of(RiverCaveAction action, RiverCaveFluidKind fluidKind) {
return new RiverCaveHydrology(action, "", fluidKind);
}
public Optional<String> floodedBiome() {
@@ -36,11 +46,11 @@ public final class RiverCaveHydrology {
}
public boolean isWet() {
return action == RiverCaveAction.WET_SOURCE || action == RiverCaveAction.FALLING_WATER;
return action == RiverCaveAction.WET_SOURCE || action == RiverCaveAction.FALLING_FLUID;
}
public boolean isFalling() {
return action == RiverCaveAction.FALLING_WATER;
return action == RiverCaveAction.FALLING_FLUID;
}
public boolean protectsPlacement() {
@@ -59,6 +69,10 @@ public final class RiverCaveHydrology {
return floodedBiomeKey;
}
public RiverCaveFluidKind fluidKind() {
return fluidKind;
}
@Override
public boolean equals(Object object) {
if (this == object) {
@@ -67,16 +81,19 @@ public final class RiverCaveHydrology {
if (!(object instanceof RiverCaveHydrology hydrology)) {
return false;
}
return action == hydrology.action && floodedBiomeKey.equals(hydrology.floodedBiomeKey);
return action == hydrology.action
&& floodedBiomeKey.equals(hydrology.floodedBiomeKey)
&& fluidKind == hydrology.fluidKind;
}
@Override
public int hashCode() {
return (31 * action.hashCode()) + floodedBiomeKey.hashCode();
return (31 * ((31 * action.hashCode()) + floodedBiomeKey.hashCode())) + fluidKind.hashCode();
}
@Override
public String toString() {
return "RiverCaveHydrology[action=" + action + ", floodedBiomeKey=" + floodedBiomeKey + "]";
return "RiverCaveHydrology[action=" + action + ", floodedBiomeKey=" + floodedBiomeKey
+ ", fluidKind=" + fluidKind + "]";
}
}
@@ -4,5 +4,6 @@ public enum RiverCaveMode {
CLOSED_COMPONENT,
GENERATED_GROTTO,
GROTTO_OR_CLOSED_COMPONENT,
WATERFALL_POOL
WATERFALL_POOL,
DEEP_POOL
}
@@ -9,6 +9,7 @@ import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverNetwork;
import art.arcane.iris.engine.object.IrisRiverCaveMode;
import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.object.IrisRiverDeepPools;
import art.arcane.iris.engine.object.IrisRiverNoiseChance;
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
import art.arcane.iris.engine.object.IrisRiverTerminalMode;
@@ -41,6 +42,8 @@ import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.ArrayList;
import java.util.HashSet;
@@ -48,6 +51,7 @@ import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class IrisRiverRuntime implements AutoCloseable {
static final int MAXIMUM_REACH_FEASIBILITY_SAMPLES = 65;
@@ -66,17 +70,22 @@ public final class IrisRiverRuntime implements AutoCloseable {
private static final long BIOME_NOISE_SALT = 0x2FFD72DBD01ADFB7L;
private static final long CAVE_ENTRY_NOISE_SALT = 0xB8E1AFED6A267E96L;
private static final long CAVE_ENTRY_GATE_SALT = 0xBA7C9045F12C7F99L;
private static final long DEEP_POOL_REACH_NOISE_SALT = 0x7137449123EF65CDL;
private static final long DEEP_POOL_REACH_GATE_SALT = 0xE9B5DBA58189DBBCL;
private static final long TUNNEL_FLOOR_NOISE_SALT = 0x8CB92BA72F3D8DD7L;
private static final long TUNNEL_ROOF_NOISE_SALT = 0xDB4F0B9175AE2165L;
private static final long TUNNEL_WIDTH_NOISE_SALT = 0xC6EF372FE94F82BEL;
private static final long FLOODED_CAVE_BIOME_SALT = 0x24A19947B3916CF7L;
private static final long TERMINAL_CAVE_ANCHOR_SALT = 0x9E3779B97F4A7C15L;
private static final int TILE_CACHE_SIZE = 32;
private static final int TUNNEL_SAMPLE_CHUNK_CACHE_SIZE = 4_096;
private static final Object ABSENT_TUNNEL_SAMPLE = new Object();
private final long seed;
private final IrisRiverNetwork configuration;
private final IrisData data;
private final int fluidHeight;
private final int riverFluidHeight;
private final int dimensionFluidHeight;
private final boolean boreMantleActive;
private final boolean caveHydrologyActive;
private final boolean blockingRoutingPossible;
@@ -101,12 +110,14 @@ public final class IrisRiverRuntime implements AutoCloseable {
private final CNG bedNoise;
private final CNG biomeNoise;
private final CNG caveEntryNoise;
private final CNG deepPoolReachNoise;
private final CNG tunnelFloorNoise;
private final CNG tunnelRoofNoise;
private final CNG tunnelWidthNoise;
private final art.arcane.iris.engine.river.RiverNetwork network;
private final RuntimeTerrainSampler terrainSampler;
private final RiverTileCache tileCache;
private final Cache<Long, TunnelSampleChunk> tunnelSampleCache;
private final ConcurrentHashMap<IdentitySettingsKey, EffectiveRiverSettings> settingsCache;
private final ConcurrentHashMap<BiomePoolKey, List<IrisBiome>> biomePoolCache;
@@ -115,7 +126,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
seed = context.seed();
configuration = context.configuration();
data = context.data();
fluidHeight = context.fluidHeight();
riverFluidHeight = context.riverFluidHeight();
dimensionFluidHeight = context.dimensionFluidHeight();
boreMantleActive = context.boreMantleActive();
caveHydrologyActive = context.caveHydrologyActive();
blockingRoutingPossible = context.blockingRoutingPossible();
@@ -146,6 +158,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT);
biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT);
caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_NOISE_SALT);
IrisRiverDeepPools deepPools = caves.getDeepPools();
deepPoolReachNoise = noise(
deepPools == null ? null : deepPools.getReach(),
DEEP_POOL_REACH_NOISE_SALT
);
tunnelFloorNoise = noise(terrain.getTunnelFloorStyle(), TUNNEL_FLOOR_NOISE_SALT);
tunnelRoofNoise = noise(terrain.getTunnelRoofStyle(), TUNNEL_ROOF_NOISE_SALT);
tunnelWidthNoise = noise(terrain.getTunnelWidthMultiplier(), TUNNEL_WIDTH_NOISE_SALT);
@@ -158,12 +175,15 @@ public final class IrisRiverRuntime implements AutoCloseable {
TILE_CACHE_SIZE,
(tileX, tileZ) -> network.buildTile(tileX, tileZ, terrainSampler)
);
tunnelSampleCache = Caffeine.newBuilder()
.maximumSize(TUNNEL_SAMPLE_CHUNK_CACHE_SIZE)
.build();
}
public IrisRiverSurfaceSample sample(double x, double z) {
ResolvedRiverColumn column = resolveColumn(x, z);
if (column == null) {
return IrisRiverSurfaceSample.none(naturalHeight.get(x, z), fluidHeight);
return IrisRiverSurfaceSample.none(naturalHeight.get(x, z), dimensionFluidHeight);
}
if (column.subterranean()) {
return new IrisRiverSurfaceSample(
@@ -229,9 +249,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
if (!column.subterranean()) {
return null;
}
if (isTunnelMouth(column, mouthBlend)) {
channelRadius += mouthBlend;
}
double mouthFactor = tunnelMouthFactor(column, mouthBlend);
channelRadius += mouthBlend * mouthFactor;
if (column.river().distance() > channelRadius) {
return null;
}
@@ -255,13 +274,20 @@ public final class IrisRiverRuntime implements AutoCloseable {
);
int ceilingY = shapedTunnelCeilingY(
waterHeadY,
caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach()),
caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach())
+ mouthBlend * mouthFactor,
profile,
roofOffset
);
return new IrisRiverTunnelSample(column.river(), bedY, waterHeadY, ceilingY);
}
public IrisRiverTunnelSample sampleTunnel(int x, int z) {
long chunkKey = ((long) (x >> 4) << 32) ^ ((z >> 4) & 0xFFFFFFFFL);
TunnelSampleChunk chunk = tunnelSampleCache.get(chunkKey, ignored -> new TunnelSampleChunk());
return chunk.sample(this, x, z);
}
static int shapedTunnelBedY(
int waterHeadY,
double baseBedY,
@@ -331,18 +357,44 @@ public final class IrisRiverRuntime implements AutoCloseable {
);
}
private boolean isTunnelMouth(ResolvedRiverColumn column, double mouthBlend) {
private double tunnelMouthFactor(ResolvedRiverColumn column, double mouthBlend) {
if (mouthBlend <= 0D) {
return false;
return 0D;
}
double length = column.reach().polyline().length();
if (length <= 0D) {
return false;
return 0D;
}
double offset = mouthBlend / length;
double alongReach = column.river().alongReach();
return !isCenterlineSubterranean(column.reach(), clamp01(alongReach - offset))
|| !isCenterlineSubterranean(column.reach(), clamp01(alongReach + offset));
return Math.max(
tunnelMouthFactor(column.reach(), alongReach, clamp01(alongReach - offset), mouthBlend),
tunnelMouthFactor(column.reach(), alongReach, clamp01(alongReach + offset), mouthBlend)
);
}
private double tunnelMouthFactor(
RiverReach reach,
double subterraneanAlong,
double candidateOpenAlong,
double mouthBlend
) {
if (candidateOpenAlong == subterraneanAlong
|| isCenterlineSubterranean(reach, candidateOpenAlong)) {
return 0D;
}
double openAlong = candidateOpenAlong;
double solidAlong = subterraneanAlong;
for (int iteration = 0; iteration < 5; iteration++) {
double midpoint = (openAlong + solidAlong) * 0.5D;
if (isCenterlineSubterranean(reach, midpoint)) {
solidAlong = midpoint;
} else {
openAlong = midpoint;
}
}
double distance = StrictMath.abs(solidAlong - subterraneanAlong) * reach.polyline().length();
return clamp01(1D - distance / mouthBlend);
}
private boolean isCenterlineSubterranean(RiverReach reach, double alongReach) {
@@ -401,6 +453,35 @@ public final class IrisRiverRuntime implements AutoCloseable {
return tileAt(centerX, centerZ).sampleFootprint(minimumX, minimumZ, maximumX, maximumZ);
}
public boolean hasRiverFootprint(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ
) {
if (minimumX >= maximumX || minimumZ >= maximumZ) {
return false;
}
int minimumTileX = network.tileXForBlock(minimumX);
int minimumTileZ = network.tileZForBlock(minimumZ);
int maximumTileX = network.tileXForBlock(maximumX - 1);
int maximumTileZ = network.tileZForBlock(maximumZ - 1);
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
RiverSample sample = tileCache.get(tileX, tileZ).sampleFootprint(
minimumX,
minimumZ,
maximumX,
maximumZ
);
if (sample.present()) {
return true;
}
}
}
return false;
}
public List<RiverAnchor> candidateAnchors(
int minimumX,
int minimumZ,
@@ -481,7 +562,9 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
public int maximumTunnelHeadroom() {
return caves.getDryHeadroom() + (int) StrictMath.ceil(terrain.getTunnelRoofVariation());
return caves.getDryHeadroom()
+ (int) StrictMath.ceil(terrain.getTunnelRoofVariation())
+ (int) StrictMath.ceil(terrain.getTunnelMouthBlend());
}
public boolean acceptsCaveAnchor(RiverAnchor anchor) {
@@ -535,6 +618,52 @@ public final class IrisRiverRuntime implements AutoCloseable {
return false;
}
public boolean acceptsDeepPoolAnchor(RiverAnchor anchor) {
Objects.requireNonNull(anchor);
IrisRiverDeepPools deepPools = caves.getDeepPools();
if (deepPools == null
|| !deepPools.isEnabled()
|| deepPools.getMaximumPerReach() <= 0
|| anchor.state() != RiverRouteState.WET
|| !caveHydrologyActive) {
return false;
}
RiverReach reach = tileAt(anchor.x(), anchor.z()).reach(anchor.reachId());
if (reach == null || reach.state() != RiverRouteState.WET) {
return false;
}
if (!deepPoolReachEligible(deepPools, reach)) {
return false;
}
TerminalCaveAnchor terminal = terminalCaveAnchor(reach);
if (terminal != null && anchor.stableId() == terminal.stableId()) {
return false;
}
double firstDistance = unit(art.arcane.iris.engine.river.RiverNetwork.mix(
reach.id().stableId() ^ anchor.samplingSalt()
)) * anchor.samplingSpacing();
double anchorDistance = firstDistance + anchor.index() * anchor.samplingSpacing();
if (anchorDistance >= reach.polyline().length()) {
return false;
}
int accepted = 0;
for (int index = 0; index <= anchor.index(); index++) {
long stableId = art.arcane.iris.engine.river.RiverNetwork.mix(
reach.id().stableId()
^ anchor.samplingSalt()
^ (long) index * 0x9E3779B97F4A7C15L
);
if (index == anchor.index()) {
return stableId == anchor.stableId() && accepted < deepPools.getMaximumPerReach();
}
accepted++;
if (accepted >= deepPools.getMaximumPerReach()) {
return false;
}
}
return false;
}
private boolean isCaveAnchorSourceable(double x, double z) {
IrisRiverSurfaceSample surface = sample(x, z);
if (surface.river().present()
@@ -603,6 +732,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
@Override
public void close() {
tileCache.close();
tunnelSampleCache.invalidateAll();
settingsCache.clear();
biomePoolCache.clear();
}
@@ -620,7 +750,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
.routingDeviationScaleCells(topology.getRoutingDeviationScaleCells())
.routingDeviationStrengthCells(topology.getRoutingDeviationStrengthCells())
.routingPlateauHeight(topology.getRoutingPlateauHeight())
.hydraulicBaseHeight(fluidHeight)
.hydraulicBaseHeight(riverFluidHeight)
.requireOcean(topology.isRequireOcean())
.sourceChance(chance(topology.getSource()))
.reachChance(chance(topology.getContinuation()))
@@ -633,6 +763,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
.channelWidth(mid(riverTerrain.getChannelWidth(), 12D))
.bankWidth(mid(riverTerrain.getBankWidth(), 8D))
.depth(mid(riverTerrain.getDepth(), 4D))
.channelRadiusBonus(riverTerrain.getChannelRadiusBonus())
.maxChannelWidth(riverTerrain.getMaxChannelWidth())
.maxBankWidth(riverTerrain.getMaxBankWidth())
.maxDepth(riverTerrain.getMaxDepth())
@@ -683,6 +814,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
configured.getDepthMultiplier(),
configured.getBodyWavelength(),
configured.getBodyDetailWavelength(),
configured.getBodyDetailInfluence(),
configured.getWidthVariation(),
configured.getBankVariation(),
configured.getDepthVariation(),
@@ -757,6 +889,24 @@ public final class IrisRiverRuntime implements AutoCloseable {
return unit(hash) < chance;
}
private boolean deepPoolReachEligible(
IrisRiverDeepPools deepPools,
RiverReach reach
) {
CenterlinePosition center = centerlinePosition(reach, 0.5D);
EffectiveRiverSettings settings = settingsAt(center.x(), center.z());
double chance = clamp01(effectiveChance(
deepPools.getReach(),
deepPoolReachNoise,
(int) StrictMath.floor(center.x()),
(int) StrictMath.floor(center.z())
) * settings.caveEntryMultiplier());
long hash = art.arcane.iris.engine.river.RiverNetwork.mix(
seed ^ reach.id().stableId() ^ DEEP_POOL_REACH_GATE_SALT
);
return unit(hash) < chance;
}
private TerminalCaveAnchor terminalCaveAnchor(RiverReach reach) {
if (!caveHydrologyActive || !reach.terminal() || reach.state() != RiverRouteState.WET) {
return null;
@@ -809,8 +959,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
double waterSurface(RiverReach reach, double alongReach, boolean naturalOcean) {
if (reach == null || water.getMode() == IrisRiverWaterMode.SEA_LEVEL || naturalOcean) {
return fluidHeight;
if (naturalOcean) {
return dimensionFluidHeight;
}
if (reach == null || water.getMode() == IrisRiverWaterMode.FIXED) {
return riverFluidHeight;
}
return terracedWaterSurface(
reach.from().hydraulicHeight(),
@@ -883,10 +1036,10 @@ public final class IrisRiverRuntime implements AutoCloseable {
private int nodeWaterHead(double naturalNodeHeight, int dropHeight) {
int availableRise = Math.max(0, water.getMaximumPoolRise());
int maximumHead = fluidHeight + availableRise;
int maximumHead = riverFluidHeight + availableRise;
int naturalHead = (int) StrictMath.floor(naturalNodeHeight - 1D);
int clamped = Math.max(fluidHeight, Math.min(maximumHead, naturalHead));
return fluidHeight + Math.floorDiv(clamped - fluidHeight, dropHeight) * dropHeight;
int clamped = Math.max(riverFluidHeight, Math.min(maximumHead, naturalHead));
return riverFluidHeight + Math.floorDiv(clamped - riverFluidHeight, dropHeight) * dropHeight;
}
private static boolean isNaturalOcean(IrisBiome biome) {
@@ -1011,6 +1164,25 @@ public final class IrisRiverRuntime implements AutoCloseable {
return (hash >>> 11) * 0x1.0p-53;
}
private static final class TunnelSampleChunk {
private final AtomicReferenceArray<Object> samples = new AtomicReferenceArray<>(256);
private IrisRiverTunnelSample sample(IrisRiverRuntime runtime, int x, int z) {
int index = ((x & 15) << 4) | (z & 15);
Object cached = samples.get(index);
if (cached == null) {
IrisRiverTunnelSample computed = runtime.sampleTunnel((double) x, (double) z);
Object encoded = computed == null ? ABSENT_TUNNEL_SAMPLE : computed;
if (samples.compareAndSet(index, null, encoded)) {
cached = encoded;
} else {
cached = samples.get(index);
}
}
return cached == ABSENT_TUNNEL_SAMPLE ? null : (IrisRiverTunnelSample) cached;
}
}
private final class RuntimeTerrainSampler implements RiverTerrainSampler {
private final IrisRiverTopology topology;
@@ -1022,11 +1194,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
public RiverTerrainNodeSample sampleNode(int blockX, int blockZ) {
boolean oceanIntent = Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ));
boolean naturalHeightRequired = topology.getTerrainHeightWeight() > 0D
|| water.getMode() != IrisRiverWaterMode.SEA_LEVEL
|| water.getMode() != IrisRiverWaterMode.FIXED
|| oceanIntent;
double sampledNaturalHeight = naturalHeightRequired
? naturalHeight.get(blockX, blockZ)
: fluidHeight;
: dimensionFluidHeight;
boolean sampledOcean = oceanIntent && isSubmergedOutlet(sampledNaturalHeight);
IrisRegion sampledRegion = region.get(blockX, blockZ);
IrisBiome sampledBiome = biomeRiverOverridesPossible ? naturalBiome.get(blockX, blockZ) : null;
@@ -1068,7 +1240,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
private boolean isSubmergedOutlet(double sampledNaturalHeight) {
return Math.round(sampledNaturalHeight) < Math.round(fluidHeight);
return Math.round(sampledNaturalHeight) < Math.round(dimensionFluidHeight);
}
@Override
@@ -1150,8 +1322,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
maximumIncision *= settings.maxIncisionMultiplier();
}
double head = configuration.getWater().getMode() == IrisRiverWaterMode.SEA_LEVEL
? fluidHeight
double head = configuration.getWater().getMode() == IrisRiverWaterMode.FIXED
? riverFluidHeight
: terracedWaterSurface(
context.from().hydraulicHeight(),
context.to().hydraulicHeight(),
@@ -12,7 +12,8 @@ public record IrisRiverRuntimeContext(
long seed,
IrisRiverNetwork configuration,
IrisData data,
int fluidHeight,
int riverFluidHeight,
int dimensionFluidHeight,
boolean boreMantleActive,
boolean caveHydrologyActive,
boolean blockingRoutingPossible,
@@ -52,6 +52,7 @@ import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.bukkit.util.Vector;
@@ -236,6 +237,11 @@ public final class BukkitPlatform implements IrisPlatform {
return io.papermc.lib.PaperLib.teleportAsync(entity, destination);
}
public static java.util.concurrent.CompletableFuture<Boolean> teleportAsync(Entity entity, Location destination,
PlayerTeleportEvent.TeleportCause cause) {
return io.papermc.lib.PaperLib.teleportAsync(entity, destination, cause);
}
public static boolean isPaperServer() {
return io.papermc.lib.PaperLib.isPaper();
}
@@ -19,12 +19,55 @@ public class SlimJar {
private static final ReentrantLock lock = new ReentrantLock();
private static final AtomicBoolean loaded = new AtomicBoolean();
public static void loadBootstrap(Path downloadPath, BootstrapLogger logger) {
if (loaded.get()) {
return;
}
lock.lock();
try {
if (loaded.get()) {
return;
}
ApplicationBuilder.appending("Iris")
.injectableFactory(InjectableFactory.selecting(
InjectableFactory.ERROR,
InjectableFactory.INJECTABLE,
InjectableFactory.WRAPPED,
InjectableFactory.UNSAFE))
.downloadDirectoryPath(downloadPath)
.logger(new ProcessLogger() {
@Override
public void info(@NotNull String message, @Nullable Object... args) {
logger.info(message.formatted(args));
}
@Override
public void error(@NotNull String message, @Nullable Object... args) {
logger.error(message.formatted(args));
}
@Override
public void debug(@NotNull String message, @Nullable Object... args) {
logger.debug(message.formatted(args));
}
})
.build();
loaded.set(true);
} finally {
lock.unlock();
}
}
public static void load() {
if (loaded.get()) return;
if (loaded.get()) {
return;
}
lock.lock();
try {
if (loaded.getAndSet(true)) return;
if (loaded.get()) {
return;
}
VolmitPlugin plugin = BukkitPlatform.volmitPlugin();
Path downloadPath = plugin.getDataFolder("cache", "libraries").toPath();
debug(plugin, "Loading libraries...");
@@ -58,6 +101,7 @@ public class SlimJar {
})
.build();
}
loaded.set(true);
debug(plugin, "Libraries loaded successfully!");
} finally {
lock.unlock();
@@ -69,4 +113,12 @@ public class SlimJar {
plugin.getLogger().info("[DEBUG] " + message);
}
}
public interface BootstrapLogger {
void info(String message);
void error(String message);
void debug(String message);
}
}
@@ -303,6 +303,43 @@ public class J {
return true;
}
public static CompletableFuture<Void> runRegionFuture(
World world,
int chunkX,
int chunkZ,
Runnable runnable
) {
if (world == null || runnable == null) {
return CompletableFuture.failedFuture(new IllegalArgumentException(
"Region task world and runnable are required."));
}
if (isFolia()) {
CompletableFuture<Void> future = new CompletableFuture<>();
if (isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
settle(future, runnable);
return future;
}
if (!runRegionImmediate(
world,
chunkX,
chunkZ,
() -> settle(future, runnable))) {
future.completeExceptionally(new IllegalStateException(
"Failed to schedule region task for " + world.getName()
+ "@" + chunkX + "," + chunkZ + "."));
}
return future;
}
if (isPrimaryThread()) {
CompletableFuture<Void> future = new CompletableFuture<>();
settle(future, runnable);
return future;
}
return sfut(runnable);
}
public static boolean runGlobal(Runnable runnable) {
if (runnable == null) {
return false;
@@ -1,6 +1,7 @@
package art.arcane.iris.util.project.matter.slices;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.matter.Sliced;
@@ -28,19 +29,21 @@ public final class RiverCaveHydrologyMatter extends RawMatter<RiverCaveHydrology
@Override
public void writeNode(RiverCaveHydrology hydrology, DataOutputStream output) throws IOException {
output.writeByte(actionCode(hydrology.action()));
output.writeByte(fluidKindCode(hydrology.fluidKind()));
output.writeUTF(hydrology.floodedBiomeKey());
}
@Override
public RiverCaveHydrology readNode(DataInputStream input) throws IOException {
RiverCaveAction action = actionFromCode(input.readUnsignedByte());
return new RiverCaveHydrology(action, input.readUTF());
RiverCaveFluidKind fluidKind = fluidKindFromCode(input.readUnsignedByte());
return new RiverCaveHydrology(action, input.readUTF(), fluidKind);
}
private int actionCode(RiverCaveAction action) {
return switch (action) {
case WET_SOURCE -> 1;
case FALLING_WATER -> 2;
case FALLING_FLUID -> 2;
case DRY_AIR -> 3;
case SEAL_GUARD -> 4;
};
@@ -49,10 +52,25 @@ public final class RiverCaveHydrologyMatter extends RawMatter<RiverCaveHydrology
private RiverCaveAction actionFromCode(int code) throws IOException {
return switch (code) {
case 1 -> RiverCaveAction.WET_SOURCE;
case 2 -> RiverCaveAction.FALLING_WATER;
case 2 -> RiverCaveAction.FALLING_FLUID;
case 3 -> RiverCaveAction.DRY_AIR;
case 4 -> RiverCaveAction.SEAL_GUARD;
default -> throw new IOException("Unknown river cave hydrology action code " + code);
};
}
private int fluidKindCode(RiverCaveFluidKind fluidKind) {
return switch (fluidKind) {
case RIVER -> 1;
case DEEP_POOL -> 2;
};
}
private RiverCaveFluidKind fluidKindFromCode(int code) throws IOException {
return switch (code) {
case 1 -> RiverCaveFluidKind.RIVER;
case 2 -> RiverCaveFluidKind.DEEP_POOL;
default -> throw new IOException("Unknown river cave fluid-kind code " + code);
};
}
}
@@ -447,6 +447,15 @@ public class ServerConfiguratorDatapackFingerprintTest {
assertFalse(ServerConfigurator.loadedRegistrySatisfies(
loaded,
Map.of("worldgen/biome/overworld:new", "biome-new")));
assertFalse(ServerConfigurator.runtimeRequiresRegistryRestart(
loaded,
Map.of(
"dimension_type/iris:overworld", "dimension-a",
"worldgen/biome/overworld:forest", "biome-a")));
assertTrue(ServerConfigurator.runtimeRequiresRegistryRestart(
loaded,
Map.of("dimension_type/iris:overworld", "dimension-b")));
assertFalse(ServerConfigurator.runtimeRequiresRegistryRestart(loaded, Map.of()));
}
@Test
@@ -0,0 +1,106 @@
package art.arcane.iris.core;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.INMSBinding;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import org.bukkit.Bukkit;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class ServerConfiguratorRegistryVerificationTest {
@Test
public void freshlyCompiledRegistryMissReportsOneRestartWarning() {
VerificationFixture fixture = missingRegistryFixture();
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(fixture.binding());
bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of());
assertTrue(ServerConfigurator.verifyDataPacksPost(Stream.of(fixture.data())));
logging.verify(() -> IrisLogging.debug("Checking Pack: packs/overworld"));
logging.verify(() -> IrisLogging.warn(ServerConfigurator.POST_COMPILE_RESTART_WARNING));
logging.verifyNoMoreInteractions();
}
}
@Test
public void worldCreationRegistryMissRetainsFullOperationalError() {
VerificationFixture fixture = missingRegistryFixture();
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(fixture.binding());
assertFalse(ServerConfigurator.verifyDataPackInstalled(fixture.dimension()));
logging.verify(() -> IrisLogging.warn(
"The Biome overworld:missing is not registered on the server."));
logging.verify(() -> IrisLogging.warn(
"The Dimension Type for packs/overworld/dimensions/overworld.json is not registered on the server."));
logging.verify(() -> IrisLogging.error(
"The Pack overworld is INCAPABLE of generating custom biomes"));
logging.verify(() -> IrisLogging.error(
"If not done automatically, restart your server before generating with this pack!"));
logging.verifyNoMoreInteractions();
}
}
@SuppressWarnings("unchecked")
private static VerificationFixture missingRegistryFixture() {
IrisBiomeCustom customBiome = mock(IrisBiomeCustom.class);
when(customBiome.getId()).thenReturn("missing");
IrisBiome biome = mock(IrisBiome.class);
when(biome.isCustom()).thenReturn(true);
when(biome.getCustomDerivitives()).thenReturn(new KList<>(customBiome));
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(new File("packs/overworld"));
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllBiomes(any())).thenReturn(new KList<>(biome));
when(dimension.getLoadKey()).thenReturn("overworld");
when(dimension.getLoadFile()).thenReturn(
new File("packs/overworld/dimensions/overworld.json"));
when(dimension.getDimensionTypeKey()).thenReturn("iris:overworld");
when(dimension.getLoader()).thenReturn(data);
ResourceLoader<IrisDimension> loader = mock(ResourceLoader.class);
when(loader.getPossibleKeys()).thenReturn(new String[]{"overworld"});
when(loader.loadAll(any(String[].class))).thenReturn(new KList<>(dimension));
when(data.getDimensionLoader()).thenReturn(loader);
INMSBinding binding = mock(INMSBinding.class);
when(binding.supportsDataPacks()).thenReturn(true);
when(binding.getCustomBiomeBaseFor("overworld:missing")).thenReturn(null);
when(binding.missingDimensionTypes("iris:overworld")).thenReturn(true);
return new VerificationFixture(data, dimension, binding);
}
private record VerificationFixture(
IrisData data,
IrisDimension dimension,
INMSBinding binding
) {
}
}
@@ -11,6 +11,7 @@ import art.arcane.volmlib.util.localization.MessageValue;
import art.arcane.volmlib.util.localization.PluralValue;
import art.arcane.volmlib.util.localization.TextValue;
import art.arcane.volmlib.util.localization.VolmitLocales;
import com.google.gson.JsonArray;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -174,6 +175,29 @@ public class IrisLanguageTest {
}
}
@Test
public void compactBukkitLocaleUsesSortedCatalogPositionsAndEnglishFallbacks() {
List<String> messageIds = IrisLanguage.catalog().ids().stream()
.filter(id -> !id.startsWith("iris.modded."))
.sorted()
.toList();
String translatedId = IrisMessages.COMMAND_UNKNOWN.id();
JsonArray values = new JsonArray(messageIds.size());
for (int i = 0; i < messageIds.size(); i++) {
values.add(messageIds.get(i).equals(translatedId) ? "Unbekannter Iris-Befehl" : null);
}
JsonArray compact = new JsonArray(2);
compact.add("de_DE");
compact.add(values);
LocaleOverlay overlay = IrisLanguage.parseOverlay("test", "de_DE", compact.toString());
assertEquals(Set.of(translatedId), overlay.values().keySet());
assertEquals(
"Unbekannter Iris-Befehl",
((TextValue) overlay.value(translatedId)).template());
}
@Test
public void bundledServerResourcesExactlyMatchNonEnglishManifest() throws Exception {
Set<String> expected = VolmitLocales.nonEnglish().stream()
@@ -651,6 +651,35 @@ public class PackDownloaderTest {
assertEquals(0, PackDownloader.downloadLockCount());
}
@Test
public void invalidBuiltInRiverSchemaPreservesExistingTarget() throws Exception {
File packsFolder = temp.newFolder("invalid-river-packs");
File target = writePack(packsFolder.toPath().resolve("overworld"), "overworld", "old");
File extracted = writePack(temp.newFolder("invalid-river-source").toPath(), "overworld", "new");
Files.writeString(
extracted.toPath().resolve("dimensions/overworld.json"),
"{\"name\":\"Overworld\",\"regions\":[\"local\"],\"logicalHeight\":256,"
+ "\"dimensionHeight\":{\"min\":-64,\"max\":320},\"rivers\":{\"enabled\":true,"
+ "\"terrain\":{},\"water\":{\"mode\":\"SEA_LEVEL\"}}}",
StandardCharsets.UTF_8
);
PackDownloader.PackInstallResult result = PackDownloader.installExtractedPack(
packsFolder,
extracted,
true,
"overworld",
ignored -> {
}
);
assertNull(result);
assertEquals("old", Files.readString(target.toPath().resolve("state.txt"), StandardCharsets.UTF_8));
assertTrue(Files.isRegularFile(target.toPath().resolve("regions/local.json")));
assertTransactionStateClean(packsFolder);
assertEquals(0, PackDownloader.downloadLockCount());
}
@Test
public void forceOverwriteReplacesEnginelessLoadedPackData() throws Exception {
// A registered loader with no engines is a stale catalog registration (startup
@@ -127,8 +127,9 @@ public class PackRiverValidatorTest {
{
"id": "trunk",
"seed": 17,
"bodyWavelength": 31,
"bodyWavelength": 7,
"bodyDetailWavelength": 16385,
"bodyDetailInfluence": 1.1,
"widthVariation": -0.1,
"bankVariation": 0.876,
"depthVariation": -0.1,
@@ -150,8 +151,9 @@ public class PackRiverValidatorTest {
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].bodyWavelength must be at least 8");
assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailWavelength must be at most 16384");
assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailInfluence must be at most 1");
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");
@@ -357,6 +359,140 @@ public class PackRiverValidatorTest {
assertContains(result.errors(), "rivers.water.dropHeight must not exceed maximumPoolRise");
}
@Test
public void acceptsIndependentCaveLavaRiverHeightAndPalette() throws Exception {
File pack = pack("""
{
"regions": ["region"],
"dimensionHeight": {"min": -64, "max": 320},
"fluidHeight": 63,
"rivers": {
"enabled": true,
"terrain": {"worms": [{"id": "river"}]},
"water": {
"mode": "FIXED",
"fluidHeight": -48,
"fluidPalette": {
"palette": [{"block": "minecraft:lava"}]
}
}
}
}
""");
PackRiverValidator.Validation result = validate(pack);
assertTrue(result.errors().toString(), result.errors().isEmpty());
}
@Test
public void acceptsSparseBlobbyDeepLavaPoolsWithIndependentHeight() throws Exception {
File pack = pack("""
{
"regions": ["region"],
"dimensionHeight": {"min": -256, "max": 512},
"rivers": {
"enabled": true,
"terrain": {"worms": [{"id": "river"}]},
"caves": {
"deepPools": {
"enabled": true,
"reach": {
"chance": 0.08,
"influence": 0.04,
"style": {"style": "IRIS", "zoom": 4096}
},
"minimumSpacing": 768,
"maximumPerReach": 1,
"minimumFluidY": -224,
"maximumFluidY": -108,
"searchRadius": 20,
"searchAttempts": 12,
"horizontalRadius": 24,
"verticalRadius": 10,
"dryHeadroom": 5,
"shapeStyle": {"style": "IRIS", "zoom": 12},
"shapeVariation": 0.6,
"warpStyle": {"style": "IRIS", "zoom": 24},
"warpStrength": 8,
"maximumVolume": 65536,
"fluidPalette": {
"palette": [{"block": "minecraft:lava"}]
}
}
}
}
}
""");
PackRiverValidator.Validation result = validate(pack);
assertTrue(result.errors().toString(), result.errors().isEmpty());
}
@Test
public void rejectsUnsafeDeepPoolEnvelopeShapeAndPalette() throws Exception {
File pack = pack("""
{
"regions": ["region"],
"dimensionHeight": {"min": -64, "max": 320},
"rivers": {
"enabled": true,
"terrain": {"worms": [{"id": "river"}]},
"caves": {
"deepPools": {
"enabled": true,
"reach": {"chance": 0},
"minimumFluidY": -90,
"maximumFluidY": -120,
"searchRadius": 120,
"horizontalRadius": 20,
"verticalRadius": 8,
"dryHeadroom": 8,
"maximumVolume": 64,
"fluidPalette": {"palette": []}
}
}
}
}
""");
PackRiverValidator.Validation result = validate(pack);
assertContains(result.errors(), "deepPools.minimumFluidY must not exceed maximumFluidY");
assertContains(result.errors(), "deepPools.dryHeadroom must be smaller than verticalRadius");
assertContains(result.errors(), "deepPools.searchRadius plus horizontalRadius must not exceed 128");
assertContains(result.errors(), "deepPools.maximumVolume must be at least");
assertContains(result.errors(), "deepPools.fluidPalette.palette must contain at least one fluid block");
assertContains(result.errors(), "deepPools fluid range and chamber envelope must remain inside dimensionHeight");
assertContains(result.warnings(), "deepPools is enabled but its reach gate cannot accept any pools");
}
@Test
public void rejectsRetiredWaterModeInvalidPaletteAndOutOfBoundsHeight() throws Exception {
File pack = pack("""
{
"regions": ["region"],
"dimensionHeight": {"min": -64, "max": 320},
"rivers": {
"enabled": true,
"terrain": {"worms": [{"id": "river"}]},
"water": {
"mode": "SEA_LEVEL",
"fluidHeight": -80,
"fluidPalette": {"palette": []}
}
}
}
""");
PackRiverValidator.Validation result = validate(pack);
assertContains(result.errors(), "rivers.water.mode must be one of");
assertContains(result.errors(), "rivers.water.fluidHeight must remain inside dimensionHeight");
assertContains(result.errors(), "rivers.water.fluidPalette.palette must contain at least one fluid block");
}
@Test
public void rejectsPathologicalCombinedTopologyComplexity() throws Exception {
File pack = pack("""
@@ -3,11 +3,17 @@ package art.arcane.iris.core.pregenerator.methods;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class AsyncPregenMethodConcurrencyCapTest {
@Test
@@ -77,4 +83,31 @@ public class AsyncPregenMethodConcurrencyCapTest {
assertEquals(0, slowRequests.get());
assertEquals("generated", request.join());
}
@Test
public void closeDrainWaitsPastWarningIntervalsUntilEveryPermitReturns() throws Exception {
Semaphore semaphore = new Semaphore(0);
AtomicInteger warnings = new AtomicInteger();
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<Boolean> drain = executor.submit(() -> AsyncPregenMethod.awaitDrain(
semaphore,
2,
10L,
TimeUnit.MILLISECONDS,
warnings::incrementAndGet
));
while (warnings.get() == 0) {
Thread.onSpinWait();
}
semaphore.release(2);
assertFalse(drain.get(1L, TimeUnit.SECONDS));
assertTrue(warnings.get() > 0);
assertEquals(0, semaphore.availablePermits());
} finally {
executor.shutdownNow();
}
}
}
@@ -51,6 +51,14 @@ public class IrisImageMapSchemaTest {
properties.getJSONObject("curveExponent").getDouble("minimum"), 0D);
assertEquals(IrisImageMap.MAXIMUM_COLOR_TOLERANCE,
properties.getJSONObject("colorTolerance").getDouble("maximum"), 0D);
JSONObject origin = properties.getJSONObject("origin");
JSONObject originProperties = mapSchema.getJSONObject("definitions")
.getJSONObject(origin.getString("$ref").substring("#/definitions/".length()))
.getJSONObject("properties");
assertTrue(originProperties.getJSONObject("x").getString("description")
.contains("X coordinate in world blocks for origin, or source pixels for sourceOrigin"));
assertTrue(originProperties.getJSONObject("z").getString("description")
.contains("Z coordinate in world blocks for origin, or source image Y pixels for sourceOrigin"));
}
@SuppressWarnings("unchecked")
@@ -4,6 +4,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisExpression;
import art.arcane.iris.engine.object.IrisRiverNetwork;
import art.arcane.iris.engine.object.IrisRiverOverride;
@@ -11,6 +12,11 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
@@ -23,6 +29,29 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisRiverSchemaTest {
private IrisPlatform previousPlatform;
@Before
public void bindPlatform() {
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
if (previousPlatform != null) {
IrisPlatforms.unbind();
}
IrisPlatform platform = mock(IrisPlatform.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(platform.registries()).thenReturn(registries);
when(registries.blockTypeKeys()).thenReturn(List.of());
IrisPlatforms.bind(platform);
}
@After
public void restorePlatform() {
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
}
@Test
public void riverNetworkSchemaExposesNestedNoiseLimitsModesAndBiomePools() {
JSONObject schema = new SchemaBuilder(IrisRiverNetwork.class, schemaData()).construct();
@@ -36,6 +65,8 @@ public class IrisRiverSchemaTest {
JSONObject worm = referencedProperties(definitions, worms.getJSONObject("items"));
JSONObject water = referencedProperties(definitions, properties.getJSONObject("water"));
JSONObject biomes = referencedProperties(definitions, properties.getJSONObject("biomes"));
JSONObject caves = referencedProperties(definitions, properties.getJSONObject("caves"));
JSONObject deepPools = referencedProperties(definitions, caves.getJSONObject("deepPools"));
assertEquals("boolean", properties.getJSONObject("enabled").getString("type"));
assertEquals(64, topology.getJSONObject("cellSize").getInt("minimum"));
@@ -60,8 +91,9 @@ public class IrisRiverSchemaTest {
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(8D, worm.getJSONObject("bodyWavelength").getDouble("minimum"), 0D);
assertEquals(16384D, worm.getJSONObject("bodyDetailWavelength").getDouble("maximum"), 0D);
assertEquals(1D, worm.getJSONObject("bodyDetailInfluence").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);
@@ -72,12 +104,27 @@ public class IrisRiverSchemaTest {
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("FIXED", "TERRACED"), enumValues(definitions, water.getJSONObject("mode")));
assertEquals(-2048, water.getJSONObject("fluidHeight").getInt("minimum"));
assertEquals(2048, water.getJSONObject("fluidHeight").getInt("maximum"));
assertTrue(water.has("fluidPalette"));
assertEquals(64D, terrain.getJSONObject("channelRadiusBonus").getDouble("maximum"), 0D);
assertEquals("array", biomes.getJSONObject("channel").getString("type"));
assertEquals("#/definitions/erzbiomes",
biomes.getJSONObject("channel").getJSONObject("items").getString("$ref"));
assertTrue(properties.has("terrain"));
assertTrue(properties.has("caves"));
assertEquals("boolean", deepPools.getJSONObject("enabled").getString("type"));
assertEquals(-2048, deepPools.getJSONObject("minimumFluidY").getInt("minimum"));
assertEquals(2048, deepPools.getJSONObject("maximumFluidY").getInt("maximum"));
assertEquals(128, deepPools.getJSONObject("horizontalRadius").getInt("maximum"));
assertEquals(64, deepPools.getJSONObject("verticalRadius").getInt("maximum"));
assertEquals(0.75D, deepPools.getJSONObject("shapeVariation").getDouble("maximum"), 0D);
assertEquals(64D, deepPools.getJSONObject("warpStrength").getDouble("maximum"), 0D);
assertTrue(deepPools.has("reach"));
assertTrue(deepPools.has("shapeStyle"));
assertTrue(deepPools.has("warpStyle"));
assertTrue(deepPools.has("fluidPalette"));
}
@Test
@@ -98,14 +145,17 @@ public class IrisRiverSchemaTest {
IrisData data = mock(IrisData.class);
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
ResourceLoader<IrisExpression> expressionLoader = mock(ResourceLoader.class);
ResourceLoader<IrisBlockData> blockLoader = mock(ResourceLoader.class);
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
loaders.put(IrisBiome.class, biomeLoader);
loaders.put(IrisExpression.class, expressionLoader);
when(data.getBlockLoader()).thenReturn(blockLoader);
when(data.getLoaders()).thenReturn(loaders);
when(data.getPossibleSnippets(anyString())).thenReturn(new KList<>());
when(biomeLoader.getPossibleKeys()).thenReturn(new String[]{"river/channel"});
when(biomeLoader.getFolderName()).thenReturn("biomes");
when(biomeLoader.getResourceTypeName()).thenReturn("Biome");
when(blockLoader.getPossibleKeys()).thenReturn(new String[0]);
when(expressionLoader.getPossibleKeys()).thenReturn(new String[0]);
when(expressionLoader.getFolderName()).thenReturn("expressions");
when(expressionLoader.getResourceTypeName()).thenReturn("Expression");
@@ -69,10 +69,26 @@ public class StudioOpenCoordinatorOpenKindTest {
null);
assertEquals(StudioOpenCoordinator.StudioOpenKind.JIGSAW, request.openKind());
assertTrue(request.requestedAtNanos() > 0L);
}
@Test
public void activeStudioTeleportIsSerializedAndBoundedBeforeNativeDelegation() throws Exception {
public void nativeTeleportWaitsForCompletionWithoutAnArrivalDeadline() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java"))
.replace("\r\n", "\n");
int methodStart = source.indexOf("private void executeOpen(");
int delegation = source.indexOf("WorldRuntimeControlService.get().teleportInMode(", methodStart);
int completion = source.indexOf("nativeTeleportFuture.get();", delegation);
assertTrue(delegation >= 0);
assertTrue(completion > delegation);
assertFalse(source.substring(methodStart, completion).contains("orTimeout("));
assertFalse(source.substring(methodStart, completion).contains("deadlineNanos"));
}
@Test
public void activeStudioTeleportIsSerializedWithoutAnArrivalDeadline() throws Exception {
String coordinator = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java"))
.replace("\r\n", "\n");
@@ -89,28 +105,22 @@ public class StudioOpenCoordinatorOpenKindTest {
String serviceMethod = service.substring(serviceStart, serviceEnd);
int transitionAdmission = serviceMethod.indexOf("studioTransitions.submit(() ->");
int projectCapture = serviceMethod.indexOf("IrisProject project = activeProject");
int publicDeadline = serviceMethod.indexOf(
"transition.orTimeout(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS");
int admissionClose = serviceMethod.indexOf(
"transition.whenComplete((ignored, failure) -> admission.set(false))");
int nativeClaim = coordinatorMethod.indexOf(
"activeAdmission.compareAndSet(true, false)");
int nativeDelegation = coordinatorMethod.indexOf(
"WorldRuntimeControlService.get().teleportInMode(player, entry, GameMode.SPECTATOR)");
assertTrue(transitionAdmission >= 0);
assertTrue(projectCapture > transitionAdmission);
assertTrue(publicDeadline > projectCapture);
assertTrue(admissionClose > publicDeadline);
assertTrue(serviceMethod.contains("deadlineNanos"));
assertFalse(serviceMethod.contains("orTimeout("));
assertFalse(serviceMethod.contains("deadlineNanos"));
assertTrue(coordinatorMethod.contains(
"WorldRuntimeControlService.get().resolveEntryAnchor(world, provider)"));
assertTrue(coordinatorMethod.contains(
"project.getActiveOpenKind() == StudioOpenKind.STANDARD"));
assertFalse(coordinatorMethod.contains("requestChunkAsync("));
assertFalse(coordinatorMethod.contains("getHighestBlockYAt("));
assertTrue(nativeClaim >= 0);
assertTrue(nativeDelegation > nativeClaim);
assertTrue(nativeDelegation >= 0);
assertFalse(coordinatorMethod.contains("orTimeout("));
assertFalse(coordinatorMethod.contains("deadlineNanos"));
}
@Test
@@ -161,6 +171,7 @@ public class StudioOpenCoordinatorOpenKindTest {
"prepare_generator",
"resolve_entry_anchor",
"prepare_structure_rings",
"prepare_generation_caches",
"teleport_standard_entry",
"finalize_open")) {
int current = source.indexOf("\"" + phase + "\"", previous + 1);
@@ -174,11 +185,13 @@ public class StudioOpenCoordinatorOpenKindTest {
public void structureStateCompletesBeforeImmediateNativeTeleport() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int completionCall = source.indexOf("endStudioEntryBootstrap(world, provider)");
int teleport = source.indexOf(
"WorldRuntimeControlService.get().teleportInMode(", completionCall);
int executeOpen = source.indexOf("private void executeOpen(");
int completionCall = source.indexOf("endStudioEntryBootstrap(entryWorld, entryProvider)", executeOpen);
int completionAwait = source.indexOf("entryBootstrap.get(", completionCall);
int cacheAwait = source.indexOf("irisEngine.awaitGenerationCacheWarm()", completionAwait);
int teleportStart = source.indexOf("WorldRuntimeControlService.get().teleportInMode(", cacheAwait);
int finalizeOpen = source.indexOf(
"updateStage(request, \"finalize_open\", 1.00D)", teleport);
"updateStage(request, \"finalize_open\", 1.00D)", teleportStart);
int futureComplete = source.indexOf(
"future.complete(new StudioOpenResult(world, entryLocation))", finalizeOpen);
int methodStart = source.indexOf(
@@ -191,14 +204,15 @@ public class StudioOpenCoordinatorOpenKindTest {
int gateRelease = method.indexOf("bukkitGenerator::endStudioEntryBootstrap");
assertTrue(completionCall >= 0);
assertTrue(teleport > completionCall);
assertFalse(source.contains("preparedEntryChunks"));
assertTrue(finalizeOpen > teleport);
assertTrue(completionAwait > completionCall);
assertTrue(cacheAwait > completionAwait);
assertTrue(teleportStart > cacheAwait);
assertTrue(finalizeOpen > teleportStart);
assertTrue(futureComplete > finalizeOpen);
assertFalse(source.contains("requestChunkAsync("));
assertTrue(scheduled >= 0);
assertTrue(claim > scheduled);
assertTrue(activation > claim);
assertTrue(gateRelease > activation);
int ringCompletion = method.indexOf("thenCompose(nativeActivation -> nativeActivation)");
assertTrue(ringCompletion > activation);
assertTrue(gateRelease > ringCompletion);
@@ -77,6 +77,27 @@ public class WorldRuntimeControlServiceSafeEntryTest {
assertEquals(63, result.getBlockY());
}
@Test
public void resolvesSafeCavityBelowDimensionRoof() {
World world = loadedWorld(0, 0);
Block netherrack = block(Material.NETHERRACK, false, false, FULL_BLOCK);
Block air = block(Material.AIR, false, true);
doReturn(300).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
doAnswer(invocation -> {
int y = invocation.getArgument(1);
if (y == 201 || y == 202) {
return air;
}
return netherrack;
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
Location source = new Location(world, 0.5D, 201D, 0.5D);
Location result = WorldRuntimeControlService.findTopSafeLocation(world, source);
assertNotNull(result);
assertEquals(201, result.getBlockY());
}
@Test
public void rejectsFluidHazardousAndCollisionBlockedCandidates() {
World world = loadedWorld(0, 0);
@@ -166,7 +187,7 @@ public class WorldRuntimeControlServiceSafeEntryTest {
Location result = WorldRuntimeControlService.findTopSafeLocation(world, source);
assertNull(result);
assertEquals(-1, lowestReadY.get());
assertEquals(-64, lowestReadY.get());
}
@Test
@@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WorldRuntimeControlServiceTeleportTest {
@@ -89,6 +90,30 @@ public class WorldRuntimeControlServiceTeleportTest {
}
}
@Test
public void successfulModeTeleportTemporarilyReducesAndRestoresViewDistance() {
Player player = mock(Player.class);
Location destination = mock(Location.class);
CompletableFuture<Boolean> nativeTeleport = new CompletableFuture<>();
when(player.getGameMode()).thenReturn(GameMode.SURVIVAL);
when(player.getViewDistance()).thenReturn(12);
try (MockedStatic<J> scheduling = immediateEntityScheduling()) {
CompletableFuture<Boolean> result = WorldRuntimeControlService.scheduleTeleport(
player,
destination,
GameMode.SPECTATOR,
(target, location) -> nativeTeleport);
nativeTeleport.complete(true);
assertTrue(result.join());
InOrder viewDistances = inOrder(player);
viewDistances.verify(player).setViewDistance(2);
viewDistances.verify(player).setViewDistance(12);
verify(player).setGameMode(GameMode.SPECTATOR);
}
}
private static MockedStatic<J> immediateEntityScheduling() {
MockedStatic<J> scheduling = mockStatic(J.class);
scheduling.when(() -> J.runEntity(any(Player.class), any(Runnable.class))).thenAnswer(invocation -> {
@@ -0,0 +1,72 @@
package art.arcane.iris.core.tools;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisCreatorInitialSpawnTest {
@Test
public void worldCreationWaitsForInitialSpawnCompletion() throws Exception {
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
CompletableFuture<Void> initialSpawn = new CompletableFuture<>();
CountDownLatch waitStarted = new CountDownLatch(1);
when(generator.getInitialSpawnReady()).thenAnswer(invocation -> {
waitStarted.countDown();
return initialSpawn;
});
CompletableFuture<Void> wait = CompletableFuture.runAsync(() -> {
try {
IrisCreator.awaitInitialSpawnPreparation(generator, "test-world");
} catch (Throwable failure) {
throw new RuntimeException(failure);
}
});
if (!waitStarted.await(5L, TimeUnit.SECONDS)) {
fail("Initial spawn wait did not start.");
}
if (wait.isDone()) {
fail("Initial spawn wait completed before the chunk was ready.");
}
initialSpawn.complete(null);
wait.get(5L, TimeUnit.SECONDS);
}
@Test
public void initialSpawnFailureFailsWorldCreation() throws Exception {
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
IllegalStateException failure = new IllegalStateException("spawn failed");
when(generator.getInitialSpawnReady()).thenReturn(CompletableFuture.failedFuture(failure));
try {
IrisCreator.awaitInitialSpawnPreparation(generator, "test-world");
fail("Expected initial spawn failure.");
} catch (ExecutionException exception) {
assertSame(failure, exception.getCause());
}
}
@Test
public void missingInitialSpawnFutureFailsWorldCreation() {
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
when(generator.getInitialSpawnReady()).thenReturn(null);
NullPointerException failure = assertThrows(
NullPointerException.class,
() -> IrisCreator.awaitInitialSpawnPreparation(generator, "test-world"));
assertEquals("Initial spawn preparation future", failure.getMessage());
}
}
@@ -43,4 +43,52 @@ public class IrisCreatorProgressContractTest {
assertFalse(source.contains("RuntimeProgressMessages.WORLD_CREATE_ACTION"));
assertFalse(source.contains("RuntimeProgressMessages.WORLD_CREATE_CONSOLE"));
}
@Test
public void persistentCreateReadinessPrecedesSuccessRegistrationAndLeaseRelease() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.irisCreatorSource")))
.replace("\r\n", "\n");
int acquireLease = source.indexOf("worldLease = coordinator.acquire(");
int createReserved = source.indexOf(
"createReserved(worldKey, resolvedDimension, creationReporter)",
acquireLease);
int reportSuccess = source.indexOf("creationReporter.succeed()", createReserved);
int releaseLease = source.indexOf("worldLease.close()", reportSuccess);
int createWorld = source.indexOf("INMS.get().createWorldAsync(wc, request)", createReserved);
int persistentGuard = source.indexOf("if (!studio && !benchmark) {", createWorld);
int awaitSpawn = source.indexOf("awaitInitialSpawnPreparation(access, name)", persistentGuard);
int creationDone = source.indexOf("done.set(true)", awaitSpawn);
int registerWorld = source.indexOf("BukkitWorldConfiguration.register(", creationDone);
int returnWorld = source.indexOf("return world;", registerWorld);
assertTrue(acquireLease >= 0);
assertTrue(createReserved > acquireLease);
assertTrue(reportSuccess > createReserved);
assertTrue(releaseLease > reportSuccess);
assertTrue(createWorld > createReserved);
assertTrue(persistentGuard > createWorld);
assertTrue(awaitSpawn > persistentGuard);
assertTrue(creationDone > awaitSpawn);
assertTrue(registerWorld > creationDone);
assertTrue(returnWorld > registerWorld);
}
@Test
public void spawnReadinessFailureReachesWorldCreationRollback() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.irisCreatorSource")))
.replace("\r\n", "\n");
int awaitSpawn = source.indexOf("awaitInitialSpawnPreparation(access, name)");
int createReservedFailure = source.indexOf("} catch (Throwable failure) {", awaitSpawn);
int rollback = source.indexOf(
"rollbackWorldCreation(worldKey, world, stagedGenerator, storageRoot, bukkitRegistered, failure)",
createReservedFailure);
int rethrow = source.indexOf("throw irisException", rollback);
assertTrue(awaitSpawn >= 0);
assertTrue(createReservedFailure > awaitSpawn);
assertTrue(rollback > createReservedFailure);
assertTrue(rethrow > rollback);
}
}
@@ -4,7 +4,6 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverOverride;
import art.arcane.iris.engine.object.IrisRiverWaterMode;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection;
@@ -60,50 +59,19 @@ public class IrisComplexSurfaceBiomeTest {
}
@Test
public void naturalOceanHeightMaskMatchesResolvedLandAndSeaBiomes() {
double fluidHeight = 64D;
IrisBiome land = new IrisBiome().setInferredType(InferredType.LAND);
IrisBiome sea = new IrisBiome().setInferredType(InferredType.SEA);
IrisBiome shore = new IrisBiome().setInferredType(InferredType.SHORE);
IrisRegion region = mock(IrisRegion.class);
for (double shoreHeight : new double[]{0D, 3D}) {
doReturn(shoreHeight).when(region).getShoreHeight(0D, 0D);
double[] heights = new double[]{
fluidHeight - 2D,
fluidHeight - 1D,
fluidHeight,
fluidHeight + shoreHeight,
fluidHeight + shoreHeight + 0.000001D
};
for (IrisBiome base : List.of(land, sea)) {
for (double height : heights) {
IrisBiome resolved = IrisComplex.resolveSurfaceBiome(
height,
base,
region,
0D,
0D,
fluidHeight,
constant(land),
constant(sea),
constant(shore)
);
Boolean ocean = IrisComplex.createNaturalOceanStream(
ProceduralStream.ofDouble((x, z) -> height),
constantType(InferredType.LAND),
null,
fluidHeight,
IrisRiverWaterMode.TERRACED
).get(0D, 0D);
assertEquals(resolved.getInferredType() == InferredType.SEA, ocean.booleanValue());
}
}
}
public void naturalOceanMaskTracksContinentalIntent() {
assertTrue(IrisComplex.createNaturalOceanStream(
constantType(InferredType.SEA),
null
).get(0D, 0D));
assertFalse(IrisComplex.createNaturalOceanStream(
constantType(InferredType.LAND),
null
).get(0D, 0D));
}
@Test
public void naturalOceanHeightMaskSamplesOnlyNaturalHeight() {
public void naturalOceanMaskDoesNotSampleNaturalHeight() {
AtomicInteger heightSamples = new AtomicInteger();
ProceduralStream<Double> height = ProceduralStream.ofDouble((x, z) -> {
heightSamples.incrementAndGet();
@@ -111,15 +79,12 @@ public class IrisComplexSurfaceBiomeTest {
});
Boolean ocean = IrisComplex.createNaturalOceanStream(
height,
constantType(InferredType.LAND),
null,
64D,
IrisRiverWaterMode.TERRACED
null
).get(8D, -3D);
assertTrue(ocean);
assertEquals(1, heightSamples.get());
assertFalse(ocean);
assertEquals(0, heightSamples.get());
}
@Test
@@ -131,31 +96,22 @@ public class IrisComplexSurfaceBiomeTest {
});
assertTrue(IrisComplex.createNaturalOceanStream(
height,
constantType(InferredType.LAND),
new IrisBiome().setInferredType(InferredType.SEA),
64D,
IrisRiverWaterMode.SEA_LEVEL
new IrisBiome().setInferredType(InferredType.SEA)
).get(0D, 0D));
assertFalse(IrisComplex.createNaturalOceanStream(
height,
constantType(InferredType.SEA),
new IrisBiome().setInferredType(InferredType.LAND),
64D,
IrisRiverWaterMode.SEA_LEVEL
new IrisBiome().setInferredType(InferredType.LAND)
).get(0D, 0D));
assertFalse(IrisComplex.createNaturalOceanStream(
height,
constantType(InferredType.SEA),
new IrisBiome().setInferredType(InferredType.SHORE),
64D,
IrisRiverWaterMode.SEA_LEVEL
new IrisBiome().setInferredType(InferredType.SHORE)
).get(0D, 0D));
assertEquals(0, heightSamples.get());
}
@Test
public void seaLevelOceanMaskUsesContinentalIntentWithoutSamplingHeight() {
public void fixedOceanMaskUsesContinentalIntentWithoutSamplingHeight() {
AtomicInteger heightSamples = new AtomicInteger();
ProceduralStream<Double> height = ProceduralStream.ofDouble((x, z) -> {
heightSamples.incrementAndGet();
@@ -163,11 +119,8 @@ public class IrisComplexSurfaceBiomeTest {
});
Boolean ocean = IrisComplex.createNaturalOceanStream(
height,
constantType(InferredType.SEA),
null,
64D,
IrisRiverWaterMode.SEA_LEVEL
null
).get(8D, -3D);
assertTrue(ocean);
@@ -0,0 +1,115 @@
package art.arcane.iris.engine;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WorldTeleportWarmupTest {
@Test
public void cancelledTeleportUsesNativeAsyncPathWithOriginalCause() {
World world = mock(World.class);
Player player = mock(Player.class);
PlayerTeleportEvent event = mock(PlayerTeleportEvent.class);
Location destination = new Location(world, 400.5D, 96D, 2.5D, 45F, 10F);
PlayerTeleportEvent.TeleportCause cause = PlayerTeleportEvent.TeleportCause.COMMAND;
CompletableFuture<Boolean> result = new CompletableFuture<>();
AtomicReference<Location> capturedDestination = new AtomicReference<>();
when(event.getPlayer()).thenReturn(player);
when(event.getTo()).thenReturn(destination);
when(event.getCause()).thenReturn(cause);
try (MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class)) {
platform.when(() -> BukkitPlatform.teleportAsync(same(player), any(Location.class), eq(cause)))
.thenAnswer(invocation -> {
capturedDestination.set(invocation.getArgument(1, Location.class));
return result;
});
new WorldTeleportWarmup().teleportAsync(event);
verify(event).setCancelled(true);
assertEquals(destination, capturedDestination.get());
assertNotSame(destination, capturedDestination.get());
}
}
@Test
public void missingDestinationLeavesTeleportUntouched() {
PlayerTeleportEvent event = mock(PlayerTeleportEvent.class);
when(event.getTo()).thenReturn(null);
try (MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class)) {
new WorldTeleportWarmup().teleportAsync(event);
verify(event, never()).setCancelled(true);
platform.verifyNoInteractions();
}
}
@Test
public void falseNativeSettlementDoesNotThrowFromCompletion() {
World world = mock(World.class);
Player player = mock(Player.class);
PlayerTeleportEvent event = mock(PlayerTeleportEvent.class);
Location destination = new Location(world, 0.5D, 80D, 0.5D);
when(event.getPlayer()).thenReturn(player);
when(event.getTo()).thenReturn(destination);
when(event.getCause()).thenReturn(PlayerTeleportEvent.TeleportCause.COMMAND);
when(player.getName()).thenReturn("Player");
try (MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class)) {
platform.when(() -> BukkitPlatform.teleportAsync(
same(player),
any(Location.class),
eq(PlayerTeleportEvent.TeleportCause.COMMAND)))
.thenReturn(CompletableFuture.completedFuture(false));
new WorldTeleportWarmup().teleportAsync(event);
verify(event).setCancelled(true);
}
}
@Test
public void missingNativeFutureDoesNotThrowAfterCancellation() {
World world = mock(World.class);
Player player = mock(Player.class);
PlayerTeleportEvent event = mock(PlayerTeleportEvent.class);
Location destination = new Location(world, 0.5D, 80D, 0.5D);
when(event.getPlayer()).thenReturn(player);
when(event.getTo()).thenReturn(destination);
when(event.getCause()).thenReturn(PlayerTeleportEvent.TeleportCause.COMMAND);
when(player.getName()).thenReturn("Player");
try (MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class)) {
platform.when(() -> BukkitPlatform.teleportAsync(
same(player),
any(Location.class),
eq(PlayerTeleportEvent.TeleportCause.COMMAND)))
.thenReturn(null);
new WorldTeleportWarmup().teleportAsync(event);
verify(event).setCancelled(true);
}
}
}
@@ -0,0 +1,185 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class EngineAssignedWorldManagerTeleportTest {
@Test
public void unloadedIrisDestinationDelegatesToAsyncTeleport() {
TeleportHarness harness = new TeleportHarness(false, true);
try (harness) {
harness.manager.on(harness.event);
assertSame(harness.event, harness.manager.teleportEvent);
}
}
@Test
public void loadedIrisDestinationContinuesWithoutInterception() {
TeleportHarness harness = new TeleportHarness(true, true);
try (harness) {
harness.manager.on(harness.event);
assertNull(harness.manager.teleportEvent);
}
}
@Test
public void classicBukkitDestinationContinuesWithoutInterception() {
TeleportHarness harness = new TeleportHarness(false, false);
try (harness) {
harness.manager.on(harness.event);
assertNull(harness.manager.teleportEvent);
}
}
@Test
public void pluginTeleportContinuesWithoutInterception() {
TeleportHarness harness = new TeleportHarness(false, true);
when(harness.event.getCause()).thenReturn(PlayerTeleportEvent.TeleportCause.PLUGIN);
try (harness) {
harness.manager.on(harness.event);
assertNull(harness.manager.teleportEvent);
}
}
@Test
public void productionWorldCommandContinuesWithoutInterception() {
TeleportHarness harness = new TeleportHarness(false, true);
when(harness.engine.isStudio()).thenReturn(false);
try (harness) {
harness.manager.on(harness.event);
assertNull(harness.manager.teleportEvent);
}
}
@Test
public void otherWorldDestinationContinuesWithoutInterception() {
TeleportHarness harness = new TeleportHarness(false, true);
World otherWorld = mock(World.class);
when(harness.event.getTo()).thenReturn(new Location(otherWorld, 400.5D, 96D, 2.5D));
try (harness) {
harness.manager.on(harness.event);
assertNull(harness.manager.teleportEvent);
}
}
private static final class TeleportHarness implements AutoCloseable {
private final Engine engine;
private final TestWorldManager manager;
private final PlayerTeleportEvent event;
private final MockedStatic<BukkitPlatform> platform;
private final MockedStatic<BukkitWorldBinding> binding;
private TeleportHarness(boolean loaded, boolean paper) {
engine = mock(Engine.class);
EngineTarget target = mock(EngineTarget.class);
IrisWorld irisWorld = mock(IrisWorld.class);
World world = mock(World.class);
event = mock(PlayerTeleportEvent.class);
Location destination = new Location(world, 400.5D, 96D, 2.5D);
when(engine.getTarget()).thenReturn(target);
when(engine.isStudio()).thenReturn(true);
when(target.getWorld()).thenReturn(irisWorld);
when(event.getTo()).thenReturn(destination);
when(event.getCause()).thenReturn(PlayerTeleportEvent.TeleportCause.COMMAND);
when(world.isChunkLoaded(25, 0)).thenReturn(loaded);
platform = mockStatic(BukkitPlatform.class);
platform.when(BukkitPlatform::isPaperServer).thenReturn(paper);
binding = mockStatic(BukkitWorldBinding.class);
binding.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
manager = new TestWorldManager(engine);
}
@Override
public void close() {
binding.close();
platform.close();
}
}
private static final class TestWorldManager extends EngineAssignedWorldManager {
private PlayerTeleportEvent teleportEvent;
private TestWorldManager(Engine engine) {
super(engine);
}
@Override
protected boolean runManagerTask(String operation, Runnable task) {
task.run();
return true;
}
@Override
public int getEntityCount() {
return 0;
}
@Override
public int getChunkCount() {
return 0;
}
@Override
public double getEntitySaturation() {
return 0D;
}
@Override
public void onTick() {
}
@Override
public void onSave() {
}
@Override
public void onBlockBreak(BlockBreakEvent event) {
}
@Override
public void onBlockPlace(BlockPlaceEvent event) {
}
@Override
public void onChunkLoad(Chunk chunk, boolean generated) {
}
@Override
public void onChunkUnload(Chunk chunk) {
}
@Override
public void teleportAsync(PlayerTeleportEvent event) {
teleportEvent = event;
}
}
}
@@ -188,7 +188,7 @@ public class StructureCaveAnchorResolverTest {
assertFalse(StructureCaveAnchorResolver.acceptsAnchorFluid(
true, fluid, RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE), 20, 8));
assertFalse(StructureCaveAnchorResolver.acceptsAnchorFluid(
true, fluid, RiverCaveHydrology.of(RiverCaveAction.FALLING_WATER), 20, 8));
true, fluid, RiverCaveHydrology.of(RiverCaveAction.FALLING_FLUID), 20, 8));
assertFalse(StructureCaveAnchorResolver.acceptsAnchorFluid(
true, null, RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD), 20, 8));
assertTrue(StructureCaveAnchorResolver.acceptsAnchorFluid(
@@ -0,0 +1,20 @@
package art.arcane.iris.engine.mantle;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MantleWriterParallelismTest {
@Test
public void multicorePrefetchKeepsOneWorkerOnSingleProcessorSystems() {
assertEquals(1, MantleWriter.resolvePrefetchParallelism(false, true, 1));
assertEquals(1, MantleWriter.resolvePrefetchParallelism(false, true, 0));
}
@Test
public void maintenanceAndSequentialPrefetchKeepTheirExistingLimits() {
assertEquals(1, MantleWriter.resolvePrefetchParallelism(true, true, 32));
assertEquals(4, MantleWriter.resolvePrefetchParallelism(false, false, 1));
assertEquals(8, MantleWriter.resolvePrefetchParallelism(false, true, 16));
}
}
@@ -54,16 +54,124 @@ public class MatterGeneratorCarvePassRadiusTest {
assertEquals(objects.visited, carving.visited);
}
@Test
@SuppressWarnings("unchecked")
public void chunkSpecificInputRadiusAvoidsUnusedPrerequisiteHalo() {
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.isUseMantle()).thenReturn(true);
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
when(chunk.use()).thenReturn(chunk);
doAnswer(invocation -> {
Runnable task = invocation.getArgument(1);
task.run();
return null;
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
RecordingComponent carving = new RecordingComponent(ReservedFlag.CARVED, 0, 1);
RecordingComponent conditional = new RecordingComponent(
ReservedFlag.RIVER_HYDROLOGY,
1,
0,
160,
0
);
TestMatterGenerator generator = new TestMatterGenerator(engine, mantle, List.of(
new MantlePass(List.of(carving), 11, 160),
new MantlePass(List.of(conditional), 0, 0)
));
generator.generateMatter(0, 0, false, mock(ChunkContext.class));
assertEquals(9, carving.visited.size());
assertEquals(Set.of("0,0"), conditional.visited);
}
@Test
@SuppressWarnings("unchecked")
public void lazyInputGenerationKeepsReadAccessWithoutEagerlyGeneratingTheHalo() {
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.isUseMantle()).thenReturn(true);
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
when(chunk.use()).thenReturn(chunk);
doAnswer(invocation -> {
Runnable task = invocation.getArgument(1);
task.run();
return null;
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
RecordingComponent carving = new RecordingComponent(ReservedFlag.CARVED, 0, 0);
RecordingComponent conditional = new RecordingComponent(
ReservedFlag.RIVER_HYDROLOGY,
1,
0,
0,
160,
true,
10
);
TestMatterGenerator generator = new TestMatterGenerator(engine, mantle, List.of(
new MantlePass(List.of(carving), 10, 160),
new MantlePass(List.of(conditional), 0, 0)
));
generator.generateMatter(0, 0, false, mock(ChunkContext.class));
assertEquals(Set.of("0,0"), carving.visited);
assertEquals(Set.of("0,0"), conditional.visited);
assertTrue(conditional.accessSucceeded);
}
private static final class RecordingComponent implements MantleComponent {
private final MantleFlag flag;
private final int priority;
private final int radius;
private final int inputRadius;
private final int chunkInputRadius;
private final boolean lazyInputGeneration;
private final int accessProbeOffset;
private final Set<String> visited = new LinkedHashSet<>();
private boolean accessSucceeded;
private RecordingComponent(MantleFlag flag, int priority, int radius) {
this(flag, priority, radius, 0, 0, false, 0);
}
private RecordingComponent(
MantleFlag flag,
int priority,
int radius,
int inputRadius,
int chunkInputRadius
) {
this(flag, priority, radius, inputRadius, chunkInputRadius, false, 0);
}
private RecordingComponent(
MantleFlag flag,
int priority,
int radius,
int inputRadius,
int chunkInputRadius,
boolean lazyInputGeneration,
int accessProbeOffset
) {
this.flag = flag;
this.priority = priority;
this.radius = radius;
this.inputRadius = inputRadius;
this.chunkInputRadius = chunkInputRadius;
this.lazyInputGeneration = lazyInputGeneration;
this.accessProbeOffset = accessProbeOffset;
}
@Override
@@ -76,6 +184,26 @@ public class MatterGeneratorCarvePassRadiusTest {
return radius;
}
@Override
public int getInputRadius() {
return inputRadius;
}
@Override
public int getInputRadius(
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius,
ChunkContext context
) {
return chunkInputRadius;
}
@Override
public boolean isInputGenerationLazy() {
return lazyInputGeneration;
}
@Override
public EngineMantle getEngineMantle() {
return null;
@@ -102,6 +230,9 @@ public class MatterGeneratorCarvePassRadiusTest {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
visited.add(x + "," + z);
if (accessProbeOffset != 0) {
accessSucceeded = writer.acquireChunk(x + accessProbeOffset, z) != null;
}
}
}
@@ -0,0 +1,287 @@
package art.arcane.iris.engine.mantle;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MatterGeneratorConcurrencyTest {
private static IrisSettings previousSettings;
@BeforeClass
public static void bindPlatform() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisPlatforms.unbind();
PlatformBlockState defaultBlock = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
IrisPlatform platform = mock(IrisPlatform.class);
when(registries.block(anyString())).thenReturn(defaultBlock);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
IrisSettings.settings = previousSettings;
}
@Test
public void multicoreChunksOverlapAndCompleteBeforeTheNextPass() {
GeneratorFixture fixture = new GeneratorFixture();
CountDownLatch overlap = new CountDownLatch(2);
AtomicInteger completed = new AtomicInteger();
AtomicBoolean barrierObserved = new AtomicBoolean();
RecordingComponent concurrent = new RecordingComponent(ReservedFlag.OBJECT, 0, 16) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
overlap.countDown();
await(overlap);
completed.incrementAndGet();
}
};
RecordingComponent barrier = new RecordingComponent(ReservedFlag.JIGSAW, 1, 0) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
assertEquals(9, completed.get());
barrierObserved.set(true);
}
};
TestMatterGenerator generator = fixture.generator(List.of(
new MantlePass(List.of(concurrent), 1, 0),
new MantlePass(List.of(barrier), 0, 0)
));
generator.generateMatter(0, 0, true, fixture.context);
assertEquals(0, overlap.getCount());
assertEquals(9, completed.get());
assertTrue(barrierObserved.get());
}
@Test
public void multicoreComponentRunsOnTheDispatcher() {
GeneratorFixture fixture = new GeneratorFixture();
Thread caller = Thread.currentThread();
Set<Thread> threads = ConcurrentHashMap.newKeySet();
RecordingComponent ordinary = new RecordingComponent(ReservedFlag.OBJECT, 0, 16) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
threads.add(Thread.currentThread());
}
};
TestMatterGenerator generator = fixture.generator(List.of(
new MantlePass(List.of(ordinary), 1, 0)
));
generator.generateMatter(0, 0, true, fixture.context);
assertFalse(threads.isEmpty());
assertFalse(threads.contains(caller));
}
@Test
public void asyncComponentReceivesCallerContextAndCallerScopeIsRestored() {
GeneratorFixture fixture = new GeneratorFixture();
AtomicReference<IrisContext> observed = new AtomicReference<>();
AtomicReference<Thread> observedThread = new AtomicReference<>();
RecordingComponent concurrent = new RecordingComponent(ReservedFlag.CARVED, 0, 0) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
observed.set(IrisContext.require());
observedThread.set(Thread.currentThread());
}
};
TestMatterGenerator generator = fixture.generator(List.of(
new MantlePass(List.of(concurrent), 0, 0)
));
assertNull(IrisContext.get());
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 91L, fixture.context)) {
IrisContext caller = IrisContext.require();
Thread callerThread = Thread.currentThread();
generator.generateMatter(0, 0, true, fixture.context);
assertNotSame(callerThread, observedThread.get());
assertSame(fixture.engine, observed.get().getEngine());
assertSame(fixture.context, observed.get().getChunkContext());
assertEquals(91L, observed.get().getGenerationSessionId());
assertSame(caller, IrisContext.require());
}
assertNull(IrisContext.get());
}
private static void await(CountDownLatch latch) {
try {
assertTrue(latch.await(5, TimeUnit.SECONDS));
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new AssertionError(exception);
}
}
private static class RecordingComponent implements MantleComponent {
private final MantleFlag flag;
private final int priority;
private final int radius;
private RecordingComponent(MantleFlag flag, int priority, int radius) {
this.flag = flag;
this.priority = priority;
this.radius = radius;
}
@Override
public int getPriority() {
return priority;
}
@Override
public int getRadius() {
return radius;
}
@Override
public EngineMantle getEngineMantle() {
return null;
}
@Override
public MantleFlag getFlag() {
return flag;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public void setEnabled(boolean enabled) {
}
@Override
public void hotload() {
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
}
}
private static final class GeneratorFixture {
private final Engine engine;
private final Mantle<Matter> mantle;
private final ChunkContext context;
@SuppressWarnings("unchecked")
private GeneratorFixture() {
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.isUseMantle()).thenReturn(true);
EngineMantle engineMantle = mock(EngineMantle.class);
engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getMantle()).thenReturn(engineMantle);
mantle = mock(Mantle.class);
ConcurrentHashMap<Long, MantleChunk<Matter>> chunks = new ConcurrentHashMap<>();
when(mantle.getChunk(anyInt(), anyInt())).thenAnswer(invocation -> {
int chunkX = invocation.getArgument(0);
int chunkZ = invocation.getArgument(1);
long key = (((long) chunkX) << 32) ^ (chunkZ & 0xffffffffL);
return chunks.computeIfAbsent(key, ignored -> chunk());
});
context = mock(ChunkContext.class);
when(context.getGenerationSessionId()).thenReturn(91L);
}
private TestMatterGenerator generator(List<MantlePass> passes) {
return new TestMatterGenerator(engine, mantle, passes);
}
@SuppressWarnings("unchecked")
private static MantleChunk<Matter> chunk() {
MantleChunk<Matter> chunk = mock(MantleChunk.class);
when(chunk.use()).thenReturn(chunk);
when(chunk.isFlagged(any())).thenReturn(false);
doAnswer(invocation -> {
Runnable task = invocation.getArgument(1);
task.run();
return null;
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
return chunk;
}
}
private static final class TestMatterGenerator implements MatterGenerator {
private final Engine engine;
private final Mantle<Matter> mantle;
private final List<MantlePass> passes;
private TestMatterGenerator(Engine engine, Mantle<Matter> mantle, List<MantlePass> passes) {
this.engine = engine;
this.mantle = mantle;
this.passes = passes;
}
@Override
public Engine getEngine() {
return engine;
}
@Override
public Mantle<Matter> getMantle() {
return mantle;
}
@Override
public int getRadius() {
return passes.getFirst().passChunkRadius();
}
@Override
public int getRealRadius() {
return passes.getLast().passChunkRadius();
}
@Override
public List<MantlePass> getComponents() {
return passes;
}
}
}
@@ -44,6 +44,9 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class IrisCaveCarver3DNearParityTest {
private static final int SURFACE_CEILING_FADE_DEPTH = 12;
private static final double SURFACE_CEILING_SOLID_EPSILON = 0.000001D;
private static Method sampleDensityMethod;
private static Method aquiferCandidateMethod;
private static Field engineField;
@@ -167,6 +170,26 @@ public class IrisCaveCarver3DNearParityTest {
assertTrue(minY(capture.carvedCells) >= 0);
}
@Test
public void flatSurfaceDoesNotHardClipLargeCaveCeiling() {
assertFlatSurfaceCeiling(createFluidProfile()
.setVerticalRange(new IrisRange(20D, 72D))
.setAllowSurfaceBreak(false)
.setSurfaceClearance(5)
.setAllowFluid(false)
.setAllowLava(false)
.setAdaptiveSampling(false)
.setSampleStep(1));
assertFlatSurfaceCeiling(createFluidProfile()
.setVerticalRange(new IrisRange(20D, 72D))
.setAllowSurfaceBreak(false)
.setSurfaceClearance(5)
.setAllowFluid(false)
.setAllowLava(false)
.setAdaptiveSampling(true)
.setSampleStep(1));
}
@Test
public void exactPathMatchesNaiveReferenceWithoutWarpOrModules() throws Exception {
assertExactParity(false, false, false);
@@ -444,6 +467,38 @@ public class IrisCaveCarver3DNearParityTest {
assertTrue("expected carved cell delta below 3.5% but was " + differingRatio, differingRatio <= 0.035D);
}
private void assertFlatSurfaceCeiling(IrisCaveProfile profile) {
Engine engine = createEngine(96, 72);
WriterCapture capture = createWriterCapture(96);
new IrisCaveCarver3D(engine, profile).carve(
capture.writer, 0, 0, fullWeights(), 0D, 0D, null, filledHeights(72));
int[] ceilingYByColumn = new int[256];
Arrays.fill(ceilingYByColumn, Integer.MIN_VALUE);
for (String cell : capture.carvedCells) {
int localX = coordinate(cell, 0);
int y = coordinate(cell, 1);
int localZ = coordinate(cell, 2);
int columnIndex = (localX << 4) | localZ;
ceilingYByColumn[columnIndex] = Math.max(ceilingYByColumn[columnIndex], y);
}
Map<Integer, Integer> ceilingPlaneCounts = new HashMap<>();
int largestPlane = 0;
for (int ceilingY : ceilingYByColumn) {
assertTrue(ceilingY != Integer.MIN_VALUE);
assertTrue(ceilingY <= 67);
int planeCount = ceilingPlaneCounts.merge(ceilingY, 1, Integer::sum);
largestPlane = Math.max(largestPlane, planeCount);
}
assertTrue("expected at least three ceiling levels but found " + ceilingPlaneCounts,
ceilingPlaneCounts.size() >= 3);
assertTrue("expected no dominant ceiling plane but found " + ceilingPlaneCounts,
largestPlane < 192);
}
private void assertExactParity(boolean warp, boolean modules, boolean adaptiveSampling) throws Exception {
Engine engine = createEngine(96, 90);
double[] columnWeights = fullWeights();
@@ -523,9 +578,11 @@ public class IrisCaveCarver3DNearParityTest {
int[] columnTopY = new int[256];
int[] surfaceBreakFloorY = new int[256];
boolean[] surfaceBreakColumn = new boolean[256];
boolean[] surfaceCeilingColumn = new boolean[256];
int[] fluidMaxY = new int[256];
double[] passThreshold = new double[256];
double[] verticalEdgeFade = computeVerticalEdgeFade(profile, minY, maxY);
double[] surfaceClosureThreshold = computeSurfaceClosureThresholds(profile, minY, maxY);
MatterCavern[] matterByY = computeMatterByY(engine, profile, carveAir, carveLava, carveForcedAir, minY, maxY);
int x0 = chunkX << 4;
@@ -542,7 +599,8 @@ public class IrisCaveCarver3DNearParityTest {
columnSurfaceY = engine.getHeight(x, z);
}
int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurfaceY - surfaceClearance));
int unclampedClearanceTopY = columnSurfaceY - surfaceClearance;
int clearanceTopY = Math.min(maxY, Math.max(minY, unclampedClearanceTopY));
boolean breakColumn = allowSurfaceBreak && signed(surfaceBreakDensity.noiseFast2D(x, z)) >= surfaceBreakNoiseThreshold;
int resolvedTopY = breakColumn ? Math.min(maxY, Math.max(minY, columnSurfaceY)) : clearanceTopY;
columnTopY[columnIndex] = resolvedTopY;
@@ -551,6 +609,7 @@ public class IrisCaveCarver3DNearParityTest {
: Integer.MIN_VALUE;
surfaceBreakFloorY[columnIndex] = Math.max(minY, columnSurfaceY - surfaceBreakDepth);
surfaceBreakColumn[columnIndex] = breakColumn;
surfaceCeilingColumn[columnIndex] = !breakColumn && unclampedClearanceTopY <= maxY;
double columnWeight = clampColumnWeight(resolvedWeights[columnIndex]);
if (columnWeight <= 0D || resolvedTopY < minY) {
passThreshold[columnIndex] = Double.NaN;
@@ -584,6 +643,14 @@ public class IrisCaveCarver3DNearParityTest {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[y - minY];
localThreshold = applySurfaceCeilingFade(
localThreshold,
surfaceCeilingColumn[columnIndex],
topY,
y,
minY,
surfaceClosureThreshold
);
double density = (double) sampleDensityMethod.invoke(carver, x, y, z);
if (density > localThreshold) {
@@ -606,6 +673,64 @@ public class IrisCaveCarver3DNearParityTest {
return carved;
}
private double applySurfaceCeilingFade(
double threshold,
boolean surfaceCeilingColumn,
int columnTopY,
int y,
int minY,
double[] surfaceClosureThreshold
) {
if (!surfaceCeilingColumn) {
return threshold;
}
int ceilingDistance = columnTopY - y;
if (ceilingDistance < 0 || ceilingDistance >= SURFACE_CEILING_FADE_DEPTH) {
return threshold;
}
double closureThreshold = surfaceClosureThreshold[y - minY];
if (threshold <= closureThreshold) {
return threshold;
}
double progress = ceilingDistance / (double) SURFACE_CEILING_FADE_DEPTH;
double smooth = progress * progress * (3D - (2D * progress));
return closureThreshold + ((threshold - closureThreshold) * smooth);
}
private double[] computeSurfaceClosureThresholds(IrisCaveProfile profile, int minY, int maxY) {
double normalization = Math.abs(profile.getBaseWeight()) + Math.abs(profile.getDetailWeight());
for (IrisCaveFieldModule module : profile.getModules()) {
normalization += Math.abs(module.getWeight());
}
if (normalization <= 0D) {
normalization = 1D;
}
double[] thresholds = new double[Math.max(0, maxY - minY + 1)];
double baseMinimum = -Math.abs(profile.getBaseWeight()) - Math.abs(profile.getDetailWeight());
for (int y = minY; y <= maxY; y++) {
double minimumDensity = baseMinimum;
for (IrisCaveFieldModule module : profile.getModules()) {
IrisRange range = module.getVerticalRange();
if (y < Math.floor(range.getMin()) || y > Math.ceil(range.getMax())) {
continue;
}
double rawMinimum = module.isInvert()
? module.getThreshold() - 1D
: -1D - module.getThreshold();
double rawMaximum = module.isInvert()
? module.getThreshold() + 1D
: 1D - module.getThreshold();
minimumDensity += Math.min(rawMinimum * module.getWeight(), rawMaximum * module.getWeight());
}
thresholds[y - minY] = (minimumDensity / normalization) - SURFACE_CEILING_SOLID_EPSILON;
}
return thresholds;
}
private double[] computeVerticalEdgeFade(IrisCaveProfile profile, int minY, int maxY) {
int size = Math.max(0, maxY - minY + 1);
double[] verticalEdgeFade = new double[size];
@@ -55,7 +55,7 @@ public class MantleObjectComponentCaveAnchorTest {
assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(
true, water, RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE), 20, 8));
assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(
true, water, RiverCaveHydrology.of(RiverCaveAction.FALLING_WATER), 20, 8));
true, water, RiverCaveHydrology.of(RiverCaveAction.FALLING_FLUID), 20, 8));
assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(
true, null, RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD), 20, 8));
}
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.river.cave.CavePosition;
import art.arcane.iris.engine.river.cave.CaveVoxel;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -10,8 +11,13 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -29,7 +35,10 @@ public class MantleRiverCaveVoxelViewTest {
mantle,
128,
(x, z) -> x < 0 ? 20 : 60,
(x, z) -> null
(x, z) -> null,
RiverCaveFluidKind.RIVER,
(chunkX, chunkZ) -> {
}
);
CavePosition cliffAir = new CavePosition(-1, 21, 0);
CavePosition terrain = new CavePosition(-1, 20, 0);
@@ -40,6 +49,29 @@ public class MantleRiverCaveVoxelViewTest {
assertFalse(view.isOpenToSurface(terrain));
}
@Test
@SuppressWarnings("unchecked")
public void carvingInputLoadsOnlyOncePerReadChunk() {
Mantle<Matter> mantle = mock(Mantle.class);
when(mantle.getLoadedRegions()).thenReturn(new KMap<Long, TectonicPlate<Matter>>());
List<String> loaded = new ArrayList<>();
MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView(
mantle,
128,
(x, z) -> 60,
(x, z) -> null,
RiverCaveFluidKind.RIVER,
(chunkX, chunkZ) -> loaded.add(chunkX + "," + chunkZ)
);
view.voxelAt(new CavePosition(0, 20, 0));
view.voxelAt(new CavePosition(15, 21, 15));
view.riverHydrologyAt(new CavePosition(1, 22, 1));
view.voxelAt(new CavePosition(16, 20, 0));
assertEquals(List.of("0,0", "1,0"), loaded);
}
@Test
@SuppressWarnings("unchecked")
public void publishedRiverActionsDoNotReplaceTheUnderlyingPlanningBaseline() {
@@ -62,10 +94,89 @@ public class MantleRiverCaveVoxelViewTest {
mantle,
128,
(x, z) -> 60,
(x, z) -> null
(x, z) -> null,
RiverCaveFluidKind.RIVER,
(chunkX, chunkZ) -> {
}
);
assertEquals(CaveVoxel.SOLID, view.voxelAt(position));
assertEquals(RiverCaveAction.WET_SOURCE, view.riverActionAt(position));
assertEquals(RiverCaveAction.WET_SOURCE, view.riverHydrologyAt(position).action());
}
@Test
@SuppressWarnings("unchecked")
public void configuredLavaRiverTreatsPublishedLavaAsCompatibleFluid() {
Mantle<Matter> mantle = mock(Mantle.class);
TectonicPlate<Matter> plate = mock(TectonicPlate.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
Matter matter = mock(Matter.class);
MatterSlice<MatterCavern> cavernSlice = mock(MatterSlice.class);
PlatformBlockState lava = mock(PlatformBlockState.class);
KMap<Long, TectonicPlate<Matter>> regions = new KMap<>();
regions.put(Mantle.key(0, 0), plate);
when(mantle.getLoadedRegions()).thenReturn(regions);
when(plate.get(0, 0)).thenReturn(chunk);
when(chunk.exists(1)).thenReturn(true);
when(chunk.get(1)).thenReturn(matter);
when(matter.hasSlice(MatterCavern.class)).thenReturn(true);
doReturn(cavernSlice).when(matter).getSlice(MatterCavern.class);
when(cavernSlice.get(0, 4, 0)).thenReturn(new MatterCavern(true, "", (byte) 2));
when(lava.materialKey()).thenReturn("minecraft:lava");
MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView(
mantle,
128,
(x, z) -> 60,
(x, z) -> lava,
RiverCaveFluidKind.RIVER,
(chunkX, chunkZ) -> {
}
);
assertEquals(CaveVoxel.COMPATIBLE_FLUID, view.voxelAt(new CavePosition(0, 20, 0)));
}
@Test
@SuppressWarnings("unchecked")
public void planningRejectsHydrologyOwnedByTheOtherFluidKind() {
Mantle<Matter> mantle = mock(Mantle.class);
TectonicPlate<Matter> plate = mock(TectonicPlate.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
Matter matter = mock(Matter.class);
MatterSlice<RiverCaveHydrology> hydrologySlice = mock(MatterSlice.class);
KMap<Long, TectonicPlate<Matter>> regions = new KMap<>();
regions.put(Mantle.key(0, 0), plate);
CavePosition position = new CavePosition(0, 20, 0);
when(mantle.getLoadedRegions()).thenReturn(regions);
when(plate.get(0, 0)).thenReturn(chunk);
when(chunk.exists(1)).thenReturn(true);
when(chunk.get(1)).thenReturn(matter);
when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true);
doReturn(hydrologySlice).when(matter).getSlice(RiverCaveHydrology.class);
when(hydrologySlice.get(0, 4, 0)).thenReturn(RiverCaveHydrology.of(
RiverCaveAction.WET_SOURCE,
RiverCaveFluidKind.DEEP_POOL
));
MantleRiverCaveVoxelView riverView = new MantleRiverCaveVoxelView(
mantle,
128,
(x, z) -> 60,
(x, z) -> null,
RiverCaveFluidKind.RIVER,
(chunkX, chunkZ) -> {
}
);
MantleRiverCaveVoxelView deepPoolView = new MantleRiverCaveVoxelView(
mantle,
128,
(x, z) -> 60,
(x, z) -> null,
RiverCaveFluidKind.DEEP_POOL,
(chunkX, chunkZ) -> {
}
);
assertEquals(CaveVoxel.INCOMPATIBLE_FLUID, riverView.voxelAt(position));
assertEquals(CaveVoxel.SOLID, deepPoolView.voxelAt(position));
}
}
@@ -1,24 +1,32 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRiverCaveFallback;
import art.arcane.iris.engine.object.IrisRiverCaveMode;
import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.object.IrisRiverDeepPools;
import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.river.RiverAnchor;
import art.arcane.iris.engine.river.RiverEdgeId;
import art.arcane.iris.engine.river.RiverNodeId;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.engine.river.RiverTopologyComplexity;
import art.arcane.iris.engine.river.cave.CavePosition;
import art.arcane.iris.engine.river.cave.CaveVoxel;
import art.arcane.iris.engine.river.cave.CaveVoxelPrecondition;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner;
import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.river.cave.RiverCaveMode;
import art.arcane.iris.engine.river.cave.RiverCavePlan;
import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings;
@@ -26,6 +34,12 @@ import art.arcane.iris.engine.river.cave.RiverCaveRejection;
import art.arcane.iris.engine.river.cave.RiverCaveSource;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import art.arcane.iris.engine.river.runtime.IrisRiverTunnelSample;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.project.context.ChunkContext;
import org.junit.Test;
import java.util.HashMap;
@@ -41,7 +55,14 @@ import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MantleRiverHydrologyComponentTest {
private final RiverCaveContainmentPlanner planner = new RiverCaveContainmentPlanner();
@@ -70,6 +91,12 @@ public class MantleRiverHydrologyComponentTest {
dimension.getRivers().getCaves().setMaximumPerReach(0);
assertTrue(MantleRiverHydrologyComponent.isEnabledFor(dimension));
assertFalse(MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension));
dimension.getRivers().getCaves().setMode(IrisRiverCaveMode.SEALED);
dimension.getRivers().getCaves().getDeepPools().setEnabled(true);
assertTrue(MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension));
dimension.getRivers().getCaves().getDeepPools().setMaximumPerReach(0);
assertFalse(MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension));
dimension.getRivers().getCaves().getDeepPools().setEnabled(false).setMaximumPerReach(1);
dimension.getRivers().getCaves().setMaximumPerReach(1);
dimension.setCarvingEnabled(false);
assertFalse(MantleRiverHydrologyComponent.isEnabledFor(dimension));
@@ -84,6 +111,69 @@ public class MantleRiverHydrologyComponentTest {
assertFalse(MantleRiverHydrologyComponent.isEnabledFor(dimension));
}
@Test
public void adaptiveInputRadiusRetainsOnlyRequiredHydrologyHalos() {
bindMockPlatform();
try {
IrisDimension dimension = new IrisDimension();
dimension.setCarvingEnabled(true);
dimension.getRivers().setEnabled(true);
IrisRiverCaves caves = dimension.getRivers().getCaves();
caves.setMode(IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT).setMaximumPerReach(1);
IrisRiverDeepPools deepPools = caves.getDeepPools();
deepPools.setEnabled(true).setMaximumPerReach(1);
Engine engine = mock(Engine.class);
EngineMantle engineMantle = mock(EngineMantle.class);
IrisComplex complex = mock(IrisComplex.class);
ChunkContext context = mock(ChunkContext.class);
IrisRiverRuntime runtime = mock(IrisRiverRuntime.class);
RiverAnchor anchor = mock(RiverAnchor.class);
when(engineMantle.getEngine()).thenReturn(engine);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
when(context.getComplex()).thenReturn(complex);
when(complex.getRiverRuntime()).thenReturn(runtime);
when(runtime.caveSettings()).thenReturn(caves);
when(runtime.maximumChannelWidth()).thenReturn(12D);
when(runtime.maximumTunnelWidthMultiplier()).thenReturn(1D);
when(runtime.tunnelMouthBlend()).thenReturn(0D);
when(runtime.candidateAnchors(
anyInt(),
anyInt(),
anyInt(),
anyInt(),
anyDouble(),
anyLong()
)).thenReturn(List.of(anchor));
MantleRiverHydrologyComponent component = new MantleRiverHydrologyComponent(engineMantle);
assertEquals(0, component.getInputRadius(0, 0, 0, context));
when(runtime.hasRiverFootprint(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(true);
assertEquals(
RiverTopologyComplexity.tunnelHalo(12D, 1D, 0D),
component.getInputRadius(0, 0, 0, context)
);
when(runtime.hasRiverFootprint(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(false);
when(runtime.acceptsCaveAnchor(anchor)).thenReturn(true);
assertEquals(
MantleRiverHydrologyComponent.planningHalo(caves),
component.getInputRadius(0, 0, 0, context)
);
when(runtime.acceptsCaveAnchor(anchor)).thenReturn(false);
when(runtime.acceptsDeepPoolAnchor(anchor)).thenReturn(true);
assertEquals(
MantleRiverHydrologyComponent.deepPoolPlanningHalo(deepPools),
component.getInputRadius(0, 0, 0, context)
);
} finally {
IrisPlatforms.unbind();
}
}
@Test
public void buriedChannelPlansWetCoreDryRoofAndSolidGuardShell() {
TestVoxelView view = new TestVoxelView();
@@ -212,7 +302,7 @@ public class MantleRiverHydrologyComponentTest {
}
@Test
public void buriedChannelMayOpenOnlyIntoItsExactSurfaceRiverMouth() {
public void buriedChannelMouthMayFlareIntoTheWetSurfaceBank() {
TestVoxelView view = new TestVoxelView();
view.set(new CavePosition(1, 11, 0), CaveVoxel.COMPATIBLE_FLUID);
view.set(new CavePosition(1, 12, 0), CaveVoxel.COMPATIBLE_FLUID);
@@ -225,7 +315,7 @@ public class MantleRiverHydrologyComponentTest {
14
);
IrisRiverSurfaceSample mouth = new IrisRiverSurfaceSample(
riverSample(RiverRouteState.WET, RiverSection.CHANNEL),
riverSample(RiverRouteState.WET, RiverSection.BANK),
14D,
10D,
12D,
@@ -411,7 +501,7 @@ public class MantleRiverHydrologyComponentTest {
RiverCavePlan plan = planner.plan(view, source, settings);
assertTrue(plan.accepted());
assertEquals(RiverCaveAction.FALLING_WATER, plan.actions().get(entry));
assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(entry));
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(target));
assertFalse(plan.actions().containsKey(connectedPit));
}
@@ -517,10 +607,107 @@ public class MantleRiverHydrologyComponentTest {
caves.setMode(IrisRiverCaveMode.SEALED);
assertEquals(6, MantleRiverHydrologyComponent.inputRadius(caves, 6));
caves.getDeepPools()
.setEnabled(true)
.setSearchRadius(20)
.setHorizontalRadius(24);
assertEquals(180, MantleRiverHydrologyComponent.inputRadius(caves, 6));
caves.getDeepPools().setEnabled(false);
caves.setMode(IrisRiverCaveMode.GENERATE_GROTTO).setMaximumPerReach(0);
assertEquals(6, MantleRiverHydrologyComponent.inputRadius(caves, 6));
}
@Test
public void deepPoolSourceSearchUsesAbsoluteConfiguredHeightAndCaveFloor() {
IrisRiverDeepPools deepPools = new IrisRiverDeepPools()
.setMinimumFluidY(-180)
.setMaximumFluidY(-130)
.setSearchRadius(0)
.setSearchAttempts(1)
.setVerticalRadius(8)
.setDryHeadroom(4);
TestVoxelView view = new TestVoxelView();
view.set(new CavePosition(12, 101, 20), CaveVoxel.CAVE_AIR);
RiverAnchor anchor = new RiverAnchor(
new RiverEdgeId(new RiverNodeId(1L, 1L), new RiverNodeId(2L, 2L)),
0,
91L,
768D,
73L,
12D,
20D,
0.5D,
RiverRouteState.WET,
1,
1
);
RiverCaveSource source = MantleRiverHydrologyComponent.deepPoolSourceFor(
view,
deepPools,
anchor,
-256,
42L
);
assertNotNull(source);
assertEquals(new CavePosition(12, 100, 20), source.entry());
assertEquals(new CavePosition(12, 96, 20), source.target());
assertEquals(100, source.waterHeadY());
assertEquals(-156, source.waterHeadY() - 256);
assertEquals(RiverCaveMode.DEEP_POOL, source.mode());
}
@Test
public void deepPoolProofRadiusContainsNoisyDiagonalLobesAndShell() {
IrisRiverDeepPools deepPools = new IrisRiverDeepPools()
.setHorizontalRadius(24)
.setVerticalRadius(10)
.setDryHeadroom(5);
RiverCavePlannerSettings settings = MantleRiverHydrologyComponent.deepPoolPlannerSettings(
deepPools,
42L,
null
);
assertEquals(35, settings.maxHorizontalRadius());
assertEquals(16, settings.maxDepth());
assertEquals(35, settings.maxClosedComponentHorizontalRadius());
}
@Test
public void managedScaleWarpedDeepPoolPassesAsOneContainedBlob() {
IrisRiverDeepPools deepPools = new IrisRiverDeepPools()
.setHorizontalRadius(24)
.setVerticalRadius(10)
.setDryHeadroom(5)
.setShapeVariation(0.6D)
.setWarpStrength(8D)
.setMaximumVolume(65536);
TestVoxelView view = new TestVoxelView();
view.set(new CavePosition(0, 101, 0), CaveVoxel.CAVE_AIR);
RiverCaveSource source = new RiverCaveSource(
92L,
new CavePosition(0, 100, 0),
new CavePosition(0, 95, 0),
100,
RiverCaveMode.DEEP_POOL
);
RiverCavePlan plan = planner.plan(
view,
source,
MantleRiverHydrologyComponent.deepPoolPlannerSettings(deepPools, 42L, null)
);
assertTrue(plan.rejection().toString(), plan.accepted());
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 100, 0)));
assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(new CavePosition(0, 101, 0)));
assertTrue(plan.actions().size() > 10000);
}
@Test
public void absentRiverFootprintSkipsEveryTunnelColumnSample() {
AtomicInteger footprintSamples = new AtomicInteger();
@@ -778,9 +965,19 @@ public class MantleRiverHydrologyComponentTest {
return Map.copyOf(owned);
}
private static void bindMockPlatform() {
IrisPlatforms.unbind();
PlatformBlockState block = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenReturn(block);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
private static final class TestVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView {
private final Map<CavePosition, CaveVoxel> voxels = new HashMap<>();
private final Map<CavePosition, RiverCaveAction> riverActions = new HashMap<>();
private final Map<CavePosition, RiverCaveHydrology> riverActions = new HashMap<>();
private final Set<CavePosition> open = new HashSet<>();
@Override
@@ -799,7 +996,7 @@ public class MantleRiverHydrologyComponentTest {
}
@Override
public RiverCaveAction riverActionAt(CavePosition position) {
public RiverCaveHydrology riverHydrologyAt(CavePosition position) {
return riverActions.get(position);
}
@@ -812,7 +1009,7 @@ public class MantleRiverHydrologyComponentTest {
}
private void publish(CavePosition position, RiverCaveAction action) {
riverActions.put(position, action);
riverActions.put(position, RiverCaveHydrology.of(action, RiverCaveFluidKind.RIVER));
voxels.put(
position,
action == RiverCaveAction.WET_SOURCE
@@ -1,6 +1,7 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.matter.MatterCavern;
@@ -24,7 +25,11 @@ public class IrisCarveModifierRiverHydrologyTest {
baseline, RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD)));
MatterCavern wet = IrisCarveModifier.composeCavern(
baseline, new RiverCaveHydrology(RiverCaveAction.WET_SOURCE, "iris:flooded"));
baseline, new RiverCaveHydrology(
RiverCaveAction.WET_SOURCE,
"iris:flooded",
RiverCaveFluidKind.DEEP_POOL
));
MatterCavern dry = IrisCarveModifier.composeCavern(
baseline, RiverCaveHydrology.of(RiverCaveAction.DRY_AIR));
@@ -47,7 +52,7 @@ public class IrisCarveModifierRiverHydrologyTest {
assertSame(source, IrisCarveModifier.resolveHydrologyState(
RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE), current, source, air));
assertSame(falling, IrisCarveModifier.resolveHydrologyState(
RiverCaveHydrology.of(RiverCaveAction.FALLING_WATER), current, source, air));
RiverCaveHydrology.of(RiverCaveAction.FALLING_FLUID), current, source, air));
assertSame(air, IrisCarveModifier.resolveHydrologyState(
RiverCaveHydrology.of(RiverCaveAction.DRY_AIR), current, source, air));
}
@@ -0,0 +1,92 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.data.VectorMap;
import art.arcane.iris.util.common.math.IrisBlockVector;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
import static org.mockito.Mockito.when;
public class IrisObjectTransformsNearestBlockIndexTest {
@Test
public void spatialLookupMatchesLinearIterationIncludingDistanceTies() {
VectorMap<PlatformBlockState> blocks = new VectorMap<>();
Random random = new Random(812734L);
for (int i = 0; i < 120; i++) {
PlatformBlockState state = mock(PlatformBlockState.class, withSettings().stubOnly());
blocks.put(new IrisBlockVector(
random.nextInt(25) - 12,
random.nextInt(25) - 12,
random.nextInt(25) - 12
), state);
}
PlatformBlockState explicitAir = mock(PlatformBlockState.class, withSettings().stubOnly());
when(explicitAir.isAir()).thenReturn(true);
blocks.put(new IrisBlockVector(0, 0, 0), explicitAir);
IrisObjectTransforms.NearestBlockIndex index = IrisObjectTransforms.NearestBlockIndex.create(blocks);
List<Map.Entry<IrisBlockVector, PlatformBlockState>> candidates = new ArrayList<>();
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : blocks) {
candidates.add(entry);
}
for (int x = -10; x <= 10; x++) {
for (int y = -10; y <= 10; y++) {
for (int z = -10; z <= 10; z++) {
IrisBlockVector query = new IrisBlockVector(x, y, z);
PlatformBlockState direct = blocks.get(query);
PlatformBlockState actual = B.isAir(direct) ? index.nearest(x, y, z, direct) : direct;
assertSame("Mismatch at " + x + "," + y + "," + z,
nearestByLinearIteration(blocks, candidates, query), actual);
}
}
}
}
@Test
public void emptyIndexRetainsMissingAndExplicitAirFallbacks() {
VectorMap<PlatformBlockState> blocks = new VectorMap<>();
PlatformBlockState explicitAir = mock(PlatformBlockState.class, withSettings().stubOnly());
when(explicitAir.isAir()).thenReturn(true);
blocks.put(new IrisBlockVector(1, 2, 3), explicitAir);
IrisObjectTransforms.NearestBlockIndex index = IrisObjectTransforms.NearestBlockIndex.create(blocks);
assertSame(explicitAir, index.nearest(1, 2, 3, explicitAir));
assertSame(null, index.nearest(4, 5, 6, null));
}
private static PlatformBlockState nearestByLinearIteration(
VectorMap<PlatformBlockState> blocks,
List<Map.Entry<IrisBlockVector, PlatformBlockState>> candidates,
IrisBlockVector query
) {
PlatformBlockState result = blocks.get(query);
if (!B.isAir(result)) {
return result;
}
double nearestDistance = Double.MAX_VALUE;
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : candidates) {
PlatformBlockState state = entry.getValue();
if (B.isAir(state)) {
continue;
}
double distance = entry.getKey().distanceSquared(query);
if (distance < nearestDistance) {
nearestDistance = distance;
result = state;
}
}
return result;
}
}
@@ -24,7 +24,9 @@ public class IrisRiverConfigurationTest {
assertNotNull(dimension.getRivers());
assertFalse(dimension.getRivers().isEnabled());
assertEquals(IrisRiverWaterMode.SEA_LEVEL, dimension.getRivers().getWater().getMode());
assertEquals(IrisRiverWaterMode.FIXED, dimension.getRivers().getWater().getMode());
assertEquals(63, dimension.getRivers().getWater().getFluidHeight());
assertEquals("water", dimension.getRivers().getWater().getFluidPalette().getPalette().getFirst().getBlock());
assertFalse(dimension.getRivers().getTopology().isRequireOcean());
assertEquals(512, dimension.getRivers().getTopology().getCellSize());
assertEquals(16, dimension.getRivers().getTopology().getMaxRouteReaches());
@@ -41,6 +43,12 @@ public class IrisRiverConfigurationTest {
assertEquals(IrisRiverCaveFallback.SEALED, dimension.getRivers().getCaves().getFallback());
assertEquals(IrisRiverExistingFluidPolicy.REJECT,
dimension.getRivers().getCaves().getExistingFluidPolicy());
assertNotNull(dimension.getRivers().getCaves().getDeepPools());
assertFalse(dimension.getRivers().getCaves().getDeepPools().isEnabled());
assertEquals(-224, dimension.getRivers().getCaves().getDeepPools().getMinimumFluidY());
assertEquals(-104, dimension.getRivers().getCaves().getDeepPools().getMaximumFluidY());
assertEquals("lava", dimension.getRivers().getCaves().getDeepPools()
.getFluidPalette().getPalette().getFirst().getBlock());
assertTrue(dimension.getRivers().getBiomes().getAllBiomeIds().isEmpty());
assertNull(new IrisRegion().getRiverOverride());
assertNull(new IrisBiome().getRiverOverride());
@@ -62,6 +70,7 @@ public class IrisRiverConfigurationTest {
"source": {"chance": 0.27, "influence": 0.4}
},
"terrain": {
"channelRadiusBonus": 3,
"maxChannelWidth": 9,
"maxBankWidth": 2.5,
"maxDepth": 8,
@@ -80,7 +89,8 @@ public class IrisRiverConfigurationTest {
"bankMultiplier": 1.2,
"depthMultiplier": 0.8,
"bodyWavelength": 704,
"bodyDetailWavelength": 88,
"bodyDetailWavelength": 18,
"bodyDetailInfluence": 0.82,
"widthVariation": 0.75,
"bankVariation": 0.65,
"depthVariation": 0.55,
@@ -89,7 +99,14 @@ public class IrisRiverConfigurationTest {
],
"terminalMode": "SUPPRESS"
},
"water": {"mode": "TERRACED", "poolLength": 80},
"water": {
"mode": "TERRACED",
"fluidHeight": -48,
"fluidPalette": {
"palette": [{"block": "minecraft:lava"}]
},
"poolLength": 80
},
"biomes": {
"channel": ["river/channel"],
"floodedCave": ["river/grotto"]
@@ -97,7 +114,29 @@ public class IrisRiverConfigurationTest {
"caves": {
"mode": "FLOOD_CLOSED_COMPONENT",
"maxFloodVolume": 2048,
"existingFluidPolicy": "ALLOW_SAME"
"existingFluidPolicy": "ALLOW_SAME",
"deepPools": {
"enabled": true,
"reach": {
"chance": 0.4,
"influence": 0.1
},
"minimumSpacing": 896,
"maximumPerReach": 2,
"minimumFluidY": -220,
"maximumFluidY": -112,
"searchRadius": 24,
"searchAttempts": 18,
"horizontalRadius": 28,
"verticalRadius": 11,
"dryHeadroom": 5,
"shapeVariation": 0.7,
"warpStrength": 9,
"maximumVolume": 65536,
"fluidPalette": {
"palette": [{"block": "minecraft:lava"}]
}
}
}
}
}
@@ -130,6 +169,7 @@ public class IrisRiverConfigurationTest {
assertFalse(dimension.getRivers().getTopology().isRequireOcean());
assertEquals(0.27D, dimension.getRivers().getTopology().getSource().getChance(), 0D);
assertEquals(9D, dimension.getRivers().getTerrain().getMaxChannelWidth(), 0D);
assertEquals(3D, dimension.getRivers().getTerrain().getChannelRadiusBonus(), 0D);
assertEquals(2.5D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D);
assertEquals(8D, dimension.getRivers().getTerrain().getMaxDepth(), 0D);
assertEquals(36, dimension.getRivers().getTerrain().getMaxIncision());
@@ -146,7 +186,8 @@ public class IrisRiverConfigurationTest {
assertEquals(1.2D, worm.getBankMultiplier(), 0D);
assertEquals(0.8D, worm.getDepthMultiplier(), 0D);
assertEquals(704D, worm.getBodyWavelength(), 0D);
assertEquals(88D, worm.getBodyDetailWavelength(), 0D);
assertEquals(18D, worm.getBodyDetailWavelength(), 0D);
assertEquals(0.82D, worm.getBodyDetailInfluence(), 0D);
assertEquals(0.75D, worm.getWidthVariation(), 0D);
assertEquals(0.65D, worm.getBankVariation(), 0D);
assertEquals(0.55D, worm.getDepthVariation(), 0D);
@@ -154,12 +195,32 @@ public class IrisRiverConfigurationTest {
assertEquals(IrisRiverTerminalMode.SUPPRESS,
dimension.getRivers().getTerrain().getTerminalMode());
assertEquals(IrisRiverWaterMode.TERRACED, dimension.getRivers().getWater().getMode());
assertEquals(-48, dimension.getRivers().getWater().getFluidHeight());
assertEquals("minecraft:lava",
dimension.getRivers().getWater().getFluidPalette().getPalette().getFirst().getBlock());
assertEquals(Set.of("river/channel", "river/grotto"),
Set.copyOf(dimension.getRivers().getBiomes().getAllBiomeIds()));
assertEquals(IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT,
dimension.getRivers().getCaves().getMode());
assertEquals(IrisRiverExistingFluidPolicy.ALLOW_SAME,
dimension.getRivers().getCaves().getExistingFluidPolicy());
IrisRiverDeepPools deepPools = dimension.getRivers().getCaves().getDeepPools();
assertTrue(deepPools.isEnabled());
assertEquals(0.4D, deepPools.getReach().getChance(), 0D);
assertEquals(0.1D, deepPools.getReach().getInfluence(), 0D);
assertEquals(896, deepPools.getMinimumSpacing());
assertEquals(2, deepPools.getMaximumPerReach());
assertEquals(-220, deepPools.getMinimumFluidY());
assertEquals(-112, deepPools.getMaximumFluidY());
assertEquals(24, deepPools.getSearchRadius());
assertEquals(18, deepPools.getSearchAttempts());
assertEquals(28, deepPools.getHorizontalRadius());
assertEquals(11, deepPools.getVerticalRadius());
assertEquals(5, deepPools.getDryHeadroom());
assertEquals(0.7D, deepPools.getShapeVariation(), 0D);
assertEquals(9D, deepPools.getWarpStrength(), 0D);
assertEquals(65536, deepPools.getMaximumVolume());
assertEquals("minecraft:lava", deepPools.getFluidPalette().getPalette().getFirst().getBlock());
assertEquals(Boolean.FALSE, region.getRiverOverride().getAllowSources());
assertEquals(IrisRiverRoutingPolicy.AVOID, region.getRiverOverride().getRoutingPolicy());
@@ -0,0 +1,189 @@
package art.arcane.iris.engine.platform;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class BukkitChunkGeneratorInitialSpawnTest {
@Test
public void spawnReadinessWaitsForChunkAndRegionPlacement() throws Exception {
CompletableFuture<Void> readiness = new CompletableFuture<>();
BukkitChunkGenerator generator = generatorWithReadiness(readiness);
World world = world("spawn-success");
Chunk chunk = chunk(world);
CompletableFuture<Chunk> chunkFuture = new CompletableFuture<>();
AtomicReference<Runnable> regionTask = new AtomicReference<>();
when(world.getChunkAtAsync(0, 0, true)).thenReturn(chunkFuture);
when(world.getSpawnLocation()).thenReturn(new Location(world, 0.5D, 64D, 0.5D));
when(world.getHighestBlockYAt(any(Location.class))).thenReturn(70);
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
scheduling.when(() -> J.runRegionFuture(
any(World.class),
anyInt(),
anyInt(),
any(Runnable.class)))
.thenAnswer(invocation -> {
regionTask.set(invocation.getArgument(3, Runnable.class));
return CompletableFuture.completedFuture(null);
});
invokeUpdateSpawnLocation(generator, world);
assertFalse(readiness.isDone());
chunkFuture.complete(chunk);
assertNotNull(regionTask.get());
assertFalse(readiness.isDone());
regionTask.get().run();
readiness.get(5L, TimeUnit.SECONDS);
}
}
@Test
public void failedChunkRequestFailsSpawnReadiness() throws Exception {
CompletableFuture<Void> readiness = new CompletableFuture<>();
BukkitChunkGenerator generator = generatorWithReadiness(readiness);
World world = world("spawn-chunk-failure");
IllegalStateException failure = new IllegalStateException("chunk failed");
when(world.getChunkAtAsync(0, 0, true)).thenReturn(CompletableFuture.failedFuture(failure));
invokeUpdateSpawnLocation(generator, world);
assertSame(failure, awaitFailure(readiness));
}
@Test
public void nullChunkFailsSpawnReadiness() throws Exception {
CompletableFuture<Void> readiness = new CompletableFuture<>();
BukkitChunkGenerator generator = generatorWithReadiness(readiness);
World world = world("spawn-null-chunk");
when(world.getChunkAtAsync(0, 0, true)).thenReturn(CompletableFuture.completedFuture(null));
invokeUpdateSpawnLocation(generator, world);
Throwable failure = awaitFailure(readiness);
assertTrue(failure.getMessage().contains("Initial spawn preparation failed"));
assertNotNull(failure.getCause());
assertTrue(failure.getCause().getMessage().contains("completed without a chunk"));
}
@Test
public void rejectedRegionScheduleFailsSpawnReadiness() throws Exception {
CompletableFuture<Void> readiness = new CompletableFuture<>();
BukkitChunkGenerator generator = generatorWithReadiness(readiness);
World world = world("spawn-schedule-failure");
Chunk chunk = chunk(world);
IllegalStateException failure = new IllegalStateException("region rejected");
when(world.getChunkAtAsync(0, 0, true)).thenReturn(CompletableFuture.completedFuture(chunk));
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
scheduling.when(() -> J.runRegionFuture(
any(World.class),
anyInt(),
anyInt(),
any(Runnable.class)))
.thenReturn(CompletableFuture.failedFuture(failure));
invokeUpdateSpawnLocation(generator, world);
}
assertSame(failure, awaitFailure(readiness));
}
@Test
public void failedRegionPlacementFailsSpawnReadiness() throws Exception {
CompletableFuture<Void> readiness = new CompletableFuture<>();
BukkitChunkGenerator generator = generatorWithReadiness(readiness);
World world = world("spawn-placement-failure");
Chunk chunk = chunk(world);
IllegalStateException failure = new IllegalStateException("spawn placement failed");
when(world.getChunkAtAsync(0, 0, true)).thenReturn(CompletableFuture.completedFuture(chunk));
when(world.getSpawnLocation()).thenThrow(failure);
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
scheduling.when(() -> J.runRegionFuture(
any(World.class),
anyInt(),
anyInt(),
any(Runnable.class)))
.thenAnswer(invocation -> {
invocation.getArgument(3, Runnable.class).run();
return CompletableFuture.completedFuture(null);
});
invokeUpdateSpawnLocation(generator, world);
}
assertSame(failure, awaitFailure(readiness));
}
private static BukkitChunkGenerator generatorWithReadiness(
CompletableFuture<Void> readiness
) throws ReflectiveOperationException {
BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
Field readinessField = BukkitChunkGenerator.class.getDeclaredField("initialSpawnReady");
readinessField.setAccessible(true);
readinessField.set(generator, readiness);
return generator;
}
private static World world(String name) {
World world = mock(World.class);
when(world.getName()).thenReturn(name);
when(world.getMinHeight()).thenReturn(-64);
when(world.getMaxHeight()).thenReturn(320);
return world;
}
private static Chunk chunk(World world) {
Chunk chunk = mock(Chunk.class);
when(chunk.getWorld()).thenReturn(world);
when(chunk.getX()).thenReturn(0);
when(chunk.getZ()).thenReturn(0);
return chunk;
}
private static void invokeUpdateSpawnLocation(
BukkitChunkGenerator generator,
World world
) throws ReflectiveOperationException {
Method updateSpawnLocation = BukkitChunkGenerator.class.getDeclaredMethod(
"updateSpawnLocation",
World.class);
updateSpawnLocation.setAccessible(true);
updateSpawnLocation.invoke(generator, world);
}
private static Throwable awaitFailure(CompletableFuture<Void> readiness) throws Exception {
try {
readiness.get(5L, TimeUnit.SECONDS);
fail("Expected initial spawn readiness to fail.");
throw new AssertionError("unreachable");
} catch (ExecutionException exception) {
return exception.getCause();
}
}
}
@@ -1018,6 +1018,117 @@ public class RiverNetworkTest {
assertTrue(maximumRoof - minimumRoof > 0.1D);
}
@Test
public void volatileBodyProfilesResolveSubTenBlockThicknessChanges() {
RiverWorm volatileBody = new RiverWorm(
"volatile-body",
4815L,
1D,
1024D,
128D,
0.65D,
0.2D,
180D,
48,
1D,
1D,
1D,
24D,
8D,
0.95D,
0.875D,
0.75D,
0.75D,
0.75D,
4,
0.35D,
1D,
0D,
0D,
List.of()
);
RiverNetwork network = new RiverNetwork(options(4815L)
.cellSize(1700)
.tileCells(1)
.siteJitter(0D)
.requireOcean(false)
.channelWidth(14D)
.maxChannelWidth(38D)
.maximumReachRadius(64D)
.worms(List.of(volatileBody))
.build());
RiverReach reach = network.buildTile(0, 0, flatTerrain(false)).reaches().getFirst();
RiverBodyProfile profile = reach.bodyProfile();
double maximumStationSpacing = 0D;
for (int index = 1; index < profile.size(); index++) {
maximumStationSpacing = StrictMath.max(
maximumStationSpacing,
reach.polyline().length() * (profile.position(index) - profile.position(index - 1))
);
}
boolean volatileChangeFound = false;
for (double distance = 0D; distance + 10D <= reach.polyline().length(); distance += 2D) {
double start = distance / reach.polyline().length();
double end = (distance + 10D) / reach.polyline().length();
if (StrictMath.abs(profile.width(start) - profile.width(end)) >= 2D) {
volatileChangeFound = true;
break;
}
}
assertTrue(maximumStationSpacing <= 10D);
assertTrue(volatileChangeFound);
}
@Test
public void channelRadiusBonusAddsThreeBlocksPerSideAfterShaping() {
RiverWorm constantBody = wormProfile(
"radius-bonus",
6501L,
8,
1D,
1D,
1D,
64D,
16D,
0D,
0D,
0D,
0D,
4,
0.35D,
1D,
0D,
0D,
List.of()
);
RiverNetwork baseline = new RiverNetwork(options(6501L)
.siteJitter(0D)
.requireOcean(false)
.channelWidth(10D)
.maxChannelWidth(38D)
.maximumReachRadius(64D)
.worms(List.of(constantBody))
.build());
RiverNetwork expanded = new RiverNetwork(options(6501L)
.siteJitter(0D)
.requireOcean(false)
.channelWidth(10D)
.channelRadiusBonus(3D)
.maxChannelWidth(38D)
.maximumReachRadius(64D)
.worms(List.of(constantBody))
.build());
RiverReach baselineReach = baseline.buildTile(0, 0, flatTerrain(false)).reaches().getFirst();
RiverReach expandedReach = expanded.buildTile(0, 0, flatTerrain(false)).reaches().getFirst();
assertEquals(baselineReach.id(), expandedReach.id());
assertEquals(6D, expandedReach.bodyProfile().width(0.5D)
- baselineReach.bodyProfile().width(0.5D), 0.0000001D);
}
@Test
public void foldedReachSamplingFindsFartherCoveringWidthEnvelope() {
RiverNode from = node(0L, 0L, 0D, 0D);
@@ -1119,17 +1230,17 @@ public class RiverNetworkTest {
public void weightedWormProfilesSelectDistinctConfigurableVariants() {
RiverWorm gentle = new RiverWorm(
"gentle", 301L, 1D, 1024D, 256D, 0.2D, 0.05D, 10D, 8, 0.5D, 0.5D, 0.5D,
512D, 128D, 0D, 0D, 0D, 0D,
512D, 128D, 0.3D, 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,
512D, 128D, 0.3D, 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,
512D, 128D, 0.3D, 0D, 0D, 0D, 0D,
4, 0.35D, 1D, 0D, 0D, List.of()
);
RiverNetwork network = new RiverNetwork(options(64L)
@@ -1547,6 +1658,7 @@ public class RiverNetworkTest {
1D,
512D,
128D,
0.3D,
0D,
0D,
0D,
@@ -1631,6 +1743,7 @@ public class RiverNetworkTest {
depthMultiplier,
bodyWavelength,
bodyDetailWavelength,
0.3D,
widthVariation,
bankVariation,
depthVariation,
@@ -160,8 +160,8 @@ public class RiverCaveContainmentPlannerTest {
RiverCavePlan rejected = planner.plan(view, source, rejectedSettings);
assertTrue(accepted.accepted());
assertEquals(RiverCaveAction.FALLING_WATER, accepted.actions().get(position(0, 11, 0)));
assertEquals(RiverCaveAction.FALLING_WATER, accepted.actions().get(position(0, 12, 0)));
assertEquals(RiverCaveAction.FALLING_FLUID, accepted.actions().get(position(0, 11, 0)));
assertEquals(RiverCaveAction.FALLING_FLUID, accepted.actions().get(position(0, 12, 0)));
assertEquals(RiverCaveRejection.DRY_HEADROOM_LIMIT, rejected.rejection());
}
@@ -341,7 +341,7 @@ public class RiverCaveContainmentPlannerTest {
RiverCavePlan plan = planner.plan(view, source, settings());
assertTrue(plan.accepted());
assertEquals(RiverCaveAction.FALLING_WATER, plan.actions().get(position(0, 12, 0)));
assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position(0, 12, 0)));
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 8, 0)));
assertTrue(plan.actions().containsValue(RiverCaveAction.SEAL_GUARD));
assertEquals(plan.actions().keySet(), plan.baselinePreconditions().keySet());
@@ -383,7 +383,7 @@ public class RiverCaveContainmentPlannerTest {
assertTrue(plan.rejection().toString(), plan.accepted());
for (CavePosition position : wetMouth) {
assertEquals(RiverCaveAction.FALLING_WATER, plan.actions().get(position));
assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position));
}
}
@@ -401,6 +401,53 @@ public class RiverCaveContainmentPlannerTest {
assertRejectedWithoutPublication(plan, RiverCaveRejection.GROTTO_SHELL_OPEN);
}
@Test
public void deepPoolOpensOnlyAboveItsContainedFluidHead() {
TestVoxelView view = new TestVoxelView();
view.set(CaveVoxel.CAVE_AIR, position(0, 11, 0), position(0, 13, 0));
RiverCaveSource source = new RiverCaveSource(
109L,
position(0, 10, 0),
position(0, 8, 0),
10,
RiverCaveMode.DEEP_POOL
);
RiverCavePlan plan = planner.plan(view, source, detailedSettings(
1,
2,
RiverCaveGrottoShape.ELLIPSOID,
RiverCaveFluidPolicy.REJECT_EXISTING
));
assertTrue(plan.rejection().toString(), plan.accepted());
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 10, 0)));
assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(position(0, 11, 0)));
assertFalse(plan.actions().containsKey(position(0, 13, 0)));
}
@Test
public void deepPoolRejectsCaveLeakAtOrBelowItsFluidHead() {
TestVoxelView view = new TestVoxelView();
view.set(CaveVoxel.CAVE_AIR, position(5, 8, 0));
RiverCaveSource source = new RiverCaveSource(
110L,
position(0, 10, 0),
position(0, 8, 0),
10,
RiverCaveMode.DEEP_POOL
);
RiverCavePlan plan = planner.plan(view, source, detailedSettings(
1,
2,
RiverCaveGrottoShape.ELLIPSOID,
RiverCaveFluidPolicy.REJECT_EXISTING
));
assertRejectedWithoutPublication(plan, RiverCaveRejection.GROTTO_SHELL_OPEN);
}
@Test
public void combinedModeUsesClosedCaveOrGeneratedGrottoFromBaseline() {
TestVoxelView caveView = new TestVoxelView();
@@ -426,8 +473,8 @@ public class RiverCaveContainmentPlannerTest {
RiverCavePlan plan = planner.plan(view, source, settings());
assertTrue(plan.accepted());
assertEquals(RiverCaveAction.FALLING_WATER, plan.actions().get(position(0, 12, 0)));
assertEquals(RiverCaveAction.FALLING_WATER, plan.actions().get(position(0, 11, 0)));
assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position(0, 12, 0)));
assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position(0, 11, 0)));
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 10, 0)));
assertFalse(plan.actions().containsValue(RiverCaveAction.DRY_AIR));
}
@@ -138,6 +138,18 @@ public class IrisRiverRuntimeTest {
}
}
@Test
public void footprintPresenceQueryDistinguishesEmptyAndRoutedAreas() {
IrisRiverNetwork emptyConfiguration = configuration(false);
emptyConfiguration.getTopology().getSource().setChance(0D);
try (IrisRiverRuntime empty = runtime(emptyConfiguration);
IrisRiverRuntime routed = runtime(configuration(false))) {
assertFalse(empty.hasRiverFootprint(-128, -128, 128, 128));
assertTrue(routed.hasRiverFootprint(-128, -128, 128, 128));
}
}
@Test
public void settingsAtSkipsNaturalBiomeWhenBiomeOverridesAreUnreachable() {
IrisRiverNetwork configuration = configuration(false);
@@ -236,6 +248,7 @@ public class IrisRiverRuntimeTest {
configuration,
mock(IrisData.class),
63,
63,
false,
true,
true,
@@ -290,6 +303,7 @@ public class IrisRiverRuntimeTest {
configuration,
mock(IrisData.class),
63,
63,
true,
true,
false,
@@ -453,6 +467,37 @@ public class IrisRiverRuntimeTest {
}
}
@Test
public void deepPoolReachGateIsIndependentSparseAndReachLimited() {
IrisRiverNetwork configuration = configuration(false);
configuration.getCaves().getDeepPools()
.setEnabled(true)
.setMaximumPerReach(1);
configuration.getCaves().getDeepPools().getReach()
.setChance(1D)
.setInfluence(0D)
.setStyle(flat());
try (IrisRiverRuntime runtime = runtime(configuration)) {
List<RiverAnchor> anchors = runtime.candidateAnchors(0, 0, 256, 256, 16D, 882L);
Map<RiverEdgeId, Integer> acceptedPerReach = new HashMap<>();
for (RiverAnchor anchor : anchors) {
if (runtime.acceptsDeepPoolAnchor(anchor)) {
acceptedPerReach.merge(anchor.reachId(), 1, Integer::sum);
}
}
assertFalse(acceptedPerReach.isEmpty());
for (int accepted : acceptedPerReach.values()) {
assertEquals(1, accepted);
}
configuration.getCaves().getDeepPools().setEnabled(false);
for (RiverAnchor anchor : anchors) {
assertFalse(runtime.acceptsDeepPoolAnchor(anchor));
}
}
}
@Test
public void finalPolylineSupercoverRejectsOneBlockedColumnMissedByWidthSpacing() {
IrisRiverNetwork configuration = configuration(false);
@@ -524,6 +569,7 @@ public class IrisRiverRuntimeTest {
configuration,
mock(IrisData.class),
63,
63,
false,
true,
false,
@@ -693,15 +739,26 @@ public class IrisRiverRuntimeTest {
}
@Test
public void cliffTransitionClassifiesOpenAndSolidColumnsIndependently() {
IrisRiverNetwork configuration = configuration(false);
configuration.getTerrain()
public void cliffTransitionCreatesAFlaredTunnelMouth() {
IrisRiverNetwork sealedConfiguration = configuration(false);
sealedConfiguration.getTerrain()
.setMaxIncision(10)
.setTunnelMouthBlend(2D);
.setTunnelMouthBlend(0D);
IrisRiverNetwork mouthConfiguration = configuration(false);
mouthConfiguration.getTerrain()
.setMaxIncision(10)
.setTunnelMouthBlend(6D);
ProceduralStream<Double> height = ProceduralStream.ofDouble((x, z) -> x < 0D ? 50D : 100D);
try (IrisRiverRuntime runtime = runtime(
configuration,
try (IrisRiverRuntime sealed = runtime(
sealedConfiguration,
height,
constantLandBiome(),
new IrisRegion(),
true,
false
); IrisRiverRuntime mouth = runtime(
mouthConfiguration,
height,
constantLandBiome(),
new IrisRegion(),
@@ -710,14 +767,16 @@ public class IrisRiverRuntimeTest {
)) {
boolean transitionFound = false;
for (int z = -2048; z <= 2048 && !transitionFound; z++) {
IrisRiverSurfaceSample open = runtime.sample(-1, z);
IrisRiverSurfaceSample solid = runtime.sample(0, z);
IrisRiverTunnelSample tunnel = runtime.sampleTunnel(0, z);
IrisRiverSurfaceSample open = mouth.sample(-1, z);
IrisRiverSurfaceSample solid = mouth.sample(0, z);
IrisRiverTunnelSample tunnel = mouth.sampleTunnel(0, z);
IrisRiverTunnelSample unflared = sealed.sampleTunnel(0, z);
if (open.river().present()
&& solid.river().present()
&& open.river().reachId().equals(solid.river().reachId())
&& open.surfaceFluid()
&& tunnel != null) {
&& tunnel != null
&& (unflared == null || tunnel.ceilingY() > unflared.ceilingY())) {
assertFalse(open.subterranean());
assertTrue(solid.subterranean());
transitionFound = true;
@@ -814,6 +873,19 @@ public class IrisRiverRuntimeTest {
}
}
@Test
public void fixedRiverHeightIsIndependentFromNaturalOceanHeight() {
IrisRiverNetwork configuration = configuration(false);
configuration.getWater()
.setMode(IrisRiverWaterMode.FIXED)
.setFluidHeight(-48);
try (IrisRiverRuntime runtime = runtimeWithFluidHeights(configuration, -48, 63)) {
assertEquals(-48D, runtime.waterSurface(null, 0D, false), 0D);
assertEquals(63D, runtime.waterSurface(null, 0D, true), 0D);
}
}
private static IrisRiverRuntime runtime(IrisRiverNetwork configuration) {
IrisBiome land = new IrisBiome().setInferredType(InferredType.LAND);
IrisRegion region = new IrisRegion();
@@ -897,6 +969,55 @@ public class IrisRiverRuntimeTest {
boolean caveHydrologyActive,
boolean blockingRoutingPossible,
boolean biomeRiverOverridesPossible
) {
return runtime(
configuration,
height,
biome,
region,
boreMantleActive,
caveHydrologyActive,
blockingRoutingPossible,
biomeRiverOverridesPossible,
63,
63
);
}
private static IrisRiverRuntime runtimeWithFluidHeights(
IrisRiverNetwork configuration,
int riverFluidHeight,
int dimensionFluidHeight
) {
IrisBiome biome = new IrisBiome().setInferredType(InferredType.LAND);
return runtime(
configuration,
constantHeight(80D),
ProceduralStream.of(
(x, z) -> biome,
Interpolated.of(value -> 0D, value -> biome)
),
new IrisRegion(),
false,
true,
true,
true,
riverFluidHeight,
dimensionFluidHeight
);
}
private static IrisRiverRuntime runtime(
IrisRiverNetwork configuration,
ProceduralStream<Double> height,
ProceduralStream<IrisBiome> biome,
IrisRegion region,
boolean boreMantleActive,
boolean caveHydrologyActive,
boolean blockingRoutingPossible,
boolean biomeRiverOverridesPossible,
int riverFluidHeight,
int dimensionFluidHeight
) {
ProceduralStream<Double> slope = ProceduralStream.ofDouble((x, z) -> 0.025D);
ProceduralStream<Boolean> oceans = ProceduralStream.of(
@@ -911,7 +1032,8 @@ public class IrisRiverRuntimeTest {
4829759234L,
configuration,
mock(IrisData.class),
63,
riverFluidHeight,
dimensionFluidHeight,
boreMantleActive,
caveHydrologyActive,
blockingRoutingPossible,
@@ -0,0 +1,24 @@
package art.arcane.iris.util.common.scheduling;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public class JRegionFutureContractTest {
@Test
public void regionFutureSettlesEverySchedulerPath() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/util/common/scheduling/J.java"))
.replace("\r\n", "\n");
int start = source.indexOf("public static CompletableFuture<Void> runRegionFuture(");
int end = source.indexOf("public static boolean runGlobal(", start);
String method = source.substring(start, end);
assertTrue(method.contains("settle(future, runnable)"));
assertTrue(method.contains("future.completeExceptionally("));
assertTrue(method.contains("return sfut(runnable);"));
}
}
@@ -1,6 +1,7 @@
package art.arcane.iris.util.project.matter.slices;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import org.junit.Test;
@@ -18,14 +19,20 @@ public class RiverCaveHydrologyMatterTest {
public void everyActionAndBiomeRoundTrips() throws IOException {
RiverCaveHydrologyMatter matter = new RiverCaveHydrologyMatter();
for (RiverCaveAction action : RiverCaveAction.values()) {
RiverCaveHydrology expected = new RiverCaveHydrology(action, "iris:flooded_grotto");
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
matter.writeNode(expected, new DataOutputStream(bytes));
for (RiverCaveFluidKind fluidKind : RiverCaveFluidKind.values()) {
RiverCaveHydrology expected = new RiverCaveHydrology(
action,
"iris:flooded_grotto",
fluidKind
);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
matter.writeNode(expected, new DataOutputStream(bytes));
RiverCaveHydrology actual = matter.readNode(
new DataInputStream(new ByteArrayInputStream(bytes.toByteArray())));
RiverCaveHydrology actual = matter.readNode(
new DataInputStream(new ByteArrayInputStream(bytes.toByteArray())));
assertEquals(expected, actual);
assertEquals(expected, actual);
}
}
}
@@ -40,5 +47,7 @@ public class RiverCaveHydrologyMatterTest {
new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))));
assertThrows(IOException.class, () -> matter.readNode(
new DataInputStream(new ByteArrayInputStream(new byte[]{99}))));
assertThrows(IOException.class, () -> matter.readNode(
new DataInputStream(new ByteArrayInputStream(new byte[]{1, 99}))));
}
}