diff --git a/.github/workflows/generic.platform_uploads.yml b/.github/workflows/generic.platform_uploads.yml index 1e994455f..c5d488a99 100644 --- a/.github/workflows/generic.platform_uploads.yml +++ b/.github/workflows/generic.platform_uploads.yml @@ -107,7 +107,7 @@ jobs: files: '["${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar"]' name: ${{ steps.release-info.outputs.tag_name }} changelog: ${{ steps.release-artifact.outputs.body }} - game_versions: 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2 + game_versions: 26.1.1, 26.1, 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2 version_type: ${{ steps.parse-release-type.outputs.release_type }} loaders: bukkit, spigot, paper, purpur dependencies: ${{ inputs.modrinth_dependencies }} @@ -121,7 +121,7 @@ jobs: changelog: ${{ steps.release-artifact.outputs.body }} changelog_type: markdown display_name: ${{ steps.release-info.outputs.tag_name }} - game_versions: 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2 + game_versions: 26.1.1, 26.1, 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2 release_type: ${{ steps.parse-release-type.outputs.release_type }} project_relations: ${{ inputs.dbo_project_relations }} file_path: ${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar @@ -136,5 +136,5 @@ jobs: channel: ${{ steps.parse-release-type.outputs.release_type }} files: '[{"path": "${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar", "platforms": ["PAPER"]}]' description: ${{ steps.release-artifact.outputs.body }} - platform_dependencies: '{"PAPER": ["1.18.2-1.21.11"]}' + platform_dependencies: '{"PAPER": ["1.18.2-26.1.1"]}' plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }} 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 new file mode 100644 index 000000000..f81d22e54 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java @@ -0,0 +1,68 @@ +package org.mvplugins.multiverse.core.utils.compatibility; + +import io.vavr.control.Option; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Server; +import org.bukkit.World; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.core.utils.ReflectHelper; + +import java.lang.reflect.Method; +import java.nio.file.Path; + +/** + * Compatibility class used to handle API changes in {@link Bukkit} class. + */ +@ApiStatus.AvailableSince("5.6") +public final class BukkitCompatibility { + + private static final Option GET_LEVEL_DIRECTORY_METHOD; + private static final Option GET_WORLD_NAMESPACED_KEY_METHOD; + + static { + GET_LEVEL_DIRECTORY_METHOD = Option.of(ReflectHelper.getMethod(Server.class, "getLevelDirectory")); + GET_WORLD_NAMESPACED_KEY_METHOD = Option.of(ReflectHelper.getMethod(Bukkit.class, "getWorld", NamespacedKey.class)); + } + + /** + * Gets the folder where all the worlds will be store. Before 26.1, all worlds are stored in the root directory + * of the server, which can be obtained by {@link Server#getWorldContainer()}. + *
+ * After 26.1, PaperMC changed all worlds are stored in the "[level]/dimensions/minecraft" folder under the world + * level directory, which needs to be manually parsed. + * + * @return The location where all the worlds folders should be, depending on server's mc version. + */ + @ApiStatus.AvailableSince("5.6") + @NotNull + public static Path getWorldFoldersDirectory() { + Server server = Bukkit.getServer(); + return GET_LEVEL_DIRECTORY_METHOD.map(method -> ReflectHelper.invokeMethod(server, method)) + .filter(Path.class::isInstance) + .map(Path.class::cast) + .map(path -> path.resolve("dimensions/minecraft")) + .getOrElse(() -> server.getWorldContainer().toPath()); + } + + /** + * Check if the world with the given name or namespaced key (e.g. minecraft:overworld) exists, + * and return it if it does. + *
+ * Note that some default world names have different namespaced key matched with them. + * E.g.: world -> minecraft:overworld, world_nether -> minecraft:the_nether, world_the_end -> minecraft:the_end. + * + * @param nameOrKey Either a name or namespaced key string representation. + * @return The world if it exists + */ + @ApiStatus.AvailableSince("5.6") + @NotNull + public static Option getWorldByNameOrKey(@NotNull String nameOrKey) { + return Option.of(Bukkit.getWorld(nameOrKey)) + .orElse(() -> GET_WORLD_NAMESPACED_KEY_METHOD + .map(method -> ReflectHelper.invokeMethod(null, method, NamespacedKey.fromString(nameOrKey))) + .filter(World.class::isInstance) + .map(World.class::cast)); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/package-info.java b/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/package-info.java new file mode 100644 index 000000000..b2212b3ff --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains classes to handle compatibility due to differing API methods across different server versions and the + * API gaps between PaperMC and Spigot. + */ +package org.mvplugins.multiverse.core.utils.compatibility; \ No newline at end of file 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 74779d66e..55af6c7c9 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java @@ -49,6 +49,7 @@ 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.compatibility.BukkitCompatibility; import org.mvplugins.multiverse.core.utils.result.Attempt; import org.mvplugins.multiverse.core.utils.result.FailureReason; import org.mvplugins.multiverse.core.utils.FileUtils; @@ -699,6 +700,7 @@ private Attempt doDeleteWorld(@NotNull LoadedMultiv private Attempt validateWorldToDelete( @NotNull LoadedMultiverseWorld world) { return world.getBukkitWorld().map(World::getWorldFolder) + .peek(folder -> Logging.finer("World folder for world %s is at: %s", world.getName(), folder.getPath())) .filter(worldNameChecker::isValidWorldFolder) .map(this::worldActionResult) .getOrElse(() -> { @@ -761,7 +763,7 @@ private Attempt cloneWorldCopyFolder(@Not options.world().getBukkitWorld().peek(this::saveWorldWithFlush); } File worldFolder = options.world().getBukkitWorld().map(World::getWorldFolder).get(); - File newWorldFolder = new File(Bukkit.getWorldContainer(), options.newWorldName()); + File newWorldFolder = BukkitCompatibility.getWorldFoldersDirectory().resolve(options.newWorldName()).toFile(); return fileUtils.copyFolder(worldFolder, newWorldFolder, CLONE_IGNORE_FILES).fold( exception -> worldActionResult(CloneFailureReason.COPY_FAILED, options.world().getName(), exception), @@ -947,12 +949,12 @@ private void throwUnloadException(World world) throws MultiverseWorldException { * @return A list of all potential worlds. */ public List getPotentialWorlds() { - File[] files = Bukkit.getWorldContainer().listFiles(); + File[] files = BukkitCompatibility.getWorldFoldersDirectory().toFile().listFiles(); if (files == null) { return Collections.emptyList(); } return Arrays.stream(files) - .filter(file -> !isWorld(file.getName())) + .filter(file -> BukkitCompatibility.getWorldByNameOrKey(file.getName()).isEmpty()) .filter(worldNameChecker::isValidWorldFolder) .map(File::getName) .toList(); diff --git a/src/main/java/org/mvplugins/multiverse/core/world/helpers/WorldNameChecker.java b/src/main/java/org/mvplugins/multiverse/core/world/helpers/WorldNameChecker.java index 33a4125f2..4bb264c20 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/helpers/WorldNameChecker.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/helpers/WorldNameChecker.java @@ -1,15 +1,17 @@ package org.mvplugins.multiverse.core.world.helpers; import java.io.File; +import java.util.List; import java.util.Locale; import java.util.Set; +import com.dumptruckman.minecraft.util.Logging; import io.vavr.control.Option; -import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.utils.REPatterns; +import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility; /** *

Utility class in helping to check the status of a world name and it's associated world folder.

@@ -29,6 +31,18 @@ public final class WorldNameChecker { "plugins", "versions"); + private static final List WORLD_FOLDER_SCHEMA = List.of( + // OLD + WorldFolderSchema.file("level.dat"), + WorldFolderSchema.folder("DIM1"), + WorldFolderSchema.folder("DIM-1"), + // NEW + WorldFolderSchema.file("paper-world.yml"), + WorldFolderSchema.folder("data"), + WorldFolderSchema.folder("entities"), + WorldFolderSchema.folder("poi"), + WorldFolderSchema.folder("region")); + /** * Checks if a world name is valid. * @@ -105,7 +119,8 @@ public FolderStatus checkFolder(@Nullable String worldName) { if (worldName == null) { return FolderStatus.DOES_NOT_EXIST; } - File worldFolder = new File(Bukkit.getWorldContainer(), worldName); + File worldFolder = BukkitCompatibility.getWorldFoldersDirectory().resolve(worldName).toFile(); + Logging.finer("Checking valid folder for world '%s' at: '%s'", worldName, worldFolder.getPath()); return checkFolder(worldFolder); } @@ -120,7 +135,7 @@ public FolderStatus checkFolder(@Nullable File worldFolder) { if (worldFolder == null || !worldFolder.exists() || !worldFolder.isDirectory()) { return FolderStatus.DOES_NOT_EXIST; } - if (!folderHasDat(worldFolder)) { + if (!folderWorldSchemaCheck(worldFolder)) { return FolderStatus.NOT_A_WORLD; } return FolderStatus.VALID; @@ -133,9 +148,54 @@ public FolderStatus checkFolder(@Nullable File worldFolder) { * @param worldFolder The File that may be a world. * @return True if it looks like a world, else false. */ - private boolean folderHasDat(@NotNull File worldFolder) { - File[] files = worldFolder.listFiles((file, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".dat")); - return files != null && files.length > 0; + private boolean folderWorldSchemaCheck(@NotNull File worldFolder) { + return WORLD_FOLDER_SCHEMA.stream() + .filter(schema -> schema.check(worldFolder)) + .count() >= 2; + } + + /** + * Helper class to check if a file or folder exist. + */ + private interface WorldFolderSchema { + + static WorldFolderSchema file(String path) { + return new WorldFile(path); + } + + static WorldFolderSchema folder(String path) { + return new WorldFolder(path); + } + + boolean check(File worldFolder); + + final class WorldFile implements WorldFolderSchema { + private final String path; + + private WorldFile(String path) { + this.path = path; + } + + @Override + public boolean check(File worldFolder) { + File thisFolder = worldFolder.toPath().resolve(path).toFile(); + return thisFolder.exists() && thisFolder.isFile(); + } + } + + final class WorldFolder implements WorldFolderSchema { + private final String path; + + private WorldFolder(String path) { + this.path = path; + } + + @Override + public boolean check(File worldFolder) { + File thisFolder = worldFolder.toPath().resolve(path).toFile(); + return thisFolder.exists() && thisFolder.isDirectory(); + } + } } /** diff --git a/src/test/java/org/mvplugins/multiverse/core/mock/MVServerMock.java b/src/test/java/org/mvplugins/multiverse/core/mock/MVServerMock.java index 1ed0cb32c..0073ce785 100644 --- a/src/test/java/org/mvplugins/multiverse/core/mock/MVServerMock.java +++ b/src/test/java/org/mvplugins/multiverse/core/mock/MVServerMock.java @@ -39,6 +39,8 @@ public World createWorld(@NotNull WorldCreator creator) { world.getWorldFolder().mkdirs(); createFile(new File(world.getWorldFolder(), "uid.dat")); createFile(new File(world.getWorldFolder(), "level.dat")); + new File(world.getWorldFolder(), "region").mkdir(); + new File(world.getWorldFolder(), "data").mkdir(); addWorld(world); return world; } 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 a6fcd63b6..36bf4efe5 100644 --- a/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt @@ -7,7 +7,15 @@ import org.hamcrest.MatcherAssert.assertThat import org.mockbukkit.mockbukkit.matcher.plugin.PluginManagerFiredEventClassMatcher.hasFiredEventInstance import org.mockbukkit.mockbukkit.matcher.plugin.PluginManagerFiredEventClassMatcher.hasNotFiredEventInstance import org.mvplugins.multiverse.core.TestWithMockBukkit -import org.mvplugins.multiverse.core.event.world.* +import org.mvplugins.multiverse.core.event.world.MVWorldClonedEvent +import org.mvplugins.multiverse.core.event.world.MVWorldCreatedEvent +import org.mvplugins.multiverse.core.event.world.MVWorldDeleteEvent +import org.mvplugins.multiverse.core.event.world.MVWorldImportedEvent +import org.mvplugins.multiverse.core.event.world.MVWorldLoadedEvent +import org.mvplugins.multiverse.core.event.world.MVWorldPropertyChangedEvent +import org.mvplugins.multiverse.core.event.world.MVWorldRegeneratedEvent +import org.mvplugins.multiverse.core.event.world.MVWorldRemovedEvent +import org.mvplugins.multiverse.core.event.world.MVWorldUnloadedEvent import org.mvplugins.multiverse.core.world.options.CloneWorldOptions import org.mvplugins.multiverse.core.world.options.CreateWorldOptions import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions @@ -190,6 +198,8 @@ class WorldManagerTest : TestWithMockBukkit() { fun `Load world failed - world folder exists but not imported`() { File(Bukkit.getWorldContainer(), "worldfolder").mkdir() File(Bukkit.getWorldContainer(), "worldfolder/level.dat").createNewFile() + File(Bukkit.getWorldContainer(), "worldfolder/data").mkdir() + File(Bukkit.getWorldContainer(), "worldfolder/region").mkdir() assertEquals( LoadFailureReason.WORLD_EXIST_FOLDER, worldManager.loadWorld("worldfolder").failureReason @@ -270,8 +280,10 @@ class WorldManagerTest : TestWithMockBukkit() { fun `Get potential worlds`() { File(Bukkit.getWorldContainer(), "newworld1").mkdir() File(Bukkit.getWorldContainer(), "newworld1/level.dat").createNewFile() + File(Bukkit.getWorldContainer(), "newworld1/data").mkdir() File(Bukkit.getWorldContainer(), "newworld2").mkdir() File(Bukkit.getWorldContainer(), "newworld2/level.dat").createNewFile() + File(Bukkit.getWorldContainer(), "newworld2/data").mkdir() assertEquals(setOf("newworld1", "newworld2"), worldManager.getPotentialWorlds().toSet()) } diff --git a/src/test/java/org/mvplugins/multiverse/core/world/WorldNameCheckerTest.kt b/src/test/java/org/mvplugins/multiverse/core/world/WorldNameCheckerTest.kt index 88782c0fa..3c586e04e 100644 --- a/src/test/java/org/mvplugins/multiverse/core/world/WorldNameCheckerTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/world/WorldNameCheckerTest.kt @@ -45,6 +45,15 @@ class WorldNameCheckerTest : TestWithMockBukkit() { fun `Valid world folder`() { File(Bukkit.getWorldContainer(), "test").mkdir() File(Bukkit.getWorldContainer(), "test/level.dat").createNewFile() + File(Bukkit.getWorldContainer(), "test/data").mkdir() + assertEquals(WorldNameChecker.FolderStatus.VALID, worldNameChecker.checkFolder("test")) + } + + @Test + fun `Valid world folder v26-1 format`() { + File(Bukkit.getWorldContainer(), "test").mkdir() + File(Bukkit.getWorldContainer(), "test/region").mkdir() + File(Bukkit.getWorldContainer(), "test/data").mkdir() assertEquals(WorldNameChecker.FolderStatus.VALID, worldNameChecker.checkFolder("test")) }