diff --git a/build.gradle b/build.gradle index 1bd04627a..c94a90c0d 100644 --- a/build.gradle +++ b/build.gradle @@ -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 diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/ImportCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/ImportCommand.java index 2f4ca5abd..b0261aa44 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/ImportCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/ImportCommand.java @@ -68,7 +68,8 @@ void onImportCommand( .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 @@ private Flags(@NotNull CommandFlagsManager flagsManager, @NotNull GeneratorProvi private final CommandValueFlag biome = flag(CommandValueFlag.builder("--biome", String.class) .addAlias("-b") .build()); + + private final CommandFlag skipFolderCheck = flag(CommandFlag.builder("--skip-folder-check") + .addAlias("-f") + .build()); } @Service diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/RemoveCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/RemoveCommand.java index 61bdcf7bb..0054cfd2c 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/RemoveCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/RemoveCommand.java @@ -15,6 +15,8 @@ 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.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("") @Description("{@@mv-core.remove.description}") void onRemoveCommand( @@ -64,12 +67,14 @@ void onRemoveCommand( ? 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 @@ private void doWorldRemoving(MVCommandIssuer issuer, MultiverseWorld world) { }); } + @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); } diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/SpawnCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/SpawnCommand.java index 047e48e67..d6cdc4273 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/SpawnCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/SpawnCommand.java @@ -108,7 +108,7 @@ private void handleSingleTeleport(MVCommandIssuer issuer, LoadedMultiverseWorld 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 @@ private void handleSingleTeleport(MVCommandIssuer issuer, LoadedMultiverseWorld ? 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, diff --git a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java index 5dc0d21a3..8b10c33e7 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java @@ -518,6 +518,7 @@ private N node(N node) { .build()); final ConfigNode 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) diff --git a/src/main/java/org/mvplugins/multiverse/core/display/filters/RegexContentFilter.java b/src/main/java/org/mvplugins/multiverse/core/display/filters/RegexContentFilter.java index a210f73cb..c34beb777 100644 --- a/src/main/java/org/mvplugins/multiverse/core/display/filters/RegexContentFilter.java +++ b/src/main/java/org/mvplugins/multiverse/core/display/filters/RegexContentFilter.java @@ -69,9 +69,12 @@ public boolean checkMatch(String value) { 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; diff --git a/src/main/java/org/mvplugins/multiverse/core/listeners/MVChatListener.java b/src/main/java/org/mvplugins/multiverse/core/listeners/MVChatListener.java index f2de2659c..948c990af 100644 --- a/src/main/java/org/mvplugins/multiverse/core/listeners/MVChatListener.java +++ b/src/main/java/org/mvplugins/multiverse/core/listeners/MVChatListener.java @@ -88,8 +88,9 @@ void asyncPlayerChat(AsyncPlayerChatEvent event) { 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) { diff --git a/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java b/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java index 78f6468c5..6ef1af267 100644 --- a/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java +++ b/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java @@ -41,6 +41,7 @@ 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 playerWorld = new ConcurrentHashMap<>(); @@ -86,7 +88,8 @@ final class MVPlayerListener implements CoreListener { Provider 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 @@ private void handleJoinLocation(PlayerSpawnLocationEvent event) { 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()) diff --git a/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java b/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java index 67d4ec238..0c35bf0f4 100644 --- a/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java +++ b/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java @@ -307,11 +307,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, diff --git a/src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissions.java b/src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissions.java index abe0e2faf..54060ced7 100644 --- a/src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissions.java +++ b/src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissions.java @@ -2,6 +2,7 @@ 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 @@ @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 addWorldPermissions(@NotNull MultiverseWorld world) { return Try.run(() -> { pluginManager.addPermission(new Permission( diff --git a/src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissionsChecker.java b/src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissionsChecker.java index 487eb43e8..231850833 100644 --- a/src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissionsChecker.java +++ b/src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissionsChecker.java @@ -3,6 +3,7 @@ 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. * diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/matcher/ExactStringMatcher.java b/src/main/java/org/mvplugins/multiverse/core/utils/matcher/ExactStringMatcher.java new file mode 100644 index 000000000..36e33dafa --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/matcher/ExactStringMatcher.java @@ -0,0 +1,73 @@ +package org.mvplugins.multiverse.core.utils.matcher; + +import org.jetbrains.annotations.ApiStatus; + +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 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(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(Collection 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(String value) { + this.exactMatches.add(value); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean matches(String value) { + return exactMatches.contains(value); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/matcher/MatcherGroup.java b/src/main/java/org/mvplugins/multiverse/core/utils/matcher/MatcherGroup.java new file mode 100644 index 000000000..7f8307156 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/matcher/MatcherGroup.java @@ -0,0 +1,103 @@ +package org.mvplugins.multiverse.core.utils.matcher; + +import com.dumptruckman.minecraft.util.Logging; +import org.jetbrains.annotations.ApiStatus; + +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 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(Collection 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(String matchString) { + if (isExact(matchString)) { + Logging.warning("Exact: " + matchString); + exactMatcher.addExactMatch(matchString); + } else { + stringMatchers.add(StringMatcher.fromString(matchString)); + } + } + + private boolean isExact(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(StringMatcher matcher) { + stringMatchers.add(matcher); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean matches(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 + + '}'; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/matcher/RegexStringMatcher.java b/src/main/java/org/mvplugins/multiverse/core/utils/matcher/RegexStringMatcher.java new file mode 100644 index 000000000..80bc69dee --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/matcher/RegexStringMatcher.java @@ -0,0 +1,45 @@ +package org.mvplugins.multiverse.core.utils.matcher; + +import org.jetbrains.annotations.ApiStatus; + +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 String regexString; + private final 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(String regexString) { + this.regexString = regexString; + this.regexPattern = compileRegex(regexString); + } + + private Pattern compileRegex(String regexString) { + if (regexString.startsWith("r=")) { + regexString = regexString.substring(2); + } + return Pattern.compile(regexString); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean matches(String value) { + return regexPattern.matcher(value).matches(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/matcher/StringMatcher.java b/src/main/java/org/mvplugins/multiverse/core/utils/matcher/StringMatcher.java new file mode 100644 index 000000000..d77e64559 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/matcher/StringMatcher.java @@ -0,0 +1,65 @@ +package org.mvplugins.multiverse.core.utils.matcher; + +import org.jetbrains.annotations.ApiStatus; + +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. + *
+ * 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 StringMatcher fromString(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(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 List filter(List values) { + return values.stream() + .filter(this::matches) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/matcher/WildcardStringMatcher.java b/src/main/java/org/mvplugins/multiverse/core/utils/matcher/WildcardStringMatcher.java new file mode 100644 index 000000000..86cded1de --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/matcher/WildcardStringMatcher.java @@ -0,0 +1,40 @@ +package org.mvplugins.multiverse.core.utils.matcher; + +import org.jetbrains.annotations.ApiStatus; + +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. + *
+ * 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(String wildcard) { + this.wildcard = wildcard; + this.pattern = Pattern.compile(("\\Q" + wildcard + "\\E").replace("*", "\\E.*\\Q")); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean matches(String value) { + return pattern.matcher(value).matches(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/result/Attempt.java b/src/main/java/org/mvplugins/multiverse/core/utils/result/Attempt.java index 795d9a4b5..3e5783cab 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/result/Attempt.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/result/Attempt.java @@ -181,7 +181,7 @@ default Attempt map(Function mapper) { if (this instanceof Success) { return new Success<>(mapper.apply(get())); } else { - return new Failure<>(getFailureReason(), getFailureMessage()); + return new Failure<>((Failure) this); } } @@ -196,7 +196,7 @@ default Attempt map(Supplier mapper) { if (this instanceof Success) { return new Success<>(mapper.get()); } else { - return new Failure<>(getFailureReason(), getFailureMessage()); + return new Failure<>((Failure) this); } } @@ -211,7 +211,7 @@ default Attempt mapAttempt(Function> mapper) if (this instanceof Success) { return mapper.apply(get()); } else { - return new Failure<>(getFailureReason(), getFailureMessage()); + return new Failure<>((Failure) this); } } @@ -226,7 +226,7 @@ default Attempt mapAttempt(Supplier> mapper) { if (this instanceof Success) { return mapper.get(); } else { - return new Failure<>(getFailureReason(), getFailureMessage()); + return new Failure<>((Failure) this); } } @@ -241,7 +241,7 @@ default Attempt transform(UF failureReason) { if (this instanceof Success) { return new Success<>(get()); } else { - return new Failure<>(failureReason, getFailureMessage()); + return new Failure<>(failureReason, getFailureMessage(), (Failure) this); } } @@ -415,10 +415,20 @@ public String toString() { final class Failure implements Attempt { private final F failureReason; private final Message message; + private final Failure causeBy; Failure(F failureReason, Message message) { + this(failureReason, message, null); + } + + Failure(Failure 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 Message getFailureMessage() { public String toString() { return "Failure{" + "reason=" + failureReason + + (causeBy != null ? ", causeBy=" + causeBy : "") + '}'; } } diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/result/FailureReason.java b/src/main/java/org/mvplugins/multiverse/core/utils/result/FailureReason.java index c21eede1f..4eae2fa24 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/result/FailureReason.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/result/FailureReason.java @@ -3,12 +3,21 @@ 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(); } diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/result/SuccessReason.java b/src/main/java/org/mvplugins/multiverse/core/utils/result/SuccessReason.java index 685201fc0..b8a345d6f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/result/SuccessReason.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/result/SuccessReason.java @@ -3,9 +3,21 @@ 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(); } diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/text/AdventureTextFormatter.java b/src/main/java/org/mvplugins/multiverse/core/utils/text/AdventureTextFormatter.java index f553010af..dd915f712 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/text/AdventureTextFormatter.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/text/AdventureTextFormatter.java @@ -4,37 +4,45 @@ 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 @Nullable String removeColor(@Nullable String message) { + TextComponent sectionComponent = LegacyComponentSerializer.legacySection().deserializeOrNull(colorize(message)); + return PlainTextComponentSerializer.plainText().serializeOrNull(sectionComponent); } - public String removeAmpColor(String message) { - return PlainTextComponentSerializer.plainText().serialize(toAmpComponent(message)); + @Override + public @Nullable String removeAmpColor(@Nullable String message) { + return PlainTextComponentSerializer.plainText().serializeOrNull(toAmpComponent(message)); } - public String removeSectionColor(String message) { - return PlainTextComponentSerializer.plainText().serialize(toSectionComponent(message)); + @Override + public @Nullable String removeSectionColor(@Nullable String message) { + return PlainTextComponentSerializer.plainText().serializeOrNull(toSectionComponent(message)); } @Override - public String colorize(String message) { - return LegacyComponentSerializer.legacySection().serialize(toAmpComponent(message)); + public @Nullable String colorize(@Nullable String message) { + return LegacyComponentSerializer.legacySection().serializeOrNull(toAmpComponent(message)); } - private TextComponent toAmpComponent(String message) { - return LegacyComponentSerializer.legacyAmpersand().deserialize(message); + private @Nullable TextComponent toAmpComponent(@Nullable String message) { + return LegacyComponentSerializer.legacyAmpersand().deserializeOrNull(message); } - private TextComponent toSectionComponent(String message) { - return LegacyComponentSerializer.legacySection().deserialize(message); + private @Nullable TextComponent toSectionComponent(@Nullable String message) { + return LegacyComponentSerializer.legacySection().deserializeOrNull(message); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/text/ChatColorTextFormatter.java b/src/main/java/org/mvplugins/multiverse/core/utils/text/ChatColorTextFormatter.java index f6251658f..cb6cadd59 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/text/ChatColorTextFormatter.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/text/ChatColorTextFormatter.java @@ -2,30 +2,36 @@ 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); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/text/ChatTextFormatter.java b/src/main/java/org/mvplugins/multiverse/core/utils/text/ChatTextFormatter.java index 43b752d66..624794c4e 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/text/ChatTextFormatter.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/text/ChatTextFormatter.java @@ -2,6 +2,8 @@ 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; /** @@ -32,7 +34,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 +46,7 @@ public static void sendFormattedMessage(CommandSender sender, String message) { * * @since 5.1 */ - public static String removeColor(String message) { + public static @Nullable String removeColor(@Nullable String message) { return wrapper.removeColor(message); } @@ -57,7 +59,7 @@ public static String removeColor(String message) { * @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 +72,7 @@ public static String removeAmpColor(String message) { * @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 +85,7 @@ public static String removeSectionColor(String message) { * @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); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/text/TextFormatter.java b/src/main/java/org/mvplugins/multiverse/core/utils/text/TextFormatter.java index 6a66b0bbb..84a2794e4 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/text/TextFormatter.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/text/TextFormatter.java @@ -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); } diff --git a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java index 1c3606578..952cfbfc6 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java @@ -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.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.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; @@ -63,6 +65,7 @@ import org.mvplugins.multiverse.core.world.options.ImportWorldOptions; import org.mvplugins.multiverse.core.world.options.KeepWorldSettingsOptions; 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 +143,7 @@ public final class WorldManager { * * @return The result of the load. */ + @ApiStatus.Internal public Try initAllWorlds() { return updateWorldsFromConfig().andThenTry(() -> { importExistingWorlds(); @@ -167,7 +171,11 @@ private void loadNewWorldConfigs(Collection newWorldConfigs) { } private void removeWorldsNotInConfigs(Collection 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))); } @@ -278,22 +286,29 @@ private Attempt doCreateWorld( * @param options The options for customizing the import of an existing world folder. * @return The result of the import. */ - public Attempt importWorld( - ImportWorldOptions options) { + public Attempt importWorld(ImportWorldOptions options) { + String worldName = options.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 validateImportWorldOptions( - ImportWorldOptions options) { + private Attempt validateImportWorldOptions(ImportWorldOptions options) { String worldName = options.worldName(); if (!worldNameChecker.isValidWorldName(worldName)) { return worldActionResult(ImportFailureReason.INVALID_WORLDNAME, worldName); - } else if (!worldNameChecker.isValidWorldFolder(worldName)) { + } else if (options.doFolderCheck() && !worldNameChecker.isValidWorldFolder(worldName)) { return worldActionResult(ImportFailureReason.WORLD_FOLDER_INVALID, worldName); - } else if (isLoadedWorld(worldName)) { - return worldActionResult(ImportFailureReason.WORLD_EXIST_LOADED, worldName); - } else if (isWorld(worldName)) { - return worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, worldName); } return worldActionResult(options); } @@ -319,6 +334,23 @@ private Attempt doImportWorld( }); } + private Attempt 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); @@ -405,6 +437,12 @@ private Attempt validateWorldToLoad(@NotNull private Attempt doLoadWorld(@NotNull MultiverseWorld mvWorld) { World bukkitWorld = Bukkit.getWorld(mvWorld.getName()); if (bukkitWorld != null) { + 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); @@ -481,11 +519,14 @@ private Attempt removeLoadedMultiverseWorl * * @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 removeWorld( - @NotNull String worldName) { + @Deprecated(since = "5.2", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") + public Attempt 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 +536,13 @@ public Attempt removeWorld( * * @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 removeWorld(@NotNull MultiverseWorld world) { - return getLoadedWorld(world).fold( - () -> removeWorldFromConfig(world), - this::removeWorld); + return removeWorld(RemoveWorldOptions.world(world)); } /** @@ -508,9 +551,40 @@ public Attempt removeWorld(@NotNull MultiverseWorld * * @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 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 removeWorld(@NotNull RemoveWorldOptions options) { + MultiverseWorld world = options.world(); + return getLoadedWorld(world).fold( + () -> removeWorldFromConfig(world), + loadedWorld -> unloadBeforeRemoveWorld(loadedWorld, options) + ); + } + + private Attempt 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); } @@ -564,7 +638,11 @@ private Attempt doDeleteWorld(@NotNull LoadedMultiv ? 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 +710,7 @@ private Attempt cloneWorldValidateWorld( private Attempt 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 +720,17 @@ private Attempt cloneWorldCopyFolder(@Not 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 dataTransfer = transferData(options, options.world()); dataTransfer.pasteAllTo(newWorld); @@ -901,6 +990,9 @@ public Option getUnloadedWorldByNameOrAlias(@Nullable String wo } private Option 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 +1028,7 @@ public boolean isUnloadedWorld(@Nullable String worldName) { * @return The multiverse world if it exists. */ public Option 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 +1038,7 @@ public Option getLoadedWorld(@Nullable World world) { * @return The multiverse world if it exists. */ public Option 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 +1048,7 @@ public Option getLoadedWorld(@Nullable MultiverseWorld wo * @return The multiverse world if it exists. */ public Option getLoadedWorld(@Nullable String worldName) { - return Option.of((LoadedMultiverseWorld) loadedWorldsMap.get(worldName)); + return Option.of(loadedWorldsMap.get(worldName)); } /** @@ -971,9 +1063,12 @@ public Option getLoadedWorldByNameOrAlias(@Nullable Strin } private Option 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 +1079,6 @@ private Option getLoadedWorldByAlias(@Nullable String ali */ public Collection getLoadedWorlds() { return loadedWorldsMap.values().stream() - .map(world -> (LoadedMultiverseWorld) world) .toList(); } diff --git a/src/main/java/org/mvplugins/multiverse/core/world/options/ImportWorldOptions.java b/src/main/java/org/mvplugins/multiverse/core/world/options/ImportWorldOptions.java index 8de15e3c8..3403e8374 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/options/ImportWorldOptions.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/options/ImportWorldOptions.java @@ -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; + } } diff --git a/src/main/java/org/mvplugins/multiverse/core/world/options/RemoveWorldOptions.java b/src/main/java/org/mvplugins/multiverse/core/world/options/RemoveWorldOptions.java new file mode 100644 index 000000000..37bd371e8 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/world/options/RemoveWorldOptions.java @@ -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. + *
+ * 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. + *
+ * 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; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/world/reasons/ImportFailureReason.java b/src/main/java/org/mvplugins/multiverse/core/world/reasons/ImportFailureReason.java index 740328e07..5dc150480 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/reasons/ImportFailureReason.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/reasons/ImportFailureReason.java @@ -3,6 +3,7 @@ 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. */ diff --git a/src/main/java/org/mvplugins/multiverse/core/world/reasons/LoadFailureReason.java b/src/main/java/org/mvplugins/multiverse/core/world/reasons/LoadFailureReason.java index 0de412fbd..ab9df6307 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/reasons/LoadFailureReason.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/reasons/LoadFailureReason.java @@ -3,6 +3,7 @@ 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 LoadFailureReason implements FailureReason { */ 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. */ diff --git a/src/main/java/org/mvplugins/multiverse/core/world/reasons/RemoveFailureReason.java b/src/main/java/org/mvplugins/multiverse/core/world/reasons/RemoveFailureReason.java index dedeec798..8ca74ddc3 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/reasons/RemoveFailureReason.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/reasons/RemoveFailureReason.java @@ -3,8 +3,10 @@ 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 @@ 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), /** diff --git a/src/main/resources/multiverse-core_en.properties b/src/main/resources/multiverse-core_en.properties index a78e3d083..14b90bd01 100644 --- a/src/main/resources/multiverse-core_en.properties +++ b/src/main/resources/multiverse-core_en.properties @@ -287,12 +287,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} &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} &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! diff --git a/src/test/java/org/mvplugins/multiverse/core/utils/StringMatcherTest.kt b/src/test/java/org/mvplugins/multiverse/core/utils/StringMatcherTest.kt new file mode 100644 index 000000000..a9b73c6e9 --- /dev/null +++ b/src/test/java/org/mvplugins/multiverse/core/utils/StringMatcherTest.kt @@ -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")) + } +} diff --git a/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt b/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt index 72b69bd73..2aa872fe2 100644 --- a/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt @@ -11,6 +11,7 @@ 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.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 +103,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)