This commit is contained in:
Brian Neumann-Fopiano
2026-08-24 02:27:26 -04:00
parent fe5651f854
commit e20bb86f40
228 changed files with 5716 additions and 3577 deletions
@@ -85,13 +85,12 @@ public class IrisSettings {
private static IrisSettings read() {
IrisSettings loaded = new IrisSettings();
File s = IrisPlatforms.get().dataFile("settings.json");
File s = IrisPlatforms.get().dataFile("iris.json");
if (!s.exists()) {
try {
IO.writeAll(s, new JSONObject(new Gson().toJson(loaded)).toString(4));
} catch (JSONException | IOException e) {
e.printStackTrace();
IrisLogging.reportError(e);
}
@@ -106,32 +105,18 @@ public class IrisSettings {
loaded = parsed;
}
migrateLegacyKeys(loaded, ss);
try {
IO.writeAll(s, new JSONObject(new Gson().toJson(loaded)).toString(4));
} catch (IOException e) {
e.printStackTrace();
}
} catch (Throwable ee) {
// IrisLogging.reportError(ee); causes a self-reference & stackoverflow
IrisLogging.error("Configuration Error in settings.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage());
IrisLogging.error("Configuration Error in iris.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage());
}
return loaded;
}
private static void migrateLegacyKeys(IrisSettings target, String rawJson) {
JSONObject root = new JSONObject(rawJson);
JSONObject worldObject = root.optJSONObject("world");
if (worldObject == null || !worldObject.has("anbientEntitySpawningSystem")) {
return;
}
target.getWorld().setAmbientEntitySpawningSystem(worldObject.optBoolean("anbientEntitySpawningSystem", target.getWorld().isAmbientEntitySpawningSystem()));
IrisLogging.info("Migrated legacy settings key world.anbientEntitySpawningSystem -> world.ambientEntitySpawningSystem");
}
public static void invalidate() {
synchronized (SETTINGS_LOCK) {
settings = null;
@@ -169,7 +154,6 @@ public class IrisSettings {
if (parsed == null) {
throw new IllegalArgumentException("Iris settings snapshot did not contain an object");
}
migrateLegacyKeys(parsed, rawJson);
} catch (RuntimeException failure) {
throw new IllegalArgumentException("Iris settings snapshot is invalid", failure);
}
@@ -179,12 +163,11 @@ public class IrisSettings {
}
public void forceSave() {
File s = IrisPlatforms.get().dataFile("settings.json");
File s = IrisPlatforms.get().dataFile("iris.json");
try {
IO.writeAll(s, new JSONObject(new Gson().toJson(this)).toString(4));
} catch (JSONException | IOException e) {
e.printStackTrace();
IrisLogging.reportError(e);
}
}
@@ -321,17 +304,18 @@ public class IrisSettings {
@Data
public static class IrisSettingsGeneral {
public String language = "en_US";
public boolean metrics = true;
public boolean commandSounds = true;
public boolean debug = false;
public boolean dumpMantleOnError = false;
public boolean disableNMS = false;
public boolean pluginMetrics = true;
public boolean splashLogoStartup = true;
public boolean useConsoleCustomColors = true;
public boolean useCustomColorsIngame = true;
/**
* Boss bar progress loaders for jobs, studio opens, world creation, chunk jobs and pack
* downloads. Turning this off keeps the action bar progress line; only the bar goes away.
* Boss bars for jobs, Studio opens, chunk jobs, and pack downloads. Ordinary
* world creation uses only its action-bar lifecycle meter; creation-time
* pregeneration retains its dedicated long-running boss bar.
*/
public boolean progressBossBar = true;
public boolean adjustVanillaHeight = false;
@@ -56,6 +56,8 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.channels.FileChannel;
import java.nio.file.AtomicMoveNotSupportedException;
@@ -281,11 +283,7 @@ public class ServerConfigurator {
IrisLogging.error("Unable to install datapacks, fixer is null!");
return DatapackInstallResult.failedResult();
}
if (fullInstall) {
IrisLogging.info("Checking Data Packs...");
} else {
IrisLogging.debug("Checking Data Packs...");
}
IrisLogging.debug("Checking Data Packs...");
List<File> packRoots;
List<IrisGeneratorBinding> bindings;
try {
@@ -349,11 +347,7 @@ public class ServerConfigurator {
}
}
}
if (fullInstall) {
IrisLogging.info("Data Packs Setup!");
} else {
IrisLogging.debug("Data Packs Setup!");
}
IrisLogging.debug("Data Packs Setup!");
boolean verifiedRestartRequired = fullInstall && verifyDataPacksPost();
boolean restartRequired = fullInstall && (reapply.changed() || verifiedRestartRequired);
@@ -1026,12 +1020,21 @@ public class ServerConfigurator {
? "Iris startup validation requires a restart."
: reason.trim();
IrisLogging.warn(restartReason + " Restarting server before default worlds are loaded.");
boolean restartInvoked = false;
try {
Bukkit.restart();
} catch (Throwable failure) {
IrisLogging.reportError("Unable to restart the server at the Iris startup boundary.", failure);
restartInvoked = invokeImmediateRestartIfSupported(Bukkit.class);
} catch (ReflectiveOperationException | RuntimeException | LinkageError failure) {
Throwable cause = failure instanceof InvocationTargetException invocationFailure
&& invocationFailure.getCause() != null
? invocationFailure.getCause()
: failure;
IrisLogging.reportError("Unable to restart the server at the Iris startup boundary.", cause);
}
if (restartInvoked) {
IrisLogging.error("The immediate Iris startup restart returned unexpectedly; stopping the server instead.");
} else {
IrisLogging.warn("This server has no immediate restart API; stopping at the Iris startup boundary instead.");
}
IrisLogging.error("The immediate Iris startup restart returned unexpectedly; stopping the server instead.");
try {
Bukkit.shutdown();
} catch (Throwable failure) {
@@ -1039,6 +1042,17 @@ public class ServerConfigurator {
}
}
static boolean invokeImmediateRestartIfSupported(Class<?> bukkitApi) throws ReflectiveOperationException {
Method restartMethod;
try {
restartMethod = Objects.requireNonNull(bukkitApi, "Bukkit API class").getMethod("restart");
} catch (NoSuchMethodException ignored) {
return false;
}
restartMethod.invoke(null);
return true;
}
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
KSet<String> keys = new KSet<>();
boolean warn = false;
@@ -1056,11 +1070,9 @@ public class ServerConfigurator {
if (!INMS.get().supportsDataPacks()) {
if (!keys.isEmpty()) {
IrisLogging.warn("===================================================================================");
IrisLogging.warn("Pack " + key + " has " + keys.size() + " custom biome(s). ");
IrisLogging.warn("Your server version does not yet support datapacks for iris.");
IrisLogging.warn("The world will generate these biomes as backup biomes.");
IrisLogging.warn("====================================================================================");
}
return true;
@@ -92,7 +92,6 @@ public final class SettingsHotloadWatch implements AutoCloseable {
} catch (RuntimeException failure) {
IrisLogging.error("Iris settings and locale hotload watcher failed: " + failureDetail(failure));
IrisLogging.reportError(failure);
failure.printStackTrace();
}
}
}
@@ -113,7 +112,7 @@ public final class SettingsHotloadWatch implements AutoCloseable {
boolean missing = "missing".equals(snapshot.signature());
if (isSettingsFile(file)) {
if (missing) {
IrisLogging.warn("settings.json was removed; retaining the last valid runtime settings.");
IrisLogging.warn("iris.json was removed; retaining the last valid runtime settings.");
return true;
}
if (snapshot.normalizedContent() == null) {
@@ -210,7 +209,6 @@ public final class SettingsHotloadWatch implements AutoCloseable {
} catch (RuntimeException failure) {
IrisLogging.error("Rejected invalid settings hotload from " + file.getAbsolutePath() + ": " + failureDetail(failure));
IrisLogging.reportError(failure);
failure.printStackTrace();
return false;
}
}
@@ -221,7 +219,6 @@ public final class SettingsHotloadWatch implements AutoCloseable {
} catch (RuntimeException failure) {
IrisLogging.error("Rejected invalid locale hotload from " + file.getAbsolutePath() + ": " + failureDetail(failure));
IrisLogging.reportError(failure);
failure.printStackTrace();
return false;
}
}
@@ -251,7 +248,6 @@ public final class SettingsHotloadWatch implements AutoCloseable {
}
IrisLogging.error("Failed to read watched Iris file " + path + ": " + failureDetail(failure));
IrisLogging.reportError(failure);
failure.printStackTrace();
}
private void clearCaptureFailure(File file) {
@@ -262,12 +258,12 @@ public final class SettingsHotloadWatch implements AutoCloseable {
File file = delta.file();
if (isSettingsFile(file)) {
if (delta.after() != null) {
IrisLogging.info("Hotloaded settings.json");
IrisLogging.debug("Hotloaded iris.json");
}
return;
}
if (IrisLanguage.isActiveOverrideFile(file)) {
IrisLogging.info("Hotloaded locale override " + file.getName());
IrisLogging.debug("Hotloaded locale override " + file.getName());
}
}
@@ -983,8 +983,8 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
notifyUser(IrisLanguage.plain(DesktopUiMessages.VISION_NO_PLAYER));
return;
}
int worldX = (int) screenToWorldX(point.x);
int worldZ = (int) screenToWorldZ(point.y);
int worldX = floorWorldCoordinate(screenToWorldX(point.x));
int worldZ = floorWorldCoordinate(screenToWorldZ(point.y));
overlay.teleport(worldX, worldZ);
notifyUser(IrisLanguage.plain(
DesktopUiMessages.VISION_TELEPORTING,
@@ -1009,6 +1009,10 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
controller.close();
}
static int floorWorldCoordinate(double coordinate) {
return (int) StrictMath.floor(coordinate);
}
private static String modeName(RenderType type) {
return IrisLanguage.plain(modeKey(type));
}
@@ -944,7 +944,7 @@ public final class IrisWorldRemovalService {
PlatformChunkGenerator generator = null;
if (world != null) {
Path loadedDirectory = world.getWorldFolder().toPath().toAbsolutePath().normalize();
WorldRemovalPathPolicy.validateStoragePath(target.levelRoot(), target.worldKey(), loadedDirectory);
WorldRemovalPathPolicy.validateStorageRoot(target.levelRoot(), target.worldKey(), loadedDirectory);
generator = IrisToolbelt.access(world);
}
boolean registryManaged = IrisWorlds.get().getWorlds().containsKey(target.worldKey().toString());
@@ -208,9 +208,11 @@ public final class WorldLifecycleService {
return worldsProviderBackend;
}
if (request.studio() && capabilities.serverFamily().isPaperLike()) {
if (capabilities.regionizedRuntime()
|| capabilities.serverFamily() == ServerFamily.FOLIA
|| (request.studio() && capabilities.serverFamily().isPaperLike())) {
if (!paperLikeRuntimeBackend.supports(request, capabilities)) {
throw new IllegalStateException("World lifecycle backend paper_like_runtime is unavailable for studio create on "
throw new IllegalStateException("World lifecycle backend paper_like_runtime is unavailable for runtime create on "
+ capabilities.serverFamily().id() + ": " + capabilities.paperLikeResolution());
}
return paperLikeRuntimeBackend;
@@ -10,7 +10,6 @@ import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
@@ -472,12 +471,8 @@ final class WorldLifecycleSupport {
) {
String worldName = world.getName();
try {
try {
if (capabilities.minecraftServer() == null || capabilities.removeLevelMethod() == null) {
return CompletableFuture.completedFuture(Bukkit.unloadWorld(world, save));
} catch (UnsupportedOperationException unsupported) {
if (capabilities.minecraftServer() == null || capabilities.removeLevelMethod() == null) {
return CompletableFuture.failedFuture(unsupported);
}
}
if (!announceManualWorldUnload(world)) {
@@ -489,8 +484,9 @@ final class WorldLifecycleSupport {
}
Method getHandleMethod = world.getClass().getMethod("getHandle");
Object serverLevel = getHandleMethod.invoke(world);
CompletableFuture<Boolean> operation = closeServerLevelAsync(world, serverLevel)
.thenCompose(unused -> detachServerLevelAsync(capabilities, serverLevel, world))
CompletableFuture<Boolean> operation = detachServerLevelAsync(capabilities, serverLevel, world)
.thenCompose(unused -> drainChunkTasksAsync(world, serverLevel))
.thenCompose(unused -> closeServerLevelAsync(world, serverLevel))
.thenApply(unused -> WorldIdentity.resolve(WorldIdentity.key(world)).isEmpty());
return contextualizeUnloadFailure(worldName, operation);
} catch (Throwable e) {
@@ -527,7 +523,13 @@ final class WorldLifecycleSupport {
boolean save
) {
CompletableFuture<Boolean> callbackFuture = new CompletableFuture<>();
Consumer<Boolean> callback = unloaded -> callbackFuture.complete(Boolean.TRUE.equals(unloaded));
Consumer<Object> callback = unloaded -> {
try {
callbackFuture.complete(asyncUnloadSucceeded(unloaded));
} catch (Throwable failure) {
callbackFuture.completeExceptionally(unwrap(failure));
}
};
try {
unloadWorldAsyncMethod.invoke(bukkitServer, world, save, callback);
} catch (Throwable e) {
@@ -536,6 +538,17 @@ final class WorldLifecycleSupport {
return callbackFuture;
}
private static boolean asyncUnloadSucceeded(Object result) throws ReflectiveOperationException {
if (result instanceof Boolean unloaded) {
return unloaded;
}
if (result == null) {
return false;
}
Method isSuccessMethod = result.getClass().getMethod("isSuccess");
return Boolean.TRUE.equals(isSuccessMethod.invoke(result));
}
private static CompletableFuture<Boolean> contextualizeUnloadFailure(
String worldName,
CompletableFuture<Boolean> operation
@@ -569,33 +582,59 @@ final class WorldLifecycleSupport {
return CompletableFuture.completedFuture(null);
}
if (!J.isFolia()) {
Runnable closeTask = () -> {
try {
closeMethod.invoke(serverLevel);
return CompletableFuture.completedFuture(null);
} catch (Throwable e) {
return CompletableFuture.failedFuture(unwrap(e));
throw new RuntimeException(unwrap(e));
}
};
return runGlobalAsync(closeTask).orTimeout(90L, TimeUnit.SECONDS);
}
private static CompletableFuture<Void> drainChunkTasksAsync(World world, Object serverLevel) {
Method schedulerMethod;
try {
schedulerMethod = CapabilityResolution.resolveMethod(
serverLevel.getClass(),
"moonrise$getChunkTaskScheduler",
method -> method.getParameterCount() == 0
);
} catch (Throwable e) {
return CompletableFuture.failedFuture(unwrap(e));
}
if (schedulerMethod == null) {
return CompletableFuture.completedFuture(null);
}
Location spawn = world.getSpawnLocation();
int chunkX = spawn == null ? 0 : spawn.getBlockX() >> 4;
int chunkZ = spawn == null ? 0 : spawn.getBlockZ() >> 4;
CompletableFuture<Void> closeFuture = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
return J.afut(() -> {
try {
closeMethod.invoke(serverLevel);
closeFuture.complete(null);
Object scheduler = schedulerMethod.invoke(serverLevel);
Method haltMethod = CapabilityResolution.resolveMethod(
scheduler.getClass(),
"halt",
method -> {
Class<?>[] parameters = method.getParameterTypes();
return parameters.length == 2
&& boolean.class.equals(parameters[0])
&& long.class.equals(parameters[1]);
}
);
if (haltMethod == null) {
return;
}
Object halted = haltMethod.invoke(
scheduler,
true,
TimeUnit.SECONDS.toNanos(90L));
if (halted instanceof Boolean complete && !complete) {
throw new IllegalStateException(
"Chunk scheduler drain timed out for world \"" + world.getName() + "\".");
}
} catch (Throwable e) {
closeFuture.completeExceptionally(unwrap(e));
throw new RuntimeException(unwrap(e));
}
});
if (!scheduled) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Failed to schedule region close task for world \"" + world.getName() + "\"."
));
}
return closeFuture.orTimeout(90L, TimeUnit.SECONDS);
}).orTimeout(90L, TimeUnit.SECONDS);
}
@SuppressWarnings({"rawtypes", "unchecked"})
@@ -47,7 +47,6 @@ public class WorldEditLink {
} catch (Throwable e) {
if (errorThrottle.flip()) {
IrisLogging.error("Could not get selection");
e.printStackTrace();
IrisLogging.reportError(e);
}
invalidate();
@@ -14,6 +14,7 @@ import art.arcane.iris.core.nms.container.BlockProperty;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.data.IrisCustomData;
import org.bukkit.block.Block;
@@ -82,7 +83,7 @@ public class NexoDataProvider extends ExternalDataProvider {
try {
return builder.build();
} catch (Exception e) {
e.printStackTrace();
IrisLogging.reportError("Failed to build Nexo item data for " + itemId + ".", e);
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
}
}
@@ -278,7 +278,6 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
return null;
@@ -450,7 +449,6 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return r;
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
IrisLogging.error("Failed to create loader! " + registrant.getCanonicalName());
}
@@ -561,7 +559,6 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
));
}
IrisLogging.reportError(failure);
failure.printStackTrace();
throw failure;
}
@@ -730,7 +727,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
.map(s -> s.split("\\Q.\\E")[0])
.forEach(s -> l.add("snippet/" + s));
} catch (Throwable e) {
e.printStackTrace();
IrisLogging.reportError("Failed to scan Iris snippets in " + snippetFolder + ".", e);
}
return l;
@@ -183,10 +183,6 @@ public final class BukkitCommandMessagesExtended {
"iris.bukkit.commandiris.install_pack_and_restart",
C.YELLOW + "Install it with " + C.AQUA + "{command}" + C.YELLOW + " and restart the server."
);
public static final TextKey COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD = TextKey.of(
"iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load",
C.GREEN + "World staging completed. Iris is restarting the server to generate/load \"" + "{worldName}" + "\"."
);
public static final TextKey COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS = TextKey.of(
"iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details",
C.RED + "Exception raised during creation. See the console for more details."
@@ -195,26 +191,6 @@ public final class BukkitCommandMessagesExtended {
"iris.bukkit.commandiris.successfully_created_your_world",
C.GREEN + "Successfully created your world!"
);
public static final TextKey COMMAND_IRIS_RUNTIME_WORLD_CREATION_IS_DISABLED_ON_FOLIA = TextKey.of(
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia",
C.YELLOW + "Runtime world creation is disabled on Folia."
);
public static final TextKey COMMAND_IRIS_PREPARING_WORLD_FILES_BUKKIT_YML_NEXT_STARTUP = TextKey.of(
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup",
C.YELLOW + "Preparing world files and bukkit.yml for next startup..."
);
public static final TextKey COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION = TextKey.of(
"iris.bukkit.commandiris.failed_stage_world_files_dimension",
C.RED + "Failed to stage world files for dimension \"" + "{value}" + "\"."
);
public static final TextKey COMMAND_IRIS_STAGED_IRIS_WORLD_WITH_GENERATOR_IRIS_SEED = TextKey.of(
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed",
C.GREEN + "Staged Iris world \"" + "{name}" + "\" with generator Iris:" + "{value}" + " and seed " + "{seed}" + "."
);
public static final TextKey COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML = TextKey.of(
"iris.bukkit.commandiris.failed_update_bukkit_yml",
C.RED + "Failed to update bukkit.yml: " + "{value}"
);
public static final TextKey COMMAND_IRIS_SPECIFIED_PLAYER_DOES_NOT_EXIST = TextKey.of(
"iris.bukkit.commandiris.specified_player_does_not_exist",
C.RED + "The specified player does not exist."
@@ -327,10 +303,6 @@ public final class BukkitCommandMessagesExtended {
"iris.bukkit.commandiris.loading_world",
C.GREEN + "Loading world: " + "{logicalWorldName}"
);
public static final TextKey COMMAND_IRIS_FOLIA_CANNOT_LOAD_NEW_WORLDS_AT_RUNTIME_RESTART_SERVER_LOAD = TextKey.of(
"iris.bukkit.commandiris.folia_cannot_load_new_worlds_at_runtime_restart_server_load",
C.YELLOW + "Folia cannot load new worlds at runtime. Restart the server to load \"" + "{logicalWorldName}" + "\"."
);
public static final TextKey COMMAND_IRIS_LOADED_SUCCESSFULLY = TextKey.of(
"iris.bukkit.commandiris.loaded_successfully",
C.GREEN + "{logicalWorldName}" + " loaded successfully."
@@ -874,14 +846,8 @@ public final class BukkitCommandMessagesExtended {
COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND,
COMMAND_IRIS_DIMENSION_NOT_FOUND,
COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD,
COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS,
COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD,
COMMAND_IRIS_RUNTIME_WORLD_CREATION_IS_DISABLED_ON_FOLIA,
COMMAND_IRIS_PREPARING_WORLD_FILES_BUKKIT_YML_NEXT_STARTUP,
COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION,
COMMAND_IRIS_STAGED_IRIS_WORLD_WITH_GENERATOR_IRIS_SEED,
COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML,
COMMAND_IRIS_SPECIFIED_PLAYER_DOES_NOT_EXIST,
COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE,
COMMAND_IRIS_TO,
@@ -910,7 +876,6 @@ public final class BukkitCommandMessagesExtended {
COMMAND_IRIS_IS_NOT_IRIS_WORLD,
COMMAND_IRIS_COULD_NOT_DETERMINE_IRIS_DIMENSION,
COMMAND_IRIS_LOADING_WORLD,
COMMAND_IRIS_FOLIA_CANNOT_LOAD_NEW_WORLDS_AT_RUNTIME_RESTART_SERVER_LOAD,
COMMAND_IRIS_LOADED_SUCCESSFULLY,
COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_3,
COMMAND_IRIS_EVACUATING_WORLD,
@@ -406,10 +406,6 @@ public final class BukkitRuntimeMessages {
"iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details",
C.RED + "Some schematics failed to convert. Check the console for details."
);
public static final TextKey STUDIO_S_V_C_INSTALLING_PACKAGE = TextKey.of(
"iris.bukkit.runtime.studiosvc.installing_package",
C.GOLD + "World pack " + C.AQUA + "{name}" + ":" + "{loadKey}" + C.GRAY + " | " + C.WHITE + "Publishing snapshot"
);
public static final TextKey STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD = TextKey.of(
"iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread",
C.RED + "Iris refused to copy the world pack on the Bukkit primary thread."
@@ -708,7 +704,6 @@ public final class BukkitRuntimeMessages {
IRIS_CONVERTER_FAILED_CONVERT,
IRIS_CONVERTER_CONVERTED_3,
IRIS_CONVERTER_SOME_SCHEMATICS_FAILED_CONVERT_CHECK_CONSOLE_DETAILS,
STUDIO_S_V_C_INSTALLING_PACKAGE,
STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD,
STUDIO_S_V_C_PACK_INSTALL_FAILED,
STUDIO_S_V_C_LOOKING_PACKAGE,
@@ -130,7 +130,6 @@ public final class IrisLanguage {
dataFolder = resolvedRoot;
IrisLogging.error("Rejected locale setting '" + locale + "'; continuing with " + activeLocale + ".");
IrisLogging.reportError(exception);
exception.printStackTrace();
return false;
}
@@ -159,7 +158,6 @@ public final class IrisLanguage {
} catch (RuntimeException exception) {
IrisLogging.error("Rejected locale setting '" + configured + "'; continuing with " + activeLocale + ".");
IrisLogging.reportError(exception);
exception.printStackTrace();
return false;
}
@@ -214,7 +212,7 @@ public final class IrisLanguage {
activeLocale = requestedLocale;
int warnings = result.validation().warnings().size();
IrisLogging.info("Loaded locale " + requestedLocale + " with " + warnings + " fallback "
IrisLogging.debug("Loaded locale " + requestedLocale + " with " + warnings + " fallback "
+ (warnings == 1 ? "entry" : "entries") + ".");
return true;
}
@@ -228,7 +226,6 @@ public final class IrisLanguage {
+ failure.getClass().getSimpleName()
+ (failure.getMessage() == null ? "" : " - " + failure.getMessage()));
IrisLogging.reportError(failure);
failure.printStackTrace();
}
}
}
@@ -641,7 +638,6 @@ public final class IrisLanguage {
}
if (result.failure() != null) {
IrisLogging.reportError(result.failure());
result.failure().printStackTrace();
}
}
@@ -1043,13 +1043,9 @@ public final class ModdedCommandMessages {
"iris.modded.moddedstudiocommands.creating_project_from_template",
"Creating project '" + "{name}" + "' from template '" + "{template}" + "'..."
);
public static final TextKey MODDED_STUDIO_COMMANDS_TEMPLATE_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS = TextKey.of(
"iris.modded.moddedstudiocommands.template_is_not_installed_downloading_irisdimensions",
"Template '" + "{template}" + "' is not installed; downloading IrisDimensions/" + "{template2}" + "..."
);
public static final TextKey MODDED_STUDIO_COMMANDS_TEMPLATE_COULD_NOT_BE_DOWNLOADED_INSTALL_PACK_WITH_DIMENSIONS_JSON = TextKey.of(
"iris.modded.moddedstudiocommands.template_could_not_be_downloaded_install_pack_with_dimensions_json",
"Template '" + "{template}" + "' could not be downloaded; install a pack with dimensions/" + "{template2}" + ".json first."
public static final TextKey MODDED_STUDIO_COMMANDS_REQUIRED_PACK_IS_NOT_INSTALLED_INSTALL_THEN_RESTART = TextKey.of(
"iris.modded.moddedstudiocommands.required_pack_is_not_installed_install_then_restart",
"Required pack '" + "{pack}" + "' is not installed. Install it with /iris download, then restart."
);
public static final TextKey MODDED_STUDIO_COMMANDS_CREATED_PROJECT_AT = TextKey.of(
"iris.modded.moddedstudiocommands.created_project_at",
@@ -1095,13 +1091,9 @@ public final class ModdedCommandMessages {
"iris.modded.moddedstudiocommands.region_sampling_failed",
"Region sampling failed: " + "{value}"
);
public static final TextKey MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS = TextKey.of(
"iris.modded.moddedworldcommands.pack_is_not_installed_downloading_irisdimensions",
"Pack '" + "{pack}" + "' is not installed; downloading IrisDimensions/" + "{pack2}" + "..."
);
public static final TextKey MODDED_WORLD_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_INSTALL_IT_WITH = TextKey.of(
"iris.modded.moddedworldcommands.pack_could_not_be_downloaded_check_name_install_it_with",
"Pack '" + "{pack}" + "' could not be downloaded; check the name or install it with /iris download " + "{pack2}" + "."
public static final TextKey MODDED_WORLD_COMMANDS_REQUIRED_PACK_IS_NOT_INSTALLED_INSTALL_THEN_RESTART = TextKey.of(
"iris.modded.moddedworldcommands.required_pack_is_not_installed_install_then_restart",
"Required pack '" + "{pack}" + "' is not installed. Install it with /iris download, then restart."
);
public static final TextKey MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_WORLD = TextKey.of(
"iris.modded.moddedworldcommands.failed_inject_iris_world",
@@ -1139,14 +1131,6 @@ public final class ModdedCommandMessages {
"iris.modded.moddedworldcommands.invalid_seed_use_number_random",
"Invalid seed '" + "{seedRaw}" + "'. Use a number or 'random'."
);
public static final TextKey MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS_2 = TextKey.of(
"iris.modded.moddedworldcommands.pack_is_not_installed_downloading_irisdimensions_2",
"Pack '" + "{pack}" + "' is not installed; downloading IrisDimensions/" + "{pack2}" + "..."
);
public static final TextKey MODDED_WORLD_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_INSTALL_IT_WITH_2 = TextKey.of(
"iris.modded.moddedworldcommands.pack_could_not_be_downloaded_check_name_install_it_with_2",
"Pack '" + "{pack}" + "' could not be downloaded; check the name or install it with /iris download " + "{pack2}" + "."
);
public static final TextKey MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND = TextKey.of(
"iris.modded.moddedworldcommands.pack_is_not_ready_yet_still_loading_validating_try_command",
"Pack '" + "{pack}" + "' is not ready yet (still loading or validating). Try the command again in a moment."
@@ -1512,8 +1496,7 @@ public final class ModdedCommandMessages {
MODDED_STUDIO_COMMANDS_INVALID_PROJECT_NAME_ALLOWED_Z_0_9,
MODDED_STUDIO_COMMANDS_PACK_ALREADY_EXISTS_AT,
MODDED_STUDIO_COMMANDS_CREATING_PROJECT_FROM_TEMPLATE,
MODDED_STUDIO_COMMANDS_TEMPLATE_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS,
MODDED_STUDIO_COMMANDS_TEMPLATE_COULD_NOT_BE_DOWNLOADED_INSTALL_PACK_WITH_DIMENSIONS_JSON,
MODDED_STUDIO_COMMANDS_REQUIRED_PACK_IS_NOT_INSTALLED_INSTALL_THEN_RESTART,
MODDED_STUDIO_COMMANDS_CREATED_PROJECT_AT,
MODDED_STUDIO_COMMANDS_EDIT_DIMENSIONS_JSON_REST_PACK_VSCODE_WORKSPACE_WITH_JSON_SCHEMA,
MODDED_STUDIO_COMMANDS_PROJECT_CREATION_FAILED,
@@ -1525,8 +1508,7 @@ public final class ModdedCommandMessages {
MODDED_STUDIO_COMMANDS_SAMPLING_REGION_DISTRIBUTION_X_CHUNKS_AROUND_YOU,
MODDED_STUDIO_COMMANDS_RARITY,
MODDED_STUDIO_COMMANDS_REGION_SAMPLING_FAILED,
MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS,
MODDED_WORLD_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_INSTALL_IT_WITH,
MODDED_WORLD_COMMANDS_REQUIRED_PACK_IS_NOT_INSTALLED_INSTALL_THEN_RESTART,
MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_WORLD,
MODDED_WORLD_COMMANDS_CREATED_IRIS_WORLD_FROM_PACK_DIMENSION_SEED,
MODDED_WORLD_COMMANDS_IT_IS_LIVE_NOW_RE_INJECTED_ON_EVERY_STARTUP_TELEPORT,
@@ -1536,8 +1518,6 @@ public final class ModdedCommandMessages {
MODDED_WORLD_COMMANDS_INSTEAD_IS_NOW_CONFIGURED_PRIMARY_WORLD_PLAYERS_VANILLA_OVERWORLD_ARE,
MODDED_WORLD_COMMANDS_IRIS_MAIN_WORLD_OVERRIDE_CLEARED_OVERWORLD_KEEPS_ITS_CURRENT_GENERATOR,
MODDED_WORLD_COMMANDS_INVALID_SEED_USE_NUMBER_RANDOM,
MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS_2,
MODDED_WORLD_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_INSTALL_IT_WITH_2,
MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND,
MODDED_WORLD_COMMANDS_FAILED_WRITE_SERVER_PROPERTIES_CHECK_FILE_PERMISSIONS_SET_LEVEL_TYPE,
MODDED_WORLD_COMMANDS_IRIS_MAIN_WORLD_SET_PRESET_SEED,
@@ -36,11 +36,11 @@ public final class ModdedHelpMessages {
);
public static final TextKey COMMAND_DEBUG_TOGGLE_IRIS_DEBUG_LOGGING_AND_SAVE_SETTINGS_JSON = TextKey.of(
"iris.modded.help.entry.command.debug",
"Toggle Iris debug logging and save settings.json"
"Toggle Iris debug logging and save iris.json"
);
public static final TextKey COMMAND_RELOAD_RELOAD_SETTINGS_JSON_ALSO_HOTLOADED_AUTOMATICALLY_EVERY_3S = TextKey.of(
"iris.modded.help.entry.command.reload",
"Reload settings.json (also hotloaded automatically every 3s)"
"Reload iris.json (also hotloaded automatically every 3s)"
);
public static final TextKey COMMAND_DOWNLOAD_DOWNLOAD_A_PACK_PROJECT = TextKey.of(
"iris.modded.help.entry.command.download",
@@ -25,16 +25,10 @@ public final class RuntimeProgressMessages {
public static final TextKey STUDIO_STAGE_CREATE_WORLD = TextKey.of("iris.runtime.studio.stage.create_world", "Creating world");
public static final TextKey STUDIO_STAGE_APPLY_WORLD_RULES = TextKey.of("iris.runtime.studio.stage.apply_world_rules", "Applying world rules");
public static final TextKey STUDIO_STAGE_PREPARE_GENERATOR = TextKey.of("iris.runtime.studio.stage.prepare_generator", "Preparing generator");
public static final TextKey STUDIO_STAGE_REQUEST_ENTRY_CHUNK = TextKey.of("iris.runtime.studio.stage.request_entry_chunk", "Loading entry chunk");
public static final TextKey STUDIO_STAGE_RESOLVE_SAFE_ENTRY = TextKey.of("iris.runtime.studio.stage.resolve_safe_entry", "Finding safe spawn");
public static final TextKey STUDIO_STAGE_TELEPORT_PLAYER = TextKey.of("iris.runtime.studio.stage.teleport_player", "Teleporting");
public static final TextKey STUDIO_STAGE_FINALIZE_OPEN = TextKey.of("iris.runtime.studio.stage.finalize_open", "Finalizing");
public static final TextKey STUDIO_STAGE_CLEANUP = TextKey.of("iris.runtime.studio.stage.cleanup", "Cleaning up");
public static final TextKey WORLD_CREATE_TELEPORT_FAILED = TextKey.of("iris.runtime.world_create.teleport_failed", C.YELLOW + "The world was created, but automatic teleport failed. Try /iris teleport world={world}");
public static final TextKey WORLD_CREATE_BOSSBAR_WORKING = TextKey.of("iris.runtime.world_create.bossbar.working", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.WHITE + "Starting");
public static final TextKey WORLD_CREATE_BOSSBAR_PROGRESS = TextKey.of("iris.runtime.world_create.bossbar.progress", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.YELLOW + "{percent}% " + C.WHITE + "{stage}");
public static final TextKey WORLD_CREATE_BOSSBAR_FAILED = TextKey.of("iris.runtime.world_create.bossbar.failed", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.RED + "FAILED " + C.DARK_GRAY + "{percent}%");
public static final TextKey WORLD_CREATE_BOSSBAR_READY = TextKey.of("iris.runtime.world_create.bossbar.ready", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.GREEN + "READY 100%");
public static final TextKey WORLD_CREATE_LIFECYCLE_ACTION = TextKey.of("iris.runtime.world_create.lifecycle.action", "{bar}" + C.GRAY + " " + C.YELLOW + "{percent}%" + C.GRAY + " | " + C.WHITE + "{stage}{detail}" + C.DARK_GRAY + " {elapsed}");
public static final TextKey WORLD_CREATE_LIFECYCLE_ACTION_FAILED = TextKey.of("iris.runtime.world_create.lifecycle.action.failed", "{bar}" + C.GRAY + " " + C.RED + "FAILED" + C.GRAY + " | " + C.WHITE + "{stage}{detail}" + C.DARK_GRAY + " {elapsed}");
public static final TextKey WORLD_CREATE_LIFECYCLE_ACTION_READY = TextKey.of("iris.runtime.world_create.lifecycle.action.ready", "{bar}" + C.GRAY + " " + C.GREEN + "100%" + C.GRAY + " | " + C.GREEN + "World ready" + C.DARK_GRAY + " {elapsed}");
@@ -49,7 +43,7 @@ public final class RuntimeProgressMessages {
public static final TextKey WORLD_CREATE_STAGE_PREPARE_GENERATOR = TextKey.of("iris.runtime.world_create.stage.prepare_generator", "Preparing generator");
public static final TextKey WORLD_CREATE_STAGE_CREATE_WORLD = TextKey.of("iris.runtime.world_create.stage.create_world", "Generating spawn");
public static final TextKey WORLD_CREATE_STAGE_REGISTER_WORLD = TextKey.of("iris.runtime.world_create.stage.register_world", "Registering world");
public static final TextKey WORLD_CREATE_STAGE_TELEPORT_PLAYER = TextKey.of("iris.runtime.world_create.stage.teleport_player", "Finding safe entry");
public static final TextKey WORLD_CREATE_STAGE_TELEPORT_PLAYER = TextKey.of("iris.runtime.world_create.stage.teleport_player", "Entering world");
public static final TextKey WORLD_CREATE_STAGE_PREGENERATE = TextKey.of("iris.runtime.world_create.stage.pregenerate", "Pregenerating");
public static final TextKey WORLD_CREATE_STAGE_FINALIZE = TextKey.of("iris.runtime.world_create.stage.finalize", "Finalizing");
public static final TextKey WORLD_CREATE_STAGE_COMPLETE = TextKey.of("iris.runtime.world_create.stage.complete", "World ready");
@@ -124,16 +118,10 @@ public final class RuntimeProgressMessages {
STUDIO_STAGE_CREATE_WORLD,
STUDIO_STAGE_APPLY_WORLD_RULES,
STUDIO_STAGE_PREPARE_GENERATOR,
STUDIO_STAGE_REQUEST_ENTRY_CHUNK,
STUDIO_STAGE_RESOLVE_SAFE_ENTRY,
STUDIO_STAGE_TELEPORT_PLAYER,
STUDIO_STAGE_FINALIZE_OPEN,
STUDIO_STAGE_CLEANUP,
WORLD_CREATE_TELEPORT_FAILED,
WORLD_CREATE_BOSSBAR_WORKING,
WORLD_CREATE_BOSSBAR_PROGRESS,
WORLD_CREATE_BOSSBAR_FAILED,
WORLD_CREATE_BOSSBAR_READY,
WORLD_CREATE_LIFECYCLE_ACTION,
WORLD_CREATE_LIFECYCLE_ACTION_FAILED,
WORLD_CREATE_LIFECYCLE_ACTION_READY,
@@ -70,7 +70,7 @@ public class INMS {
private static INMSBinding bind() {
boolean disableNms = IrisSettings.get().getGeneral().isDisableNMS();
if (disableNms) {
IrisLogging.info("Craftbukkit BUKKIT <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound");
IrisLogging.debug("Craftbukkit BUKKIT <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound");
IrisLogging.warn("NMS support is disabled. Iris world creation is unavailable until general.disableNMS=false.");
return new NMSBinding1X();
}
@@ -87,7 +87,6 @@ public class INMS {
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to determine server minecraft version!");
e.printStackTrace();
if (e instanceof IllegalStateException illegalStateException) {
throw illegalStateException;
}
@@ -96,19 +95,18 @@ public class INMS {
}
private static INMSBinding bindExact(String code) {
IrisLogging.info("Locating exact NMS Binding for " + code);
IrisLogging.debug("Locating exact NMS Binding for " + code);
try {
Class<?> clazz = Class.forName("art.arcane.iris.core.nms." + code + ".NMSBinding");
Object candidate = clazz.getConstructor().newInstance();
if (candidate instanceof INMSBinding binding) {
IrisLogging.info("Craftbukkit " + code + " <-> " + candidate.getClass().getSimpleName() + " Successfully Bound");
IrisLogging.debug("Craftbukkit " + code + " <-> " + candidate.getClass().getSimpleName() + " Successfully Bound");
return binding;
}
throw new IllegalStateException("Exact NMS binding class for " + code
+ " does not implement " + INMSBinding.class.getName());
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
if (e instanceof IllegalStateException illegalStateException) {
throw illegalStateException;
}
@@ -54,8 +54,6 @@ import org.bukkit.generator.ChunkGenerator;
import org.bukkit.inventory.ItemStack;
import java.awt.Color;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -272,14 +270,6 @@ public interface INMSBinding {
default void uninjectBukkit() {
}
default void writeCurrentPaperWorldData(
Path sourceWorldDirectory,
Path targetWorldDirectory,
long seed
) throws IOException {
throw new UnsupportedOperationException("The active NMS binding does not support current Paper world data staging.");
}
default boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) {
return true;
}
@@ -251,7 +251,7 @@ public final class ContentKeyValidator {
/**
* Whether unresolved pack content keys are blocking errors instead of warnings. Enabled by
* {@code -Diris.strictContent} or {@code general.strictContentKeys} in settings.json; the system property wins.
* {@code -Diris.strictContent} or {@code general.strictContentKeys} in iris.json; the system property wins.
*/
public static boolean strictContent() {
String property = System.getProperty(STRICT_PROPERTY);
@@ -223,7 +223,6 @@ public class IrisPack {
IO.writeAll(ws, generateWorkspaceConfig());
} catch (IOException e1) {
IrisLogging.reportError(e1);
e1.printStackTrace();
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.core.pack;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.volmlib.util.format.Form;
@@ -126,7 +127,7 @@ public class IrisPackRepository {
try {
FileUtils.copyDirectory(work.listFiles()[0], pack);
} catch (IOException e) {
e.printStackTrace();
IrisLogging.reportError("Failed to install Iris pack into " + pack + ".", e);
}
})).execute(sender, whenComplete);
} else {
@@ -94,7 +94,15 @@ final class PackRiverValidator {
validateWater(path + ".water", water, errors);
}
if (biomes != null) {
validateBiomePools(packFolder, path + ".biomes", biomes, false, errors, warnings);
validateBiomePools(
packFolder,
path + ".biomes",
biomes,
false,
usesOverworldNativeStructureRoles(context),
errors,
warnings
);
}
boolean sinkholeTerminal = terrain != null
&& "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL"));
@@ -108,7 +116,7 @@ final class PackRiverValidator {
if (Double.isFinite(meanderStrength) && meanderStrength > cellSize) {
warnings.add(path + ".terrain.meanderStrength exceeds topology.cellSize; reaches may require large cache halos.");
}
validateTopologyComplexity(path, topology, terrain, errors);
validateTopologyComplexity(packFolder, path, topology, terrain, errors);
}
if (sinkholeTerminal && caves != null) {
validateSinkholeCapability(
@@ -130,8 +138,14 @@ final class PackRiverValidator {
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "minimumSourcesPerTile", 0, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "sinkSearchReaches", 0, 7, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "routingBasinCells", 8, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "routingDeviationScaleCells", 8, 256, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingDeviationStrengthCells", 0D, 32D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingPlateauHeight", 1D, 64D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingNoiseWeight", 0D, 1024D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "flowAlignmentWeight", 0D, 1024D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "confluenceWeight", 0D, 1024D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "branchSoftCap", 1, 8, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "branchChildShrinkFactor", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainHeightWeight", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainSlopeWeight", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "oceanAttraction", 0D, 16D, errors);
@@ -155,6 +169,7 @@ final class PackRiverValidator {
validateStyledRange(packFolder, terrain, "channelWidth", path, 1D, 2048D, errors, warnings);
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, "maxChannelWidth", 1D, 2048D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxBankWidth", 0D, 2048D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxDepth", 1D, 512D, errors);
@@ -162,6 +177,9 @@ final class PackRiverValidator {
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "orderDepthFactor", 0D, 8D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "maxIncision", 0, 512, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bankExponent", 0.125D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelMouthBlend", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelFloorVariation", 0D, 8D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelRoofVariation", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "meanderStrength", 0D, 1024D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "meanderSubdivisions", 1, 64, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bedRoughness", 0D, 8D, errors);
@@ -169,8 +187,32 @@ final class PackRiverValidator {
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "terminalTaper", 8, 1024, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "dryContinuationChance", 0D, 1D, errors);
validateNoiseChance(packFolder, terrain, "incision", path, errors);
validateStyle(packFolder, terrain, "tunnelFloorStyle", path, errors);
validateStyle(packFolder, terrain, "tunnelRoofStyle", path, errors);
validateStyle(packFolder, terrain, "meanderStyle", path, errors);
validateStyle(packFolder, terrain, "bedRoughnessStyle", path, errors);
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
double maximumTunnelWidthMultiplier = styledRangeMaximum(
packFolder,
terrain,
"tunnelWidthMultiplier",
path,
1D
);
double tunnelMouthBlend = doubleValue(terrain, "tunnelMouthBlend", 2D);
if (Double.isFinite(maximumChannelWidth) && maximumChannelWidth >= 1D && maximumChannelWidth <= 2048D
&& Double.isFinite(maximumTunnelWidthMultiplier)
&& maximumTunnelWidthMultiplier >= 1D && maximumTunnelWidthMultiplier <= 8D
&& Double.isFinite(tunnelMouthBlend) && tunnelMouthBlend >= 0D && tunnelMouthBlend <= 16D) {
String violation = RiverTopologyComplexity.tunnelPlanViolation(
maximumChannelWidth,
maximumTunnelWidthMultiplier,
tunnelMouthBlend
);
if (violation != null) {
errors.add(path + " exceeds the safe derived hydrology budget. " + violation);
}
}
}
private static void validateWater(String path, JSONObject water, List<String> errors) {
@@ -188,6 +230,7 @@ final class PackRiverValidator {
}
private static void validateTopologyComplexity(
File packFolder,
String path,
JSONObject topology,
JSONObject terrain,
@@ -201,6 +244,14 @@ final class PackRiverValidator {
int meanderSubdivisions = integerValue(terrain, "meanderSubdivisions", 8);
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
double maximumBankWidth = doubleValue(terrain, "maxBankWidth", 4D);
double maximumTunnelWidthMultiplier = styledRangeMaximum(
packFolder,
terrain,
"tunnelWidthMultiplier",
path,
1D
);
double tunnelMouthBlend = doubleValue(terrain, "tunnelMouthBlend", 2D);
if (cellSize < 64 || cellSize > 4096
|| tileCells < 1 || tileCells > 64
|| !Double.isFinite(siteJitter) || siteJitter < 0D || siteJitter > 0.49D
@@ -208,10 +259,16 @@ final class PackRiverValidator {
|| !Double.isFinite(meanderStrength) || meanderStrength < 0D || meanderStrength > 1024D
|| meanderSubdivisions < 1 || meanderSubdivisions > 64
|| !Double.isFinite(maximumChannelWidth) || maximumChannelWidth < 1D || maximumChannelWidth > 2048D
|| !Double.isFinite(maximumBankWidth) || maximumBankWidth < 0D || maximumBankWidth > 2048D) {
|| !Double.isFinite(maximumBankWidth) || maximumBankWidth < 0D || maximumBankWidth > 2048D
|| !Double.isFinite(maximumTunnelWidthMultiplier)
|| maximumTunnelWidthMultiplier < 1D || maximumTunnelWidthMultiplier > 8D
|| !Double.isFinite(tunnelMouthBlend) || tunnelMouthBlend < 0D || tunnelMouthBlend > 16D) {
return;
}
double maximumReachRadius = maximumChannelWidth * 0.5D + maximumBankWidth;
double maximumSurfaceRadius = maximumChannelWidth * 0.5D + maximumBankWidth;
double maximumTunnelRadius = maximumChannelWidth * 0.5D * maximumTunnelWidthMultiplier
+ tunnelMouthBlend;
double maximumReachRadius = Math.max(maximumSurfaceRadius, maximumTunnelRadius);
RiverTopologyComplexity.Estimate estimate = RiverTopologyComplexity.estimate(
cellSize,
tileCells,
@@ -239,6 +296,7 @@ final class PackRiverValidator {
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "grottoHorizontalRadius", 2, 128, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "grottoVerticalRadius", 2, 128, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, caves, "grottoWarpStrength", 0D, 32D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, caves, "parentBiomeInheritance", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodRadius", 4, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodDepth", 4, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodVolume", 64, 1048576, errors);
@@ -525,12 +583,18 @@ final class PackRiverValidator {
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "continuationChanceMultiplier", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "caveEntryMultiplier", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalEnum(path, override, "terminalMode", TERMINAL_MODES, errors);
validateBiomePool(packFolder, path, override, "channelBiomes", RiverBiomeRole.CHANNEL, true, errors, warnings);
validateBiomePool(packFolder, path, override, "bankBiomes", RiverBiomeRole.BANK, true, errors, warnings);
validateBiomePool(packFolder, path, override, "mouthBiomes", RiverBiomeRole.MOUTH, true, errors, warnings);
validateBiomePool(packFolder, path, override, "dryBiomes", RiverBiomeRole.DRY, true, errors, warnings);
boolean validateSuitability = reachesOverworldNativeStructureRoles(
packFolder, resourceKey, resourceType, contexts);
validateBiomePool(packFolder, path, override, "channelBiomes", RiverBiomeRole.CHANNEL, true,
validateSuitability, errors, warnings);
validateBiomePool(packFolder, path, override, "bankBiomes", RiverBiomeRole.BANK, true,
validateSuitability, errors, warnings);
validateBiomePool(packFolder, path, override, "mouthBiomes", RiverBiomeRole.MOUTH, true,
validateSuitability, errors, warnings);
validateBiomePool(packFolder, path, override, "dryBiomes", RiverBiomeRole.DRY, true,
validateSuitability, errors, warnings);
validateBiomePool(packFolder, path, override, "floodedCaveBiomes", RiverBiomeRole.FLOODED_CAVE, true,
errors, warnings);
validateSuitability, errors, warnings);
if ("SINKHOLE_GROTTO".equals(stringValue(override, "terminalMode", null))) {
validateOverrideSinkhole(
packFolder,
@@ -545,18 +609,24 @@ final class PackRiverValidator {
}
private static void validateBiomePools(File packFolder, String path, JSONObject biomes, boolean allowNull,
boolean validateSuitability,
List<String> errors, List<String> warnings) {
validateStyle(packFolder, biomes, "selectionStyle", path, errors);
validateBiomePool(packFolder, path, biomes, "channel", RiverBiomeRole.CHANNEL, allowNull, errors, warnings);
validateBiomePool(packFolder, path, biomes, "bank", RiverBiomeRole.BANK, allowNull, errors, warnings);
validateBiomePool(packFolder, path, biomes, "mouth", RiverBiomeRole.MOUTH, allowNull, errors, warnings);
validateBiomePool(packFolder, path, biomes, "dry", RiverBiomeRole.DRY, allowNull, errors, warnings);
validateBiomePool(packFolder, path, biomes, "channel", RiverBiomeRole.CHANNEL, allowNull,
validateSuitability, errors, warnings);
validateBiomePool(packFolder, path, biomes, "bank", RiverBiomeRole.BANK, allowNull,
validateSuitability, errors, warnings);
validateBiomePool(packFolder, path, biomes, "mouth", RiverBiomeRole.MOUTH, allowNull,
validateSuitability, errors, warnings);
validateBiomePool(packFolder, path, biomes, "dry", RiverBiomeRole.DRY, allowNull,
validateSuitability, errors, warnings);
validateBiomePool(packFolder, path, biomes, "floodedCave", RiverBiomeRole.FLOODED_CAVE, allowNull,
errors, warnings);
validateSuitability, errors, warnings);
}
private static void validateBiomePool(File packFolder, String path, JSONObject owner, String field,
RiverBiomeRole role, boolean allowNull,
boolean validateSuitability,
List<String> errors, List<String> warnings) {
if (!owner.has(field)) {
return;
@@ -588,10 +658,36 @@ final class PackRiverValidator {
errors.add(entryPath + " references missing biome '" + key + "'.");
continue;
}
validateBiomeSuitability(entryPath, key, role, PackValidationIo.readJson(biomeFile), warnings);
if (validateSuitability) {
validateBiomeSuitability(entryPath, key, role, PackValidationIo.readJson(biomeFile), warnings);
}
}
}
private static boolean usesOverworldNativeStructureRoles(DimensionRiverContext context) {
return "NORMAL".equals(stringValue(context.dimension(), "environment", "NORMAL"));
}
private static boolean reachesOverworldNativeStructureRoles(
File packFolder,
String resourceKey,
String resourceType,
List<DimensionRiverContext> contexts
) {
for (DimensionRiverContext context : contexts) {
if (!usesOverworldNativeStructureRoles(context)) {
continue;
}
boolean reachable = "Region".equals(resourceType)
? context.regionKeys().contains(resourceKey)
: referencedSurfaceBiomes(packFolder, context.regionKeys()).contains(resourceKey);
if (reachable) {
return true;
}
}
return false;
}
private static void validateBiomeSuitability(String path, String biomeKey, RiverBiomeRole role,
JSONObject biome, List<String> warnings) {
if (biome == null || role == RiverBiomeRole.DRY || role == RiverBiomeRole.FLOODED_CAVE) {
@@ -661,6 +757,26 @@ final class PackRiverValidator {
validateStyle(packFolder, range, "style", rangePath, errors);
}
private static double styledRangeMaximum(
File packFolder,
JSONObject owner,
String field,
String path,
double fallback
) {
if (!owner.has(field)) {
return fallback;
}
JSONObject range = resolveObject(
packFolder,
owner.opt(field),
"snippet/style-range/",
path + "." + field,
new ArrayList<>()
);
return range == null ? fallback : doubleValue(range, "max", fallback);
}
private static void validateStyle(File packFolder, JSONObject owner, String field, String path,
List<String> errors) {
if (!owner.has(field)) {
@@ -173,7 +173,6 @@ public class PregenCacheImpl implements PregenCache {
return readPlate(x, z, input);
} catch (IOException e) {
IrisLogging.error("Failed to read pregen cache " + file);
e.printStackTrace();
IrisLogging.reportError(e);
}
@@ -195,7 +194,6 @@ public class PregenCacheImpl implements PregenCache {
plate.dirty = false;
} catch (Throwable e) {
IrisLogging.error("Failed to write pregen cache " + (file != null ? file : "c." + plate.x + "." + plate.z));
e.printStackTrace();
IrisLogging.reportError(e);
}
}
@@ -464,7 +464,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
IrisLogging.reportError(throwable);
} catch (Throwable e) {
e.printStackTrace();
}
return null;
@@ -489,7 +488,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
} finally {
try {
markFinished(success);
@@ -520,7 +518,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
String suppressedText = suppressed <= 0 ? "" : " suppressed=" + suppressed;
// Slow chunk loads are what the adaptive in flight limit exists to absorb, and this line is already
// interval throttled with a suppression count. It reports the throttle working, not a fault.
IrisLogging.info("Async pregen chunk load at " + x + "," + z
IrisLogging.debug("Async pregen chunk load at " + x + "," + z
+ " is still pending after " + slowRequestWarningSeconds + "s."
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
+ suppressedText + " " + metricsSnapshot());
@@ -583,7 +581,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
if (lastAdaptiveLogAt.compareAndSet(last, now)) {
IrisLogging.info("Async pregen adaptive limit " + mode + " -> " + value + " " + metricsSnapshot());
IrisLogging.debug("Async pregen adaptive limit " + mode + " -> " + value + " " + metricsSnapshot());
}
}
@@ -1001,7 +999,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
IrisLogging.warn("For more information see https://docs.papermc.io/paper/reference/global-configuration#chunk_system_worker_threads");
if (e instanceof InvocationTargetException) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
return 0;
@@ -1023,7 +1020,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to reset worker threads");
e.printStackTrace();
}
return i;
});
@@ -119,7 +119,6 @@ public class IrisCodeWorkspace {
IO.writeAll(ws, rendered);
} catch (Throwable e1) {
IrisLogging.reportError(e1);
e1.printStackTrace();
}
}
@@ -281,7 +281,6 @@ public class IrisPackageCompiler {
return p;
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_FAILED));
return null;
@@ -27,7 +27,6 @@ import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import lombok.Data;
import org.bukkit.GameMode;
import org.bukkit.World;
import java.io.File;
@@ -45,6 +44,7 @@ public class IrisProject {
private File path;
private String name;
private PlatformChunkGenerator activeProvider;
private StudioOpenCoordinator.StudioOpenKind activeOpenKind;
public IrisProject(File path) {
this.path = path;
@@ -132,10 +132,6 @@ public class IrisProject {
return;
}
if (sender.isPlayer() && sender.player() != null) {
J.runEntity(sender.player(), () -> sender.player().setGameMode(GameMode.CREATIVE));
}
activeProvider = IrisToolbelt.access(result.world());
maintenanceWorld = result.world();
if (maintenanceWorld != null) {
IrisToolbelt.beginWorldMaintenance(maintenanceWorld, "studio-open");
@@ -888,7 +888,6 @@ public class SchemaBuilder {
a.put(j);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
} else {
a.put(function.apply(gg));
@@ -189,8 +189,6 @@ final class StudioOpenProgressReporter {
case "create_world" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CREATE_WORLD);
case "apply_world_rules" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_APPLY_WORLD_RULES);
case "prepare_generator" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_GENERATOR);
case "request_entry_chunk" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_REQUEST_ENTRY_CHUNK);
case "resolve_safe_entry" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_SAFE_ENTRY);
case "teleport_player" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_TELEPORT_PLAYER);
case "finalize_open" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_FINALIZE_OPEN);
case "cleanup" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CLEANUP);
@@ -0,0 +1,24 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.protocol;
@FunctionalInterface
public interface CursorInfoRequestHandler {
void handle(String sessionId, int blockX, int blockZ);
}
@@ -0,0 +1,205 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.protocol;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisProtocol;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public final class IrisCursorRequestService implements CursorInfoRequestHandler {
private static final int DEFAULT_MAX_PENDING_SESSIONS = 2048;
private final EngineResolver engineResolver;
private final IrisSessionRegistry registry;
private final Executor executor;
private final int maxPendingSessions;
private final LinkedHashMap<String, PendingRequest> pending;
private final AtomicBoolean drainScheduled;
private final AtomicLong coalesced;
private final AtomicLong droppedSaturated;
private final AtomicLong droppedNoEngine;
private final AtomicLong droppedNoSession;
private final AtomicLong resolved;
IrisCursorRequestService(
EngineResolver engineResolver,
IrisSessionRegistry registry,
Executor executor,
int maxPendingSessions
) {
this.engineResolver = Objects.requireNonNull(engineResolver, "engine resolver");
this.registry = Objects.requireNonNull(registry, "session registry");
this.executor = Objects.requireNonNull(executor, "executor");
this.maxPendingSessions = Math.max(1, maxPendingSessions);
this.pending = new LinkedHashMap<>();
this.drainScheduled = new AtomicBoolean(false);
this.coalesced = new AtomicLong(0L);
this.droppedSaturated = new AtomicLong(0L);
this.droppedNoEngine = new AtomicLong(0L);
this.droppedNoSession = new AtomicLong(0L);
this.resolved = new AtomicLong(0L);
}
public static IrisCursorRequestService create(EngineResolver engineResolver, IrisSessionRegistry registry) {
AtomicInteger threadIndex = new AtomicInteger(0);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1,
1,
30L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
runnable -> {
Thread thread = new Thread(runnable);
thread.setName("Iris Cursor Lookup " + threadIndex.incrementAndGet());
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
executor.allowCoreThreadTimeOut(true);
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
if (preservation != null) {
preservation.register(executor);
}
return new IrisCursorRequestService(engineResolver, registry, executor, DEFAULT_MAX_PENDING_SESSIONS);
}
@Override
public void handle(String sessionId, int blockX, int blockZ) {
if (sessionId == null || sessionId.isBlank()) {
droppedNoSession.incrementAndGet();
return;
}
int shed = 0;
synchronized (pending) {
PendingRequest previous = pending.put(sessionId, new PendingRequest(sessionId, blockX, blockZ));
if (previous != null) {
coalesced.incrementAndGet();
}
while (pending.size() > maxPendingSessions) {
Iterator<Map.Entry<String, PendingRequest>> iterator = pending.entrySet().iterator();
iterator.next();
iterator.remove();
shed++;
}
}
if (shed > 0) {
droppedSaturated.addAndGet(shed);
}
scheduleDrain();
}
public void clearSession(String sessionId) {
if (sessionId == null || sessionId.isBlank()) {
return;
}
synchronized (pending) {
pending.remove(sessionId);
}
}
public int pendingSize() {
synchronized (pending) {
return pending.size();
}
}
public long coalescedCount() {
return coalesced.get();
}
public long droppedSaturatedCount() {
return droppedSaturated.get();
}
public long droppedNoEngineCount() {
return droppedNoEngine.get();
}
public long droppedNoSessionCount() {
return droppedNoSession.get();
}
public long resolvedCount() {
return resolved.get();
}
private void scheduleDrain() {
if (!drainScheduled.compareAndSet(false, true)) {
return;
}
try {
executor.execute(this::drainPending);
} catch (RuntimeException failure) {
drainScheduled.set(false);
IrisLogging.reportError(failure);
}
}
private void drainPending() {
while (true) {
PendingRequest request;
synchronized (pending) {
Iterator<PendingRequest> iterator = pending.values().iterator();
if (!iterator.hasNext()) {
drainScheduled.set(false);
return;
}
request = iterator.next();
iterator.remove();
}
try {
process(request);
} catch (Throwable failure) {
IrisLogging.reportError(failure);
}
}
}
private void process(PendingRequest request) {
IrisSession session = registry.get(request.sessionId());
if (session == null || !session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_CURSOR)) {
droppedNoSession.incrementAndGet();
return;
}
Engine engine = engineResolver.resolve(request.sessionId());
if (engine == null || engine.isClosed()) {
droppedNoEngine.incrementAndGet();
return;
}
session.send(IrisCursorResolver.resolve(engine, request.blockX(), request.blockZ()));
resolved.incrementAndGet();
}
private record PendingRequest(String sessionId, int blockX, int blockZ) {
}
}
@@ -45,11 +45,13 @@ public final class IrisProtocolServer {
private final AtomicLong cursorOutOfBounds;
private final AtomicLong visionTileForwarded;
private final AtomicLong visionRateLimited;
private final AtomicLong visionOutOfBounds;
private final AtomicLong pregenRegionDeltasBroadcast;
private final AtomicLong studioHotloadsBroadcast;
private final AtomicLong toastsBroadcast;
private final AtomicLong toastsSent;
private volatile EngineResolver engineResolver;
private volatile CursorInfoRequestHandler cursorInfoHandler;
private volatile VisionTileRequestHandler visionTileHandler;
public IrisProtocolServer(IrisSessionRegistry registry, long serverCapabilities, String serverBrand, boolean irisActive) {
@@ -73,6 +75,7 @@ public final class IrisProtocolServer {
this.cursorOutOfBounds = new AtomicLong(0L);
this.visionTileForwarded = new AtomicLong(0L);
this.visionRateLimited = new AtomicLong(0L);
this.visionOutOfBounds = new AtomicLong(0L);
this.pregenRegionDeltasBroadcast = new AtomicLong(0L);
this.studioHotloadsBroadcast = new AtomicLong(0L);
this.toastsBroadcast = new AtomicLong(0L);
@@ -87,6 +90,10 @@ public final class IrisProtocolServer {
this.engineResolver = engineResolver;
}
public void setCursorInfoHandler(CursorInfoRequestHandler cursorInfoHandler) {
this.cursorInfoHandler = cursorInfoHandler;
}
public void setVisionTileHandler(VisionTileRequestHandler visionTileHandler) {
this.visionTileHandler = visionTileHandler;
}
@@ -279,6 +286,10 @@ public final class IrisProtocolServer {
return visionRateLimited.get();
}
public long visionOutOfBoundsCount() {
return visionOutOfBounds.get();
}
public long pregenRegionDeltasBroadcastCount() {
return pregenRegionDeltasBroadcast.get();
}
@@ -338,6 +349,12 @@ public final class IrisProtocolServer {
cursorRateLimited.incrementAndGet();
return;
}
CursorInfoRequestHandler handler = cursorInfoHandler;
if (handler != null) {
handler.handle(session.id(), request.blockX(), request.blockZ());
cursorInfoServed.incrementAndGet();
return;
}
EngineResolver resolver = engineResolver;
if (resolver == null) {
noEngineDrops.incrementAndGet();
@@ -357,6 +374,10 @@ public final class IrisProtocolServer {
capabilityRejected.incrementAndGet();
return;
}
if (visionTileOutOfWorldBounds(request)) {
visionOutOfBounds.incrementAndGet();
return;
}
if (!session.allowVisionTile(clock.getAsLong())) {
visionRateLimited.incrementAndGet();
return;
@@ -373,4 +394,15 @@ public final class IrisProtocolServer {
private static boolean outOfWorldBounds(int coordinate) {
return coordinate > IrisProtocol.MAX_QUERY_BLOCK_COORDINATE || coordinate < -IrisProtocol.MAX_QUERY_BLOCK_COORDINATE;
}
private static boolean visionTileOutOfWorldBounds(IrisMessage.VisionTileRequest request) {
int zoom = Math.max(0, Math.min(request.zoomLevel(), IrisTileEncoder.MAX_ZOOM_LEVEL));
long tileSize = (long) IrisTileEncoder.TILE_PIXELS << zoom;
long minBlockX = (long) request.tileX() * tileSize;
long minBlockZ = (long) request.tileZ() * tileSize;
long maxBlockX = minBlockX + tileSize - 1L;
long maxBlockZ = minBlockZ + tileSize - 1L;
long limit = IrisProtocol.MAX_QUERY_BLOCK_COORDINATE;
return minBlockX < -limit || maxBlockX > limit || minBlockZ < -limit || maxBlockZ > limit;
}
}
@@ -26,8 +26,12 @@ import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisProtocol;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
@@ -37,7 +41,9 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public final class IrisVisionRequestService implements VisionTileRequestHandler {
private static final int DEFAULT_MAX_PENDING = 64;
private static final int DEFAULT_MAX_PENDING = 8_192;
private static final int MAX_PENDING_PER_SESSION = 8;
private static final int MAX_DRAIN_WORKERS = 2;
private static final long SHED_LOG_INTERVAL_MILLIS = 60_000L;
private static final int SEQUENCE_WRAP_GUARD = Integer.MAX_VALUE - 1024;
@@ -45,7 +51,11 @@ public final class IrisVisionRequestService implements VisionTileRequestHandler
private final IrisSessionRegistry registry;
private final Executor executor;
private final int maxPending;
private final ArrayDeque<PendingRequest> pending;
private final LinkedHashMap<String, LinkedHashMap<TileKey, PendingRequest>> pendingBySession;
private final ArrayDeque<String> sessionOrder;
private final Set<String> scheduledSessions;
private final AtomicInteger activeDrains;
private int pendingCount;
/**
* One counter per session, not one per (session, tile, zoom). The client only ever compares sequences
* within a single tile key, so a session-wide monotonic counter satisfies the "newer wins" contract in
@@ -63,7 +73,10 @@ public final class IrisVisionRequestService implements VisionTileRequestHandler
this.registry = Objects.requireNonNull(registry, "session registry");
this.executor = Objects.requireNonNull(executor, "executor");
this.maxPending = Math.max(1, maxPending);
this.pending = new ArrayDeque<>();
this.pendingBySession = new LinkedHashMap<>();
this.sessionOrder = new ArrayDeque<>();
this.scheduledSessions = new HashSet<>();
this.activeDrains = new AtomicInteger(0);
this.sequences = new ConcurrentHashMap<>();
this.nextShedLogAt = new AtomicLong(0L);
this.droppedSaturated = new AtomicLong(0L);
@@ -91,20 +104,42 @@ public final class IrisVisionRequestService implements VisionTileRequestHandler
@Override
public void handle(String sessionId, int tileX, int tileZ, int zoomLevel) {
PendingRequest request = new PendingRequest(sessionId, tileX, tileZ, zoomLevel);
if (sessionId == null || sessionId.isBlank()) {
droppedNoSession.incrementAndGet();
return;
}
TileKey tileKey = new TileKey(tileX, tileZ, zoomLevel);
PendingRequest request = new PendingRequest(sessionId, tileKey);
int shed = 0;
synchronized (pending) {
while (pending.size() >= maxPending) {
pending.pollFirst();
synchronized (pendingBySession) {
LinkedHashMap<TileKey, PendingRequest> sessionPending = pendingBySession.get(sessionId);
if (sessionPending != null && sessionPending.containsKey(tileKey)) {
sessionPending.put(tileKey, request);
scheduleDrains();
return;
}
if (sessionPending != null
&& sessionPending.size() >= Math.min(MAX_PENDING_PER_SESSION, maxPending)) {
removeOldest(sessionPending);
pendingCount--;
shed++;
}
pending.addLast(request);
while (pendingCount >= maxPending) {
shedOnePendingSession();
shed++;
}
sessionPending = pendingBySession.computeIfAbsent(sessionId, ignored -> new LinkedHashMap<>());
sessionPending.put(tileKey, request);
pendingCount++;
if (scheduledSessions.add(sessionId)) {
sessionOrder.addLast(sessionId);
}
}
if (shed > 0) {
droppedSaturated.addAndGet(shed);
logShed();
}
executor.execute(this::drainOne);
scheduleDrains();
}
/**
@@ -112,12 +147,18 @@ public final class IrisVisionRequestService implements VisionTileRequestHandler
* disconnects or unregisters so neither structure grows with the player count over a server's uptime.
*/
public void clearSession(String sessionId) {
if (sessionId == null || sessionId.isEmpty()) {
if (sessionId == null || sessionId.isBlank()) {
return;
}
sequences.remove(sessionId);
synchronized (pending) {
pending.removeIf((PendingRequest request) -> sessionId.equals(request.sessionId()));
synchronized (pendingBySession) {
LinkedHashMap<TileKey, PendingRequest> removed = pendingBySession.remove(sessionId);
if (removed != null) {
pendingCount -= removed.size();
}
if (scheduledSessions.remove(sessionId)) {
sessionOrder.removeIf(sessionId::equals);
}
}
}
@@ -138,20 +179,103 @@ public final class IrisVisionRequestService implements VisionTileRequestHandler
}
public int pendingSize() {
synchronized (pending) {
return pending.size();
synchronized (pendingBySession) {
return pendingCount;
}
}
private void drainOne() {
PendingRequest request;
synchronized (pending) {
request = pending.pollFirst();
private void scheduleDrains() {
while (true) {
int active = activeDrains.get();
synchronized (pendingBySession) {
if (pendingCount <= active || active >= MAX_DRAIN_WORKERS) {
return;
}
}
if (!activeDrains.compareAndSet(active, active + 1)) {
continue;
}
try {
executor.execute(this::drainPending);
} catch (RuntimeException failure) {
activeDrains.decrementAndGet();
IrisLogging.reportError(failure);
return;
}
}
if (request == null) {
}
private void drainPending() {
try {
while (true) {
PendingRequest request = pollNextRequest();
if (request == null) {
return;
}
try {
process(request);
} catch (Throwable failure) {
IrisLogging.reportError(failure);
}
}
} finally {
activeDrains.decrementAndGet();
scheduleDrains();
}
}
private PendingRequest pollNextRequest() {
synchronized (pendingBySession) {
while (true) {
String sessionId = sessionOrder.pollFirst();
if (sessionId == null) {
return null;
}
scheduledSessions.remove(sessionId);
LinkedHashMap<TileKey, PendingRequest> sessionPending = pendingBySession.get(sessionId);
if (sessionPending == null || sessionPending.isEmpty()) {
pendingBySession.remove(sessionId);
continue;
}
PendingRequest request = removeOldest(sessionPending);
pendingCount--;
if (sessionPending.isEmpty()) {
pendingBySession.remove(sessionId);
} else if (scheduledSessions.add(sessionId)) {
sessionOrder.addLast(sessionId);
}
return request;
}
}
}
private void shedOnePendingSession() {
String selectedSession = null;
int selectedSize = 0;
for (Map.Entry<String, LinkedHashMap<TileKey, PendingRequest>> entry : pendingBySession.entrySet()) {
if (entry.getValue().size() > selectedSize) {
selectedSession = entry.getKey();
selectedSize = entry.getValue().size();
}
}
if (selectedSession == null) {
return;
}
process(request);
LinkedHashMap<TileKey, PendingRequest> selected = pendingBySession.get(selectedSession);
removeOldest(selected);
pendingCount--;
if (selected.isEmpty()) {
pendingBySession.remove(selectedSession);
if (scheduledSessions.remove(selectedSession)) {
sessionOrder.removeIf(selectedSession::equals);
}
}
}
private static PendingRequest removeOldest(LinkedHashMap<TileKey, PendingRequest> requests) {
Map.Entry<TileKey, PendingRequest> oldest = requests.entrySet().iterator().next();
requests.remove(oldest.getKey());
return oldest.getValue();
}
private void process(PendingRequest request) {
@@ -166,7 +290,13 @@ public final class IrisVisionRequestService implements VisionTileRequestHandler
return;
}
int sequence = nextSequence(request);
List<IrisMessage.VisionTile> chunks = IrisTileEncoder.encode(engine, request.tileX(), request.tileZ(), request.zoomLevel(), sequence);
List<IrisMessage.VisionTile> chunks = IrisTileEncoder.encode(
engine,
request.tile().tileX(),
request.tile().tileZ(),
request.tile().zoomLevel(),
sequence
);
for (IrisMessage.VisionTile chunk : chunks) {
session.send(chunk);
}
@@ -189,6 +319,9 @@ public final class IrisVisionRequestService implements VisionTileRequestHandler
IrisLogging.warn("vision: request queue saturated at " + maxPending + ", shed " + droppedSaturated.get() + " total");
}
private record PendingRequest(String sessionId, int tileX, int tileZ, int zoomLevel) {
private record TileKey(int tileX, int tileZ, int zoomLevel) {
}
private record PendingRequest(String sessionId, TileKey tile) {
}
}
@@ -505,7 +505,7 @@ public final class GoldenHashEngine {
MessageArgument.untrusted("file", diag.getName())
));
}
IrisLogging.info("goldenhash diag: chunk=" + chunkX + "," + chunkZ + " repeatStable=" + diffs.isEmpty() + " -> " + diag.getAbsolutePath());
IrisLogging.debug("goldenhash diag: chunk=" + chunkX + "," + chunkZ + " repeatStable=" + diffs.isEmpty() + " -> " + diag.getAbsolutePath());
} catch (Throwable e) {
IrisLogging.reportError(e);
feedback.fail(IrisLanguage.plain(
@@ -21,7 +21,7 @@ import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
@@ -37,12 +37,10 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -53,15 +51,10 @@ import java.util.function.Supplier;
public final class StudioOpenCoordinator {
private static final long STUDIO_CLOSE_TIMEOUT_SECONDS = 120L;
private static final long STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS = 30L;
private static final long STUDIO_ENTRY_RELEASE_TIMEOUT_SECONDS = 15L;
private static final long STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS = 30L;
private static final long STUDIO_ENTRY_CLEANUP_BOUNDARY_SECONDS = 120L;
private static volatile StudioOpenCoordinator instance;
private final EntryLoadRegistry entryLoads;
private StudioOpenCoordinator() {
entryLoads = new EntryLoadRegistry();
}
public static StudioOpenCoordinator get() {
@@ -115,9 +108,6 @@ public final class StudioOpenCoordinator {
AtomicBoolean activeAdmission = Objects.requireNonNull(
admission,
"Studio teleport admission");
if (!isStudioTeleportAdmitted(activeAdmission, deadlineNanos)) {
return studioTeleportDeadlineFailure("entry loading");
}
PlatformChunkGenerator provider = project.getActiveProvider();
if (provider == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
@@ -128,77 +118,30 @@ public final class StudioOpenCoordinator {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio world is not loaded."));
}
Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world);
if (entryAnchor == null) {
Location entry = WorldRuntimeControlService.get().resolveEntryAnchor(world, provider);
if (entry == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio entry anchor could not be resolved."));
"Studio entry point could not be resolved."));
}
EntryChunkResolution entryResolution = loadEntryChunk(world, entryAnchor);
CompletableFuture<Boolean> teleportOperation = beforeStudioTeleportDeadline(
entryResolution.chunk(),
activeAdmission,
deadlineNanos,
"entry loading")
.thenCompose(ignored -> {
if (!isStudioTeleportAdmitted(activeAdmission, deadlineNanos)) {
return studioTeleportDeadlineFailure("safe-entry resolution");
}
return beforeStudioTeleportDeadline(
entryResolution.safeEntry(),
activeAdmission,
deadlineNanos,
"safe-entry resolution");
})
.thenCompose(entry -> {
if (entry == null) {
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 =
WorldRuntimeControlService.get().teleport(player, entry);
if (teleport == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio native teleport returned no completion future."));
}
return teleport;
});
return teleportOperation;
}
private <T> CompletableFuture<T> beforeStudioTeleportDeadline(
CompletableFuture<T> stage,
AtomicBoolean admission,
long deadlineNanos,
String stageName
) {
if (stage == null) {
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);
if (teleport == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio " + stageName + " returned no completion future."));
"Studio native teleport returned no completion future."));
}
long remainingNanos = deadlineNanos - System.nanoTime();
if (!admission.get() || remainingNanos <= 0L) {
return studioTeleportDeadlineFailure(stageName);
if (remainingNanos <= 0L) {
teleport.completeExceptionally(new TimeoutException(
"Studio teleport deadline expired before native teleport completion."));
return teleport;
}
CompletableFuture<T> bounded = new CompletableFuture<>();
stage.whenComplete((value, failure) -> {
if (failure == null) {
bounded.complete(value);
} else {
bounded.completeExceptionally(failure);
}
});
CompletableFuture.delayedExecutor(remainingNanos, TimeUnit.NANOSECONDS)
.execute(() -> bounded.completeExceptionally(new TimeoutException(
"Studio teleport deadline expired during " + stageName + ".")));
return bounded;
}
private boolean isStudioTeleportAdmitted(AtomicBoolean admission, long deadlineNanos) {
return admission.get() && System.nanoTime() < deadlineNanos;
teleport.orTimeout(remainingNanos, TimeUnit.NANOSECONDS);
return teleport;
}
private <T> CompletableFuture<T> studioTeleportDeadlineFailure(String stageName) {
@@ -209,13 +152,10 @@ public final class StudioOpenCoordinator {
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
World world = null;
PlatformChunkGenerator provider = null;
CompletableFuture<Void> entryLoadFuture = null;
CompletableFuture<Void> entryUseFuture = null;
CompletableFuture<Boolean> nativeTeleportFuture = null;
try {
long openStart = System.nanoTime();
long t = openStart;
entryLoads.rejectNewOpen();
updateStage(request, "resolve_dimension", 0.04D);
if (IrisToolbelt.getDimension(request.dimensionKey()) == null) {
throw new IrisException("Dimension cannot be found for id " + request.dimensionKey() + ".");
@@ -263,75 +203,11 @@ public final class StudioOpenCoordinator {
}
t = logStudioPhase(request, "resolve_entry_anchor", t, openStart);
long entryPrecomputeStartedAt = System.nanoTime();
CompletableFuture<Void> preparedEntryChunks = CompletableFuture.completedFuture(null);
if (requiresLoadedEntry(request)) {
if (!(provider instanceof BukkitChunkGenerator bukkitGenerator)) {
throw new IllegalStateException(
"Studio runtime provider cannot prepare its entry chunks.");
}
preparedEntryChunks = bukkitGenerator.prepareStudioEntryChunks(
world,
entryAnchor.getBlockX() >> 4,
entryAnchor.getBlockZ() >> 4
);
}
updateStage(request, "prepare_structure_rings", 0.79D);
endStudioEntryBootstrap(world, provider);
t = logStudioPhase(request, "prepare_structure_rings", t, openStart);
if (requiresLoadedEntry(request)) {
try {
preparedEntryChunks.get(
STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS,
TimeUnit.SECONDS
);
} catch (TimeoutException e) {
throw new IllegalStateException(
"Studio entry chunk precompute did not finish in time.", e);
}
t = logOverlappedStudioPhase(
request,
"prepare_entry_chunks",
entryPrecomputeStartedAt,
openStart
);
}
Location safeEntry = entryAnchor;
if (requiresLoadedEntry(request)) {
updateStage(request, "load_entry_chunk", 0.80D);
int entryChunkX = entryAnchor.getBlockX() >> 4;
int entryChunkZ = entryAnchor.getBlockZ() >> 4;
EntryChunkResolution entryResolution = loadEntryChunk(world, entryAnchor);
CompletableFuture<Void> useSettlement = new CompletableFuture<>();
entryUseFuture = useSettlement;
entryLoadFuture = entryResolution.safeEntry().thenCompose(ignored -> useSettlement);
entryLoads.register(request.worldName(), entryLoadFuture);
try {
entryResolution.chunk().get(STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (TimeoutException e) {
throw new IllegalStateException("Studio entry chunk did not load in time at "
+ entryChunkX + "," + entryChunkZ + " — chunk system may be stalled.", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Studio entry chunk load was interrupted at "
+ entryChunkX + "," + entryChunkZ + ".", e);
}
t = logStudioPhase(request, "load_entry_chunk", t, openStart);
updateStage(request, "resolve_safe_entry", 0.84D);
try {
safeEntry = entryResolution.safeEntry().get(5L, TimeUnit.SECONDS);
} catch (TimeoutException e) {
throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled.");
}
if (safeEntry == null) {
throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\".");
}
t = logStudioPhase(request, "resolve_safe_entry", t, openStart);
}
Location entryLocation = entryAnchor;
if (request.openKind().teleportThroughStandardEntry()
&& request.playerName() != null
@@ -341,16 +217,19 @@ public final class StudioOpenCoordinator {
if (player == null) {
throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online.");
}
Boolean teleported;
try {
nativeTeleportFuture = WorldRuntimeControlService.get().teleport(player, safeEntry);
nativeTeleportFuture = WorldRuntimeControlService.get().teleportInMode(
player,
entryLocation,
GameMode.SPECTATOR);
if (nativeTeleportFuture == null) {
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.");
}
if (!Boolean.TRUE.equals(teleported)) {
@@ -361,6 +240,7 @@ public final class StudioOpenCoordinator {
updateStage(request, "finalize_open", 1.00D);
if (request.project() != null) {
request.project().setActiveOpenKind(request.openKind());
request.project().setActiveProvider(provider);
}
if (request.openKind().openWorkspace() && request.project() != null) {
@@ -369,17 +249,10 @@ public final class StudioOpenCoordinator {
runOpenFinalizer(request.onDone(), world);
t = logStudioPhase(request, "finalize_open", t, openStart);
settleEntryUseAfterOperation(entryUseFuture, nativeTeleportFuture);
if (entryLoadFuture != null) {
entryLoadFuture.get(STUDIO_ENTRY_RELEASE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
IrisLogging.info("Studio open: " + world.getName() + " ready in "
IrisLogging.debug("Studio open: " + world.getName() + " ready in "
+ elapsedMillis(openStart) + "ms");
entryLoads.release(request.worldName(), entryLoadFuture);
future.complete(new StudioOpenResult(world, safeEntry));
future.complete(new StudioOpenResult(world, entryLocation));
} catch (Throwable e) {
settleEntryUseAfterOperation(entryUseFuture, nativeTeleportFuture);
abandonStudioEntryBootstrap(world, e);
IrisLogging.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
if (!request.retainOnFailure()) {
@@ -390,15 +263,7 @@ public final class StudioOpenCoordinator {
deferFailedOpenCleanupToRestart(
provider,
request.worldName(),
world,
entryLoadFuture);
} else if (requiresDeferredEntryCleanup(entryLoadFuture)) {
deferFailedOpenCleanup(
entryLoadFuture,
provider,
request.worldName(),
world,
request.project());
world);
} else {
try {
CompletableFuture<Void> cleanup = cleanupFailedOpen(
@@ -406,50 +271,27 @@ public final class StudioOpenCoordinator {
request.worldName(),
world,
request.project());
entryLoads.releaseAfterSuccessfulCompletion(
request.worldName(),
entryLoadFuture,
cleanup);
cleanup.get(45L, TimeUnit.SECONDS);
} catch (Throwable cleanupError) {
IrisLogging.reportError("Studio cleanup failed for world \""
+ request.worldName() + "\".", unwrapFailure(cleanupError));
}
}
} else if (entryLoadFuture != null) {
entryLoads.releaseAfterSuccessfulCompletion(
request.worldName(),
entryLoadFuture,
entryLoadFuture);
}
future.completeExceptionally(e);
}
}
static boolean requiresLoadedEntry(StudioOpenRequest request) {
Objects.requireNonNull(request, "Studio open request");
return request.openKind().teleportThroughStandardEntry()
&& request.playerName() != null
&& !request.playerName().isBlank();
}
private void deferFailedOpenCleanupToRestart(
PlatformChunkGenerator provider,
String worldName,
World world,
CompletableFuture<Void> entryLoadFuture
World world
) {
if (provider != null || world != null || transientWorldStorageExists(worldName)) {
queueStartupCleanup(
worldName,
new IllegalStateException("Studio cleanup deferred across the queued server restart."));
}
if (entryLoadFuture != null) {
entryLoads.releaseAfterSuccessfulCompletion(
worldName,
entryLoadFuture,
entryLoadFuture);
}
}
private boolean transientWorldStorageExists(String worldName) {
@@ -467,7 +309,7 @@ public final class StudioOpenCoordinator {
private long logStudioPhase(StudioOpenRequest request, String phase, long t, long openStart) {
long now = System.nanoTime();
IrisLogging.info("[Studio timing] world=%s kind=%s phase=%s duration=%dms cumulative=%dms",
IrisLogging.debug("[Studio timing] world=%s kind=%s phase=%s duration=%dms cumulative=%dms",
request.worldName(),
request.openKind().name().toLowerCase(Locale.ROOT),
phase,
@@ -476,22 +318,6 @@ public final class StudioOpenCoordinator {
return now;
}
private long logOverlappedStudioPhase(
StudioOpenRequest request,
String phase,
long phaseStart,
long openStart
) {
long now = System.nanoTime();
IrisLogging.info("[Studio timing] world=%s kind=%s phase=%s duration=%dms cumulative=%dms",
request.worldName(),
request.openKind().name().toLowerCase(Locale.ROOT),
phase,
TimeUnit.NANOSECONDS.toMillis(now - phaseStart),
TimeUnit.NANOSECONDS.toMillis(now - openStart));
return now;
}
private void runOpenFinalizer(Consumer<World> finalizer, World world)
throws InterruptedException, ExecutionException, TimeoutException {
if (finalizer == null) {
@@ -511,91 +337,13 @@ public final class StudioOpenCoordinator {
}
private void logMeasuredStudioPhase(StudioOpenRequest request, String phase, long duration) {
IrisLogging.info("[Studio timing] world=%s kind=%s phase=%s duration=%dms",
IrisLogging.debug("[Studio timing] world=%s kind=%s phase=%s duration=%dms",
request.worldName(),
request.openKind().name().toLowerCase(Locale.ROOT),
phase,
duration);
}
private EntryChunkResolution loadEntryChunk(World world, Location entryAnchor) {
int chunkX = entryAnchor.getBlockX() >> 4;
int chunkZ = entryAnchor.getBlockZ() >> 4;
CompletableFuture<Chunk> chunkFuture = new CompletableFuture<>();
CompletableFuture<Location> safeEntryFuture = new CompletableFuture<>();
EntryChunkResolution resolution = new EntryChunkResolution(chunkFuture, safeEntryFuture);
CompletableFuture<Chunk> requested;
try {
requested = WorldRuntimeControlService.get().requestChunkAsync(
world,
chunkX,
chunkZ,
true,
true);
} catch (Throwable throwable) {
failEntryResolution(resolution, throwable);
return resolution;
}
if (requested == null) {
failEntryResolution(resolution, new IllegalStateException(
"Entry-chunk async request did not return a future at " + chunkX + "," + chunkZ + "."));
return resolution;
}
requested.whenComplete((chunk, failure) -> {
if (failure != null) {
failEntryResolution(resolution, failure);
return;
}
if (chunk == null) {
failEntryResolution(resolution, new IllegalStateException(
"Entry-chunk async request returned no chunk at " + chunkX + "," + chunkZ + "."));
return;
}
Runnable resolve = () -> {
chunkFuture.complete(chunk);
try {
Location safeEntry = WorldRuntimeControlService.findTopSafeStudioLocation(world, entryAnchor);
safeEntryFuture.complete(safeEntry);
} catch (Throwable resolutionFailure) {
failEntryResolution(resolution, resolutionFailure);
}
};
try {
if (J.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
resolve.run();
return;
}
if (!J.runRegion(world, chunkX, chunkZ, resolve)) {
failEntryResolution(resolution, new IllegalStateException(
"Failed to resolve the entry chunk on its owning region at "
+ chunkX + "," + chunkZ + "."));
}
} catch (Throwable schedulingFailure) {
failEntryResolution(resolution, schedulingFailure);
}
});
return resolution;
}
private void failEntryResolution(EntryChunkResolution resolution, Throwable failure) {
resolution.chunk().completeExceptionally(failure);
resolution.safeEntry().completeExceptionally(failure);
}
private void settleEntryUseAfterOperation(
CompletableFuture<Void> entryUseFuture,
CompletableFuture<?> operation
) {
if (entryUseFuture == null) {
return;
}
if (operation == null) {
entryUseFuture.complete(null);
return;
}
operation.whenComplete((ignored, failure) -> entryUseFuture.complete(null));
}
private void endStudioEntryBootstrap(World world, PlatformChunkGenerator provider) {
if (!(provider instanceof BukkitChunkGenerator bukkitGenerator)) {
throw new IllegalStateException("Studio runtime provider cannot finish its entry bootstrap.");
@@ -678,40 +426,6 @@ public final class StudioOpenCoordinator {
}
}
private void deferFailedOpenCleanup(
CompletableFuture<Void> entryLoadFuture,
PlatformChunkGenerator provider,
String worldName,
World world,
IrisProject project
) {
CompletableFuture<Void> cleanup = deferCleanupUntilEntrySettlement(
entryLoadFuture,
() -> cleanupFailedOpen(provider, worldName, world, project));
entryLoads.releaseAfterSuccessfulCompletion(worldName, entryLoadFuture, cleanup);
cleanup.whenComplete((ignored, cleanupFailure) -> {
if (cleanupFailure != null) {
IrisLogging.reportError("Deferred Studio cleanup failed for world \""
+ worldName + "\".", unwrapFailure(cleanupFailure));
}
});
observeDeferredEntryCleanupBoundary(entryLoadFuture, worldName);
}
static CompletableFuture<Void> deferCleanupUntilEntrySettlement(
CompletableFuture<?> entryLoadFuture,
Supplier<CompletableFuture<Void>> cleanup
) {
Objects.requireNonNull(entryLoadFuture, "Studio entry-load future");
Objects.requireNonNull(cleanup, "Studio cleanup");
return entryLoadFuture.handle((ignored, entryFailure) -> null)
.thenCompose(ignored -> invokePhase(cleanup));
}
static boolean requiresDeferredEntryCleanup(CompletableFuture<?> entryLoadFuture) {
return entryLoadFuture != null && !entryLoadFuture.isDone();
}
private CompletableFuture<Void> cleanupFailedOpen(
PlatformChunkGenerator provider,
String worldName,
@@ -724,33 +438,6 @@ public final class StudioOpenCoordinator {
: CompletableFuture.failedFuture(result.failureCause()));
}
private void observeDeferredEntryCleanupBoundary(
CompletableFuture<?> entryLoadFuture,
String worldName
) {
CompletableFuture.delayedExecutor(
STUDIO_ENTRY_CLEANUP_BOUNDARY_SECONDS,
TimeUnit.SECONDS).execute(() -> {
if (entryLoadFuture.isDone()) {
return;
}
TimeoutException timeout = new TimeoutException(
"Studio entry generation remained active for "
+ STUDIO_ENTRY_CLEANUP_BOUNDARY_SECONDS
+ " seconds after the open timeout for \"" + worldName + "\".");
boolean queued = queueStartupCleanup(worldName, timeout);
String recovery = queued
? " The transient world is queued for deletion at the next clean startup."
: " The transient world could not be queued for startup deletion.";
IrisLogging.reportError(
"Studio world \"" + worldName
+ "\" remains loaded because its entry generation is still active;"
+ " Iris did not unload or close its generator."
+ recovery,
timeout);
});
}
private CompletableFuture<StudioCloseResult> closeWorldCoordinated(
PlatformChunkGenerator provider,
String worldName,
@@ -801,6 +488,7 @@ public final class StudioOpenCoordinator {
IrisProject project
) {
AtomicBoolean unloadConfirmed = new AtomicBoolean(false);
AtomicBoolean unloadStarted = new AtomicBoolean(false);
AtomicBoolean folderDeleted = new AtomicBoolean(!deleteFolder);
AtomicBoolean terminalTimeout = new AtomicBoolean(false);
if (world != null) {
@@ -809,16 +497,20 @@ public final class StudioOpenCoordinator {
CompletableFuture<Void> sequence = sequenceStudioClose(
() -> evacuateWorldFamily(worldName, world),
() -> unloadWorldFamily(worldName, world).thenRun(() -> {
if (terminalTimeout.get()) {
throw new CompletionException(new TimeoutException(
"Studio close stopped after its terminal timeout."));
}
unloadConfirmed.set(true);
if (project != null) {
project.setActiveProvider(null);
}
}),
() -> {
unloadStarted.set(true);
return unloadWorldFamily(worldName, world).thenRun(() -> {
if (terminalTimeout.get()) {
throw new CompletionException(new TimeoutException(
"Studio close stopped after its terminal timeout."));
}
unloadConfirmed.set(true);
if (project != null) {
project.setActiveProvider(null);
project.setActiveOpenKind(null);
}
});
},
() -> provider == null ? CompletableFuture.completedFuture(null) : provider.closeAsync(),
() -> deleteFolder
? deleteWorldFamily(worldName).thenRun(() -> folderDeleted.set(true))
@@ -838,6 +530,7 @@ public final class StudioOpenCoordinator {
))
.exceptionally(throwable -> {
Throwable failure = unwrapFailure(throwable);
requestRestartAfterPartialClose(worldName, unloadStarted.get());
boolean queued = deleteFolder && queueStartupCleanup(worldName, failure);
return new StudioCloseResult(
worldName,
@@ -854,6 +547,12 @@ public final class StudioOpenCoordinator {
});
}
static void requestRestartAfterPartialClose(String worldName, boolean unloadStarted) {
if (unloadStarted) {
ServerConfigurator.restart("Studio close failed after world unload began for \"" + worldName + "\".");
}
}
static CompletableFuture<Void> sequenceStudioClose(
Supplier<CompletableFuture<Void>> evacuate,
Supplier<CompletableFuture<Void>> unload,
@@ -1039,11 +738,6 @@ public final class StudioOpenCoordinator {
}
for (String staleWorldName : staleWorldNames) {
if (entryLoads.isFenced(staleWorldName)) {
IrisLogging.warn("Skipping stale Studio cleanup for \"" + staleWorldName
+ "\" because its open or deferred cleanup is still active.");
continue;
}
try {
StudioCloseResult cleanupResult = closeWorldCoordinated(
null,
@@ -1202,6 +896,10 @@ public final class StudioOpenCoordinator {
true,
true,
IrisCreator.DatapackPreparation.REUSE_LOADED_RUNTIME_IF_READY),
OBJECT(
false,
true,
IrisCreator.DatapackPreparation.REUSE_LOADED_RUNTIME_IF_READY),
JIGSAW(
false,
false,
@@ -1232,7 +930,7 @@ public final class StudioOpenCoordinator {
}
public boolean prepareGeneratorState() {
return this == STANDARD;
return this != JIGSAW;
}
public IrisCreator.DatapackPreparation datapackPreparation() {
@@ -1258,69 +956,4 @@ public final class StudioOpenCoordinator {
}
}
private record EntryChunkResolution(
CompletableFuture<Chunk> chunk,
CompletableFuture<Location> safeEntry
) {
}
static final class EntryLoadRegistry {
private final ConcurrentHashMap<String, CompletableFuture<?>> entryLoads;
EntryLoadRegistry() {
entryLoads = new ConcurrentHashMap<>();
}
void register(String worldName, CompletableFuture<?> entryLoadFuture) {
Objects.requireNonNull(worldName, "Studio world name");
Objects.requireNonNull(entryLoadFuture, "Studio entry-load future");
CompletableFuture<?> existing = entryLoads.putIfAbsent(worldName, entryLoadFuture);
if (existing != null) {
throw new IllegalStateException("Studio open or deferred cleanup is already active for \""
+ worldName + "\".");
}
}
void release(
String worldName,
CompletableFuture<?> entryLoadFuture
) {
if (worldName == null || entryLoadFuture == null) {
return;
}
entryLoads.remove(worldName, entryLoadFuture);
}
void releaseAfterSuccessfulCompletion(
String worldName,
CompletableFuture<?> entryLoadFuture,
CompletableFuture<?> completion
) {
Objects.requireNonNull(completion, "Studio lifecycle completion");
completion.whenComplete((ignored, failure) -> {
if (failure == null) {
release(worldName, entryLoadFuture);
}
});
}
void rejectNewOpen() {
ArrayList<String> activeWorlds = new ArrayList<>();
for (Map.Entry<String, CompletableFuture<?>> entry : entryLoads.entrySet()) {
activeWorlds.add(entry.getKey());
}
if (activeWorlds.isEmpty()) {
return;
}
Collections.sort(activeWorlds);
throw new IllegalStateException("A previous Studio open or deferred cleanup is still active for "
+ String.join(", ", activeWorlds)
+ ". Wait for it to settle before opening another Studio; a reported cleanup failure"
+ " requires the queued clean restart.");
}
boolean isFenced(String worldName) {
return entryLoads.containsKey(worldName);
}
}
}
@@ -14,6 +14,7 @@ import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.GameMode;
import org.bukkit.GameRule;
import org.bukkit.HeightMap;
import org.bukkit.Location;
@@ -21,7 +22,6 @@ import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Levelled;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.entity.Player;
import org.bukkit.event.world.TimeSkipEvent;
@@ -32,10 +32,13 @@ import org.bukkit.util.VoxelShape;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public final class WorldRuntimeControlService {
private static final int MAX_SAFE_ENTRY_HORIZONTAL_RADIUS = 15;
@@ -312,18 +315,75 @@ public final class WorldRuntimeControlService {
}
public CompletableFuture<Boolean> teleport(Player player, Location location) {
return scheduleTeleport(player, location, null);
}
public CompletableFuture<Boolean> teleportInMode(
Player player,
Location location,
GameMode gameMode
) {
return scheduleTeleport(
player,
location,
Objects.requireNonNull(gameMode, "Teleport game mode"));
}
static CompletableFuture<Boolean> scheduleTeleport(
Player player,
Location location,
GameMode gameMode
) {
return scheduleTeleport(player, location, gameMode, PaperLib::teleportAsync);
}
static CompletableFuture<Boolean> scheduleTeleport(
Player player,
Location location,
GameMode gameMode,
TeleportExecutor teleporter
) {
if (player == null || location == null) {
return CompletableFuture.completedFuture(false);
}
Objects.requireNonNull(teleporter, "teleporter");
CompletableFuture<Boolean> future = new CompletableFuture<>();
GameModeRestore modeRestore = new GameModeRestore(player);
AtomicReference<CompletableFuture<Boolean>> activeTeleport = new AtomicReference<>();
future.whenComplete((success, failure) -> {
if (Boolean.TRUE.equals(success)) {
return;
}
CompletableFuture<Boolean> teleport = activeTeleport.get();
if (teleport != null && !teleport.isDone()) {
teleport.cancel(false);
}
modeRestore.restore();
});
boolean scheduled = J.runEntity(player, () -> {
try {
CompletableFuture<Boolean> teleportFuture = PaperLib.teleportAsync(player, location);
if (future.isDone()) {
return;
}
if (gameMode != null) {
modeRestore.apply(gameMode);
if (future.isDone()) {
modeRestore.restore();
return;
}
}
CompletableFuture<Boolean> teleportFuture = teleporter.teleport(player, location);
if (teleportFuture == null) {
future.complete(false);
return;
}
activeTeleport.set(teleportFuture);
if (future.isDone()) {
teleportFuture.cancel(false);
modeRestore.restore();
return;
}
teleportFuture.whenComplete((success, throwable) -> {
if (throwable != null) {
@@ -332,8 +392,9 @@ public final class WorldRuntimeControlService {
}
if (Boolean.TRUE.equals(success)) {
J.runEntity(player, () -> IrisServices.get(BoardSVC.class).updatePlayer(player));
future.complete(true);
if (future.complete(true)) {
J.runEntity(player, () -> IrisServices.get(BoardSVC.class).updatePlayer(player));
}
return;
}
@@ -344,7 +405,7 @@ public final class WorldRuntimeControlService {
}
});
if (!scheduled) {
return CompletableFuture.failedFuture(new IllegalStateException("Failed to schedule teleport for " + player.getName() + "."));
future.completeExceptionally(new IllegalStateException("Failed to schedule teleport for " + player.getName() + "."));
}
return future;
@@ -431,62 +492,6 @@ public final class WorldRuntimeControlService {
return null;
}
static Location findTopSafeStudioLocation(World world, Location source) {
Location dryLocation = findTopSafeLocation(world, source);
if (dryLocation != null) {
return dryLocation;
}
int sourceX = source.getBlockX();
int sourceZ = source.getBlockZ();
int chunkX = sourceX >> 4;
int chunkZ = sourceZ >> 4;
if (!world.isChunkLoaded(chunkX, chunkZ)) {
return null;
}
int minimumFloorY = world.getMinHeight();
int maximumFloorY = world.getMaxHeight() - 3;
if (minimumFloorY > maximumFloorY) {
return null;
}
int minimumX = chunkX << 4;
int minimumZ = chunkZ << 4;
int maximumX = minimumX + 15;
int maximumZ = minimumZ + 15;
for (int radius = 0; radius <= MAX_SAFE_ENTRY_HORIZONTAL_RADIUS; radius++) {
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
if (Math.max(Math.abs(offsetX), Math.abs(offsetZ)) != radius) {
continue;
}
int x = sourceX + offsetX;
int z = sourceZ + offsetZ;
if (x < minimumX || x > maximumX || z < minimumZ || z > maximumZ) {
continue;
}
Location waterLocation = findSafeWaterSurfaceLocationInColumn(
world,
x,
z,
minimumFloorY,
maximumFloorY,
source.getYaw(),
source.getPitch()
);
if (waterLocation != null) {
return waterLocation;
}
}
}
}
return null;
}
private static Location findSafeLocationInColumn(
World world,
int x,
@@ -515,48 +520,6 @@ public final class WorldRuntimeControlService {
return null;
}
private static Location findSafeWaterSurfaceLocationInColumn(
World world,
int x,
int z,
int minimumFloorY,
int maximumFloorY,
float yaw,
float pitch
) {
int surfaceY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
if (surfaceY <= minimumFloorY || surfaceY > maximumFloorY) {
return null;
}
Block surface = world.getBlockAt(x, surfaceY, z);
if (!isStableWater(surface)) {
return null;
}
Block support = world.getBlockAt(x, surfaceY - 1, z);
if (!isStableWater(support) && !isSafeFloor(support)) {
return null;
}
Block feet = world.getBlockAt(x, surfaceY + 1, z);
Block head = world.getBlockAt(x, surfaceY + 2, z);
if (!isClearEntryBlock(feet) || !isClearEntryBlock(head)) {
return null;
}
return new Location(world, x + BLOCK_CENTER, surfaceY + 1D, z + BLOCK_CENTER, yaw, pitch);
}
private static boolean isStableWater(Block block) {
if (block.getType() != Material.WATER || !block.isLiquid()) {
return false;
}
BlockData blockData = block.getBlockData();
return blockData instanceof Levelled levelled && levelled.getLevel() == 0;
}
private static boolean isSafeFloor(Block block) {
Material material = block.getType();
if (material == null
@@ -813,4 +776,50 @@ public final class WorldRuntimeControlService {
Method method = instance.getClass().getMethod(methodName);
return method.invoke(instance);
}
@FunctionalInterface
interface TeleportExecutor {
CompletableFuture<Boolean> teleport(Player player, Location location);
}
private static final class GameModeRestore {
private final Player player;
private final AtomicBoolean changed;
private final AtomicBoolean restored;
private GameMode previousMode;
private GameModeRestore(Player player) {
this.player = player;
changed = new AtomicBoolean(false);
restored = new AtomicBoolean(false);
}
private void apply(GameMode targetMode) {
GameMode currentMode = player.getGameMode();
if (currentMode == targetMode) {
return;
}
previousMode = currentMode;
changed.set(true);
player.setGameMode(targetMode);
}
private void restore() {
if (!changed.get() || !restored.compareAndSet(false, true)) {
return;
}
Runnable restoration = () -> {
try {
player.setGameMode(previousMode);
} catch (Throwable failure) {
IrisLogging.reportError("Failed to restore a player's game mode after an unsuccessful teleport.", failure);
}
};
if (!J.runEntity(player, restoration)) {
IrisLogging.reportError(
"Failed to restore a player's game mode after an unsuccessful teleport.",
new IllegalStateException("The player entity scheduler rejected the game-mode restoration."));
}
}
}
}
@@ -66,7 +66,7 @@ public class ExternalDataSVC implements IrisService {
@Override
public void onEnable() {
IrisLogging.info("Loading ExternalDataProvider...");
IrisLogging.debug("Loading ExternalDataProvider...");
// enable() registers every enabled service as a listener; self-registration here
// doubled every handler invocation.
@@ -147,7 +147,8 @@ public class ExternalDataSVC implements IrisService {
public Optional<ItemStack> getItemStack(Identifier key, KMap<String, Object> customNbt) {
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(key, DataType.ITEM)).findFirst();
if (provider.isEmpty()) {
IrisLogging.warn("No matching Provider found for modded material \"%s\"!", key);
IrisLogging.warnOnce("external-provider:item:" + key,
"No matching Provider found for modded material \"%s\"!", key);
return Optional.empty();
}
try {
@@ -161,7 +162,8 @@ public class ExternalDataSVC implements IrisService {
public void processUpdate(Engine engine, Block block, Identifier blockId) {
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(blockId, DataType.BLOCK)).findFirst();
if (provider.isEmpty()) {
IrisLogging.warn("No matching Provider found for modded material \"%s\"!", blockId);
IrisLogging.warnOnce("external-provider:block:" + blockId,
"No matching Provider found for modded material \"%s\"!", blockId);
return;
}
provider.get().processUpdate(engine, block, blockId);
@@ -170,7 +172,8 @@ public class ExternalDataSVC implements IrisService {
public Entity spawnMob(Location location, Identifier mobId) {
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(mobId, DataType.ENTITY)).findFirst();
if (provider.isEmpty()) {
IrisLogging.warn("No matching Provider found for modded mob \"%s\"!", mobId);
IrisLogging.warnOnce("external-provider:mob:" + mobId,
"No matching Provider found for modded mob \"%s\"!", mobId);
return null;
}
try {
@@ -243,7 +246,7 @@ public class ExternalDataSVC implements IrisService {
Class<?> rawProviderClass = Class.forName(definition.className(), true, ExternalDataSVC.class.getClassLoader());
Class<? extends ExternalDataProvider> providerClass = rawProviderClass.asSubclass(ExternalDataProvider.class);
ExternalDataProvider provider = providerClass.getDeclaredConstructor().newInstance();
IrisLogging.info(definition.pluginId() + " found, loading " + providerClass.getSimpleName() + "...");
IrisLogging.debug(definition.pluginId() + " found, loading " + providerClass.getSimpleName() + "...");
activateProvider(provider);
} catch (Throwable error) {
IrisLogging.reportError("Failed to create Iris external data provider " + definition.className() + ".", error);
@@ -263,7 +266,7 @@ public class ExternalDataSVC implements IrisService {
providers.add(provider);
}
activeProviders.add(provider);
IrisLogging.info("Enabled ExternalDataProvider for %s.", provider.getPluginId());
IrisLogging.debug("Enabled ExternalDataProvider for %s.", provider.getPluginId());
} catch (Throwable error) {
IrisLogging.reportError("Failed to enable Iris external data provider " + provider.getPluginId() + ".", error);
}
@@ -362,7 +362,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
evaluations.remove(displacedRequestId);
previewRenderer.removeRequest(displacedRequestId);
}
IrisLogging.info("Jigsaw Studio authoring registered: world=%s structure=%s bays=%d",
IrisLogging.debug("Jigsaw Studio authoring registered: world=%s structure=%s bays=%d",
world.getName(), activeGenerator.getSession().structureKey(), activeGenerator.getLayout().bays().size());
scheduleInitialEvaluation(next);
scheduleOnlinePlayers(world.getUID());
@@ -460,7 +460,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
previewRenderer.forgetRequest(request.requestId());
JigsawStudioActivation.deactivate(request.packKey(), request.requestId());
clearWorldPlayerContexts(world.getUID());
IrisLogging.info("Jigsaw Studio authoring unregistered: world=%s", world.getName());
IrisLogging.debug("Jigsaw Studio authoring unregistered: world=%s", world.getName());
}
private void scheduleUnregisterRetry(World world, UUID requestId) {
@@ -2385,7 +2385,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
request.source().invalidateStructureResources();
message(player, "Deleted Jigsaw project '" + request.structureKey() + "' and "
+ result.removedResourceCount() + " owned resource(s).");
IrisLogging.info("Jigsaw Studio project deleted: structure=%s resources=%d",
IrisLogging.debug("Jigsaw Studio project deleted: structure=%s resources=%d",
request.structureKey(), result.removedResourceCount());
} catch (Throwable exception) {
IrisLogging.reportError(exception);
@@ -6086,7 +6086,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
if (unchanged) {
scheduleEvaluation(studio);
}
IrisLogging.info("Jigsaw Studio saved: structure=%s piece=%s object=%s connectors=%d status=%s",
IrisLogging.debug("Jigsaw Studio saved: structure=%s piece=%s object=%s connectors=%d status=%s",
request.structureKey(), coordinator.saveIdentity().variantKey(), assembly.objectKey(),
connectors.size(), result.status());
} catch (Throwable exception) {
@@ -107,7 +107,7 @@ public class ObjectStudioSaveService implements IrisService {
String packKey = engine.getDimension() == null ? null : engine.getDimension().getLoadKey();
studios.put(world.getUID(), new ActiveStudio(world.getUID(), layout, objectsDirs, packKey));
IrisLogging.info("Object Studio live-save registered: world=%s cells=%d packs=%d",
IrisLogging.debug("Object Studio live-save registered: world=%s cells=%d packs=%d",
world.getName(), layout.cells().size(), objectsDirs.size());
}
@@ -118,7 +118,7 @@ public class ObjectStudioSaveService implements IrisService {
if (removed.packKey != null) {
ObjectStudioActivation.deactivate(removed.packKey);
}
IrisLogging.info("Object Studio live-save unregistered: world=%s", world.getName());
IrisLogging.debug("Object Studio live-save unregistered: world=%s", world.getName());
}
}
@@ -147,7 +147,7 @@ public class ObjectStudioSaveService implements IrisService {
}
player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVING_X_X, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())), MessageArgument.untrusted("w", String.valueOf(cell.w())), MessageArgument.untrusted("h", String.valueOf(cell.h())), MessageArgument.untrusted("d", String.valueOf(cell.d()))));
IrisLogging.info("Object Studio save triggered by %s for %s/%s", player.getName(), cell.pack(), cell.key());
IrisLogging.debug("Object Studio save triggered by %s for %s/%s", player.getName(), cell.pack(), cell.key());
J.runRegion(world, cell.chunkMinX(), cell.chunkMinZ(), () -> {
try {
captureAndSave(studio, world, cell, player);
@@ -192,7 +192,7 @@ public class ObjectStudioSaveService implements IrisService {
double targetY = cell.originY() + cell.h() + 2.0D;
Location location = new Location(world, targetX, targetY, targetZ);
J.runEntity(player, () -> PaperLib.teleportAsync(player, location));
IrisLogging.info("Object Studio goto: %s -> %s at %.0f,%.0f,%.0f",
IrisLogging.debug("Object Studio goto: %s -> %s at %.0f,%.0f,%.0f",
player.getName(), objectKey, location.getX(), location.getY(), location.getZ());
return true;
}
@@ -266,7 +266,7 @@ public class ObjectStudioSaveService implements IrisService {
parent.mkdirs();
}
snapshot.write(targetFile);
IrisLogging.info("Object Studio saved: %s/%s (%dx%dx%d)",
IrisLogging.debug("Object Studio saved: %s/%s (%dx%dx%d)",
cell.pack(), cell.key(), cell.w(), cell.h(), cell.d());
if (notify != null) {
J.runEntity(notify, () -> notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVED, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())))));
@@ -63,7 +63,7 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
p += i.getUsage();
}
IrisLogging.info("Cached " + Form.f(s) + " / " + Form.f(m) + " (" + Form.pc(p / mf) + ") from " + caches.size() + " Caches");
IrisLogging.debug("Cached " + Form.f(s) + " / " + Form.f(m) + " (" + Form.pc(p / mf) + ") from " + caches.size() + " Caches");
}
public void dereference() {
@@ -104,7 +104,7 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
if (i.isAlive()) {
try {
i.interrupt();
IrisLogging.info("Shutdown Thread " + i.getName());
IrisLogging.debug("Shutdown Thread " + i.getName());
} catch (Throwable e) {
IrisLogging.reportError(e);
}
@@ -114,7 +114,7 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
for (ExecutorService i : services) {
try {
i.shutdownNow();
IrisLogging.info("Shutdown Executor Service " + i);
IrisLogging.debug("Shutdown Executor Service " + i);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
@@ -213,7 +213,6 @@ public class StudioSVC implements IrisService {
IrisData previousData = IrisData.getLoaded(target.toFile()).orElse(null);
IrisData createdData = null;
boolean refreshedPreviousData = false;
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_INSTALLING_PACKAGE, MessageArgument.untrusted("name", String.valueOf(source.getFileName())), MessageArgument.untrusted("loadKey", String.valueOf(dimensionKey))));
try {
if (parent == null) {
throw new IOException("World pack target has no parent: " + target);
@@ -556,10 +555,23 @@ public class StudioSVC implements IrisService {
}
public void open(VolmitSender sender, long seed, String dimm, Consumer<World> onDone) throws IrisException {
open(sender, seed, dimm, StudioOpenCoordinator.StudioOpenKind.STANDARD, onDone);
}
public void open(
VolmitSender sender,
long seed,
String dimm,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
) throws IrisException {
if (reportPackAdmissionFailure(sender, dimm) != null) {
return;
}
studioTransitions.submit(() -> replaceActiveProject(sender, seed, dimm, onDone))
StudioOpenCoordinator.StudioOpenKind requiredOpenKind = Objects.requireNonNull(
openKind,
"Studio open kind");
studioTransitions.submit(() -> replaceActiveProject(sender, seed, dimm, requiredOpenKind, onDone))
.whenComplete((ignored, throwable) -> {
if (throwable == null) {
return;
@@ -650,6 +662,7 @@ public class StudioSVC implements IrisService {
VolmitSender sender,
long seed,
String dimension,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
) {
return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> {
@@ -678,7 +691,7 @@ public class StudioSVC implements IrisService {
}
return true;
}).thenCompose(closed -> closed
? beginStudioOpen(sender, seed, dimension, onDone)
? beginStudioOpen(sender, seed, dimension, openKind, onDone)
: CompletableFuture.completedFuture(null));
}
@@ -686,6 +699,7 @@ public class StudioSVC implements IrisService {
VolmitSender sender,
long seed,
String dimension,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
) {
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
@@ -695,7 +709,7 @@ public class StudioSVC implements IrisService {
opening = project.open(
sender,
seed,
StudioOpenCoordinator.StudioOpenKind.STANDARD,
openKind,
onDone);
} catch (IrisException e) {
if (activeProject == project) {
@@ -124,7 +124,6 @@ public final class StructureCaptureImporter {
failed++;
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STRUCTURE_CAPTURE_IMPORTER_FAIL_2, MessageArgument.untrusted("key", String.valueOf(key)), MessageArgument.untrusted("error", String.valueOf(e.getMessage()))));
IrisLogging.reportError(e);
e.printStackTrace();
}
int processed = imported + skipped + failed;
@@ -162,7 +162,6 @@ public final class StructureImporter {
structure = Bukkit.getStructureManager().loadStructure(key);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Failed to load structure " + key + ": " + e.getMessage(), 0, List.of(), true);
}
if (structure == null || structure.getPalettes().isEmpty()) {
@@ -174,7 +173,6 @@ public final class StructureImporter {
captured = captureStructure(structure);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Failed to capture structure " + key + ": " + e.getMessage(), 0, List.of(), true);
}
@@ -221,7 +219,6 @@ public final class StructureImporter {
return new Result(false, "Failed writing import for '" + name + "': " + e.getMessage(), count, losses);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Failed writing import for '" + name + "': " + e.getMessage(), count, losses,
true);
}
@@ -353,7 +350,6 @@ public final class StructureImporter {
private static void reportWriteFailure(StructureWriteResult result) {
result.failure().ifPresent((Throwable failure) -> {
IrisLogging.reportError(failure);
failure.printStackTrace();
});
}
@@ -395,7 +391,6 @@ public final class StructureImporter {
} catch (Throwable e) {
IrisLogging.reportError(e);
if (VillageImporter.shouldPrintFullTrace(e)) {
e.printStackTrace();
}
return new FinalStateResult(null, false);
}
@@ -432,7 +427,6 @@ public final class StructureImporter {
return LegacyTileData.fromBukkit(block);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return null;
}
}
@@ -465,7 +459,6 @@ public final class StructureImporter {
maxSpan = Math.max(maxSpan, readObjectSpan(iob));
} catch (IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Cannot read imported object '" + rel + "': " + e.getMessage(), 0,
List.of(), true);
}
@@ -508,7 +501,6 @@ public final class StructureImporter {
+ " template variants" + writeResultNote(writeResult), pieceNames.size(), List.of());
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Failed writing group structure '" + groupName + "': " + e.getMessage(), 0,
List.of(), true);
}
@@ -598,7 +590,6 @@ public final class StructureImporter {
return new Result(false, "Failed writing capture for '" + name + "': " + e.getMessage(), 0, List.of());
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Failed writing capture for '" + name + "': " + e.getMessage(), 0, List.of(),
true);
}
@@ -1204,7 +1204,6 @@ public final class VillageImporter {
private static void reportFailure(Throwable failure) {
IrisLogging.reportError(failure);
if (shouldPrintFullTrace(failure)) {
failure.printStackTrace();
}
}
@@ -154,7 +154,7 @@ public class IrisConverter {
} catch (Exception e) {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_CONVERTER_FAILED_CONVERT, MessageArgument.untrusted("name", String.valueOf(schem.getName()))));
e.printStackTrace();
IrisLogging.reportError("Failed to convert schematic " + schem.getName() + ".", e);
}
}
stopwatch.end();
@@ -184,4 +184,3 @@ public class IrisConverter {
}
}
}
@@ -103,10 +103,9 @@ public class IrisPackBenchmarking {
writer.write(" - Highest CPS: " + findHighest(cps) + "\n");
writer.write(" - Lowest CPS: " + findLowest(cps) + "\n");
writer.write("-----------------\n");
IrisLogging.info("Finished generating a report!");
IrisLogging.debug("Finished generating an Iris pack benchmark report.");
} catch (IOException e) {
IrisLogging.error("An error occurred writing to the file.");
e.printStackTrace();
IrisLogging.reportError("Failed to write the Iris pack benchmark report.", e);
}
J.sfut(() -> {
@@ -135,8 +134,7 @@ public class IrisPackBenchmarking {
stopwatch.end();
} catch (Exception e) {
IrisLogging.error("Something has gone wrong!");
e.printStackTrace();
IrisLogging.reportError("Iris pack benchmarking failed.", e);
}
}
@@ -18,22 +18,15 @@
package art.arcane.iris.core.tools;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeProgressMessages;
import art.arcane.iris.spi.IrisLogging;
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 art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.localization.TextKey;
import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -56,8 +49,6 @@ final class WorldCreationProgressReporter {
private final AtomicBoolean playerRenderQueued;
private final AtomicInteger taskId;
private final AtomicLong nextConsoleUpdate;
private volatile BossBar bossBar;
private volatile boolean hudDisabled;
private WorldCreationProgressReporter(VolmitSender sender, String worldName) {
this.sender = sender;
@@ -72,45 +63,14 @@ final class WorldCreationProgressReporter {
this.playerRenderQueued = new AtomicBoolean(false);
this.taskId = new AtomicInteger(-1);
this.nextConsoleUpdate = new AtomicLong(0L);
this.bossBar = null;
this.hudDisabled = false;
}
static WorldCreationProgressReporter start(VolmitSender sender, String worldName) {
WorldCreationProgressReporter reporter = new WorldCreationProgressReporter(sender, worldName);
if (sender.isPlayer() && sender.player() != null
&& IrisSettings.get().getGeneral().isProgressBossBar()) {
try {
J.sfut(reporter::initializePlayerHud).get(5L, TimeUnit.SECONDS);
} catch (Throwable failure) {
reporter.hudDisabled = true;
J.runGlobal(reporter::releaseHud);
IrisLogging.reportError("Failed to initialize world creation progress HUD for \""
+ worldName + "\".", failure);
}
}
reporter.taskId.set(J.ar(reporter::tick, 3));
return reporter;
}
private void initializePlayerHud() {
if (hudDisabled) {
return;
}
bossBar = Bukkit.createBossBar(
IrisLanguage.text(
RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_WORKING,
MessageArgument.untrusted("world", worldName)
),
BarColor.BLUE,
BarStyle.SEGMENTED_20
);
bossBar.setProgress(0.01D);
bossBar.addPlayer(sender.player());
bossBar.setVisible(true);
}
void update(double progress, String stage) {
update(progress, stage, "");
}
@@ -151,29 +111,19 @@ final class WorldCreationProgressReporter {
if (!terminalRendered.compareAndSet(false, true)) {
return;
}
renderTerminal(currentProgress, currentStage, currentDetail, percent, elapsed);
renderTerminal(currentProgress, currentStage, currentDetail, elapsed);
return;
}
if (sender.isPlayer() && sender.player() != null) {
if (hasPlayerHud()) {
schedulePlayerRender(() -> renderPlayerProgress(
currentProgress,
currentStage,
currentDetail,
percent,
elapsed
));
} else {
schedulePlayerRender(() -> sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION,
MessageArgument.trusted("bar", buildPlayerBar(currentProgress)),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("detail", currentDetail),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
)));
}
schedulePlayerRender(() -> sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION,
MessageArgument.trusted("bar", buildPlayerBar(currentProgress)),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("detail", currentDetail),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
)));
return;
}
@@ -192,59 +142,20 @@ final class WorldCreationProgressReporter {
}
}
private void renderPlayerProgress(
double currentProgress,
String currentStage,
String currentDetail,
int percent,
long elapsed
) {
bossBar.setProgress(currentProgress);
bossBar.setTitle(IrisLanguage.text(
RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_PROGRESS,
MessageArgument.untrusted("world", worldName),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage)
));
sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION,
MessageArgument.trusted("bar", buildPlayerBar(currentProgress)),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("detail", currentDetail),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
));
}
private void renderTerminal(
double currentProgress,
String currentStage,
String currentDetail,
int percent,
long elapsed
) {
if (sender.isPlayer() && sender.player() != null) {
if (hasPlayerHud()) {
schedulePlayerTerminalRender(() -> renderPlayerTerminal(
currentProgress,
currentStage,
currentDetail,
percent,
elapsed
));
} else {
schedulePlayerTerminalRender(() -> sender.sendAction(IrisLanguage.text(
failed.get()
? RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_FAILED
: RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_READY,
MessageArgument.trusted("bar", buildPlayerBar(currentProgress)),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("detail", currentDetail),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 1))
)));
}
schedulePlayerTerminalRender(() -> sender.sendAction(terminalPlayerMessage(
failed.get(),
currentProgress,
currentStage,
currentDetail,
elapsed
)));
return;
}
@@ -257,48 +168,6 @@ final class WorldCreationProgressReporter {
));
}
private void renderPlayerTerminal(
double currentProgress,
String currentStage,
String currentDetail,
int percent,
long elapsed
) {
bossBar.setProgress(currentProgress);
bossBar.setColor(failed.get() ? BarColor.RED : BarColor.GREEN);
bossBar.setTitle(IrisLanguage.text(
failed.get()
? RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_FAILED
: RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_READY,
MessageArgument.untrusted("world", worldName),
MessageArgument.trusted("percent", percent)
));
sender.sendAction(IrisLanguage.text(
failed.get()
? RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_FAILED
: RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_READY,
MessageArgument.trusted("bar", buildPlayerBar(currentProgress)),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("detail", currentDetail),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 1))
));
Runnable cleanup = () -> {
bossBar.removeAll();
bossBar.setVisible(false);
};
J.runEntity(sender.player(), cleanup, 60, cleanup);
}
private boolean hasPlayerHud() {
return !hudDisabled
&& sender.isPlayer()
&& sender.player() != null
&& bossBar != null;
}
private void schedulePlayerRender(Runnable render) {
if (!playerRenderQueued.compareAndSet(false, true)) {
return;
@@ -314,24 +183,10 @@ final class WorldCreationProgressReporter {
return;
}
playerRenderQueued.set(false);
hudDisabled = true;
J.runGlobal(this::releaseHud);
}
private void schedulePlayerTerminalRender(Runnable render) {
if (J.runEntity(sender.player(), render)) {
return;
}
hudDisabled = true;
J.runGlobal(this::releaseHud);
}
private void releaseHud() {
BossBar activeBossBar = bossBar;
if (activeBossBar != null) {
activeBossBar.removeAll();
activeBossBar.setVisible(false);
}
J.runEntity(sender.player(), render);
}
private void cancel() {
@@ -359,6 +214,29 @@ final class WorldCreationProgressReporter {
return buildBar(progress, CONSOLE_BAR_WIDTH, false);
}
static String terminalPlayerMessage(
boolean failed,
double progress,
String stage,
String detail,
long elapsed
) {
if (failed) {
return IrisLanguage.text(
RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_FAILED,
MessageArgument.trusted("bar", buildPlayerBar(progress)),
MessageArgument.trusted("stage", stage),
MessageArgument.trusted("detail", detail),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 1))
);
}
return IrisLanguage.text(
RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_READY,
MessageArgument.trusted("bar", buildPlayerBar(progress)),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 1))
);
}
private static String buildBar(double progress, int width, boolean colored) {
int filled = (int) Math.round(clampProgress(progress) * width);
StringBuilder bar = new StringBuilder(colored ? width * 3 + 4 : width + 2);
@@ -75,7 +75,6 @@ final class EngineDataStore {
}
} catch (IOException | JsonParseException e) {
IrisLogging.reportError(e);
e.printStackTrace();
throw new IllegalStateException("Failed to read Iris engine data without modifying it: " + f.getAbsolutePath(), e);
}
}
@@ -94,7 +93,6 @@ final class EngineDataStore {
engineDataEstablished = true;
} catch (IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
throw new IllegalStateException("Failed to create Iris engine data: " + f.getAbsolutePath(), e);
}
}
@@ -141,7 +139,6 @@ final class EngineDataStore {
} catch (IOException e) {
IrisLogging.error("Failed to save Engine Data");
IrisLogging.reportError(e);
e.printStackTrace();
throw new IllegalStateException("Failed to save Iris engine data: " + f.getAbsolutePath(), e);
}
}
@@ -19,8 +19,6 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.localization.ClientUiMessages;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
@@ -31,9 +29,7 @@ import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.localization.MessageArgument;
import static art.arcane.iris.engine.EngineShutdownSequence.runCleanup;
@@ -132,7 +128,6 @@ final class EngineHotloader {
if (previousDataFailure != null) {
IrisLogging.error("Failed to completely release the previous Iris data runtime.");
IrisLogging.reportError(previousDataFailure);
previousDataFailure.printStackTrace();
}
engine.getPrefetchSaveStarted().set(false);
engine.getEngineData().getStatistics().hotloaded();
@@ -190,13 +185,8 @@ final class EngineHotloader {
if (protocolServer == null) {
return;
}
IrisDimension dimension = engine.getDimension();
String packKey = dimension == null ? "" : dimension.getLoadKey();
String packKey = engine.getData().getDataFolder().getName();
protocolServer.broadcastStudioHotload(packKey, 0, failed, message);
protocolServer.broadcastToast(
failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS,
IrisLanguage.plain(ClientUiMessages.TOAST_STUDIO_HOTLOAD),
failed ? IrisLanguage.plain(ClientUiMessages.TOAST_PACK_FAILED, MessageArgument.untrusted("pack", packKey)) : packKey);
} catch (Throwable broadcastFailure) {
IrisLogging.error("Iris studio hotload broadcast failed: " + broadcastFailure.getClass().getSimpleName()
+ ": " + broadcastFailure.getMessage());
@@ -172,7 +172,6 @@ final class EngineRuntimeBuilder {
} catch (Throwable e) {
engineRuntime.hash32().completeExceptionally(e);
IrisLogging.reportError(e);
e.printStackTrace();
}
try {
if (!engine.backgroundTasks.scheduleTrackedTask(() -> engine.getPlatformHooks().refreshDatapackWorkspace(engine))) {
@@ -180,7 +179,6 @@ final class EngineRuntimeBuilder {
}
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
@@ -219,7 +217,6 @@ final class EngineRuntimeBuilder {
} catch (Throwable e) {
configuredFailure = e;
IrisLogging.reportError(e);
e.printStackTrace();
if (engine.getModeFallbackLogged().compareAndSet(false, true)) {
IrisLogging.warn("Failed to initialize configured dimension mode for " + engine.getDimension().getLoadKey() + ", falling back to OVERWORLD mode.");
}
@@ -156,7 +156,6 @@ final class EngineShutdownSequence {
private void reportIncompleteClose(Throwable failure) {
IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + ".");
IrisLogging.reportError(failure);
failure.printStackTrace();
throw new IllegalStateException("Iris engine shutdown remains incomplete after cleanup failures.", failure);
}
@@ -68,7 +68,6 @@ final class EngineTickRegistry {
engine.tickRandomPlayer();
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
}
@@ -224,7 +224,6 @@ public class IrisEngine implements Engine {
StructureIndexService.writeOnce(getData());
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
IrisLogging.notice("Engine init: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " seed=" + getSeedManager().getSeed());
_t0 = M.ms();
@@ -653,7 +652,6 @@ public class IrisEngine implements Engine {
failing = true;
IrisLogging.error(error);
IrisLogging.reportError(e);
e.printStackTrace();
}
@Override
@@ -27,7 +27,6 @@ import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.math.M;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -41,11 +40,13 @@ import java.util.concurrent.atomic.AtomicLong;
public class IrisEngineEffects extends EngineAssignedComponent implements EngineEffects {
private static final long EFFECT_BUDGET_NANOS = 1_500_000L;
private static final long EMPTY_PLAYER_REFRESH_NANOS = 1_000_000_000L;
private static final EnginePlayer[] NO_PLAYERS = new EnginePlayer[0];
private final ConcurrentHashMap<UUID, EnginePlayer> players;
private final Semaphore limit;
private final AtomicBoolean playerMapUpdateQueued;
private final AtomicLong nextEmptyRefresh;
private volatile EnginePlayer[] playerSnapshot;
public IrisEngineEffects(Engine engine) {
super(engine, "FX");
@@ -53,6 +54,7 @@ public class IrisEngineEffects extends EngineAssignedComponent implements Engine
limit = new Semaphore(1);
playerMapUpdateQueued = new AtomicBoolean(false);
nextEmptyRefresh = new AtomicLong(0L);
playerSnapshot = NO_PLAYERS;
}
@Override
@@ -89,6 +91,7 @@ public class IrisEngineEffects extends EngineAssignedComponent implements Engine
}
players.keySet().removeIf((UUID playerId) -> !activeIds.contains(playerId));
playerSnapshot = players.values().toArray(EnginePlayer[]::new);
}
@Override
@@ -97,7 +100,8 @@ public class IrisEngineEffects extends EngineAssignedComponent implements Engine
return;
}
try {
if (players.isEmpty()) {
EnginePlayer[] snapshot = playerSnapshot;
if (snapshot.length == 0) {
long now = System.nanoTime();
long next = nextEmptyRefresh.get();
if (now >= next && nextEmptyRefresh.compareAndSet(next, now + EMPTY_PLAYER_REFRESH_NANOS)) {
@@ -110,15 +114,14 @@ public class IrisEngineEffects extends EngineAssignedComponent implements Engine
return;
}
List<EnginePlayer> snapshot = new ArrayList<>(players.values());
if (snapshot.isEmpty()) {
return;
}
long started = System.nanoTime();
int remaining = snapshot.size();
int index = ThreadLocalRandom.current().nextInt(snapshot.length);
int remaining = snapshot.length;
while (remaining-- > 0 && System.nanoTime() - started < EFFECT_BUDGET_NANOS) {
snapshot.get(ThreadLocalRandom.current().nextInt(snapshot.size())).tick();
snapshot[index].tick();
if (++index == snapshot.length) {
index = 0;
}
}
} finally {
limit.release();
@@ -325,7 +325,6 @@ public class IrisEngineMantle implements EngineMantle {
panic.add("read.byte.range", start + " " + end);
panic.add("read.byte.current", din.count() + "");
IrisLogging.reportError(error);
error.printStackTrace();
panic.panic();
TectonicPlate.addError();
}
@@ -350,7 +349,6 @@ public class IrisEngineMantle implements EngineMantle {
panic.add("read.byte.range", start + " " + end);
panic.add("read.byte.current", din.count() + "");
IrisLogging.reportError(error);
error.printStackTrace();
panic.panic();
}
@@ -43,10 +43,10 @@ import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
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;
@@ -63,7 +63,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
private final ChronoLatch chunkUpdater;
private final ChronoLatch chunkDiscovery;
private final KMap<Long, Future<?>> cleanup = new KMap<>();
private final ScheduledExecutorService cleanupService;
private final ScheduledThreadPoolExecutor cleanupService;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final WorldEntitySpawner entitySpawner = new WorldEntitySpawner(this);
@@ -80,7 +80,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
@Setter(AccessLevel.NONE)
final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup(this);
private boolean looperStopped;
private boolean cleanupServiceStopped;
private volatile boolean cleanupServiceStopped;
volatile int entityCount = 0;
volatile boolean entityCountValid = false;
volatile boolean playersPresent = false;
@@ -105,11 +105,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
cln = new ChronoLatch(60000);
cl = new ChronoLatch(3000);
clw = new ChronoLatch(1000, true);
cleanupService = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable, "Iris Mantle Cleanup " + getTarget().getWorld().name());
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
cleanupService = createCleanupExecutor(getTarget().getWorld().name());
looper = new Looper() {
@Override
protected long loop() {
@@ -126,6 +122,17 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
looper.setName("Iris World Manager " + getTarget().getWorld().name());
}
static ScheduledThreadPoolExecutor createCleanupExecutor(String worldName) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, runnable -> {
Thread thread = new Thread(runnable, "Iris Mantle Cleanup " + worldName);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
executor.setRemoveOnCancelPolicy(true);
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
return executor;
}
private long runLoop() {
if (getEngine().isClosed()) {
looper.interrupt();
@@ -288,6 +295,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
try {
cleanupServiceStopped = true;
if (cleanupService != null) {
for (Future<?> future : cleanup.values()) {
future.cancel(false);
}
cleanup.clear();
cleanupService.shutdownNow();
awaitQuietly(cleanupService);
}
@@ -41,11 +41,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -64,6 +60,7 @@ final class WorldChunkMaintenance {
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean();
private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean();
private volatile Position2[] loadedChunkPositions = new Position2[0];
private int forcedChunkUpdateCursor = 0;
WorldChunkMaintenance(IrisWorldManager manager) {
@@ -182,8 +179,14 @@ final class WorldChunkMaintenance {
}
List<Player> players = new ArrayList<>(world.getPlayers());
Chunk[] loadedChunks = world.getLoadedChunks();
Position2[] currentLoadedChunkPositions = new Position2[loadedChunks.length];
for (int i = 0; i < loadedChunks.length; i++) {
currentLoadedChunkPositions[i] = new Position2(loadedChunks[i].getX(), loadedChunks[i].getZ());
}
loadedChunkPositions = currentLoadedChunkPositions;
manager.playersPresent = !players.isEmpty();
manager.loadedChunkCount = world.getLoadedChunks().length;
manager.loadedChunkCount = currentLoadedChunkPositions.length;
for (Player player : players) {
if (player == null) {
continue;
@@ -373,33 +376,6 @@ final class WorldChunkMaintenance {
if (world == null) {
return new Position2[0];
}
CompletableFuture<Position2[]> future = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
Chunk[] chunks = world.getLoadedChunks();
Position2[] positions = new Position2[chunks.length];
for (int i = 0; i < chunks.length; i++) {
positions[i] = new Position2(chunks[i].getX(), chunks[i].getZ());
}
manager.loadedChunkCount = positions.length;
future.complete(positions);
} catch (Throwable e) {
future.completeExceptionally(e);
}
});
if (!scheduled) {
return new Position2[0];
}
try {
return future.get(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new Position2[0];
} catch (ExecutionException | TimeoutException e) {
IrisLogging.reportError(e);
return new Position2[0];
}
return loadedChunkPositions;
}
}
@@ -42,29 +42,23 @@ import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Ambient and marker entity spawning for a Bukkit Iris world. Every count and spawn hops onto the
* thread that owns the data it reads (global for whole-world scans, entity for Folia candidate
* scans, region for the chunk being populated) and spawning is paused whenever a count could not
* be completed, so an incomplete saturation reading can never authorize a spawn.
* thread that owns the data it reads (global for synchronized whole-world snapshots, region for
* the chunk being populated) and spawning is paused whenever a count could not be completed, so
* an incomplete saturation reading can never authorize a spawn.
*/
final class WorldEntitySpawner {
private final IrisWorldManager manager;
@@ -101,26 +95,11 @@ final class WorldEntitySpawner {
if (realWorld == null) {
manager.entityCount = 0;
manager.entityCountValid = false;
} else if (J.isFolia()) {
Integer count = getFoliaLivingEntityCount(realWorld);
if (count != null) {
manager.entityCount = count;
manager.entityCountValid = true;
resetEntityCountFailures();
} else {
manager.entityCountValid = false;
}
} else {
CompletableFuture<Integer> future = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
int count = 0;
for (Entity entity : realWorld.getEntities()) {
if (entity instanceof LivingEntity && !entity.isDead()) {
count++;
}
}
future.complete(count);
future.complete(livingEntityCount(realWorld));
} catch (Throwable ex) {
future.completeExceptionally(ex);
}
@@ -184,6 +163,10 @@ final class WorldEntitySpawner {
return actuallySpawned.get() > 0;
}
static int livingEntityCount(World world) {
return world.getLivingEntities().size();
}
boolean isPregenActiveForThisWorld() {
World world = BukkitWorldBinding.world(manager.getEngine().getWorld());
if (world == null) {
@@ -202,118 +185,6 @@ final class WorldEntitySpawner {
return job.targetsWorldIdentity(WorldIdentity.serialize(world));
}
private Integer getFoliaLivingEntityCount(World world) {
CompletableFuture<List<Player>> playerFuture = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
playerFuture.complete(new ArrayList<>(world.getPlayers()));
} catch (Throwable e) {
playerFuture.completeExceptionally(e);
}
});
if (!scheduled) {
reportEntityCountFailure("Unable to schedule the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null);
return null;
}
List<Player> players;
try {
players = playerFuture.get(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
manager.entityCountValid = false;
Thread.currentThread().interrupt();
return null;
} catch (TimeoutException e) {
reportEntityCountFailure("Timed out while reading the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null);
return null;
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportEntityCountFailure("Failed to read the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", cause);
return null;
}
Map<String, Entity> candidates = new ConcurrentHashMap<>();
AtomicBoolean incomplete = new AtomicBoolean();
AtomicReference<Throwable> failure = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(players.size());
for (Player player : players) {
if (player == null) {
latch.countDown();
continue;
}
if (!J.runEntity(player, () -> {
try {
if (!player.isOnline() || !world.equals(player.getWorld())) {
return;
}
candidates.put(player.getUniqueId().toString(), player);
for (Entity nearby : player.getNearbyEntities(64, 64, 64)) {
if (nearby != null) {
candidates.put(nearby.getUniqueId().toString(), nearby);
}
}
} catch (Throwable e) {
incomplete.set(true);
failure.compareAndSet(null, e);
} finally {
latch.countDown();
}
})) {
incomplete.set(true);
latch.countDown();
}
}
if (!awaitEntityTasks(latch, 2, TimeUnit.SECONDS) || incomplete.get()) {
if (!Thread.currentThread().isInterrupted()) {
reportEntityCountFailure("The Folia entity candidate scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get());
}
return null;
}
AtomicInteger count = new AtomicInteger();
incomplete.set(false);
failure.set(null);
CountDownLatch entityLatch = new CountDownLatch(candidates.size());
for (Entity entity : candidates.values()) {
if (!J.runEntity(entity, () -> {
try {
if (entity instanceof LivingEntity && world.equals(entity.getWorld()) && !entity.isDead()) {
count.incrementAndGet();
}
} catch (Throwable e) {
incomplete.set(true);
failure.compareAndSet(null, e);
} finally {
entityLatch.countDown();
}
})) {
incomplete.set(true);
entityLatch.countDown();
}
}
if (!awaitEntityTasks(entityLatch, 2, TimeUnit.SECONDS) || incomplete.get()) {
if (!Thread.currentThread().isInterrupted()) {
reportEntityCountFailure("The Folia entity validation scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get());
}
return null;
}
return count.get();
}
static boolean awaitEntityTasks(CountDownLatch latch, long timeout, TimeUnit unit) {
try {
return latch.await(timeout, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
private boolean spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) {
if (world == null) {
return false;
@@ -64,7 +64,7 @@ public class AtomicCache<T> {
try {
return t.get();
} catch (Throwable e) {
e.printStackTrace();
IrisLogging.reportError("Atomic cache supplier failed.", e);
return null;
}
});
@@ -91,7 +91,6 @@ public final class StructureGraphCatalog {
}
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return false;
}
}
@@ -46,7 +46,7 @@ public final class MantleSliceRetention {
return;
}
if (retained.add(className)) {
IrisLogging.info("Mantle slice retained across chunk cleanup: " + className);
IrisLogging.debug("Mantle slice retained across chunk cleanup: " + className);
}
}
@@ -77,14 +77,14 @@ final class GoldenDebugObjectPlacer implements IObjectPlacer {
@Override
public int getHighest(int x, int z, IrisData data) {
int result = delegate.getHighest(x, z, data);
IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ")=" + result);
IrisLogging.debug("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ")=" + result);
return result;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
int result = delegate.getHighest(x, z, data, ignoreFluid);
IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ",ignoreFluid=" + ignoreFluid + ")=" + result);
IrisLogging.debug("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ",ignoreFluid=" + ignoreFluid + ")=" + result);
return result;
}
@@ -106,7 +106,7 @@ final class GoldenDebugObjectPlacer implements IObjectPlacer {
@Override
public boolean isCarved(int x, int y, int z) {
boolean result = delegate.isCarved(x, y, z);
IrisLogging.info("Goldendebug query: tag=" + tag + " isCarved(" + x + "," + y + "," + z + ")=" + result);
IrisLogging.debug("Goldendebug query: tag=" + tag + " isCarved(" + x + "," + y + "," + z + ")=" + result);
return result;
}
@@ -118,7 +118,7 @@ final class GoldenDebugObjectPlacer implements IObjectPlacer {
@Override
public boolean isSolid(int x, int y, int z) {
boolean result = delegate.isSolid(x, y, z);
IrisLogging.info("Goldendebug query: tag=" + tag + " isSolid(" + x + "," + y + "," + z + ")=" + result);
IrisLogging.debug("Goldendebug query: tag=" + tag + " isSolid(" + x + "," + y + "," + z + ")=" + result);
return result;
}
@@ -95,7 +95,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
boolean trace = IrisSettings.get().getGeneral().isDebug();
if (trace) {
IrisLogging.info("[StructTrace] ORIGIN chunk=" + cx + "," + cz + " structures=" + placement.getStructures()
IrisLogging.debug("[StructTrace] ORIGIN chunk=" + cx + "," + cz + " structures=" + placement.getStructures()
+ " anchor=" + placement.resolvedAnchor() + " band=" + placement.getMinHeight() + ".." + placement.getMaxHeight());
}
@@ -108,7 +108,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
RNG rng = resolved.rng();
int baseY = resolved.baseY();
if (trace) {
IrisLogging.info("[StructTrace] ASSEMBLED chunk=" + cx + "," + cz + " key=" + key + " baseY=" + baseY + " pieces=" + pieces.size());
IrisLogging.debug("[StructTrace] ASSEMBLED chunk=" + cx + "," + cz + " key=" + key + " baseY=" + baseY + " pieces=" + pieces.size());
}
if (!placement.isAnchoredUnderground()) {
@@ -132,7 +132,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
carvedBlocks++;
}
}
IrisLogging.info("Cave object diag: chunk=" + x + "," + z
IrisLogging.debug("Cave object diag: chunk=" + x + "," + z
+ " surfaceBiome=" + surfaceBiome.getLoadKey()
+ " caveBiome=" + caveBiome.getLoadKey()
+ " surfaceY=" + surfaceY
@@ -143,7 +143,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
+ " sameBiome=" + (caveBiome == surfaceBiome || java.util.Objects.equals(caveBiome.getLoadKey(), surfaceBiome.getLoadKey())));
}
if (traceRegen) {
IrisLogging.info("Regen object layer start: chunk=" + x + "," + z
IrisLogging.debug("Regen object layer start: chunk=" + x + "," + z
+ " surfaceBiome=" + surfaceBiome.getLoadKey()
+ " caveBiome=" + caveBiome.getLoadKey()
+ " region=" + region.getLoadKey()
@@ -160,7 +160,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
placeUpperObjects(writer, rng, x, z, xxx, zzz, surfaceY, upperCtx, dimension, complex, traceRegen);
}
if (traceRegen) {
IrisLogging.info("Regen object layer done: chunk=" + x + "," + z
IrisLogging.debug("Regen object layer done: chunk=" + x + "," + z
+ " biomeSurfacePlacersChecked=" + summary.biomeSurfacePlacersChecked()
+ " biomeSurfacePlacersTriggered=" + summary.biomeSurfacePlacersTriggered()
+ " biomeCavePlacersChecked=" + summary.biomeCavePlacersChecked()
@@ -240,7 +240,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
biomeSurfaceChecked++;
boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005));
if (traceRegen) {
IrisLogging.info("Regen object placer chance: chunk=" + x + "," + z
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=biome-surface"
+ " chanceResult=" + chance
+ " chanceBase=" + i.getChance()
@@ -262,7 +262,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisLogging.error("Failed to place objects in the following biome: " + surfaceBiome.getName());
IrisLogging.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ").");
IrisLogging.error("Are these objects missing?");
e.printStackTrace();
}
}
}
@@ -274,7 +273,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
biomeCaveChecked++;
boolean chance = rng.chance(i.getChance());
if (traceRegen) {
IrisLogging.info("Regen object placer chance: chunk=" + x + "," + z
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=biome-cave"
+ " chanceResult=" + chance
+ " chanceBase=" + i.getChance()
@@ -296,7 +295,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisLogging.error("Failed to place cave objects in the following biome: " + caveBiome.getName());
IrisLogging.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ").");
IrisLogging.error("Are these objects missing?");
e.printStackTrace();
}
}
}
@@ -305,7 +303,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
regionSurfaceChecked++;
boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005));
if (traceRegen) {
IrisLogging.info("Regen object placer chance: chunk=" + x + "," + z
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=region-surface"
+ " chanceResult=" + chance
+ " chanceBase=" + i.getChance()
@@ -327,7 +325,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisLogging.error("Failed to place objects in the following region: " + region.getName());
IrisLogging.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ").");
IrisLogging.error("Are these objects missing?");
e.printStackTrace();
}
}
}
@@ -339,7 +336,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
regionCaveChecked++;
boolean chance = rng.chance(i.getChance());
if (traceRegen) {
IrisLogging.info("Regen object placer chance: chunk=" + x + "," + z
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=region-cave"
+ " chanceResult=" + chance
+ " chanceBase=" + i.getChance()
@@ -361,7 +358,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisLogging.error("Failed to place cave objects in the following region: " + region.getName());
IrisLogging.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ").");
IrisLogging.error("Are these objects missing?");
e.printStackTrace();
}
}
}
@@ -418,7 +414,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
boolean treePlacement = p instanceof IrisProceduralTree;
boolean chancePassed = passesProceduralChance(rng, p.getChance());
if (golden) {
IrisLogging.info("Goldendebug procedural chance: chunk=" + x + "," + z
IrisLogging.debug("Goldendebug procedural chance: chunk=" + x + "," + z
+ " scope=" + scope
+ " placement=" + p.getName()
+ " passed=" + chancePassed);
@@ -443,7 +439,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisObject variant = p.getVariantObject(getData(), rng);
if (variant == null) {
if (golden) {
IrisLogging.info("Goldendebug procedural pick: chunk=" + x + "," + z
IrisLogging.debug("Goldendebug procedural pick: chunk=" + x + "," + z
+ " placement=" + p.getName()
+ " densityIndex=" + i
+ " variant=null");
@@ -468,7 +464,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
if (carving && caveAnchor == null) {
if (golden) {
IrisLogging.info("Goldendebug procedural cave anchor rejected: chunk=" + x + "," + z
IrisLogging.debug("Goldendebug procedural cave anchor rejected: chunk=" + x + "," + z
+ " placement=" + p.getName()
+ " minDepthBelowSurface=" + minDepthBelowSurface);
}
@@ -479,7 +475,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int id = rng.i(0, Integer.MAX_VALUE);
if (golden) {
KList<IrisObject> pool = p.getVariantObjects(getData());
IrisLogging.info("Goldendebug procedural pick: chunk=" + x + "," + z
IrisLogging.debug("Goldendebug procedural pick: chunk=" + x + "," + z
+ " placement=" + p.getName()
+ " densityIndex=" + i
+ " variant=" + variant.getLoadKey()
@@ -495,7 +491,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (carving) {
int caveFloorY = caveAnchor.y();
if (golden) {
IrisLogging.info("Goldendebug procedural caveFloor: chunk=" + x + "," + z
IrisLogging.debug("Goldendebug procedural caveFloor: chunk=" + x + "," + z
+ " placement=" + p.getName()
+ " xx=" + xx
+ " zz=" + zz
@@ -529,7 +525,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}, null, getData());
}
if (golden) {
IrisLogging.info("Goldendebug procedural result: chunk=" + x + "," + z
IrisLogging.debug("Goldendebug procedural result: chunk=" + x + "," + z
+ " placement=" + p.getName()
+ " variant=" + variant.getLoadKey()
+ " xx=" + xx
@@ -540,7 +536,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to place procedural object '" + p.getName() + "' in " + scope);
e.printStackTrace();
}
}
}
@@ -690,7 +685,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (v == null) {
nullObjects++;
if (traceRegen) {
IrisLogging.warn("Regen object placement null object: chunk=" + chunkX + "," + chunkZ
IrisLogging.debug("Regen object placement null object: chunk=" + chunkX + "," + chunkZ
+ " scope=" + scope
+ " densityIndex=" + i
+ " density=" + density
@@ -705,7 +700,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int id = rng.i(0, Integer.MAX_VALUE);
IObjectPlacer placePlacer = golden ? new GoldenDebugObjectPlacer(writer, scope + "/" + v.getLoadKey()) : writer;
if (golden) {
IrisLogging.info("Goldendebug object attempt: chunk=" + chunkX + "," + chunkZ
IrisLogging.debug("Goldendebug object attempt: chunk=" + chunkX + "," + chunkZ
+ " scope=" + scope
+ " object=" + v.getLoadKey()
+ " densityIndex=" + i
@@ -734,7 +729,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
if (golden) {
IrisLogging.info("Goldendebug object result: chunk=" + chunkX + "," + chunkZ
IrisLogging.debug("Goldendebug object result: chunk=" + chunkX + "," + chunkZ
+ " scope=" + scope
+ " object=" + v.getLoadKey()
+ " resultY=" + result
@@ -742,7 +737,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
if (traceRegen) {
IrisLogging.info("Regen object placement result: chunk=" + chunkX + "," + chunkZ
IrisLogging.debug("Regen object placement result: chunk=" + chunkX + "," + chunkZ
+ " scope=" + scope
+ " object=" + v.getLoadKey()
+ " resultY=" + result
@@ -829,7 +824,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (object == null) {
nullObjects++;
if (traceRegen) {
IrisLogging.warn("Regen cave object placement null object: chunk=" + metricChunkX + "," + metricChunkZ
IrisLogging.debug("Regen cave object placement null object: chunk=" + metricChunkX + "," + metricChunkZ
+ " scope=" + scope
+ " densityIndex=" + i
+ " density=" + density
@@ -943,7 +938,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
if (traceRegen) {
IrisLogging.info("Regen cave object placement result: chunk=" + metricChunkX + "," + metricChunkZ
IrisLogging.debug("Regen cave object placement result: chunk=" + metricChunkX + "," + metricChunkZ
+ " scope=" + scope
+ " object=" + object.getLoadKey()
+ " resultY=" + result
@@ -1017,7 +1012,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisLogging.reportError(e);
IrisLogging.error("Failed to place upper-dimension objects in biome " + upperBiome.getName()
+ ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")");
e.printStackTrace();
}
}
}
@@ -1033,7 +1027,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisLogging.reportError(e);
IrisLogging.error("Failed to place upper-dimension objects in region " + upperRegion.getName()
+ ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")");
e.printStackTrace();
}
}
}
@@ -1104,7 +1097,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}, null, getData());
if (traceRegen) {
IrisLogging.info("Upper object placement: chunk=" + chunkX + "," + chunkZ
IrisLogging.debug("Upper object placement: chunk=" + chunkX + "," + chunkZ
+ " scope=" + scope
+ " object=" + v.getLoadKey()
+ " anchorY=" + anchorY
@@ -1163,7 +1156,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int suppressed = state.suppressed.getAndSet(0);
String anchorYText = anchorY == null ? "none" : String.valueOf(anchorY);
String errorText = error == null ? "none" : error.getClass().getSimpleName() + ":" + String.valueOf(error.getMessage());
IrisLogging.warn("Cave object reject: scope=" + scope
IrisLogging.debug("Cave object reject: scope=" + scope
+ " reason=" + reason
+ " chunk=" + chunkX + "," + chunkZ
+ " object=" + objectKey
@@ -1609,7 +1602,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
return IrisObject.sampleSize(getData().getObjectLoader().findFile(objectKey));
} catch (IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
return null;
@@ -52,10 +52,6 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
@Override
public CaveVoxel voxelAt(CavePosition position) {
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
if (hydrology != null && hydrology.carves()) {
return hydrology.isWet() ? CaveVoxel.COMPATIBLE_FLUID : CaveVoxel.CAVE_AIR;
}
MatterCavern cavern = dataIfPresent(position, MatterCavern.class);
if (cavern != null) {
if (cavern.isLava()) {
@@ -15,6 +15,7 @@ import art.arcane.iris.engine.river.RiverAnchor;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.engine.river.RiverTopologyComplexity;
import art.arcane.iris.engine.river.cave.CavePosition;
import art.arcane.iris.engine.river.cave.CaveVoxel;
import art.arcane.iris.engine.river.cave.CaveVoxelPrecondition;
@@ -223,7 +224,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
}
static int tunnelHalo(IrisRiverRuntime runtime) {
return Math.max(1, (int) StrictMath.ceil(runtime.maximumChannelWidth() * 0.5D) + 1);
return RiverTopologyComplexity.tunnelHalo(
runtime.maximumChannelWidth(),
runtime.maximumTunnelWidthMultiplier(),
runtime.tunnelMouthBlend()
);
}
static int waterHeadY(IrisRiverSurfaceSample sample, IrisRiverCaves caves) {
@@ -261,6 +266,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
int chunkX,
int chunkZ,
int halo,
int dryHeadroom,
FootprintSampler footprintSampler,
TunnelSampler tunnelSampler,
SurfaceSampler surfaceSampler
@@ -286,13 +292,25 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
return TunnelPlan.empty();
}
Map<CavePosition, RiverCaveAction> candidateActions = mergeActions(solidColumns);
ArrayList<TunnelColumn> containedColumns = new ArrayList<>(solidColumns.size());
for (TunnelColumn column : solidColumns) {
if (isTunnelColumnContained(view, column, candidateActions.keySet(), surfaceSampler)) {
containedColumns.add(column);
ArrayList<TunnelColumn> containedColumns = new ArrayList<>(solidColumns);
boolean changed;
do {
changed = false;
Set<CavePosition> candidateActions = mergeActions(containedColumns).keySet();
for (int index = containedColumns.size() - 1; index >= 0; index--) {
TunnelColumn column = containedColumns.get(index);
if (!isTunnelColumnContained(
view,
column,
candidateActions,
dryHeadroom,
surfaceSampler
)) {
containedColumns.remove(index);
changed = true;
}
}
}
} while (changed && !containedColumns.isEmpty());
Map<CavePosition, RiverCaveAction> actions = mergeActions(containedColumns);
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
for (CavePosition position : actions.keySet()) {
@@ -302,15 +320,32 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
));
}
for (CavePosition position : List.copyOf(actions.keySet())) {
RiverCaveAction action = actions.get(position);
for (int[] offset : NEIGHBORS) {
CavePosition neighbor = offset(position, offset);
if (actions.containsKey(neighbor)
|| !view.isInWorld(neighbor)
|| view.voxelAt(neighbor) != CaveVoxel.SOLID) {
if (actions.containsKey(neighbor) || !view.isInWorld(neighbor)) {
continue;
}
CaveVoxel neighborVoxel = view.voxelAt(neighbor);
boolean sealsSolidBoundary = neighborVoxel == CaveVoxel.SOLID;
boolean sealsCaveWaterline = action == RiverCaveAction.WET_SOURCE
&& neighborVoxel == CaveVoxel.CAVE_AIR;
if (!sealsSolidBoundary && !sealsCaveWaterline) {
if (action == RiverCaveAction.DRY_AIR
&& neighborVoxel == CaveVoxel.CAVE_AIR
&& !view.isOpenToSurface(neighbor)) {
preconditions.putIfAbsent(
neighbor,
new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, false)
);
}
continue;
}
actions.putIfAbsent(neighbor, RiverCaveAction.SEAL_GUARD);
preconditions.putIfAbsent(neighbor, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
preconditions.putIfAbsent(neighbor, new CaveVoxelPrecondition(
neighborVoxel,
view.isOpenToSurface(neighbor)
));
}
}
return new TunnelPlan(Map.copyOf(actions), Map.copyOf(preconditions));
@@ -332,7 +367,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
? RiverCaveAction.WET_SOURCE
: RiverCaveAction.DRY_AIR;
if (!view.isInWorld(position)
|| (view.voxelAt(position) != CaveVoxel.SOLID
|| (!canCarveTunnelVoxel(view, position)
&& !matchesPublishedAction(view, position, action))) {
return null;
}
@@ -345,6 +380,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
CaveVoxelView view,
TunnelColumn column,
Set<CavePosition> candidateActions,
int dryHeadroom,
SurfaceSampler surfaceSampler
) {
for (CavePosition position : column.actions().keySet()) {
@@ -356,7 +392,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
if (!view.isInWorld(neighbor)) {
return false;
}
if (view.voxelAt(neighbor) == CaveVoxel.SOLID || isSurfaceMouth(neighbor, surfaceSampler)) {
CaveVoxel neighborVoxel = view.voxelAt(neighbor);
if (neighborVoxel == CaveVoxel.SOLID
|| (neighborVoxel == CaveVoxel.CAVE_AIR
&& !view.isOpenToSurface(neighbor))
|| isSurfaceMouth(neighbor, dryHeadroom, surfaceSampler)) {
continue;
}
return false;
@@ -365,14 +405,24 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
return true;
}
private static boolean isSurfaceMouth(CavePosition position, SurfaceSampler surfaceSampler) {
private static boolean canCarveTunnelVoxel(CaveVoxelView view, CavePosition position) {
CaveVoxel voxel = view.voxelAt(position);
return voxel == CaveVoxel.SOLID
|| (voxel == CaveVoxel.CAVE_AIR && !view.isOpenToSurface(position));
}
private static boolean isSurfaceMouth(
CavePosition position,
int dryHeadroom,
SurfaceSampler surfaceSampler
) {
IrisRiverSurfaceSample sample = surfaceSampler.sample(position.x(), position.z());
if (!isWetChannelBed(sample) || sample.subterranean()) {
return false;
}
int bedY = (int) Math.round(sample.terrainHeight());
int headY = (int) Math.round(sample.waterSurfaceY());
return position.y() > bedY && position.y() <= headY;
return position.y() > bedY && position.y() <= headY + Math.max(0, dryHeadroom);
}
private static Map<CavePosition, RiverCaveAction> mergeActions(List<TunnelColumn> columns) {
@@ -423,6 +473,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
chunkX,
chunkZ,
tunnelHalo(runtime),
runtime.maximumTunnelHeadroom(),
runtime::sampleFootprint,
runtime::sampleTunnel,
runtime::sample
@@ -470,11 +521,16 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
int x = (int) StrictMath.floor(anchor.x());
int z = (int) StrictMath.floor(anchor.z());
IrisRiverSurfaceSample sample = runtime.sample(x, z);
if (!isWetChannelBed(sample)) {
IrisRiverTunnelSample tunnel = runtime.sampleTunnel(x, z);
if (!isWetChannelBed(sample) && tunnel == null) {
return null;
}
int bedY = (int) Math.round(sample.terrainHeight());
int headY = waterHeadY(sample, caves);
int bedY = tunnel == null
? (int) Math.round(sample.terrainHeight())
: tunnel.bedY();
int headY = tunnel == null
? waterHeadY(sample, caves)
: tunnel.waterHeadY() + caves.getWaterLevelOffset();
int entryY = Math.max(bedY, headY);
CavePosition entry = new CavePosition(x, entryY, z);
if (!view.isInWorld(entry)) {
@@ -548,7 +604,10 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
int offsetX,
int offsetZ
) {
int preferredY = headY + caves.getDryHeadroom() - caves.getGrottoVerticalRadius();
int preferredY = Math.min(
headY + caves.getDryHeadroom() - caves.getGrottoVerticalRadius(),
entry.y() - caves.getGrottoVerticalRadius() - 1
);
int maximumY = Math.min(Math.min(entry.y() - 1, headY), preferredY);
int minimumY = Math.max(1, entry.y() - caves.getMaxBoreDepth());
for (int y = maximumY; y >= minimumY; y--) {
@@ -28,6 +28,7 @@ import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.util.project.context.ChunkContext;
@@ -58,6 +59,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
private static final int CAVE_BIOME_BLEND_RADIUS = 3;
private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4;
private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8;
private static final int RIVER_BIOME_INHERITANCE_CELL_SIZE = 4;
private static final long RIVER_BIOME_INHERITANCE_SALT = 0x4CF5AD432745937FL;
private static final MatterCavern BASIC_CAVERN = new MatterCavern(true, "", (byte) 0);
private final RNG rng;
private final PlatformBlockState AIR = B.getState("CAVE_AIR");
@@ -139,7 +142,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
try {
walls.forEach((rx, yy, rz, cavern) -> {
walls.forEach((rx, yy, rz, cavern, riverBoundary) -> {
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, rx, yy, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
@@ -408,16 +411,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int yy = columnMask.nextSetBit(0);
while (yy >= 0) {
if (rz < 15 && !columnMasks[columnIndex + 1].contains(yy)) {
walls.put(rx, yy, rz + 1, BASIC_CAVERN);
walls.put(rx, yy, rz + 1, BASIC_CAVERN, false);
}
if (rx < 15 && !columnMasks[columnIndex + 16].contains(yy)) {
walls.put(rx + 1, yy, rz, BASIC_CAVERN);
walls.put(rx + 1, yy, rz, BASIC_CAVERN, false);
}
if (rz > 0 && !columnMasks[columnIndex - 1].contains(yy)) {
walls.put(rx, yy, rz - 1, BASIC_CAVERN);
walls.put(rx, yy, rz - 1, BASIC_CAVERN, false);
}
if (rx > 0 && !columnMasks[columnIndex - 16].contains(yy)) {
walls.put(rx - 1, yy, rz, BASIC_CAVERN);
walls.put(rx - 1, yy, rz, BASIC_CAVERN, false);
}
yy = columnMask.nextSetBit(yy + 1);
}
@@ -437,17 +440,19 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
while (yy >= 0) {
MatterCavern cavern = composedCavernAt(mc, rx, yy, rz);
if (cavern != null) {
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, yy, rz, RiverCaveHydrology.class);
boolean riverBoundary = hydrology != null && !hydrology.floodedBiomeKey().isEmpty();
if (rz < 15 && composedCavernAt(mc, rx, yy, rz + 1) == null) {
walls.put(rx, yy, rz + 1, cavern);
walls.put(rx, yy, rz + 1, cavern, riverBoundary);
}
if (rx < 15 && composedCavernAt(mc, rx + 1, yy, rz) == null) {
walls.put(rx + 1, yy, rz, cavern);
walls.put(rx + 1, yy, rz, cavern, riverBoundary);
}
if (rz > 0 && composedCavernAt(mc, rx, yy, rz - 1) == null) {
walls.put(rx, yy, rz - 1, cavern);
walls.put(rx, yy, rz - 1, cavern, riverBoundary);
}
if (rx > 0 && composedCavernAt(mc, rx - 1, yy, rz) == null) {
walls.put(rx - 1, yy, rz, cavern);
walls.put(rx - 1, yy, rz, cavern, riverBoundary);
}
}
yy = columnMask.nextSetBit(yy + 1);
@@ -523,7 +528,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return;
}
walls.put(localX, yy, localZ, neighbor);
RiverCaveHydrology hydrology = dataIfPresent(
neighborChunk, neighborX, yy, neighborZ, RiverCaveHydrology.class);
boolean riverBoundary = hydrology != null && !hydrology.floodedBiomeKey().isEmpty();
walls.put(localX, yy, localZ, neighbor, riverBoundary);
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
boundaryMasks[columnIndex].add(yy);
}
@@ -667,10 +675,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
) {
IrisBiome floorBiome = resolveCaveBoundaryBiome(
walls.get(rx, zoneFloor, rz), worldX, zoneFloor, worldZ,
resolverState, caveBiomeCache, customBiomeCache);
resolverState, caveBiomeCache, customBiomeCache,
walls.isRiverBoundary(rx, zoneFloor, rz));
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(
walls.get(rx, zoneCeiling, rz), worldX, zoneCeiling, worldZ,
resolverState, caveBiomeCache, customBiomeCache);
resolverState, caveBiomeCache, customBiomeCache,
walls.isRiverBoundary(rx, zoneCeiling, rz));
if (floorBiome == null && ceilingBiome == null) {
return;
}
@@ -688,12 +698,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, rx, floorY, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != RiverCaveAction.SEAL_GUARD) {
continue;
}
PlatformBlockState existing = output.getRaw(rx, floorY, rz);
PlatformBlockState layer = floorLayers.get(i);
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
if (!B.isSolid(existing)
|| !canReplaceRiverGuard(hydrology, layer, false)
|| !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
continue;
}
if (B.isOre(existing)) {
@@ -715,7 +729,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, rx, ceilingY, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != RiverCaveAction.SEAL_GUARD) {
continue;
}
PlatformBlockState existing = output.getRaw(rx, ceilingY, rz);
@@ -723,6 +739,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
continue;
}
PlatformBlockState layer = ceilingLayers.get(i);
if (!canReplaceRiverGuard(hydrology, layer, true)) {
continue;
}
if (B.isOre(existing)) {
output.setRaw(rx, ceilingY, rz, B.toDeepSlateOre(existing, layer));
continue;
@@ -786,12 +805,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
int y = zone.floor - i - 1;
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, y, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != RiverCaveAction.SEAL_GUARD) {
continue;
}
PlatformBlockState block = floorBlocks.get(i);
PlatformBlockState existing = output.getRaw(rx, y, rz);
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, y, rz, block)) {
if (!B.isSolid(existing)
|| !canReplaceRiverGuard(hydrology, block, false)
|| !canReplaceCaveFloorLayer(output, rx, y, rz, block)) {
continue;
}
if (B.isOre(existing)) {
@@ -810,12 +833,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
break;
}
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, cy, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != RiverCaveAction.SEAL_GUARD) {
continue;
}
PlatformBlockState block = ceilingBlocks.get(i);
PlatformBlockState existing = output.getRaw(rx, cy, rz);
if (!B.isSolid(existing)) {
if (!B.isSolid(existing) || !canReplaceRiverGuard(hydrology, block, true)) {
continue;
}
if (B.isOre(existing)) {
@@ -876,17 +901,90 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
IrisBiome resolveCaveBoundaryBiome(MantleChunk<Matter> mantleChunk, int x, int y, int z, int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
MatterCavern cavern = composedCavernAt(mantleChunk, x, y, z);
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, x, y, z, RiverCaveHydrology.class);
return resolveCaveBoundaryBiome(
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache);
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache,
hydrology != null && !hydrology.floodedBiomeKey().isEmpty());
}
IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
return resolveCaveBoundaryBiome(
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache, false);
}
private IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache, boolean riverBoundary) {
if (cavern != null && !cavern.getCustomBiome().isEmpty()) {
if (riverBoundary && selectsParentRiverBiome(
getEngine().getSeedManager().getCarve(),
worldX,
worldZ,
riverParentBiomeInheritance())) {
IrisBiome parent = resolveRiverParentBiome(
caveBiomeCache, worldX, y, worldZ, resolverState);
if (parent != null) {
return parent;
}
}
return resolveCustomBiome(customBiomeCache, cavern.getCustomBiome());
}
return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState);
}
static boolean selectsParentRiverBiome(long seed, int worldX, int worldZ, double inheritance) {
if (inheritance <= 0D) {
return false;
}
if (inheritance >= 1D) {
return true;
}
int cellX = Math.floorDiv(worldX, RIVER_BIOME_INHERITANCE_CELL_SIZE);
int cellZ = Math.floorDiv(worldZ, RIVER_BIOME_INHERITANCE_CELL_SIZE);
long hash = (seed + RIVER_BIOME_INHERITANCE_SALT) ^ BlockPosition.toLong(cellX, 0, cellZ);
hash = (hash ^ (hash >>> 30)) * 0xBF58476D1CE4E5B9L;
hash = (hash ^ (hash >>> 27)) * 0x94D049BB133111EBL;
hash ^= hash >>> 31;
double roll = (hash >>> 11) * 0x1.0p-53;
return roll < inheritance;
}
static boolean canReplaceRiverGuard(
RiverCaveHydrology hydrology,
PlatformBlockState layer,
boolean ceiling
) {
if (hydrology == null || hydrology.action() != RiverCaveAction.SEAL_GUARD) {
return true;
}
return layer != null
&& B.isSolid(layer)
&& !B.isFluid(layer)
&& (!ceiling || !isGravityAffected(layer));
}
private double riverParentBiomeInheritance() {
if (getDimension().getRivers() == null || getDimension().getRivers().getCaves() == null) {
return 0D;
}
IrisRiverCaves caves = getDimension().getRivers().getCaves();
return Math.max(0D, Math.min(1D, caves.getParentBiomeInheritance()));
}
private IrisBiome resolveRiverParentBiome(
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
int worldX,
int y,
int worldZ,
IrisDimensionCarvingResolver.State resolverState
) {
IrisBiome parent = resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState);
if (parent != null && parent == getEngine().getSurfaceBiome(worldX, worldZ)) {
IrisBiome natural = getComplex().getNaturalTrueBiomeStream().get(worldX, worldZ);
return natural == null ? parent : natural;
}
return parent;
}
static boolean canReplaceCaveFloorLayer(Hunk<PlatformBlockState> output, int x, int y, int z, PlatformBlockState layer) {
return !isGravityAffected(layer) || y > 0 && B.isSolid(output.getRaw(x, y - 1, z));
}
@@ -140,6 +140,7 @@ final class CarveWallBuffer {
private int[] keys;
private MatterCavern[] values;
private boolean[] riverBoundaries;
private int mask;
private int resizeAt;
private int size;
@@ -154,11 +155,12 @@ final class CarveWallBuffer {
keys = new int[capacity];
Arrays.fill(keys, EMPTY_KEY);
values = new MatterCavern[capacity];
riverBoundaries = new boolean[capacity];
mask = capacity - 1;
resizeAt = Math.max(1, (int) (capacity * LOAD_FACTOR));
}
void put(int x, int y, int z, MatterCavern value) {
void put(int x, int y, int z, MatterCavern value, boolean riverBoundary) {
int key = pack(x, y, z);
int index = mix(key) & mask;
@@ -167,6 +169,7 @@ final class CarveWallBuffer {
if (existingKey == EMPTY_KEY) {
keys[index] = key;
values[index] = value;
riverBoundaries[index] = riverBoundary;
size++;
if (size >= resizeAt) {
resize();
@@ -176,6 +179,7 @@ final class CarveWallBuffer {
if (existingKey == key) {
values[index] = value;
riverBoundaries[index] = riverBoundaries[index] || riverBoundary;
return;
}
@@ -198,6 +202,21 @@ final class CarveWallBuffer {
}
}
boolean isRiverBoundary(int x, int y, int z) {
int key = pack(x, y, z);
int index = mix(key) & mask;
while (true) {
int existingKey = keys[index];
if (existingKey == EMPTY_KEY) {
return false;
}
if (existingKey == key) {
return riverBoundaries[index];
}
index = (index + 1) & mask;
}
}
void forEach(Consumer consumer) {
for (int index = 0; index < keys.length; index++) {
int key = keys[index];
@@ -207,7 +226,7 @@ final class CarveWallBuffer {
MatterCavern cavern = values[index];
if (cavern != null) {
consumer.accept(unpackX(key), unpackY(key), unpackZ(key), cavern);
consumer.accept(unpackX(key), unpackY(key), unpackZ(key), cavern, riverBoundaries[index]);
}
}
}
@@ -215,16 +234,19 @@ final class CarveWallBuffer {
void clear() {
Arrays.fill(keys, EMPTY_KEY);
Arrays.fill(values, null);
Arrays.fill(riverBoundaries, false);
size = 0;
}
private void resize() {
int[] oldKeys = keys;
MatterCavern[] oldValues = values;
boolean[] oldRiverBoundaries = riverBoundaries;
int nextCapacity = oldKeys.length << 1;
keys = new int[nextCapacity];
Arrays.fill(keys, EMPTY_KEY);
values = new MatterCavern[nextCapacity];
riverBoundaries = new boolean[nextCapacity];
mask = nextCapacity - 1;
resizeAt = Math.max(1, (int) (nextCapacity * LOAD_FACTOR));
size = 0;
@@ -233,12 +255,12 @@ final class CarveWallBuffer {
int key = oldKeys[index];
MatterCavern value = oldValues[index];
if (key != EMPTY_KEY && value != null) {
reinsert(key, value);
reinsert(key, value, oldRiverBoundaries[index]);
}
}
}
private void reinsert(int key, MatterCavern value) {
private void reinsert(int key, MatterCavern value, boolean riverBoundary) {
int index = mix(key) & mask;
while (keys[index] != EMPTY_KEY) {
index = (index + 1) & mask;
@@ -246,6 +268,7 @@ final class CarveWallBuffer {
keys[index] = key;
values[index] = value;
riverBoundaries[index] = riverBoundary;
size++;
}
@@ -272,6 +295,6 @@ final class CarveWallBuffer {
@FunctionalInterface
interface Consumer {
void accept(int x, int y, int z, MatterCavern cavern);
void accept(int x, int y, int z, MatterCavern cavern, boolean riverBoundary);
}
}
@@ -100,7 +100,6 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
generated.add(layer.get(random.nextParallelRNG(i + j), (wx + j) / layer.getZoom(), j, (wz - j) / layer.getZoom(), data));
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
@@ -88,7 +88,6 @@ final class IrisBiomeLayerGenerator {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
@@ -147,7 +146,6 @@ final class IrisBiomeLayerGenerator {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
@@ -206,7 +204,6 @@ final class IrisBiomeLayerGenerator {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
}
@@ -256,7 +253,6 @@ final class IrisBiomeLayerGenerator {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
@@ -72,7 +72,6 @@ public class IrisCompat {
def.getItemFilters().add(i);
}
} catch (Throwable e) {
e.printStackTrace();
IrisLogging.reportError(e);
}
}
@@ -18,6 +18,9 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.volmlib.util.json.JSONObject;
import java.util.Objects;
public record IrisDimensionRuntimeContract(
@@ -58,6 +61,7 @@ public record IrisDimensionRuntimeContract(
IrisDimensionRuntimeContract actual = expected(active, namespace);
IrisDimensionRuntimeContract proposed = expected(replacement, namespace);
proposed.requireExact(runtimeName, actual);
requireGeneratedTypeCompatible(runtimeName, active, replacement);
}
public int maxHeight() {
@@ -95,4 +99,35 @@ public record IrisDimensionRuntimeContract(
+ " and logical height " + actualLogical
+ ". Generation was refused before any chunk writes. Restart after installing the exact Iris dimension type; terrain clipping is not allowed.");
}
private static void requireGeneratedTypeCompatible(
String runtimeName,
IrisDimension active,
IrisDimension replacement
) {
if (!Objects.equals(active.getEnvironment(), replacement.getEnvironment())) {
throw hotloadMismatch(runtimeName, active, replacement);
}
JSONObject activeType = effectiveDimensionType(active);
JSONObject replacementType = effectiveDimensionType(replacement);
if (!replacementType.similar(activeType)) {
throw hotloadMismatch(runtimeName, active, replacement);
}
}
private static JSONObject effectiveDimensionType(IrisDimension dimension) {
return new JSONObject(dimension.getDimensionType().toJson(DataVersion.getLatest().get()));
}
private static IrisDimensionContractException hotloadMismatch(
String runtimeName,
IrisDimension active,
IrisDimension replacement
) {
return new IrisDimensionContractException(runtimeName
+ " cannot hotload a changed dimension environment or generated dimension type. The active environment is "
+ active.getEnvironment() + " and the replacement environment is " + replacement.getEnvironment()
+ ". Close and reopen Studio after changing environment, dimensionOptions, or fullbright values that alter the generated type.");
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.link.Identifier;
@@ -253,9 +254,10 @@ public class IrisEntity extends IrisRegistrant {
}
}).get();
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
IrisLogging.reportError("Iris entity spawn preparation was interrupted.", e);
} catch (ExecutionException e) {
e.printStackTrace();
IrisLogging.reportError("Iris entity spawn preparation failed.", e);
}
at = f.get();
}
@@ -206,7 +206,6 @@ public class IrisEntitySpawn implements IRare {
return e;
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
IrisLogging.error(" Failed to retrieve real entity @ " + at + " (entity: " + getEntity() + ")");
return null;
}
@@ -74,8 +74,7 @@ public class IrisExpression extends IrisRegistrant {
scope.addInvocationVariable("y");
scope.addInvocationVariable("z");
} catch (Throwable e) {
e.printStackTrace();
IrisLogging.error("Script Variable load error in " + getLoadFile().getPath());
IrisLogging.reportError("Script variable load failed in " + getLoadFile().getPath() + ".", e);
}
for (IrisExpressionFunction f : functions) {
@@ -87,8 +86,7 @@ public class IrisExpression extends IrisRegistrant {
try {
return parser.parse(getExpression(), scope);
} catch (Throwable e) {
e.printStackTrace();
IrisLogging.error("Script load error in " + getLoadFile().getPath());
IrisLogging.reportError("Script load failed in " + getLoadFile().getPath() + ".", e);
}
return null;
@@ -292,7 +292,7 @@ public class IrisProceduralTree implements IrisProceduralPlacement {
long position = ((long) vector.getBlockX() << 40) ^ ((long) vector.getBlockY() << 20) ^ vector.getBlockZ();
digest[0] ^= Long.rotateLeft(position * 0x9E3779B97F4A7C15L ^ state.key().hashCode(), (int) (position & 63));
});
IrisLogging.info("Goldendebug bake: " + object.getLoadKey() + " blocks=" + object.getBlocks().size() + " digest=" + Long.toHexString(digest[0]));
IrisLogging.debug("Goldendebug bake: " + object.getLoadKey() + " blocks=" + object.getBlocks().size() + " digest=" + Long.toHexString(digest[0]));
}
}
@@ -72,6 +72,11 @@ public class IrisRiverCaves {
@Desc("The maximum coordinate warp applied to generated sealed grottos in blocks.")
private double grottoWarpStrength = 2D;
@MinNumber(0)
@MaxNumber(1)
@Desc("The fraction of river-cave boundary columns that inherit the naturally resolved parent cave or surface biome instead of a flooded-cave override.")
private double parentBiomeInheritance = 0.5D;
@MinNumber(4)
@MaxNumber(256)
@Desc("The horizontal proof radius for an existing closed cave component.")
@@ -59,17 +59,41 @@ public class IrisRiverTerrain {
@Desc("The exponent shaping the channel-to-bank cross-section transition.")
private double bankExponent = 2D;
@Desc("Modulates perpendicular spline displacement while preserving graph endpoints.")
@MinNumber(0)
@MaxNumber(16)
@Desc("The extra lateral tunnel-mouth blend carved on each side where a surface river enters or exits solid terrain.")
private double tunnelMouthBlend = 2D;
@Desc("Noise modulating the submerged floor of river tunnels.")
private IrisGeneratorStyle tunnelFloorStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(48D);
@Desc("The subterranean tunnel width multiplier relative to the surface river width.")
private IrisStyledRange tunnelWidthMultiplier = range(1D, 1D, NoiseStyle.FLAT, 1D);
@MinNumber(0)
@MaxNumber(8)
@Desc("The maximum vertical floor variation in river tunnels.")
private double tunnelFloorVariation = 2D;
@Desc("Noise modulating the dry roof of river tunnels.")
private IrisGeneratorStyle tunnelRoofStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(64D);
@MinNumber(0)
@MaxNumber(16)
@Desc("The maximum vertical roof variation in river tunnels.")
private double tunnelRoofVariation = 3D;
@Desc("Warps the spacing and amplitude of varied local-normal sweeps, hooks, curls, and wandering bends while preserving graph endpoints.")
private IrisGeneratorStyle meanderStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(512D);
@MinNumber(0)
@MaxNumber(1024)
@Desc("The maximum perpendicular meander displacement in blocks.")
@Desc("The total endpoint-spline and shape-personality displacement envelope in blocks.")
private double meanderStrength = 72D;
@MinNumber(1)
@MaxNumber(64)
@Desc("The number of straight segments used to flatten each meandering graph reach.")
@Desc("The number of straight segments used to flatten and resolve each meandering graph reach.")
private int meanderSubdivisions = 8;
@Desc("Modulates small river-bed height variation after the connected channel shape is solved.")
@@ -47,6 +47,16 @@ public class IrisRiverTopology {
@Desc("The spacing of deterministic drainage-basin sinks in routing cells. Larger values produce longer trunks and wider tributary trees.")
private int routingBasinCells = 64;
@MinNumber(8)
@MaxNumber(256)
@Desc("The wavelength in routing cells of the smooth domain warp applied to drainage distance.")
private int routingDeviationScaleCells = 24;
@MinNumber(0)
@MaxNumber(32)
@Desc("The maximum drainage-domain displacement in routing cells. Zero keeps straight radial basin gradients.")
private double routingDeviationStrengthCells = 0D;
@MinNumber(1)
@MaxNumber(64)
@Desc("The horizontal basin-distance span in routing cells per one block of terraced water rise.")
@@ -72,6 +82,26 @@ public class IrisRiverTopology {
@Desc("The maximum routing-cost contribution from routingStyle.")
private double routingNoiseWeight = 24D;
@MinNumber(0)
@MaxNumber(1024)
@Desc("The penalty for choosing a downstream edge that does not follow the local routingStyle tangent.")
private double flowAlignmentWeight = 24D;
@MinNumber(0)
@MaxNumber(1024)
@Desc("The deterministic attraction toward shared downstream nodes. Larger values form stronger tributary trees and confluences.")
private double confluenceWeight = 0D;
@MinNumber(1)
@MaxNumber(8)
@Desc("The number of upstream children a graph node accepts before additional branches begin shrinking probabilistically.")
private int branchSoftCap = 4;
@MinNumber(0)
@MaxNumber(1)
@Desc("The multiplicative survival factor for each child beyond branchSoftCap. Recursive generations remain unbounded by depth.")
private double branchChildShrinkFactor = 0.35D;
@MinNumber(0)
@MaxNumber(16)
@Desc("The contribution of natural terrain height to downstream routing cost.")
@@ -92,10 +92,6 @@ import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -109,11 +105,6 @@ import java.util.function.Supplier;
@Data
public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChunkGenerator, Listener {
private static final int LOAD_LOCKS = Runtime.getRuntime().availableProcessors() * 4;
private static final int STUDIO_ENTRY_PRECOMPUTE_RADIUS = 2;
private static final int STUDIO_ENTRY_PRECOMPUTE_THREADS = Math.max(
1,
Math.min(6, Runtime.getRuntime().availableProcessors() / 2));
private static final AtomicInteger STUDIO_ENTRY_THREAD_SEQUENCE = new AtomicInteger();
private static final long HOTLOAD_LOOP_DELAY_MS = 250L;
private static final long HOTLOAD_MAINTENANCE_DELAY_MS = 4000L;
private final GenerationStageGate loadLock;
@@ -127,7 +118,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private final AtomicBoolean setup;
private final boolean studio;
private final AtomicBoolean studioEntryBootstrapActive;
private final ConcurrentHashMap<Long, PreparedStudioChunk> preparedStudioEntryChunks;
private final AtomicInteger a = new AtomicInteger(0);
private volatile long lastChunkGenTime = 0L;
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
@@ -156,7 +146,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
this.hotloadChecker = new ChronoLatch(1000, false);
this.studio = studio;
this.studioEntryBootstrapActive = new AtomicBoolean(studio);
this.preparedStudioEntryChunks = new ConcurrentHashMap<>();
this.dataLocation = dataLocation;
this.dimensionKey = dimensionKey;
this.folder = new ReactiveFolder(
@@ -211,7 +200,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
spawnChunks.completeExceptionally(e);
IrisLogging.reportError(e);
IrisLogging.error("Failed to initialize Iris generator for " + world.getName());
e.printStackTrace();
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
@@ -489,7 +477,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (currentEngine != null && !currentEngine.isClosed()) {
currentEngine.close();
}
preparedStudioEntryChunks.clear();
folder.clear();
populators.clear();
});
@@ -540,124 +527,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
studioEntryBootstrapActive.set(false);
}
public CompletableFuture<Void> prepareStudioEntryChunks(
World bukkitWorld,
int centerChunkX,
int centerChunkZ
) {
if (!studio || closing) {
return CompletableFuture.completedFuture(null);
}
Engine activeEngine = getEngine(bukkitWorld);
computeStudioGenerator();
if (studioGenerator != null) {
return CompletableFuture.completedFuture(null);
}
long generationSessionId = activeEngine.getGenerationSessionId();
ConcurrentHashMap<Long, PreparedStudioChunk> prepared = new ConcurrentHashMap<>();
ExecutorService executor = createStudioEntryExecutor();
int diameter = STUDIO_ENTRY_PRECOMPUTE_RADIUS * 2 + 1;
ArrayList<CompletableFuture<Void>> tasks = new ArrayList<>(diameter * diameter);
for (int offsetX = -STUDIO_ENTRY_PRECOMPUTE_RADIUS;
offsetX <= STUDIO_ENTRY_PRECOMPUTE_RADIUS;
offsetX++) {
for (int offsetZ = -STUDIO_ENTRY_PRECOMPUTE_RADIUS;
offsetZ <= STUDIO_ENTRY_PRECOMPUTE_RADIUS;
offsetZ++) {
int chunkX = centerChunkX + offsetX;
int chunkZ = centerChunkZ + offsetZ;
CompletableFuture<Void> task = CompletableFuture.runAsync(
() -> prepareStudioEntryChunk(
bukkitWorld,
activeEngine,
generationSessionId,
chunkX,
chunkZ,
prepared),
executor);
tasks.add(task);
}
}
CompletableFuture<Void> completion = CompletableFuture.allOf(
tasks.toArray(new CompletableFuture<?>[0]));
CompletableFuture<Void> publication = completion.thenRun(() -> {
if (closing
|| engine != activeEngine
|| activeEngine.getGenerationSessionId() != generationSessionId) {
throw new IllegalStateException(
"Studio entry precompute finished for a replaced engine runtime.");
}
preparedStudioEntryChunks.clear();
preparedStudioEntryChunks.putAll(prepared);
});
return publication.whenComplete((ignored, failure) -> executor.shutdownNow());
}
private ExecutorService createStudioEntryExecutor() {
return Executors.newFixedThreadPool(STUDIO_ENTRY_PRECOMPUTE_THREADS, runnable -> {
Thread thread = new Thread(
runnable,
"Iris Studio Entry-" + STUDIO_ENTRY_THREAD_SEQUENCE.incrementAndGet());
thread.setDaemon(true);
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
});
}
private void prepareStudioEntryChunk(
World bukkitWorld,
Engine activeEngine,
long generationSessionId,
int chunkX,
int chunkZ,
ConcurrentHashMap<Long, PreparedStudioChunk> prepared
) {
try (GenerationStagePermit ignored = acquireGenerationStage(
"studio_entry_chunk_precompute")) {
TerrainChunk terrainChunk = TerrainChunk.create(bukkitWorld);
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(terrainChunk.getChunkData());
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(terrainChunk);
ChunkContext context = createStudioEntryContext(
activeEngine,
generationSessionId,
chunkX,
chunkZ);
activeEngine.generateMatter(chunkX, chunkZ, true, context);
try {
activeEngine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
} catch (WrongEngineBroException exception) {
throw new CompletionException(exception);
}
prepared.put(
chunkKey(chunkX, chunkZ),
new PreparedStudioChunk(activeEngine, generationSessionId, blocks)
);
}
}
private ChunkContext createStudioEntryContext(
Engine activeEngine,
long generationSessionId,
int chunkX,
int chunkZ
) {
boolean cacheContext = !activeEngine.getPlatformHooks()
.shouldDisableChunkContextCache(activeEngine);
ChunkContext.PrefillPlan prefillPlan = cacheContext
? ChunkContext.PrefillPlan.NO_CAVE
: ChunkContext.PrefillPlan.NONE;
return new ChunkContext(
chunkX << 4,
chunkZ << 4,
activeEngine.getComplex(),
generationSessionId,
cacheContext,
prefillPlan,
activeEngine.getMetrics());
}
public boolean isStudioEntryBootstrapActive() {
return studioEntryBootstrapActive.get();
}
@@ -783,10 +652,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
IrisLogging.reportError(e);
e.printStackTrace();
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
} finally {
if (acquired) {
loadLock.releaseExclusive();
@@ -916,14 +783,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (studioGenerator != null) {
studioGenerator.generateChunk(engine, tc, x, z);
} else {
PreparedStudioChunk prepared = preparedStudioEntryChunks.remove(chunkKey(x, z));
if (prepared != null
&& prepared.engine() == engine
&& prepared.generationSessionId() == engine.getGenerationSessionId()) {
prepared.blocks().applyTo(d);
IrisLogging.debug("Applied prepared Studio entry chunk " + x + " " + z);
return;
}
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(tc);
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
@@ -941,17 +800,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
throw new IllegalStateException("Iris chunk generation was rejected during an engine transition.", e);
}
IrisLogging.error("======================================");
e.printStackTrace();
IrisLogging.reportError("Iris chunk generation could not acquire its engine runtime at " + x + "," + z + ".", e);
reportErrorChunk(x, z, e);
IrisLogging.error("======================================");
throw new IllegalStateException("Iris chunk generation could not acquire its engine runtime.", e);
} catch (Throwable e) {
IrisLogging.error("======================================");
e.printStackTrace();
IrisLogging.reportError("Iris failed to generate chunk " + x + "," + z + ".", e);
reportErrorChunk(x, z, e);
IrisLogging.error("======================================");
throw new IllegalStateException("Iris failed to generate chunk " + x + "," + z + ".", e);
}
@@ -969,17 +824,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
return isMaintenanceActive();
}
private static long chunkKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) ^ (chunkZ & 0xFFFFFFFFL);
}
private record PreparedStudioChunk(
Engine engine,
long generationSessionId,
ChunkDataHunkHolder blocks
) {
}
private boolean isMaintenanceActive() {
World realWorld = BukkitWorldBinding.world(this.world);
return realWorld != null && IrisToolbelt.isWorldMaintenanceActive(realWorld);
@@ -620,7 +620,6 @@ public final class EngineBukkitOps {
Thread.currentThread().interrupt();
} catch (WrongEngineBroException | ExecutionException e) {
IrisLogging.reportError(e);
e.printStackTrace();
} finally {
if (search != null && !resultDispatched) {
ACTIVE_LOCATE_REQUESTS.remove(playerId, search);
@@ -267,7 +267,7 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
int cellCount = layout.cells().size();
IrisBlockVector worldExtent = computeExtent(layout);
IrisLogging.info("Object Studio layout built: %d cells from %d pack(s), extent %d x %d blocks",
IrisLogging.debug("Object Studio layout built: %d cells from %d pack(s), extent %d x %d blocks",
cellCount, sources.size(), worldExtent.getBlockX(), worldExtent.getBlockZ());
}
}
@@ -14,12 +14,28 @@ public final class RiverNetwork {
private static final long NODE_RANK_SALT = 0x3C6EF372FE94F82BL;
private static final long BASIN_X_SALT = 0xCBBB9D5DC1059ED8L;
private static final long BASIN_Z_SALT = 0x629A292A367CD507L;
private static final long BASIN_DEVIATION_X_SALT = 0xA4093822299F31D0L;
private static final long BASIN_DEVIATION_Z_SALT = 0x082EFA98EC4E6C89L;
private static final long DIAGONAL_SALT = 0xA54FF53A5F1D36F1L;
private static final long SOURCE_SALT = 0x510E527FADE682D1L;
private static final long SOURCE_FLOOR_SALT = 0xD6E8FEB86659FD93L;
private static final long REACH_SALT = 0x9B05688C2B3E6C1FL;
private static final long DRY_SALT = 0x1F83D9ABFB41BD6BL;
private static final long MEANDER_SALT = 0x5BE0CD19137E2179L;
private static final long MEANDER_PERSONALITY_SALT = 0x243F6A8885A308D3L;
private static final long MEANDER_AMPLITUDE_SALT = 0x13198A2E03707344L;
private static final long MEANDER_PHASE_SALT = 0xD1310BA698DFB5ACL;
private static final long MEANDER_SKEW_SALT = 0x2FFD72DBD01ADFB7L;
private static final long MEANDER_CYCLE_SALT = 0x452821E638D01377L;
private static final long MEANDER_FEATURE_A_SALT = 0xBE5466CF34E90C6CL;
private static final long MEANDER_FEATURE_B_SALT = 0xC0AC29B7C97C50DDL;
private static final long CONFLUENCE_SALT = 0x9E3779B97F4A7C15L;
private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL;
private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL;
private static final int MEANDER_NOISE_SAMPLES = 12;
private static final int WIDTH_PROFILE_SAMPLES = 12;
private static final double TWO_PI = StrictMath.PI * 2D;
private static final MeanderPersonality[] MEANDER_PERSONALITIES = MeanderPersonality.values();
private final RiverNetworkOptions options;
@@ -213,10 +229,20 @@ public final class RiverNetwork {
private double drainageDistance(RiverNodeId id) {
int basinCells = options.routingBasinCells();
long basinX = Math.floorDiv(id.cellX(), basinCells);
long basinZ = Math.floorDiv(id.cellZ(), basinCells);
double nodeX = id.cellX() + 0.5D;
double nodeZ = id.cellZ() + 0.5D;
double deviationStrength = options.routingDeviationStrengthCells();
if (deviationStrength > 0D) {
int deviationScale = options.routingDeviationScaleCells();
double originalX = nodeX;
double originalZ = nodeZ;
nodeX += smoothCellNoise(originalX, originalZ, deviationScale, BASIN_DEVIATION_X_SALT)
* deviationStrength;
nodeZ += smoothCellNoise(originalX, originalZ, deviationScale, BASIN_DEVIATION_Z_SALT)
* deviationStrength;
}
long basinX = (long) StrictMath.floor(nodeX / basinCells);
long basinZ = (long) StrictMath.floor(nodeZ / basinCells);
double jitterRadius = basinCells * 0.45D;
double nearestDistance = Double.MAX_VALUE;
for (long candidateX = basinX - 1L; candidateX <= basinX + 1L; candidateX++) {
@@ -234,6 +260,24 @@ public final class RiverNetwork {
return nearestDistance;
}
private double smoothCellNoise(double x, double z, int scale, long salt) {
double scaledX = x / scale;
double scaledZ = z / scale;
long minimumX = (long) StrictMath.floor(scaledX);
long minimumZ = (long) StrictMath.floor(scaledZ);
double fractionX = scaledX - minimumX;
double fractionZ = scaledZ - minimumZ;
double fadeX = fractionX * fractionX * (3D - 2D * fractionX);
double fadeZ = fractionZ * fractionZ * (3D - 2D * fractionZ);
double northwest = centered(hash(minimumX, minimumZ, salt));
double northeast = centered(hash(minimumX + 1L, minimumZ, salt));
double southwest = centered(hash(minimumX, minimumZ + 1L, salt));
double southeast = centered(hash(minimumX + 1L, minimumZ + 1L, salt));
double north = northwest + (northeast - northwest) * fadeX;
double south = southwest + (southeast - southwest) * fadeX;
return north + (south - north) * fadeZ;
}
private NodePosition nodePosition(RiverNodeId id) {
double centerX = ((double) id.cellX() + 0.5D) * options.cellSize();
double centerZ = ((double) id.cellZ() + 0.5D) * options.cellSize();
@@ -261,13 +305,19 @@ public final class RiverNetwork {
if (compareRank(neighbor, node) >= 0) {
continue;
}
if (!branchPermitted(node, neighbor, resolver)) {
continue;
}
RiverRoutingContext context = resolver.routingContext(node, neighbor);
double routingCost = finiteNonNegative(resolver.terrain.reachRoutingCost(context));
double oceanAttraction = neighbor.ocean() ? options.oceanAttraction() : 0.0;
double flowAlignmentCost = flowAlignmentCost(node, neighbor, resolver);
double confluenceAttraction = unit(hash(neighbor.id(), CONFLUENCE_SALT))
* options.confluenceWeight();
ranked.add(new RankedCandidate(
neighbor,
neighbor.routingScore() + routingCost + flowAlignmentCost - oceanAttraction
neighbor.routingScore() + routingCost + flowAlignmentCost
- oceanAttraction - confluenceAttraction
));
}
ranked.sort((first, second) -> {
@@ -281,6 +331,19 @@ public final class RiverNetwork {
return List.copyOf(candidates);
}
private boolean branchPermitted(RiverNode child, RiverNode parent, NodeResolver resolver) {
RiverEdgeId childEdge = RiverEdgeId.of(child.id(), parent.id());
int childSlot = resolver.branchSlot(parent, child);
if (childSlot < options.branchSoftCap()) {
return true;
}
double survivalChance = 1D;
for (int overflow = options.branchSoftCap(); overflow <= childSlot; overflow++) {
survivalChance *= options.branchChildShrinkFactor();
}
return gate(hash(childEdge, BRANCH_GATE_SALT), survivalChance);
}
private RiverRoute trace(RiverNodeId sourceId, NodeResolver resolver) {
if (!resolver.sourcePermitted(sourceId)) {
return new RiverRoute(sourceId, RiverRouteState.SUPPRESSED, List.of(), false, false);
@@ -290,6 +353,7 @@ public final class RiverNetwork {
ArrayList<RiverEdgeId> edges = new ArrayList<>(options.maxRouteReaches());
RiverNode current = source;
boolean reachedOcean = false;
boolean exhaustedHorizon = true;
for (int reachIndex = 0; reachIndex < options.maxRouteReaches(); reachIndex++) {
RiverNode next = null;
int examined = 0;
@@ -305,10 +369,12 @@ public final class RiverNetwork {
}
}
if (next == null) {
exhaustedHorizon = false;
break;
}
RiverEdgeId edgeId = RiverEdgeId.of(current.id(), next.id());
if (!resolver.continuationPermitted(resolver.routingContext(current, next))) {
exhaustedHorizon = false;
break;
}
edges.add(edgeId);
@@ -322,6 +388,9 @@ public final class RiverNetwork {
if (reachedOcean) {
return new RiverRoute(sourceId, RiverRouteState.WET, edges, true, false);
}
if (exhaustedHorizon && !options.requireOcean()) {
return new RiverRoute(sourceId, RiverRouteState.WET, edges, false, false);
}
if (!edges.isEmpty()) {
RiverTerminalPolicy terminalPolicy = resolver.terminalPolicy(current);
if (terminalPolicy == RiverTerminalPolicy.WET
@@ -388,13 +457,11 @@ public final class RiverNetwork {
NodeResolver resolver
) {
int pointCount = options.meanderSubdivisions() + 1;
double[] x = new double[pointCount];
double[] z = new double[pointCount];
double[] baseX = new double[pointCount];
double[] baseZ = new double[pointCount];
double deltaX = to.x() - from.x();
double deltaZ = to.z() - from.z();
double length = StrictMath.hypot(deltaX, deltaZ);
double normalX = length == 0.0 ? 0.0 : -deltaZ / length;
double normalZ = length == 0.0 ? 0.0 : deltaX / length;
double maximumOffset = StrictMath.min(options.meanderStrength(), length * 0.35);
double directionX = length == 0D ? 1D : deltaX / length;
double directionZ = length == 0D ? 0D : deltaZ / length;
@@ -416,17 +483,29 @@ public final class RiverNetwork {
+ fromTangentWeight * fromTangent.z() * maximumOffset
+ toWeight * to.z()
+ toTangentWeight * toTangent.z() * maximumOffset;
baseX[point] = curvedX;
baseZ[point] = curvedZ;
}
if (maximumOffset <= 0D) {
return new RiverPolyline(baseX, baseZ);
}
MeanderProfile profile = meanderProfile(id);
double[] meanderNoise = meanderNoise(id, baseX, baseZ, resolver);
double[] meanderSignal = meanderSignal(id, profile, meanderNoise);
double[] x = new double[pointCount];
double[] z = new double[pointCount];
for (int point = 0; point < pointCount; point++) {
double t = (double) point / (pointCount - 1);
double tSquared = t * t;
double envelope = 16D * tSquared * (1D - t) * (1D - t);
double straightX = from.x() + deltaX * t;
double straightZ = from.z() + deltaZ * t;
RiverMeanderContext context = new RiverMeanderContext(id, t, straightX, straightZ);
double configuredNoise = resolver.terrain.meanderNoise(context);
double noise = Double.isFinite(configuredNoise)
? StrictMath.max(-1.0, StrictMath.min(1.0, configuredNoise))
: smoothEdgeNoise(id, t * 3.0);
double envelope = 16D * tSquared * (1D - t) * (1D - t);
double offset = maximumOffset * 0.5D * envelope * noise;
x[point] = curvedX + normalX * offset;
z[point] = curvedZ + normalZ * offset;
double baseDisplacement = StrictMath.hypot(baseX[point] - straightX, baseZ[point] - straightZ);
double availableOffset = StrictMath.max(0D, maximumOffset - baseDisplacement);
double offset = availableOffset * 0.92D * envelope * meanderSignal[point];
FlowTangent normal = localNormal(baseX, baseZ, point);
x[point] = baseX[point] + normal.x() * offset;
z[point] = baseZ[point] + normal.z() * offset;
}
x[0] = from.x();
z[0] = from.z();
@@ -435,6 +514,200 @@ public final class RiverNetwork {
return new RiverPolyline(x, z);
}
private double[] meanderNoise(
RiverEdgeId id,
double[] baseX,
double[] baseZ,
NodeResolver resolver
) {
int sampleCount = StrictMath.min(MEANDER_NOISE_SAMPLES, baseX.length);
double[] knots = new double[sampleCount];
for (int sample = 0; sample < sampleCount; sample++) {
double t = (double) sample / (sampleCount - 1);
double position = t * (baseX.length - 1);
int lower = (int) StrictMath.floor(position);
int upper = StrictMath.min(baseX.length - 1, lower + 1);
double interpolation = position - lower;
double curvedX = baseX[lower] + (baseX[upper] - baseX[lower]) * interpolation;
double curvedZ = baseZ[lower] + (baseZ[upper] - baseZ[lower]) * interpolation;
RiverMeanderContext context = new RiverMeanderContext(id, t, curvedX, curvedZ);
double configuredNoise = resolver.terrain.meanderNoise(context);
knots[sample] = Double.isFinite(configuredNoise)
? StrictMath.max(-1D, StrictMath.min(1D, configuredNoise))
: smoothEdgeNoise(id, t * 3D);
}
double[] noise = new double[baseX.length];
for (int point = 0; point < noise.length; point++) {
double t = (double) point / (noise.length - 1);
double position = t * (sampleCount - 1);
int lower = (int) StrictMath.floor(position);
int upper = StrictMath.min(sampleCount - 1, lower + 1);
double interpolation = smoothStep(position - lower);
noise[point] = knots[lower] + (knots[upper] - knots[lower]) * interpolation;
}
return noise;
}
private MeanderProfile meanderProfile(RiverEdgeId id) {
int personalityIndex = (int) StrictMath.floor(
unit(hash(id, MEANDER_PERSONALITY_SALT)) * MEANDER_PERSONALITIES.length
);
MeanderPersonality personality = MEANDER_PERSONALITIES[
StrictMath.min(MEANDER_PERSONALITIES.length - 1, personalityIndex)
];
double amplitudeRoll = unit(hash(id, MEANDER_AMPLITUDE_SALT));
double amplitude = personality == MeanderPersonality.QUIET
? 0.16D + amplitudeRoll * 0.24D
: 0.48D + amplitudeRoll * 0.52D;
double maximumCycles = StrictMath.max(1D, StrictMath.min(8D, options.meanderSubdivisions() / 5D));
double cycleRoll = unit(hash(id, MEANDER_CYCLE_SALT));
double cycles = switch (personality) {
case SWEEP, HOOK, QUIET -> 0.45D + cycleRoll * 0.65D;
case S_CURVE -> 0.75D + cycleRoll * StrictMath.min(1.25D, maximumCycles);
case OXBOW -> 1D + cycleRoll * StrictMath.min(1.5D, maximumCycles);
case COIL -> StrictMath.min(maximumCycles, 2.25D + cycleRoll * 4.75D);
case WANDER -> 0.65D + cycleRoll * StrictMath.min(2.35D, maximumCycles);
case CHIRP -> StrictMath.min(maximumCycles, 1.5D + cycleRoll * 5.5D);
};
double handedness = (hash(id, MEANDER_SALT) & 1L) == 0L ? -1D : 1D;
return new MeanderProfile(
personality,
amplitude,
handedness,
unit(hash(id, MEANDER_PHASE_SALT)) * TWO_PI,
centered(hash(id, MEANDER_SKEW_SALT)) * 0.72D,
cycles,
unit(hash(id, MEANDER_FEATURE_A_SALT)),
unit(hash(id, MEANDER_FEATURE_B_SALT))
);
}
private double[] meanderSignal(RiverEdgeId id, MeanderProfile profile, double[] noise) {
int subdivisions = noise.length - 1;
for (int point = 0; point < noise.length; point++) {
double t = (double) point / subdivisions;
noise[point] = noise[point] * 0.78D + smoothEdgeNoise(id, t * 3.5D) * 0.22D;
}
smoothSignal(noise);
double detailCycles = StrictMath.max(0.5D, profile.cycles() + profile.featureB() * 1.5D);
double baseStep = TWO_PI * detailCycles / subdivisions;
double phase = profile.phase() + profile.featureA() * StrictMath.PI;
double[] signal = new double[noise.length];
for (int point = 0; point < noise.length; point++) {
double t = (double) point / subdivisions;
if (point > 0) {
double frequencyNoise = (noise[point - 1] + noise[point]) * 0.25D + 0.5D;
phase += baseStep * (0.45D + frequencyNoise * 1.1D);
}
double warpedT = warpMeanderPosition(t, profile.skew());
double macro = personalitySignal(profile, warpedT, noise[point]);
double detailWeight = detailWeight(profile.personality());
double detail = detailWeight <= 0D
? 0D
: StrictMath.sin(phase) * (0.65D + StrictMath.abs(noise[point]) * 0.35D);
double noiseWeight = noiseWeight(profile.personality());
double macroWeight = 1D - detailWeight - noiseWeight;
signal[point] = clampSigned(profile.amplitude()
* (macro * macroWeight + detail * detailWeight + noise[point] * noiseWeight));
}
smoothSignal(signal);
return signal;
}
private double personalitySignal(MeanderProfile profile, double t, double noise) {
double handedness = profile.handedness();
return switch (profile.personality()) {
case SWEEP -> handedness * (0.68D + StrictMath.sin(StrictMath.PI * t) * 0.32D);
case HOOK -> handedness * (0.12D + smoothStep(
profile.featureA() < 0.5D ? t : 1D - t
) * 0.88D);
case S_CURVE -> handedness * StrictMath.sin(
TWO_PI * profile.cycles() * t + profile.phase()
);
case COIL -> StrictMath.sin(TWO_PI * profile.cycles() * t + profile.phase())
* (0.62D + StrictMath.sin(StrictMath.PI * t) * 0.38D);
case WANDER -> clampSigned(
noise * 0.82D
+ StrictMath.sin(TWO_PI * profile.cycles() * t + profile.phase()) * 0.38D
+ handedness * 0.12D
);
case OXBOW -> handedness * clampSigned(
compactLobe(t, 0.18D + profile.featureA() * 0.2D, 0.24D + profile.featureB() * 0.12D)
- compactLobe(t, 0.62D + profile.featureB() * 0.2D, 0.22D + profile.featureA() * 0.14D)
* (0.55D + profile.featureA() * 0.45D)
);
case CHIRP -> StrictMath.sin(
profile.phase() + TWO_PI * (0.45D * t + profile.cycles() * t * t)
);
case QUIET -> handedness * (0.72D + noise * 0.28D);
};
}
private double detailWeight(MeanderPersonality personality) {
return switch (personality) {
case SWEEP, HOOK, OXBOW, QUIET -> 0D;
case S_CURVE -> 0.12D;
case COIL, WANDER -> 0.08D;
case CHIRP -> 0.1D;
};
}
private double noiseWeight(MeanderPersonality personality) {
return switch (personality) {
case SWEEP, S_CURVE, COIL, OXBOW, CHIRP -> 0.08D;
case HOOK -> 0.1D;
case WANDER -> 0.34D;
case QUIET -> 0.14D;
};
}
private double warpMeanderPosition(double t, double skew) {
return t + skew * t * (1D - t);
}
private double compactLobe(double t, double center, double radius) {
double distance = StrictMath.abs(t - center) / radius;
if (distance >= 1D) {
return 0D;
}
return smoothStep(1D - distance);
}
private double smoothStep(double value) {
double clamped = StrictMath.max(0D, StrictMath.min(1D, value));
return clamped * clamped * (3D - 2D * clamped);
}
private void smoothSignal(double[] signal) {
if (signal.length < 3) {
return;
}
double previousRaw = signal[0];
double currentRaw = signal[1];
for (int point = 1; point < signal.length - 1; point++) {
double nextRaw = signal[point + 1];
signal[point] = (previousRaw + currentRaw * 2D + nextRaw) * 0.25D;
previousRaw = currentRaw;
currentRaw = nextRaw;
}
}
private double clampSigned(double value) {
return StrictMath.max(-1D, StrictMath.min(1D, value));
}
private FlowTangent localNormal(double[] x, double[] z, int point) {
int previous = StrictMath.max(0, point - 1);
int next = StrictMath.min(x.length - 1, point + 1);
double tangentX = x[next] - x[previous];
double tangentZ = z[next] - z[previous];
double tangentLength = StrictMath.hypot(tangentX, tangentZ);
if (tangentLength <= 0.0000001D) {
return new FlowTangent(0D, 0D);
}
return new FlowTangent(-tangentZ / tangentLength, tangentX / tangentLength);
}
private FlowTangent flowTangent(
RiverNode node,
double fallbackX,
@@ -643,6 +916,8 @@ public final class RiverNetwork {
private final Map<RiverNodeId, RiverTerminalPolicy> terminalPolicies;
private final Map<RiverEdgeId, RiverRoutingContext> routingContexts;
private final Map<RiverNodeId, FlowTangent> flowTangents;
private final Map<RiverEdgeId, Integer> branchSlots;
private final Map<RiverNodeId, Boolean> resolvedBranchParents;
private NodeResolver(RiverTerrainSampler terrain) {
this.terrain = terrain;
@@ -656,6 +931,8 @@ public final class RiverNetwork {
terminalPolicies = new HashMap<>();
routingContexts = new HashMap<>();
flowTangents = new HashMap<>();
branchSlots = new HashMap<>();
resolvedBranchParents = new HashMap<>();
}
private RiverNode resolve(RiverNodeId id) {
@@ -668,6 +945,32 @@ public final class RiverNetwork {
ignored -> computeDownstreamCandidates(node, this));
}
private int branchSlot(RiverNode parent, RiverNode child) {
if (!resolvedBranchParents.containsKey(parent.id())) {
ArrayList<RiverNode> upstream = new ArrayList<>(8);
for (RiverNodeId siblingId : neighbors(parent.id())) {
RiverNode sibling = resolve(siblingId);
if (sibling.riverAllowed() && compareRank(sibling, parent) > 0) {
upstream.add(sibling);
}
}
upstream.sort((first, second) -> {
long firstPriority = hash(RiverEdgeId.of(first.id(), parent.id()), BRANCH_SLOT_SALT);
long secondPriority = hash(RiverEdgeId.of(second.id(), parent.id()), BRANCH_SLOT_SALT);
int priorityComparison = Long.compareUnsigned(firstPriority, secondPriority);
return priorityComparison != 0
? priorityComparison
: first.id().compareTo(second.id());
});
for (int slot = 0; slot < upstream.size(); slot++) {
RiverNode sibling = upstream.get(slot);
branchSlots.put(RiverEdgeId.of(sibling.id(), parent.id()), slot);
}
resolvedBranchParents.put(parent.id(), true);
}
return branchSlots.getOrDefault(RiverEdgeId.of(child.id(), parent.id()), Integer.MAX_VALUE);
}
private boolean sourcePermitted(RiverNodeId sourceId) {
return sourceGates.computeIfAbsent(sourceId, this::computeSourcePermitted);
}
@@ -819,6 +1122,29 @@ public final class RiverNetwork {
private record FlowTangent(double x, double z) {
}
private record MeanderProfile(
MeanderPersonality personality,
double amplitude,
double handedness,
double phase,
double skew,
double cycles,
double featureA,
double featureB
) {
}
private enum MeanderPersonality {
QUIET,
SWEEP,
HOOK,
S_CURVE,
COIL,
WANDER,
OXBOW,
CHIRP
}
private record SourceTileId(long tileX, long tileZ) {
}
@@ -867,10 +1193,6 @@ public final class RiverNetwork {
private RiverReach build() {
int flow = wetFlow + dryFlow;
int order = 1 + (31 - Integer.numberOfLeadingZeros(flow));
double baseWidth = positiveOrFallback(
terrain.channelWidth(context, options.channelWidth()),
options.channelWidth()
);
double bankWidth = nonNegativeOrFallback(
terrain.bankWidth(context, options.bankWidth()),
options.bankWidth()
@@ -879,10 +1201,8 @@ public final class RiverNetwork {
terrain.depth(context, options.depth()),
options.depth()
);
double width = StrictMath.min(
options.maxChannelWidth(),
baseWidth * (1.0 + options.orderWidthFactor() * (order - 1))
);
RiverWidthProfile widthProfile = widthProfile(order);
double width = widthProfile.maximum();
bankWidth = StrictMath.min(options.maxBankWidth(), bankWidth);
double depth = StrictMath.min(
options.maxDepth(),
@@ -897,6 +1217,7 @@ public final class RiverNetwork {
flow,
order,
width,
widthProfile,
bankWidth,
depth,
state == RiverRouteState.WET && to.ocean(),
@@ -906,6 +1227,54 @@ public final class RiverNetwork {
context.polyline()
);
}
private RiverWidthProfile widthProfile(int order) {
RiverPolyline polyline = context.polyline();
int sampleCount = Math.min(WIDTH_PROFILE_SAMPLES, Math.max(2, polyline.size()));
double[] positions = new double[sampleCount];
double[] widths = new double[sampleCount];
double orderScale = 1D + options.orderWidthFactor() * (order - 1);
for (int index = 0; index < sampleCount; index++) {
double alongReach = (double) index / (sampleCount - 1);
ReachPosition position = positionAt(polyline, alongReach);
double baseWidth = positiveOrFallback(
terrain.channelWidth(
context,
position.x(),
position.z(),
options.channelWidth()
),
options.channelWidth()
);
positions[index] = alongReach;
widths[index] = StrictMath.min(
options.maxChannelWidth(),
StrictMath.max(1D, baseWidth * orderScale)
);
}
return new RiverWidthProfile(positions, widths);
}
}
private static ReachPosition positionAt(RiverPolyline polyline, double alongReach) {
double targetDistance = Math.max(0D, Math.min(1D, alongReach)) * polyline.length();
for (int point = 0; point < polyline.size() - 1; point++) {
double segmentStart = polyline.cumulativeLength(point);
double segmentEnd = polyline.cumulativeLength(point + 1);
if (targetDistance > segmentEnd && point < polyline.size() - 2) {
continue;
}
double segmentLength = segmentEnd - segmentStart;
double interpolation = segmentLength <= 0D
? 0D
: (targetDistance - segmentStart) / segmentLength;
return new ReachPosition(
polyline.x(point) + (polyline.x(point + 1) - polyline.x(point)) * interpolation,
polyline.z(point) + (polyline.z(point + 1) - polyline.z(point)) * interpolation
);
}
int last = polyline.size() - 1;
return new ReachPosition(polyline.x(last), polyline.z(last));
}
private static double positiveOrFallback(double value, double fallback) {
@@ -915,4 +1284,7 @@ public final class RiverNetwork {
private static double nonNegativeOrFallback(double value, double fallback) {
return Double.isFinite(value) && value >= 0.0 ? value : fallback;
}
private record ReachPosition(double x, double z) {
}
}
@@ -9,6 +9,8 @@ public record RiverNetworkOptions(
int minimumSourcesPerTile,
int downstreamCandidateLimit,
int routingBasinCells,
int routingDeviationScaleCells,
double routingDeviationStrengthCells,
double routingPlateauHeight,
double hydraulicBaseHeight,
boolean requireOcean,
@@ -18,6 +20,9 @@ public record RiverNetworkOptions(
double terrainHeightWeight,
double routingNoiseWeight,
double flowAlignmentWeight,
double confluenceWeight,
int branchSoftCap,
double branchChildShrinkFactor,
double oceanAttraction,
double channelWidth,
double bankWidth,
@@ -38,6 +43,8 @@ public record RiverNetworkOptions(
requireRange(minimumSourcesPerTile, 0, tileCells * tileCells, "minimumSourcesPerTile");
requireRange(downstreamCandidateLimit, 1, 8, "downstreamCandidateLimit");
requireRange(routingBasinCells, 8, 256, "routingBasinCells");
requireRange(routingDeviationScaleCells, 8, 256, "routingDeviationScaleCells");
requireRange(routingDeviationStrengthCells, 0D, 32D, "routingDeviationStrengthCells");
requirePositive(routingPlateauHeight, "routingPlateauHeight");
requireFinite(hydraulicBaseHeight, "hydraulicBaseHeight");
requireRange(meanderSubdivisions, 1, 64, "meanderSubdivisions");
@@ -48,6 +55,9 @@ public record RiverNetworkOptions(
requireFiniteNonNegative(terrainHeightWeight, "terrainHeightWeight");
requireFiniteNonNegative(routingNoiseWeight, "routingNoiseWeight");
requireFiniteNonNegative(flowAlignmentWeight, "flowAlignmentWeight");
requireFiniteNonNegative(confluenceWeight, "confluenceWeight");
requireRange(branchSoftCap, 1, 8, "branchSoftCap");
requireProbability(branchChildShrinkFactor, "branchChildShrinkFactor");
requireFiniteNonNegative(oceanAttraction, "oceanAttraction");
requirePositive(channelWidth, "channelWidth");
requireFiniteNonNegative(bankWidth, "bankWidth");
@@ -80,6 +90,12 @@ public record RiverNetworkOptions(
}
}
private static void requireRange(double value, double minimum, double maximum, String name) {
if (!Double.isFinite(value) || value < minimum || value > maximum) {
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
}
}
private static void requireProbability(double value, String name) {
if (!Double.isFinite(value) || value < 0.0 || value > 1.0) {
throw new IllegalArgumentException(name + " must be finite and between 0 and 1");
@@ -113,6 +129,8 @@ public record RiverNetworkOptions(
private int minimumSourcesPerTile;
private int downstreamCandidateLimit;
private int routingBasinCells;
private int routingDeviationScaleCells;
private double routingDeviationStrengthCells;
private double routingPlateauHeight;
private double hydraulicBaseHeight;
private boolean requireOcean;
@@ -122,6 +140,9 @@ public record RiverNetworkOptions(
private double terrainHeightWeight;
private double routingNoiseWeight;
private double flowAlignmentWeight;
private double confluenceWeight;
private int branchSoftCap;
private double branchChildShrinkFactor;
private double oceanAttraction;
private double channelWidth;
private double bankWidth;
@@ -144,6 +165,8 @@ public record RiverNetworkOptions(
minimumSourcesPerTile = 0;
downstreamCandidateLimit = 4;
routingBasinCells = 64;
routingDeviationScaleCells = 24;
routingDeviationStrengthCells = 0D;
routingPlateauHeight = 8.0;
hydraulicBaseHeight = 64D;
requireOcean = false;
@@ -153,6 +176,9 @@ public record RiverNetworkOptions(
terrainHeightWeight = 1.0;
routingNoiseWeight = 24.0;
flowAlignmentWeight = 0D;
confluenceWeight = 0D;
branchSoftCap = 4;
branchChildShrinkFactor = 0.35D;
oceanAttraction = 64.0;
channelWidth = 10.0;
bankWidth = 8.0;
@@ -202,6 +228,16 @@ public record RiverNetworkOptions(
return this;
}
public Builder routingDeviationScaleCells(int value) {
routingDeviationScaleCells = value;
return this;
}
public Builder routingDeviationStrengthCells(double value) {
routingDeviationStrengthCells = value;
return this;
}
public Builder routingPlateauHeight(double value) {
routingPlateauHeight = value;
return this;
@@ -247,6 +283,21 @@ public record RiverNetworkOptions(
return this;
}
public Builder confluenceWeight(double value) {
confluenceWeight = value;
return this;
}
public Builder branchSoftCap(int value) {
branchSoftCap = value;
return this;
}
public Builder branchChildShrinkFactor(double value) {
branchChildShrinkFactor = value;
return this;
}
public Builder oceanAttraction(double value) {
oceanAttraction = value;
return this;
@@ -320,6 +371,8 @@ public record RiverNetworkOptions(
minimumSourcesPerTile,
downstreamCandidateLimit,
routingBasinCells,
routingDeviationScaleCells,
routingDeviationStrengthCells,
routingPlateauHeight,
hydraulicBaseHeight,
requireOcean,
@@ -329,6 +382,9 @@ public record RiverNetworkOptions(
terrainHeightWeight,
routingNoiseWeight,
flowAlignmentWeight,
confluenceWeight,
branchSoftCap,
branchChildShrinkFactor,
oceanAttraction,
channelWidth,
bankWidth,
@@ -10,6 +10,7 @@ public record RiverReach(
int flow,
int order,
double width,
RiverWidthProfile widthProfile,
double bankWidth,
double depth,
boolean mouth,
@@ -21,6 +22,7 @@ public record RiverReach(
Objects.requireNonNull(from);
Objects.requireNonNull(to);
Objects.requireNonNull(state);
Objects.requireNonNull(widthProfile);
Objects.requireNonNull(polyline);
if (state == RiverRouteState.SUPPRESSED) {
throw new IllegalArgumentException("Suppressed routes cannot produce reaches");
@@ -32,5 +34,12 @@ public record RiverReach(
|| !Double.isFinite(depth) || depth <= 0.0) {
throw new IllegalArgumentException("River reach dimensions must be finite and valid");
}
if (Double.compare(width, widthProfile.maximum()) != 0) {
throw new IllegalArgumentException("River reach width must equal its profile maximum");
}
}
public double widthAt(double alongReach) {
return widthProfile.sample(alongReach);
}
}
@@ -62,6 +62,15 @@ public interface RiverTerrainSampler {
return fallback;
}
default double channelWidth(
RiverRoutingContext context,
double x,
double z,
double fallback
) {
return channelWidth(context, fallback);
}
default double bankWidth(RiverRoutingContext context, double fallback) {
return fallback;
}
@@ -117,13 +117,27 @@ public final class RiverTile {
}
public RiverSample sample(double x, double z) {
return sampleExpanded(x, z, 0D);
}
public RiverSample sampleExpanded(double x, double z, double additionalRadius) {
if (!Double.isFinite(additionalRadius) || additionalRadius < 0D) {
throw new IllegalArgumentException("Additional river sample radius must be finite and non-negative");
}
RiverReach nearestReach = null;
double nearestDistanceSquared = Double.POSITIVE_INFINITY;
double nearestAlongReach = 0.0;
for (RiverReach reach : indexedReaches(x, z)) {
ClosestPoint closest = closestPoint(reach.polyline(), x, z);
double outerRadius = reach.width() * 0.5 + reach.bankWidth();
if (closest.distanceSquared() > outerRadius * outerRadius) {
List<RiverReach> candidates = additionalRadius == 0D
? indexedReaches(x, z)
: indexedReaches(
x - additionalRadius,
z - additionalRadius,
x + additionalRadius,
z + additionalRadius
);
for (RiverReach reach : candidates) {
ClosestPoint closest = closestCoveringPoint(reach, x, z, additionalRadius);
if (closest == null) {
continue;
}
if (closest.distanceSquared() < nearestDistanceSquared
@@ -162,15 +176,14 @@ public final class RiverTile {
queryMaximumX,
queryMaximumZ
)) {
ClosestPoint closest = closestPoint(
reach.polyline(),
ClosestPoint closest = closestCoveringPoint(
reach,
queryMinimumX,
queryMinimumZ,
queryMaximumX,
queryMaximumZ
);
double outerRadius = reach.width() * 0.5 + reach.bankWidth();
if (closest.distanceSquared() > outerRadius * outerRadius) {
if (closest == null) {
continue;
}
if (closest.distanceSquared() < nearestDistanceSquared
@@ -196,7 +209,8 @@ public final class RiverTile {
) {
double distance = StrictMath.sqrt(nearestDistanceSquared);
double channelRadius = nearestReach.width() * 0.5;
double localWidth = nearestReach.widthAt(nearestAlongReach);
double channelRadius = localWidth * 0.5;
RiverSection section = section(nearestReach, distance, channelRadius);
double carveWeight = carveWeight(distance, channelRadius, nearestReach.bankWidth());
return new RiverSample(
@@ -208,7 +222,7 @@ public final class RiverTile {
carveWeight,
nearestReach.flow(),
nearestReach.order(),
nearestReach.width(),
localWidth,
nearestReach.bankWidth(),
nearestReach.depth(),
nearestReach.terminal(),
@@ -302,190 +316,252 @@ public final class RiverTile {
return 1.0 - smooth;
}
private static ClosestPoint closestPoint(RiverPolyline polyline, double x, double z) {
private static ClosestPoint closestCoveringPoint(
RiverReach reach,
double x,
double z,
double additionalRadius
) {
RiverPolyline polyline = reach.polyline();
if (polyline.length() == 0D) {
double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0));
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidth() + additionalRadius;
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++) {
SegmentPoint segmentPoint = segmentPoint(
polyline.x(point),
polyline.z(point),
polyline.x(point + 1),
polyline.z(point + 1),
x,
z
);
if (segmentPoint.distanceSquared() < nearest) {
nearest = segmentPoint.distanceSquared();
double segmentLength = polyline.cumulativeLength(point + 1) - polyline.cumulativeLength(point);
double alongLength = polyline.cumulativeLength(point) + segmentLength * segmentPoint.t();
nearestAlong = polyline.length() == 0.0 ? 0.0 : alongLength / polyline.length();
double segmentStartAlong = polyline.cumulativeLength(point) / polyline.length();
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polyline.length();
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.widthProfile().size() - 1; profileIndex++) {
double profileStart = reach.widthProfile().position(profileIndex);
double profileEnd = reach.widthProfile().position(profileIndex + 1);
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
if (overlapStart > overlapEnd) {
continue;
}
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
double widthSlope = (reach.widthProfile().width(profileIndex + 1)
- reach.widthProfile().width(profileIndex)) / (profileEnd - profileStart);
double radiusBase = (reach.widthProfile().width(profileIndex)
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
+ reach.bankWidth()
+ additionalRadius;
double radiusSlope = widthSlope * segmentAlongSpan * 0.5D;
ClosestPoint candidate = coveringPoint(
intervalStart,
intervalEnd,
deltaX,
polyline.x(point) - x,
deltaZ,
polyline.z(point) - z,
radiusSlope,
radiusBase,
segmentStartAlong,
segmentAlongSpan
);
if (candidate != null && candidate.distanceSquared() < nearest) {
nearest = candidate.distanceSquared();
nearestAlong = candidate.alongReach();
}
}
}
return new ClosestPoint(nearest, nearestAlong);
return Double.isFinite(nearest) ? new ClosestPoint(nearest, nearestAlong) : null;
}
private static ClosestPoint closestPoint(
RiverPolyline polyline,
private static ClosestPoint closestCoveringPoint(
RiverReach reach,
double minimumX,
double minimumZ,
double maximumX,
double maximumZ
) {
double nearest = Double.POSITIVE_INFINITY;
double nearestAlong = 0.0;
for (int point = 0; point < polyline.size() - 1; point++) {
SegmentPoint segmentPoint = segmentRectanglePoint(
polyline.x(point),
polyline.z(point),
polyline.x(point + 1),
polyline.z(point + 1),
RiverPolyline polyline = reach.polyline();
if (polyline.length() == 0D) {
double distanceSquared = pointRectangleDistanceSquared(
polyline.x(0),
polyline.z(0),
minimumX,
minimumZ,
maximumX,
maximumZ
);
if (segmentPoint.distanceSquared() < nearest) {
nearest = segmentPoint.distanceSquared();
double segmentLength = polyline.cumulativeLength(point + 1) - polyline.cumulativeLength(point);
double alongLength = polyline.cumulativeLength(point) + segmentLength * segmentPoint.t();
nearestAlong = polyline.length() == 0.0 ? 0.0 : alongLength / polyline.length();
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidth();
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();
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
if (segmentAlongSpan == 0D) {
continue;
}
double startX = polyline.x(point);
double startZ = polyline.z(point);
double deltaX = polyline.x(point + 1) - startX;
double deltaZ = polyline.z(point + 1) - startZ;
for (int profileIndex = 0; profileIndex < reach.widthProfile().size() - 1; profileIndex++) {
double profileStart = reach.widthProfile().position(profileIndex);
double profileEnd = reach.widthProfile().position(profileIndex + 1);
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
if (overlapStart > overlapEnd) {
continue;
}
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
double widthSlope = (reach.widthProfile().width(profileIndex + 1)
- reach.widthProfile().width(profileIndex)) / (profileEnd - profileStart);
double radiusBase = (reach.widthProfile().width(profileIndex)
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D + reach.bankWidth();
double radiusSlope = widthSlope * segmentAlongSpan * 0.5D;
double cursor = intervalStart;
do {
double next = intervalEnd;
next = nextCrossing(startX, deltaX, minimumX, cursor, next);
next = nextCrossing(startX, deltaX, maximumX, cursor, next);
next = nextCrossing(startZ, deltaZ, minimumZ, cursor, next);
next = nextCrossing(startZ, deltaZ, maximumZ, cursor, next);
double middle = (cursor + next) * 0.5D;
double middleX = startX + deltaX * middle;
double middleZ = startZ + deltaZ * middle;
double distanceSlopeX = middleX < minimumX || middleX > maximumX ? deltaX : 0D;
double distanceBaseX = middleX < minimumX
? startX - minimumX
: middleX > maximumX ? startX - maximumX : 0D;
double distanceSlopeZ = middleZ < minimumZ || middleZ > maximumZ ? deltaZ : 0D;
double distanceBaseZ = middleZ < minimumZ
? startZ - minimumZ
: middleZ > maximumZ ? startZ - maximumZ : 0D;
ClosestPoint candidate = coveringPoint(
cursor,
next,
distanceSlopeX,
distanceBaseX,
distanceSlopeZ,
distanceBaseZ,
radiusSlope,
radiusBase,
segmentStartAlong,
segmentAlongSpan
);
if (candidate != null && candidate.distanceSquared() < nearest) {
nearest = candidate.distanceSquared();
nearestAlong = candidate.alongReach();
}
cursor = next;
} while (cursor < intervalEnd);
}
}
return new ClosestPoint(nearest, nearestAlong);
return Double.isFinite(nearest) ? new ClosestPoint(nearest, nearestAlong) : null;
}
private static SegmentPoint segmentPoint(
double startX,
double startZ,
double endX,
double endZ,
double x,
double z
private static ClosestPoint coveringPoint(
double intervalStart,
double intervalEnd,
double distanceSlopeX,
double distanceBaseX,
double distanceSlopeZ,
double distanceBaseZ,
double radiusSlope,
double radiusBase,
double segmentStartAlong,
double segmentAlongSpan
) {
double deltaX = endX - startX;
double deltaZ = endZ - startZ;
double lengthSquared = deltaX * deltaX + deltaZ * deltaZ;
if (lengthSquared == 0.0) {
return new SegmentPoint(squared(x - startX) + squared(z - startZ), 0.0);
double distanceQuadratic = squared(distanceSlopeX) + squared(distanceSlopeZ);
double distanceLinear = 2D * (distanceSlopeX * distanceBaseX + distanceSlopeZ * distanceBaseZ);
double distanceConstant = squared(distanceBaseX) + squared(distanceBaseZ);
double coverageQuadratic = distanceQuadratic - squared(radiusSlope);
double coverageLinear = distanceLinear - 2D * radiusSlope * radiusBase;
double coverageConstant = distanceConstant - squared(radiusBase);
double distancePosition = distanceQuadratic == 0D
? intervalStart
: clamp(-distanceLinear / (2D * distanceQuadratic), intervalStart, intervalEnd);
if (quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, distancePosition) <= 0D) {
return new ClosestPoint(
StrictMath.max(0D, quadraticValue(
distanceQuadratic,
distanceLinear,
distanceConstant,
distancePosition
)),
segmentStartAlong + segmentAlongSpan * distancePosition
);
}
double projection = ((x - startX) * deltaX + (z - startZ) * deltaZ) / lengthSquared;
double t = StrictMath.max(0.0, StrictMath.min(1.0, projection));
double nearestX = startX + deltaX * t;
double nearestZ = startZ + deltaZ * t;
return new SegmentPoint(squared(x - nearestX) + squared(z - nearestZ), t);
double coveragePosition = intervalStart;
double minimumCoverage = quadraticValue(
coverageQuadratic,
coverageLinear,
coverageConstant,
coveragePosition
);
double endCoverage = quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, intervalEnd);
if (endCoverage < minimumCoverage) {
minimumCoverage = endCoverage;
coveragePosition = intervalEnd;
}
if (coverageQuadratic > 0D) {
double vertex = clamp(-coverageLinear / (2D * coverageQuadratic), intervalStart, intervalEnd);
double vertexCoverage = quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, vertex);
if (vertexCoverage < minimumCoverage) {
minimumCoverage = vertexCoverage;
coveragePosition = vertex;
}
}
if (minimumCoverage > 0D) {
return null;
}
double uncovered = distancePosition;
double covered = coveragePosition;
for (int iteration = 0; iteration < 40; iteration++) {
double middle = (uncovered + covered) * 0.5D;
if (quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, middle) <= 0D) {
covered = middle;
} else {
uncovered = middle;
}
}
return new ClosestPoint(
StrictMath.max(0D, quadraticValue(
distanceQuadratic,
distanceLinear,
distanceConstant,
covered
)),
segmentStartAlong + segmentAlongSpan * covered
);
}
private static SegmentPoint segmentRectanglePoint(
double startX,
double startZ,
double endX,
double endZ,
double minimumX,
double minimumZ,
double maximumX,
double maximumZ
private static double nextCrossing(
double start,
double delta,
double boundary,
double cursor,
double currentNext
) {
double intersectionPosition = segmentRectangleIntersectionPosition(
startX,
startZ,
endX,
endZ,
minimumX,
minimumZ,
maximumX,
maximumZ
);
if (!Double.isNaN(intersectionPosition)) {
return new SegmentPoint(0.0, intersectionPosition);
if (delta == 0D) {
return currentNext;
}
double nearestDistanceSquared = pointRectangleDistanceSquared(
startX,
startZ,
minimumX,
minimumZ,
maximumX,
maximumZ
);
double nearestPosition = 0.0;
double endDistanceSquared = pointRectangleDistanceSquared(
endX,
endZ,
minimumX,
minimumZ,
maximumX,
maximumZ
);
if (endDistanceSquared < nearestDistanceSquared) {
nearestDistanceSquared = endDistanceSquared;
nearestPosition = 1.0;
}
SegmentPoint corner = segmentPoint(startX, startZ, endX, endZ, minimumX, minimumZ);
if (corner.distanceSquared() < nearestDistanceSquared) {
nearestDistanceSquared = corner.distanceSquared();
nearestPosition = corner.t();
}
corner = segmentPoint(startX, startZ, endX, endZ, minimumX, maximumZ);
if (corner.distanceSquared() < nearestDistanceSquared) {
nearestDistanceSquared = corner.distanceSquared();
nearestPosition = corner.t();
}
corner = segmentPoint(startX, startZ, endX, endZ, maximumX, minimumZ);
if (corner.distanceSquared() < nearestDistanceSquared) {
nearestDistanceSquared = corner.distanceSquared();
nearestPosition = corner.t();
}
corner = segmentPoint(startX, startZ, endX, endZ, maximumX, maximumZ);
if (corner.distanceSquared() < nearestDistanceSquared) {
nearestDistanceSquared = corner.distanceSquared();
nearestPosition = corner.t();
}
return new SegmentPoint(nearestDistanceSquared, nearestPosition);
double crossing = (boundary - start) / delta;
return crossing > cursor && crossing < currentNext ? crossing : currentNext;
}
private static double segmentRectangleIntersectionPosition(
double startX,
double startZ,
double endX,
double endZ,
double minimumX,
double minimumZ,
double maximumX,
double maximumZ
) {
double minimumPosition = 0.0;
double maximumPosition = 1.0;
double deltaX = endX - startX;
if (deltaX == 0.0) {
if (startX < minimumX || startX > maximumX) {
return Double.NaN;
}
} else {
double first = (minimumX - startX) / deltaX;
double second = (maximumX - startX) / deltaX;
minimumPosition = StrictMath.max(minimumPosition, StrictMath.min(first, second));
maximumPosition = StrictMath.min(maximumPosition, StrictMath.max(first, second));
if (minimumPosition > maximumPosition) {
return Double.NaN;
}
}
private static double quadraticValue(double quadratic, double linear, double constant, double value) {
return (quadratic * value + linear) * value + constant;
}
double deltaZ = endZ - startZ;
if (deltaZ == 0.0) {
if (startZ < minimumZ || startZ > maximumZ) {
return Double.NaN;
}
} else {
double first = (minimumZ - startZ) / deltaZ;
double second = (maximumZ - startZ) / deltaZ;
minimumPosition = StrictMath.max(minimumPosition, StrictMath.min(first, second));
maximumPosition = StrictMath.min(maximumPosition, StrictMath.max(first, second));
if (minimumPosition > maximumPosition) {
return Double.NaN;
}
}
return minimumPosition;
private static double clamp(double value, double minimum, double maximum) {
return StrictMath.max(minimum, StrictMath.min(maximum, value));
}
private static double pointRectangleDistanceSquared(
@@ -616,6 +692,4 @@ public final class RiverTile {
private record ClosestPoint(double distanceSquared, double alongReach) {
}
private record SegmentPoint(double distanceSquared, double t) {
}
}
@@ -7,6 +7,7 @@ public final class RiverTopologyComplexity {
public static final long MAXIMUM_SOURCE_WINDOW_CELLS = 65_536L;
public static final long MAXIMUM_ROUTE_SCAN_STEPS = 65_536L;
public static final long MAXIMUM_BUCKET_WRITES_PER_REACH = 1_048_576L;
public static final long MAXIMUM_TUNNEL_SAMPLE_COLUMNS = 65_536L;
private static final int SPATIAL_BUCKET_SIZE = 64;
private RiverTopologyComplexity() {
@@ -83,6 +84,70 @@ public final class RiverTopologyComplexity {
}
}
public static int tunnelHalo(
double maximumChannelWidth,
double maximumTunnelWidthMultiplier,
double tunnelMouthBlend
) {
if (!Double.isFinite(maximumChannelWidth) || maximumChannelWidth <= 0D
|| !Double.isFinite(maximumTunnelWidthMultiplier) || maximumTunnelWidthMultiplier < 1D
|| !Double.isFinite(tunnelMouthBlend) || tunnelMouthBlend < 0D) {
throw new IllegalArgumentException("River tunnel dimensions must be finite and valid");
}
return Math.max(
1,
(int) StrictMath.ceil(
maximumChannelWidth * 0.5D * maximumTunnelWidthMultiplier + tunnelMouthBlend
) + 1
);
}
public static long tunnelSampleColumns(
double maximumChannelWidth,
double maximumTunnelWidthMultiplier,
double tunnelMouthBlend
) {
long axis = 16L + 2L * tunnelHalo(
maximumChannelWidth,
maximumTunnelWidthMultiplier,
tunnelMouthBlend
);
return saturatedMultiply(axis, axis);
}
public static String tunnelPlanViolation(
double maximumChannelWidth,
double maximumTunnelWidthMultiplier,
double tunnelMouthBlend
) {
long columns = tunnelSampleColumns(
maximumChannelWidth,
maximumTunnelWidthMultiplier,
tunnelMouthBlend
);
if (columns <= MAXIMUM_TUNNEL_SAMPLE_COLUMNS) {
return null;
}
return "River tunnel planning may sample " + columns
+ " columns per generated chunk, above the safe limit of " + MAXIMUM_TUNNEL_SAMPLE_COLUMNS
+ "; reduce maxChannelWidth, tunnelWidthMultiplier.max, or tunnelMouthBlend.";
}
public static void requireSafeTunnelPlan(
double maximumChannelWidth,
double maximumTunnelWidthMultiplier,
double tunnelMouthBlend
) {
String violation = tunnelPlanViolation(
maximumChannelWidth,
maximumTunnelWidthMultiplier,
tunnelMouthBlend
);
if (violation != null) {
throw new IllegalArgumentException(violation);
}
}
private static long ceilToLong(double value) {
if (!Double.isFinite(value) || value >= Long.MAX_VALUE) {
return Long.MAX_VALUE;
@@ -0,0 +1,89 @@
package art.arcane.iris.engine.river;
import java.util.Arrays;
public final class RiverWidthProfile {
private final double[] positions;
private final double[] widths;
private final double maximum;
public RiverWidthProfile(double[] positions, double[] widths) {
if (positions == null || widths == null || positions.length < 2 || positions.length != widths.length) {
throw new IllegalArgumentException("River width profiles require matching position and width samples");
}
this.positions = positions.clone();
this.widths = widths.clone();
double resolvedMaximum = 0D;
for (int index = 0; index < this.positions.length; index++) {
double position = this.positions[index];
double width = this.widths[index];
if (!Double.isFinite(position) || position < 0D || position > 1D
|| (index > 0 && position <= this.positions[index - 1])) {
throw new IllegalArgumentException("River width profile positions must increase from zero to one");
}
if (!Double.isFinite(width) || width <= 0D) {
throw new IllegalArgumentException("River width profile widths must be finite and positive");
}
resolvedMaximum = Math.max(resolvedMaximum, width);
}
if (this.positions[0] != 0D || this.positions[this.positions.length - 1] != 1D) {
throw new IllegalArgumentException("River width profile positions must include zero and one");
}
maximum = resolvedMaximum;
}
public static RiverWidthProfile constant(double width) {
return new RiverWidthProfile(new double[]{0D, 1D}, new double[]{width, width});
}
public double sample(double alongReach) {
double position = Math.max(0D, Math.min(1D, alongReach));
int index = Arrays.binarySearch(positions, position);
if (index >= 0) {
return widths[index];
}
int upper = -index - 1;
if (upper <= 0) {
return widths[0];
}
if (upper >= positions.length) {
return widths[widths.length - 1];
}
int lower = upper - 1;
double range = positions[upper] - positions[lower];
double interpolation = range <= 0D ? 0D : (position - positions[lower]) / range;
return widths[lower] + (widths[upper] - widths[lower]) * interpolation;
}
public double maximum() {
return maximum;
}
public int size() {
return widths.length;
}
public double position(int index) {
return positions[index];
}
public double width(int index) {
return widths[index];
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof RiverWidthProfile profile)) {
return false;
}
return Arrays.equals(positions, profile.positions) && Arrays.equals(widths, profile.widths);
}
@Override
public int hashCode() {
return 31 * Arrays.hashCode(positions) + Arrays.hashCode(widths);
}
}
@@ -656,6 +656,9 @@ public final class RiverCaveContainmentPlanner {
return boundsRejection;
}
CaveVoxel voxel = voxelAt(view, position);
if (isGeneratedInletCarve(view, source, settings, position, voxel)) {
continue;
}
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
if (hazard != RiverCaveRejection.NONE) {
return hazard;
@@ -680,7 +683,8 @@ public final class RiverCaveContainmentPlanner {
if (carve.contains(neighbor)) {
continue;
}
if (isInletOpening(source, neighbor)) {
if (isInletOpening(source, neighbor)
|| isGeneratedInletOpening(view, source, settings, neighbor)) {
continue;
}
if (!view.isInWorld(neighbor)) {
@@ -726,7 +730,8 @@ public final class RiverCaveContainmentPlanner {
RiverCaveAction action;
if (position.y() <= source.waterHeadY()) {
action = RiverCaveAction.WET_SOURCE;
} else if (source.mode() == RiverCaveMode.WATERFALL_POOL) {
} else if (source.mode() == RiverCaveMode.WATERFALL_POOL
|| source.mode() == RiverCaveMode.GENERATED_GROTTO) {
action = RiverCaveAction.FALLING_WATER;
} else {
action = RiverCaveAction.DRY_AIR;
@@ -763,6 +768,40 @@ public final class RiverCaveContainmentPlanner {
return position.equals(source.entry().offset(0, 1, 0));
}
private boolean isGeneratedInletCarve(
CaveVoxelView view,
RiverCaveSource source,
RiverCavePlannerSettings settings,
CavePosition position,
CaveVoxel voxel
) {
if (voxel != CaveVoxel.CAVE_AIR && voxel != CaveVoxel.COMPATIBLE_FLUID) {
return false;
}
int extent = Math.max(0, settings.throatRadius() - 1);
long deltaX = (long) position.x() - source.entry().x();
long deltaZ = (long) position.z() - source.entry().z();
return position.y() >= source.entry().y() - extent
&& position.y() <= source.entry().y()
&& deltaX * deltaX + deltaZ * deltaZ <= (long) extent * extent
&& view.isOpenToSurface(position);
}
private boolean isGeneratedInletOpening(
CaveVoxelView view,
RiverCaveSource source,
RiverCavePlannerSettings settings,
CavePosition position
) {
int radius = Math.max(1, settings.throatRadius());
long deltaX = (long) position.x() - source.entry().x();
long deltaZ = (long) position.z() - source.entry().z();
return position.y() >= source.entry().y()
&& position.y() <= source.entry().y() + 1
&& deltaX * deltaX + deltaZ * deltaZ < (long) radius * radius
&& view.isOpenToSurface(position);
}
private RiverCaveRejection validateSource(RiverCaveSource source) {
if (source.entry().y() < source.waterHeadY()) {
return RiverCaveRejection.INVALID_SOURCE;
@@ -34,6 +34,7 @@ import art.arcane.iris.engine.river.RiverTerrainSampler;
import art.arcane.iris.engine.river.RiverTerminalPolicy;
import art.arcane.iris.engine.river.RiverTile;
import art.arcane.iris.engine.river.RiverTileCache;
import art.arcane.iris.engine.river.RiverTopologyComplexity;
import art.arcane.iris.util.project.interpolation.NoiseBounds;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.stream.ProceduralStream;
@@ -65,6 +66,9 @@ 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 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;
@@ -86,6 +90,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
private final ProceduralStream<IrisRegion> region;
private final IrisRiverTerrain terrain;
private final IrisRiverWater water;
private final IrisRiverCaves caves;
private final CNG sourceNoise;
private final CNG continuationNoise;
private final CNG incisionNoise;
@@ -97,6 +102,9 @@ public final class IrisRiverRuntime implements AutoCloseable {
private final CNG bedNoise;
private final CNG biomeNoise;
private final CNG caveEntryNoise;
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;
@@ -121,7 +129,13 @@ public final class IrisRiverRuntime implements AutoCloseable {
naturalBiome = context.naturalBiome();
region = context.region();
terrain = Objects.requireNonNull(configuration.getTerrain());
RiverTopologyComplexity.requireSafeTunnelPlan(
terrain.getMaxChannelWidth(),
maximumTunnelWidthMultiplier(terrain),
terrain.getTunnelMouthBlend()
);
water = Objects.requireNonNull(configuration.getWater());
caves = configuration.getCaves() == null ? new IrisRiverCaves() : configuration.getCaves();
IrisRiverTopology topology = Objects.requireNonNull(configuration.getTopology());
sourceNoise = noise(topology.getSource(), SOURCE_NOISE_SALT);
continuationNoise = noise(topology.getContinuation(), CONTINUATION_NOISE_SALT);
@@ -133,7 +147,10 @@ public final class IrisRiverRuntime implements AutoCloseable {
meanderNoise = noise(terrain.getMeanderStyle(), MEANDER_NOISE_SALT);
bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT);
biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT);
caveEntryNoise = noise(caveSettings().getEntry(), CAVE_ENTRY_NOISE_SALT);
caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_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);
settingsCache = new ConcurrentHashMap<>();
biomePoolCache = new ConcurrentHashMap<>();
RiverNetworkOptions options = options(topology, terrain);
@@ -190,34 +207,98 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
public IrisRiverTunnelSample sampleTunnel(double x, double z) {
ResolvedRiverColumn column = resolveColumn(x, z);
if (column == null
|| !column.subterranean()
|| column.river().state() != RiverRouteState.WET
|| column.river().section() != RiverSection.CHANNEL) {
double mouthBlend = terrain.getTunnelMouthBlend();
double maximumMultiplier = maximumTunnelWidthMultiplier(terrain);
double maximumExtraRadius = terrain.getMaxChannelWidth() * 0.5D * (maximumMultiplier - 1D)
+ mouthBlend;
RiverTile tile = tileAt(x, z);
RiverSample river = tile.sampleExpanded(x, z, maximumExtraRadius);
if (!river.present() || river.state() != RiverRouteState.WET) {
return null;
}
double widthMultiplier = styled(
terrain.getTunnelWidthMultiplier(),
tunnelWidthNoise,
(int) StrictMath.round(x),
(int) StrictMath.round(z),
1D
);
double channelRadius = river.width() * 0.5D * widthMultiplier;
if (river.distance() > channelRadius + mouthBlend) {
return null;
}
ResolvedRiverColumn column = resolveColumn(x, z, tile, river);
if (!column.subterranean()) {
return null;
}
if (isTunnelMouth(column, mouthBlend)) {
channelRadius += mouthBlend;
}
if (column.river().distance() > channelRadius) {
return null;
}
double channelRadius = column.river().width() * 0.5D;
double normalizedDistance = channelRadius <= 0D
? 1D
: clamp01(column.river().distance() / channelRadius);
double roofProfile = StrictMath.sqrt(Math.max(0D, 1D - normalizedDistance * normalizedDistance));
int bedY = (int) Math.round(column.bedHeight());
double profile = StrictMath.sqrt(Math.max(0D, 1D - normalizedDistance * normalizedDistance));
int waterHeadY = (int) Math.round(column.waterSurfaceY());
int ceilingY = waterHeadY + (int) StrictMath.ceil(caveSettings().getDryHeadroom() * roofProfile);
if (bedY >= waterHeadY) {
return null;
}
double floorOffset = tunnelFloorNoise.fitDouble(
-terrain.getTunnelFloorVariation(),
terrain.getTunnelFloorVariation(),
x,
z
);
int bedY = shapedTunnelBedY(waterHeadY, column.bedHeight(), profile, floorOffset);
double roofOffset = tunnelRoofNoise.fitDouble(
-terrain.getTunnelRoofVariation(),
terrain.getTunnelRoofVariation(),
x,
z
);
int ceilingY = shapedTunnelCeilingY(
waterHeadY, caves.getDryHeadroom(), profile, roofOffset);
return new IrisRiverTunnelSample(column.river(), bedY, waterHeadY, ceilingY);
}
static int shapedTunnelBedY(
int waterHeadY,
double baseBedY,
double profile,
double floorOffset
) {
double baseDepth = Math.max(1D, waterHeadY - baseBedY);
double shapedDepth = Math.max(1D, (baseDepth + floorOffset) * clamp01(profile));
return waterHeadY - Math.max(1, (int) StrictMath.ceil(shapedDepth));
}
static int shapedTunnelCeilingY(
int waterHeadY,
double dryHeadroom,
double profile,
double roofOffset
) {
double shapedHeadroom = Math.max(
0D,
(Math.max(0D, dryHeadroom) + roofOffset) * clamp01(profile)
);
return waterHeadY + (int) StrictMath.ceil(shapedHeadroom);
}
private ResolvedRiverColumn resolveColumn(double x, double z) {
double sampledNaturalHeight = naturalHeight.get(x, z);
return resolveColumn(x, z, 0D);
}
private ResolvedRiverColumn resolveColumn(double x, double z, double additionalRadius) {
RiverTile tile = tileAt(x, z);
RiverSample river = tile.sample(x, z);
RiverSample river = tile.sampleExpanded(x, z, additionalRadius);
if (!river.present()) {
return null;
}
return resolveColumn(x, z, tile, river);
}
private ResolvedRiverColumn resolveColumn(double x, double z, RiverTile tile, RiverSample river) {
double sampledNaturalHeight = naturalHeight.get(x, z);
RiverReach reach = tile.reach(river.reachId());
IrisRegion sampledRegion = region.get(x, z);
IrisBiome sampledBiome = naturalBiome.get(x, z);
@@ -229,6 +310,12 @@ public final class IrisRiverRuntime implements AutoCloseable {
? waterSurfaceY - river.depth() + bedRoughness(x, z)
: sampledNaturalHeight - river.depth() + bedRoughness(x, z);
double maximumIncision = Math.max(0D, terrain.getMaxIncision() * settings.maxIncisionMultiplier());
double cappedSurface = incisedHeight(
sampledNaturalHeight,
bedHeight,
1D,
maximumIncision
);
return new ResolvedRiverColumn(
river,
reach,
@@ -236,14 +323,27 @@ public final class IrisRiverRuntime implements AutoCloseable {
waterSurfaceY,
bedHeight,
maximumIncision,
isSubterraneanSegment(reach, river.alongReach())
boreMantleActive
&& river.state() == RiverRouteState.WET
&& Math.round(cappedSurface) >= Math.round(waterSurfaceY)
);
}
private boolean isSubterraneanSegment(RiverReach reach, double alongReach) {
if (!boreMantleActive || reach.state() != RiverRouteState.WET) {
private boolean isTunnelMouth(ResolvedRiverColumn column, double mouthBlend) {
if (mouthBlend <= 0D) {
return false;
}
double length = column.reach().polyline().length();
if (length <= 0D) {
return false;
}
double offset = mouthBlend / length;
double alongReach = column.river().alongReach();
return !isCenterlineSubterranean(column.reach(), clamp01(alongReach - offset))
|| !isCenterlineSubterranean(column.reach(), clamp01(alongReach + offset));
}
private boolean isCenterlineSubterranean(RiverReach reach, double alongReach) {
CenterlinePosition center = centerlinePosition(reach, alongReach);
IrisRegion sampledRegion = region.get(center.x(), center.z());
IrisBiome sampledBiome = naturalBiome.get(center.x(), center.z());
@@ -363,14 +463,25 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
public IrisRiverCaves caveSettings() {
IrisRiverCaves caves = configuration.getCaves();
return caves == null ? new IrisRiverCaves() : caves;
return caves;
}
public double maximumChannelWidth() {
return terrain.getMaxChannelWidth();
}
public double maximumTunnelWidthMultiplier() {
return maximumTunnelWidthMultiplier(terrain);
}
public double tunnelMouthBlend() {
return terrain.getTunnelMouthBlend();
}
public int maximumTunnelHeadroom() {
return caves.getDryHeadroom() + (int) StrictMath.ceil(terrain.getTunnelRoofVariation());
}
public boolean acceptsCaveAnchor(RiverAnchor anchor) {
Objects.requireNonNull(anchor);
IrisRiverCaves caves = caveSettings();
@@ -408,6 +519,9 @@ public final class IrisRiverRuntime implements AutoCloseable {
if (!caveEntryEligible(caves, stableId, position.x(), position.z())) {
continue;
}
if (!isCaveAnchorSourceable(position.x(), position.z())) {
continue;
}
if (index == anchor.index()) {
return stableId == anchor.stableId() && accepted < caves.getMaximumPerReach();
}
@@ -419,6 +533,17 @@ public final class IrisRiverRuntime implements AutoCloseable {
return false;
}
private boolean isCaveAnchorSourceable(double x, double z) {
IrisRiverSurfaceSample surface = sample(x, z);
if (surface.river().present()
&& surface.river().state() == RiverRouteState.WET
&& surface.river().section() == RiverSection.CHANNEL
&& surface.surfaceFluid()) {
return true;
}
return sampleTunnel(x, z) != null;
}
public boolean isTerminalCaveAnchor(RiverAnchor anchor) {
TerminalCaveAnchor terminal = terminalCaveAnchor(anchor);
return terminal != null && anchor.stableId() == terminal.stableId();
@@ -490,6 +615,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
.minimumSourcesPerTile(topology.getMinimumSourcesPerTile())
.downstreamCandidateLimit(Math.max(1, Math.min(8, topology.getSinkSearchReaches() + 1)))
.routingBasinCells(topology.getRoutingBasinCells())
.routingDeviationScaleCells(topology.getRoutingDeviationScaleCells())
.routingDeviationStrengthCells(topology.getRoutingDeviationStrengthCells())
.routingPlateauHeight(topology.getRoutingPlateauHeight())
.hydraulicBaseHeight(fluidHeight)
.requireOcean(topology.isRequireOcean())
@@ -498,7 +625,10 @@ public final class IrisRiverRuntime implements AutoCloseable {
.dryChannelChance(dryChance)
.terrainHeightWeight(topology.getTerrainHeightWeight())
.routingNoiseWeight(0D)
.flowAlignmentWeight(topology.getRoutingNoiseWeight())
.flowAlignmentWeight(topology.getFlowAlignmentWeight())
.confluenceWeight(topology.getConfluenceWeight())
.branchSoftCap(topology.getBranchSoftCap())
.branchChildShrinkFactor(topology.getBranchChildShrinkFactor())
.oceanAttraction(topology.getOceanAttraction())
.channelWidth(mid(riverTerrain.getChannelWidth(), 12D))
.bankWidth(mid(riverTerrain.getBankWidth(), 8D))
@@ -792,11 +922,29 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
double minimum = Math.min(range.getMin(), range.getMax());
double maximum = Math.max(range.getMin(), range.getMax());
if (minimum == maximum) {
return minimum;
}
return noise.fitDouble(minimum, maximum, x, z);
}
private static double maximumReachRadius(IrisRiverTopology topology, IrisRiverTerrain riverTerrain) {
return riverTerrain.getMaxChannelWidth() * 0.5D + riverTerrain.getMaxBankWidth();
double surfaceRadius = riverTerrain.getMaxChannelWidth() * 0.5D
+ riverTerrain.getMaxBankWidth();
double tunnelRadius = riverTerrain.getMaxChannelWidth() * 0.5D
* maximumTunnelWidthMultiplier(riverTerrain)
+ riverTerrain.getTunnelMouthBlend();
return Math.max(surfaceRadius, tunnelRadius);
}
private static double maximumTunnelWidthMultiplier(IrisRiverTerrain riverTerrain) {
IrisStyledRange configured = riverTerrain.getTunnelWidthMultiplier();
if (configured == null
|| !Double.isFinite(configured.getMin())
|| !Double.isFinite(configured.getMax())) {
return 1D;
}
return Math.max(1D, Math.max(configured.getMin(), configured.getMax()));
}
private static double clamp01(double value) {
@@ -820,12 +968,14 @@ public final class IrisRiverRuntime implements AutoCloseable {
@Override
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.SEA_LEVEL
|| oceanIntent;
double sampledNaturalHeight = naturalHeightRequired
? naturalHeight.get(blockX, blockZ)
: fluidHeight;
boolean sampledOcean = Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ));
boolean sampledOcean = oceanIntent && isSubmergedOutlet(sampledNaturalHeight);
IrisRegion sampledRegion = region.get(blockX, blockZ);
IrisBiome sampledBiome = biomeRiverOverridesPossible ? naturalBiome.get(blockX, blockZ) : null;
EffectiveRiverSettings settings = settingsFor(sampledRegion, sampledBiome);
@@ -846,7 +996,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
return new RiverTerrainSourceSample(
chanceMultiplier,
settings.routingPolicy() != IrisRiverRoutingPolicy.BLOCK,
Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ))
isSubmergedOceanIntent(blockX, blockZ)
);
}
@@ -857,7 +1007,16 @@ public final class IrisRiverRuntime implements AutoCloseable {
@Override
public boolean isOcean(int blockX, int blockZ) {
return Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ));
return isSubmergedOceanIntent(blockX, blockZ);
}
private boolean isSubmergedOceanIntent(int blockX, int blockZ) {
return Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ))
&& isSubmergedOutlet(naturalHeight.get(blockX, blockZ));
}
private boolean isSubmergedOutlet(double sampledNaturalHeight) {
return Math.round(sampledNaturalHeight) < Math.round(fluidHeight);
}
@Override
@@ -999,6 +1158,24 @@ public final class IrisRiverRuntime implements AutoCloseable {
* settings.widthMultiplier();
}
@Override
public double channelWidth(
RiverRoutingContext context,
double x,
double z,
double fallback
) {
EffectiveRiverSettings settings = settingsAt(x, z);
return styled(
terrain.getChannelWidth(),
widthNoise,
(int) StrictMath.round(x),
(int) StrictMath.round(z),
fallback
)
* settings.widthMultiplier();
}
@Override
public double bankWidth(RiverRoutingContext context, double fallback) {
EffectiveRiverSettings settings = settingsAt(context.midpointX(), context.midpointZ());
@@ -17,7 +17,6 @@ public interface DirectorContextHandler<T> extends DirectorContextHandlerType<T,
h -> ((DirectorContextHandler<?>) h).getType(),
e -> {
IrisLogging.reportError(e);
e.printStackTrace();
});
}
}
@@ -1,7 +1,6 @@
package art.arcane.iris.util.common.misc;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import io.github.slimjar.app.builder.ApplicationBuilder;
import io.github.slimjar.app.builder.SpigotApplicationBuilder;
@@ -13,7 +12,6 @@ import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
public class SlimJar {
private static final boolean DEBUG = Boolean.getBoolean("iris.debug-slimjar");
@@ -29,9 +27,7 @@ public class SlimJar {
if (loaded.getAndSet(true)) return;
VolmitPlugin plugin = BukkitPlatform.volmitPlugin();
Path downloadPath = plugin.getDataFolder("cache", "libraries").toPath();
Logger logger = plugin.getLogger();
logger.info("Loading libraries...");
debug(plugin, "Loading libraries...");
try {
new SpigotApplicationBuilder(plugin)
.downloadDirectoryPath(downloadPath)
@@ -40,15 +36,14 @@ public class SlimJar {
} catch (Throwable e) {
// The Spigot builder is a probe: not every server exposes it, and the fallback is the
// supported path on the ones that do not.
IrisLogging.info("Failed to inject the library loader, falling back to application builder");
debug(plugin, "Failed to inject the library loader, falling back to application builder");
ApplicationBuilder.appending(plugin.getName())
.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) {
if (!DEBUG) return;
plugin.getLogger().info(message.formatted(args));
SlimJar.debug(plugin, message.formatted(args));
}
@Override
@@ -58,15 +53,20 @@ public class SlimJar {
@Override
public void debug(@NotNull String message, @Nullable Object... args) {
if (!DEBUG) return;
plugin.getLogger().info(message.formatted(args));
SlimJar.debug(plugin, message.formatted(args));
}
})
.build();
}
logger.info("Libraries loaded successfully!");
debug(plugin, "Libraries loaded successfully!");
} finally {
lock.unlock();
}
}
private static void debug(VolmitPlugin plugin, String message) {
if (DEBUG) {
plugin.getLogger().info("[DEBUG] " + message);
}
}
}
@@ -1,6 +1,7 @@
package art.arcane.iris.util.common.misc;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
@@ -53,7 +54,7 @@ public class getHardware {
String cpuModel = processor.getProcessorIdentifier().getName();
return cpuModel.isEmpty() ? "Unknown CPU Model" : cpuModel;
} catch (Exception e) {
e.printStackTrace();
logProbeFailure("CPU model", e);
return "Unknown CPU Model";
}
}
@@ -67,7 +68,7 @@ public class getHardware {
temps.add("Fan Speeds: " + Arrays.toString(systemInfo.getHardware().getSensors().getFanSpeeds()));
return temps.copy();
} catch (Exception e) {
e.printStackTrace();
logProbeFailure("sensors", e);
}
return null;
}
@@ -83,7 +84,7 @@ public class getHardware {
}
return gpus.copy();
} catch (Exception e) {
e.printStackTrace();
logProbeFailure("graphics cards", e);
}
return null;
}
@@ -117,7 +118,7 @@ public class getHardware {
}
return systemDisks.copy();
} catch (Exception e) {
e.printStackTrace();
logProbeFailure("disks", e);
}
return null;
}
@@ -140,7 +141,7 @@ public class getHardware {
}
return systemPowerSources.copy();
} catch (Exception e) {
e.printStackTrace();
logProbeFailure("power sources", e);
}
return null;
}
@@ -159,7 +160,7 @@ public class getHardware {
}
return systemEDID.copy();
} catch (Exception e) {
e.printStackTrace();
logProbeFailure("displays", e);
}
return null;
}
@@ -177,8 +178,13 @@ public class getHardware {
}
return interfaces.copy();
} catch (Exception e) {
e.printStackTrace();
logProbeFailure("network interfaces", e);
}
return null;
}
}
private static void logProbeFailure(String component, Exception failure) {
IrisLogging.debug("Hardware " + component + " probe failed: " + failure.getClass().getSimpleName()
+ (failure.getMessage() == null ? "" : " - " + failure.getMessage()));
}
}
@@ -78,10 +78,11 @@ public class NBTWorld {
IrisLogging::info,
IrisLogging::debug,
(message, error) -> {
IrisLogging.error(message);
if (error != null) {
error.printStackTrace();
IrisLogging.reportError(message, error);
return;
}
IrisLogging.error(message);
}
),
M::ms,
@@ -40,6 +40,8 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
@@ -77,7 +79,6 @@ public class J {
SchedulerBridge.setCancelScheduler(J::car);
SchedulerBridge.setErrorHandler(e -> {
IrisLogging.reportError(e);
e.printStackTrace();
});
SchedulerBridge.setInfoLogger(IrisLogging::debug);
SchedulerBridge.setThreadRegistrar(thread -> {
@@ -104,7 +105,6 @@ public class J {
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to run async task");
e.printStackTrace();
}
});
}
@@ -116,7 +116,6 @@ public class J {
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to run async task");
e.printStackTrace();
}
});
}
@@ -379,6 +378,7 @@ public class J {
}
public static void cancelPluginTasks() {
cancelTrackedRepeatingTasks();
if (!BukkitPlatform.hasPlugin()) {
return;
}
@@ -393,6 +393,17 @@ public class J {
}
}
static void cancelTrackedRepeatingTasks() {
List<Runnable> cancelActions;
synchronized (REPEATING_CANCELLERS) {
cancelActions = new ArrayList<>(REPEATING_CANCELLERS.values());
REPEATING_CANCELLERS.clear();
}
for (Runnable cancelAction : cancelActions) {
cancelAction.run();
}
}
public static void s(Runnable r) {
if (!BUKKIT_PRESENT) {
if (IrisPlatforms.isBound()) {
@@ -587,7 +598,6 @@ public class J {
r.run();
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
if (state.cancelled || !canSchedule()) {
REPEATING_CANCELLERS.remove(taskId);
@@ -677,7 +687,6 @@ public class J {
r.run();
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
if (state.cancelled || !canSchedule()) {
REPEATING_CANCELLERS.remove(taskId);
@@ -708,13 +717,18 @@ public class J {
}
private static int trackRepeatingTask(Runnable cancelAction) {
int id = TASK_IDS.getAndIncrement();
REPEATING_CANCELLERS.put(id, cancelAction);
return id;
synchronized (REPEATING_CANCELLERS) {
int id = TASK_IDS.getAndIncrement();
REPEATING_CANCELLERS.put(id, cancelAction);
return id;
}
}
private static void cancelRepeatingTask(int id) {
Runnable cancelAction = REPEATING_CANCELLERS.remove(id);
Runnable cancelAction;
synchronized (REPEATING_CANCELLERS) {
cancelAction = REPEATING_CANCELLERS.remove(id);
}
if (cancelAction != null) {
cancelAction.run();
}
@@ -20,6 +20,7 @@ package art.arcane.iris.util.common.scheduling.jobs;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.network.DL;
import art.arcane.volmlib.util.network.DownloadMonitor;
@@ -63,7 +64,7 @@ public class DownloadJob implements Job {
download.downloadChunk();
}
} catch (IOException e) {
e.printStackTrace();
IrisLogging.reportError("Iris pack download failed.", e);
}
cw = tw;
@@ -112,10 +112,9 @@ public final class IrisMatterSupport {
long oldSize = folder.length();
object.read(folder);
from(object).write(folder);
IrisLogging.info("Converted " + folder.getPath() + " Saved " + (oldSize - folder.length()));
IrisLogging.debug("Converted " + folder.getPath() + " Saved " + (oldSize - folder.length()));
} catch (Throwable e) {
IrisLogging.error("Failed to convert " + folder.getPath());
e.printStackTrace();
IrisLogging.reportError("Failed to convert " + folder.getPath() + ".", e);
}
return 0;
@@ -217,7 +217,7 @@ public class CNG {
r += cng.fit(-1000, 1000, i, i);
}
System.out.println(Form.duration(p.getMilliseconds(), 10) + " merged = " + r);
IrisLogging.info(Form.duration(p.getMilliseconds(), 10) + " merged = " + r);
}
public CNG cellularize(RNG seed, double freq) {
@@ -81,7 +81,6 @@ public interface ProceduralStream<T> extends ProceduralLayer, Interpolated<T> {
} catch (IncompatibleClassChangeError e) {
IrisLogging.warn(f.toString());
IrisLogging.reportError(e);
e.printStackTrace();
return null;
}
}

Some files were not shown because too many files have changed in this diff Show More