From cd05fd1bca1a8eef38315c575a262d09c82dcf9d Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 12 Jul 2026 17:06:30 -0400 Subject: [PATCH] World Changes --- adapters/bukkit/plugin/build.gradle | 4 +- .../src/main/java/art/arcane/iris/Iris.java | 56 +++++--- .../iris/core/commands/CommandFind.java | 36 +++-- .../iris/core/commands/CommandIris.java | 52 ++++--- .../iris/core/commands/CommandStructure.java | 8 +- .../iris/core/service/IrisEngineSVC.java | 4 +- .../core/service/IrisProtocolService.java | 3 +- .../art/arcane/iris/core/service/WandSVC.java | 10 +- .../arcane/iris/modded/ModdedParityProbe.java | 1 + .../iris/modded/ModdedWorldEngines.java | 1 + .../iris/modded/ModdedWorldManager.java | 2 +- .../iris/modded/command/ModdedRegen.java | 6 +- .../service/ModdedChunkUpdateService.java | 2 +- .../ModdedEngineMaintenanceService.java | 4 +- .../service/ModdedStudioHotloadService.java | 2 +- build.gradle | 2 +- core/build.gradle | 6 +- .../art/arcane/iris/core/IrisSettings.java | 1 - .../arcane/iris/core/IrisWorldStorage.java | 129 ++++++++++++++++++ .../java/art/arcane/iris/core/IrisWorlds.java | 82 +++++++---- .../arcane/iris/core/ServerConfigurator.java | 80 +++-------- .../arcane/iris/core/gui/PregeneratorJob.java | 17 ++- .../core/lifecycle/BukkitPublicBackend.java | 4 +- .../lifecycle/PaperLikeRuntimeBackend.java | 12 +- .../core/lifecycle/WorldLifecycleRequest.java | 5 +- .../core/lifecycle/WorldLifecycleService.java | 25 ++-- .../core/lifecycle/WorldLifecycleSupport.java | 48 ++++--- .../core/lifecycle/WorldsProviderBackend.java | 5 +- .../core/runtime/StudioOpenCoordinator.java | 14 +- .../runtime/TransientWorldCleanupSupport.java | 33 +++-- .../iris/core/safeguard/task/Tasks.java | 3 +- .../iris/core/service/GlobalCacheSVC.java | 24 ++-- .../arcane/iris/core/service/StudioSVC.java | 6 +- .../iris/core/structure/FeatureImporter.java | 6 +- .../arcane/iris/core/tools/IrisCreator.java | 6 +- .../iris/core/tools/IrisPackBenchmarking.java | 22 ++- .../arcane/iris/core/tools/IrisToolbelt.java | 32 +++-- .../iris/core/tools/IrisWorldCreator.java | 9 +- .../art/arcane/iris/engine/IrisEngine.java | 2 +- .../arcane/iris/engine/IrisWorldManager.java | 2 +- .../arcane/iris/engine/framework/Engine.java | 6 +- .../iris/engine/framework/EngineMode.java | 4 +- .../iris/engine/mantle/MantleWriter.java | 2 +- .../arcane/iris/engine/object/IrisWorld.java | 23 +++- .../engine/platform/BukkitChunkGenerator.java | 9 +- .../arcane/iris/util/common/misc/SlimJar.java | 10 +- .../iris/util/common/nbt/mca/NBTWorld.java | 4 +- .../iris/util/common/plugin/CommandDummy.java | 7 + .../iris/util/common/plugin/VolmitSender.java | 7 + .../iris/core/IrisWorldStorageTest.java | 51 +++++++ .../WorldLifecycleDiagnosticsTest.java | 3 +- .../WorldLifecycleRuntimeLevelStemTest.java | 2 + .../WorldLifecycleSelectionTest.java | 23 ++-- .../TransientWorldCleanupSupportTest.java | 11 +- .../tools/IrisToolbeltPackReferenceTest.java | 23 ++++ .../engine/object/IrisWorldIdentityTest.java | 27 ++++ gradle/libs.versions.toml | 2 + .../arcane/iris/probe/GenerationProbe.java | 1 + 58 files changed, 668 insertions(+), 313 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java create mode 100644 core/src/test/java/art/arcane/iris/core/IrisWorldStorageTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/object/IrisWorldIdentityTest.java diff --git a/adapters/bukkit/plugin/build.gradle b/adapters/bukkit/plugin/build.gradle index 3e4cd7cf4..a66a60578 100644 --- a/adapters/bukkit/plugin/build.gradle +++ b/adapters/bukkit/plugin/build.gradle @@ -10,9 +10,9 @@ dependencies { compileOnly(volmLibCoordinate) { transitive = false } - compileOnly(libs.spigot) + compileOnly(libs.paper.api) testImplementation('junit:junit:4.13.2') - testImplementation(libs.spigot) + testImplementation(libs.paper.api) testImplementation(libs.bstats) testImplementation(libs.sentry) testImplementation(project(':core')) diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java index 362d219f8..f831d7203 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java @@ -23,6 +23,7 @@ import art.arcane.iris.engine.IrisWorldManager; import art.arcane.iris.engine.framework.EngineWorldManagerProvider; import art.arcane.iris.core.splash.IrisSplashComposer; import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.datapack.DatapackIngestService; @@ -61,6 +62,7 @@ import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.LogLevel; import art.arcane.volmlib.integration.ReloadAware; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.exceptions.IrisException; @@ -88,6 +90,7 @@ import lombok.NonNull; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.WorldCreator; import org.bukkit.block.data.BlockData; @@ -590,6 +593,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { } private void enable() { + alreadyDrained.set(false); PaperLibBootstrap.install(); SimdSupport.install(); services = new KMap<>(); @@ -646,7 +650,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { J.ar(this::checkConfigHotload, 60); J.sr(this::tickQueue, 0); J.s(this::setupPapi); - J.a(ServerConfigurator::configureIfDeferred, 20); J.a(DatapackIngestService::autoIngestOnStartup, 60); autoStartStudio(); @@ -668,17 +671,22 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { } } shutdownHook = new Thread(() -> { - Bukkit.getWorlds() - .stream() - .map(IrisToolbelt::access) - .filter(Objects::nonNull) - .forEach(PlatformChunkGenerator::close); + if (alreadyDrained.compareAndSet(false, true)) { + try { + Bukkit.getWorlds() + .stream() + .map(World::getGenerator) + .filter(PlatformChunkGenerator.class::isInstance) + .map(PlatformChunkGenerator.class::cast) + .forEach(PlatformChunkGenerator::close); + } catch (Throwable e) { + Iris.reportError("Failed to close Iris world generators from the JVM shutdown hook.", e); + } + } MultiBurst.burst.close(); MultiBurst.ioBurst.close(); - if (services != null) { - services.clear(); - } + IrisServices.clear(); }, "Iris-ShutdownHook"); try { Runtime.getRuntime().addShutdownHook(shutdownHook); @@ -692,15 +700,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { KList deferredStartupWorlds = new KList<>(); IrisWorlds.readBukkitWorlds().forEach((s, generator) -> { try { - if (Bukkit.getWorld(s) != null || !filter.test(s)) return; + NamespacedKey worldKey = IrisWorldStorage.keyFromLegacyName(s); + if (WorldIdentity.resolve(worldKey).isPresent() || !filter.test(s)) return; Iris.info("Loading World: %s | Generator: %s", s, generator); - var gen = getDefaultWorldGenerator(s, generator); - var dim = loadDimension(s, generator); + ChunkGenerator gen = getDefaultWorldGenerator(s, generator); + IrisDimension dim = loadDimension(s, generator); assert dim != null && gen != null; Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "..."); - WorldCreator c = new WorldCreator(s) + WorldCreator c = WorldCreator.ofKey(worldKey) .generator(gen) .environment(dim.getEnvironment()); Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s); @@ -786,7 +795,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { } LinkedHashMap queue = loadPendingWorldDeleteMap(); - for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(Bukkit.getWorldContainer())) { + for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())) { queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld); } if (queue.isEmpty()) { @@ -800,7 +809,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { continue; } - org.bukkit.World loaded = Bukkit.getWorld(worldName); + NamespacedKey worldKey = IrisWorldStorage.keyFromLegacyName(worldName); + World loaded = WorldIdentity.resolve(worldKey).orElse(null); if (loaded != null) { if (TransientWorldCleanupSupport.isTransientStudioWorldName(worldName)) { try { @@ -815,7 +825,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { Iris.reportError("Failed to unload leftover studio world \"" + worldName + "\".", e); } - if (Bukkit.getWorld(worldName) != null) { + if (WorldIdentity.resolve(worldKey).isPresent()) { Iris.warn("Studio world \"" + worldName + "\" is still loaded after unload; will retry next startup."); remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); continue; @@ -830,7 +840,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { boolean foundAny = false; boolean deletedAll = true; for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) { - File worldFolder = new File(Bukkit.getWorldContainer(), familyWorldName); + File worldFolder = IrisWorldStorage.dimensionRoot(familyWorldName); if (!worldFolder.exists()) { continue; } @@ -975,7 +985,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { public void onDisable() { if (IrisSafeguard.isForceShutdown()) return; - if (!alreadyDrained.get()) { + if (alreadyDrained.compareAndSet(false, true)) { drainWorldGenerators("onDisable", 30L); } if (services != null) { @@ -1202,12 +1212,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { } Iris.debug("Assuming IrisDimension: " + dim.getName()); + NamespacedKey worldKey = IrisWorldStorage.keyFromLegacyName(worldName); IrisWorld w = IrisWorld.builder() + .key(worldKey) .name(worldName) .seed(1337) .environment(dim.getEnvironment()) - .worldFolder(new File(Bukkit.getWorldContainer(), worldName)) + .worldFolder(IrisWorldStorage.dimensionRoot(worldKey)) .minHeight(dim.getMinHeight()) .maxHeight(dim.getMaxHeight()) .build(); @@ -1215,7 +1227,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { Iris.debug("Generator Config: " + w.toString()); File ff = new File(w.worldFolder(), "iris/pack"); - var files = ff.listFiles(); + File[] files = ff.listFiles(); if (files == null || files.length == 0) IO.delete(ff); @@ -1263,8 +1275,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { @Nullable public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) { - File pack = new File(Bukkit.getWorldContainer(), String.join(File.separator, worldName, "iris", "pack")); - var dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null; + File pack = IrisWorldStorage.packRoot(IrisWorldStorage.keyFromLegacyName(worldName)); + IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null; if (dimension == null) dimension = IrisData.loadAnyDimension(id, null); if (dimension == null) { Iris.warn("Unable to find dimension type " + id + " Looking for online packs..."); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java index 1d4b68d53..10f6afb0a 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java @@ -31,15 +31,19 @@ import art.arcane.iris.util.common.director.DirectorExecutor; import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler; import art.arcane.iris.util.common.director.specialhandlers.StructureHandler; 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.collection.KList; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; import org.bukkit.Location; import org.bukkit.NamespacedKey; import org.bukkit.Registry; import org.bukkit.entity.Player; +import org.bukkit.generator.structure.Structure; import org.bukkit.util.StructureSearchResult; @Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", aliases = "goto") @@ -99,10 +103,16 @@ public class CommandFind implements DirectorExecutor { @Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class) String structure ) { + VolmitSender commandSender = sender(); + if (commandSender == null) { + Iris.reportError("Structure lookup started without a command sender context.", new IllegalStateException("Missing command sender context")); + return; + } + Engine e = engine(); if (e == null) { - sender().sendMessage(C.GOLD + "Not in an Iris World!"); + commandSender.sendMessage(C.GOLD + "Not in an Iris World!"); return; } @@ -113,43 +123,45 @@ public class CommandFind implements DirectorExecutor { Player target = player(); if (target == null) { - sender().sendMessage(C.GOLD + "Run this in-game to teleport to a structure."); + commandSender.sendMessage(C.GOLD + "Run this in-game to teleport to a structure."); return; } - sender().sendMessage(C.GRAY + "Locating " + structure + "..."); + commandSender.sendMessage(C.GRAY + "Locating " + structure + "..."); J.s(() -> { try { - org.bukkit.generator.structure.Structure match = null; - for (org.bukkit.generator.structure.Structure s : Registry.STRUCTURE) { - NamespacedKey key = s.getKey(); + Registry structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE); + Structure match = null; + for (Structure candidate : structureRegistry) { + NamespacedKey key = structureRegistry.getKey(candidate); if (key != null && key.toString().equalsIgnoreCase(structure)) { - match = s; + match = candidate; break; } } if (match == null) { - sender().sendMessage(C.RED + "Unknown structure: " + structure); + commandSender.sendMessage(C.RED + "Unknown structure: " + structure); return; } if (!StructureReachability.isReachable(e, structure)) { KList miss = StructureReachability.missingBiomeKeys(e, structure); - sender().sendMessage(C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack" + commandSender.sendMessage(C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack" + (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ")."); return; } StructureSearchResult result = target.getWorld().locateNearestStructure(target.getLocation(), match, 100, true); if (result == null || result.getLocation() == null) { - sender().sendMessage(C.YELLOW + "No " + structure + " found within range of you."); + commandSender.sendMessage(C.YELLOW + "No " + structure + " found within range of you."); return; } Location at = result.getLocation(); int y = target.getWorld().getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2; Location dest = new Location(target.getWorld(), at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5); BukkitPlatform.teleportAsync(target, dest); - sender().sendMessage(C.GREEN + "Teleported to " + structure + " @ " + at.getBlockX() + ", " + at.getBlockZ()); + commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ " + at.getBlockX() + ", " + at.getBlockZ()); } catch (Throwable t) { - sender().sendMessage(C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName()); + commandSender.sendMessage(C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName()); + Iris.reportError("Could not locate structure '" + structure + "'.", t); } }); } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java index be55651ac..76caf4eda 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java @@ -21,6 +21,7 @@ package art.arcane.iris.core.commands; import art.arcane.iris.Iris; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.lifecycle.WorldLifecycleService; @@ -30,6 +31,7 @@ import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.director.DirectorParameterHandler; import art.arcane.iris.util.common.director.DirectorExecutor; import art.arcane.volmlib.util.director.DirectorOrigin; @@ -44,6 +46,7 @@ import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; @@ -55,6 +58,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.LinkedHashSet; import java.util.List; +import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @@ -108,7 +112,7 @@ public class CommandIris implements DirectorExecutor { return; } - if (new File(Bukkit.getWorldContainer(), name).exists()) { + if (IrisWorldStorage.dimensionRoot(name).exists()) { sender().sendMessage(C.RED + "That folder already exists!"); return; } @@ -161,29 +165,44 @@ public class CommandIris implements DirectorExecutor { private boolean updateMainWorld(String newName) { try { - File worlds = Bukkit.getWorldContainer(); - var data = ServerProperties.DATA; - try (var in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) { + File oldLevelRoot = IrisWorldStorage.levelRoot(); + File worldContainer = oldLevelRoot.getParentFile(); + Properties data = ServerProperties.DATA; + try (FileInputStream in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) { data.load(in); } - File oldWorldFolder = new File(worlds, ServerProperties.LEVEL_NAME); - File newWorldFolder = new File(worlds, newName); - if (!newWorldFolder.exists() && !newWorldFolder.mkdirs()) { - Iris.warn("Could not create target main world folder: " + newWorldFolder.getAbsolutePath()); + File sourceDimensionRoot = IrisWorldStorage.dimensionRoot(IrisWorldStorage.keyFromLegacyName(newName)); + if (!sourceDimensionRoot.isDirectory()) { + throw new IllegalStateException("Source dimension folder does not exist: " + sourceDimensionRoot.getAbsolutePath()); } - for (String sub : List.of("datapacks", "playerdata", "advancements", "stats")) { - File source = new File(oldWorldFolder, sub); + File newLevelRoot = new File(worldContainer, newName); + if (!newLevelRoot.exists() && !newLevelRoot.mkdirs()) { + throw new IllegalStateException("Could not create target level folder: " + newLevelRoot.getAbsolutePath()); + } + + for (String sub : List.of("data", "datapacks", "players")) { + File source = new File(oldLevelRoot, sub); if (!source.exists()) { continue; } - IO.copyDirectory(source.toPath(), new File(newWorldFolder, sub).toPath()); + IO.copyDirectory(source.toPath(), new File(newLevelRoot, sub).toPath()); } + File targetDimensionRoot = IrisWorldStorage.dimensionRoot(newLevelRoot, NamespacedKey.minecraft("overworld")); + IO.copyDirectory(sourceDimensionRoot.toPath(), targetDimensionRoot.toPath()); + + World sourceWorld = WorldIdentity.resolve(IrisWorldStorage.keyFromLegacyName(newName)).orElse(null); + Long stagedSeed = IrisWorlds.readBukkitWorldSeed(newName); + if (sourceWorld == null && stagedSeed == null) { + throw new IllegalStateException("Cannot determine the promoted world's seed."); + } + long promotedSeed = sourceWorld == null ? stagedSeed : sourceWorld.getSeed(); data.setProperty("level-name", newName); - try (var out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) { + data.setProperty("level-seed", Long.toString(promotedSeed)); + try (FileOutputStream out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) { data.store(out, null); } return true; @@ -198,7 +217,7 @@ public class CommandIris implements DirectorExecutor { sender().sendMessage(C.YELLOW + "Runtime world creation is disabled on Folia."); sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup..."); - File worldFolder = new File(Bukkit.getWorldContainer(), name); + File worldFolder = IrisWorldStorage.dimensionRoot(name); IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension, worldFolder); if (installed == null) { sender().sendMessage(C.RED + "Failed to stage world files for dimension \"" + dimension.getLoadKey() + "\"."); @@ -497,7 +516,6 @@ public class CommandIris implements DirectorExecutor { @Param(description = "The name of the world to load") String world ) { - World worldloaded = Bukkit.getWorld(world); worldNameToCheck = world; boolean worldExists = doesWorldExist(worldNameToCheck); WorldEngine = world; @@ -507,8 +525,7 @@ public class CommandIris implements DirectorExecutor { return; } - String pathtodim = world + File.separator +"iris"+File.separator +"pack"+File.separator +"dimensions"+File.separator; - File directory = new File(Bukkit.getWorldContainer(), pathtodim); + File directory = new File(IrisWorldStorage.packRoot(IrisWorldStorage.keyFromLegacyName(world)), "dimensions"); String dimension = null; if (directory.exists() && directory.isDirectory()) { @@ -562,8 +579,7 @@ public class CommandIris implements DirectorExecutor { } boolean doesWorldExist(String worldName) { - File worldContainer = Bukkit.getWorldContainer(); - File worldDirectory = new File(worldContainer, worldName); + File worldDirectory = IrisWorldStorage.dimensionRoot(worldName); return worldDirectory.exists() && worldDirectory.isDirectory(); } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java index 628affd98..794e433f1 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java @@ -42,6 +42,8 @@ import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.math.RNG; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.NamespacedKey; @@ -49,6 +51,7 @@ import org.bukkit.Registry; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; +import org.bukkit.generator.structure.Structure; import org.bukkit.util.StructureSearchResult; import java.io.File; @@ -139,8 +142,9 @@ public class CommandStructure implements DirectorExecutor { int irisPlaced = 0; KList notFound = new KList<>(); KList cannotGenerate = new KList<>(); - for (org.bukkit.generator.structure.Structure structure : Registry.STRUCTURE) { - NamespacedKey key = structure.getKey(); + Registry structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE); + for (Structure structure : structureRegistry) { + NamespacedKey key = structureRegistry.getKey(structure); String keyName = key == null ? structure.toString() : key.toString(); boolean isIrisPlaced = engine != null && IrisStructureLocator.suppressesVanilla(engine, keyName); boolean isReachable = engine != null && reachable.contains(keyName.toLowerCase()); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java index 13cb13cf3..bb07abc84 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java @@ -389,7 +389,7 @@ public class IrisEngineSVC implements IrisService { } PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); - return pregeneratorJob != null && pregeneratorJob.targetsWorldName(engine.getWorld().name()); + return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(engine.getWorld().identity()); } private int activeTectonicLimit(Engine engine) { @@ -446,7 +446,7 @@ public class IrisEngineSVC implements IrisService { } PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); - boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldName(world.getName()); + boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(world); return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld); } } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java index 79745ea49..ab56f9341 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java @@ -32,6 +32,7 @@ import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.protocol.IrisProtocol; import art.arcane.iris.util.common.plugin.IrisService; import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.entity.Player; @@ -167,7 +168,7 @@ public class IrisProtocolService implements IrisService, PluginMessageListener, world.getSeed(), engine.getMinHeight(), engine.getMaxHeight(), true); return; } - protocol.sendDimensionStatus(sessionId, world.getName(), "", world.getSeed(), world.getMinHeight(), world.getMaxHeight(), false); + protocol.sendDimensionStatus(sessionId, WorldIdentity.serialize(world), "", world.getSeed(), world.getMinHeight(), world.getMaxHeight(), false); } private static Engine resolveEngine(String sessionId) { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java index 54ae6b50f..bfe8ec95b 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java @@ -24,6 +24,7 @@ import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.Sound; +import org.bukkit.World; import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.edit.DustRevealer; @@ -32,6 +33,7 @@ import art.arcane.iris.core.wand.WandSelection; import art.arcane.iris.engine.object.IrisObject; import art.arcane.iris.util.project.matter.WorldMatter; import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.data.Cuboid; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.math.M; @@ -210,7 +212,9 @@ public class WandSVC implements IrisService { if (f.length != 2) return null; String[] g = f[0].split("\\Q,\\E"); if (g.length != 3) return null; - return new Location(Bukkit.getWorld(f[1]), Integer.parseInt(g[0]), Integer.parseInt(g[1]), Integer.parseInt(g[2])); + World world = WorldIdentity.resolve(f[1]).orElse(null); + if (world == null) return null; + return new Location(world, Integer.parseInt(g[0]), Integer.parseInt(g[1]), Integer.parseInt(g[2])); } catch (Throwable e) { Iris.reportError(e); return null; @@ -228,7 +232,7 @@ public class WandSVC implements IrisService { return "<#>"; } - return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + loc.getWorld().getName(); + return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + WorldIdentity.serialize(loc.getWorld()); } /** @@ -532,7 +536,7 @@ public class WandSVC implements IrisService { Location[] f = getCuboidFromItem(item); Location other = left ? f[1] : f[0]; - if (other != null && !other.getWorld().getName().equals(a.getWorld().getName())) { + if (other != null && !WorldIdentity.key(other.getWorld()).equals(WorldIdentity.key(a.getWorld()))) { other = null; } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedParityProbe.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedParityProbe.java index 345dbea92..94f8f5527 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedParityProbe.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedParityProbe.java @@ -133,6 +133,7 @@ public final class ModdedParityProbe { } IrisWorld world = IrisWorld.builder() + .platformIdentity("iris:parity") .name("parity") .seed(SEED) .worldFolder(new File(workRoot, "world")) diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java index 20c0dcc92..25a76e3aa 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java @@ -84,6 +84,7 @@ public final class ModdedWorldEngines { long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride; File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile(); IrisWorld world = IrisWorld.builder() + .platformIdentity(level.dimension().identifier().toString()) .name(level.dimension().identifier().toString().replace(':', '_')) .seed(seed) .worldFolder(worldFolder) diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java index edebcfeb1..2f51960ea 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java @@ -641,7 +641,7 @@ public final class ModdedWorldManager implements EngineWorldManager { private boolean isPregenActive() { PregeneratorJob job = PregeneratorJob.getInstance(); - return job != null && job.targetsWorldName(engine.getWorld().name()); + return job != null && job.targetsWorldIdentity(engine.getWorld().identity()); } private static boolean markerSystemEnabled() { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java index 1fcf200d9..7e1a9e776 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java @@ -106,8 +106,8 @@ public final class ModdedRegen { private void run() { long startedAt = M.ms(); - String worldName = engine.getWorld() == null ? null : engine.getWorld().name(); - WorldMaintenance.beginWorldMaintenance(worldName, "regen"); + String worldIdentity = engine.getWorld() == null ? null : engine.getWorld().identity(); + WorldMaintenance.beginWorldMaintenance(worldIdentity, "regen"); try { resetMantleMargin(); List targets = ChunkSpiral.centerOut(centerX, centerZ, radius); @@ -120,7 +120,7 @@ public final class ModdedRegen { LOGGER.error("Iris regen failed", e); fail("Regen failed: " + e); } finally { - WorldMaintenance.endWorldMaintenance(worldName, "regen"); + WorldMaintenance.endWorldMaintenance(worldIdentity, "regen"); ACTIVE.set(false); } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java index d21292cab..2b8590179 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java @@ -127,7 +127,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService { private boolean isPregenActive(Engine engine) { PregeneratorJob job = PregeneratorJob.getInstance(); - return job != null && job.targetsWorldName(engine.getWorld().name()); + return job != null && job.targetsWorldIdentity(engine.getWorld().identity()); } private void updateNearPlayers(Engine engine, ServerLevel level) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java index ddfc84b7c..86603fd2b 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java @@ -169,7 +169,7 @@ public final class ModdedEngineMaintenanceService implements ModdedTickableServi } private boolean shouldSkipForMaintenance(Engine engine) { - if (engine.getWorld() == null || !WorldMaintenance.isWorldMaintenanceActive(engine.getWorld().name())) { + if (engine.getWorld() == null || !WorldMaintenance.isWorldMaintenanceActive(engine.getWorld().identity())) { return false; } return !pregenTargets(engine); @@ -180,7 +180,7 @@ public final class ModdedEngineMaintenanceService implements ModdedTickableServi return false; } PregeneratorJob job = PregeneratorJob.getInstance(); - return job != null && job.targetsWorldName(engine.getWorld().name()); + return job != null && job.targetsWorldIdentity(engine.getWorld().identity()); } private int activeTectonicLimit(Engine engine, int share) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java index 5f6d5b47b..d94344790 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java @@ -139,7 +139,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService { return true; } PregeneratorJob job = PregeneratorJob.getInstance(); - return job != null && job.targetsWorldName(engine.getWorld().name()); + return job != null && job.targetsWorldIdentity(engine.getWorld().identity()); } private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) { diff --git a/build.gradle b/build.gradle index 98bb02397..d7d1d2269 100644 --- a/build.gradle +++ b/build.gradle @@ -81,7 +81,7 @@ String forgeConsumerPath = "${consumerLocation}/forge-mod-consumers/dropins/mods String neoForgeConsumerPath = "${consumerLocation}/neoforge-mod-consumers/dropins/mods" def nmsBindings = [ - v26_2_R1: '26.2.build.25-alpha', + v26_2_R1: '26.2.build.60-beta', ] Class nmsTypeClass = Class.forName('NMSBinding$Type') nmsBindings.each { key, value -> diff --git a/core/build.gradle b/core/build.gradle index 04b94cd60..eb4df3688 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -62,7 +62,7 @@ dependencies { api(project(':spi')) // Provided or Classpath - compileOnly(libs.spigot) + compileOnly(libs.paper.api) compileOnly(libs.log4j.api) compileOnly(libs.log4j.core) @@ -120,8 +120,8 @@ dependencies { testImplementation('junit:junit:4.13.2') testImplementation('org.mockito:mockito-core:5.16.1') - testImplementation(libs.spigot) - testRuntimeOnly(libs.spigot) + testImplementation(libs.paper.api) + testRuntimeOnly(libs.paper.api) } java { diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index 380069819..6867309e4 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -254,7 +254,6 @@ public class IrisSettings { public boolean adjustVanillaHeight = false; public boolean autoIngestDatapacks = true; public boolean autoImportDatapackStructures = true; - public String forceMainWorld = ""; public int spinh = -20; public int spins = 7; public int spinb = 8; diff --git a/core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java b/core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java new file mode 100644 index 000000000..e6097946b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java @@ -0,0 +1,129 @@ +package art.arcane.iris.core; + +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.generator.WorldInfo; + +import java.io.File; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; + +public final class IrisWorldStorage { + private IrisWorldStorage() { + } + + public static File levelRoot() { + return Bukkit.getServer().getLevelDirectory().toAbsolutePath().normalize().toFile(); + } + + public static File levelRoot(File dimensionRoot) { + Path current = Objects.requireNonNull(dimensionRoot, "dimensionRoot").toPath().toAbsolutePath().normalize(); + while (current != null) { + Path fileName = current.getFileName(); + if (fileName != null && "dimensions".equals(fileName.toString())) { + Path parent = current.getParent(); + if (parent != null) { + return parent.toFile(); + } + break; + } + current = current.getParent(); + } + return dimensionRoot.getAbsoluteFile(); + } + + public static NamespacedKey keyFromLegacyName(String worldName) { + return keyFromLegacyName(worldName, levelRoot().getName()); + } + + static NamespacedKey keyFromLegacyName(String worldName, String levelName) { + String name = Objects.requireNonNull(worldName, "worldName").trim(); + String mainLevelName = Objects.requireNonNull(levelName, "levelName").trim(); + if (name.isEmpty()) { + throw new IllegalArgumentException("World name cannot be empty."); + } + if (name.equals(mainLevelName)) { + return NamespacedKey.minecraft("overworld"); + } + if (name.equals(mainLevelName + "_nether")) { + return NamespacedKey.minecraft("the_nether"); + } + if (name.equals(mainLevelName + "_the_end")) { + return NamespacedKey.minecraft("the_end"); + } + + String key = name.toLowerCase(Locale.ENGLISH).replace(' ', '_'); + return NamespacedKey.minecraft(key); + } + + public static File dimensionRoot(String worldName) { + return dimensionRoot(keyFromLegacyName(worldName)); + } + + public static File dimensionRoot(WorldInfo world) { + NamespacedKey key = WorldIdentity.key(world); + Optional loadedWorld = WorldIdentity.resolve(key); + if (loadedWorld.isPresent()) { + return loadedWorld.get().getWorldFolder().getAbsoluteFile(); + } + return dimensionRoot(key); + } + + public static File dimensionRoot(NamespacedKey key) { + return dimensionRoot(levelRoot(), key); + } + + public static File dimensionRoot(File levelRoot, NamespacedKey key) { + Path dimensionsRoot = Objects.requireNonNull(levelRoot, "levelRoot") + .toPath() + .toAbsolutePath() + .normalize() + .resolve("dimensions"); + NamespacedKey worldKey = Objects.requireNonNull(key, "key"); + Path namespaceRoot = dimensionsRoot.resolve(worldKey.getNamespace()).normalize(); + Path dimensionRoot = namespaceRoot.resolve(worldKey.getKey()).normalize(); + if (!dimensionRoot.startsWith(namespaceRoot)) { + throw new IllegalArgumentException("World key escapes its namespace storage root: " + worldKey); + } + return dimensionRoot.toFile(); + } + + public static Optional keyFromDimensionRoot(File levelRoot, File dimensionRoot) { + Path dimensionsPath = Objects.requireNonNull(levelRoot, "levelRoot") + .toPath() + .toAbsolutePath() + .normalize() + .resolve("dimensions"); + Path worldPath = Objects.requireNonNull(dimensionRoot, "dimensionRoot") + .toPath() + .toAbsolutePath() + .normalize(); + if (!worldPath.startsWith(dimensionsPath)) { + return Optional.empty(); + } + + Path relative = dimensionsPath.relativize(worldPath); + if (relative.getNameCount() < 2) { + return Optional.empty(); + } + + String namespace = relative.getName(0).toString(); + StringBuilder key = new StringBuilder(); + for (int i = 1; i < relative.getNameCount(); i++) { + if (!key.isEmpty()) { + key.append('/'); + } + key.append(relative.getName(i)); + } + + return Optional.ofNullable(NamespacedKey.fromString(namespace + ":" + key)); + } + + public static File packRoot(NamespacedKey key) { + return new File(dimensionRoot(key), "iris/pack"); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisWorlds.java b/core/src/main/java/art/arcane/iris/core/IrisWorlds.java index fe1208ab0..7340ffe7d 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisWorlds.java +++ b/core/src/main/java/art/arcane/iris/core/IrisWorlds.java @@ -1,16 +1,20 @@ package art.arcane.iris.core; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.common.misc.ServerProperties; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.io.IO; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; -import art.arcane.iris.spi.IrisLogging; -import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.volmlib.util.collection.KMap; -import art.arcane.volmlib.util.io.IO; -import art.arcane.iris.util.common.misc.ServerProperties; import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; @@ -30,8 +34,15 @@ public class IrisWorlds { private volatile boolean dirty = false; private IrisWorlds(KMap worlds) { - this.worlds = worlds; - readBukkitWorlds().forEach(this::put0); + this.worlds = new KMap<>(); + worlds.forEach((identity, type) -> { + String normalizedIdentity = migrateLoadedIdentity(identity); + this.worlds.put(normalizedIdentity, type); + if (!normalizedIdentity.equals(identity)) { + dirty = true; + } + }); + readBukkitWorlds().forEach((name, type) -> put0(IrisWorldStorage.keyFromLegacyName(name).toString(), type)); save(); } @@ -56,20 +67,23 @@ public class IrisWorlds { }); } - public void put(String name, String type) { - put0(name, type); + public void put(String identity, String type) { + put0(identity, type); save(); } - private void put0(String name, String type) { - String old = worlds.put(name, type); + private void put0(String identity, String type) { + String canonicalIdentity = WorldIdentity.parse(identity).toString(); + String old = worlds.put(canonicalIdentity, type); if (!type.equals(old)) dirty = true; } public KMap getWorlds() { clean(); - return readBukkitWorlds().put(worlds); + KMap result = new KMap<>(); + readBukkitWorlds().forEach((name, type) -> result.put(IrisWorldStorage.keyFromLegacyName(name).toString(), type)); + return result.put(worlds); } public Stream getPacks() { @@ -87,7 +101,15 @@ public class IrisWorlds { } public void clean() { - dirty = worlds.entrySet().removeIf(entry -> !new File(Bukkit.getWorldContainer(), entry.getKey() + "/iris/pack/dimensions/" + entry.getValue() + ".json").exists()); + boolean removed = worlds.entrySet().removeIf(entry -> { + try { + File packRoot = IrisWorldStorage.packRoot(WorldIdentity.parse(entry.getKey())); + return !new File(packRoot, "dimensions/" + entry.getValue() + ".json").exists(); + } catch (IllegalArgumentException e) { + return true; + } + }); + dirty = dirty || removed; } public synchronized void save() { @@ -114,13 +136,13 @@ public class IrisWorlds { } public static KMap readBukkitWorlds() { - var bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML); - var worlds = bukkit.getConfigurationSection("worlds"); + YamlConfiguration bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML); + ConfigurationSection worlds = bukkit.getConfigurationSection("worlds"); if (worlds == null) return new KMap<>(); - var result = new KMap(); + KMap result = new KMap<>(); for (String world : worlds.getKeys(false)) { - var gen = worlds.getString(world + ".generator"); + String gen = worlds.getString(world + ".generator"); if (gen == null) continue; String loadKey; @@ -136,20 +158,28 @@ public class IrisWorlds { return result; } - private static art.arcane.iris.engine.object.IrisDimension loadDimension(String worldName, String id) { - java.io.File pack = new java.io.File(org.bukkit.Bukkit.getWorldContainer(), String.join(java.io.File.separator, worldName, "iris", "pack")); - art.arcane.iris.engine.object.IrisDimension dimension = pack.isDirectory() ? art.arcane.iris.core.loader.IrisData.get(pack).getDimensionLoader().load(id) : null; + private static IrisDimension loadDimension(String worldIdentity, String id) { + File pack = IrisWorldStorage.packRoot(WorldIdentity.parse(worldIdentity)); + IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null; if (dimension == null) { - dimension = art.arcane.iris.core.loader.IrisData.loadAnyDimension(id, null); + dimension = IrisData.loadAnyDimension(id, null); } if (dimension == null) { IrisLogging.warn("Unable to find dimension type " + id + " Looking for online packs..."); - art.arcane.iris.spi.IrisServices.get(art.arcane.iris.core.service.StudioSVC.class).downloadSearch(new art.arcane.iris.util.common.plugin.VolmitSender(org.bukkit.Bukkit.getConsoleSender()), id, false); - dimension = art.arcane.iris.core.loader.IrisData.loadAnyDimension(id, null); + IrisServices.get(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false); + dimension = IrisData.loadAnyDimension(id, null); if (dimension != null) { IrisLogging.info("Resolved missing dimension, proceeding."); } } return dimension; } + + private static String migrateLoadedIdentity(String identity) { + try { + return WorldIdentity.parse(identity).toString(); + } catch (IllegalArgumentException e) { + return IrisWorldStorage.keyFromLegacyName(identity).toString(); + } + } } diff --git a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java index 711194f9f..282b41c85 100644 --- a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -36,6 +36,7 @@ import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import lombok.NonNull; import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; @@ -46,6 +47,7 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -58,8 +60,6 @@ import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.stream.Stream; public class ServerConfigurator { - private static volatile boolean deferredInstallPending = false; - public static void configure() { IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration(); if (s.isConfigureSpigotTimeoutTime()) { @@ -70,26 +70,9 @@ public class ServerConfigurator { J.attempt(ServerConfigurator::increasePaperWatchdog); } - if (shouldDeferInstallUntilWorldsReady()) { - deferredInstallPending = true; - return; - } - - deferredInstallPending = false; installDataPacks(true); } - public static void configureIfDeferred() { - if (!deferredInstallPending) { - return; - } - - configure(); - if (deferredInstallPending) { - J.a(ServerConfigurator::configureIfDeferred, 20); - } - } - private static void increaseKeepAliveSpigot() throws IOException, InvalidConfigurationException { File spigotConfig = new File("spigot.yml"); FileConfiguration f = new YamlConfiguration(); @@ -122,20 +105,7 @@ public class ServerConfigurator { } public static KList getDatapacksFolder() { - if (!IrisSettings.get().getGeneral().forceMainWorld.isEmpty()) { - return new KList().qadd(new File(Bukkit.getWorldContainer(), IrisSettings.get().getGeneral().forceMainWorld + "/datapacks")); - } - KList worlds = new KList<>(); - Bukkit.getServer().getWorlds().forEach(w -> { - File folder = resolveDatapacksFolder(w.getWorldFolder()); - if (!worlds.contains(folder)) { - worlds.add(folder); - } - }); - if (worlds.isEmpty()) { - worlds.add(new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME + "/datapacks")); - } - return worlds; + return new KList().qadd(new File(IrisWorldStorage.levelRoot(), "datapacks")); } public static boolean installDataPacks(boolean fullInstall) { @@ -250,15 +220,6 @@ public class ServerConfigurator { } } - private static boolean shouldDeferInstallUntilWorldsReady() { - String forcedMainWorld = IrisSettings.get().getGeneral().forceMainWorld; - if (forcedMainWorld != null && !forcedMainWorld.isBlank()) { - return false; - } - - return Bukkit.getServer().getWorlds().isEmpty(); - } - public static File resolveDatapacksFolder(File worldFolder) { File rootFolder = resolveWorldRootFolder(worldFolder); return new File(rootFolder, "datapacks"); @@ -266,22 +227,9 @@ public class ServerConfigurator { static File resolveWorldRootFolder(File worldFolder) { if (worldFolder == null) { - return new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME); + return IrisWorldStorage.levelRoot(); } - - File current = worldFolder.getAbsoluteFile(); - while (current != null) { - if ("dimensions".equals(current.getName())) { - File parent = current.getParentFile(); - if (parent != null) { - return parent; - } - break; - } - current = current.getParentFile(); - } - - return worldFolder.getAbsoluteFile(); + return IrisWorldStorage.levelRoot(worldFolder); } private static boolean verifyDataPacksPost(boolean allowRestarting) { @@ -401,13 +349,19 @@ public class ServerConfigurator { @Nullable public static String getWorld(@NonNull IrisData data) { - String worldContainer = Bukkit.getWorldContainer().getAbsolutePath(); - if (!worldContainer.endsWith(File.separator)) worldContainer += File.separator; + Path packPath = data.getDataFolder().toPath().toAbsolutePath().normalize(); + Path irisPath = packPath.getParent(); + if (irisPath == null || !"pack".equals(packPath.getFileName().toString()) || !"iris".equals(irisPath.getFileName().toString())) { + return null; + } - String path = data.getDataFolder().getAbsolutePath(); - if (!path.startsWith(worldContainer)) return null; - int l = path.endsWith(File.separator) ? 11 : 10; - return path.substring(worldContainer.length(), path.length() - l); + Path dimensionPath = irisPath.getParent(); + if (dimensionPath == null) { + return null; + } + File dimensionRoot = dimensionPath.toFile(); + NamespacedKey key = IrisWorldStorage.keyFromDimensionRoot(IrisWorldStorage.levelRoot(), dimensionRoot).orElse(null); + return key == null ? null : key.toString(); } public static class DimensionHeight { diff --git a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java index c87c25ea3..3de379158 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java +++ b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java @@ -36,6 +36,8 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.math.Position2; import art.arcane.volmlib.util.scheduling.ChronoLatch; import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.generator.WorldInfo; import java.awt.Color; import java.util.concurrent.ExecutorService; @@ -216,13 +218,20 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource { return engine.getWorld().name(); } - public boolean targetsWorldName(String worldName) { - if (worldName == null || engine == null || engine.getWorld() == null) { + public boolean targetsWorld(WorldInfo world) { + if (world == null || engine == null || engine.getWorld() == null) { return false; } - String targetName = engine.getWorld().name(); - return targetName != null && targetName.equalsIgnoreCase(worldName); + return WorldIdentity.key(world).equals(engine.getWorld().key()); + } + + public boolean targetsWorldIdentity(String worldIdentity) { + if (worldIdentity == null || engine == null || engine.getWorld() == null) { + return false; + } + + return worldIdentity.equals(engine.getWorld().identity()); } private static Color parseColor(String c) { diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java index b330ccba9..18d98914c 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java @@ -1,7 +1,7 @@ package art.arcane.iris.core.lifecycle; import art.arcane.iris.core.nms.INMS; -import org.bukkit.Bukkit; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import org.bukkit.World; import org.bukkit.WorldCreator; @@ -21,7 +21,7 @@ final class BukkitPublicBackend implements WorldLifecycleBackend { @Override public CompletableFuture create(WorldLifecycleRequest request) { - World existing = Bukkit.getWorld(request.worldName()); + World existing = WorldIdentity.resolve(request.worldKey()).orElse(null); if (existing != null) { return CompletableFuture.completedFuture(existing); } diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java index 8424c6247..8aa47e3f3 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java @@ -1,7 +1,7 @@ package art.arcane.iris.core.lifecycle; import art.arcane.iris.spi.IrisLogging; -import org.bukkit.Bukkit; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import org.bukkit.World; import java.util.Locale; @@ -31,7 +31,7 @@ final class PaperLikeRuntimeBackend implements WorldLifecycleBackend { public CompletableFuture create(WorldLifecycleRequest request) { Object legacyStorageAccess = null; try { - World existing = Bukkit.getWorld(request.worldName()); + World existing = WorldIdentity.resolve(request.worldKey()).orElse(null); if (existing != null) { return CompletableFuture.completedFuture(existing); } @@ -47,7 +47,7 @@ final class PaperLikeRuntimeBackend implements WorldLifecycleBackend { + ", backend=paper_like_runtime, flavor=" + capabilities.paperLikeFlavor().name().toLowerCase(Locale.ROOT) + ", registrySource=" + WorldLifecycleSupport.runtimeLevelStemRegistrySource(request)); Object levelStem = WorldLifecycleSupport.resolveRuntimeLevelStem(capabilities, request); - Object stemKey = WorldLifecycleSupport.createRuntimeLevelStemKey(request.worldName()); + Object stemKey = WorldLifecycleSupport.createRuntimeLevelStemKey(request.worldKey()); if (capabilities.paperLikeFlavor() == CapabilitySnapshot.PaperLikeFlavor.CURRENT_INFO_AND_DATA) { Object dimensionKey = WorldLifecycleSupport.createDimensionKey(stemKey); @@ -55,18 +55,18 @@ final class PaperLikeRuntimeBackend implements WorldLifecycleBackend { Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, !request.studio()); Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData); Object worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName()); - if (!WorldLifecycleSupport.hasExistingWorldData(request.worldName())) { + if (!WorldLifecycleSupport.hasExistingWorldData(request.worldKey())) { worldDataAndGenSettings = WorldLifecycleSupport.applySeedToWorldDataAndGenSettings(worldDataAndGenSettings, request.seed()); } capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings); } else { - legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, request.worldName()); + legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities); Object primaryLevelData = WorldLifecycleSupport.createLegacyPrimaryLevelData(capabilities, legacyStorageAccess, request.worldName()); Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(0, request.worldName(), request.environment().name().toLowerCase(Locale.ROOT), stemKey, !request.studio()); capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfo, legacyStorageAccess, primaryLevelData); } - World loadedWorld = Bukkit.getWorld(request.worldName()); + World loadedWorld = WorldIdentity.resolve(request.worldKey()).orElse(null); if (loadedWorld == null) { return CompletableFuture.failedFuture(new IllegalStateException("Paper-like runtime backend did not load world \"" + request.worldName() + "\".")); } diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java index d45016136..b2e32a530 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.lifecycle; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.WorldCreator; import org.bukkit.WorldType; @@ -8,6 +9,7 @@ import org.bukkit.generator.ChunkGenerator; public record WorldLifecycleRequest( String worldName, + NamespacedKey worldKey, World.Environment environment, ChunkGenerator generator, BiomeProvider biomeProvider, @@ -22,6 +24,7 @@ public record WorldLifecycleRequest( public static WorldLifecycleRequest fromCreator(WorldCreator creator, boolean studio, boolean benchmark, WorldLifecycleCaller callerKind) { return new WorldLifecycleRequest( creator.name(), + creator.key(), creator.environment(), creator.generator(), creator.biomeProvider(), @@ -36,7 +39,7 @@ public record WorldLifecycleRequest( } public WorldCreator toWorldCreator() { - WorldCreator creator = new WorldCreator(worldName) + WorldCreator creator = WorldCreator.ofKey(worldKey) .environment(environment) .generateStructures(generateStructures) .hardcore(hardcore) diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java index 7d8bf5314..41c1ecac2 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java @@ -2,6 +2,8 @@ package art.arcane.iris.core.lifecycle; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.NamespacedKey; import org.bukkit.World; import java.util.List; @@ -18,7 +20,7 @@ public final class WorldLifecycleService { private final PaperLikeRuntimeBackend paperLikeRuntimeBackend; private final BukkitPublicBackend bukkitPublicBackend; private final List backends; - private final Map worldBackendByName; + private final Map worldBackendByKey; public WorldLifecycleService(CapabilitySnapshot capabilities) { this.capabilities = capabilities; @@ -26,7 +28,7 @@ public final class WorldLifecycleService { this.paperLikeRuntimeBackend = new PaperLikeRuntimeBackend(capabilities); this.bukkitPublicBackend = new BukkitPublicBackend(capabilities); this.backends = List.of(worldsProviderBackend, paperLikeRuntimeBackend, bukkitPublicBackend); - this.worldBackendByName = new ConcurrentHashMap<>(); + this.worldBackendByKey = new ConcurrentHashMap<>(); } public static WorldLifecycleService get() { @@ -74,7 +76,7 @@ public final class WorldLifecycleService { return; } if (world != null) { - worldBackendByName.put(world.getName(), backend.backendName()); + worldBackendByKey.put(WorldIdentity.serialize(world), backend.backendName()); } }); } @@ -104,7 +106,8 @@ public final class WorldLifecycleService { } private boolean unloadDirect(World world, boolean save) { - WorldLifecycleBackend backend = selectUnloadBackend(world.getName()); + String worldIdentity = WorldIdentity.serialize(world); + WorldLifecycleBackend backend = selectUnloadBackend(worldIdentity); IrisLogging.info("WorldLifecycle unload: world=%s, backend=%s", world.getName(), backend.backendName()); @@ -124,13 +127,13 @@ public final class WorldLifecycleService { throw new IllegalStateException(e); } if (unloaded) { - worldBackendByName.remove(world.getName()); + worldBackendByKey.remove(worldIdentity); } return unloaded; } - public String backendNameForWorld(String worldName) { - return selectUnloadBackend(worldName).backendName(); + public String backendNameForWorld(NamespacedKey worldKey) { + return selectUnloadBackend(worldKey.toString()).backendName(); } WorldLifecycleBackend selectCreateBackend(WorldLifecycleRequest request) { @@ -155,8 +158,8 @@ public final class WorldLifecycleService { throw new IllegalStateException("No world lifecycle backend supports request for \"" + request.worldName() + "\"."); } - WorldLifecycleBackend selectUnloadBackend(String worldName) { - String backendName = worldBackendByName.get(worldName); + WorldLifecycleBackend selectUnloadBackend(String worldIdentity) { + String backendName = worldBackendByKey.get(worldIdentity); if (backendName == null) { return bukkitPublicBackend; } @@ -170,7 +173,7 @@ public final class WorldLifecycleService { return bukkitPublicBackend; } - void rememberBackend(String worldName, String backendName) { - worldBackendByName.put(worldName, backendName); + void rememberBackend(NamespacedKey worldKey, String backendName) { + worldBackendByKey.put(worldKey.toString(), backendName); } } diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java index c5ba748fa..5c361d30b 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java @@ -1,12 +1,15 @@ package art.arcane.iris.core.lifecycle; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.link.Identifier; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.nms.INMSBinding; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; @@ -18,7 +21,6 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Path; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; @@ -119,10 +121,8 @@ final class WorldLifecycleSupport { return lookupMethod.invoke(datapackDimensions, levelStemRegistryKey); } - static Object createRuntimeLevelStemKey(String worldName) throws ReflectiveOperationException { - String sanitized = worldName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/_-]", "_"); - String path = "runtime/" + sanitized; - Identifier identifier = new Identifier("iris", path); + static Object createRuntimeLevelStemKey(NamespacedKey worldKey) throws ReflectiveOperationException { + Identifier identifier = new Identifier(worldKey.getNamespace(), worldKey.getKey()); Object rawIdentifier = Class.forName("net.minecraft.resources.Identifier") .getMethod("fromNamespaceAndPath", String.class, String.class) .invoke(null, identifier.namespace(), identifier.key()); @@ -253,11 +253,13 @@ final class WorldLifecycleSupport { setModdedInfoMethod.invoke(worldData, modName, modified); } - static boolean hasExistingWorldData(String worldName) { - File worldFolder = new File(Bukkit.getWorldContainer(), worldName); - return new File(worldFolder, "level.dat").exists() - || new File(worldFolder, "region").exists() - || new File(worldFolder, "data").exists(); + static boolean hasExistingWorldData(NamespacedKey worldKey) { + File worldFolder = IrisWorldStorage.dimensionRoot(worldKey); + return new File(worldFolder, "region").exists() + || new File(worldFolder, "entities").exists() + || new File(worldFolder, "poi").exists() + || new File(worldFolder, "data/paper/metadata.dat").exists() + || new File(worldFolder, "paper-world.yml").exists(); } static Object applySeedToWorldDataAndGenSettings(Object worldDataAndGenSettings, long seed) throws ReflectiveOperationException { @@ -386,18 +388,19 @@ final class WorldLifecycleSupport { return primaryLevelData; } - static Object createLegacyStorageAccess(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException { + static Object createLegacyStorageAccess(CapabilitySnapshot capabilities) throws ReflectiveOperationException { Class levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource"); Method createDefaultMethod = levelStorageSourceClass.getMethod("createDefault", Path.class); - Object levelStorageSource = createDefaultMethod.invoke(null, Bukkit.getWorldContainer().toPath()); + File levelRoot = IrisWorldStorage.levelRoot(); + Object levelStorageSource = createDefaultMethod.invoke(null, levelRoot.getParentFile().toPath()); Method storageAccessMethod = capabilities.levelStorageAccessMethod(); if (storageAccessMethod.getParameterCount() == 1) { - return storageAccessMethod.invoke(levelStorageSource, worldName); + return storageAccessMethod.invoke(levelStorageSource, levelRoot.getName()); } Object overworldStemKey = Class.forName("net.minecraft.world.level.dimension.LevelStem") .getField("OVERWORLD") .get(null); - return storageAccessMethod.invoke(levelStorageSource, worldName, overworldStemKey); + return storageAccessMethod.invoke(levelStorageSource, levelRoot.getName(), overworldStemKey); } static void closeLevelStorageAccess(Object levelStorageAccess) { @@ -437,8 +440,8 @@ final class WorldLifecycleSupport { Method getHandleMethod = world.getClass().getMethod("getHandle"); Object serverLevel = getHandleMethod.invoke(world); closeServerLevel(world, serverLevel); - detachServerLevel(capabilities, serverLevel, world.getName()); - return Bukkit.getWorld(world.getName()) == null; + detachServerLevel(capabilities, serverLevel, world); + return WorldIdentity.resolve(WorldIdentity.key(world)).isEmpty(); } catch (Throwable e) { throw new IllegalStateException("Failed to unload world \"" + world.getName() + "\" through the selected world lifecycle backend.", unwrap(e)); } @@ -527,7 +530,7 @@ final class WorldLifecycleSupport { } @SuppressWarnings({"rawtypes", "unchecked"}) - private static void removeWorldFromCraftServerMap(String worldName) throws ReflectiveOperationException { + private static void removeWorldFromCraftServerMap(World world) throws ReflectiveOperationException { Object bukkitServer = Bukkit.getServer(); if (bukkitServer == null) { return; @@ -536,16 +539,17 @@ final class WorldLifecycleSupport { Field worldsField = CapabilityResolution.resolveField(bukkitServer.getClass(), "worlds"); Object rawWorlds = worldsField.get(bukkitServer); if (rawWorlds instanceof Map map) { - map.remove(worldName); - map.remove(worldName.toLowerCase(Locale.ROOT)); + map.remove(WorldIdentity.key(world)); + map.remove(WorldIdentity.serialize(world)); + map.remove(world.getName()); } } - private static void detachServerLevel(CapabilitySnapshot capabilities, Object serverLevel, String worldName) throws Throwable { + private static void detachServerLevel(CapabilitySnapshot capabilities, Object serverLevel, World world) throws Throwable { Runnable detachTask = () -> { try { capabilities.removeLevelMethod().invoke(capabilities.minecraftServer(), serverLevel); - removeWorldFromCraftServerMap(worldName); + removeWorldFromCraftServerMap(world); } catch (Throwable e) { throw new RuntimeException(e); } @@ -558,7 +562,7 @@ final class WorldLifecycleSupport { CompletableFuture detachFuture = J.sfut(detachTask); if (detachFuture == null) { - throw new IllegalStateException("Failed to schedule global detach task for world \"" + worldName + "\"."); + throw new IllegalStateException("Failed to schedule global detach task for world \"" + world.getName() + "\"."); } detachFuture.get(15, TimeUnit.SECONDS); } diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java index c997caf1d..2548b0006 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java @@ -1,10 +1,9 @@ package art.arcane.iris.core.lifecycle; -import org.bukkit.Bukkit; +import art.arcane.iris.core.IrisWorldStorage; import org.bukkit.World; import org.bukkit.WorldType; -import java.io.File; import java.nio.file.Path; import java.util.Locale; import java.util.concurrent.CompletableFuture; @@ -25,7 +24,7 @@ final class WorldsProviderBackend implements WorldLifecycleBackend { @SuppressWarnings("unchecked") public CompletableFuture create(WorldLifecycleRequest request) { try { - Path worldPath = new File(Bukkit.getWorldContainer(), request.worldName()).toPath(); + Path worldPath = IrisWorldStorage.dimensionRoot(request.worldKey()).toPath(); Object builder = WorldLifecycleSupport.invokeNamed(capabilities.worldsProvider(), "levelBuilder", new Class[]{Path.class}, worldPath); builder = WorldLifecycleSupport.invokeNamed(builder, "name", new Class[]{String.class}, request.worldName()); builder = WorldLifecycleSupport.invokeNamed(builder, "seed", new Class[]{long.class}, request.seed()); diff --git a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java index b4bcc7ddd..fabdb56fe 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.runtime; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisServices; import art.arcane.iris.core.lifecycle.WorldLifecycleService; @@ -10,6 +11,7 @@ import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.exceptions.IrisException; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.io.IO; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -327,7 +329,7 @@ public final class StudioOpenCoordinator { } for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) { - World familyWorld = Bukkit.getWorld(familyWorldName); + World familyWorld = WorldIdentity.resolve(IrisWorldStorage.keyFromLegacyName(familyWorldName)).orElse(null); if (familyWorld == null) { continue; } @@ -342,10 +344,9 @@ public final class StudioOpenCoordinator { return new WorldFamilyDeleteResult(true, false); } - File container = Bukkit.getWorldContainer(); boolean liveDeleted = true; for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) { - File folder = new File(container, familyWorldName); + File folder = IrisWorldStorage.dimensionRoot(familyWorldName); if (!folder.exists()) { continue; } @@ -378,15 +379,14 @@ public final class StudioOpenCoordinator { } private void cleanupStaleTransientWorlds(String worldName) { - File container = Bukkit.getWorldContainer(); - LinkedHashSet staleWorldNames = TransientWorldCleanupSupport.collectTransientStudioWorldNames(container); + LinkedHashSet staleWorldNames = TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot()); String requestedBaseName = TransientWorldCleanupSupport.transientStudioBaseWorldName(worldName); if (requestedBaseName != null) { staleWorldNames.add(requestedBaseName); } for (String staleWorldName : staleWorldNames) { - if (Bukkit.getWorld(staleWorldName) != null) { + if (WorldIdentity.resolve(IrisWorldStorage.keyFromLegacyName(staleWorldName)).isPresent()) { continue; } @@ -480,7 +480,7 @@ public final class StudioOpenCoordinator { } for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) { - if (Bukkit.getWorld(familyWorldName) != null) { + if (WorldIdentity.resolve(IrisWorldStorage.keyFromLegacyName(familyWorldName)).isPresent()) { return true; } } diff --git a/core/src/main/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupport.java b/core/src/main/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupport.java index 9549ba9dd..3ac0d6680 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupport.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupport.java @@ -49,31 +49,38 @@ public final class TransientWorldCleanupSupport { return names; } - public static LinkedHashSet collectTransientStudioWorldNames(File worldContainer) { + public static LinkedHashSet collectTransientStudioWorldNames(File levelRoot) { LinkedHashSet names = new LinkedHashSet<>(); - if (worldContainer == null) { + if (levelRoot == null) { return names; } - File[] children = worldContainer.listFiles(); - if (children == null) { + File namespacesRoot = new File(levelRoot, "dimensions"); + File[] namespaces = namespacesRoot.listFiles(File::isDirectory); + if (namespaces == null) { return names; } + for (File namespace : namespaces) { + collectTransientStudioWorldNames(namespace, names); + } + return names; + } + + private static void collectTransientStudioWorldNames(File folder, LinkedHashSet names) { + File[] children = folder.listFiles(File::isDirectory); + if (children == null) { + return; + } + for (File child : children) { - if (child == null || !child.isDirectory()) { - continue; - } - String baseName = transientStudioBaseWorldName(child.getName()); - if (baseName == null) { + if (baseName != null) { + names.add(baseName); continue; } - - names.add(baseName); + collectTransientStudioWorldNames(child, names); } - - return names; } private static String normalizeWorldName(String worldName) { diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java b/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java index ab16e2709..78d1f5217 100644 --- a/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java +++ b/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java @@ -3,6 +3,7 @@ package art.arcane.iris.core.safeguard.task; import art.arcane.iris.BuildConstants; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.IrisWorlds; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.nms.v1X.NMSBinding1X; import art.arcane.iris.core.safeguard.Mode; @@ -137,7 +138,7 @@ public final class Tasks { }); private static final Task DISK_SPACE = Task.of("diskSpace", () -> { - double freeGiB = server().getWorldContainer().getFreeSpace() / (double) 0x4000_0000; + double freeGiB = IrisWorldStorage.levelRoot().getFreeSpace() / (double) 0x4000_0000; if (freeGiB > 3.0) { return withDiagnostics(Mode.STABLE); } diff --git a/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java b/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java index 4e37a3602..d7bc1b041 100644 --- a/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java @@ -1,8 +1,10 @@ package art.arcane.iris.core.service; import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.pregenerator.cache.PregenCache; import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.collection.KMap; import art.arcane.iris.util.common.plugin.IrisService; import art.arcane.volmlib.util.scheduling.Looper; @@ -64,7 +66,7 @@ public class GlobalCacheSVC implements IrisService { @Nullable public PregenCache get(@NonNull World world) { - return globalCache.get(world.getName()); + return globalCache.get(WorldIdentity.serialize(world)); } @Nullable @@ -80,7 +82,7 @@ public class GlobalCacheSVC implements IrisService { @EventHandler(priority = EventPriority.MONITOR) public void on(WorldUnloadEvent event) { - var cache = globalCache.remove(event.getWorld().getName()); + PregenCache cache = globalCache.remove(WorldIdentity.serialize(event.getWorld())); if (cache == null) return; cache.write(); } @@ -94,7 +96,8 @@ public class GlobalCacheSVC implements IrisService { private void createCache(World world) { if (!IrisToolbelt.isIrisWorld(world)) return; - globalCache.computeIfAbsent(world.getName(), GlobalCacheSVC::createDefault); + String worldIdentity = WorldIdentity.serialize(world); + globalCache.computeIfAbsent(worldIdentity, GlobalCacheSVC::createDefault); } private boolean isDisabled() { @@ -116,25 +119,26 @@ public class GlobalCacheSVC implements IrisService { @NonNull - public static PregenCache createCache(@NonNull String worldName, @NonNull Function provider) { + public static PregenCache createCache(@NonNull String worldIdentity, @NonNull Function provider) { PregenCache[] holder = new PregenCache[1]; - REFERENCE_CACHE.compute(worldName, (name, ref) -> { + REFERENCE_CACHE.compute(worldIdentity, (identity, ref) -> { if (ref != null) { if ((holder[0] = ref.get()) != null) return ref; } - return new WeakReference<>(holder[0] = provider.apply(worldName)); + return new WeakReference<>(holder[0] = provider.apply(worldIdentity)); }); return holder[0]; } @NonNull - public static PregenCache createDefault(@NonNull String worldName) { - return createCache(worldName, GlobalCacheSVC::createDefault0); + public static PregenCache createDefault(@NonNull String worldIdentity) { + return createCache(worldIdentity, GlobalCacheSVC::createDefault0); } - private static PregenCache createDefault0(String worldName) { + private static PregenCache createDefault0(String worldIdentity) { if (disabled) return PregenCache.EMPTY; - return PregenCache.create(new File(Bukkit.getWorldContainer(), String.join(File.separator, worldName, "iris", "pregen"))).sync(); + File dimensionRoot = IrisWorldStorage.dimensionRoot(WorldIdentity.parse(worldIdentity)); + return PregenCache.create(new File(dimensionRoot, "iris/pregen")).sync(); } } diff --git a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java index 409d538d5..f87d4d326 100644 --- a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java @@ -22,6 +22,7 @@ import com.google.gson.JsonSyntaxException; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisServices; import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.lifecycle.WorldLifecycleService; import art.arcane.iris.core.loader.IrisData; @@ -86,7 +87,7 @@ public class StudioSVC implements IrisService { public void onDisable() { IrisLogging.debug("Studio Mode Active: Closing Projects"); boolean stopping = IrisToolbelt.isServerStopping(); - LinkedHashSet worldNamesToDelete = new LinkedHashSet<>(TransientWorldCleanupSupport.collectTransientStudioWorldNames(Bukkit.getWorldContainer())); + LinkedHashSet worldNamesToDelete = new LinkedHashSet<>(TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())); if (activeProject != null) { PlatformChunkGenerator activeProvider = activeProject.getActiveProvider(); @@ -425,9 +426,8 @@ public class StudioSVC implements IrisService { return; } - File container = Bukkit.getWorldContainer(); for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) { - File folder = new File(container, familyWorldName); + File folder = IrisWorldStorage.dimensionRoot(familyWorldName); if (!folder.exists()) { continue; } diff --git a/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java b/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java index de342582e..dac34f575 100644 --- a/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java +++ b/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java @@ -18,6 +18,7 @@ package art.arcane.iris.core.structure; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; @@ -28,6 +29,7 @@ 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.collection.KList; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.io.IO; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -293,11 +295,11 @@ public final class FeatureImporter { static World createScratchWorld(VolmitSender sender) { try { - World existing = Bukkit.getWorld(SCRATCH_WORLD_NAME); + World existing = WorldIdentity.resolve(IrisWorldStorage.keyFromLegacyName(SCRATCH_WORLD_NAME)).orElse(null); if (existing != null) { return existing; } - WorldCreator creator = new WorldCreator(SCRATCH_WORLD_NAME) + WorldCreator creator = WorldCreator.ofKey(IrisWorldStorage.keyFromLegacyName(SCRATCH_WORLD_NAME)) .environment(World.Environment.NORMAL) .type(WorldType.FLAT) .generateStructures(false); diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index 4c05d4c9d..4eaaa83c7 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -25,6 +25,7 @@ import art.arcane.iris.spi.IrisServices; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.link.MultiverseCoreLink; import art.arcane.iris.core.IrisRuntimeSchedulerMode; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.ServerConfigurator; @@ -50,7 +51,6 @@ import org.bukkit.WorldCreator; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; -import java.io.File; import java.io.IOException; import java.util.Locale; import java.util.concurrent.CompletableFuture; @@ -166,7 +166,7 @@ public class IrisCreator { reportStudioProgress(0.16D, "prepare_world_pack"); if (!studio() || benchmark) { - d = IrisServices.get(StudioSVC.class).installIntoWorld(sender, d, new File(Bukkit.getWorldContainer(), name())); + d = IrisServices.get(StudioSVC.class).installIntoWorld(sender, d, IrisWorldStorage.dimensionRoot(name())); if (d == null) { throw new IrisException("Failed to install dimension pack for " + dimension()); } @@ -188,7 +188,7 @@ public class IrisCreator { .studio(studio) .create(); if (!studio()) { - IrisWorlds.get().put(name(), dimension()); + IrisWorlds.get().put(wc.key().toString(), dimension()); } ServerConfigurator.installDataPacksIfChanged(!studio()); IrisLogging.debug("[Studio timing] create.packPrep + datapacks = " + (System.currentTimeMillis() - createStart) + "ms (cumulative in create)"); diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java b/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java index 02a4534bc..9f7276d7c 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java @@ -1,6 +1,7 @@ package art.arcane.iris.core.tools; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.core.lifecycle.WorldLifecycleService; @@ -9,12 +10,13 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.exceptions.IrisException; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.io.IO; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; -import org.bukkit.Bukkit; +import org.bukkit.World; import java.io.File; import java.io.FileWriter; @@ -47,9 +49,9 @@ public class IrisPackBenchmarking { .name("PackBenchmarking") .start(() -> { IrisLogging.info("Setting up benchmark environment "); - IO.delete(new File(Bukkit.getWorldContainer(), "benchmark")); + IO.delete(IrisWorldStorage.dimensionRoot("benchmark")); createBenchmark(); - while (!IrisToolbelt.isIrisWorld(Bukkit.getWorld("benchmark"))) { + while (!IrisToolbelt.isIrisWorld(benchmarkWorld())) { J.sleep(1000); IrisLogging.debug("Iris PackBenchmark: Waiting..."); } @@ -63,7 +65,11 @@ public class IrisPackBenchmarking { public void finishedBenchmark(KList cps) { try { String time = Form.duration((long) stopwatch.getMilliseconds()); - Engine engine = IrisToolbelt.access(Bukkit.getWorld("benchmark")).getEngine(); + World benchmarkWorld = benchmarkWorld(); + if (benchmarkWorld == null) { + throw new IllegalStateException("Benchmark world is not loaded."); + } + Engine engine = IrisToolbelt.access(benchmarkWorld).getEngine(); IrisLogging.info("-----------------"); IrisLogging.info("Results:"); IrisLogging.info("- Total time: " + time); @@ -101,7 +107,7 @@ public class IrisPackBenchmarking { } J.s(() -> { - org.bukkit.World world = Bukkit.getWorld("benchmark"); + World world = benchmarkWorld(); if (world == null) return; IrisToolbelt.evacuate(world); WorldLifecycleService.get().unload(world, true); @@ -136,13 +142,17 @@ public class IrisPackBenchmarking { .gui(gui) .radiusX(radius) .radiusZ(radius) - .build(), Bukkit.getWorld("benchmark") + .build(), benchmarkWorld() ); } finally { instance.remove(); } } + private static World benchmarkWorld() { + return WorldIdentity.resolve(IrisWorldStorage.keyFromLegacyName("benchmark")).orElse(null); + } + private double calculateAverage(KList list) { double sum = 0; for (int num : list) { diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java index f9cd16dad..c0d1c3617 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java @@ -37,10 +37,12 @@ import art.arcane.iris.core.service.GlobalCacheSVC; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.engine.platform.BukkitChunkGenerator; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import io.papermc.lib.PaperLib; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -222,14 +224,14 @@ public class IrisToolbelt { return ((PlatformChunkGenerator) world.getGenerator()); } - StudioSVC studioService = IrisServices.get(StudioSVC.class); + StudioSVC studioService = IrisServices.getOrNull(StudioSVC.class); if (studioService != null && studioService.isProjectOpen()) { IrisProject activeProject = studioService.getActiveProject(); if (activeProject != null) { PlatformChunkGenerator activeProvider = activeProject.getActiveProvider(); if (activeProvider != null) { World activeWorld = activeProvider.getTarget().getWorld().realWorld(); - if (activeWorld != null && activeWorld.getName().equals(world.getName())) { + if (activeWorld != null && WorldIdentity.key(activeWorld).equals(WorldIdentity.key(world))) { if (activeProvider instanceof BukkitChunkGenerator bukkit) { bukkit.touch(world); } @@ -267,14 +269,18 @@ public class IrisToolbelt { IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen()); useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA; } - return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, resolvePregenCache(engine.getWorld().name()), task) : method, engine); + return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, resolvePregenCache(engine.getWorld()), task) : method, engine); } - private static PregenCache resolvePregenCache(String worldName) { - PregenCache cache = IrisServices.get(GlobalCacheSVC.class).get(worldName); + private static PregenCache resolvePregenCache(IrisWorld world) { + String worldIdentity = world.key() == null ? null : world.key().toString(); + if (worldIdentity == null) { + throw new IllegalStateException("Pregeneration requires a namespaced world identity."); + } + PregenCache cache = IrisServices.get(GlobalCacheSVC.class).get(worldIdentity); if (cache == null) { - IrisLogging.debug("Could not find existing cache for " + worldName + " creating fallback"); - cache = GlobalCacheSVC.createDefault(worldName); + IrisLogging.debug("Could not find existing cache for " + worldIdentity + " creating fallback"); + cache = GlobalCacheSVC.createDefault(worldIdentity); } return cache; } @@ -351,7 +357,7 @@ public class IrisToolbelt { } for (World i : Bukkit.getWorlds()) { - if (!i.getName().equals(world.getName())) { + if (!WorldIdentity.key(i).equals(WorldIdentity.key(world))) { for (Player j : new ArrayList<>(world.getPlayers())) { new VolmitSender(j, BukkitPlatform.volmitPlugin().getTag()).sendMessage("You have been evacuated from this world."); Location target = i.getSpawnLocation(); @@ -381,7 +387,7 @@ public class IrisToolbelt { } for (World i : Bukkit.getWorlds()) { - if (!i.getName().equals(world.getName())) { + if (!WorldIdentity.key(i).equals(WorldIdentity.key(world))) { for (Player j : new ArrayList<>(world.getPlayers())) { new VolmitSender(j, BukkitPlatform.volmitPlugin().getTag()).sendMessage("You have been evacuated from this world. " + m); Location target = i.getSpawnLocation(); @@ -460,7 +466,7 @@ public class IrisToolbelt { return; } - beginWorldMaintenance(world.getName(), reason, bypassMantleStages); + beginWorldMaintenance(WorldIdentity.serialize(world), reason, bypassMantleStages); } public static void beginWorldMaintenance(String worldName, String reason) { @@ -476,7 +482,7 @@ public class IrisToolbelt { return; } - endWorldMaintenance(world.getName(), reason); + endWorldMaintenance(WorldIdentity.serialize(world), reason); } public static void endWorldMaintenance(String worldName, String reason) { @@ -484,7 +490,7 @@ public class IrisToolbelt { } public static boolean isWorldMaintenanceActive(World world) { - return world != null && isWorldMaintenanceActive(world.getName()); + return world != null && isWorldMaintenanceActive(WorldIdentity.serialize(world)); } public static boolean isWorldMaintenanceActive(String worldName) { @@ -492,7 +498,7 @@ public class IrisToolbelt { } public static boolean isWorldMaintenanceBypassingMantleStages(World world) { - return world != null && isWorldMaintenanceBypassingMantleStages(world.getName()); + return world != null && isWorldMaintenanceBypassingMantleStages(WorldIdentity.serialize(world)); } public static boolean isWorldMaintenanceBypassingMantleStages(String worldName) { diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java index c4397efb3..5378c02c9 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java @@ -18,11 +18,12 @@ package art.arcane.iris.core.tools; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.engine.platform.BukkitChunkGenerator; -import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.WorldCreator; import org.bukkit.generator.ChunkGenerator; @@ -74,13 +75,15 @@ public class IrisWorldCreator { public WorldCreator create() { IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension; + NamespacedKey worldKey = IrisWorldStorage.keyFromLegacyName(name); IrisWorld w = IrisWorld.builder() + .key(worldKey) .name(name) .minHeight(dim.getMinHeight()) .maxHeight(dim.getMaxHeight()) .seed(seed) - .worldFolder(new File(Bukkit.getWorldContainer(), name)) + .worldFolder(IrisWorldStorage.dimensionRoot(worldKey)) .environment(findEnvironment()) .build(); ChunkGenerator g = new BukkitChunkGenerator(w, studio, studio @@ -88,7 +91,7 @@ public class IrisWorldCreator { new File(w.worldFolder(), "iris/pack"), dimensionName); - return new WorldCreator(name) + return WorldCreator.ofKey(worldKey) .environment(w.environment()) .generateStructures(true) .generator(g).seed(seed); diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index 790d9a3fb..0b3f200b1 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -650,7 +650,7 @@ public class IrisEngine implements Engine { private boolean isPregeneratorActiveForThisWorld() { PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); - return pregeneratorJob != null && pregeneratorJob.targetsWorldName(getWorld().name()); + return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(getWorld().identity()); } private void savePrefetchOnce() { diff --git a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java index 7beef7b7b..0af5a2029 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -574,7 +574,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return false; } - return job.targetsWorldName(world.getName()); + return job.targetsWorld(world); } private Position2[] getLoadedChunkPositionsSnapshot(World world) { diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index ca4337a3d..33ddc2f17 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -691,11 +691,11 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat if (world == null) { return false; } - String worldName = world.name(); - if (!WorldMaintenance.isWorldMaintenanceActive(worldName)) { + String worldIdentity = world.identity(); + if (!WorldMaintenance.isWorldMaintenanceActive(worldIdentity)) { return false; } PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); - return pregeneratorJob == null || !pregeneratorJob.targetsWorldName(worldName); + return pregeneratorJob == null || !pregeneratorJob.targetsWorldIdentity(worldIdentity); } } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java index f8af5dac9..465cff9d1 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java @@ -96,13 +96,13 @@ public interface EngineMode extends Staged { } private boolean shouldDisableContextCacheForMaintenance() { - boolean maintenanceActive = WorldMaintenance.isWorldMaintenanceActive(getEngine().getWorld().name()); + boolean maintenanceActive = WorldMaintenance.isWorldMaintenanceActive(getEngine().getWorld().identity()); if (!maintenanceActive) { return false; } PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); - boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldName(getEngine().getWorld().name()); + boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(getEngine().getWorld().identity()); return shouldDisableContextCacheForMaintenance(maintenanceActive, pregeneratorTargetsWorld); } } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java index 1ff81fb3d..077c69089 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java @@ -77,7 +77,7 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { this.z = z; final boolean foliaMaintenance = J.isFolia() - && WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().name()); + && WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity()); final int parallelism = foliaMaintenance ? 1 : (multicore ? Runtime.getRuntime().availableProcessors() / 2 : 4); if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) { IrisLogging.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + "."); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java b/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java index a94ec3862..9a583dd6e 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java @@ -18,8 +18,10 @@ package art.arcane.iris.engine.object; +import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.collection.KList; import lombok.AccessLevel; import lombok.Builder; @@ -27,8 +29,8 @@ import lombok.Data; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; -import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -44,6 +46,8 @@ import java.util.List; public class IrisWorld { private static final KList NO_PLAYERS = new KList<>(); private static final KList NO_ENTITIES = new KList<>(); + private NamespacedKey key; + private String platformIdentity; private String name; private File worldFolder; @@ -60,7 +64,8 @@ public class IrisWorld { } private static IrisWorld bindWorld(IrisWorld iw, World world) { - return iw.name(world.getName()) + return iw.key(WorldIdentity.key(world)) + .name(world.getName()) .worldFolder(world.getWorldFolder()) .minHeight(world.getMinHeight()) .maxHeight(world.getMaxHeight()) @@ -72,6 +77,10 @@ public class IrisWorld { return seed; } + public String identity() { + return key == null ? platformIdentity : key.toString(); + } + public void setRawWorldSeed(long seed) { this.seed = seed; } @@ -81,7 +90,11 @@ public class IrisWorld { return true; } - World w = Bukkit.getWorld(name); + if (key == null) { + return false; + } + + World w = WorldIdentity.resolve(key).orElse(null); if (w != null) { realWorld = w; @@ -111,8 +124,8 @@ public class IrisWorld { } public void bind(WorldInfo worldInfo) { - name(worldInfo.getName()) - .worldFolder(new File(Bukkit.getWorldContainer(), worldInfo.getName())) + key(WorldIdentity.key(worldInfo)) + .worldFolder(IrisWorldStorage.dimensionRoot(worldInfo)) .minHeight(worldInfo.getMinHeight()) .maxHeight(worldInfo.getMaxHeight()) .environment(worldInfo.getEnvironment()); diff --git a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java index ac84e66a9..d9300d06f 100644 --- a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java @@ -50,6 +50,7 @@ import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.M; import art.arcane.iris.util.project.hunk.Hunk; @@ -77,6 +78,7 @@ import java.io.File; import java.io.PrintWriter; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.Random; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -136,8 +138,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun @EventHandler(priority = EventPriority.LOWEST) public void onWorldInit(WorldInitEvent event) { - if (!world.name().equals(event.getWorld().getName())) return; + if (!Objects.equals(world.key(), WorldIdentity.key(event.getWorld()))) return; BukkitPlatform.volmitPlugin().unregisterListener(this); + world.bind(event.getWorld()); world.setRawWorldSeed(event.getWorld().getSeed()); if (initialize(event.getWorld())) return; @@ -165,7 +168,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun } spawnChunks.complete(INMS.get().getSpawnChunkCount(world)); BukkitPlatform.volmitPlugin().unregisterListener(this); - IrisWorlds.get().put(world.getName(), dimensionKey); + IrisWorlds.get().put(WorldIdentity.serialize(world), dimensionKey); return true; } @@ -524,7 +527,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun World realWorld = this.world.realWorld(); PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); - return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorldName(realWorld.getName()); + return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorld(realWorld); } @Override diff --git a/core/src/main/java/art/arcane/iris/util/common/misc/SlimJar.java b/core/src/main/java/art/arcane/iris/util/common/misc/SlimJar.java index 19880ee6a..9d326ac5e 100644 --- a/core/src/main/java/art/arcane/iris/util/common/misc/SlimJar.java +++ b/core/src/main/java/art/arcane/iris/util/common/misc/SlimJar.java @@ -10,12 +10,13 @@ import io.github.slimjar.logging.ProcessLogger; import org.jetbrains.annotations.NotNull; 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"); - private static final boolean DISABLE_REMAPPER = Boolean.getBoolean("iris.disable-remapper"); private static final ReentrantLock lock = new ReentrantLock(); private static final AtomicBoolean loaded = new AtomicBoolean(); @@ -26,16 +27,15 @@ public class SlimJar { try { if (loaded.getAndSet(true)) return; - final VolmitPlugin plugin = BukkitPlatform.volmitPlugin(); - final var downloadPath = plugin.getDataFolder("cache", "libraries").toPath(); - final var logger = plugin.getLogger(); + VolmitPlugin plugin = BukkitPlatform.volmitPlugin(); + Path downloadPath = plugin.getDataFolder("cache", "libraries").toPath(); + Logger logger = plugin.getLogger(); logger.info("Loading libraries..."); try { new SpigotApplicationBuilder(plugin) .downloadDirectoryPath(downloadPath) .debug(DEBUG) - .remap(!DISABLE_REMAPPER) .build(); } catch (Throwable e) { IrisLogging.warn("Failed to inject the library loader, falling back to application builder"); diff --git a/core/src/main/java/art/arcane/iris/util/common/nbt/mca/NBTWorld.java b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/NBTWorld.java index 57c86b0c4..9f916ce4f 100644 --- a/core/src/main/java/art/arcane/iris/util/common/nbt/mca/NBTWorld.java +++ b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/NBTWorld.java @@ -138,8 +138,8 @@ public class NBTWorld { Map biomeIds = new KMap<>(); for (Biome biome : Registry.BIOME) { - NamespacedKey key = biome.getKeyOrNull(); - if (key != null && !key.getKey().equals("custom")) { + NamespacedKey key = biome.getKey(); + if (!key.getKey().equals("custom")) { biomeIds.put(biome, INMS.get().getBiomeId(biome)); } } diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/CommandDummy.java b/core/src/main/java/art/arcane/iris/util/common/plugin/CommandDummy.java index 9263a64d3..a0fe84bb7 100644 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/CommandDummy.java +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/CommandDummy.java @@ -18,6 +18,7 @@ package art.arcane.iris.util.common.plugin; +import net.kyori.adventure.text.Component; import org.bukkit.Server; import org.bukkit.command.CommandSender; import org.bukkit.permissions.Permission; @@ -63,6 +64,12 @@ public class CommandDummy implements CommandSender { return null; } + @NotNull + @Override + public Component name() { + return Component.empty(); + } + @NotNull @Override public Spigot spigot() { diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java index ad2093477..114950750 100644 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java @@ -46,6 +46,7 @@ import org.bukkit.entity.Player; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionAttachment; import org.bukkit.permissions.PermissionAttachmentInfo; +import org.jetbrains.annotations.NotNull; import org.bukkit.plugin.Plugin; import java.util.Set; @@ -507,6 +508,12 @@ public class VolmitSender implements CommandSender { return s.getName(); } + @NotNull + @Override + public Component name() { + return s.name(); + } + @Override public Spigot spigot() { return s.spigot(); diff --git a/core/src/test/java/art/arcane/iris/core/IrisWorldStorageTest.java b/core/src/test/java/art/arcane/iris/core/IrisWorldStorageTest.java new file mode 100644 index 000000000..2bd6438c5 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/IrisWorldStorageTest.java @@ -0,0 +1,51 @@ +package art.arcane.iris.core; + +import org.bukkit.NamespacedKey; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class IrisWorldStorageTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void derivesPaperDimensionRootFromNamespacedKey() throws Exception { + File levelRoot = temporaryFolder.newFolder("world"); + NamespacedKey key = new NamespacedKey("iris", "runtime/studio"); + + File dimensionRoot = IrisWorldStorage.dimensionRoot(levelRoot, key); + + assertEquals(new File(levelRoot, "dimensions/iris/runtime/studio").getAbsoluteFile(), dimensionRoot); + assertEquals(key, IrisWorldStorage.keyFromDimensionRoot(levelRoot, dimensionRoot).orElseThrow()); + } + + @Test + public void separatesLevelRootFromDimensionRoot() throws Exception { + File levelRoot = temporaryFolder.newFolder("world"); + File dimensionRoot = new File(levelRoot, "dimensions/minecraft/overworld"); + + assertEquals(levelRoot.getAbsoluteFile(), IrisWorldStorage.levelRoot(dimensionRoot)); + } + + @Test + public void mapsLegacyBukkitNamesToPaperKeys() { + assertEquals(NamespacedKey.minecraft("overworld"), IrisWorldStorage.keyFromLegacyName("world", "world")); + assertEquals(NamespacedKey.minecraft("the_nether"), IrisWorldStorage.keyFromLegacyName("world_nether", "world")); + assertEquals(NamespacedKey.minecraft("the_end"), IrisWorldStorage.keyFromLegacyName("world_the_end", "world")); + assertEquals(NamespacedKey.minecraft("iris_world"), IrisWorldStorage.keyFromLegacyName("Iris World", "world")); + } + + @Test + public void rejectsKeysThatEscapeNamespaceStorage() throws Exception { + File levelRoot = temporaryFolder.newFolder("world"); + NamespacedKey key = NamespacedKey.minecraft("../outside"); + + assertThrows(IllegalArgumentException.class, () -> IrisWorldStorage.dimensionRoot(levelRoot, key)); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleDiagnosticsTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleDiagnosticsTest.java index 6f0603606..69a7e07ff 100644 --- a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleDiagnosticsTest.java +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleDiagnosticsTest.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.lifecycle; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.junit.Test; @@ -15,7 +16,7 @@ public class WorldLifecycleDiagnosticsTest { @Test public void studioCreateSelectionFailurePrintsFullStacktrace() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, false)); - WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); + WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); ByteArrayOutputStream output = new ByteArrayOutputStream(); PrintStream originalErr = System.err; diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java index 3358c7587..3f72b9a75 100644 --- a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java @@ -5,6 +5,7 @@ import art.arcane.iris.core.nms.INMSBinding; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineTarget; import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.generator.ChunkGenerator; import org.junit.Test; @@ -26,6 +27,7 @@ public class WorldLifecycleRuntimeLevelStemTest { CapabilitySnapshot capabilities = CapabilitySnapshot.forTestingRuntimeRegistries(ServerFamily.PURPUR, false, datapackDimensions, serverRegistryAccess); WorldLifecycleRequest request = new WorldLifecycleRequest( "studio", + NamespacedKey.minecraft("studio"), World.Environment.NORMAL, new TestingPlatformChunkGenerator(), null, diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleSelectionTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleSelectionTest.java index c9da0f867..5483e06df 100644 --- a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleSelectionTest.java +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleSelectionTest.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.lifecycle; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.junit.Test; @@ -9,7 +10,7 @@ public class WorldLifecycleSelectionTest { @Test public void studioSelectsPaperLikeBackendOnPaper() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, true)); - WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); + WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); } @@ -17,7 +18,7 @@ public class WorldLifecycleSelectionTest { @Test public void studioSelectsPaperLikeBackendOnPurpur() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true)); - WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); + WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); } @@ -25,7 +26,7 @@ public class WorldLifecycleSelectionTest { @Test public void studioSelectsPaperLikeBackendOnCanvas() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.CANVAS, true, false, true)); - WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); + WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); } @@ -33,7 +34,7 @@ public class WorldLifecycleSelectionTest { @Test public void studioSelectsPaperLikeBackendOnFolia() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, true)); - WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); + WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); } @@ -41,7 +42,7 @@ public class WorldLifecycleSelectionTest { @Test public void studioSelectsBukkitBackendOnSpigot() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.SPIGOT, false, false, false)); - WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); + WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); assertEquals("bukkit_public", service.selectCreateBackend(request).backendName()); } @@ -49,7 +50,7 @@ public class WorldLifecycleSelectionTest { @Test public void persistentCreatePrefersBukkitBackendOnPaperLikeServers() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true)); - WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); + WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", NamespacedKey.minecraft("persistent"), World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); assertEquals("bukkit_public", service.selectCreateBackend(request).backendName()); } @@ -57,7 +58,7 @@ public class WorldLifecycleSelectionTest { @Test public void persistentCreateSelectsPaperLikeBackendOnFolia() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, true)); - WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); + WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", NamespacedKey.minecraft("persistent"), World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); } @@ -65,7 +66,7 @@ public class WorldLifecycleSelectionTest { @Test public void persistentCreateSelectsPaperLikeBackendOnCanvas() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.CANVAS, true, false, true)); - WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); + WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", NamespacedKey.minecraft("persistent"), World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); } @@ -73,7 +74,7 @@ public class WorldLifecycleSelectionTest { @Test public void persistentCreateFallsBackToBukkitBackendOnFoliaWithoutRuntime() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, false)); - WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); + WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", NamespacedKey.minecraft("persistent"), World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); assertEquals("bukkit_public", service.selectCreateBackend(request).backendName()); } @@ -82,7 +83,7 @@ public class WorldLifecycleSelectionTest { public void unloadUsesRememberedBackendFamily() { WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true)); - service.rememberBackend("studio", "paper_like_runtime"); - assertEquals("paper_like_runtime", service.selectUnloadBackend("studio").backendName()); + service.rememberBackend(NamespacedKey.minecraft("studio"), "paper_like_runtime"); + assertEquals("paper_like_runtime", service.selectUnloadBackend("minecraft:studio").backendName()); } } diff --git a/core/src/test/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupportTest.java b/core/src/test/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupportTest.java index 8782ad64f..231da998d 100644 --- a/core/src/test/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupportTest.java +++ b/core/src/test/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupportTest.java @@ -39,10 +39,11 @@ public class TransientWorldCleanupSupportTest { public void collectsOnlyTransientStudioWorldFamiliesFromContainer() throws IOException { File container = Files.createTempDirectory("transient-world-cleanup-test").toFile(); String baseName = "iris-123e4567-e89b-12d3-a456-426614174000"; - File baseFolder = new File(container, baseName); - File netherFolder = new File(container, baseName + "_nether"); - File smokeFolder = new File(container, "iris-smoke-studio-deadbeef"); - File regularFolder = new File(container, "overworld"); + File namespace = new File(container, "dimensions/minecraft"); + File baseFolder = new File(namespace, baseName); + File netherFolder = new File(namespace, baseName + "_nether"); + File smokeFolder = new File(namespace, "iris-smoke-studio-deadbeef"); + File regularFolder = new File(namespace, "overworld"); baseFolder.mkdirs(); netherFolder.mkdirs(); smokeFolder.mkdirs(); @@ -57,6 +58,8 @@ public class TransientWorldCleanupSupportTest { Files.deleteIfExists(netherFolder.toPath()); Files.deleteIfExists(smokeFolder.toPath()); Files.deleteIfExists(regularFolder.toPath()); + Files.deleteIfExists(namespace.toPath()); + Files.deleteIfExists(namespace.getParentFile().toPath()); Files.deleteIfExists(container.toPath()); } } diff --git a/core/src/test/java/art/arcane/iris/core/tools/IrisToolbeltPackReferenceTest.java b/core/src/test/java/art/arcane/iris/core/tools/IrisToolbeltPackReferenceTest.java index 5fd1cecb2..0d20aa104 100644 --- a/core/src/test/java/art/arcane/iris/core/tools/IrisToolbeltPackReferenceTest.java +++ b/core/src/test/java/art/arcane/iris/core/tools/IrisToolbeltPackReferenceTest.java @@ -1,13 +1,24 @@ package art.arcane.iris.core.tools; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.spi.IrisServices; +import org.bukkit.World; +import org.junit.After; import org.junit.Test; +import java.lang.reflect.Proxy; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; public class IrisToolbeltPackReferenceTest { + @After + public void cleanServices() { + IrisServices.remove(StudioSVC.class); + } + @Test public void plainPackUsesMatchingDimensionKey() { IrisToolbelt.PackReference reference = IrisToolbelt.parsePackReference("overworld"); @@ -34,4 +45,16 @@ public class IrisToolbeltPackReferenceTest { assertNull(IrisToolbelt.parsePackReference("pack:")); assertNull(IrisToolbelt.parsePackReference(":dimension")); } + + @Test + public void accessReturnsNullAfterStudioServiceIsRemoved() { + IrisServices.remove(StudioSVC.class); + World world = (World) Proxy.newProxyInstance( + World.class.getClassLoader(), + new Class[]{World.class}, + (proxy, method, args) -> null + ); + + assertNull(IrisToolbelt.access(world)); + } } diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisWorldIdentityTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisWorldIdentityTest.java new file mode 100644 index 000000000..d05475bb2 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisWorldIdentityTest.java @@ -0,0 +1,27 @@ +package art.arcane.iris.engine.object; + +import org.bukkit.NamespacedKey; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class IrisWorldIdentityTest { + @Test + public void usesBukkitKeyBeforePlatformIdentity() { + IrisWorld world = IrisWorld.builder() + .key(new NamespacedKey("iris", "bukkit")) + .platformIdentity("iris:modded") + .build(); + + assertEquals("iris:bukkit", world.identity()); + } + + @Test + public void usesPlatformIdentityWithoutBukkitKey() { + IrisWorld world = IrisWorld.builder() + .platformIdentity("minecraft:the_nether") + .build(); + + assertEquals("minecraft:the_nether", world.identity()); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1437bf293..583a24b14 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ grgit = "5.3.2" # https://github.com/ajoberstar/grgit # Core Libraries lombok = "1.18.44" spigot = "26.2-R0.1-SNAPSHOT" # https://hub.spigotmc.org/nexus/repository/snapshots/org/spigotmc/spigot-api/maven-metadata.xml +paper-api = "26.2.build.60-beta" log4j = "2.19.0" # https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api adventure-api = "5.1.1" # https://github.com/KyoriPowered/adventure adventure-platform = "4.4.1" # https://github.com/KyoriPowered/adventure-platform @@ -71,6 +72,7 @@ fabricApi-keyMapping = "2.0.5+e2bdee789e" # Core Libraries lombok = { module = "org.projectlombok:lombok", version.ref ="lombok" } spigot = { module = "org.spigotmc:spigot-api", version.ref = "spigot" } +paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper-api" } log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" } diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java index 7153ff4b1..100700a4a 100644 --- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java @@ -155,6 +155,7 @@ public final class GenerationProbe { } IrisWorld world = IrisWorld.builder() + .platformIdentity("iris:probe") .name("probe") .seed(SEED) .worldFolder(new File(workRoot, "world"))