From 53c7459c1ddf482dd2822ba5dbb9115a353adbf6 Mon Sep 17 00:00:00 2001
From: Ben Woo <30431861+benwoo1110@users.noreply.github.com>
Date: Wed, 8 Apr 2026 11:00:25 +0800
Subject: [PATCH] Add support for detecting PaperMC 26.1 folder migration
---
.../compatibility/BukkitCompatibility.java | 23 ++++++++++
.../multiverse/core/world/WorldManager.java | 25 +++++++++--
.../core/world/helpers/WorldNameChecker.java | 44 ++++++++++++++++---
3 files changed, 81 insertions(+), 11 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 f81d22e54..8cb1c219b 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
@@ -14,6 +14,8 @@
/**
* Compatibility class used to handle API changes in {@link Bukkit} class.
+ *
+ * @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
public final class BukkitCompatibility {
@@ -26,6 +28,23 @@ public final class BukkitCompatibility {
GET_WORLD_NAMESPACED_KEY_METHOD = Option.of(ReflectHelper.getMethod(Bukkit.class, "getWorld", NamespacedKey.class));
}
+ /**
+ * Check whether the server is using the new dimension storage system introduced in PaperMC 26.1.
+ *
+ * This is based of whether getLevelDirectory method exists in the server class,which is the main API change for
+ * the new dimension storage system.
+ *
+ * @return True if the server is using the new dimension storage system, else false.
+ *
+ * @since 5.6
+ */
+ @ApiStatus.AvailableSince("5.6")
+ public static boolean isUsingNewDimensionStorage() {
+ return GET_LEVEL_DIRECTORY_METHOD
+ .flatMap(method -> Option.of(ReflectHelper.invokeMethod(Bukkit.getServer(), method)))
+ .isDefined();
+ }
+
/**
* 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()}.
@@ -34,6 +53,8 @@ public final class BukkitCompatibility {
* level directory, which needs to be manually parsed.
*
* @return The location where all the worlds folders should be, depending on server's mc version.
+ *
+ * @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
@NotNull
@@ -55,6 +76,8 @@ public static Path getWorldFoldersDirectory() {
*
* @param nameOrKey Either a name or namespaced key string representation.
* @return The world if it exists
+ *
+ * @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
@NotNull
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 6471b214f..0639bf2c9 100644
--- a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java
+++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java
@@ -317,8 +317,17 @@ private Attempt validateImportWorldOpti
String worldName = options.worldName();
if (!worldNameChecker.isValidWorldName(worldName)) {
return worldActionResult(ImportFailureReason.INVALID_WORLDNAME, worldName);
- } else if (options.doFolderCheck() && !worldNameChecker.isValidWorldFolder(worldName)) {
- return worldActionResult(ImportFailureReason.WORLD_FOLDER_INVALID, worldName);
+ } else if (options.doFolderCheck()) {
+ //todo This is a duplicate of folder check in load world
+ WorldNameChecker.FolderStatus folderStatus = worldNameChecker.checkFolder(options.worldName());
+ if (!folderStatus.isLoadable()) {
+ return worldActionResult(ImportFailureReason.WORLD_FOLDER_INVALID, options.worldName());
+ }
+ if (folderStatus == WorldNameChecker.FolderStatus.REQUIRES_MIGRATION) {
+ Logging.info("World '%s' will be automatically migrated by PaperMC to the new dimension " +
+ "location. If you face any issue with migration, please contact PaperMC support!",
+ options.worldName());
+ }
}
return worldActionResult(options);
}
@@ -480,8 +489,16 @@ private Attempt doLoadWorld(@NotNull L
return doLoadBukkitWorld(bukkitWorld, mvWorld);
}
- if (options.doFolderCheck() && !worldNameChecker.isValidWorldFolder(mvWorld.getName())) {
- return worldActionResult(LoadFailureReason.WORLD_FOLDER_INVALID, mvWorld.getName());
+ if (options.doFolderCheck()) {
+ WorldNameChecker.FolderStatus folderStatus = worldNameChecker.checkFolder(mvWorld.getName());
+ if (!folderStatus.isLoadable()) {
+ return worldActionResult(LoadFailureReason.WORLD_FOLDER_INVALID, mvWorld.getName());
+ }
+ if (folderStatus == WorldNameChecker.FolderStatus.REQUIRES_MIGRATION) {
+ Logging.info("World '%s' will be automatically migrated by PaperMC to the new dimension " +
+ "location. If you face any issue with migration, please contact PaperMC support!",
+ mvWorld.getName());
+ }
}
WorldCreator worldCreator = WorldCreator.name(mvWorld.getName())
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 4bb264c20..6554a856d 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
@@ -5,8 +5,9 @@
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.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
@@ -95,7 +96,7 @@ public boolean hasWorldFolder(@Nullable String worldName) {
* @return True if check result is valid, else false.
*/
public boolean isValidWorldFolder(@Nullable String worldName) {
- return checkFolder(worldName) == FolderStatus.VALID;
+ return checkFolder(worldName).loadable;
}
/**
@@ -105,7 +106,7 @@ public boolean isValidWorldFolder(@Nullable String worldName) {
* @return True if check result is valid, else false.
*/
public boolean isValidWorldFolder(@Nullable File worldFolder) {
- return checkFolder(worldFolder) == FolderStatus.VALID;
+ return checkFolder(worldFolder).loadable;
}
/**
@@ -120,7 +121,13 @@ public FolderStatus checkFolder(@Nullable String worldName) {
return FolderStatus.DOES_NOT_EXIST;
}
File worldFolder = BukkitCompatibility.getWorldFoldersDirectory().resolve(worldName).toFile();
- Logging.finer("Checking valid folder for world '%s' at: '%s'", worldName, worldFolder.getPath());
+ if (BukkitCompatibility.isUsingNewDimensionStorage()) {
+ File oldWorldFolder = Bukkit.getWorldContainer().toPath().resolve(worldName).toFile();
+ if (checkFolder(oldWorldFolder) == FolderStatus.VALID) {
+ return FolderStatus.REQUIRES_MIGRATION;
+ }
+ }
+
return checkFolder(worldFolder);
}
@@ -230,16 +237,39 @@ public enum FolderStatus {
/**
* Folder is valid.
*/
- VALID,
+ VALID(true),
+
+ /**
+ * This folder will cause PaperMC to migrate to new dimension world folder in 26.1+
+ */
+ REQUIRES_MIGRATION(true),
/**
* Folder exist, but contents in it doesnt look like a world.
*/
- NOT_A_WORLD,
+ NOT_A_WORLD(false),
/**
* Folder does not exist.
*/
- DOES_NOT_EXIST
+ DOES_NOT_EXIST(false),
+ ;
+
+ private final boolean loadable;
+
+ FolderStatus(boolean loadable) {
+ this.loadable = loadable;
+ }
+
+ /**
+ * Whether this folder status is loadable, meaning it has the basic world data and can be loaded as a world.
+ * Note that this does not guarantee the server will definitely load the world with no errors.
+ *
+ * @return True if folder probably is loadable by the server, else false.
+ */
+ @ApiStatus.AvailableSince("5.6")
+ public boolean isLoadable() {
+ return loadable;
+ }
}
}