From f66e673ea9af93ee3e0254f17a6dde62738856a6 Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Thu, 1 Jan 2026 12:42:18 +0800 Subject: [PATCH] Fix case-insensitive world name handling --- .../core/utils/CaseInsensitiveStringMap.java | 92 +++++++++++++++++++ .../multiverse/core/world/WorldManager.java | 23 ++--- .../multiverse/core/world/WorldManagerTest.kt | 38 +++++--- 3 files changed, 130 insertions(+), 23 deletions(-) create mode 100644 src/main/java/org/mvplugins/multiverse/core/utils/CaseInsensitiveStringMap.java diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/CaseInsensitiveStringMap.java b/src/main/java/org/mvplugins/multiverse/core/utils/CaseInsensitiveStringMap.java new file mode 100644 index 000000000..5b8c05646 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/CaseInsensitiveStringMap.java @@ -0,0 +1,92 @@ +package org.mvplugins.multiverse.core.utils; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * A map with case-insensitive String keys. All keys are stored in lower-case form. + * + * @param the type of mapped values + * + * @since 5.5 + */ +@ApiStatus.AvailableSince("5.5") +public class CaseInsensitiveStringMap implements Map { + + private final Map map; + + public CaseInsensitiveStringMap() { + map = new HashMap<>(); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(normalizeKey(key)); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public T get(Object key) { + return map.get(normalizeKey(key)); + } + + @Override + public @Nullable T put(String key, T value) { + return map.put(normalizeKey(key), value); + } + + @Override + public T remove(Object key) { + return map.remove(normalizeKey(key)); + } + + @Override + public void putAll(@NonNull Map m) { + m.forEach((key, value) -> map.put(normalizeKey(key), value)); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public @NonNull Set keySet() { + return map.keySet(); + } + + @Override + public @NonNull Collection values() { + return map.values(); + } + + @Override + public @NonNull Set> entrySet() { + return map.entrySet(); + } + + private String normalizeKey(Object key) { + return String.valueOf(key).toLowerCase(Locale.ROOT); + } +} 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 d66db8145..08e259661 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.permissions.CorePermissions; import org.mvplugins.multiverse.core.teleportation.BlockSafety; import org.mvplugins.multiverse.core.teleportation.LocationManipulation; +import org.mvplugins.multiverse.core.utils.CaseInsensitiveStringMap; import org.mvplugins.multiverse.core.utils.ReflectHelper; import org.mvplugins.multiverse.core.utils.ServerProperties; import org.mvplugins.multiverse.core.utils.result.Attempt; @@ -91,8 +92,8 @@ public final class WorldManager { private static final DimensionFormat DEFAULT_NETHER_FORMAT = new DimensionFormat("%overworld%_nether"); private static final DimensionFormat DEFAULT_END_FORMAT = new DimensionFormat("%overworld%_the_end"); - private final Map worldsMap; - private final Map loadedWorldsMap; + private final CaseInsensitiveStringMap worldsMap; + private final CaseInsensitiveStringMap loadedWorldsMap; private final List unloadTracker; private final List loadTracker; private final WorldsConfigManager worldsConfigManager; @@ -135,8 +136,8 @@ public final class WorldManager { this.config = config; this.entityPurger = entityPurger; - this.worldsMap = new HashMap<>(); - this.loadedWorldsMap = new HashMap<>(); + this.worldsMap = new CaseInsensitiveStringMap<>(); + this.loadedWorldsMap = new CaseInsensitiveStringMap<>(); this.unloadTracker = new ArrayList<>(); this.loadTracker = new ArrayList<>(); } @@ -345,7 +346,7 @@ private Attempt doImportBukkitWorld( private MultiverseWorld newMultiverseWorld(String worldName, WorldConfig worldConfig) { MultiverseWorld mvWorld = new MultiverseWorld(worldName, worldConfig, config); - worldsMap.put(mvWorld.getName().toLowerCase(Locale.ENGLISH), mvWorld); + worldsMap.put(mvWorld.getName(), mvWorld); corePermissions.addWorldPermissions(mvWorld); return mvWorld; } @@ -382,7 +383,7 @@ private LoadedMultiverseWorld newLoadedMultiverseWorld( locationManipulation, entityPurger ); - loadedWorldsMap.put(loadedWorld.getName().toLowerCase(Locale.ENGLISH), loadedWorld); + loadedWorldsMap.put(loadedWorld.getName(), loadedWorld); saveWorldsConfig(); pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld)); return loadedWorld; @@ -501,7 +502,7 @@ private Attempt newLoadedMultiverseWor locationManipulation, entityPurger ); - loadedWorldsMap.put(loadedWorld.getName().toLowerCase(Locale.ENGLISH), loadedWorld); + loadedWorldsMap.put(loadedWorld.getName(), loadedWorld); saveWorldsConfig(); pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld)); return Attempt.success(loadedWorld); @@ -1010,7 +1011,7 @@ public Collection getWorlds() { * @return True if the world is a world is known to multiverse, but may or may not be loaded. */ public boolean isWorld(@Nullable String worldName) { - return worldName != null && worldsMap.containsKey(worldName.toLowerCase(Locale.ENGLISH)); + return worldName != null && worldsMap.containsKey(worldName); } /** @@ -1022,7 +1023,7 @@ public boolean isWorld(@Nullable String worldName) { public Option getUnloadedWorld(@Nullable String worldName) { return isLoadedWorld(worldName) ? Option.none() - : Option.of(worldName).flatMap(name -> Option.of(worldsMap.get(name.toLowerCase(Locale.ENGLISH)))); + : Option.of(worldName).flatMap(name -> Option.of(worldsMap.get(name))); } /** @@ -1095,7 +1096,7 @@ public Option getLoadedWorld(@Nullable MultiverseWorld wo */ public Option getLoadedWorld(@Nullable String worldName) { return Option.of(worldName) - .flatMap(name -> Option.of(loadedWorldsMap.get(name.toLowerCase(Locale.ENGLISH)))); + .flatMap(name -> Option.of(loadedWorldsMap.get(name))); } /** @@ -1156,7 +1157,7 @@ public boolean isLoadedWorld(@Nullable MultiverseWorld world) { * @return True if the world is a multiverse world that is loaded. */ public boolean isLoadedWorld(@Nullable String worldName) { - return worldName != null && loadedWorldsMap.containsKey(worldName.toLowerCase(Locale.ENGLISH)); + return worldName != null && loadedWorldsMap.containsKey(worldName); } /** 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 86b104dc3..a6fcd63b6 100644 --- a/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt @@ -36,8 +36,8 @@ class WorldManagerTest : TestWithMockBukkit() { world = worldManager.getLoadedWorld("world").get() assertNotNull(world) - assertTrue(worldManager.createWorld(CreateWorldOptions.worldName("world2")).isSuccess) - world2 = worldManager.getLoadedWorld("world2").get() + assertTrue(worldManager.createWorld(CreateWorldOptions.worldName("World2")).isSuccess) + world2 = worldManager.getLoadedWorld("World2").get() assertNotNull(world2) } @@ -122,6 +122,11 @@ class WorldManagerTest : TestWithMockBukkit() { assertFalse(worldManager.getLoadedWorld("world").isDefined) assertFalse(worldManager.getUnloadedWorld("world").isDefined) + assertTrue(worldManager.removeWorld(RemoveWorldOptions.world(world2)).isSuccess) + assertFalse(worldManager.getWorld("World2").isDefined) + assertFalse(worldManager.getLoadedWorld("World2").isDefined) + assertFalse(worldManager.getUnloadedWorld("World2").isDefined) + assertThat(server.pluginManager, hasFiredEventInstance(MVWorldUnloadedEvent::class.java)) assertThat(server.pluginManager, hasFiredEventInstance(MVWorldRemovedEvent::class.java)) } @@ -130,9 +135,18 @@ class WorldManagerTest : TestWithMockBukkit() { fun `Delete world`() { assertTrue(File(Bukkit.getWorldContainer(), "world").isDirectory) assertTrue(worldManager.deleteWorld(DeleteWorldOptions.world(world)).isSuccess) + assertFalse(worldManager.getWorld("world").isDefined) assertFalse(worldManager.getLoadedWorld("world").isDefined) + assertFalse(worldManager.getUnloadedWorld("world").isDefined) assertFalse(File(Bukkit.getWorldContainer(), "world").isDirectory) + assertTrue(File(Bukkit.getWorldContainer(), "World2").isDirectory) + assertTrue(worldManager.deleteWorld(DeleteWorldOptions.world(world2)).isSuccess) + assertFalse(worldManager.getWorld("World2").isDefined) + assertFalse(worldManager.getLoadedWorld("World2").isDefined) + assertFalse(worldManager.getUnloadedWorld("World2").isDefined) + assertFalse(File(Bukkit.getWorldContainer(), "World2").isDirectory) + assertThat(server.pluginManager, hasFiredEventInstance(MVWorldDeleteEvent::class.java)) assertThat(server.pluginManager, hasFiredEventInstance(MVWorldUnloadedEvent::class.java)) assertThat(server.pluginManager, hasFiredEventInstance(MVWorldRemovedEvent::class.java)) @@ -143,21 +157,21 @@ class WorldManagerTest : TestWithMockBukkit() { assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world2).saveBukkitWorld(true)).isSuccess) assertFalse(world2.isLoaded) assertFalse(world2.bukkitWorld.isDefined) - assertFalse(worldManager.getLoadedWorld("world2").isDefined) - assertTrue(worldManager.getWorld("world2").isDefined) - assertTrue(worldManager.getUnloadedWorld("world2").isDefined) + assertFalse(worldManager.getLoadedWorld("World2").isDefined) + assertTrue(worldManager.getWorld("World2").isDefined) + assertTrue(worldManager.getUnloadedWorld("World2").isDefined) assertTrue(worldManager.loadWorld(LoadWorldOptions.world(world2)).isSuccess) assertTrue(world2.isLoaded) - assertTrue(worldManager.getLoadedWorld("world2").flatMap{ w -> w.bukkitWorld }.isDefined) - assertTrue(worldManager.getLoadedWorld("world2").isDefined) - assertFalse(worldManager.getUnloadedWorld("world2").isDefined) + assertTrue(worldManager.getLoadedWorld("World2").flatMap{ w -> w.bukkitWorld }.isDefined) + assertTrue(worldManager.getLoadedWorld("World2").isDefined) + assertFalse(worldManager.getUnloadedWorld("World2").isDefined) } @Test fun `Load world failed - invalid world folder`() { assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world2)).isSuccess) - File(Bukkit.getWorldContainer(), "world2/").deleteRecursively() + File(Bukkit.getWorldContainer(), "World2/").deleteRecursively() assertEquals( LoadFailureReason.WORLD_FOLDER_INVALID, worldManager.loadWorld(LoadWorldOptions.world(world2)).failureReason @@ -190,7 +204,7 @@ class WorldManagerTest : TestWithMockBukkit() { .seed(4321L) ).isSuccess) - val getWorld = worldManager.getLoadedWorld("world2") + val getWorld = worldManager.getLoadedWorld("World2") assertTrue(getWorld.isDefined) val world = getWorld.get() assertNotNull(world) @@ -230,7 +244,7 @@ class WorldManagerTest : TestWithMockBukkit() { fun `Clone world failed - target world exists and loaded`() { assertEquals( CloneFailureReason.WORLD_EXIST_LOADED, - worldManager.cloneWorld(CloneWorldOptions.fromTo(world, "world2")).failureReason + worldManager.cloneWorld(CloneWorldOptions.fromTo(world, "World2")).failureReason ) } @@ -239,7 +253,7 @@ class WorldManagerTest : TestWithMockBukkit() { assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world2)).isSuccess) assertEquals( CloneFailureReason.WORLD_EXIST_UNLOADED, - worldManager.cloneWorld(CloneWorldOptions.fromTo(world, "world2")).failureReason + worldManager.cloneWorld(CloneWorldOptions.fromTo(world, "World2")).failureReason ) }