This commit is contained in:
Brian Neumann-Fopiano committed 2026-08-15 21:32:00 -04:00
1 parent ef2e1b8f58
commit fc0fdf4ce4
81 files changed
+5109 -511

No files matched your search

@@ -289,6 +289,19 @@ public final class IrisWorldStorage {
return requireSafeManagedDimensionRoot(levelRoot(), key);
}
public static File requireSafePersistentDimensionRoot(NamespacedKey key) {
return requireSafePersistentDimensionRoot(levelRoot(), key);
}
static File requireSafePersistentDimensionRoot(File levelRoot, NamespacedKey key) {
NamespacedKey worldKey = Objects.requireNonNull(key, "key");
WorldSlotKey slotKey = new WorldSlotKey(worldKey.getNamespace(), worldKey.getKey());
return ExactWorldSlotPathPolicy.resolve(
Objects.requireNonNull(levelRoot, "levelRoot").toPath(),
slotKey
).worldDirectory().toFile();
}
public static File requireSafeManagedDimensionRoot(File levelRoot, NamespacedKey key) {
NamespacedKey worldKey = Objects.requireNonNull(key, "key");
if (!IRIS_NAMESPACE.equals(worldKey.getNamespace()) || !worldKey.getKey().matches("[a-z0-9_-]+")) {
@@ -368,4 +381,131 @@ public final class IrisWorldStorage {
public static File packRoot(NamespacedKey key) {
return new File(dimensionRoot(key), "iris/pack");
}
public static File requireFrozenDimensionRoot(
File worldContainer,
File levelRoot,
String bukkitWorldName,
NamespacedKey key
) {
NamespacedKey worldKey = Objects.requireNonNull(key, "key");
return frozenDimensionRoot(worldContainer, levelRoot, bukkitWorldName, worldKey)
.orElseThrow(() -> new IllegalStateException(
"Frozen Iris world storage is missing for " + worldKey + "."));
}
public static Optional<File> frozenDimensionRoot(
File worldContainer,
File levelRoot,
String bukkitWorldName,
NamespacedKey key
) {
File requiredLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot").getAbsoluteFile();
NamespacedKey worldKey = Objects.requireNonNull(key, "key");
String requiredWorldName = Objects.requireNonNull(bukkitWorldName, "bukkitWorldName").trim();
if (requiredWorldName.isEmpty()) {
throw new IllegalArgumentException("Bukkit world name cannot be empty.");
}
File directRoot = dimensionRoot(requiredLevelRoot, worldKey);
boolean directExists = isExistingSafeDimensionRoot(requiredLevelRoot.toPath(), directRoot.toPath());
if (!IRIS_NAMESPACE.equals(worldKey.getNamespace())) {
return directExists ? Optional.of(directRoot) : Optional.empty();
}
String configuredName = configuredWorldName(worldKey, requiredLevelRoot.getName());
if (!configuredName.equals(requiredWorldName)) {
return directExists ? Optional.of(directRoot) : Optional.empty();
}
File configuredRoot = configuredDimensionRoot(worldContainer, requiredLevelRoot, worldKey);
boolean configuredExists = isExistingSafeDimensionRoot(
Objects.requireNonNull(worldContainer, "worldContainer").toPath(),
configuredRoot.toPath()
);
if (directExists == configuredExists) {
if (directExists) {
throw new IllegalStateException("Frozen Iris world storage is ambiguous for " + worldKey + ".");
}
return Optional.empty();
}
return Optional.of(directExists ? directRoot : configuredRoot);
}
public static File configuredLevelRoot(File worldContainer, File levelRoot, NamespacedKey key) {
Path container = Objects.requireNonNull(worldContainer, "worldContainer")
.toPath()
.toAbsolutePath()
.normalize();
String configuredName = configuredWorldName(
Objects.requireNonNull(key, "key"),
Objects.requireNonNull(levelRoot, "levelRoot").getName()
);
Path configuredLevelRoot = container.resolve(configuredName).normalize();
if (!Objects.equals(configuredLevelRoot.getParent(), container)) {
throw new IllegalStateException("Configured Bukkit world storage escapes the world container.");
}
isExistingSafeDimensionRoot(container, configuredLevelRoot.resolve("dimensions"));
return configuredLevelRoot.toFile();
}
public static File configuredDimensionRoot(File worldContainer, File levelRoot, NamespacedKey key) {
File configuredLevelRoot = configuredLevelRoot(worldContainer, levelRoot, key);
File configuredRoot = dimensionRoot(configuredLevelRoot, key);
isExistingSafeDimensionRoot(
Objects.requireNonNull(worldContainer, "worldContainer").toPath(),
configuredRoot.toPath()
);
return configuredRoot;
}
public static File requireFrozenPackRoot(File dimensionRoot) {
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toPath()
.toAbsolutePath()
.normalize();
Path irisRoot = root.resolve("iris");
Path packRoot = irisRoot.resolve("pack");
for (Path path : new Path[]{root, irisRoot, packRoot}) {
if (Files.isSymbolicLink(path)) {
throw new IllegalStateException("Frozen Iris pack path contains a symbolic link: " + path);
}
if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)
&& !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalStateException("Frozen Iris pack path is not a directory: " + path);
}
}
if (!Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalStateException("Frozen Iris pack snapshot is missing: " + packRoot);
}
return packRoot.toFile();
}
private static boolean isExistingSafeDimensionRoot(Path storageRoot, Path dimensionRoot) {
Path root = storageRoot.toAbsolutePath().normalize();
Path target = dimensionRoot.toAbsolutePath().normalize();
if (Files.isSymbolicLink(root)) {
throw new IllegalStateException("Iris world storage root is a symbolic link: " + root);
}
if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalStateException("Iris world storage root is not a directory: " + root);
}
if (!target.startsWith(root) || Objects.equals(target, root)) {
throw new IllegalStateException("Iris world storage escapes its expected root: " + target);
}
Path relative = root.relativize(target);
Path current = root;
for (Path segment : relative) {
current = current.resolve(segment);
if (Files.isSymbolicLink(current)) {
throw new IllegalStateException("Iris world storage contains a symbolic link: " + current);
}
if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)
&& !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalStateException("Iris world storage path is not a directory: " + current);
}
}
return Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS);
}
}
@@ -26,12 +26,12 @@ import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
public class IrisWorlds {
@@ -147,8 +147,9 @@ public class IrisWorlds {
public synchronized void clean() {
boolean removed = worlds.entrySet().removeIf(entry -> {
try {
File packRoot = packRoot(entry.getKey());
return !new File(packRoot, "dimensions/" + entry.getValue() + ".json").exists();
Optional<File> packRoot = packRoot(entry.getKey());
return packRoot.isEmpty()
|| !new File(packRoot.get(), "dimensions/" + entry.getValue() + ".json").exists();
} catch (IllegalArgumentException e) {
return true;
}
@@ -236,12 +237,20 @@ public class IrisWorlds {
.equals(configuredWorldName)) {
continue;
}
Path dimensionRoot = IrisWorldStorage.dimensionRoot(root.toFile(), worldKey)
.toPath()
.toAbsolutePath()
.normalize();
if (Files.isDirectory(dimensionRoot, LinkOption.NOFOLLOW_LINKS)) {
result.put(configuredWorldName, entry.getValue());
Path worldContainer = root.getParent();
if (worldContainer == null) {
throw new IllegalArgumentException("Selected level root has no world container: " + root);
}
try {
if (IrisWorldStorage.frozenDimensionRoot(
worldContainer.toFile(),
root.toFile(),
configuredWorldName,
worldKey
).isPresent()) {
result.put(configuredWorldName, entry.getValue());
}
} catch (IllegalStateException ignored) {
}
}
return result;
@@ -270,14 +279,28 @@ public class IrisWorlds {
return filterBukkitWorldsByStorage(levelRoot, result);
}
private File packRoot(String worldIdentity) {
private Optional<File> packRoot(String worldIdentity) {
NamespacedKey worldKey = WorldIdentity.parse(worldIdentity);
return new File(IrisWorldStorage.dimensionRoot(levelRoot.toFile(), worldKey), "iris/pack");
Path worldContainer = levelRoot.getParent();
if (worldContainer == null) {
throw new IllegalStateException("Selected level root has no world container: " + levelRoot);
}
String configuredWorldName = IrisWorldStorage.configuredWorldName(
worldKey,
levelRoot.getFileName().toString()
);
Optional<File> dimensionRoot = IrisWorldStorage.frozenDimensionRoot(
worldContainer.toFile(),
levelRoot.toFile(),
configuredWorldName,
worldKey
);
return dimensionRoot.map(IrisWorldStorage::requireFrozenPackRoot);
}
private IrisDimension loadDimension(String worldIdentity, String id) {
File pack = packRoot(worldIdentity);
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
File pack = packRoot(worldIdentity).orElse(null);
IrisDimension dimension = pack == null ? null : IrisData.get(pack).getDimensionLoader().load(id);
if (dimension == null) {
dimension = IrisData.loadAnyDimension(id, null);
}
@@ -1,13 +1,17 @@
package art.arcane.iris.core;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.WorldCreator;
import java.io.File;
/**
* WorldCreator.ofKey and WorldCreator#key are Paper-API-only. Once a call throws
* NoSuchMethodError (plain Spigot/CraftBukkit) this flips and every later call goes
* straight to the fallback. The fallback derives names/keys through IrisWorldStorage's
* logical mapping so keyFromName(creator.name()) round-trips on Spigot.
* current configured-name mapping so persistent Spigot worlds round-trip without changing
* their startup directory.
*/
public final class WorldCreatorCompat {
private static volatile boolean keyedCreatorsUnavailable;
@@ -16,14 +20,45 @@ public final class WorldCreatorCompat {
}
public static WorldCreator ofKey(NamespacedKey worldKey) {
if (!keyedCreatorsUnavailable) {
try {
return WorldCreator.ofKey(worldKey);
} catch (NoSuchMethodError e) {
keyedCreatorsUnavailable = true;
}
return ofKey(worldKey, IrisWorldStorage.logicalName(worldKey));
}
public static WorldCreator ofKey(NamespacedKey worldKey, String fallbackWorldName) {
WorldCreator keyedCreator = keyedCreator(worldKey);
if (keyedCreator != null) {
return keyedCreator;
}
return new WorldCreator(IrisWorldStorage.logicalName(worldKey));
return new WorldCreator(fallbackWorldName);
}
public static WorldCreator ofPersistentKey(NamespacedKey worldKey) {
WorldCreator keyedCreator = keyedCreator(worldKey);
if (keyedCreator != null) {
return keyedCreator;
}
return new WorldCreator(fallbackPersistentName(worldKey, IrisWorldStorage.levelRoot().getName()));
}
public static File persistentDimensionRoot(NamespacedKey worldKey) {
if (keyedCreator(worldKey) != null) {
return IrisWorldStorage.requireSafePersistentDimensionRoot(worldKey);
}
return IrisWorldStorage.configuredDimensionRoot(
Bukkit.getWorldContainer(),
IrisWorldStorage.levelRoot(),
worldKey
);
}
public static File persistentLevelRoot(NamespacedKey worldKey) {
if (keyedCreator(worldKey) != null) {
return persistentDimensionRoot(worldKey);
}
return IrisWorldStorage.configuredLevelRoot(
Bukkit.getWorldContainer(),
IrisWorldStorage.levelRoot(),
worldKey
);
}
public static NamespacedKey keyOf(WorldCreator creator) {
@@ -34,14 +69,33 @@ public final class WorldCreatorCompat {
keyedCreatorsUnavailable = true;
}
}
return IrisWorldStorage.keyFromName(creator.name());
return IrisWorldStorage.keyFromConfiguredWorldName(
creator.name(),
IrisWorldStorage.levelRoot().getName()
);
}
static String fallbackName(NamespacedKey worldKey, String levelName) {
return IrisWorldStorage.logicalName(worldKey, levelName);
}
static String fallbackPersistentName(NamespacedKey worldKey, String levelName) {
return IrisWorldStorage.configuredWorldName(worldKey, levelName);
}
static NamespacedKey fallbackKey(String creatorName, String levelName) {
return IrisWorldStorage.keyFromName(creatorName, levelName);
return IrisWorldStorage.keyFromConfiguredWorldName(creatorName, levelName);
}
private static WorldCreator keyedCreator(NamespacedKey worldKey) {
if (keyedCreatorsUnavailable) {
return null;
}
try {
return WorldCreator.ofKey(worldKey);
} catch (NoSuchMethodError e) {
keyedCreatorsUnavailable = true;
return null;
}
}
}
@@ -50,22 +50,21 @@ public final class WorldRemovalPathPolicy {
throw new Rejection(RejectionReason.SYMBOLIC_LINK,
"World storage path contains a symbolic link: " + normalizedLevelRoot);
}
Path target;
StorageLayout storageLayout;
try {
target = IrisWorldStorage.requireSafeManagedDimensionRoot(
normalizedLevelRoot.toFile(),
worldKey
).toPath().toAbsolutePath().normalize();
} catch (IllegalArgumentException failure) {
storageLayout = resolveStorageLayout(normalizedLevelRoot, worldKey);
} catch (RuntimeException failure) {
throw classifyStorageFailure(normalizedLevelRoot, worldKey, failure);
}
validateStoragePath(normalizedLevelRoot, worldKey, target);
validateStoragePath(normalizedLevelRoot, worldKey, storageLayout.dimensionDirectory());
validateStorageRoot(normalizedLevelRoot, worldKey, storageLayout.storageDirectory());
return new Target(
requestedIdentifier,
worldKey,
IrisWorldStorage.logicalName(worldKey, mainWorld),
normalizedLevelRoot,
target
storageLayout.dimensionDirectory(),
storageLayout.storageDirectory()
);
}
@@ -77,11 +76,11 @@ public final class WorldRemovalPathPolicy {
}
Path expected;
try {
expected = IrisWorldStorage.requireSafeManagedDimensionRoot(
normalizedLevelRoot.toFile(),
expected = resolveStorageLayout(
normalizedLevelRoot,
Objects.requireNonNull(worldKey, "worldKey")
).toPath().toAbsolutePath().normalize();
} catch (IllegalArgumentException failure) {
).dimensionDirectory();
} catch (RuntimeException failure) {
throw classifyStorageFailure(normalizedLevelRoot, worldKey, failure);
}
Path normalizedCandidate = Objects.requireNonNull(candidate, "candidate").toAbsolutePath().normalize();
@@ -91,6 +90,28 @@ public final class WorldRemovalPathPolicy {
}
}
public static void validateStorageRoot(Path levelRoot, NamespacedKey worldKey, Path candidate) {
Path normalizedLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize();
if (Files.isSymbolicLink(normalizedLevelRoot)) {
throw new Rejection(RejectionReason.SYMBOLIC_LINK,
"World storage path contains a symbolic link: " + normalizedLevelRoot);
}
Path expected;
try {
expected = resolveStorageLayout(
normalizedLevelRoot,
Objects.requireNonNull(worldKey, "worldKey")
).storageDirectory();
} catch (RuntimeException failure) {
throw classifyStorageFailure(normalizedLevelRoot, worldKey, failure);
}
Path normalizedCandidate = Objects.requireNonNull(candidate, "candidate").toAbsolutePath().normalize();
if (!normalizedCandidate.equals(expected)) {
throw new Rejection(RejectionReason.OUTSIDE_STORAGE_ROOT,
"The world storage directory is outside its exact current platform root.");
}
}
private static Rejection classifyIdentifierFailure(String identifier, IllegalArgumentException failure) {
NamespacedKey parsed = identifier.contains(":")
? NamespacedKey.fromString(identifier.toLowerCase(Locale.ENGLISH))
@@ -104,12 +125,14 @@ public final class WorldRemovalPathPolicy {
private static Rejection classifyStorageFailure(
Path levelRoot,
NamespacedKey worldKey,
IllegalArgumentException failure
RuntimeException failure
) {
Path dimensions = levelRoot.resolve("dimensions");
Path namespace = dimensions.resolve(worldKey.getNamespace());
Path target = namespace.resolve(worldKey.getKey());
RejectionReason reason = Files.isSymbolicLink(dimensions)
String failureMessage = String.valueOf(failure.getMessage()).toLowerCase(Locale.ENGLISH);
RejectionReason reason = failureMessage.contains("symbolic link")
|| Files.isSymbolicLink(dimensions)
|| Files.isSymbolicLink(namespace)
|| Files.isSymbolicLink(target)
? RejectionReason.SYMBOLIC_LINK
@@ -117,6 +140,45 @@ public final class WorldRemovalPathPolicy {
return new Rejection(reason, failure.getMessage(), failure);
}
private static StorageLayout resolveStorageLayout(Path levelRoot, NamespacedKey worldKey) {
Path worldContainer = levelRoot.getParent();
if (worldContainer == null) {
throw new IllegalArgumentException("Selected level root has no world container: " + levelRoot);
}
String configuredWorldName = IrisWorldStorage.configuredWorldName(
worldKey,
levelRoot.getFileName().toString()
);
Path directDimension = IrisWorldStorage.requireSafeManagedDimensionRoot(
levelRoot.toFile(),
worldKey
).toPath().toAbsolutePath().normalize();
Path dimensionDirectory = IrisWorldStorage.frozenDimensionRoot(
worldContainer.toFile(),
levelRoot.toFile(),
configuredWorldName,
worldKey
).map(file -> file.toPath().toAbsolutePath().normalize()).orElse(directDimension);
if (dimensionDirectory.equals(directDimension)) {
return new StorageLayout(directDimension, directDimension);
}
Path configuredDimension = IrisWorldStorage.configuredDimensionRoot(
worldContainer.toFile(),
levelRoot.toFile(),
worldKey
).toPath().toAbsolutePath().normalize();
if (!dimensionDirectory.equals(configuredDimension)) {
throw new IllegalStateException("Iris world storage does not match the current platform layout.");
}
Path configuredLevel = IrisWorldStorage.configuredLevelRoot(
worldContainer.toFile(),
levelRoot.toFile(),
worldKey
).toPath().toAbsolutePath().normalize();
return new StorageLayout(configuredDimension, configuredLevel);
}
private static String requireIdentifier(String identifier) {
if (identifier == null || identifier.isBlank()) {
throw new Rejection(RejectionReason.INVALID_IDENTIFIER, "The world identifier cannot be empty.");
@@ -129,7 +191,8 @@ public final class WorldRemovalPathPolicy {
NamespacedKey worldKey,
String logicalName,
Path levelRoot,
Path worldDirectory
Path worldDirectory,
Path storageDirectory
) {
public Target {
Objects.requireNonNull(requestedIdentifier, "requestedIdentifier");
@@ -137,9 +200,13 @@ public final class WorldRemovalPathPolicy {
Objects.requireNonNull(logicalName, "logicalName");
Objects.requireNonNull(levelRoot, "levelRoot");
Objects.requireNonNull(worldDirectory, "worldDirectory");
Objects.requireNonNull(storageDirectory, "storageDirectory");
}
}
private record StorageLayout(Path dimensionDirectory, Path storageDirectory) {
}
public enum RejectionReason {
INVALID_IDENTIFIER,
CONFIGURED_MAIN_WORLD,
@@ -165,10 +165,22 @@ public final class BukkitWorldConfiguration {
))) {
continue;
}
if (!IrisWorldStorage.isExistingManagedDimensionRoot(
requiredLevelRoot.toFile(),
namespacedKey
)) {
Path worldContainer = requiredLevelRoot.getParent();
if (worldContainer == null) {
throw new IOException("Selected level root has no world container: " + requiredLevelRoot);
}
boolean storagePresent;
try {
storagePresent = IrisWorldStorage.frozenDimensionRoot(
worldContainer.toFile(),
requiredLevelRoot.toFile(),
configuredName,
namespacedKey
).isPresent();
} catch (IllegalStateException failure) {
storagePresent = false;
}
if (!storagePresent) {
continue;
}
String dimension = selectedIrisDimension(configuredGenerator, configuredName);
@@ -852,7 +852,9 @@ public final class IrisWorldRemovalService {
) {
return onGlobal(() -> {
requireNotTerminal(terminal, "Multiverse unregistration");
return IrisServices.get(MultiverseCoreLink.class).removeFromConfig(target.logicalName());
return IrisServices.get(MultiverseCoreLink.class).removeFromConfig(
bukkitConfigurationWorldName(target)
);
}).thenCompose(multiverseChanged -> {
if (terminal.getAsBoolean()) {
return CompletableFuture.failedFuture(new IllegalStateException(
@@ -892,7 +894,10 @@ public final class IrisWorldRemovalService {
WorldRemovalPathPolicy.Target target,
BooleanSupplier terminal
) {
Path quarantine = target.worldDirectory().resolveSibling(".iris-delete-" + UUID.randomUUID());
Path quarantine = target.levelRoot()
.resolve("dimensions/iris/.iris-delete-" + UUID.randomUUID())
.toAbsolutePath()
.normalize();
return CompletableFuture.supplyAsync(
() -> {
requireNotTerminal(terminal, "deletion intent");
@@ -952,7 +957,12 @@ public final class IrisWorldRemovalService {
target.worldKey(),
target.worldDirectory()
);
boolean directoryPresent = Files.isDirectory(target.worldDirectory(), LinkOption.NOFOLLOW_LINKS);
WorldRemovalPathPolicy.validateStorageRoot(
target.levelRoot(),
target.worldKey(),
target.storageDirectory()
);
boolean directoryPresent = Files.isDirectory(target.storageDirectory(), LinkOption.NOFOLLOW_LINKS);
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
String generator = bukkitGenerator(configuration, target);
boolean configurationManaged = generator != null
@@ -1020,17 +1030,24 @@ public final class IrisWorldRemovalService {
target.worldKey(),
target.worldDirectory()
);
Path worldDirectory = target.worldDirectory();
if (!Files.exists(worldDirectory, LinkOption.NOFOLLOW_LINKS)) {
WorldRemovalPathPolicy.validateStorageRoot(
target.levelRoot(),
target.worldKey(),
target.storageDirectory()
);
Path storageDirectory = target.storageDirectory();
if (!Files.exists(storageDirectory, LinkOption.NOFOLLOW_LINKS)) {
return null;
}
if (!Files.isDirectory(worldDirectory, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isDirectory(storageDirectory, LinkOption.NOFOLLOW_LINKS)) {
throw new RemovalFailure(
RemovalStatus.QUARANTINE_FAILED,
new IOException("Iris world target is not a directory: " + worldDirectory)
new IOException("Iris world target is not a directory: " + storageDirectory)
);
}
requireSafeQuarantineParent(target, quarantine);
WorldDeletionQueue deletionQueue = IrisServices.getOrNull(WorldDeletionQueue.class);
if (deletionQueue == null) {
throw new RemovalFailure(
@@ -1059,21 +1076,26 @@ public final class IrisWorldRemovalService {
target.worldKey(),
target.worldDirectory()
);
Path worldDirectory = target.worldDirectory();
if (!Files.exists(worldDirectory, LinkOption.NOFOLLOW_LINKS)) {
WorldRemovalPathPolicy.validateStorageRoot(
target.levelRoot(),
target.worldKey(),
target.storageDirectory()
);
Path storageDirectory = target.storageDirectory();
if (!Files.exists(storageDirectory, LinkOption.NOFOLLOW_LINKS)) {
return null;
}
if (!Files.isDirectory(worldDirectory, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isDirectory(storageDirectory, LinkOption.NOFOLLOW_LINKS)) {
throw new RemovalFailure(
RemovalStatus.QUARANTINE_FAILED,
new IOException("Iris world target is not a directory: " + worldDirectory)
new IOException("Iris world target is not a directory: " + storageDirectory)
);
}
try {
try {
Files.move(worldDirectory, quarantine, StandardCopyOption.ATOMIC_MOVE);
Files.move(storageDirectory, quarantine, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException unsupported) {
Files.move(worldDirectory, quarantine);
Files.move(storageDirectory, quarantine);
}
return quarantine;
} catch (IOException failure) {
@@ -1081,6 +1103,31 @@ public final class IrisWorldRemovalService {
}
}
private static void requireSafeQuarantineParent(
WorldRemovalPathPolicy.Target target,
Path quarantine
) {
Path expectedParent = target.levelRoot().resolve("dimensions/iris").toAbsolutePath().normalize();
if (!Objects.equals(quarantine.getParent(), expectedParent)) {
throw new RemovalFailure(
RemovalStatus.QUARANTINE_FAILED,
new IOException("Iris quarantine path is outside its exact namespace root: " + quarantine)
);
}
try {
Path current = target.levelRoot().toAbsolutePath().normalize();
for (Path segment : target.levelRoot().relativize(expectedParent)) {
current = current.resolve(segment);
if (Files.isSymbolicLink(current)) {
throw new IOException("Iris quarantine storage contains a symbolic link: " + current);
}
}
Files.createDirectories(expectedParent);
} catch (IOException failure) {
throw new RemovalFailure(RemovalStatus.QUARANTINE_FAILED, failure);
}
}
private DeleteDisposition deleteQuarantine(Path quarantine) {
try {
SnapshotDirectoryTreeDeleter.delete(quarantine);
@@ -40,7 +40,7 @@ public record WorldLifecycleRequest(
}
public WorldCreator toWorldCreator() {
WorldCreator creator = WorldCreatorCompat.ofKey(worldKey)
WorldCreator creator = WorldCreatorCompat.ofKey(worldKey, worldName)
.environment(environment)
.generateStructures(generateStructures)
.hardcore(hardcore)
@@ -44,6 +44,11 @@ public final class WorldLifecycleStaging {
return stagedStemGenerators.remove(worldName);
}
@Nullable
public static ChunkGenerator peekStemGenerator(@NotNull String worldName) {
return stagedStemGenerators.get(worldName);
}
public static void clearGenerator(@NotNull String worldName) {
stagedGenerators.remove(worldName);
stagedBiomeProviders.remove(worldName);
@@ -1,6 +1,7 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
import art.arcane.iris.core.WorldSlotKey;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSnapshot;
import art.arcane.iris.core.lifecycle.WorldReplacementFilesystem.ReplacementPaths;
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Phase;
@@ -8,6 +9,8 @@ import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Transaction;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
@@ -138,6 +141,7 @@ public final class WorldReplacementBootstrap {
throw conflict(active, "bukkit.yml matches neither the replacement nor its retained original state.");
}
try {
refreshOverworldEntryGuard(levelRoot, active, paths);
WorldReplacementFilesystem.publish(
paths,
active.originalTargetPresent(),
@@ -166,6 +170,7 @@ public final class WorldReplacementBootstrap {
}
throw conflict(active, "bukkit.yml matches neither the replacement nor its retained original state.");
}
refreshOverworldEntryGuard(levelRoot, active, paths);
WorldReplacementFilesystem.publish(
paths,
active.originalTargetPresent(),
@@ -176,6 +181,20 @@ public final class WorldReplacementBootstrap {
throw conflict(active, "Replacement journal reached an unsupported bootstrap phase.");
}
private static void refreshOverworldEntryGuard(
Path levelRoot,
Transaction transaction,
ReplacementPaths paths
) throws IOException {
if (!WorldSlotKey.minecraft("overworld").equals(transaction.worldKey())) {
return;
}
Path replacementWorld = Files.isDirectory(paths.stage(), LinkOption.NOFOLLOW_LINKS)
? paths.stage()
: paths.target();
WorldReplacementEntryGuard.refreshPlayers(levelRoot, replacementWorld, transaction.id());
}
private static void rollback(
Path dataDirectory,
File bukkitConfiguration,
@@ -0,0 +1,231 @@
package art.arcane.iris.core.lifecycle;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class WorldReplacementEntryGuard {
public static final String MARKER_NAME = "replacement-entry.properties";
private static final Pattern PLAYER_DATA_NAME = Pattern.compile(
"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\\.dat$");
private WorldReplacementEntryGuard() {
}
public static Entry stage(Path levelRoot, Path stagedWorld, UUID transactionId) throws IOException {
Path requiredLevelRoot = normalize(levelRoot, "levelRoot");
Path requiredStagedWorld = normalize(stagedWorld, "stagedWorld");
Entry entry = new Entry(
Objects.requireNonNull(transactionId, "transactionId"),
discoverPlayers(requiredLevelRoot)
);
write(requiredStagedWorld, entry);
return entry;
}
public static Entry refreshPlayers(Path levelRoot, Path worldDirectory, UUID transactionId) throws IOException {
Path requiredLevelRoot = normalize(levelRoot, "levelRoot");
Path requiredWorldDirectory = normalize(worldDirectory, "worldDirectory");
UUID requiredTransactionId = Objects.requireNonNull(transactionId, "transactionId");
Optional<Entry> loaded = load(requiredWorldDirectory);
if (loaded.isEmpty()) {
throw new IOException("The staged Overworld replacement is missing its entry marker.");
}
Entry current = loaded.get();
if (!current.transactionId().equals(requiredTransactionId)) {
throw new IOException("Replacement entry marker belongs to another transaction.");
}
HashSet<UUID> pendingPlayers = new HashSet<>(current.pendingPlayers());
pendingPlayers.addAll(discoverPlayers(requiredLevelRoot));
Entry refreshed = new Entry(requiredTransactionId, pendingPlayers);
write(requiredWorldDirectory, refreshed);
return refreshed;
}
public static Optional<Entry> load(Path worldDirectory) throws IOException {
Path marker = marker(worldDirectory);
if (!Files.exists(marker, LinkOption.NOFOLLOW_LINKS)) {
return Optional.empty();
}
if (Files.isSymbolicLink(marker) || !Files.isRegularFile(marker, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Replacement entry marker is unsafe: " + marker);
}
Properties properties = new Properties();
try (InputStream input = Files.newInputStream(marker)) {
properties.load(input);
}
UUID transactionId;
try {
transactionId = UUID.fromString(required(properties, "transaction"));
} catch (IllegalArgumentException failure) {
throw new IOException("Replacement entry marker contains an invalid transaction id.", failure);
}
HashSet<UUID> pendingPlayers = new HashSet<>();
String encodedPlayers = properties.getProperty("pendingPlayers", "").trim();
if (!encodedPlayers.isEmpty()) {
for (String encodedPlayer : encodedPlayers.split(",", -1)) {
try {
if (!pendingPlayers.add(UUID.fromString(encodedPlayer))) {
throw new IOException("Replacement entry marker contains a duplicate player id.");
}
} catch (IllegalArgumentException failure) {
throw new IOException("Replacement entry marker contains an invalid player id.", failure);
}
}
}
return Optional.of(new Entry(transactionId, pendingPlayers));
}
public static Optional<Entry> completePlayer(
Path worldDirectory,
UUID transactionId,
UUID playerId
) throws IOException {
Path requiredWorldDirectory = normalize(worldDirectory, "worldDirectory");
UUID requiredTransactionId = Objects.requireNonNull(transactionId, "transactionId");
UUID requiredPlayerId = Objects.requireNonNull(playerId, "playerId");
Optional<Entry> loaded = load(requiredWorldDirectory);
if (loaded.isEmpty()) {
return Optional.empty();
}
Entry current = loaded.get();
if (!current.transactionId().equals(requiredTransactionId)) {
throw new IOException("Replacement entry marker belongs to another transaction.");
}
HashSet<UUID> remaining = new HashSet<>(current.pendingPlayers());
remaining.remove(requiredPlayerId);
Entry updated = new Entry(requiredTransactionId, remaining);
write(requiredWorldDirectory, updated);
return Optional.of(updated);
}
public static boolean retireIfEmpty(Path worldDirectory, UUID transactionId) throws IOException {
Path requiredWorldDirectory = normalize(worldDirectory, "worldDirectory");
Optional<Entry> loaded = load(requiredWorldDirectory);
if (loaded.isEmpty()) {
return true;
}
Entry current = loaded.get();
if (!current.transactionId().equals(Objects.requireNonNull(transactionId, "transactionId"))) {
throw new IOException("Replacement entry marker belongs to another transaction.");
}
if (!current.pendingPlayers().isEmpty()) {
return false;
}
Path marker = marker(requiredWorldDirectory);
Files.delete(marker);
DirectoryDurability.forceDirectoryAfterCommit(marker.getParent(), "A replacement entry marker retirement");
return true;
}
private static Set<UUID> discoverPlayers(Path levelRoot) throws IOException {
Path playerData = levelRoot.resolve("players/data");
if (!Files.exists(playerData, LinkOption.NOFOLLOW_LINKS)) {
return Set.of();
}
if (Files.isSymbolicLink(playerData) || !Files.isDirectory(playerData, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Player data storage is unsafe: " + playerData);
}
HashSet<UUID> players = new HashSet<>();
try (DirectoryStream<Path> files = Files.newDirectoryStream(playerData)) {
for (Path file : files) {
Matcher matcher = PLAYER_DATA_NAME.matcher(file.getFileName().toString());
if (!matcher.matches()) {
continue;
}
if (Files.isSymbolicLink(file) || !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Player data entry is unsafe: " + file);
}
players.add(UUID.fromString(matcher.group(1)));
}
}
return Set.copyOf(players);
}
private static void write(Path worldDirectory, Entry entry) throws IOException {
Path marker = marker(worldDirectory);
Path parent = marker.getParent();
if (Files.isSymbolicLink(parent)) {
throw new IOException("Replacement entry storage is unsafe: " + parent);
}
Files.createDirectories(parent);
if (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Replacement entry storage is not a directory: " + parent);
}
Properties properties = new Properties();
properties.setProperty("transaction", entry.transactionId().toString());
ArrayList<UUID> orderedPlayers = new ArrayList<>(entry.pendingPlayers());
orderedPlayers.sort(Comparator.comparing(UUID::toString));
properties.setProperty(
"pendingPlayers",
String.join(",", orderedPlayers.stream().map(UUID::toString).toList())
);
ByteArrayOutputStream output = new ByteArrayOutputStream();
properties.store(output, null);
Path staged = Files.createTempFile(parent, ".replacement-entry-", ".tmp");
try {
try (FileChannel channel = FileChannel.open(
staged,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE
)) {
ByteBuffer buffer = ByteBuffer.wrap(output.toByteArray());
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}
try {
Files.move(staged, marker, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException failure) {
throw new IOException("Replacement entry marker requires atomic publication.", failure);
}
DirectoryDurability.forceDirectoryAfterCommit(parent, "A replacement entry marker change");
} finally {
Files.deleteIfExists(staged);
}
}
private static Path marker(Path worldDirectory) {
return normalize(worldDirectory, "worldDirectory").resolve("iris").resolve(MARKER_NAME);
}
private static Path normalize(Path path, String name) {
return Objects.requireNonNull(path, name).toAbsolutePath().normalize();
}
private static String required(Properties properties, String key) throws IOException {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) {
throw new IOException("Replacement entry marker is missing " + key + ".");
}
return value.trim();
}
public record Entry(UUID transactionId, Set<UUID> pendingPlayers) {
public Entry {
Objects.requireNonNull(transactionId, "transactionId");
pendingPlayers = Set.copyOf(Objects.requireNonNull(pendingPlayers, "pendingPlayers"));
}
}
}
@@ -418,9 +418,7 @@ public class IrisPregenerator {
IrisLogging.reportError(e);
}
if (MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
MantleHeapPressure.requestPanicReclaim();
}
private void checkRegion(int x, int z) {
@@ -18,18 +18,37 @@
package art.arcane.iris.core.pregenerator;
import art.arcane.iris.spi.IrisLogging;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.DoubleConsumer;
import java.util.function.LongSupplier;
public final class MantleHeapPressure {
private static final double HIGH_WATER = 0.92D;
private static final double LOW_WATER = 0.82D;
private static final double PANIC_WATER = 0.96D;
private static final long PANIC_GC_INTERVAL_MS = 30_000L;
private static final AtomicBoolean engaged = new AtomicBoolean(false);
private static final AtomicLong lastPanicGcAt = new AtomicLong(0L);
private static final long MAXIMUM_HYSTERESIS_MS = 60_000L;
private static final ObjectName DIAGNOSTIC_COMMAND = diagnosticCommandName();
private static final PanicGcReclaimer PANIC_RECLAIMER = new PanicGcReclaimer(
new PanicGcPolicy(10_000L, 60_000L, 60_000L, 15 * 60_000L),
new PanicGcActions(
System::currentTimeMillis,
System::gc,
() -> invokeHotSpotDiagnosticGc(ManagementFactory.getPlatformMBeanServer()),
(double fraction) -> IrisLogging.warn(
"Iris heap remained at %.1f%% after normal panic reclaim; invoking the current JVM's diagnostic full GC to keep generation live.",
fraction * 100.0D),
(String context, Throwable failure) -> IrisLogging.reportError(context, failure)));
private static final HeapPressureGate PRESSURE_GATE = new HeapPressureGate(
HIGH_WATER,
LOW_WATER,
MAXIMUM_HYSTERESIS_MS,
System::currentTimeMillis,
PANIC_RECLAIMER::resetEpisode);
private MantleHeapPressure() {
}
@@ -50,19 +69,7 @@ public final class MantleHeapPressure {
}
public static boolean overHighWater() {
double fraction = usedFraction();
if (engaged.get()) {
if (fraction <= LOW_WATER) {
engaged.set(false);
return false;
}
return true;
}
if (fraction >= HIGH_WATER) {
engaged.set(true);
return true;
}
return false;
return PRESSURE_GATE.update(usedFraction());
}
public static double reclaimUrgency(double fraction) {
@@ -75,18 +82,203 @@ public final class MantleHeapPressure {
return (fraction - LOW_WATER) / (HIGH_WATER - LOW_WATER);
}
public static boolean overPanicWater() {
return usedFraction() >= PANIC_WATER;
public static void requestPanicReclaim() {
PANIC_RECLAIMER.request(usedFraction());
}
public static void requestPanicReclaim() {
long now = System.currentTimeMillis();
long last = lastPanicGcAt.get();
if (now - last < PANIC_GC_INTERVAL_MS) {
return;
static void invokeHotSpotDiagnosticGc(MBeanServer server) throws Exception {
if (!server.isRegistered(DIAGNOSTIC_COMMAND)) {
throw new UnsupportedOperationException("HotSpot DiagnosticCommand MBean is not registered on this JVM");
}
if (lastPanicGcAt.compareAndSet(last, now)) {
System.gc();
server.invoke(
DIAGNOSTIC_COMMAND,
"gcRun",
new Object[0],
new String[0]);
}
private static ObjectName diagnosticCommandName() {
try {
return new ObjectName("com.sun.management:type=DiagnosticCommand");
} catch (Exception failure) {
throw new ExceptionInInitializerError(failure);
}
}
record PanicGcPolicy(
long diagnosticDelayMs,
long diagnosticCooldownMs,
long initialFailureBackoffMs,
long maximumFailureBackoffMs
) {
PanicGcPolicy {
if (diagnosticDelayMs < 0L
|| diagnosticCooldownMs < 0L
|| initialFailureBackoffMs < 0L
|| maximumFailureBackoffMs < initialFailureBackoffMs) {
throw new IllegalArgumentException("Invalid panic GC timing policy");
}
}
}
record PanicGcActions(
LongSupplier clock,
Runnable explicitGc,
DiagnosticGc diagnosticGc,
DoubleConsumer diagnosticStart,
BiConsumer<String, Throwable> failureSink
) {
}
@FunctionalInterface
interface DiagnosticGc {
void run() throws Exception;
}
static final class HeapPressureGate {
private static final long NOT_BELOW_HIGH_WATER = Long.MIN_VALUE;
private final double highWater;
private final double lowWater;
private final long maximumHysteresisMs;
private final LongSupplier clock;
private final Runnable releaseAction;
private boolean engaged;
private long belowHighWaterSince;
HeapPressureGate(double highWater, double lowWater, long maximumHysteresisMs, LongSupplier clock, Runnable releaseAction) {
if (!Double.isFinite(highWater)
|| !Double.isFinite(lowWater)
|| lowWater < 0.0D
|| highWater <= lowWater
|| maximumHysteresisMs < 0L) {
throw new IllegalArgumentException("Invalid heap pressure hysteresis policy");
}
this.highWater = highWater;
this.lowWater = lowWater;
this.maximumHysteresisMs = maximumHysteresisMs;
this.clock = clock;
this.releaseAction = releaseAction;
this.belowHighWaterSince = NOT_BELOW_HIGH_WATER;
}
synchronized boolean update(double fraction) {
if (!Double.isFinite(fraction)) {
return engaged;
}
if (!engaged) {
if (fraction >= highWater) {
engaged = true;
belowHighWaterSince = NOT_BELOW_HIGH_WATER;
}
return engaged;
}
if (fraction <= lowWater) {
release();
return false;
}
if (fraction >= highWater) {
belowHighWaterSince = NOT_BELOW_HIGH_WATER;
return true;
}
long now = clock.getAsLong();
if (belowHighWaterSince == NOT_BELOW_HIGH_WATER) {
belowHighWaterSince = now;
return true;
}
if (elapsed(now, belowHighWaterSince) < maximumHysteresisMs) {
return true;
}
release();
return false;
}
private void release() {
engaged = false;
belowHighWaterSince = NOT_BELOW_HIGH_WATER;
releaseAction.run();
}
private static long elapsed(long now, long then) {
return now >= then ? now - then : Long.MAX_VALUE;
}
}
static final class PanicGcReclaimer {
private final PanicGcPolicy policy;
private final PanicGcActions actions;
private boolean panicEpisode;
private long explicitAttemptAt;
private long nextDiagnosticAllowedAt;
private long failureBackoffMs;
PanicGcReclaimer(PanicGcPolicy policy, PanicGcActions actions) {
this.policy = policy;
this.actions = actions;
this.failureBackoffMs = policy.initialFailureBackoffMs();
}
synchronized void request(double fraction) {
if (!Double.isFinite(fraction) || fraction <= LOW_WATER) {
return;
}
long now = actions.clock().getAsLong();
if (!panicEpisode) {
if (fraction < HIGH_WATER) {
return;
}
beginEpisode(now);
return;
}
if (elapsed(now, explicitAttemptAt) < policy.diagnosticDelayMs()
|| now < nextDiagnosticAllowedAt) {
return;
}
actions.diagnosticStart().accept(fraction);
try {
actions.diagnosticGc().run();
failureBackoffMs = policy.initialFailureBackoffMs();
nextDiagnosticAllowedAt = deadline(now, policy.diagnosticCooldownMs());
} catch (Exception failure) {
actions.failureSink().accept(
"Iris could not invoke the current JVM's DiagnosticCommand GC after normal panic reclaim was ineffective; generation remains pressure-limited to avoid an OOM.",
failure);
nextDiagnosticAllowedAt = deadline(now, failureBackoffMs);
failureBackoffMs = Math.min(policy.maximumFailureBackoffMs(), doubled(failureBackoffMs));
}
}
synchronized void resetEpisode() {
panicEpisode = false;
explicitAttemptAt = 0L;
}
private void beginEpisode(long now) {
panicEpisode = true;
explicitAttemptAt = now;
try {
actions.explicitGc().run();
} catch (RuntimeException failure) {
actions.failureSink().accept(
"Iris normal panic heap reclaim failed; the diagnostic fallback will be attempted if pressure remains critical.",
failure);
}
}
private static long elapsed(long now, long then) {
return now >= then ? now - then : Long.MAX_VALUE;
}
private static long deadline(long now, long delay) {
return delay > Long.MAX_VALUE - now ? Long.MAX_VALUE : now + delay;
}
private static long doubled(long value) {
return value > Long.MAX_VALUE / 2L ? Long.MAX_VALUE : value * 2L;
}
}
}
@@ -133,9 +133,7 @@ public final class PregenMantleBackpressure {
IrisLogging.reportError(e);
}
if (MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
MantleHeapPressure.requestPanicReclaim();
long elapsed = M.ms() - waitStart;
if (elapsed >= timeoutMs) {
@@ -35,6 +35,7 @@ import java.util.Map;
@Builder
@Data
public class PregenTask {
static final int MAX_WORLD_BLOCK = 29_999_984;
/**
* Saturation limits for block bounds. The full int range is safe downstream: the widest derived value is
* regionToChunk(blockToRegionFloor(MAX_BLOCK)) + 31 shifted back to blocks, which lands inside int.
@@ -71,6 +72,7 @@ public class PregenTask {
if (radiusX <= 0 || radiusZ <= 0) {
throw new IllegalArgumentException("Pregen radii must be greater than zero blocks.");
}
requireWithinWorld(center, radiusX, radiusZ);
this.gui = gui;
this.center = new ProxiedPos(center);
@@ -79,6 +81,19 @@ public class PregenTask {
bounds.update();
}
private static void requireWithinWorld(Position2 center, int radiusX, int radiusZ) {
long minX = (long) center.getX() - radiusX;
long maxX = (long) center.getX() + radiusX;
long minZ = (long) center.getZ() - radiusZ;
long maxZ = (long) center.getZ() + radiusZ;
if (minX < -MAX_WORLD_BLOCK || maxX > MAX_WORLD_BLOCK
|| minZ < -MAX_WORLD_BLOCK || maxZ > MAX_WORLD_BLOCK) {
throw new IllegalArgumentException("Pregen area exceeds Minecraft's coordinate limit of +/-"
+ MAX_WORLD_BLOCK + " blocks: center " + center.getX() + "," + center.getZ()
+ " radius " + radiusX + "x" + radiusZ + ".");
}
}
public static void iterateRegion(int xr, int zr, Spiraled s, Position2 pull) {
iterateRegion(xr, zr, s, pull.getX(), pull.getZ());
}
@@ -17,10 +17,16 @@ import org.bukkit.Chunk;
import org.bukkit.GameRule;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.entity.Player;
import org.bukkit.event.world.TimeSkipEvent;
import org.bukkit.plugin.PluginManager;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.VoxelShape;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@@ -31,6 +37,26 @@ import java.util.Set;
import java.util.concurrent.CompletableFuture;
public final class WorldRuntimeControlService {
private static final int MAX_SAFE_ENTRY_HORIZONTAL_RADIUS = 15;
private static final int MAX_SAFE_ENTRY_VERTICAL_SEARCH = 64;
private static final double BLOCK_CENTER = 0.5D;
private static final double COLLISION_EPSILON = 0.000001D;
private static final Set<Material> UNSAFE_ENTRY_MATERIALS = Set.of(
Material.CACTUS,
Material.CAMPFIRE,
Material.COBWEB,
Material.END_GATEWAY,
Material.END_PORTAL,
Material.FIRE,
Material.MAGMA_BLOCK,
Material.NETHER_PORTAL,
Material.POINTED_DRIPSTONE,
Material.POWDER_SNOW,
Material.SOUL_CAMPFIRE,
Material.SOUL_FIRE,
Material.SWEET_BERRY_BUSH,
Material.WITHER_ROSE
);
private static volatile WorldRuntimeControlService instance;
private final CapabilitySnapshot capabilities;
@@ -352,19 +378,138 @@ public final class WorldRuntimeControlService {
}
static Location findTopSafeLocation(World world, Location source) {
int x = source.getBlockX();
int z = source.getBlockZ();
int sourceX = source.getBlockX();
int sourceZ = source.getBlockZ();
float yaw = source.getYaw();
float pitch = source.getPitch();
int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2;
if (world.isChunkLoaded(x >> 4, z >> 4)) {
int raw = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
int y = Math.max(minY, Math.min(maxY, raw + 1));
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
int chunkX = sourceX >> 4;
int chunkZ = sourceZ >> 4;
if (!world.isChunkLoaded(chunkX, chunkZ)) {
return null;
}
int y = Math.max(minY, Math.min(maxY, source.getBlockY()));
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
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 safeLocation = findSafeLocationInColumn(
world,
x,
z,
minimumFloorY,
maximumFloorY,
yaw,
pitch
);
if (safeLocation != null) {
return safeLocation;
}
}
}
}
return null;
}
private static Location findSafeLocationInColumn(
World world,
int x,
int z,
int minimumFloorY,
int maximumFloorY,
float yaw,
float pitch
) {
int highestY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
int startingFloorY = Math.max(minimumFloorY, Math.min(maximumFloorY, highestY));
int lowestFloorY = Math.max(minimumFloorY, startingFloorY - MAX_SAFE_ENTRY_VERTICAL_SEARCH + 1);
for (int floorY = startingFloorY; floorY >= lowestFloorY; floorY--) {
Block floor = world.getBlockAt(x, floorY, z);
if (!isSafeFloor(floor)) {
continue;
}
Block feet = world.getBlockAt(x, floorY + 1, z);
Block head = world.getBlockAt(x, floorY + 2, z);
if (isClearEntryBlock(feet) && isClearEntryBlock(head)) {
return new Location(world, x + BLOCK_CENTER, floorY + 1D, z + BLOCK_CENTER, yaw, pitch);
}
}
return null;
}
private static boolean isSafeFloor(Block block) {
Material material = block.getType();
if (material == null
|| isAir(material)
|| material.name().endsWith("_LEAVES")
|| UNSAFE_ENTRY_MATERIALS.contains(material)
|| block.isLiquid()
|| block.isPassable()
|| isWaterlogged(block)) {
return false;
}
VoxelShape collisionShape = block.getCollisionShape();
if (collisionShape == null) {
return false;
}
for (BoundingBox boundingBox : collisionShape.getBoundingBoxes()) {
if (boundingBox.getMinX() <= BLOCK_CENTER
&& boundingBox.getMaxX() >= BLOCK_CENTER
&& boundingBox.getMinZ() <= BLOCK_CENTER
&& boundingBox.getMaxZ() >= BLOCK_CENTER
&& boundingBox.getMaxY() > COLLISION_EPSILON
&& boundingBox.getMaxY() <= 1D + COLLISION_EPSILON) {
return true;
}
}
return false;
}
private static boolean isClearEntryBlock(Block block) {
Material material = block.getType();
if (material == null
|| block.isLiquid()
|| isWaterlogged(block)
|| UNSAFE_ENTRY_MATERIALS.contains(material)
|| !block.isPassable()) {
return false;
}
VoxelShape collisionShape = block.getCollisionShape();
return collisionShape != null && collisionShape.getBoundingBoxes().isEmpty();
}
private static boolean isAir(Material material) {
return material == Material.AIR || material == Material.CAVE_AIR || material == Material.VOID_AIR;
}
private static boolean isWaterlogged(Block block) {
BlockData blockData = block.getBlockData();
return blockData instanceof Waterlogged waterlogged && waterlogged.isWaterlogged();
}
@SuppressWarnings("unchecked")
@@ -67,7 +67,7 @@ public final class EngineMaintenance {
long unloadStart = System.nanoTime();
int unloadedTectonicPlates = engine.getMantle().unloadTectonicPlate(
plan.multicoreUnload() ? 0 : Integer.MAX_VALUE);
if (plan.heapPressure() && MantleHeapPressure.overPanicWater()) {
if (plan.heapPressure()) {
MantleHeapPressure.requestPanicReclaim();
}
@@ -11,6 +11,7 @@ import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.volmlib.util.scheduling.Looper;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@@ -145,7 +146,21 @@ public class GlobalCacheSVC implements IrisService {
private static PregenCache createDefault0(String worldIdentity) {
if (disabled) return PregenCache.EMPTY;
File dimensionRoot = IrisWorldStorage.dimensionRoot(WorldIdentity.parse(worldIdentity));
NamespacedKey worldKey = WorldIdentity.parse(worldIdentity);
File dimensionRoot = requireCacheDimensionRoot(
Bukkit.getWorldContainer(),
IrisWorldStorage.levelRoot(),
worldKey
);
return PregenCache.create(new File(dimensionRoot, "iris/pregen")).sync();
}
static File requireCacheDimensionRoot(File worldContainer, File levelRoot, NamespacedKey worldKey) {
return IrisWorldStorage.requireFrozenDimensionRoot(
worldContainer,
levelRoot,
IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName()),
worldKey
);
}
}
@@ -32,6 +32,7 @@ import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.WorldCreatorCompat;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
@@ -165,7 +166,7 @@ public class IrisCreator {
NamespacedKey worldKey;
try {
worldKey = IrisWorldStorage.managedKeyFromName(name);
} catch (IllegalArgumentException e) {
} catch (RuntimeException e) {
throw new IrisException(e.getMessage(), e);
}
name = IrisWorldStorage.logicalName(worldKey);
@@ -197,12 +198,19 @@ public class IrisCreator {
private World createReserved(NamespacedKey worldKey, IrisDimension resolvedDimension) throws IrisException {
File dimensionRoot;
File storageRoot;
try {
dimensionRoot = IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
} catch (IllegalArgumentException e) {
if (!studio && !benchmark) {
dimensionRoot = WorldCreatorCompat.persistentDimensionRoot(worldKey);
storageRoot = WorldCreatorCompat.persistentLevelRoot(worldKey);
} else {
dimensionRoot = IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
storageRoot = dimensionRoot;
}
} catch (RuntimeException e) {
throw new IrisException(e.getMessage(), e);
}
if (Files.exists(dimensionRoot.toPath()) || WorldIdentity.resolve(worldKey).isPresent()) {
if (Files.exists(storageRoot.toPath()) || WorldIdentity.resolve(worldKey).isPresent()) {
throw new IrisException("World \"" + name + "\" already exists or is loaded.");
}
if (sender == null) {
@@ -249,6 +257,7 @@ public class IrisCreator {
.name(name)
.seed(seed)
.studio(studio)
.persistent(!studio && !benchmark)
.create();
reportStudioTiming("prepare_studio_generator", generatorPrepareStart);
reportStudioProgress(0.40D, "install_datapacks");
@@ -343,7 +352,7 @@ public class IrisCreator {
}
return world;
} catch (Throwable failure) {
rollbackWorldCreation(worldKey, world, stagedGenerator, dimensionRoot, bukkitRegistered, failure);
rollbackWorldCreation(worldKey, world, stagedGenerator, storageRoot, bukkitRegistered, failure);
if (failure instanceof IrisException irisException) {
throw irisException;
}
@@ -692,7 +701,7 @@ public class IrisCreator {
NamespacedKey worldKey,
World createdWorld,
PlatformChunkGenerator stagedGenerator,
File dimensionRoot,
File storageRoot,
boolean bukkitRegistered,
Throwable failure
) {
@@ -749,7 +758,12 @@ public class IrisCreator {
if (bukkitRegistered) {
try {
CompletableFuture<Boolean> multiverseRemoval = J.sfut(
() -> IrisServices.get(MultiverseCoreLink.class).removeFromConfig(name)
() -> IrisServices.get(MultiverseCoreLink.class).removeFromConfig(
IrisWorldStorage.configuredWorldName(
worldKey,
IrisWorldStorage.levelRoot().getName()
)
)
);
if (multiverseRemoval == null) {
throw new IllegalStateException("Failed to schedule Multiverse rollback for \"" + name + "\".");
@@ -782,7 +796,7 @@ public class IrisCreator {
return;
}
try {
AtomicDirectoryPublisher.deleteTree(dimensionRoot.toPath());
AtomicDirectoryPublisher.deleteTree(storageRoot.toPath());
} catch (Throwable rollbackFailure) {
failure.addSuppressed(rollbackFailure);
queueRollbackDeletion(name, failure);
@@ -38,6 +38,7 @@ public class IrisWorldCreator {
private String dimensionName = null;
private IrisDimension dimension;
private long seed = 1337;
private boolean persistent;
public IrisWorldCreator() {
@@ -75,26 +76,35 @@ public class IrisWorldCreator {
return this;
}
public IrisWorldCreator persistent(boolean persistent) {
this.persistent = persistent;
return this;
}
public WorldCreator create() {
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
NamespacedKey worldKey = IrisWorldStorage.keyFromName(name);
World.Environment environment = findEnvironment();
WorldCreator creator = persistent
? WorldCreatorCompat.ofPersistentKey(worldKey)
: WorldCreatorCompat.ofKey(worldKey);
File worldFolder = persistent
? WorldCreatorCompat.persistentDimensionRoot(worldKey)
: IrisWorldStorage.dimensionRoot(worldKey);
IrisWorld w = IrisWorld.builder()
.platformIdentity(worldKey.toString())
.name(name)
.name(creator.name())
.minHeight(dim.getMinHeight())
.maxHeight(dim.getMaxHeight())
.seed(seed)
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
.worldFolder(worldFolder)
.build();
ChunkGenerator g = new BukkitChunkGenerator(w, studio, studio
? dim.getLoader().getDataFolder() :
new File(w.worldFolder(), "iris/pack"), dimensionName);
return WorldCreatorCompat.ofKey(worldKey)
.environment(environment)
return creator.environment(environment)
.generateStructures(true)
.generator(g).seed(seed);
}
@@ -118,23 +118,33 @@ final class EngineRuntimeBuilder {
engine.runtime = null;
}
try {
next.worldManager().start();
} catch (Throwable e) {
Throwable cleanupFailure = engine.shutdownSequence.closeRuntime(next, e);
if (cleanupFailure != e) {
e.addSuppressed(cleanupFailure);
}
engine.lifecycleState = LifecycleState.FAILED;
throw new IllegalStateException("Failed to start the Iris world manager.", e);
}
engine.runtime = next;
engine.publishedTarget = next.target();
engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission();
try {
next.worldManager().start();
} catch (Throwable e) {
engine.getClosing().set(true);
engine.backgroundTasks.closeBackgroundTaskAdmission();
engine.lifecycleState = LifecycleState.FAILED;
try {
engine.getGenerationSessions().sealAndAwait(
"failed world manager start",
IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS,
true
);
} catch (Throwable drainFailure) {
e.addSuppressed(drainFailure);
}
engine.shutdownSequence.closeRuntime(next, e);
if (engine.runtime == next) {
engine.runtime = null;
}
throw new IllegalStateException("Failed to start the Iris world manager.", e);
}
scheduleRuntimeTasks(next);
IrisLogging.debug("Engine Setup Complete " + next.cacheId());
}
@@ -1,10 +1,12 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.project.noise.CNG;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
@@ -14,18 +16,20 @@ import java.util.Map;
public final class IrisDimensionCarvingResolver {
private static final int MAX_CHILD_DEPTH = 32;
private static final long CHILD_SEED_SALT = 0x9E3779B97F4A7C15L;
private static final ThreadLocal<State> THREAD_STATE = ThreadLocal.withInitial(State::new);
private static final ThreadLocal<WeakReference<State>> THREAD_STATE =
ThreadLocal.withInitial(() -> new WeakReference<>(null));
private IrisDimensionCarvingResolver() {
}
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY) {
return resolveRootEntry(engine, worldY, THREAD_STATE.get());
return resolveRootEntry(engine, worldY, threadState());
}
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) {
State resolvedState = state == null ? THREAD_STATE.get() : state;
State resolvedState = state == null ? threadState() : state;
resolvedState.bind(engine);
if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) {
return resolvedState.rootEntriesByWorldY.get(worldY);
}
@@ -51,11 +55,12 @@ public final class IrisDimensionCarvingResolver {
}
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) {
return resolveFromRoot(engine, rootEntry, worldX, worldZ, THREAD_STATE.get());
return resolveFromRoot(engine, rootEntry, worldX, worldZ, threadState());
}
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) {
State resolvedState = state == null ? THREAD_STATE.get() : state;
State resolvedState = state == null ? threadState() : state;
resolvedState.bind(engine);
if (rootEntry == null) {
return null;
}
@@ -103,6 +108,7 @@ public final class IrisDimensionCarvingResolver {
return entry.getRealBiome(engine.getData());
}
state.bind(engine);
if (state.biomeCache.containsKey(entry)) {
return state.biomeCache.get(entry);
}
@@ -266,12 +272,48 @@ public final class IrisDimensionCarvingResolver {
return state.childSeed;
}
private static State threadState() {
WeakReference<State> reference = THREAD_STATE.get();
State state = reference.get();
if (state != null) {
return state;
}
State replacement = new State();
THREAD_STATE.set(new WeakReference<>(replacement));
return replacement;
}
public static final class State {
private final Map<Integer, IrisDimensionCarvingEntry> rootEntriesByWorldY = new HashMap<>();
private final Map<IrisDimensionCarvingEntry, ParentSelectionPlan> selectionPlans = new IdentityHashMap<>();
private final Map<IrisDimensionCarvingEntry, IrisBiome> biomeCache = new IdentityHashMap<>();
private WeakReference<Engine> engineIdentity;
private WeakReference<IrisDimension> dimensionIdentity;
private WeakReference<IrisData> dataIdentity;
private Map<String, IrisDimensionCarvingEntry> entryIndex;
private Long childSeed;
private void bind(Engine engine) {
IrisDimension dimension = engine.getDimension();
IrisData data = engine.getData();
if (references(engineIdentity, engine)
&& references(dimensionIdentity, dimension)
&& references(dataIdentity, data)) {
return;
}
engineIdentity = new WeakReference<>(engine);
dimensionIdentity = new WeakReference<>(dimension);
dataIdentity = new WeakReference<>(data);
rootEntriesByWorldY.clear();
selectionPlans.clear();
biomeCache.clear();
entryIndex = null;
childSeed = null;
}
private static boolean references(WeakReference<?> identity, Object value) {
return identity != null && identity.get() == value;
}
}
private static final class ParentSelectionPlan {
@@ -250,7 +250,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2;
int y = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn)));
int y = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn) + 1));
world.setSpawnLocation(new Location(world, initialSpawn.getX(), y, initialSpawn.getZ(), initialSpawn.getYaw(), initialSpawn.getPitch()));
}