From 8f19be1671454ea6a542f384eeb62a01e5d766dd Mon Sep 17 00:00:00 2001
From: Ben Woo <30431861+benwoo1110@users.noreply.github.com>
Date: Sun, 27 Sep 2026 22:54:19 +0800
Subject: [PATCH] Add world ref api to handle stored world references for later
use
---
.../core/world/LoadedMultiverseWorld.java | 22 +++----
.../core/world/MultiverseWorld.java | 28 ++++++++-
.../core/world/MultiverseWorldRef.java | 57 +++++++++++++++++++
.../core/world/WorldConfigNodes.java | 4 +-
.../multiverse/core/world/WorldManager.java | 32 +++--------
.../multiverse/core/world/WorldManagerTest.kt | 42 ++++++++++++++
6 files changed, 147 insertions(+), 38 deletions(-)
create mode 100644 src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorldRef.java
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(