Compare commits

...
1 Commits
Author SHA1 Message Date
Ben Woo 8f19be1671 Add world ref api to handle stored world references for later use 2026-09-27 22:54:19 +08:00
6 changed files with 147 additions and 38 deletions

No files matched your search

@@ -13,7 +13,7 @@ import org.bukkit.WorldType;
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 @@ import org.mvplugins.multiverse.core.world.location.SpawnLocation;
/**
* Extension of {@link MultiverseWorld} that represents a world that is currently loaded with bukkit world object.
* <p>
* 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);
@@ -20,6 +20,7 @@ import org.jetbrains.annotations.NotNull;
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 @@ import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
/**
* Represents a world handled by Multiverse which has all the custom properties provided by Multiverse.
* <p>
* 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 sealed class MultiverseWorld permits LoadedMultiverseWorld {
.map(world -> (LoadedMultiverseWorld) world);
}
/**
* Creates a reference to this world's key for storing and retrieving the current world instance at runtime.
* <p>
* 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.
*
@@ -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.
* <p>
* 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.
* <p>
* 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<MultiverseWorld> 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<LoadedMultiverseWorld> getLoaded() {
return worldStore.getLoadedWorld(worldKey);
}
}
@@ -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 @@ final class WorldConfigNodes {
final ConfigNode<String> 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()
@@ -31,6 +31,7 @@ import org.jetbrains.annotations.Nullable;
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.MVCorei18n;
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.result.FailureReason;
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<String> unloadTracker;
private final List<String> 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> 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> 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 @@ public final class WorldManager {
}
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 @@ public final class WorldManager {
LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld(
world,
worldConfig,
config,
blockSafety,
locationManipulation,
entityPurger
multiverseCore
);
worldStore.putLoadedWorld(loadedWorld);
saveWorldsConfig();
@@ -592,10 +581,7 @@ public final class WorldManager {
LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld(
bukkitWorld,
worldConfig,
config,
blockSafety,
locationManipulation,
entityPurger
multiverseCore
);
worldStore.putLoadedWorld(loadedWorld);
saveWorldsConfig();
@@ -1278,10 +1264,6 @@ public final class WorldManager {
});
}
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.
@@ -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(