Skip to content

Commit 6b5ce2a

Browse files
committed
Update world folder discovery and checking to support new 26.1 format
1 parent e9905ca commit 6b5ce2a

4 files changed

Lines changed: 140 additions & 9 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.mvplugins.multiverse.core.utils.compatibility;
2+
3+
import io.vavr.control.Option;
4+
import org.bukkit.Bukkit;
5+
import org.bukkit.NamespacedKey;
6+
import org.bukkit.Server;
7+
import org.bukkit.World;
8+
import org.jetbrains.annotations.ApiStatus;
9+
import org.jetbrains.annotations.NotNull;
10+
import org.mvplugins.multiverse.core.utils.ReflectHelper;
11+
12+
import java.lang.reflect.Method;
13+
import java.nio.file.Path;
14+
15+
/**
16+
* Compatibility class used to handle API changes in {@link Bukkit} class.
17+
*/
18+
@ApiStatus.AvailableSince("5.6")
19+
public final class BukkitCompatibility {
20+
21+
private static final Option<Method> getLevelDirectoryMethod;
22+
private static final Option<Method> getWorldNamespacedKeyMethod;
23+
24+
static {
25+
getLevelDirectoryMethod = Option.of(ReflectHelper.getMethod(Server.class, "getLevelDirectory"));
26+
getWorldNamespacedKeyMethod = Option.of(ReflectHelper.getMethod(Bukkit.class, "getWorld", NamespacedKey.class));
27+
}
28+
29+
/**
30+
* Gets the folder where all the worlds will be store. Before 26.1, all worlds are stored in the root directory
31+
* of the server, which can be obtained by {@link Server#getWorldContainer()}.
32+
* <br />
33+
* After 26.1, PaperMC changed all worlds are stored in the "[level]/dimensions/minecraft" folder under the world
34+
* level directory, which needs to be manually parsed.
35+
*
36+
* @return The location where all the worlds folders should be, depending on server's mc version.
37+
*/
38+
@ApiStatus.AvailableSince("5.6")
39+
@NotNull
40+
public static Path getWorldFoldersDirectory() {
41+
Server server = Bukkit.getServer();
42+
return getLevelDirectoryMethod.map(method -> ReflectHelper.invokeMethod(server, method))
43+
.filter(Path.class::isInstance)
44+
.map(Path.class::cast)
45+
.map(path -> path.resolve("dimensions/minecraft"))
46+
.getOrElse(() -> server.getWorldContainer().toPath());
47+
}
48+
49+
/**
50+
* Check if the world with the given name or namespaced key (e.g. minecraft:overworld) exists, and return it if it does.
51+
*
52+
* @param nameOrKey Either a name or namespaced key string representation.
53+
* @return The world if it exists
54+
*/
55+
@ApiStatus.AvailableSince("5.6")
56+
@NotNull
57+
public static Option<World> getWorldByNameOrKey(@NotNull String nameOrKey) {
58+
return Option.of(Bukkit.getWorld(nameOrKey))
59+
.orElse(() -> getWorldNamespacedKeyMethod
60+
.map(method -> ReflectHelper.invokeMethod(null, method, NamespacedKey.fromString(nameOrKey)))
61+
.filter(World.class::isInstance)
62+
.map(World.class::cast));
63+
}
64+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Contains classes to handle compatibility due to differing API methods across different server versions and the
3+
* API gaps between PaperMC and Spigot.
4+
*/
5+
package org.mvplugins.multiverse.core.utils.compatibility;

src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.mvplugins.multiverse.core.utils.CaseInsensitiveStringMap;
5050
import org.mvplugins.multiverse.core.utils.ReflectHelper;
5151
import org.mvplugins.multiverse.core.utils.ServerProperties;
52+
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
5253
import org.mvplugins.multiverse.core.utils.result.Attempt;
5354
import org.mvplugins.multiverse.core.utils.result.FailureReason;
5455
import org.mvplugins.multiverse.core.utils.FileUtils;
@@ -699,6 +700,7 @@ private Attempt<String, DeleteFailureReason> doDeleteWorld(@NotNull LoadedMultiv
699700
private Attempt<File, DeleteFailureReason> validateWorldToDelete(
700701
@NotNull LoadedMultiverseWorld world) {
701702
return world.getBukkitWorld().map(World::getWorldFolder)
703+
.peek(folder -> Logging.finer("World folder for world %s is at: %s", world.getName(), folder.getPath()))
702704
.filter(worldNameChecker::isValidWorldFolder)
703705
.map(this::<File, DeleteFailureReason>worldActionResult)
704706
.getOrElse(() -> {
@@ -761,7 +763,7 @@ private Attempt<CloneWorldOptions, CloneFailureReason> cloneWorldCopyFolder(@Not
761763
options.world().getBukkitWorld().peek(this::saveWorldWithFlush);
762764
}
763765
File worldFolder = options.world().getBukkitWorld().map(World::getWorldFolder).get();
764-
File newWorldFolder = new File(Bukkit.getWorldContainer(), options.newWorldName());
766+
File newWorldFolder = BukkitCompatibility.getWorldFoldersDirectory().resolve(options.newWorldName()).toFile();
765767
return fileUtils.copyFolder(worldFolder, newWorldFolder, CLONE_IGNORE_FILES).fold(
766768
exception -> worldActionResult(CloneFailureReason.COPY_FAILED,
767769
options.world().getName(), exception),
@@ -947,12 +949,12 @@ private void throwUnloadException(World world) throws MultiverseWorldException {
947949
* @return A list of all potential worlds.
948950
*/
949951
public List<String> getPotentialWorlds() {
950-
File[] files = Bukkit.getWorldContainer().listFiles();
952+
File[] files = BukkitCompatibility.getWorldFoldersDirectory().toFile().listFiles();
951953
if (files == null) {
952954
return Collections.emptyList();
953955
}
954956
return Arrays.stream(files)
955-
.filter(file -> !isWorld(file.getName()))
957+
.filter(file -> BukkitCompatibility.getWorldByNameOrKey(file.getName()).isEmpty())
956958
.filter(worldNameChecker::isValidWorldFolder)
957959
.map(File::getName)
958960
.toList();

src/main/java/org/mvplugins/multiverse/core/world/helpers/WorldNameChecker.java

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package org.mvplugins.multiverse.core.world.helpers;
22

33
import java.io.File;
4+
import java.util.List;
45
import java.util.Locale;
56
import java.util.Set;
67

8+
import com.dumptruckman.minecraft.util.Logging;
79
import io.vavr.control.Option;
8-
import org.bukkit.Bukkit;
910
import org.jetbrains.annotations.NotNull;
1011
import org.jetbrains.annotations.Nullable;
1112
import org.jvnet.hk2.annotations.Service;
1213
import org.mvplugins.multiverse.core.utils.REPatterns;
14+
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
1315

1416
/**
1517
* <p>Utility class in helping to check the status of a world name and it's associated world folder.</p>
@@ -29,6 +31,18 @@ public final class WorldNameChecker {
2931
"plugins",
3032
"versions");
3133

34+
private static final List<WorldFolderSchema> WORLD_FOLDER_SCHEMA = List.of(
35+
// OLD
36+
WorldFolderSchema.file("level.dat"),
37+
WorldFolderSchema.folder("DIM1"),
38+
WorldFolderSchema.folder("DIM-1"),
39+
// NEW
40+
WorldFolderSchema.file("paper-world.yml"),
41+
WorldFolderSchema.folder("data"),
42+
WorldFolderSchema.folder("entities"),
43+
WorldFolderSchema.folder("poi"),
44+
WorldFolderSchema.folder("region"));
45+
3246
/**
3347
* Checks if a world name is valid.
3448
*
@@ -105,7 +119,8 @@ public FolderStatus checkFolder(@Nullable String worldName) {
105119
if (worldName == null) {
106120
return FolderStatus.DOES_NOT_EXIST;
107121
}
108-
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
122+
File worldFolder = BukkitCompatibility.getWorldFoldersDirectory().resolve(worldName).toFile();
123+
Logging.finer("Checking valid folder for world '%s' at: '%s'", worldName, worldFolder.getPath());
109124
return checkFolder(worldFolder);
110125
}
111126

@@ -120,7 +135,7 @@ public FolderStatus checkFolder(@Nullable File worldFolder) {
120135
if (worldFolder == null || !worldFolder.exists() || !worldFolder.isDirectory()) {
121136
return FolderStatus.DOES_NOT_EXIST;
122137
}
123-
if (!folderHasDat(worldFolder)) {
138+
if (!folderWorldSchemaCheck(worldFolder)) {
124139
return FolderStatus.NOT_A_WORLD;
125140
}
126141
return FolderStatus.VALID;
@@ -133,9 +148,54 @@ public FolderStatus checkFolder(@Nullable File worldFolder) {
133148
* @param worldFolder The File that may be a world.
134149
* @return True if it looks like a world, else false.
135150
*/
136-
private boolean folderHasDat(@NotNull File worldFolder) {
137-
File[] files = worldFolder.listFiles((file, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".dat"));
138-
return files != null && files.length > 0;
151+
private boolean folderWorldSchemaCheck(@NotNull File worldFolder) {
152+
return WORLD_FOLDER_SCHEMA.stream()
153+
.filter(schema -> schema.check(worldFolder))
154+
.count() >= 2;
155+
}
156+
157+
/**
158+
* Helper class to check if a file or folder exist
159+
*/
160+
private interface WorldFolderSchema {
161+
162+
static WorldFolderSchema file(String path) {
163+
return new WorldFile(path);
164+
}
165+
166+
static WorldFolderSchema folder(String path) {
167+
return new WorldFolder(path);
168+
}
169+
170+
boolean check(File worldFolder);
171+
172+
class WorldFile implements WorldFolderSchema {
173+
private final String path;
174+
175+
private WorldFile(String path) {
176+
this.path = path;
177+
}
178+
179+
@Override
180+
public boolean check(File worldFolder) {
181+
File thisFolder = worldFolder.toPath().resolve(path).toFile();
182+
return thisFolder.exists() && thisFolder.isFile();
183+
}
184+
}
185+
186+
class WorldFolder implements WorldFolderSchema {
187+
private final String path;
188+
189+
private WorldFolder(String path) {
190+
this.path = path;
191+
}
192+
193+
@Override
194+
public boolean check(File worldFolder) {
195+
File thisFolder = worldFolder.toPath().resolve(path).toFile();
196+
return thisFolder.exists() && thisFolder.isDirectory();
197+
}
198+
}
139199
}
140200

141201
/**

0 commit comments

Comments
 (0)