mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Way too much
This commit is contained in:
131 files changed
+6825
-1093
No files matched your search
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user