mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
f
This commit is contained in:
@@ -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;
|
||||
|
||||
-35
@@ -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)) {
|
||||
|
||||
-2
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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()) {
|
||||
|
||||
+21
-29
@@ -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;
|
||||
|
||||
-4
@@ -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()) {
|
||||
|
||||
+78
-19
@@ -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);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
+41
-2
@@ -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
Reference in New Issue
Block a user