diff --git a/src/main/java/org/mvplugins/multiverse/core/world/LoadedMultiverseWorld.java b/src/main/java/org/mvplugins/multiverse/core/world/LoadedMultiverseWorld.java index ea285cbae..be209a3f3 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/LoadedMultiverseWorld.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/LoadedMultiverseWorld.java @@ -13,7 +13,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import org.mvplugins.multiverse.core.config.CoreConfig; +import org.mvplugins.multiverse.core.MultiverseCore; import org.mvplugins.multiverse.core.teleportation.BlockSafety; import org.mvplugins.multiverse.core.teleportation.LocationManipulation; import org.mvplugins.multiverse.core.world.entity.EntityPurger; @@ -22,6 +22,11 @@ /** * Extension of {@link MultiverseWorld} that represents a world that is currently loaded with bukkit world object. + *

+ * Do not store instances of this class for future use during runtime. Worlds can be unloaded, removed, reloaded, + * or regenerated, making stored instances stale. Instead, store a {@link MultiverseWorldRef} obtained from + * {@link #asRef()} and call {@link MultiverseWorldRef#getLoaded()} when a loaded world is needed. The reference + * returns an empty Option when the world is unloaded or no longer managed by Multiverse. */ public final class LoadedMultiverseWorld extends MultiverseWorld { @@ -34,16 +39,13 @@ public final class LoadedMultiverseWorld extends MultiverseWorld { LoadedMultiverseWorld( @NotNull World world, @NotNull WorldConfig worldConfig, - @NotNull CoreConfig config, - @NotNull BlockSafety blockSafety, - @NotNull LocationManipulation locationManipulation, - @NotNull EntityPurger entityPurger - ) { - super(worldConfig, config); + @NotNull MultiverseCore multiverseCore + ) { + super(worldConfig, multiverseCore); this.worldUid = world.getUID(); - this.blockSafety = blockSafety; - this.locationManipulation = locationManipulation; - this.entityPurger = entityPurger; + this.blockSafety = multiverseCore.getServiceLocator().getService(BlockSafety.class); + this.locationManipulation = multiverseCore.getServiceLocator().getService(LocationManipulation.class); + this.entityPurger = multiverseCore.getServiceLocator().getService(EntityPurger.class); setupWorldConfig(world); setupSpawnLocation(world); diff --git a/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java b/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java index 81f7fe6f6..9f45817b2 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java @@ -20,6 +20,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; +import org.mvplugins.multiverse.core.MultiverseCore; import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle; import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; @@ -29,6 +30,11 @@ /** * Represents a world handled by Multiverse which has all the custom properties provided by Multiverse. + *

+ * Do not store instances of this class for future use during runtime. Worlds can be unloaded, removed, reloaded, + * or regenerated, making stored instances stale. Instead, store a {@link MultiverseWorldRef} obtained from + * {@link #asRef()} and retrieve the current world instance when needed. The reference returns an empty Option + * when the world is no longer available. */ public sealed class MultiverseWorld permits LoadedMultiverseWorld { /** @@ -37,11 +43,13 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld { WorldConfig worldConfig; protected final CoreConfig config; + private final WorldStore worldStore; private String colourlessAlias = ""; - MultiverseWorld(WorldConfig worldConfig, CoreConfig config) { + MultiverseWorld(@NotNull WorldConfig worldConfig, @NotNull MultiverseCore multiverseCore) { this.worldConfig = worldConfig; - this.config = config; + this.config = multiverseCore.getServiceLocator().getService(CoreConfig.class); + this.worldStore = multiverseCore.getServiceLocator().getService(WorldStore.class); this.worldConfig.setMVWorld(this); updateColourlessAlias(); } @@ -125,6 +133,22 @@ public Option asLoadedWorld() { .map(world -> (LoadedMultiverseWorld) world); } + /** + * Creates a reference to this world's key for storing and retrieving the current world instance at runtime. + *

+ * Store this reference when retaining a world across reloads or regeneration, which make stored + * {@link MultiverseWorld} and {@link LoadedMultiverseWorld} instances stale. Use {@link MultiverseWorldRef#get()} + * or {@link MultiverseWorldRef#getLoaded()} to retrieve the current instance when needed. + * + * @return A reference that looks up the current world instance using this world's key. + * + * @since 5.9 + */ + @ApiStatus.AvailableSince("5.9") + public MultiverseWorldRef asRef() { + return new MultiverseWorldRef(getKey(), worldStore); + } + /** * Gets the properties handler of this world. * diff --git a/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorldRef.java b/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorldRef.java new file mode 100644 index 000000000..e7761e4c5 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorldRef.java @@ -0,0 +1,57 @@ +package org.mvplugins.multiverse.core.world; + +import io.vavr.control.Option; +import org.bukkit.NamespacedKey; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A reference to a world by its key that can be stored and used to retrieve the current world instance at runtime. + *

+ * Stored {@link MultiverseWorld} and {@link LoadedMultiverseWorld} instances become stale when a world is reloaded + * or regenerated. This reference looks up the world on each call to {@link #get()} or {@link #getLoaded()}, allowing + * callers to keep the reference across those operations and retrieve the current instance when needed. + * Store this reference rather than caching the world instances returned by its methods. + *

+ * Obtain a reference using {@link MultiverseWorld#asRef()}. Resolving a reference does not load the world. + * + * @since 5.9 + */ +@ApiStatus.AvailableSince("5.9") +public class MultiverseWorldRef { + + private final NamespacedKey worldKey; + private final WorldStore worldStore; + + MultiverseWorldRef(@NotNull NamespacedKey worldKey, @NotNull WorldStore worldStore) { + this.worldKey = worldKey; + this.worldStore = worldStore; + } + + /** + * Gets the current world instance for this reference's key, whether loaded or unloaded. + * The world is looked up on each call so the returned instance reflects reloads and regeneration. + * + * @return The current world instance wrapped in an Option, or None if the world is no longer managed by Multiverse. + * + * @since 5.9 + */ + @ApiStatus.AvailableSince("5.9") + public Option get() { + return worldStore.getWorld(worldKey); + } + + /** + * Gets the current loaded world instance for this reference's key without loading the world. + * The world is looked up on each call so the returned instance reflects reloads and regeneration. + * + * @return The current loaded world instance wrapped in an Option, or None if the world is unloaded or no longer + * managed by Multiverse. + * + * @since 5.9 + */ + @ApiStatus.AvailableSince("5.9") + public Option getLoaded() { + return worldStore.getLoadedWorld(worldKey); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/world/WorldConfigNodes.java b/src/main/java/org/mvplugins/multiverse/core/world/WorldConfigNodes.java index b31040a31..8a45c0670 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldConfigNodes.java @@ -43,6 +43,7 @@ final class WorldConfigNodes { private final NodeGroup nodes = new NodeGroup(); private WorldManager worldManager; + private WorldStore worldStore; private EnforcementHandler enforcementHandler; private CoreConfig config; private AliasNameConflictChecker aliasNameConflictChecker; @@ -52,6 +53,7 @@ final class WorldConfigNodes { WorldConfigNodes(@NotNull MultiverseCore multiverseCore, @NotNull WorldKeyOrName keyOrName) { this.worldManager = multiverseCore.getServiceLocator().getService(WorldManager.class); + this.worldStore = multiverseCore.getServiceLocator().getService(WorldStore.class); this.enforcementHandler = multiverseCore.getServiceLocator().getService(EnforcementHandler.class); this.config = multiverseCore.getServiceLocator().getService(CoreConfig.class); this.aliasNameConflictChecker = multiverseCore.getServiceLocator().getService(AliasNameConflictChecker.class); @@ -100,7 +102,7 @@ private ConfigNode node(ConfigNode.Builder nodeBuilder) { final ConfigNode alias = node(ConfigNode.builder("alias", String.class) .defaultValue("") .onLoadAndChange((sender, oldValue, newValue) -> { - worldManager.getWorldStore().changeAlias( + worldStore.changeAlias( ChatTextFormatter.removeColor(oldValue), ChatTextFormatter.removeColor(newValue), keyOrName.usableKey() diff --git a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java index b574a54a4..cebe7b9b2 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java @@ -31,6 +31,7 @@ import org.jetbrains.annotations.Unmodifiable; import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.MultiverseCore; import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.event.world.MVWorldClonedEvent; import org.mvplugins.multiverse.core.event.world.MVWorldCreatedEvent; @@ -45,8 +46,6 @@ import org.mvplugins.multiverse.core.locale.message.Message; import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace; import org.mvplugins.multiverse.core.permissions.CorePermissions; -import org.mvplugins.multiverse.core.teleportation.BlockSafety; -import org.mvplugins.multiverse.core.teleportation.LocationManipulation; import org.mvplugins.multiverse.core.utils.ServerProperties; import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility; import org.mvplugins.multiverse.core.utils.compatibility.WorldCompatibility; @@ -56,7 +55,6 @@ import org.mvplugins.multiverse.core.utils.FileUtils; import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; import org.mvplugins.multiverse.core.world.biomeprovider.BiomeProviderFactory; -import org.mvplugins.multiverse.core.world.entity.EntityPurger; import org.mvplugins.multiverse.core.world.generators.GeneratorProvider; import org.mvplugins.multiverse.core.world.helpers.DataStore.GameRulesStore; import org.mvplugins.multiverse.core.world.helpers.DataTransfer; @@ -96,6 +94,7 @@ public final class WorldManager { "data/paper/metadata.dat" // New papermc format for 26.1+ ); + private final MultiverseCore multiverseCore; private final WorldStore worldStore; private final List unloadTracker; private final List loadTracker; @@ -104,44 +103,37 @@ public final class WorldManager { private final BiomeProviderFactory biomeProviderFactory; private final GeneratorProvider generatorProvider; private final FileUtils fileUtils; - private final BlockSafety blockSafety; - private final LocationManipulation locationManipulation; private final PluginManager pluginManager; private final CorePermissions corePermissions; private final ServerProperties serverProperties; private final CoreConfig config; - private final EntityPurger entityPurger; private final Provider potentialWorldFinder; @Inject WorldManager( + @NotNull MultiverseCore multiverseCore, @NotNull WorldStore worldStore, @NotNull WorldsConfigManager worldsConfigManager, @NotNull WorldNameChecker worldNameChecker, @NotNull BiomeProviderFactory biomeProviderFactory, @NotNull GeneratorProvider generatorProvider, @NotNull FileUtils fileUtils, - @NotNull BlockSafety blockSafety, - @NotNull LocationManipulation locationManipulation, @NotNull PluginManager pluginManager, @NotNull CorePermissions corePermissions, @NotNull ServerProperties serverProperties, @NotNull CoreConfig config, - @NotNull EntityPurger entityPurger, @NotNull Provider potentialWorldFinder) { + this.multiverseCore = multiverseCore; this.worldStore = worldStore; this.worldsConfigManager = worldsConfigManager; this.worldNameChecker = worldNameChecker; this.biomeProviderFactory = biomeProviderFactory; this.generatorProvider = generatorProvider; this.fileUtils = fileUtils; - this.blockSafety = blockSafety; - this.locationManipulation = locationManipulation; this.pluginManager = pluginManager; this.corePermissions = corePermissions; this.serverProperties = serverProperties; this.config = config; - this.entityPurger = entityPurger; this.potentialWorldFinder = potentialWorldFinder; this.unloadTracker = new ArrayList<>(); @@ -417,7 +409,7 @@ private Attempt doImportBukkitWorld( } private MultiverseWorld newMultiverseWorld(WorldConfig worldConfig) { - MultiverseWorld mvWorld = new MultiverseWorld(worldConfig, config); + MultiverseWorld mvWorld = new MultiverseWorld(worldConfig, multiverseCore); worldStore.putUnloadedWorld(mvWorld); corePermissions.addWorldPermissions(mvWorld); return mvWorld; @@ -456,10 +448,7 @@ private LoadedMultiverseWorld newLoadedMultiverseWorld( LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld( world, worldConfig, - config, - blockSafety, - locationManipulation, - entityPurger + multiverseCore ); worldStore.putLoadedWorld(loadedWorld); saveWorldsConfig(); @@ -592,10 +581,7 @@ private Attempt newLoadedMultiverseWor LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld( bukkitWorld, worldConfig, - config, - blockSafety, - locationManipulation, - entityPurger + multiverseCore ); worldStore.putLoadedWorld(loadedWorld); saveWorldsConfig(); @@ -1278,10 +1264,6 @@ public Try saveWorldsConfig() { }); } - WorldStore getWorldStore() { - return worldStore; - } - /** * A simple pair wrapper for convenience to pass both the world key or name and the options together between methods * when parsing world options. diff --git a/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt b/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt index 32edb3935..c3ef0ef24 100644 --- a/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt @@ -49,6 +49,48 @@ class WorldManagerTest : TestWithMockBukkit() { assertNotNull(world2) } + @Test + fun `World reference follows unload and reload`() { + val ref = world2.asRef() + assertSame(world2, ref.get().get()) + assertSame(world2, ref.getLoaded().get()) + + assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world2)).isSuccess) + assertTrue(ref.getLoaded().isEmpty) + val unloaded = ref.get().get() + assertFalse(unloaded.isLoaded) + assertSame(worldManager.getWorld(world2.name).get(), unloaded) + + val unloadedRef = unloaded.asRef() + assertTrue(worldManager.loadWorld(LoadWorldOptions.world(unloaded)).isSuccess) + val reloaded = worldManager.getLoadedWorld(world2.name).get() + assertNotSame(world2, reloaded) + assertSame(reloaded, ref.get().get()) + assertSame(reloaded, ref.getLoaded().get()) + assertSame(reloaded, unloadedRef.getLoaded().get()) + } + + @Test + fun `World reference follows regeneration`() { + val ref = world2.asRef() + assertTrue(worldManager.regenWorld(RegenWorldOptions.world(world2).seed(4321L)).isSuccess) + + val regenerated = ref.getLoaded().get() + assertNotSame(world2, regenerated) + assertSame(worldManager.getLoadedWorld(world2.name).get(), regenerated) + assertSame(regenerated, ref.get().get()) + assertEquals(4321L, regenerated.seed) + } + + @Test + fun `World reference is empty after removal`() { + val ref = world2.asRef() + assertTrue(worldManager.removeWorld(RemoveWorldOptions.world(world2)).isSuccess) + + assertTrue(ref.get().isEmpty) + assertTrue(ref.getLoaded().isEmpty) + } + @Test fun `Create world with custom options`() { assertTrue(worldManager.createWorld(