From 142ef02ca08282914bb9ceca195077a79f6d9b87 Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Fri, 8 May 2026 20:37:06 +0800 Subject: [PATCH] Fix world exist checking not looking up both name and key --- .../compatibility/BukkitCompatibility.java | 26 ++++++ .../multiverse/core/world/WorldManager.java | 15 ++-- .../multiverse/core/world/WorldStore.java | 86 +++++++++++++++++++ .../core/world/key/WorldKeyOrName.java | 25 ++++-- 4 files changed, 137 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java b/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java index 3ab34bbda..abf4b613f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.core.utils.ReflectHelper; +import org.mvplugins.multiverse.core.world.key.WorldKeyOrName; import java.lang.reflect.Method; import java.nio.file.Path; @@ -91,6 +92,31 @@ public static Option getWorldByNameOrKey(@NotNull String nameOrKey) { .toOption()); } + + /** + * Check if the world represented by the given {@link WorldKeyOrName} exists and return it if present. + * + *

This first attempts to resolve the world by name using {@link WorldKeyOrName#usableName()} (which + * delegates to the traditional Bukkit world name lookup). If that fails it will attempt to resolve a + * {@link NamespacedKey} using {@link WorldKeyOrName#usableKey()} (which mirrors the newer Bukkit API + * that accepts namespaced keys, e.g. "minecraft:overworld"). + * + * @param keyOrName The key-or-name wrapper providing either a world name or a {@link NamespacedKey}. + * @return The world if it exists. + * + * @since 5.7 + */ + @ApiStatus.AvailableSince("5.7") + @NotNull + public static Option getWorldByNameOrKey(@NotNull WorldKeyOrName keyOrName) { + return Option.of(Bukkit.getWorld(keyOrName.usableName())) + .orElse(() -> GET_WORLD_NAMESPACED_KEY_METHOD + .flatMap(method -> ReflectHelper.tryInvokeStaticMethod(method, keyOrName.usableKey())) + .filter(World.class::isInstance) + .map(World.class::cast) + .toOption()); + } + private BukkitCompatibility() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } 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 de71677f8..17a12481d 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java @@ -48,6 +48,7 @@ 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; import org.mvplugins.multiverse.core.utils.compatibility.WorldCreatorCompatibility; import org.mvplugins.multiverse.core.utils.result.Attempt; @@ -263,9 +264,9 @@ private Attempt, CreateFailureReason> p keyOrName -> worldActionResult(CreateFailureReason.INVALID_WORLDNAME, keyOrName)) .failIf(keyOrName -> keyOrName.isKey() && !WorldCreatorCompatibility.canCreateWorldWithKey(), keyOrName -> worldActionResult(CreateFailureReason.NAMESPACEDKEY_UNSUPPORTED, keyOrName)) - .failIf(keyOrName -> getLoadedWorld(keyOrName.usableName()).isDefined(), + .failIf(worldStore::isLoadedWorld, keyOrName -> worldActionResult(CreateFailureReason.WORLD_EXIST_LOADED, keyOrName)) - .failIf(keyOrName -> getWorld(keyOrName.usableName()).isDefined(), + .failIf(worldStore::isUnloadedWorld, keyOrName -> worldActionResult(CreateFailureReason.WORLD_EXIST_UNLOADED, keyOrName)) .failIf(keyOrName -> options.doFolderCheck() && worldNameChecker.hasWorldFolder(keyOrName), keyOrName -> worldActionResult(CreateFailureReason.WORLD_EXIST_FOLDER, keyOrName)) @@ -321,12 +322,12 @@ public Attempt importWorld(ImportWor keyOrName -> worldActionResult(ImportFailureReason.INVALID_WORLDNAME, keyOrName)) .failIf(keyOrName -> keyOrName.isKey() && !WorldCreatorCompatibility.canCreateWorldWithKey(), keyOrName -> worldActionResult(ImportFailureReason.NAMESPACEDKEY_UNSUPPORTED, keyOrName)) - .failIf(keyOrName -> getLoadedWorld(keyOrName.usableName()).isDefined(), + .failIf(worldStore::isLoadedWorld, keyOrName -> worldActionResult(ImportFailureReason.WORLD_EXIST_LOADED, keyOrName)) - .failIf(keyOrName -> getWorld(keyOrName.usableName()).isDefined(), + .failIf(worldStore::isUnloadedWorld, keyOrName -> worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, keyOrName)) .map(keyOrName -> new KeyOrNameWithOptions<>(keyOrName, options)) - .mapAttempt(pair -> Option.of(Bukkit.getWorld(pair.keyOrName().usableName())) + .mapAttempt(pair -> BukkitCompatibility.getWorldByNameOrKey(pair.keyOrName()) .map(bukkitWorld -> doImportBukkitWorld(pair, bukkitWorld)) .getOrElse(() -> validateImportWorldOptions(pair).mapAttempt(this::doImportWorld))); } @@ -809,9 +810,9 @@ private Attempt, CloneFailureReason> par keyOrName -> worldActionResult(CloneFailureReason.INVALID_WORLDNAME, keyOrName)) .failIf(keyOrName -> keyOrName.isKey() && !WorldCreatorCompatibility.canCreateWorldWithKey(), keyOrName -> worldActionResult(CloneFailureReason.NAMESPACEDKEY_UNSUPPORTED, keyOrName)) - .failIf(keyOrName -> isLoadedWorld(keyOrName.usableName()), + .failIf(worldStore::isLoadedWorld, keyOrName -> worldActionResult(CloneFailureReason.WORLD_EXIST_LOADED, keyOrName)) - .failIf(keyOrName -> isWorld(keyOrName.usableName()), + .failIf(worldStore::isWorld, keyOrName -> worldActionResult(CloneFailureReason.WORLD_EXIST_UNLOADED, keyOrName)) .failIf(worldNameChecker::hasWorldFolder, keyOrName -> worldActionResult(CloneFailureReason.WORLD_EXIST_FOLDER, keyOrName)) diff --git a/src/main/java/org/mvplugins/multiverse/core/world/WorldStore.java b/src/main/java/org/mvplugins/multiverse/core/world/WorldStore.java index 16da4b016..41f809580 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldStore.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldStore.java @@ -5,11 +5,13 @@ import com.google.common.collect.Multimap; import io.vavr.control.Option; import jakarta.inject.Inject; +import org.bukkit.NamespacedKey; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.utils.CaseInsensitiveStringMap; +import org.mvplugins.multiverse.core.world.key.WorldKeyOrName; import java.util.ArrayList; import java.util.List; @@ -148,24 +150,108 @@ List getUnloadedWorlds() { return List.copyOf(unloadedList); } + @NotNull + Option getWorld(@Nullable WorldKeyOrName keyOrName) { + return keyOrName == null + ? Option.none() + : getWorld(keyOrName.usableKey()).orElse(() -> getWorld(keyOrName.usableName())); + } + + @NotNull + Option getWorld(@Nullable NamespacedKey worldKey) { + return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getWorld); + } + @NotNull Option getWorld(@Nullable String worldKeyString) { return Option.of((MultiverseWorld) loadedMap.get(worldKeyString)) .orElse(() -> Option.of(unloadedMap.get(worldKeyString))); } + boolean isWorld(@Nullable WorldKeyOrName keyOrName) { + return keyOrName != null && (isWorld(keyOrName.usableKey()) || isWorld(keyOrName.usableName())); + } + + boolean isWorld(@Nullable NamespacedKey worldKey) { + return getWorld(worldKey).isDefined(); + } + + boolean isWorld(@Nullable String worldKeyString) { + return getWorld(worldKeyString).isDefined(); + } + + @NotNull + Option getLoadedWorld(@Nullable WorldKeyOrName keyOrName) { + return keyOrName == null + ? Option.none() + : getLoadedWorld(keyOrName.usableKey()).orElse(() -> getLoadedWorld(keyOrName.usableName())); + } + + @NotNull + Option getLoadedWorld(@Nullable NamespacedKey worldKey) { + return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getLoadedWorld); + } + @NotNull Option getLoadedWorld(@Nullable String worldKeyString) { return Option.of(loadedMap.get(worldKeyString)) .filter(MultiverseWorld::isLoaded); } + boolean isLoadedWorld(@Nullable WorldKeyOrName keyOrName) { + return keyOrName != null && (isLoadedWorld(keyOrName.usableKey()) || isLoadedWorld(keyOrName.usableName())); + } + + boolean isLoadedWorld(@Nullable NamespacedKey worldKey) { + return getLoadedWorld(worldKey).isDefined(); + } + + boolean isLoadedWorld(@Nullable String worldKeyString) { + return getLoadedWorld(worldKeyString).isDefined(); + } + + @NotNull + Option getUnloadedWorld(@Nullable WorldKeyOrName keyOrName) { + return keyOrName == null + ? Option.none() + : getUnloadedWorld(keyOrName.usableKey()).orElse(() -> getUnloadedWorld(keyOrName.usableName())); + } + + @NotNull + Option getUnloadedWorld(@Nullable NamespacedKey worldKey) { + return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getUnloadedWorld); + } + @NotNull Option getUnloadedWorld(@Nullable String worldKeyString) { return Option.of(unloadedMap.get(worldKeyString)) .filter(world -> !world.isLoaded()); } + boolean isUnloadedWorld(@Nullable WorldKeyOrName keyOrName) { + return keyOrName != null && (isUnloadedWorld(keyOrName.usableKey()) || isUnloadedWorld(keyOrName.usableName())); + } + + boolean isUnloadedWorld(@Nullable NamespacedKey worldKey) { + return getUnloadedWorld(worldKey).isDefined(); + } + + boolean isUnloadedWorld(@Nullable String worldKeyString) { + return getUnloadedWorld(worldKeyString).isDefined(); + } + + @NotNull + Option getUnloadedWorldRef(@Nullable WorldKeyOrName keyOrName) { + return keyOrName == null + ? Option.none() + : getUnloadedWorldRef(keyOrName.usableKey()).orElse(() -> getUnloadedWorldRef(keyOrName.usableName())); + } + + @NotNull + Option getUnloadedWorldRef(@Nullable NamespacedKey worldKey) { + return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getUnloadedWorldRef); + } + @NotNull Option getUnloadedWorldRef(@Nullable String worldKeyString) { return Option.of(unloadedMap.get(worldKeyString)); diff --git a/src/main/java/org/mvplugins/multiverse/core/world/key/WorldKeyOrName.java b/src/main/java/org/mvplugins/multiverse/core/world/key/WorldKeyOrName.java index 8fdf94fb8..4069cdaa9 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/key/WorldKeyOrName.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/key/WorldKeyOrName.java @@ -6,8 +6,9 @@ import org.bukkit.NamespacedKey; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; +import org.jetbrains.annotations.Nullable; +import org.mvplugins.multiverse.core.locale.message.LocalizableMessage; +import org.mvplugins.multiverse.core.locale.message.Message; import org.mvplugins.multiverse.core.locale.message.MessageReplacement; import org.mvplugins.multiverse.core.utils.ServerProperties; import org.mvplugins.multiverse.core.utils.compatibility.UnsafeValuesCompatibility; @@ -23,7 +24,7 @@ * @since 5.7 */ @ApiStatus.AvailableSince("5.7") -public sealed abstract class WorldKeyOrName implements Comparable permits WorldKeyOrName.Key, WorldKeyOrName.Name { +public sealed abstract class WorldKeyOrName implements Comparable, LocalizableMessage permits WorldKeyOrName.Key, WorldKeyOrName.Name { private static final String DEFAULT_OVERWORLD_KEY = "overworld"; private static final String DEFAULT_NETHER_KEY = "the_nether"; @@ -63,7 +64,7 @@ public static Attempt parse(@Nullable S * @since 5.7 */ @ApiStatus.AvailableSince("5.7") - public static Attempt parseName(@NonNull String name) { + public static Attempt parseName(@NotNull String name) { return Try.of(() -> NamespacedKey.minecraft(mapWorldNameToMinecraftKey(name))) .map(usableKey -> Attempt.success(new Name(name, usableKey))) .recover(throwable -> Attempt.failure(WorldKeyParseFailReason.INVALID_WORLD_NAME, @@ -72,7 +73,7 @@ public static Attempt parseName(@NonNul MessageReplacement.Replace.WORLD.with(name))); } - private static String mapWorldNameToMinecraftKey(@NonNull String nameOrKey) { + private static String mapWorldNameToMinecraftKey(@NotNull String nameOrKey) { String defaultLevelName = getMostAccurateLevelName(); String lowerCaseName = nameOrKey.toLowerCase(Locale.ROOT); if (defaultLevelName.equalsIgnoreCase(lowerCaseName)) { @@ -95,7 +96,7 @@ private static String mapWorldNameToMinecraftKey(@NonNull String nameOrKey) { * @since 5.7 */ @ApiStatus.AvailableSince("5.7") - public static Attempt parseKey(@NonNull String nameOrKey) { + public static Attempt parseKey(@NotNull String nameOrKey) { return Option.of(NamespacedKey.fromString(nameOrKey)) .filter(Objects::nonNull) .map(key -> Attempt.success(new Key(key, usableNameFromKey(key)))) @@ -112,7 +113,7 @@ public static Attempt parseKey(@NonNull * @since 5.7 */ @ApiStatus.AvailableSince("5.7") - public static WorldKeyOrName parseKey(@NonNull NamespacedKey key) { + public static WorldKeyOrName parseKey(@NotNull NamespacedKey key) { return new Key(key, usableNameFromKey(key)); } @@ -237,10 +238,18 @@ private static String getMostAccurateLevelName() { * @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object. */ @Override - public int compareTo(@NonNull WorldKeyOrName o) { + public int compareTo(WorldKeyOrName o) { return serialise().compareTo(o.serialise()); } + /** + * {@inheritDoc} + */ + @Override + public @Nullable Message getLocalizableMessage() { + return Message.of(serialise()); + } + public static final class Key extends WorldKeyOrName { private final NamespacedKey key;