Compare commits

...
47 Commits
Author SHA1 Message Date
Ben Woo 9926041c2f Merge pull request #3321 from KioProject123/fix/spawn-location-angle
fix spawn location angle
2025-08-18 15:06:10 +08:00
KioProject 1c99898d0d fix 2025-08-18 13:49:01 +08:00
KioProject 41e0261eee fix spawn location angle 2025-08-18 13:13:25 +08:00
Ben Woo 642f3ab1b8 Merge pull request #3320 from MrXiaoM/patch-1
Fix adventure dep in spigot-based server
2025-08-17 22:57:20 +08:00
人間工作 57d1844756 fix NoClassDefFoundError in spigot-based server 2025-08-17 22:09:17 +08:00
Ben Woo 73e1650fc3 Merge pull request #3318 from Multiverse/fix/match-invalid-regex
Properly handle error from invalid regex string in matcher
2025-08-11 13:08:49 +08:00
Ben Woo 800709ca95 Properly annotate parameters for matcher classes 2025-08-11 12:43:38 +08:00
Ben Woo 935cbad21c Properly handle error from invalid wildcard string in matcher 2025-08-11 12:40:05 +08:00
Ben Woo 0e2a27b298 Merge pull request #3317 from Multiverse/fix/remove-logging
Fix unwanted warning logging for matcher
2025-08-11 12:28:59 +08:00
Ben Woo baaa175423 Properly handle error from invalid regex string in matcher 2025-08-11 12:24:36 +08:00
Ben Woo e2b7a31191 Fix unwanted warning logging for matcher 2025-08-11 12:15:16 +08:00
Ben Woo afa2825bc7 Merge pull request #3315 from Multiverse/feat/load-folder-check
Add folder checking before loading world
2025-07-31 16:32:37 +08:00
Ben Woo 21b4f6b087 Add folder checking before loading world 2025-07-31 15:25:14 +08:00
Ben Woo a771652c8f Merge pull request #3291 from Multiverse/5.2
5.2
2025-07-28 10:11:52 +08:00
Ben Woo e89b4f19ee Merge pull request #3314 from Multiverse/fix/debug-permissions
Fix debug-permission comment spacing
2025-07-27 14:23:34 +08:00
Ben Woo eb8ef32a94 Fix debug-permission comment spacing 2025-07-27 12:38:50 +08:00
Ben Woo f9d783bc5c Merge pull request #3311 from Multiverse/refactor/remove-deprecate
Deprecate other removeWorld methods in favour of using RemoveWorldOpt…
2025-07-27 11:36:20 +08:00
Ben Woo 87dd4168cd Merge pull request #3313 from Multiverse/fix/save-flush
Use newer save method with chunk flushing when possible
2025-07-27 11:35:35 +08:00
Ben Woo 2da6ece606 Merge pull request #3312 from Multiverse/chore/bump-deps
Bump dependencies
2025-07-27 11:35:08 +08:00
Ben Woo cbb3a7a518 Merge pull request #3310 from Multiverse/fix/spawn-command
Fix spawn command using deprecated teleport method
2025-07-27 11:34:32 +08:00
Ben Woo 57796d295b Bump dependencies 2025-07-26 12:26:38 +08:00
Ben Woo 09314e8bce Use newer save method with chunk flushing when possible 2025-07-26 12:08:23 +08:00
Ben Woo 907a6dd5c0 Fix spawn command using deprecated teleport method 2025-07-26 11:50:53 +08:00
Ben Woo a50bd807b6 Deprecate other removeWorld methods in favour of using RemoveWorldOptions 2025-07-26 11:50:02 +08:00
Ben Woo 02081b4654 Merge pull request #3305 from Multiverse/fix/text-format-null
Fix ChatTextFormatter handling null strings
2025-07-26 10:44:56 +08:00
Ben Woo 156ec7911d Merge pull request #3308 from Multiverse/fix/world-name-dot
Fix v4 config migration when world name contains dot
2025-07-26 10:44:10 +08:00
Ben Woo 2b95c62a84 Fix v4 config migration when world name contains dot 2025-07-24 22:56:08 +08:00
Ben Woo 455396713e Fix ChatTextFormatter handling null strings 2025-07-21 22:44:45 +08:00
Ben Woo 8bc3a593d0 Merge pull request #3303 from Multiverse/feat/remove-options
Add options to remove a world without unloading or saving the bukkit …
2025-07-20 15:05:33 +08:00
Ben Woo 5177f290ae Merge pull request #3304 from Multiverse/refactor/wm-cleanup
Refactor/wm cleanup
2025-07-20 15:05:11 +08:00
Ben Woo 5118864bb8 Use our ChatTextFormatter to remove colour from string 2025-07-20 14:56:18 +08:00
Ben Woo 654da03add Remove unnecessary casting 2025-07-20 14:54:41 +08:00
Ben Woo 61229356ad Mark initAllWorlds as internal api 2025-07-20 14:53:16 +08:00
Ben Woo fa5aa41547 Add options to remove a world without unloading or saving the bukkit world 2025-07-20 14:49:57 +08:00
Ben Woo 04f07657eb Merge pull request #3301 from Multiverse/feat/import-bukkit
Improve support for importing worlds already loaded on the server
2025-07-20 11:44:45 +08:00
Ben Woo eea2af169f Improve support for importing worlds already loaded on the server 2025-07-19 23:33:51 +08:00
Ben Woo 6d393723ff Merge pull request #3299 from Multiverse/feat/skip-folder-check
Add option to skip folder check on import
2025-07-19 23:08:28 +08:00
Ben Woo 9137ef04d2 Merge pull request #3300 from Multiverse/feat/load-env-check
Add an environment check when loading a world that already exist in b…
2025-07-19 23:06:56 +08:00
Ben Woo 878c4bea3d Add option to skip folder check on import 2025-07-19 23:05:17 +08:00
Ben Woo 021d86e21c Add an environment check when loading a world that already exist in bukkit 2025-07-19 22:59:15 +08:00
Ben Woo 3e9425c8f1 Merge pull request #3297 from Multiverse/feat/matcher
Implement StringMatcher api
2025-07-18 15:02:20 +08:00
Ben Woo cc60b9c938 Implement StringMatcher api 2025-07-18 14:46:59 +08:00
Ben Woo d3cc950a97 Merge pull request #3296 from Multiverse/feat/attempt-causeby
Add causeBy chaining for Attempt
2025-07-18 11:09:41 +08:00
Ben Woo 7c6b1d07b0 Add causeBy chaining for Attempt 2025-07-18 11:04:41 +08:00
Ben Woo 35b129701f Merge pull request #3285 from Multiverse/feat/joinlocation-bypass
Add bypass permission for join location
2025-07-15 13:46:45 +08:00
Ben Woo d92fe17c4f Merge branch 'main' into feat/joinlocation-bypass
# Conflicts:
#	src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java
2025-07-15 13:40:51 +08:00
Ben Woo d1660d7e81 Add bypass permission for join location 2025-07-07 10:41:38 +08:00
40 changed files with 1210 additions and 130 deletions

No files matched your search

