mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
Way too much
This commit is contained in:
+148
-6
@@ -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);
|
||||
|
||||
+28
-12
@@ -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();
|
||||
|
||||
+27
-1
@@ -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];
|
||||
|
||||
+5
-2
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
+33
-12
@@ -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;
|
||||
|
||||
+474
-34
@@ -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
|
||||
}
|
||||
|
||||
+107
-1
@@ -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(),
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+21
-3
@@ -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
|
||||
|
||||
+106
@@ -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("""
|
||||
|
||||
+33
@@ -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");
|
||||
|
||||
+34
-20
@@ -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);
|
||||
|
||||
+22
-1
@@ -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
|
||||
|
||||
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+185
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
+131
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
-1
@@ -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];
|
||||
|
||||
+1
-1
@@ -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));
|
||||
}
|
||||
|
||||
+114
-3
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+203
-6
@@ -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
|
||||
|
||||
+7
-2
@@ -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));
|
||||
}
|
||||
|
||||
+92
@@ -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());
|
||||
|
||||
+189
@@ -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,
|
||||
|
||||
+53
-6
@@ -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));
|
||||
}
|
||||
|
||||
+133
-11
@@ -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,
|
||||
|
||||
+24
@@ -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);"));
|
||||
}
|
||||
}
|
||||
+15
-6
@@ -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}))));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user