Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/generic.platform_uploads.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
Expand All @@ -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 }}
Original file line number Diff line number Diff line change
@@ -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;

Check warning on line 10 in src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 'org.mvplugins.multiverse.core.utils.ReflectHelper' should be separated from previous imports. Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java:10:1: warning: 'org.mvplugins.multiverse.core.utils.ReflectHelper' should be separated from previous imports. (com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck)

import java.lang.reflect.Method;

Check warning on line 12 in src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Wrong order for 'java.lang.reflect.Method' import. Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java:12:1: warning: Wrong order for 'java.lang.reflect.Method' import. (com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck)
import java.nio.file.Path;

/**
* Compatibility class used to handle API changes in {@link Bukkit} class.
*/
@ApiStatus.AvailableSince("5.6")

Check warning on line 18 in src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Utility classes should not have a public or default constructor. Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java:18:1: warning: Utility classes should not have a public or default constructor. (com.puppycrawl.tools.checkstyle.checks.design.HideUtilityClassConstructorCheck)
public final class BukkitCompatibility {

Check warning on line 19 in src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a private constructor to hide the implicit public one.

See more on https://sonarcloud.io/project/issues?id=Multiverse_Multiverse-Core&issues=AZ1Wf0WrojYqUzZiFmnr&open=AZ1Wf0WrojYqUzZiFmnr&pullRequest=3435

private static final Option<Method> GET_LEVEL_DIRECTORY_METHOD;
private static final Option<Method> 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));

Check warning on line 26 in src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Line is longer than 120 characters (found 124). Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/BukkitCompatibility.java:26:0: warning: Line is longer than 120 characters (found 124). (com.puppycrawl.tools.checkstyle.checks.sizes.LineLengthCheck)
}

/**
* 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()}.
* <br />
* 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.
* <br />
* 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<World> 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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**

Check warning on line 1 in src/main/java/org/mvplugins/multiverse/core/utils/compatibility/package-info.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 File does not end with a newline. Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/utils/compatibility/package-info.java:1:0: warning: File does not end with a newline. (com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck)
* 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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -699,6 +700,7 @@ private Attempt<String, DeleteFailureReason> doDeleteWorld(@NotNull LoadedMultiv
private Attempt<File, DeleteFailureReason> 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::<File, DeleteFailureReason>worldActionResult)
.getOrElse(() -> {
Expand Down Expand Up @@ -761,7 +763,7 @@ private Attempt<CloneWorldOptions, CloneFailureReason> 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),
Expand Down Expand Up @@ -947,12 +949,12 @@ private void throwUnloadException(World world) throws MultiverseWorldException {
* @return A list of all potential worlds.
*/
public List<String> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

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

private static final List<WorldFolderSchema> 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.
*
Expand Down Expand Up @@ -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);
}

Expand All @@ -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;
Expand All @@ -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();
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}

Expand Down
Loading