+6 -6
View File
@@ -27,7 +27,7 @@ repositories {
configure(apiDependencies) {
serverApiVersion = '1.19.4-R0.1-SNAPSHOT'
mockBukkitServerApiVersion = '1.21'
mockBukkitVersion = '4.31.1'
mockBukkitVersion = '4.72.2'
}
dependencies {
@@ -48,9 +48,9 @@ dependencies {
}
// Utils
shadowed 'io.vavr:vavr:0.10.4'
shadowed 'org.glassfish.hk2:hk2-locator:3.0.3'
shadowed('org.glassfish.hk2:hk2-inhabitant-generator:3.0.3') {
shadowed 'io.vavr:vavr:0.10.7'
shadowed 'org.glassfish.hk2:hk2-locator:3.1.1'
shadowed('org.glassfish.hk2:hk2-inhabitant-generator:3.1.1') {
exclude group: 'org.apache.maven', module: 'maven-core'
}
shadowed('com.dumptruckman.minecraft:Logging:1.1.1') {
@@ -60,8 +60,8 @@ dependencies {
shadowed('org.bstats:bstats-bukkit:3.1.0') {
exclude group: 'org.bukkit', module: 'bukkit'
}
shadowed 'net.minidev:json-smart:2.4.9'
shadowed 'org.jetbrains:annotations:22.0.0'
shadowed 'net.minidev:json-smart:2.5.2'
shadowed 'org.jetbrains:annotations:26.0.2'
shadowed 'io.papermc:paperlib:1.0.8'
// Tests
@@ -68,7 +68,8 @@ class ImportCommand extends CoreCommand {
.biome(parsedFlags.flagValue(flags.biome, ""))
.environment(environment)
.generator(parsedFlags.flagValue(flags.generator, String.class))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn)))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.doFolderCheck(!parsedFlags.hasFlag(flags.skipFolderCheck)))
.onSuccess(newWorld -> {
Logging.fine("World import success: " + newWorld);
issuer.sendInfo(MVCorei18n.IMPORT_SUCCESS, Replace.WORLD.with(newWorld.getName()));
@@ -105,6 +106,10 @@ class ImportCommand extends CoreCommand {
private final CommandValueFlag<String> biome = flag(CommandValueFlag.builder("--biome", String.class)
.addAlias("-b")
.build());
private final CommandFlag skipFolderCheck = flag(CommandFlag.builder("--skip-folder-check")
.addAlias("-f")
.build());
}
@Service
@@ -3,9 +3,8 @@ package org.mvplugins.multiverse.core.commands;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Conditions;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Single;
import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
@@ -15,19 +14,26 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.options.LoadWorldOptions;
@Service
class LoadCommand extends CoreCommand {
private final WorldManager worldManager;
private final LoadCommand.Flags flags;
@Inject
LoadCommand(@NotNull WorldManager worldManager) {
LoadCommand(@NotNull WorldManager worldManager, @NotNull Flags flags) {
this.worldManager = worldManager;
this.flags = flags;
}
@Subcommand("load")
@@ -38,13 +44,19 @@ class LoadCommand extends CoreCommand {
void onLoadCommand(
MVCommandIssuer issuer,
@Single
@Conditions("worldname:scope=unloaded")
@Syntax("<world>")
@Description("{@@mv-core.load.world.description}")
String worldName) {
issuer.sendInfo(MVCorei18n.LOAD_LOADING, Replace.WORLD.with(worldName));
worldManager.loadWorld(worldName)
MultiverseWorld world,
@Optional
@Syntax("[--remove-players]")
@Description("")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
issuer.sendInfo(MVCorei18n.LOAD_LOADING, Replace.WORLD.with(world.getName()));
worldManager.loadWorld(LoadWorldOptions.world(world)
.doFolderCheck(!parsedFlags.hasFlag(flags.skipFolderCheck)))
.onSuccess(newWorld -> {
Logging.fine("World load success: " + newWorld);
issuer.sendInfo(MVCorei18n.LOAD_SUCCESS, Replace.WORLD.with(newWorld.getName()));
@@ -54,17 +66,32 @@ class LoadCommand extends CoreCommand {
});
}
@Service
private static final class Flags extends RemovePlayerFlags {
private static final String NAME = "mvunload";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag skipFolderCheck = flag(CommandFlag.builder("--skip-folder-check")
.addAlias("-f")
.build());
}
@Service
private static final class LegacyAlias extends LoadCommand implements LegacyAliasCommand {
@Inject
LegacyAlias(@NotNull WorldManager worldManager) {
super(worldManager);
LegacyAlias(@NotNull WorldManager worldManager, @NotNull Flags flags) {
super(worldManager, flags);
}
@Override
@CommandAlias("mvload")
void onLoadCommand(MVCommandIssuer issuer, String worldName) {
super.onLoadCommand(issuer, worldName);
void onLoadCommand(MVCommandIssuer issuer, MultiverseWorld world, String[] flagArray) {
super.onLoadCommand(issuer, world, flagArray);
}
}
}
@@ -15,6 +15,8 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
@@ -23,19 +25,20 @@ import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions;
@Service
class RemoveCommand extends CoreCommand {
private final WorldManager worldManager;
private final PlayerWorldTeleporter playerWorldTeleporter;
private final RemovePlayerFlags flags;
private final Flags flags;
@Inject
RemoveCommand(
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter,
@NotNull RemovePlayerFlags flags
@NotNull Flags flags
) {
this.worldManager = worldManager;
this.playerWorldTeleporter = playerWorldTeleporter;
@@ -44,7 +47,7 @@ class RemoveCommand extends CoreCommand {
@Subcommand("remove")
@CommandPermission("multiverse.core.remove")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + RemovePlayerFlags.NAME)
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + Flags.NAME)
@Syntax("<world>")
@Description("{@@mv-core.remove.description}")
void onRemoveCommand(
@@ -64,12 +67,14 @@ class RemoveCommand extends CoreCommand {
? worldManager.getLoadedWorld(world).map(playerWorldTeleporter::removeFromWorld).getOrElse(AsyncAttemptsAggregate::emptySuccess)
: AsyncAttemptsAggregate.emptySuccess();
future.onSuccess(() -> doWorldRemoving(issuer, world))
future.onSuccess(() -> doWorldRemoving(issuer, world, parsedFlags))
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
}
private void doWorldRemoving(MVCommandIssuer issuer, MultiverseWorld world) {
worldManager.removeWorld(world)
private void doWorldRemoving(MVCommandIssuer issuer, MultiverseWorld world, ParsedCommandFlags parsedFlags) {
worldManager.removeWorld(RemoveWorldOptions.world(world)
.saveBukkitWorld(!parsedFlags.hasFlag(flags.noSave))
.unloadBukkitWorld(!parsedFlags.hasFlag(flags.noUnloadBukkitWorld)))
.onSuccess(removedWorldName -> {
Logging.fine("World remove success: " + removedWorldName);
issuer.sendInfo(MVCorei18n.REMOVE_SUCCESS, Replace.WORLD.with(removedWorldName));
@@ -79,13 +84,32 @@ class RemoveCommand extends CoreCommand {
});
}
@Service
private static final class Flags extends RemovePlayerFlags {
private static final String NAME = "mvremove";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag noUnloadBukkitWorld = flag(CommandFlag.builder("--no-unload-bukkit-world")
.addAlias("-b")
.build());
private final CommandFlag noSave = flag(CommandFlag.builder("--no-save")
.addAlias("-n")
.build());
}
@Service
private static final class LegacyAlias extends RemoveCommand implements LegacyAliasCommand {
@Inject
LegacyAlias(
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter,
RemovePlayerFlags flags
@NotNull Flags flags
) {
super(worldManager, playerWorldTeleporter, flags);
}
@@ -108,7 +108,7 @@ final class SpawnCommand extends CoreCommand {
safetyTeleporter.to(mvWorld.getSpawnLocation())
.by(issuer)
.checkSafety(checkSafety)
.teleport(entity)
.teleportSingle(entity)
.onSuccess(() -> issuer.sendInfo(MVCorei18n.SPAWN_SUCCESS,
Replace.PLAYER.with(entity.equals(issuer.getPlayer())
? Message.of(MVCorei18n.GENERIC_YOU)
@@ -119,7 +119,7 @@ final class SpawnCommand extends CoreCommand {
? Message.of(MVCorei18n.GENERIC_YOU)
: Message.of(entity.getName())),
Replace.WORLD.with(mvWorld.getName()),
Replace.REASON.with(failure.getFailureMessage())));
Replace.REASON.with(failure.getFirst().getFailureMessage())));
}
private void handleMultiTeleport(MVCommandIssuer issuer, LoadedMultiverseWorld mvWorld,
@@ -518,6 +518,7 @@ final class CoreConfigNodes {
.build());
final ConfigNode<Boolean> debugPermissions = node(ConfigNode.builder("misc.debug-permissions", Boolean.class)
.comment("")
.comment("Sets whether console will log every permission check done by all multiverse plugins.")
.comment("This will only work if the above 'global-debug' is set to 1 or more.")
.defaultValue(false)
@@ -69,9 +69,12 @@ public class RegexContentFilter implements ContentFilter {
if (!hasValidRegex()) {
return false;
}
String text = ChatTextFormatter.removeColor(String.valueOf(value)).toLowerCase();
String text = ChatTextFormatter.removeColor(String.valueOf(value));
if (text == null) {
return false;
}
try {
return regexPattern.matcher(text).find();
return regexPattern.matcher(text.toLowerCase()).find();
} catch (PatternSyntaxException ignored) {
Logging.warning("Error parsing regex '%s' for input '%s'", regexString, text);
return false;
@@ -88,8 +88,9 @@ final class MVChatListener implements CoreListener {
String prefixChatFormat = config.getPrefixChatFormat();
prefixChatFormat = prefixChatFormat.replace("%world%", worldName).replace("%chat%", chat);
prefixChatFormat = ChatTextFormatter.colorize(prefixChatFormat);
event.setFormat(prefixChatFormat);
if (prefixChatFormat != null) {
event.setFormat(prefixChatFormat);
}
}
private String getWorldName(Player player) {
@@ -41,6 +41,7 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventPriorityKe
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.event.MVRespawnEvent;
import org.mvplugins.multiverse.core.locale.PluginLocales;
import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker;
import org.mvplugins.multiverse.core.teleportation.BlockSafety;
import org.mvplugins.multiverse.core.teleportation.TeleportQueue;
import org.mvplugins.multiverse.core.utils.result.ResultChain;
@@ -70,6 +71,7 @@ final class MVPlayerListener implements CoreListener {
private final DestinationsProvider destinationsProvider;
private final EnforcementHandler enforcementHandler;
private final DimensionFinder dimensionFinder;
private final CorePermissionsChecker corePermissionsChecker;
private final Map<String, String> playerWorld = new ConcurrentHashMap<>();
@@ -86,7 +88,8 @@ final class MVPlayerListener implements CoreListener {
Provider<MVCommandManager> commandManagerProvider,
DestinationsProvider destinationsProvider,
EnforcementHandler enforcementHandler,
DimensionFinder dimensionFinder) {
DimensionFinder dimensionFinder,
CorePermissionsChecker corePermissionsChecker) {
this.plugin = plugin;
this.config = config;
this.worldManagerProvider = worldManagerProvider;
@@ -99,6 +102,7 @@ final class MVPlayerListener implements CoreListener {
this.destinationsProvider = destinationsProvider;
this.enforcementHandler = enforcementHandler;
this.dimensionFinder = dimensionFinder;
this.corePermissionsChecker = corePermissionsChecker;
}
private WorldManager getWorldManager() {
@@ -244,6 +248,10 @@ final class MVPlayerListener implements CoreListener {
Logging.warning("Joindestination is enabled but no destination has been specified in config!");
return;
}
if (corePermissionsChecker.hasJoinLocationBypassPermission(event.getPlayer())) {
Logging.finer("Player %s has bypass permission for JoinDestination", event.getPlayer().getName());
return;
}
Logging.finer("JoinDestination is " + config.getJoinDestination());
destinationsProvider.parseDestination(config.getJoinDestination())
.map(destination -> destination.getLocation(event.getPlayer())
@@ -18,6 +18,7 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.options.LoadWorldOptions;
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions;
import org.mvplugins.multiverse.core.world.reasons.LoadFailureReason;
import org.mvplugins.multiverse.core.world.reasons.UnloadFailureReason;
@@ -68,7 +69,7 @@ final class MVWorldListener implements CoreListener {
worldManager.getUnloadedWorld(event.getWorld().getName())
.peek(world -> {
Logging.fine("Loading world: " + world.getName());
worldManager.loadWorld(world).onFailure(failure -> {
worldManager.loadWorld(LoadWorldOptions.world(world)).onFailure(failure -> {
if (failure.getFailureReason() != LoadFailureReason.WORLD_ALREADY_LOADING) {
Logging.severe("Failed to load world: " + failure);
}
@@ -308,11 +308,13 @@ public enum MVCorei18n implements MessageKeyProvider {
IMPORTWORLD_WORLDEXISTUNLOADED,
IMPORTWORLD_WORLDEXISTLOADED,
IMPORTWORLD_WORLDFOLDERINVALID,
IMPORTWORLD_BUKKITENVIRONMENTMISMATCH,
LOADWORLD_WORLDALREADYLOADING,
LOADWORLD_WORLDNONEXISTENT,
LOADWORLD_WORLDEXISTFOLDER,
LOADWORLD_WORLDEXISTLOADED,
LOADWORLD_BUKKITENVIRONMENTMISMATCH,
REMOVEWORLD_WORLDNONEXISTENT,
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.permissions;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
@@ -15,6 +16,11 @@ import static org.mvplugins.multiverse.core.permissions.PermissionUtils.concatPe
@Service
public final class CorePermissions {
/**
* Permission to bypass the join location.
*/
static final String JOINLOCATION_BYPASS = "mv.bypass.joinlocation";
/**
* Permission to access a world.
*/
@@ -52,6 +58,12 @@ public final class CorePermissions {
this.pluginManager = pluginManager;
}
@PostConstruct
void registerBasePermissions() {
pluginManager.addPermission(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE));
Logging.fine("Successfully registered base permissions");
}
public Try<Void> addWorldPermissions(@NotNull MultiverseWorld world) {
return Try.run(() -> {
pluginManager.addPermission(new Permission(
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.permissions;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
@@ -50,6 +51,19 @@ public final class CorePermissionsChecker {
this.worldManager = worldManager;
}
/**
* Checks if the sender has permission to bypass the join location restriction.
*
* @param sender The command sender.
* @return True if the sender has bypass permission, false otherwise.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean hasJoinLocationBypassPermission(@NotNull CommandSender sender) {
return hasPermission(sender, CorePermissions.JOINLOCATION_BYPASS);
}
/**
* Checks if the sender has permission to access the specified world.
*
@@ -0,0 +1,75 @@
package org.mvplugins.multiverse.core.utils.matcher;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* ExactStringMatcher is a StringMatcher that matches strings against a set of exact values.
* It can be initialized with a single string, a collection of strings, or can have exact matches added later.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public class ExactStringMatcher implements StringMatcher {
private final Set<String> exactMatches;
/**
* Creates a new ExactStringMatcher with no initial matches. Use {@link #addExactMatch(String)} to add matches later.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public ExactStringMatcher() {
this.exactMatches = new HashSet<>();
}
/**
* Creates a new ExactStringMatcher with a single exact match.
*
* @param exactMatch the exact string to match against
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public ExactStringMatcher(@NotNull String exactMatch) {
this.exactMatches = new HashSet<>();
this.exactMatches.add(exactMatch);
}
/**
* Creates a new ExactStringMatcher with multiple exact matches.
*
* @param exactMatches the collection of exact strings to match against
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public ExactStringMatcher(@NotNull Collection<String> exactMatches) {
this.exactMatches = new HashSet<>(exactMatches);
}
/**
* Adds an exact match string to this matcher.
*
* @param value the exact string to add to the matcher
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public void addExactMatch(@NotNull String value) {
this.exactMatches.add(value);
}
/**
* {@inheritDoc}
*/
@Override
public boolean matches(@Nullable String value) {
return exactMatches.contains(value);
}
}
@@ -0,0 +1,107 @@
package org.mvplugins.multiverse.core.utils.matcher;
import com.dumptruckman.minecraft.util.Logging;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* MatcherGroup is a collection of StringMatchers that can match against a string.
* It works with all format supported by {@link StringMatcher#fromString(String)}.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public class MatcherGroup implements StringMatcher {
private final ExactStringMatcher exactMatcher;
private final List<StringMatcher> stringMatchers;
/**
* Creates a new empty MatcherGroup.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public MatcherGroup() {
this.exactMatcher = new ExactStringMatcher();
this.stringMatchers = new ArrayList<>();
}
/**
* Creates a new MatcherGroup with multiple strings to be parsed into matchers.
*
* @param matchStrings the collection of match strings
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public MatcherGroup(@NotNull Collection<String> matchStrings) {
this();
for (String matchString : matchStrings) {
addMatcher(matchString);
}
}
/**
* Creates a new MatcherGroup with a single exact match.
*
* @param matchString the single match string
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public void addMatcher(@Nullable String matchString) {
if (matchString == null || matchString.isEmpty()) {
return;
}
if (isExact(matchString)) {
exactMatcher.addExactMatch(matchString);
} else {
stringMatchers.add(StringMatcher.fromString(matchString));
}
}
private boolean isExact(@NotNull String matcherString) {
return !matcherString.contains("*") && !matcherString.startsWith("r=");
}
/**
* Adds an existing matcher to the group.
*
* @param matcher the StringMatcher to add
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public void addMatcher(@NotNull StringMatcher matcher) {
stringMatchers.add(matcher);
}
/**
* {@inheritDoc}
*/
@Override
public boolean matches(@Nullable String value) {
if (exactMatcher.matches(value)) {
return true;
}
for (StringMatcher matcher : stringMatchers) {
if (matcher.matches(value)) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "MatcherGroup{" + "exactMatcher=" + exactMatcher +
", stringMatchers=" + stringMatchers +
'}';
}
}
@@ -0,0 +1,57 @@
package org.mvplugins.multiverse.core.utils.matcher;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.regex.Pattern;
/**
* RegexStringMatcher is a StringMatcher that matches strings against a regex pattern.
* It can be initialized with a regex string, which can optionally start with 'r=' to indicate it's a regex.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public class RegexStringMatcher implements StringMatcher {
private final @NotNull String regexString;
private final @Nullable Pattern regexPattern;
/**
* Creates a new RegexStringMatcher with a regex string. 'r=' prefix will be stripped if present.
*
* @param regexString the regex string to match against. If it starts with 'r=', that part will be ignored.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public RegexStringMatcher(@NotNull String regexString) {
this.regexString = regexString;
this.regexPattern = compileRegex(regexString);
}
private Pattern compileRegex(String regexString) {
if (regexString.startsWith("r=")) {
regexString = regexString.substring(2);
}
String finalRegexString = regexString;
return Try.of(() -> Pattern.compile(finalRegexString))
.onFailure(ex -> Logging.warning("Failed to compile regex '%s': %s",
finalRegexString, ex.getMessage()))
.getOrNull();
}
/**
* {@inheritDoc}
*/
@Override
public boolean matches(@Nullable String value) {
if (regexPattern == null || value == null) {
return false;
}
return regexPattern.matcher(value).matches();
}
}
@@ -0,0 +1,67 @@
package org.mvplugins.multiverse.core.utils.matcher;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.stream.Collectors;
/**
* StringMatcher is an interface for matching strings against various patterns.
* It currently built-in supports exact matches, wildcard matches, and regex matches.
* <br />
* Implement this interface to create your own custom string matchers.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public interface StringMatcher {
/**
* Creates a StringMatcher from a string representation. When the string starts with "r=", it is treated as a regex.
* If the string contains a '*', it is treated as a wildcard match.
*
* @param matcherString the string to be parsed into a matcher.
* @return a StringMatcher instance based on the provided string.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
static @NotNull StringMatcher fromString(@NotNull String matcherString) {
if (matcherString.startsWith("r=")) {
return new RegexStringMatcher(matcherString);
} else if (matcherString.contains("*")) {
return new WildcardStringMatcher(matcherString);
} else {
return new ExactStringMatcher(matcherString);
}
}
/**
* Checks if the given value matches the pattern defined by this StringMatcher.
*
* @param value the string to match against the pattern.
* @return true if the value matches, false otherwise.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
boolean matches(@Nullable String value);
/**
* Filters a list of strings, returning only those that match the pattern defined by this StringMatcher.
* This method is a convenience for applying the matcher to a collection of strings.
*
* @param values the list of strings to filter.
* @return A list of strings that match the pattern defined by this StringMatcher.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
default @NotNull List<String> filter(@NotNull List<String> values) {
return values.stream()
.filter(this::matches)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,50 @@
package org.mvplugins.multiverse.core.utils.matcher;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.regex.Pattern;
/**
* WildcardStringMatcher is a StringMatcher that matches strings against a wildcard pattern.
* It supports '*' as a wildcard character, which can match any sequence of characters.
* <br />
* For example, the wildcard "foo*bar" will match any string that starts with "foo" and ends with "bar",
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public class WildcardStringMatcher implements StringMatcher {
private final String wildcard;
private final Pattern pattern;
/**
* Creates a new WildcardStringMatcher with a wildcard string.
* The wildcard string can contain '*' characters, which will be replaced with a regex equivalent.
*
* @param wildcard the wildcard string to match against.
*/
@ApiStatus.AvailableSince("5.2")
public WildcardStringMatcher(@NotNull String wildcard) {
this.wildcard = wildcard;
this.pattern = Try.of(() -> Pattern.compile(("\\Q" + wildcard + "\\E").replace("*", "\\E.*\\Q")))
.onFailure(ex -> Logging.warning("Failed to compile wildcard '%s': %s",
wildcard, ex.getMessage()))
.getOrNull();
}
/**
* {@inheritDoc}
*/
@Override
public boolean matches(@Nullable String value) {
if (pattern == null || value == null) {
return false;
}
return pattern.matcher(value).matches();
}
}
@@ -181,7 +181,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
if (this instanceof Success) {
return new Success<>(mapper.apply(get()));
} else {
return new Failure<>(getFailureReason(), getFailureMessage());
return new Failure<>((Failure<T, F>) this);
}
}
@@ -196,7 +196,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
if (this instanceof Success) {
return new Success<>(mapper.get());
} else {
return new Failure<>(getFailureReason(), getFailureMessage());
return new Failure<>((Failure<T, F>) this);
}
}
@@ -211,7 +211,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
if (this instanceof Success) {
return mapper.apply(get());
} else {
return new Failure<>(getFailureReason(), getFailureMessage());
return new Failure<>((Failure<T, F>) this);
}
}
@@ -226,7 +226,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
if (this instanceof Success) {
return mapper.get();
} else {
return new Failure<>(getFailureReason(), getFailureMessage());
return new Failure<>((Failure<T, F>) this);
}
}
@@ -241,7 +241,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
if (this instanceof Success) {
return new Success<>(get());
} else {
return new Failure<>(failureReason, getFailureMessage());
return new Failure<>(failureReason, getFailureMessage(), (Failure<T, F>) this);
}
}
@@ -415,10 +415,20 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
final class Failure<T, F extends FailureReason> implements Attempt<T, F> {
private final F failureReason;
private final Message message;
private final Failure<?, ?> causeBy;
Failure(F failureReason, Message message) {
this(failureReason, message, null);
}
Failure(Failure<?, F> failure) {
this(failure.failureReason, failure.message, failure.causeBy);
}
Failure(F failureReason, Message message, Failure<?, ?> causeBy) {
this.failureReason = failureReason;
this.message = message;
this.causeBy = causeBy;
}
@Override
@@ -465,6 +475,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
public String toString() {
return "Failure{"
+ "reason=" + failureReason
+ (causeBy != null ? ", causeBy=" + causeBy : "")
+ '}';
}
}
@@ -3,12 +3,21 @@ package org.mvplugins.multiverse.core.utils.result;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
/**
* Represents a failure reason for an {@link Attempt}.
*/
public interface FailureReason extends MessageKeyProvider {
/**
* A generic failure reason that can be used when no specific reason is applicable.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
FailureReason GENERIC = new FailureReason() { };
default MessageKey getMessageKey() {
return MVCorei18n.GENERIC_FAILURE.getMessageKey();
}
@@ -3,9 +3,21 @@ package org.mvplugins.multiverse.core.utils.result;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
/**
* Represents a success reason for an {@link Attempt}.
*/
public interface SuccessReason extends MessageKeyProvider {
/**
* A generic success reason that can be used when no specific reason is applicable.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
SuccessReason GENERIC = new SuccessReason() { };
default MessageKey getMessageKey() {
return MVCorei18n.GENERIC_SUCCESS.getMessageKey();
}
@@ -4,37 +4,45 @@ import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
final class AdventureTextFormatter implements TextFormatter {
@Override
public void sendFormattedMessage(CommandSender sender, String message) {
sender.sendMessage(colorize(message));
public void sendFormattedMessage(@NotNull CommandSender sender, @Nullable String message) {
String colorizedMessage = colorize(message);
if (colorizedMessage == null) {
return; // Avoid sending null messages
}
sender.sendMessage(colorizedMessage);
}
@Override
public String removeColor(String message) {
TextComponent sectionComponent = LegacyComponentSerializer.legacySection().deserialize(colorize(message));
return PlainTextComponentSerializer.plainText().serialize(sectionComponent);
}
public String removeAmpColor(String message) {
return PlainTextComponentSerializer.plainText().serialize(toAmpComponent(message));
}
public String removeSectionColor(String message) {
return PlainTextComponentSerializer.plainText().serialize(toSectionComponent(message));
public @Nullable String removeColor(@Nullable String message) {
TextComponent sectionComponent = LegacyComponentSerializer.legacySection().deserializeOrNull(colorize(message));
return PlainTextComponentSerializer.plainText().serializeOrNull(sectionComponent);
}
@Override
public String colorize(String message) {
return LegacyComponentSerializer.legacySection().serialize(toAmpComponent(message));
public @Nullable String removeAmpColor(@Nullable String message) {
return PlainTextComponentSerializer.plainText().serializeOrNull(toAmpComponent(message));
}
private TextComponent toAmpComponent(String message) {
return LegacyComponentSerializer.legacyAmpersand().deserialize(message);
@Override
public @Nullable String removeSectionColor(@Nullable String message) {
return PlainTextComponentSerializer.plainText().serializeOrNull(toSectionComponent(message));
}
private TextComponent toSectionComponent(String message) {
return LegacyComponentSerializer.legacySection().deserialize(message);
@Override
public @Nullable String colorize(@Nullable String message) {
return LegacyComponentSerializer.legacySection().serializeOrNull(toAmpComponent(message));
}
private @Nullable TextComponent toAmpComponent(@Nullable String message) {
return LegacyComponentSerializer.legacyAmpersand().deserializeOrNull(message);
}
private @Nullable TextComponent toSectionComponent(@Nullable String message) {
return LegacyComponentSerializer.legacySection().deserializeOrNull(message);
}
}
@@ -2,30 +2,36 @@ package org.mvplugins.multiverse.core.utils.text;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@SuppressWarnings("deprecation")
final class ChatColorTextFormatter implements TextFormatter {
@Override
public void sendFormattedMessage(CommandSender sender, String message) {
public void sendFormattedMessage(@NotNull CommandSender sender, String message) {
sender.sendMessage(colorize(message));
}
@Override
public String removeColor(String message) {
public @Nullable String removeColor(@Nullable String message) {
return ChatColor.stripColor(colorize(message));
}
@Override
public String removeAmpColor(String message) {
public @Nullable String removeAmpColor(@Nullable String message) {
return removeColor(message);
}
@Override
public String removeSectionColor(String message) {
public @Nullable String removeSectionColor(@Nullable String message) {
return ChatColor.stripColor(message);
}
@Override
public String colorize(String message) {
public @Nullable String colorize(@Nullable String message) {
if (message == null) {
return null;
}
return ChatColor.translateAlternateColorCodes('&', message);
}
}
@@ -2,6 +2,8 @@ package org.mvplugins.multiverse.core.utils.text;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
/**
@@ -16,7 +18,8 @@ public final class ChatTextFormatter {
private static final TextFormatter wrapper;
static {
if (ReflectHelper.hasClass("net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer")) {
if (ReflectHelper.hasClass("net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer")
&& ReflectHelper.hasClass("net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer")) {
wrapper = new AdventureTextFormatter();
} else {
wrapper = new ChatColorTextFormatter();
@@ -32,7 +35,7 @@ public final class ChatTextFormatter {
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static void sendFormattedMessage(CommandSender sender, String message) {
public static void sendFormattedMessage(@NotNull CommandSender sender, @Nullable String message) {
wrapper.sendFormattedMessage(sender, message);
}
@@ -44,7 +47,7 @@ public final class ChatTextFormatter {
*
* @since 5.1
*/
public static String removeColor(String message) {
public static @Nullable String removeColor(@Nullable String message) {
return wrapper.removeColor(message);
}
@@ -57,7 +60,7 @@ public final class ChatTextFormatter {
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static String removeAmpColor(String message) {
public static @Nullable String removeAmpColor(@Nullable String message) {
return wrapper.removeAmpColor(message);
}
@@ -70,7 +73,7 @@ public final class ChatTextFormatter {
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static String removeSectionColor(String message) {
public static @Nullable String removeSectionColor(@Nullable String message) {
return wrapper.removeSectionColor(message);
}
@@ -83,7 +86,7 @@ public final class ChatTextFormatter {
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static String colorize(String message) {
public static @Nullable String colorize(@Nullable String message) {
return wrapper.colorize(message);
}
}
@@ -1,15 +1,17 @@
package org.mvplugins.multiverse.core.utils.text;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
interface TextFormatter {
void sendFormattedMessage(CommandSender sender, String message);
void sendFormattedMessage(@NotNull CommandSender sender, @Nullable String message);
String removeColor(String message);
@Nullable String removeColor(@Nullable String message);
String removeAmpColor(String message);
@Nullable String removeAmpColor(@Nullable String message);
String removeSectionColor(String message);
@Nullable String removeSectionColor(@Nullable String message);
String colorize(String message);
@Nullable String colorize(@Nullable String message);
}
@@ -238,7 +238,7 @@ final class WorldConfigNodes {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
if (newValue == null || newValue instanceof NullSpawnLocation) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> {
bukkitWorld.setSpawnLocation(newValue.getBlockX(), newValue.getBlockY(), newValue.getBlockZ());
bukkitWorld.setSpawnLocation(newValue);
newValue.setWorld(bukkitWorld);
});
}));
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.world;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -19,12 +20,12 @@ import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
@@ -45,6 +46,7 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
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.ReflectHelper;
import org.mvplugins.multiverse.core.utils.ServerProperties;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
@@ -62,7 +64,9 @@ import org.mvplugins.multiverse.core.world.options.CreateWorldOptions;
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions;
import org.mvplugins.multiverse.core.world.options.ImportWorldOptions;
import org.mvplugins.multiverse.core.world.options.KeepWorldSettingsOptions;
import org.mvplugins.multiverse.core.world.options.LoadWorldOptions;
import org.mvplugins.multiverse.core.world.options.RegenWorldOptions;
import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions;
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions;
import org.mvplugins.multiverse.core.world.reasons.CloneFailureReason;
import org.mvplugins.multiverse.core.world.reasons.CreateFailureReason;
@@ -140,6 +144,7 @@ public final class WorldManager {
*
* @return The result of the load.
*/
@ApiStatus.Internal
public Try<Void> initAllWorlds() {
return updateWorldsFromConfig().andThenTry(() -> {
importExistingWorlds();
@@ -167,7 +172,11 @@ public final class WorldManager {
}
private void removeWorldsNotInConfigs(Collection<String> removedWorlds) {
removedWorlds.forEach(worldName -> removeWorld(worldName)
removedWorlds.forEach(worldName -> getWorld(worldName)
.fold(
() -> Attempt.failure(FailureReason.GENERIC, Message.of("world already removed")),
world -> removeWorld(RemoveWorldOptions.world(world))
)
.onFailure(failure -> Logging.severe("Failed to unload world %s: %s", worldName, failure))
.onSuccess(success -> Logging.fine("Unloaded world %s as it was removed from config", worldName)));
}
@@ -218,8 +227,9 @@ public final class WorldManager {
if (isLoadedWorld(world) || !world.isAutoLoad()) {
return;
}
loadWorld(world).onFailure(failure -> Logging.severe("Failed to load world %s: %s",
world.getName(), failure));
loadWorld(LoadWorldOptions.world(world))
.onFailure(failure ->
Logging.severe("Failed to load world %s: %s", world.getName(), failure));
});
}
@@ -278,23 +288,30 @@ public final class WorldManager {
* @param options The options for customizing the import of an existing world folder.
* @return The result of the import.
*/
public Attempt<LoadedMultiverseWorld, ImportFailureReason> importWorld(
ImportWorldOptions options) {
return validateImportWorldOptions(options).mapAttempt(this::doImportWorld);
}
private Attempt<ImportWorldOptions, ImportFailureReason> validateImportWorldOptions(
ImportWorldOptions options) {
public Attempt<LoadedMultiverseWorld, ImportFailureReason> importWorld(ImportWorldOptions options) {
String worldName = options.worldName();
if (!worldNameChecker.isValidWorldName(worldName)) {
return worldActionResult(ImportFailureReason.INVALID_WORLDNAME, worldName);
} else if (!worldNameChecker.isValidWorldFolder(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_FOLDER_INVALID, worldName);
} else if (isLoadedWorld(worldName)) {
if (isLoadedWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_LOADED, worldName);
} else if (isWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, worldName);
}
World bukkitWorld = Bukkit.getWorld(worldName);
if (bukkitWorld != null) {
// World is already loaded, maybe by another plugin
return doImportBukkitWorld(options, bukkitWorld);
}
return validateImportWorldOptions(options).mapAttempt(this::doImportWorld);
}
private Attempt<ImportWorldOptions, ImportFailureReason> validateImportWorldOptions(ImportWorldOptions options) {
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);
}
return worldActionResult(options);
}
@@ -319,6 +336,23 @@ public final class WorldManager {
});
}
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportBukkitWorld(ImportWorldOptions options, World bukkitWorld) {
if (options.environment() != bukkitWorld.getEnvironment()) {
return Attempt.failure(ImportFailureReason.BUKKIT_ENVIRONMENT_MISMATCH,
Replace.WORLD.with(bukkitWorld.getName()),
replace("{bukkitEnvironment}").with(bukkitWorld.getEnvironment().name()),
replace("{mvEnvironment}").with(options.environment().name()));
}
LoadedMultiverseWorld loadedWorld = newLoadedMultiverseWorld(
bukkitWorld,
generatorProvider.parseGeneratorString(options.worldName(), options.generator()),
options.biome(),
options.useSpawnAdjust());
pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld));
return Attempt.success(loadedWorld);
}
private MultiverseWorld newMultiverseWorld(String worldName, WorldConfig worldConfig) {
MultiverseWorld mvWorld = new MultiverseWorld(worldName, worldConfig, config);
worldsMap.put(mvWorld.getName(), mvWorld);
@@ -371,10 +405,14 @@ public final class WorldManager {
*
* @param worldName The name of the world to load.
* @return The result of the load.
*
* @deprecated Use {@link #loadWorld(LoadWorldOptions)} instead.
*/
@Deprecated(since = "5.2", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public Attempt<LoadedMultiverseWorld, LoadFailureReason> loadWorld(@NotNull String worldName) {
return getWorld(worldName)
.map(this::loadWorld)
.map(world -> loadWorld(LoadWorldOptions.world(world)))
.getOrElse(() -> worldNameChecker.isValidWorldFolder(worldName)
? worldActionResult(LoadFailureReason.WORLD_EXIST_FOLDER, worldName)
: worldActionResult(LoadFailureReason.WORLD_NON_EXISTENT, worldName));
@@ -385,12 +423,30 @@ public final class WorldManager {
*
* @param world The world to load.
* @return The result of the load.
*
* @deprecated Use {@link #loadWorld(LoadWorldOptions)} instead.
*/
@Deprecated(since = "5.2", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public Attempt<LoadedMultiverseWorld, LoadFailureReason> loadWorld(@NotNull MultiverseWorld world) {
return validateWorldToLoad(world).mapAttempt(this::doLoadWorld);
return loadWorld(LoadWorldOptions.world(world));
}
private Attempt<MultiverseWorld, LoadFailureReason> validateWorldToLoad(@NotNull MultiverseWorld mvWorld) {
/**
* Loads an existing world in config.
*
* @param options The options for customizing the loading of a world.
* @return The result of the load.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public Attempt<LoadedMultiverseWorld, LoadFailureReason> loadWorld(@NotNull LoadWorldOptions options) {
return validateWorldToLoad(options).mapAttempt(this::doLoadWorld);
}
private Attempt<LoadWorldOptions, LoadFailureReason> validateWorldToLoad(@NotNull LoadWorldOptions options) {
MultiverseWorld mvWorld = options.world();
if (loadTracker.contains(mvWorld.getName())) {
// This is to prevent recursive calls by WorldLoadEvent
Logging.fine("World already loading: " + mvWorld.getName());
@@ -399,15 +455,19 @@ public final class WorldManager {
Logging.severe("World already loaded: " + mvWorld.getName());
return worldActionResult(LoadFailureReason.WORLD_EXIST_LOADED, mvWorld.getName());
}
return worldActionResult(mvWorld);
return worldActionResult(options);
}
private Attempt<LoadedMultiverseWorld, LoadFailureReason> doLoadWorld(@NotNull MultiverseWorld mvWorld) {
private Attempt<LoadedMultiverseWorld, LoadFailureReason> doLoadWorld(@NotNull LoadWorldOptions options) {
MultiverseWorld mvWorld = options.world();
World bukkitWorld = Bukkit.getWorld(mvWorld.getName());
if (bukkitWorld != null) {
// World already loaded, maybe by another plugin
Logging.finer("World already loaded in bukkit: " + mvWorld.getName());
return newLoadedMultiverseWorld(mvWorld, bukkitWorld);
return doLoadBukkitWorld(bukkitWorld, mvWorld);
}
if (options.doFolderCheck() && !worldNameChecker.isValidWorldFolder(mvWorld.getName())) {
return worldActionResult(LoadFailureReason.WORLD_FOLDER_INVALID, mvWorld.getName());
}
WorldCreator worldCreator = WorldCreator.name(mvWorld.getName())
@@ -420,6 +480,18 @@ public final class WorldManager {
.mapAttempt(newBukkitWorld -> newLoadedMultiverseWorld(mvWorld, newBukkitWorld));
}
private @NotNull Attempt<LoadedMultiverseWorld, LoadFailureReason> doLoadBukkitWorld(World bukkitWorld, MultiverseWorld mvWorld) {
if (bukkitWorld.getEnvironment() != mvWorld.getEnvironment()) {
return Attempt.failure(LoadFailureReason.BUKKIT_ENVIRONMENT_MISMATCH,
Replace.WORLD.with(mvWorld.getName()),
replace("{bukkitEnvironment}").with(bukkitWorld.getEnvironment().name()),
replace("{mvEnvironment}").with(mvWorld.getEnvironment().name()));
}
// World already loaded, maybe by another plugin
Logging.finer("World already loaded in bukkit: " + mvWorld.getName());
return newLoadedMultiverseWorld(mvWorld, bukkitWorld);
}
private Attempt<LoadedMultiverseWorld, LoadFailureReason> newLoadedMultiverseWorld(MultiverseWorld mvWorld, World bukkitWorld) {
WorldConfig worldConfig = worldsConfigManager.getWorldConfig(mvWorld.getName()).get(); //TODO: null check here, but logically it should never be null.
LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld(
@@ -481,11 +553,14 @@ public final class WorldManager {
*
* @param worldName The name of the world to remove.
* @return The result of the remove.
*
* @deprecated Get the {@link MultiverseWorld} yourself and use {@link #removeWorld(RemoveWorldOptions)} instead.
*/
public Attempt<String, RemoveFailureReason> removeWorld(
@NotNull String worldName) {
@Deprecated(since = "5.2", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public Attempt<String, RemoveFailureReason> removeWorld(@NotNull String worldName) {
return getWorld(worldName)
.map(this::removeWorld)
.map(world -> removeWorld(RemoveWorldOptions.world(world)))
.getOrElse(() -> worldActionResult(RemoveFailureReason.WORLD_NON_EXISTENT, worldName));
}
@@ -495,11 +570,13 @@ public final class WorldManager {
*
* @param world The multiverse world to remove.
* @return The result of the remove.
*
* @deprecated Use {@link #removeWorld(RemoveWorldOptions)} instead.
*/
@Deprecated(since = "5.2", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public Attempt<String, RemoveFailureReason> removeWorld(@NotNull MultiverseWorld world) {
return getLoadedWorld(world).fold(
() -> removeWorldFromConfig(world),
this::removeWorld);
return removeWorld(RemoveWorldOptions.world(world));
}
/**
@@ -508,9 +585,40 @@ public final class WorldManager {
*
* @param loadedWorld The multiverse world to remove.
* @return The result of the remove.
*
* @deprecated Use {@link #removeWorld(RemoveWorldOptions)} instead.
*/
@Deprecated(since = "5.2", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public Attempt<String, RemoveFailureReason> removeWorld(@NotNull LoadedMultiverseWorld loadedWorld) {
return unloadWorld(UnloadWorldOptions.world(loadedWorld))
return removeWorld(RemoveWorldOptions.world(loadedWorld));
}
/**
* Removes an existing multiverse world. It will be deleted from the worlds config and will no longer be known
* to Multiverse. World files will not be deleted.
*
* @param options The options for customizing the removal of a world.
* @return The result of the remove action.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public Attempt<String, RemoveFailureReason> removeWorld(@NotNull RemoveWorldOptions options) {
MultiverseWorld world = options.world();
return getLoadedWorld(world).fold(
() -> removeWorldFromConfig(world),
loadedWorld -> unloadBeforeRemoveWorld(loadedWorld, options)
);
}
private Attempt<String, RemoveFailureReason> unloadBeforeRemoveWorld(@NotNull LoadedMultiverseWorld loadedWorld,
@NotNull RemoveWorldOptions options) {
UnloadWorldOptions unloadWorldOptions = UnloadWorldOptions.world(loadedWorld)
.saveBukkitWorld(options.saveBukkitWorld())
.unloadBukkitWorld(options.unloadBukkitWorld());
return unloadWorld(unloadWorldOptions)
.transform(RemoveFailureReason.UNLOAD_FAILED)
.mapAttempt(this::removeWorldFromConfig);
}
@@ -541,7 +649,7 @@ public final class WorldManager {
*/
public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull DeleteWorldOptions options) {
return getLoadedWorld(options.world()).fold(
() -> loadWorld(options.world())
() -> loadWorld(LoadWorldOptions.world(options.world()))
.transform(DeleteFailureReason.LOAD_FAILED)
.mapAttempt(world -> doDeleteWorld(world, options)),
world -> doDeleteWorld(world, options));
@@ -564,7 +672,11 @@ public final class WorldManager {
? Attempt.failure(DeleteFailureReason.EVENT_CANCELLED)
: Attempt.success(null);
})
.mapAttempt(() -> removeWorld(world).transform(DeleteFailureReason.REMOVE_FAILED))
.mapAttempt(() -> removeWorld(RemoveWorldOptions
.world(world)
.unloadBukkitWorld(true)
.saveBukkitWorld(false)
).transform(DeleteFailureReason.REMOVE_FAILED))
.mapAttempt(() -> fileUtils.deleteFolder(worldFolder.get(), options.keepFiles()).fold(
exception -> worldActionResult(DeleteFailureReason.FAILED_TO_DELETE_FOLDER,
world.getName(), exception),
@@ -632,7 +744,7 @@ public final class WorldManager {
private Attempt<CloneWorldOptions, CloneFailureReason> cloneWorldCopyFolder(@NotNull CloneWorldOptions options) {
if (options.saveBukkitWorld()) {
Logging.finer("Saving bukkit world before cloning: " + options.world().getName());
options.world().getBukkitWorld().peek(World::save);
options.world().getBukkitWorld().peek(this::saveWorldWithFlush);
}
File worldFolder = options.world().getBukkitWorld().map(World::getWorldFolder).get();
File newWorldFolder = new File(Bukkit.getWorldContainer(), options.newWorldName());
@@ -642,6 +754,17 @@ public final class WorldManager {
success -> worldActionResult(options));
}
// This method is only available since 1.21
private final Method saveWithFlush = ReflectHelper.getMethod(World.class, "save", boolean.class);
private void saveWorldWithFlush(World world) {
if (saveWithFlush != null) {
Logging.fine("Using world save method with flush...");
ReflectHelper.invokeMethod(world, saveWithFlush, true);
} else {
world.save();
}
}
private void cloneWorldTransferData(@NotNull CloneWorldOptions options, @NotNull LoadedMultiverseWorld newWorld) {
DataTransfer<LoadedMultiverseWorld> dataTransfer = transferData(options, options.world());
dataTransfer.pasteAllTo(newWorld);
@@ -901,6 +1024,9 @@ public final class WorldManager {
}
private Option<MultiverseWorld> getUnloadedWorldByAlias(@Nullable String alias) {
if (alias == null || alias.isEmpty()) {
return Option.none();
}
String colourlessAlias = ChatTextFormatter.removeColor(alias);
return Option.ofOptional(worldsMap.values().stream()
.filter(world -> !world.isLoaded())
@@ -936,7 +1062,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable World world) {
return world == null ? Option.none() : Option.of((LoadedMultiverseWorld) loadedWorldsMap.get(world.getName()));
return world == null ? Option.none() : Option.of(loadedWorldsMap.get(world.getName()));
}
/**
@@ -946,7 +1072,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable MultiverseWorld world) {
return world == null ? Option.none() : Option.of((LoadedMultiverseWorld) loadedWorldsMap.get(world.getName()));
return world == null ? Option.none() : Option.of(loadedWorldsMap.get(world.getName()));
}
/**
@@ -956,7 +1082,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable String worldName) {
return Option.of((LoadedMultiverseWorld) loadedWorldsMap.get(worldName));
return Option.of(loadedWorldsMap.get(worldName));
}
/**
@@ -971,9 +1097,12 @@ public final class WorldManager {
}
private Option<LoadedMultiverseWorld> getLoadedWorldByAlias(@Nullable String alias) {
if (alias == null || alias.isEmpty()) {
return Option.none();
}
return Option.ofOptional(loadedWorldsMap.values().stream()
.filter(world -> world.getColourlessAlias().equalsIgnoreCase(ChatColor.stripColor(alias)))
.map(world -> (LoadedMultiverseWorld) world)
.filter(world -> world.getColourlessAlias()
.equalsIgnoreCase(ChatTextFormatter.removeColor(alias)))
.findFirst());
}
@@ -984,7 +1113,6 @@ public final class WorldManager {
*/
public Collection<LoadedMultiverseWorld> getLoadedWorlds() {
return loadedWorldsMap.values().stream()
.map(world -> (LoadedMultiverseWorld) world)
.toList();
}
@@ -84,7 +84,7 @@ final class WorldsConfigManager {
Path oldWorldConfig = worldConfigFile.toPath().getParent().resolve(CONFIG_FILENAME + ".old");
Files.copy(worldConfigFile.toPath(), oldWorldConfig, COPY_ATTRIBUTES);
return configData.replace("==: MVWorld", "")
return configData.replace("==: MVWorld", "w@: world")
.replace("==: MVSpawnSettings", "")
.replace("==: MVSpawnSubSettings", "")
.replace("==: MVEntryFee", "");
@@ -92,16 +92,26 @@ final class WorldsConfigManager {
.andThenTry(configData -> Files.writeString(worldConfigFile.toPath(), configData))
.andThenTry(() -> {
YamlConfiguration config = YamlConfiguration.loadConfiguration(worldConfigFile);
List<ConfigurationSection> worlds = config.getConfigurationSection("worlds")
.getKeys(false)
.stream()
.map(worldName -> config.getConfigurationSection("worlds." + worldName))
.toList();
ConfigurationSection worldsSection = config.getConfigurationSection("worlds");
if (worldsSection == null) {
worldsSection = config.createSection("worlds");
}
List<String> worldNames = getOldConfigWorldNames(worldsSection);
Map<String, ConfigurationSection> worldConfigMap = new HashMap<>();
for (String worldName : worldNames) {
ConfigurationSection worldSection = worldsSection.getConfigurationSection(worldName);
if (worldSection != null) {
worldConfigMap.put(worldName, worldSection);
}
}
config.set("worlds", null);
for (ConfigurationSection world : worlds) {
config.createSection(world.getName(), world.getValues(true));
for (Map.Entry<String, ConfigurationSection> entry : worldConfigMap.entrySet()) {
config.set(encodeWorldName(entry.getKey()), entry.getValue());
}
config.save(worldConfigFile);
})
@@ -115,6 +125,35 @@ final class WorldsConfigManager {
});
}
private @NotNull List<String> getOldConfigWorldNames(ConfigurationSection worldsSection) {
List<String> worldNames = new ArrayList<>();
recursiveGetOldConfigWorldNames(worldsSection, worldNames);
return worldNames;
}
private void recursiveGetOldConfigWorldNames(ConfigurationSection section, List<String> worldNames) {
Set<String> keys = section.getKeys(false);
if (keys.isEmpty()) {
// No keys in this section, nothing to do
return;
}
if (keys.contains("w@")) {
// this is the world data section already, get path without the "worlds." prefix
worldNames.add(section.getCurrentPath().substring(7));
return;
}
for (String key : keys) {
ConfigurationSection dataSection = section.getConfigurationSection(key);
if (dataSection == null) {
// Something is wrong with the config, skip this key
continue;
}
recursiveGetOldConfigWorldNames(dataSection, worldNames);
}
}
/**
* Parses the worlds.yml file and creates a WorldConfig for each world in the file if it doesn't already exist.
*
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.world.options;
import org.bukkit.World;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -24,6 +25,7 @@ public final class ImportWorldOptions {
private World.Environment environment = World.Environment.NORMAL;
private String generator = null;
private boolean useSpawnAdjust = true;
private boolean doFolderCheck = true;
ImportWorldOptions(String worldName) {
this.worldName = worldName;
@@ -119,4 +121,30 @@ public final class ImportWorldOptions {
public boolean useSpawnAdjust() {
return useSpawnAdjust;
}
/**
* Sets whether to ensure the world folder is a valid world before importing it.
*
* @param doFolderCheckInput Whether to do the folder check
* @return This {@link ImportWorldOptions} instance
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public @NotNull ImportWorldOptions doFolderCheck(boolean doFolderCheckInput) {
this.doFolderCheck = doFolderCheckInput;
return this;
}
/**
* Gets whether to ensure the world folder is a valid world before importing it.
*
* @return Whether to do the folder check
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean doFolderCheck() {
return doFolderCheck;
}
}
@@ -0,0 +1,72 @@
package org.mvplugins.multiverse.core.world.options;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
/**
* Options for customizing the loading of a world.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public final class LoadWorldOptions {
/**
* Creates a new {@link LoadWorldOptions} instance with the given world.
*
* @param world The world to load.
* @return A new {@link LoadWorldOptions} instance.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public static LoadWorldOptions world(MultiverseWorld world) {
return new LoadWorldOptions(world);
}
private final MultiverseWorld world;
private boolean doFolderCheck = true;
LoadWorldOptions(MultiverseWorld world) {
this.world = world;
}
/**
* Gets the world to load.
*
* @return The world to load.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public MultiverseWorld world() {
return world;
}
/**
* Sets whether to check if the world folder is valid before loading the world.
* <br />
* This helps to prevent deleted world folders from being re-created.
*
* @return Whether to check if the world folder is valid before loading the world.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean doFolderCheck() {
return doFolderCheck;
}
/**
* Sets whether to check if the world folder is valid before loading the world.
*
* @return This {@link LoadWorldOptions} instance.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public LoadWorldOptions doFolderCheck(boolean doFolderCheck) {
this.doFolderCheck = doFolderCheck;
return this;
}
}
@@ -0,0 +1,103 @@
package org.mvplugins.multiverse.core.world.options;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
/**
* Options for customizing the removal of a world.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public final class RemoveWorldOptions {
/**
* Creates a new {@link RemoveWorldOptions} instance with the given world. The world may be a loaded or unloaded.
*
* @param world The world to remove.
* @return A new {@link RemoveWorldOptions} instance.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public static RemoveWorldOptions world(MultiverseWorld world) {
return new RemoveWorldOptions(world);
}
private final MultiverseWorld world;
private boolean saveBukkitWorld = true;
private boolean unloadBukkitWorld = true;
private RemoveWorldOptions(MultiverseWorld world) {
this.world = world;
}
/**
* Gets the world to remove. The world may be a loaded or unloaded.
*
* @return The world to remove.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public MultiverseWorld world() {
return world;
}
/**
* Sets whether to save the Bukkit world before removing it.
* <br />
* This option only applies when {@link #unloadBukkitWorld()} is true.
*
* @param saveBukkitWorldInput Whether to save the Bukkit world.
* @return This {@link RemoveWorldOptions} instance.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public RemoveWorldOptions saveBukkitWorld(boolean saveBukkitWorldInput) {
this.saveBukkitWorld = saveBukkitWorldInput;
return this;
}
/**
* Gets whether to save the Bukkit world before removing it. By default, this is true.
* <br />
* This option only applies when {@link #unloadBukkitWorld()} is true.
*
* @return Whether to save the Bukkit world.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean saveBukkitWorld() {
return saveBukkitWorld;
}
/**
* Sets whether to unload the Bukkit world before removing it. This option is usually only used if the world is
* managed by another plugin and you want to untrack it from Multiverse without unloading from the server.
*
* @param unloadBukkitWorldInput Whether to unload the Bukkit world.
* @return This {@link RemoveWorldOptions} instance.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public RemoveWorldOptions unloadBukkitWorld(boolean unloadBukkitWorldInput) {
this.unloadBukkitWorld = unloadBukkitWorldInput;
return this;
}
/**
* Gets whether to unload the Bukkit world before removing it. By default, this is true.
*
* @return Whether to unload the Bukkit world.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean unloadBukkitWorld() {
return unloadBukkitWorld;
}
}
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.world.reasons;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
@@ -30,6 +31,14 @@ public enum ImportFailureReason implements FailureReason {
*/
WORLD_EXIST_LOADED(MVCorei18n.IMPORTWORLD_WORLDEXISTLOADED),
/**
* The import environment input does not match the loaded Bukkit world's environment.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
BUKKIT_ENVIRONMENT_MISMATCH(MVCorei18n.IMPORTWORLD_BUKKITENVIRONMENTMISMATCH),
/**
* Bukkit API failed to create the world.
*/
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.world.reasons;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
@@ -25,11 +26,27 @@ public enum LoadFailureReason implements FailureReason {
*/
WORLD_EXIST_FOLDER(MVCorei18n.LOADWORLD_WORLDEXISTFOLDER),
/**
* The world folder is invalid.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
WORLD_FOLDER_INVALID(MVCorei18n.IMPORTWORLD_WORLDFOLDERINVALID),
/**
* The world is already loaded.
*/
WORLD_EXIST_LOADED(MVCorei18n.LOADWORLD_WORLDEXISTLOADED),
/**
* The mv world's environment does not match the loaded Bukkit world's environment.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
BUKKIT_ENVIRONMENT_MISMATCH(MVCorei18n.LOADWORLD_BUKKITENVIRONMENTMISMATCH),
/**
* Bukkit API failed to create the world.
*/
@@ -3,8 +3,10 @@ package org.mvplugins.multiverse.core.world.reasons;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.WorldManager;
/**
* Result of a world removal operation.
@@ -12,7 +14,11 @@ import org.mvplugins.multiverse.core.utils.result.FailureReason;
public enum RemoveFailureReason implements FailureReason {
/**
* The world does not exist.
*
* @deprecated No longer in use as {@link WorldManager#removeWorld(String)} is now deprecated.
*/
@Deprecated(forRemoval = true, since = "5.2")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
WORLD_NON_EXISTENT(MVCorei18n.REMOVEWORLD_WORLDNONEXISTENT),
/**
@@ -288,12 +288,14 @@ mv-core.deleteworld.failedtodeletefolder=Failed to delete world folder '{world}'
mv-core.importworld.invalidworldname=World '{world}' contains invalid characters!
mv-core.importworld.worldexistunloaded=World '{world}' already exists, but it's not loaded!&f Type '&a/mv load {world}&f' if you wish to load it.
mv-core.importworld.worldexistloaded=World '{world}' already exists!
mv-core.importworld.worldfolderinvalid=World '{world}' folder contents does not seem to be a valid world!
mv-core.importworld.worldfolderinvalid=World '{world}' folder contents does not seem to be a valid world!\n&cIf the server software does something different with world folders, or you are very certain the world is valid, use '&3--skip-folder-check&c' flag.
mv-core.importworld.bukkitenvironmentmismatch=Environment mismatch detected!&f The world '{world}' is already loaded with environment '{bukkitEnvironment}', but Multiverse is trying to import it with environment '{mvEnvironment}'.
mv-core.loadworld.worldalreadyloading=World '{world}' is already loading! Please wait...
mv-core.loadworld.worldnonexistent=World '{world}' not found! Use '&a/mv create {world} <environment>&f' to create it.
mv-core.loadworld.worldexistfolder=World '{world}' exists in server folders, but it's not known to Multiverse!&f Type '&a/mv import {world} <environment>&f' if you wish to import it.
mv-core.loadworld.worldexistloaded=World '{world}' is already loaded!
mv-core.loadworld.bukkitenvironmentmismatch=Environment mismatch detected!&f The world '{world}' is already loaded with environment '{bukkitEnvironment}', but Multiverse is trying to load it with environment '{mvEnvironment}'.
mv-core.removeworld.worldnonexistent=World '{world}' not found!
@@ -0,0 +1,58 @@
package org.mvplugins.multiverse.core.utils
import org.mvplugins.multiverse.core.utils.matcher.ExactStringMatcher
import org.mvplugins.multiverse.core.utils.matcher.RegexStringMatcher
import org.mvplugins.multiverse.core.utils.matcher.WildcardStringMatcher
import kotlin.test.Test
class StringMatcherTest {
@Test
fun `exact string matcher - single string`() {
val matcher = ExactStringMatcher("test")
assert(matcher.matches("test"))
assert(!matcher.matches("Test"))
assert(!matcher.matches("testing"))
}
@Test
fun `exact string matcher - multiple strings`() {
val matcher = ExactStringMatcher(setOf("test", "example", "sample"));
assert(matcher.matches("test"))
assert(matcher.matches("example"))
assert(matcher.matches("sample"))
assert(!matcher.matches("Test"))
assert(!matcher.matches("testing"))
}
@Test
fun `wildcard string matcher - single`() {
val matcher = WildcardStringMatcher("test*")
assert(matcher.matches("test"))
assert(matcher.matches("test123"))
assert(matcher.matches("testing"))
assert(!matcher.matches("TEST"))
assert(!matcher.matches("nottest123"))
}
@Test
fun `wildcard string matcher - multiple`() {
val matcher = WildcardStringMatcher("test-*_*-world")
assert(matcher.matches("test-123_pl-world"))
assert(matcher.matches("test-abc_xyz-world"))
assert(matcher.matches("test-hello_world-world"))
assert(!matcher.matches("test-hello-world"))
assert(!matcher.matches("test-hello_world-world!"))
assert(!matcher.matches("TEST-123_PL-WORLD"))
}
@Test
fun `regex string matcher`() {
val matcher = RegexStringMatcher("r=^test[0-9]+$")
assert(matcher.matches("test123"))
assert(matcher.matches("test456"))
assert(!matcher.matches("test"))
assert(!matcher.matches("TEST123"))
assert(!matcher.matches("testing"))
}
}
@@ -49,7 +49,7 @@ class WorldConfigMangerTest : TestWithMockBukkit() {
assertEquals(MVEconomist.VAULT_ECONOMY_MATERIAL, endWorldConfig.entryFeeCurrency)
assertEquals(0.0, endWorldConfig.entryFeeAmount)
val worldConfig = worldConfigManager.getWorldConfig("world").orNull
val worldConfig = worldConfigManager.getWorldConfig("world.a.b").orNull
assertNotNull(worldConfig)
assertEquals(-5176596003035866649, worldConfig.seed)
@@ -58,6 +58,9 @@ class WorldConfigMangerTest : TestWithMockBukkit() {
assertEquals(Material.DIRT, worldConfig.entryFeeCurrency)
assertEquals(5.0, worldConfig.entryFeeAmount)
val world2Config = worldConfigManager.getWorldConfig("world.a.c").orNull
assertNotNull(world2Config)
assertConfigEquals("/worlds/migrated_worlds.yml", "worlds.yml")
}
@@ -10,7 +10,9 @@ import org.mvplugins.multiverse.core.event.world.*
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions
import org.mvplugins.multiverse.core.world.options.CreateWorldOptions
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions
import org.mvplugins.multiverse.core.world.options.LoadWorldOptions
import org.mvplugins.multiverse.core.world.options.RegenWorldOptions
import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions
import org.mvplugins.multiverse.core.world.reasons.CloneFailureReason
import org.mvplugins.multiverse.core.world.reasons.CreateFailureReason
@@ -102,7 +104,7 @@ class WorldManagerTest : TestWithMockBukkit() {
@Test
fun `Remove world`() {
assertTrue(worldManager.removeWorld(world).isSuccess)
assertTrue(worldManager.removeWorld(RemoveWorldOptions.world(world)).isSuccess)
assertFalse(worldManager.getWorld("world").isDefined)
assertFalse(worldManager.getLoadedWorld("world").isDefined)
assertFalse(worldManager.getUnloadedWorld("world").isDefined)
@@ -132,13 +134,23 @@ class WorldManagerTest : TestWithMockBukkit() {
assertTrue(worldManager.getWorld("world2").isDefined)
assertTrue(worldManager.getUnloadedWorld("world2").isDefined)
assertTrue(worldManager.loadWorld("world2").isSuccess)
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)
}
@Test
fun `Load world failed - invalid world folder`() {
assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world2)).isSuccess)
File(Bukkit.getWorldContainer(), "world2/").deleteRecursively()
assertEquals(
LoadFailureReason.WORLD_FOLDER_INVALID,
worldManager.loadWorld(LoadWorldOptions.world(world2)).failureReason
)
}
@Test
fun `Load world failed - non-existent world`() {
assertEquals(
+50 -1
View File
@@ -46,7 +46,7 @@ world_the_end:
tick-rate: -1
world-blacklist: []
version: 1.2
world:
world[dot]a[dot]b:
adjust-spawn: true
alias: ''
allow-advancement-grant: true
@@ -95,3 +95,52 @@ world:
world-blacklist:
- test
version: 1.2
world[dot]a[dot]c:
adjust-spawn: true
alias: ''
allow-advancement-grant: true
allow-flight: false
allow-weather: true
anchor-respawn: true
auto-heal: true
auto-load: true
bed-respawn: true
difficulty: normal
entry-fee:
enabled: true
amount: 5.0
currency: DIRT
environment: normal
gamemode: survival
biome: ''
generator: ''
hidden: false
hunger: true
keep-spawn-in-memory: true
player-limit: -1
portal-form: all
pvp: true
respawn-world: ''
scale: 1.0
seed: -5176596003035866649
spawn-location:
==: MVSpawnLocation
x: -64.0
y: 64.0
z: 48.0
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
world-blacklist:
- test
version: 1.2
+50 -1
View File
@@ -46,7 +46,7 @@ worlds:
generator: 'null'
playerLimit: '-1'
allowFlight: 'true'
world:
world.a.b:
==: MVWorld
hidden: 'false'
alias: ''
@@ -95,3 +95,52 @@ worlds:
generator: 'null'
playerLimit: '-1'
allowFlight: 'false'
world.a.c:
==: MVWorld
hidden: 'false'
alias: ''
color: WHITE
style: NORMAL
pvp: 'true'
scale: '1.0'
respawnWorld: ''
allowWeather: 'true'
difficulty: NORMAL
spawning:
==: MVSpawnSettings
animals:
==: MVSpawnSubSettings
spawn: 'true'
spawnrate: '-1'
exceptions: []
monsters:
==: MVSpawnSubSettings
spawn: 'true'
spawnrate: '-1'
exceptions: []
entryfee:
==: MVEntryFee
amount: '5.0'
currency: 'DIRT'
hunger: 'true'
autoHeal: 'true'
adjustSpawn: 'true'
portalForm: ALL
gameMode: SURVIVAL
keepSpawnInMemory: 'true'
spawnLocation:
==: MVSpawnLocation
x: -64.0
y: 64.0
z: 48.0
pitch: 0.0
yaw: 0.0
autoLoad: 'true'
bedRespawn: 'true'
worldBlacklist:
- test
environment: NORMAL
seed: '-5176596003035866649'
generator: 'null'
playerLimit: '-1'
allowFlight: 'false'