diff --git a/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java b/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java index 9e3dd580d..dd1edd4dc 100644 --- a/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java +++ b/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java @@ -15,6 +15,7 @@ import jakarta.inject.Provider; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; @@ -238,7 +239,11 @@ private void logEnableMessage() { * Gets the MultiverseCoreApi * * @return The MultiverseCoreApi + * + * @deprecated Use {@link MultiverseCoreApi#get()} directly. */ + @Deprecated(since = "5.1", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") public MultiverseCoreApi getApi() { return MultiverseCoreApi.get(); } diff --git a/src/main/java/org/mvplugins/multiverse/core/MultiverseCoreApi.java b/src/main/java/org/mvplugins/multiverse/core/MultiverseCoreApi.java index 3e486914a..b185613ce 100644 --- a/src/main/java/org/mvplugins/multiverse/core/MultiverseCoreApi.java +++ b/src/main/java/org/mvplugins/multiverse/core/MultiverseCoreApi.java @@ -2,6 +2,7 @@ import org.bukkit.Bukkit; import org.bukkit.plugin.ServicePriority; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.core.anchor.AnchorManager; import org.mvplugins.multiverse.core.config.CoreConfig; @@ -15,7 +16,10 @@ import org.mvplugins.multiverse.core.world.biomeprovider.BiomeProviderFactory; import org.mvplugins.multiverse.core.world.generators.GeneratorProvider; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import java.util.function.Consumer; /** * Provides access to the MultiverseCore API. @@ -23,6 +27,7 @@ public class MultiverseCoreApi { private static MultiverseCoreApi instance; + private static final List> whenLoadedCallbacks = new ArrayList<>(); static void init(@NotNull MultiverseCore multiverseCore) { if (instance != null) { @@ -30,6 +35,9 @@ static void init(@NotNull MultiverseCore multiverseCore) { } instance = new MultiverseCoreApi(multiverseCore.getServiceLocator()); Bukkit.getServicesManager().register(MultiverseCoreApi.class, instance, multiverseCore, ServicePriority.Normal); + + whenLoadedCallbacks.forEach(c -> c.accept(instance)); + whenLoadedCallbacks.clear(); } static void shutdown() { @@ -37,10 +45,49 @@ static void shutdown() { instance = null; } + /** + * Hook your plugin into the MultiverseCoreApi here to ensure you only start using the API after it has been initialized. + * Use this if you know your plugin may load before Multiverse-Core is fully initialized. + *
+ * This handy method removes the need for you to check with plugin manager or listen to plugin enable event. + *
+ * Callback will be called immediately if the MultiverseCoreApi has already been initialized. + * + * @param consumer The callback to execute when the MultiverseCoreApi has been initialized. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static void whenLoaded(@NotNull Consumer consumer) { + if (instance != null) { + consumer.accept(instance); + } else { + whenLoadedCallbacks.add(consumer); + } + } + + /** + * Checks if the MultiverseCoreApi has been initialized. + * + * @return True if the MultiverseCoreApi has been initialized, false otherwise + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static boolean isLoaded() { + return instance != null; + } + /** * Gets the MultiverseCoreApi. This will throw an exception if the Multiverse-Core has not been initialized. + *
+ * You can check if the MultiverseCoreApi has been initialized with {@link #isLoaded()} before using this method. + *
+ * Alternatively, you can use {@link #whenLoaded(Consumer)} to hook into the MultiverseCoreApi if your plugin may + * load before Multiverse-Core is fully initialized. * * @return The MultiverseCoreApi + * @throws IllegalStateException if the MultiverseCoreApi has not been initialized */ public static @NotNull MultiverseCoreApi get() { if (instance == null) { diff --git a/src/main/java/org/mvplugins/multiverse/core/PlaceholderExpansionHook.java b/src/main/java/org/mvplugins/multiverse/core/PlaceholderExpansionHook.java index fbf22df48..14a8e8c40 100644 --- a/src/main/java/org/mvplugins/multiverse/core/PlaceholderExpansionHook.java +++ b/src/main/java/org/mvplugins/multiverse/core/PlaceholderExpansionHook.java @@ -1,5 +1,6 @@ package org.mvplugins.multiverse.core; +import com.google.common.collect.Lists; import io.vavr.control.Option; import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; @@ -13,11 +14,16 @@ import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.economy.MVEconomist; +import org.mvplugins.multiverse.core.utils.MinecraftTimeFormatter; import org.mvplugins.multiverse.core.utils.REPatterns; import org.mvplugins.multiverse.core.utils.StringFormatter; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + @Service final class PlaceholderExpansionHook extends PlaceholderExpansion { @@ -68,32 +74,54 @@ public boolean persist() { @Override public @Nullable String onRequest(OfflinePlayer offlinePlayer, @NotNull String params) { // Split string in to an Array with underscores - String[] paramsArray = REPatterns.UNDERSCORE.split(params, 2); + List paramsArray = Lists.newArrayList(REPatterns.UNDERSCORE.split(params)); // No placeholder defined - if (paramsArray.length < 1) { - warning("No placeholder defined"); + if (paramsArray.isEmpty()) { + warning("No placeholder key defined"); return null; } - final var placeholder = paramsArray[0]; - Option targetWorld; + final var placeholder = paramsArray.removeFirst(); + + String worldName = parseWorldName(offlinePlayer, paramsArray); + if (worldName == null) return null; + + return worldManager.getLoadedWorld(worldName) + .onEmpty(() -> warning("Multiverse World not found: " + worldName)) + .map(world -> getWorldPlaceHolderValue(placeholder, paramsArray, world)) + .getOrNull(); + } - // If no world is defined, use the player's world - if (paramsArray.length == 1) { - if (!offlinePlayer.isOnline()) { + private @Nullable String parseWorldName(OfflinePlayer offlinePlayer, List paramsArray) { + // No world defined, get from player + if (paramsArray.isEmpty()) { + if (offlinePlayer instanceof Player player) { + return player.getWorld().getName(); + } else { + warning("You must specify a world name for non-player placeholders"); return null; } - targetWorld = worldManager.getLoadedWorld(((Player)offlinePlayer).getWorld()); - } else { - targetWorld = worldManager.getLoadedWorld(paramsArray[1]); } - // Fail if world is null - return targetWorld.map(world -> getWorldPlaceHolderValue(placeholder, world)).getOrNull(); + // Try get from params + String paramWorldName = paramsArray.getLast(); + if (worldManager.isLoadedWorld(paramWorldName)) { + paramsArray.removeLast(); + return paramWorldName; + } + + // Param not a world, fallback to player + if (offlinePlayer instanceof Player player) { + return player.getWorld().getName(); + } + warning("Multiverse World not found: " + paramWorldName); + return null; } - private @Nullable String getWorldPlaceHolderValue(@NotNull String placeholder, @NotNull LoadedMultiverseWorld world) { + private @Nullable String getWorldPlaceHolderValue(@NotNull String placeholder, + @NotNull List placeholderParams, + @NotNull LoadedMultiverseWorld world) { // Switch to find what specific placeholder we want switch (placeholder.toLowerCase()) { case "alias" -> { @@ -151,7 +179,22 @@ public boolean persist() { return String.valueOf(world.getSeed()); } case "time" -> { - return String.valueOf(world.getBukkitWorld().map(World::getTime).getOrElse(0L)); + String timeFormat = !placeholderParams.isEmpty() ? placeholderParams.getFirst() : ""; + long time = world.getBukkitWorld().map(World::getTime).getOrElse(0L); + switch (timeFormat) { + case "" -> { + return String.valueOf(time); + } + case "12h" -> { + return MinecraftTimeFormatter.format12h(time); + } + case "24h" -> { + return MinecraftTimeFormatter.format24h(time); + } + default -> { + return MinecraftTimeFormatter.formatTime(time, timeFormat); + } + } } case "type" -> { return world.getBukkitWorld().map(World::getWorldType).map(Enum::name).getOrElse("null"); diff --git a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandCompletions.java b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandCompletions.java index 134ecca8d..60a23cfaa 100644 --- a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandCompletions.java +++ b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandCompletions.java @@ -33,12 +33,15 @@ import org.mvplugins.multiverse.core.anchor.AnchorManager; import org.mvplugins.multiverse.core.anchor.MultiverseAnchor; +import org.mvplugins.multiverse.core.command.context.issueraware.IssuerAwareValue; +import org.mvplugins.multiverse.core.command.context.issueraware.MultiverseWorldValue; +import org.mvplugins.multiverse.core.command.context.issueraware.PlayerArrayValue; import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.config.node.functions.DefaultSuggesterProvider; import org.mvplugins.multiverse.core.config.handle.PropertyModifyAction; import org.mvplugins.multiverse.core.destination.DestinationInstance; +import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket; import org.mvplugins.multiverse.core.destination.DestinationsProvider; -import org.mvplugins.multiverse.core.destination.core.WorldDestination; import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker; import org.mvplugins.multiverse.core.utils.REPatterns; import org.mvplugins.multiverse.core.utils.StringFormatter; @@ -129,6 +132,22 @@ private Collection completeWithPreconditions( if (context.hasConfig("playerOnly") && !context.getIssuer().isPlayer()) { return Collections.emptyList(); } + if (context.hasConfig("byIssuerForArg")) { + Boolean byIssuerForArg = Try.of(() -> context.getContextValueByName(IssuerAwareValue.class, context.getConfig("byIssuerForArg"))) + .map(IssuerAwareValue::isByIssuer) + .getOrElse(false); + if (!byIssuerForArg) { + return Collections.emptyList(); + } + } + if (context.hasConfig("notByIssuerForArg")) { + Boolean byIssuerForArg = Try.of(() -> context.getContextValueByName(IssuerAwareValue.class, context.getConfig("notByIssuerForArg"))) + .map(IssuerAwareValue::isByIssuer) + .getOrElse(false); + if (byIssuerForArg) { + return Collections.emptyList(); + } + } if (context.hasConfig("resolveUntil")) { if (!Try.run(() -> context.getContextValueByName(Object.class, context.getConfig("resolveUntil"))).isSuccess()) { return Collections.emptyList(); @@ -195,7 +214,10 @@ private boolean checkPerms(CommandIssuer issuer, RegisteredCommand command) { } private Collection suggestDestinations(BukkitCommandCompletionContext context) { - return Try.of(() -> context.getContextValue(Player[].class)) + return Try.of(() -> context.getContextValue(PlayerArrayValue.class)) + .map(PlayerArrayValue::value) + .recover(IllegalStateException.class, e -> context.getContextValue(Player[].class)) + .recover(IllegalStateException.class, e -> new Player[]{context.getContextValue(Player.class)}) .map(players -> { if (players.length == 0) { // Most likely console did not specify a player @@ -213,9 +235,7 @@ private Collection suggestDestinationsWithPerms(CommandSender teleporter return destinationsProvider.suggestDestinations(teleporter, deststring).stream() .filter(packet -> corePermissionsChecker .checkDestinationPacketPermission(teleporter, Arrays.asList(players), packet)) - .map(packet -> packet.destination() instanceof WorldDestination - ? packet.destinationString() - : packet.destination().getIdentifier() + ":" + packet.destinationString()) + .map(DestinationSuggestionPacket::parsableString) .toList(); } @@ -248,7 +268,7 @@ private Collection suggestGeneratorPlugins(BukkitCommandCompletionContex private Collection suggestMVConfigValues(BukkitCommandCompletionContext context) { return Try.of(() -> context.getContextValue(String.class)) .map(propertyName -> config.getStringPropertyHandle() - .getSuggestedPropertyValue(propertyName, context.getInput(), PropertyModifyAction.SET)) + .getSuggestedPropertyValue(propertyName, context.getInput(), PropertyModifyAction.SET, context.getSender())) .getOrElse(Collections.emptyList()); } @@ -283,7 +303,7 @@ private Collection suggestMVWorlds(BukkitCommandCompletionContext contex private Collection suggestMVWorldPropsName(BukkitCommandCompletionContext context) { return Try.of(() -> { - MultiverseWorld world = context.getContextValue(MultiverseWorld.class); + MultiverseWorld world = context.getContextValue(MultiverseWorldValue.class).value(); PropertyModifyAction action = context.getContextValue(PropertyModifyAction.class); return world.getStringPropertyHandle().getModifiablePropertyNames(action); }).getOrElse(Collections.emptyList()); @@ -291,10 +311,10 @@ private Collection suggestMVWorldPropsName(BukkitCommandCompletionContex private Collection suggestMVWorldPropsValue(BukkitCommandCompletionContext context) { return Try.of(() -> { - MultiverseWorld world = context.getContextValue(MultiverseWorld.class); + MultiverseWorld world = context.getContextValue(MultiverseWorldValue.class).value(); PropertyModifyAction action = context.getContextValue(PropertyModifyAction.class); String propertyName = context.getContextValue(String.class); - return world.getStringPropertyHandle().getSuggestedPropertyValue(propertyName, context.getInput(), action); + return world.getStringPropertyHandle().getSuggestedPropertyValue(propertyName, context.getInput(), action, context.getSender()); }).getOrElse(Collections.emptyList()); } @@ -335,7 +355,7 @@ private Collection suggestSpawnCategoryPropsValue(BukkitCommandCompletio return world.getEntitySpawnConfig() .getSpawnCategoryConfig(spawnCategory) .getStringPropertyHandle() - .getSuggestedPropertyValue(propertyName, context.getInput(), action); + .getSuggestedPropertyValue(propertyName, context.getInput(), action, context.getSender()); }).getOrElse(Collections.emptyList()); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java index 4ef451729..7dfd49f30 100644 --- a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java +++ b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java @@ -7,11 +7,18 @@ import co.aikar.commands.BukkitCommandExecutionContext; import co.aikar.commands.BukkitCommandIssuer; import co.aikar.commands.InvalidCommandArgument; +import co.aikar.commands.MinecraftMessageKeys; import co.aikar.commands.PaperCommandContexts; import co.aikar.commands.contexts.ContextResolver; import com.google.common.base.Strings; +import io.vavr.control.Try; import jakarta.inject.Inject; +import org.bukkit.Bukkit; import org.bukkit.GameRule; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.command.BlockCommandSender; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.entity.SpawnCategory; import org.jetbrains.annotations.Nullable; @@ -20,6 +27,10 @@ import org.mvplugins.multiverse.core.anchor.AnchorManager; import org.mvplugins.multiverse.core.anchor.MultiverseAnchor; import org.mvplugins.multiverse.core.command.context.GameRuleValue; +import org.mvplugins.multiverse.core.command.context.issueraware.IssuerAwareContextBuilder; +import org.mvplugins.multiverse.core.command.context.PlayerLocation; +import org.mvplugins.multiverse.core.command.context.issueraware.MultiverseWorldValue; +import org.mvplugins.multiverse.core.command.context.issueraware.PlayerArrayValue; import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.destination.DestinationInstance; import org.mvplugins.multiverse.core.destination.DestinationsProvider; @@ -27,6 +38,7 @@ import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter; import org.mvplugins.multiverse.core.display.filters.RegexContentFilter; import org.mvplugins.multiverse.core.exceptions.command.MVInvalidCommandArgument; +import org.mvplugins.multiverse.core.locale.message.Message; import org.mvplugins.multiverse.core.utils.PlayerFinder; import org.mvplugins.multiverse.core.utils.REPatterns; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; @@ -69,13 +81,16 @@ public class MVCommandContexts extends PaperCommandContexts { registerContext(GameRule.class, this::parseGameRule); registerContext(GameRuleValue.class, this::parseGameRuleValue); registerContext(GeneratorPlugin.class, this::parseGeneratorPlugin); - registerIssuerAwareContext(LoadedMultiverseWorld.class, this::parseLoadedMultiverseWorld); - registerIssuerAwareContext(LoadedMultiverseWorld[].class, this::parseLoadedMultiverseWorldArray); - registerIssuerAwareContext(MultiverseWorld.class, this::parseMultiverseWorld); - registerIssuerAwareContext(MultiverseWorld[].class, this::parseMultiverseWorldArray); + registerIssuerAwareContext(LoadedMultiverseWorld.class, loadedMultiverseWorldContextBuilder().generateContext()); + registerIssuerAwareContext(LoadedMultiverseWorld[].class, loadedMultiverseWorldArrayContextBuilder().generateContext()); + registerIssuerAwareContext(MultiverseWorld.class, multiverseWorldContextBuilder().generateContext()); + registerIssuerAwareContext(MultiverseWorldValue.class, multiverseWorldContextBuilder().generateContext(MultiverseWorldValue::new)); + registerIssuerAwareContext(MultiverseWorld[].class, multiverseWorldArrayContextBuilder().generateContext()); registerContext(MultiverseAnchor.class, this::parseMultiverseAnchor); - registerIssuerAwareContext(Player.class, this::parsePlayer); - registerIssuerAwareContext(Player[].class, this::parsePlayerArray); + registerIssuerAwareContext(Player.class, playerContextBuilder().generateContext()); + registerIssuerAwareContext(Player[].class, playerArrayContextBuilder().generateContext()); + registerIssuerAwareContext(PlayerArrayValue.class, playerArrayContextBuilder().generateContext(PlayerArrayValue::new)); + registerIssuerAwareContext(PlayerLocation.class, this::parsePlayerLocation); registerContext(SpawnCategory[].class, this::parseSpawnCategories); } @@ -100,7 +115,7 @@ private ContentFilter parseContentFilter(BukkitCommandExecutionContext context) throw new InvalidCommandArgument("No destination specified."); } - return destinationsProvider.parseDestination(destination) + return destinationsProvider.parseDestination(context.getSender(), destination) .getOrThrow(failure -> MVInvalidCommandArgument.of(failure.getFailureMessage())); } @@ -149,107 +164,42 @@ private GeneratorPlugin parseGeneratorPlugin(BukkitCommandExecutionContext conte return generatorPlugin; } - private LoadedMultiverseWorld parseLoadedMultiverseWorld(BukkitCommandExecutionContext context) { - String resolve = context.getFlagValue("resolve", ""); - // Get world based on sender only - if (resolve.equals("issuerOnly")) { - if (context.getIssuer().isPlayer()) { - return worldManager.getLoadedWorld(context.getIssuer().getPlayer().getWorld()).getOrNull(); - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("This command can only be used by a player in a Multiverse World."); - } - - String worldName = context.getFirstArg(); - LoadedMultiverseWorld world = getLoadedMultiverseWorld(worldName); - - // Get world based on input, fallback to sender if input is not a world - if (resolve.equals("issuerAware")) { - if (world != null) { - context.popFirstArg(); - return world; - } - if (context.getIssuer().isPlayer()) { - return worldManager.getLoadedWorld(context.getPlayer().getWorld()) - .getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world.")); - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("World '" + worldName + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console."); - } - - // Get world based on input only - if (world != null) { - context.popFirstArg(); - return world; - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world."); + private IssuerAwareContextBuilder loadedMultiverseWorldContextBuilder() { + return new IssuerAwareContextBuilder() + .fromPlayer((context, player) -> worldManager.getLoadedWorld(player.getWorld()).getOrNull()) + .fromInput((context, input) -> getLoadedMultiverseWorld(input)) + .issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a loaded Multiverse World.")) + .issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a loaded multiverse world. Either specify a multiverse world name or use this command in a loaded multiverse world.")) + .issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console.")) + .inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a loaded multiverse world.")); } - private LoadedMultiverseWorld[] parseLoadedMultiverseWorldArray(BukkitCommandExecutionContext context) { - String resolve = context.getFlagValue("resolve", ""); - - // Get world based on sender only - if (resolve.equals("issuerOnly")) { - if (context.getIssuer().isPlayer()) { - LoadedMultiverseWorld playerWorld = worldManager.getLoadedWorld(context.getIssuer().getPlayer().getWorld()) - .getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world.")); - return new LoadedMultiverseWorld[]{playerWorld}; - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("This command can only be used by a player in a Multiverse World."); - } - - String worldStrings = context.getFirstArg(); - String[] worldNames = worldStrings == null ? new String[0] : REPatterns.COMMA.split(worldStrings); - Set worlds = new HashSet<>(worldNames.length); - for (String worldName : worldNames) { - if ("*".equals(worldName)) { - worlds.addAll(worldManager.getLoadedWorlds()); - break; - } - LoadedMultiverseWorld world = getLoadedMultiverseWorld(worldName); - if (world == null) { - throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world."); - } - worlds.add(world); - } - - // Get world based on input, fallback to sender if input is not a world - if (resolve.equals("issuerAware")) { - if (!worlds.isEmpty()) { - context.popFirstArg(); - return worlds.toArray(new LoadedMultiverseWorld[0]); - } - if (context.getIssuer().isPlayer()) { - LoadedMultiverseWorld playerWorld = worldManager.getLoadedWorld(context.getIssuer().getPlayer().getWorld()) - .getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world.")); - return new LoadedMultiverseWorld[]{playerWorld}; - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("Worlds '" + worldStrings + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console."); - } - - // Get world based on input only - if (!worlds.isEmpty()) { - context.popFirstArg(); - return worlds.toArray(new LoadedMultiverseWorld[0]); - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("World " + worldStrings + " is not a loaded multiverse world."); + private IssuerAwareContextBuilder loadedMultiverseWorldArrayContextBuilder() { + return new IssuerAwareContextBuilder() + .fromPlayer((context, player) -> worldManager.getLoadedWorld(player.getWorld()) + .map(world -> new LoadedMultiverseWorld[]{world}) + .getOrNull()) + .fromInput((context, input) -> { + String[] worldNames = input == null ? new String[0] : REPatterns.COMMA.split(input); + Set worlds = new HashSet<>(worldNames.length); + for (String worldName : worldNames) { + if ("*".equals(worldName)) { + worlds.addAll(worldManager.getLoadedWorlds()); + break; + } + LoadedMultiverseWorld world = getLoadedMultiverseWorld(worldName); + if (world == null) { + throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world."); + } + worlds.add(world); + } + return worlds.isEmpty() ? null : worlds.toArray(new LoadedMultiverseWorld[0]); + }) + .issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a loaded Multiverse World.")) + .issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a loaded multiverse world. Either specify a multiverse world name or use this command in a loaded multiverse world.")) + .issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console.")) + .inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a loaded multiverse world.")); } @Nullable @@ -259,107 +209,41 @@ private LoadedMultiverseWorld getLoadedMultiverseWorld(String worldName) { : worldManager.getLoadedWorld(worldName).getOrNull(); } - private MultiverseWorld parseMultiverseWorld(BukkitCommandExecutionContext context) { - String resolve = context.getFlagValue("resolve", ""); - - // Get world based on sender only - if (resolve.equals("issuerOnly")) { - if (context.getIssuer().isPlayer()) { - return worldManager.getWorld(context.getIssuer().getPlayer().getWorld()).getOrNull(); - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("This command can only be used by a player in a Multiverse World."); - } - - String worldName = context.getFirstArg(); - MultiverseWorld world = getMultiverseWorld(worldName); - - // Get world based on input, fallback to sender if input is not a world - if (resolve.equals("issuerAware")) { - if (world != null) { - context.popFirstArg(); - return world; - } - if (context.getIssuer().isPlayer()) { - return worldManager.getWorld(context.getPlayer().getWorld()) - .getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world.")); - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("World '" + worldName + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console."); - } - - // Get world based on input only - if (world != null) { - context.popFirstArg(); - return world; - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world."); + private IssuerAwareContextBuilder multiverseWorldContextBuilder() { + return new IssuerAwareContextBuilder() + .fromPlayer((context, player) -> worldManager.getWorld(player.getWorld()).getOrNull()) + .fromInput((context, input) -> getMultiverseWorld(input)) + .issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a Multiverse World.")) + .issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world.")) + .issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a multiverse world. Remember to specify the world name when using this command in console.")) + .inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a multiverse world.")); } - private MultiverseWorld[] parseMultiverseWorldArray(BukkitCommandExecutionContext context) { - String resolve = context.getFlagValue("resolve", ""); - - // Get world based on sender only - if (resolve.equals("issuerOnly")) { - if (context.getIssuer().isPlayer()) { - MultiverseWorld playerWorld = worldManager.getWorld(context.getIssuer().getPlayer().getWorld()) - .getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world.")); - return new MultiverseWorld[]{playerWorld}; - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("This command can only be used by a player in a Multiverse World."); - } - - String worldStrings = context.getFirstArg(); - String[] worldNames = worldStrings == null ? new String[0] : REPatterns.COMMA.split(worldStrings); - Set worlds = new HashSet<>(worldNames.length); - for (String worldName : worldNames) { - if ("*".equals(worldName)) { - worlds.addAll(worldManager.getWorlds()); - break; - } - MultiverseWorld world = getMultiverseWorld(worldName); - if (world == null) { - throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world."); - } - worlds.add(world); - } - - // Get world based on input, fallback to sender if input is not a world - if (resolve.equals("issuerAware")) { - if (!worlds.isEmpty()) { - context.popFirstArg(); - return worlds.toArray(new MultiverseWorld[0]); - } - if (context.getIssuer().isPlayer()) { - MultiverseWorld playerWorld = worldManager.getWorld(context.getIssuer().getPlayer().getWorld()) - .getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world.")); - return new MultiverseWorld[]{playerWorld}; - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("Worlds '" + worldStrings + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console."); - } - - // Get world based on input only - if (!worlds.isEmpty()) { - context.popFirstArg(); - return worlds.toArray(new MultiverseWorld[0]); - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("World " + worldStrings + " is not a loaded multiverse world."); + private IssuerAwareContextBuilder multiverseWorldArrayContextBuilder() { + return new IssuerAwareContextBuilder() + .fromPlayer((context, player) -> worldManager.getWorld(player.getWorld()) + .map(world -> new MultiverseWorld[]{world}) + .getOrNull()) + .fromInput((context, input) -> { + String[] worldNames = input == null ? new String[0] : REPatterns.COMMA.split(input); + Set worlds = new HashSet<>(worldNames.length); + for (String worldName : worldNames) { + if ("*".equals(worldName)) { + worlds.addAll(worldManager.getWorlds()); + break; + } + MultiverseWorld world = getMultiverseWorld(worldName); + if (world == null) { + throw new InvalidCommandArgument("World " + worldName + " is not a multiverse world."); + } + worlds.add(world); + } + return worlds.isEmpty() ? null : worlds.toArray(new MultiverseWorld[0]); + }) + .issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a Multiverse World.")) + .issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world.")) + .issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a multiverse world. Remember to specify the world name when using this command in console.")) + .inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a multiverse world.")); } @Nullable @@ -375,92 +259,110 @@ private MultiverseAnchor parseMultiverseAnchor(BukkitCommandExecutionContext con .getOrElseThrow(() -> new InvalidCommandArgument("The anchor '" +anchorName + "' does not exist.")); } - private Player parsePlayer(BukkitCommandExecutionContext context) { - String resolve = context.getFlagValue("resolve", ""); - - // Get player based on sender only - if (resolve.equals("issuerOnly")) { - if (context.getIssuer().isPlayer()) { - return context.getIssuer().getPlayer(); - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("This command can only be used by a player."); - } + private IssuerAwareContextBuilder playerContextBuilder() { + return new IssuerAwareContextBuilder() + .fromPlayer((context, player) -> player) + .fromInput((context, input) -> PlayerFinder.get(input, context.getSender())) + .issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player.")) + .issuerAwareInputFailMessage((context, input) -> Message.of("Invalid player: " + input + ". Either specify an online player or use this command as a player.")) + .inputOnlyFailMessage((context, input) -> Message.of("Player " + input + " not found.")); + } - String playerIdentifier = context.getFirstArg(); - Player player = PlayerFinder.get(playerIdentifier, context.getSender()); + private IssuerAwareContextBuilder playerArrayContextBuilder() { + return new IssuerAwareContextBuilder() + .fromPlayer((context, player) -> new Player[]{player}) + .fromInput((context, input) -> { + Player[] players = PlayerFinder.getMulti(input, context.getSender()).toArray(new Player[0]); + return (players.length == 0) ? null : players; + }) + .issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player.")) + .issuerAwareInputFailMessage((context, input) -> Message.of("Invalid player: " + input + ". Either specify an online player or use this command as a player.")) + .inputOnlyFailMessage((context, input) -> Message.of("Player " + input + " not found.")); + } - // Get player based on input, fallback to sender if input is not a player - if (resolve.equals("issuerAware")) { - if (player != null) { - context.popFirstArg(); - return player; - } - if (context.getIssuer().isPlayer()) { - return context.getIssuer().getPlayer(); - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("Invalid player: " + playerIdentifier - + ". Either specify an online player or use this command as a player."); - } + private PlayerLocation parsePlayerLocation(BukkitCommandExecutionContext context) { + Try location = Try.of(() -> parseLocationFromInput(context.getFirstArg(), context.getSender())); - // Get player based on input only - if (player != null) { + if (location.isSuccess()) { context.popFirstArg(); - return player; + return new PlayerLocation(location.get()); } - if (context.isOptional()) { - return null; + if (context.getPlayer() != null) { + return new PlayerLocation(context.getPlayer().getLocation()); } - throw new InvalidCommandArgument("Player " + playerIdentifier + " not found."); - } - - private Player[] parsePlayerArray(BukkitCommandExecutionContext context) { - String resolve = context.getFlagValue("resolve", ""); - - // Get player based on sender only - if (resolve.equals("issuerOnly")) { - if (context.getIssuer().isPlayer()) { - return new Player[]{context.getIssuer().getPlayer()}; - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("This command can only be used by a player."); + if (context.getFirstArg() == null) { + throw new InvalidCommandArgument("You must specify a world location when using this command in console."); } - - String playerIdentifier = context.getFirstArg(); - Player[] players = PlayerFinder.getMulti(playerIdentifier, context.getSender()).toArray(new Player[0]); - - // Get player based on input, fallback to sender if input is not a player - if (resolve.equals("issuerAware")) { - if (players.length > 0) { - context.popFirstArg(); - return players; - } - if (context.getIssuer().isPlayer()) { - return new Player[]{context.getIssuer().getPlayer()}; - } - if (context.isOptional()) { - return null; - } - throw new InvalidCommandArgument("Invalid player: " + playerIdentifier - + ". Either specify an online player or use this command as a player."); + if (location.getCause() instanceof InvalidCommandArgument) { + throw (InvalidCommandArgument)location.getCause(); } + throw new RuntimeException(location.getCause()); + } - // Get player based on input only - if (players.length > 0) { - context.popFirstArg(); - return players; - } - if (context.isOptional()) { - return null; + // copied from ACF + private Location parseLocationFromInput(String input, CommandSender sender) { + String[] split = REPatterns.COLON.split(input, 2); + if (split.length == 0) { + throw new InvalidCommandArgument(true); + } else if (split.length < 2 && !(sender instanceof Player) && !(sender instanceof BlockCommandSender)) { + throw new InvalidCommandArgument(MinecraftMessageKeys.LOCATION_PLEASE_SPECIFY_WORLD, new String[0]); + } else { + Location sourceLoc = null; + String world; + String rest; + if (split.length == 2) { + world = split[0]; + rest = split[1]; + } else if (sender instanceof Player) { + sourceLoc = ((Player)sender).getLocation(); + world = sourceLoc.getWorld().getName(); + rest = split[0]; + } else { + if (!(sender instanceof BlockCommandSender)) { + throw new InvalidCommandArgument(true); + } + + sourceLoc = ((BlockCommandSender)sender).getBlock().getLocation(); + world = sourceLoc.getWorld().getName(); + rest = split[0]; + } + + boolean rel = rest.startsWith("~"); + split = REPatterns.COMMA.split(rel ? rest.substring(1) : rest); + if (split.length < 3) { + throw new InvalidCommandArgument(MinecraftMessageKeys.LOCATION_PLEASE_SPECIFY_XYZ, new String[0]); + } else { + Double x = ACFUtil.parseDouble(split[0], rel ? (double)0.0F : null); + Double y = ACFUtil.parseDouble(split[1], rel ? (double)0.0F : null); + Double z = ACFUtil.parseDouble(split[2], rel ? (double)0.0F : null); + if (sourceLoc != null && rel) { + x = x + sourceLoc.getX(); + y = y + sourceLoc.getY(); + z = z + sourceLoc.getZ(); + } else if (rel) { + throw new InvalidCommandArgument(MinecraftMessageKeys.LOCATION_CONSOLE_NOT_RELATIVE, new String[0]); + } + + if (x != null && y != null && z != null) { + World worldObj = Bukkit.getWorld(world); + if (worldObj == null) { + throw new InvalidCommandArgument(MinecraftMessageKeys.INVALID_WORLD, new String[0]); + } else if (split.length >= 5) { + Float yaw = ACFUtil.parseFloat(split[3]); + Float pitch = ACFUtil.parseFloat(split[4]); + if (pitch != null && yaw != null) { + return new Location(worldObj, x, y, z, yaw, pitch); + } else { + throw new InvalidCommandArgument(MinecraftMessageKeys.LOCATION_PLEASE_SPECIFY_XYZ, new String[0]); + } + } else { + return new Location(worldObj, x, y, z); + } + } else { + throw new InvalidCommandArgument(MinecraftMessageKeys.LOCATION_PLEASE_SPECIFY_XYZ, new String[0]); + } + } } - throw new InvalidCommandArgument("Player " + playerIdentifier + " not found."); } private SpawnCategory[] parseSpawnCategories(BukkitCommandExecutionContext context) { diff --git a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandIssuer.java b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandIssuer.java index cd9ce0648..5b8789325 100644 --- a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandIssuer.java +++ b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandIssuer.java @@ -1,7 +1,6 @@ package org.mvplugins.multiverse.core.command; import co.aikar.commands.BukkitCommandIssuer; -import co.aikar.commands.MessageKeys; import co.aikar.commands.MessageType; import co.aikar.locales.MessageKeyProvider; import org.bukkit.command.CommandSender; @@ -9,6 +8,7 @@ import org.mvplugins.multiverse.core.locale.message.Message; import org.mvplugins.multiverse.core.locale.message.MessageReplacement; +import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; public class MVCommandIssuer extends BukkitCommandIssuer { @@ -24,8 +24,13 @@ public MVCommandManager getManager() { return commandManager; } + @Override + public void sendMessageInternal(String message) { + ChatTextFormatter.sendFormattedMessage(getIssuer(), message); + } + public void sendError(String message, MessageReplacement... replacements) { - sendMessage(MessageType.INFO, message, replacements); + sendMessage(MessageType.ERROR, message, replacements); } public void sendSyntax(String message, MessageReplacement... replacements) { diff --git a/src/main/java/org/mvplugins/multiverse/core/command/context/PlayerLocation.java b/src/main/java/org/mvplugins/multiverse/core/command/context/PlayerLocation.java new file mode 100644 index 000000000..59cf14d62 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/command/context/PlayerLocation.java @@ -0,0 +1,16 @@ +package org.mvplugins.multiverse.core.command.context; + +import org.bukkit.Location; +import org.jetbrains.annotations.ApiStatus; + +/** + * Wrapper for context that get location from player if executed by player, + * else requires user input of location coordinates. + * + * @param value The location + * + * @since 5.1 + */ +@ApiStatus.AvailableSince("5.1") +public record PlayerLocation(Location value) { +} diff --git a/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java b/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java new file mode 100644 index 000000000..a9c1057b7 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java @@ -0,0 +1,226 @@ +package org.mvplugins.multiverse.core.command.context.issueraware; + +import co.aikar.commands.BukkitCommandExecutionContext; +import co.aikar.commands.BukkitCommandIssuer; +import co.aikar.commands.InvalidCommandArgument; +import co.aikar.commands.contexts.IssuerAwareContextResolver; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.core.locale.message.Message; + +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Reusable logic of issuer only and issuer aware context resolvers + * + * @since 5.1 + */ +@ApiStatus.AvailableSince("5.1") +public final class IssuerAwareContextBuilder { + + private BiFunction fromPlayer; + private BiFunction fromInput; + private Function issuerOnlyFailMessage = context -> Message.of("This command can only be used by a player."); + private BiFunction issuerAwarePlayerFailMessage = (context, player) -> Message.of("Unable to resolve context for player '" + player.getName() + "'."); + private BiFunction issuerAwareInputFailMessage = (context, input) -> Message.of("Unable to resolve context for input '" + input + "'."); + private BiFunction inputOnlyFailMessage = (context, input) -> Message.of("Unable to resolve context for input '" + input + "'."); + + public IssuerAwareContextBuilder() { + } + + /** + * Parse value from player itself. + * + * @param fromInput the parsing function + * @return The same builder for chaining + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public IssuerAwareContextBuilder fromPlayer(BiFunction fromInput) { + this.fromPlayer = fromInput; + return this; + } + + /** + * Parse value from command input string. + * + * @param fromInput the parsing function + * @return The same builder for chaining + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public IssuerAwareContextBuilder fromInput(BiFunction fromInput) { + this.fromInput = fromInput; + return this; + } + + /** + * When getting value fails as issuer is not a player. + * + * @param issuerOnlyFailMessage The message + * @return The same builder for chaining + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public IssuerAwareContextBuilder issuerOnlyFailMessage(Function issuerOnlyFailMessage) { + this.issuerOnlyFailMessage = issuerOnlyFailMessage; + return this; + } + + /** + * When getting value fails as player cannot parse the value. + * + * @param issuerAwarePlayerFailMessage The message + * @return The same builder for chaining + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public IssuerAwareContextBuilder issuerAwarePlayerFailMessage(BiFunction issuerAwarePlayerFailMessage) { + this.issuerAwarePlayerFailMessage = issuerAwarePlayerFailMessage; + return this; + } + + /** + * When getting value fails as input cannot parse the value. + * + * @param issuerAwareInputFailMessage The message + * @return The same builder for chaining + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public IssuerAwareContextBuilder issuerAwareInputFailMessage(BiFunction issuerAwareInputFailMessage) { + this.issuerAwareInputFailMessage = issuerAwareInputFailMessage; + return this; + } + + /** + * When getting value fails as input cannot parse the value. + * + * @param inputOnlyFailMessage The message + * @return The same builder for chaining + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public IssuerAwareContextBuilder inputOnlyFailMessage(BiFunction inputOnlyFailMessage) { + this.inputOnlyFailMessage = inputOnlyFailMessage; + return this; + } + + /** + * Creates the issuer aware context resolver logic. + * + * @return The generated context resolver + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public IssuerAwareContextResolver generateContext() { + Objects.requireNonNull(fromPlayer); + Objects.requireNonNull(fromInput); + Objects.requireNonNull(issuerOnlyFailMessage); + Objects.requireNonNull(issuerAwarePlayerFailMessage); + Objects.requireNonNull(issuerAwareInputFailMessage); + Objects.requireNonNull(inputOnlyFailMessage); + + return context -> { + BukkitCommandIssuer issuer = context.getIssuer(); + String resolve = context.getFlagValue("resolve", ""); + + if (resolve.equals("issuerOnly")) { + if (issuer.isPlayer()) { + T result = fromPlayer.apply(context, issuer.getPlayer()); + if (result != null) { + return result; + } + } + throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer)); + } + + String input = context.getFirstArg(); + T result = fromInput.apply(context, input); + if (result != null) { + context.popFirstArg(); + return result; + } + + if (resolve.equals("issuerAware")) { + if (issuer.isPlayer()) { + Player player = issuer.getPlayer(); + result = fromPlayer.apply(context, player); + if (result != null) { + return result; + } + throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer)); + } + throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer)); + } + + throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer)); + }; + } + + /** + * Creates the issuer aware context resolver logic, with marking of whether the value was resolved from player or input. + * + * @param createValue The function to create the {@link IssuerAwareValue} value + * @param The type of the value + * @return The generated context resolver + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public IssuerAwareContextResolver generateContext(BiFunction createValue) { + // todo: This is a copy and paste from above + + Objects.requireNonNull(fromPlayer); + Objects.requireNonNull(fromInput); + Objects.requireNonNull(issuerOnlyFailMessage); + Objects.requireNonNull(issuerAwarePlayerFailMessage); + Objects.requireNonNull(issuerAwareInputFailMessage); + Objects.requireNonNull(inputOnlyFailMessage); + + return context -> { + BukkitCommandIssuer issuer = context.getIssuer(); + String resolve = context.getFlagValue("resolve", ""); + + if (resolve.equals("issuerOnly")) { + if (issuer.isPlayer()) { + T result = fromPlayer.apply(context, issuer.getPlayer()); + if (result != null) { + return createValue.apply(true, result); + } + } + throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer)); + } + + String input = context.getFirstArg(); + T result = fromInput.apply(context, input); + if (result != null) { + context.popFirstArg(); + return createValue.apply(false, result); + } + + if (resolve.equals("issuerAware")) { + if (issuer.isPlayer()) { + Player player = issuer.getPlayer(); + result = fromPlayer.apply(context, player); + if (result != null) { + return createValue.apply(true, result); + } + throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer)); + } + throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer)); + } + + throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer)); + }; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareValue.java b/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareValue.java new file mode 100644 index 000000000..f752dd816 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareValue.java @@ -0,0 +1,35 @@ +package org.mvplugins.multiverse.core.command.context.issueraware; + +import org.jetbrains.annotations.ApiStatus; + +/** + * Base class for issuer aware values + * + * @since 5.1 + */ +@ApiStatus.AvailableSince("5.1") +public abstract class IssuerAwareValue { + protected final boolean byIssuer; + + /** + * Constructor to create an issuer aware value + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + protected IssuerAwareValue(boolean byIssuer) { + this.byIssuer = byIssuer; + } + + /** + * Gets whether the value is by issuer or input + * + * @return true if by issuer, false if by input + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public boolean isByIssuer() { + return byIssuer; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/MultiverseWorldValue.java b/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/MultiverseWorldValue.java new file mode 100644 index 000000000..2c4f970c7 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/MultiverseWorldValue.java @@ -0,0 +1,38 @@ +package org.mvplugins.multiverse.core.command.context.issueraware; + +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.core.world.MultiverseWorld; + +/** + * Issuer aware value wrapper for {@link MultiverseWorld} + * + * @since 5.1 + */ +@ApiStatus.AvailableSince("5.1") +public final class MultiverseWorldValue extends IssuerAwareValue { + + private final MultiverseWorld world; + + /** + * Constructor for issuer aware value + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public MultiverseWorldValue(boolean byIssuer, MultiverseWorld world) { + super(byIssuer); + this.world = world; + } + + /** + * The containing world wrapped + * + * @return wrapped world + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public MultiverseWorld value() { + return world; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/PlayerArrayValue.java b/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/PlayerArrayValue.java new file mode 100644 index 000000000..7d5fa9ee8 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/PlayerArrayValue.java @@ -0,0 +1,36 @@ +package org.mvplugins.multiverse.core.command.context.issueraware; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; + +/** + * Issuer aware value wrapper for {@link Player} array + * + * @since 5.1 + */ +@ApiStatus.AvailableSince("5.1") +public final class PlayerArrayValue extends IssuerAwareValue { + + private final Player[] players; + + /** + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public PlayerArrayValue(boolean byIssuer, Player[] players) { + super(byIssuer); + this.players = players; + } + + /** + * The containing player array wrapped + * + * @return wrapped player array + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public Player[] value() { + return players; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/ConfigCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/ConfigCommand.java index ffe8bc2aa..1f9cec11f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/ConfigCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/ConfigCommand.java @@ -65,12 +65,13 @@ private void showConfigValue(MVCommandIssuer issuer, String name) { } private void updateConfigValue(MVCommandIssuer issuer, String name, String value) { - config.getStringPropertyHandle().setPropertyString(name, value) + var stringPropertyHandle = config.getStringPropertyHandle(); + stringPropertyHandle.setPropertyString(issuer.getIssuer(), name, value) .onSuccess(ignore -> { config.save(); issuer.sendMessage(MVCorei18n.CONFIG_SET_SUCCESS, Replace.NAME.with(name), - Replace.VALUE.with(value)); + Replace.VALUE.with(stringPropertyHandle.getProperty(name).getOrNull())); }) .onFailure(e -> issuer.sendMessage(MVCorei18n.CONFIG_SET_ERROR, Replace.NAME.with(name), diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/ModifyCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/ModifyCommand.java index f2d3b93b4..dc1930b89 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/ModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/ModifyCommand.java @@ -10,18 +10,16 @@ import co.aikar.commands.annotation.Subcommand; import co.aikar.commands.annotation.Syntax; import jakarta.inject.Inject; -import org.bukkit.ChatColor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; 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.context.issueraware.MultiverseWorldValue; import org.mvplugins.multiverse.core.config.handle.PropertyModifyAction; import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle; import org.mvplugins.multiverse.core.locale.MVCorei18n; -import org.mvplugins.multiverse.core.locale.message.MessageReplacement; import org.mvplugins.multiverse.core.world.MultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; @@ -40,7 +38,10 @@ class ModifyCommand extends CoreCommand { @Subcommand("modify") @CommandPermission("multiverse.core.modify") - @CommandCompletion("@mvworlds:scope=both @propsmodifyaction @mvworldpropsname @mvworldpropsvalue") + @CommandCompletion("@mvworlds:scope=both|@propsmodifyaction:byIssuerForArg=arg1 " + + "@propsmodifyaction:notByIssuerForArg=arg1|@mvworldpropsname:byIssuerForArg=arg1 " + + "@mvworldpropsname:notByIssuerForArg=arg1|@mvworldpropsvalue:byIssuerForArg=arg1 " + + "@mvworldpropsvalue:notByIssuerForArg=arg1") @Syntax("[world] ") @Description("{@@mv-core.modify.description}") void onModifyCommand(// SUPPRESS CHECKSTYLE: ParameterNumber @@ -49,7 +50,7 @@ void onModifyCommand(// SUPPRESS CHECKSTYLE: ParameterNumber @Flags("resolve=issuerAware") @Syntax("[world]") @Description("{@@mv-core.modify.world.description}") - @NotNull MultiverseWorld world, + @NotNull MultiverseWorldValue worldValue, @Syntax("") @Description("") @@ -64,6 +65,8 @@ void onModifyCommand(// SUPPRESS CHECKSTYLE: ParameterNumber @Syntax("[value]") @Description("{@@mv-core.modify.value.description}") @Nullable String propertyValue) { + MultiverseWorld world = worldValue.value(); + if (action.isRequireValue() && propertyValue == null) { issuer.sendMessage(MVCorei18n.MODIFY_SPECIFYVALUE, replace("{action}").with(action.name().toLowerCase()), @@ -77,7 +80,7 @@ void onModifyCommand(// SUPPRESS CHECKSTYLE: ParameterNumber } StringPropertyHandle worldPropertyHandle = world.getStringPropertyHandle(); - worldPropertyHandle.modifyPropertyString(propertyName, propertyValue, action).onSuccess(ignore -> { + worldPropertyHandle.modifyPropertyString(issuer.getIssuer(), propertyName, propertyValue, action).onSuccess(ignore -> { issuer.sendMessage(MVCorei18n.MODIFY_SUCCESS, replace("{action}").with(action.name().toLowerCase()), replace("{property}").with(propertyName), @@ -111,7 +114,7 @@ private static final class LegacyAlias extends ModifyCommand implements LegacyAl @Override @CommandAlias("mvmodify|mvm") - void onModifyCommand(MVCommandIssuer issuer, @NotNull MultiverseWorld world, @NotNull PropertyModifyAction action, @NotNull String propertyName, @Nullable String propertyValue) { + void onModifyCommand(MVCommandIssuer issuer, @NotNull MultiverseWorldValue world, @NotNull PropertyModifyAction action, @NotNull String propertyName, @Nullable String propertyValue) { super.onModifyCommand(issuer, world, action, propertyName, propertyValue); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/SetSpawnCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/SetSpawnCommand.java index 90fea5c80..4697fc810 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/SetSpawnCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/SetSpawnCommand.java @@ -1,76 +1,94 @@ package org.mvplugins.multiverse.core.commands; -import co.aikar.commands.BukkitCommandIssuer; import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandCompletion; import co.aikar.commands.annotation.CommandPermission; import co.aikar.commands.annotation.Description; import co.aikar.commands.annotation.Optional; import co.aikar.commands.annotation.Subcommand; import co.aikar.commands.annotation.Syntax; -import io.vavr.control.Option; import jakarta.inject.Inject; import org.bukkit.Location; import org.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.command.LegacyAliasCommand; -import org.mvplugins.multiverse.core.command.MVCommandManager; +import org.mvplugins.multiverse.core.command.MVCommandIssuer; +import org.mvplugins.multiverse.core.command.context.PlayerLocation; +import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags; +import org.mvplugins.multiverse.core.command.flags.UnsafeFlags; +import org.mvplugins.multiverse.core.locale.MVCorei18n; +import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace; +import org.mvplugins.multiverse.core.teleportation.BlockSafety; import org.mvplugins.multiverse.core.world.WorldManager; @Service class SetSpawnCommand extends CoreCommand { private final WorldManager worldManager; + private final BlockSafety blockSafety; + private final UnsafeFlags flags; @Inject - SetSpawnCommand(@NotNull WorldManager worldManager) { + SetSpawnCommand(@NotNull WorldManager worldManager, @NotNull BlockSafety blockSafety, @NotNull UnsafeFlags flags) { this.worldManager = worldManager; + this.blockSafety = blockSafety; + this.flags = flags; } @CommandAlias("mvsetspawn") @Subcommand("setspawn") @CommandPermission("multiverse.core.spawn.set") - @Syntax("[x],[y],[z],[pitch],[yaw]") + @CommandCompletion("@flags:groupName=" + UnsafeFlags.NAME + " @flags:resolveUntil=arg1,groupName=" + UnsafeFlags.NAME) + @Syntax("[worldname:x,y,z[,pitch,yaw]] [--unsafe]") @Description("{@@mv-core.setspawn.description}") void onSetSpawnCommand( - BukkitCommandIssuer issuer, + MVCommandIssuer issuer, - @Optional - @Syntax("") + @Syntax("[worldname:x,y,z[,pitch,yaw]]") @Description("{@@mv-core.setspawn.location.description}") - Location location) { - Option.of(location).orElse(() -> { - if (issuer.isPlayer()) { - return Option.of(issuer.getPlayer().getLocation()); - } - return Option.none(); - }).peek(finalLocation -> - worldManager.getLoadedWorld(finalLocation.getWorld()) - .peek(mvWorld -> mvWorld.setSpawnLocation(finalLocation) - .onSuccess(ignore -> issuer.sendMessage( - "Successfully set spawn in " + mvWorld.getName() + " to " - + prettyLocation(mvWorld.getSpawnLocation()))) - .onFailure(e -> issuer.sendMessage(e.getLocalizedMessage()))) - .onEmpty(() -> issuer.sendMessage("That world is not loaded or does not exist!"))) - .onEmpty(() -> issuer.sendMessage("You must specify a location in the format: worldname:x,y,z")); + PlayerLocation playerLocation, + + @Optional + @Syntax("[--unsafe]") + @Description("") + String[] flagArray) { + ParsedCommandFlags parsedFlags = flags.parse(flagArray); + Location location = playerLocation.value(); + + if (!parsedFlags.hasFlag(flags.unsafe) && !blockSafety.canSpawnAtLocationSafely(location)) { + issuer.sendMessage(MVCorei18n.SETSPAWN_UNSAFE); + return; + } + + worldManager.getLoadedWorld(location.getWorld()) + .peek(mvWorld -> mvWorld.setSpawnLocation(location) + .onSuccess(ignore -> issuer.sendMessage(MVCorei18n.SETSPAWN_SUCCESS, + Replace.WORLD.with(mvWorld.getName()), + Replace.LOCATION.with(prettyLocation(location)))) + .onFailure(e -> issuer.sendMessage(MVCorei18n.SETSPAWN_FAILED, + Replace.WORLD.with(mvWorld.getName()), + Replace.ERROR.with(e)))) + .onEmpty(() -> issuer.sendMessage(MVCorei18n.SETSPAWN_NOTMVWORLD, + Replace.WORLD.with(location.getWorld().getName()))); } private String prettyLocation(Location location) { - return location.getX() + ", " + location.getY() + ", " + location.getZ() + ". pitch:" + location.getPitch() - + ", yaw:" + location.getYaw(); + return "%.2f, %.2f, %.2f, pitch:%.2f, yaw:%.2f" + .formatted(location.getX(), location.getY(), location.getZ(), location.getPitch(), location.getYaw()); } @Service private static final class LegacyAlias extends SetSpawnCommand implements LegacyAliasCommand { @Inject - LegacyAlias(@NotNull WorldManager worldManager) { - super(worldManager); + LegacyAlias(@NotNull WorldManager worldManager, @NotNull BlockSafety blockSafety, @NotNull UnsafeFlags flags) { + super(worldManager, blockSafety, flags); } @Override @CommandAlias("mvss") - void onSetSpawnCommand(BukkitCommandIssuer issuer, Location location) { - super.onSetSpawnCommand(issuer, location); + void onSetSpawnCommand(MVCommandIssuer issuer, PlayerLocation location, String[] flagArray) { + super.onSetSpawnCommand(issuer, location, flagArray); } } } 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 863056d96..047e48e67 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/SpawnCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/SpawnCommand.java @@ -60,7 +60,7 @@ final class SpawnCommand extends CoreCommand { @CommandAlias("mvspawn") @Subcommand("spawn") @CommandPermission("@mvspawn") - @CommandCompletion("@playersarray:checkPermissions=@mvspawnother|@flags:groupName=" + UnsafeFlags.NAME + ",resolveUntil=arg1" + @CommandCompletion("@playersarray:checkPermissions=@mvspawnother|@flags:resolveUntil=arg1,groupName=" + UnsafeFlags.NAME + " @flags:groupName=" + UnsafeFlags.NAME) @Syntax("[player]") @Description("{@@mv-core.spawn.description}") diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java index e8121e21e..8030c1792 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java @@ -1,9 +1,7 @@ package org.mvplugins.multiverse.core.commands; import java.util.Arrays; -import java.util.EnumMap; import java.util.List; -import java.util.Map; import co.aikar.commands.annotation.CommandAlias; import co.aikar.commands.annotation.CommandCompletion; @@ -20,8 +18,7 @@ import org.jvnet.hk2.annotations.Service; 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.context.issueraware.PlayerArrayValue; import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags; import org.mvplugins.multiverse.core.command.flags.UnsafeFlags; import org.mvplugins.multiverse.core.config.CoreConfig; @@ -31,7 +28,6 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace; import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker; import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter; -import org.mvplugins.multiverse.core.teleportation.TeleportFailureReason; @Service final class TeleportCommand extends CoreCommand { @@ -57,10 +53,9 @@ final class TeleportCommand extends CoreCommand { @CommandAlias("mvtp") @Subcommand("teleport|tp") @CommandPermission("@mvteleport") - @CommandCompletion( - "@destinations:playerOnly|@playersarray:checkPermissions=@mvteleportother " - + "@destinations:othersOnly|@flags:groupName=" + UnsafeFlags.NAME + ",resolveUntil=arg2 " - + "@flags:groupName=" + UnsafeFlags.NAME) + @CommandCompletion("@playersarray:checkPermissions=@mvteleportother|@destinations:byIssuerForArg=arg1 " + + "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME + " " + + "@flags:notByIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME) @Syntax("[player] [--unsafe]") @Description("{@@mv-core.teleport.description}") void onTeleportCommand( @@ -69,7 +64,7 @@ void onTeleportCommand( @Flags("resolve=issuerAware") @Syntax("[player]") @Description("{@@mv-core.teleport.player.description}") - Player[] players, + PlayerArrayValue playersValue, @Syntax("") @Description("{@@mv-core.teleport.destination.description}") @@ -79,6 +74,7 @@ void onTeleportCommand( @Syntax("[--unsafe]") @Description("") String[] flagArray) { + Player[] players = playersValue.value(); ParsedCommandFlags parsedFlags = flags.parse(flagArray); if (players.length == 1) { @@ -105,14 +101,21 @@ private void teleportSinglePlayer(MVCommandIssuer issuer, Player player, safetyTeleporter.to(destination) .by(issuer) .checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety()) - .teleport(player) + .passengerMode(config.getPassengerMode()) + .teleportSingle(player) .onSuccess(() -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS, Replace.PLAYER.with(getYouOrName(issuer, player)), Replace.DESTINATION.with(destination.toString()))) - .onFailure(failure -> issuer.sendError(MVCorei18n.TELEPORT_FAILED, - Replace.PLAYER.with(getYouOrName(issuer, player)), - Replace.DESTINATION.with(destination.toString()), - Replace.REASON.with(failure.getFailureMessage()))); + .onFailureCount(reasonsCountMap -> { + for (var entry : reasonsCountMap.entrySet()) { + Logging.finer("Failed to teleport %s players to %s: %s", + entry.getValue(), destination, entry.getKey()); + issuer.sendError(MVCorei18n.TELEPORT_FAILED, + Replace.PLAYER.with(player.getName()), + Replace.DESTINATION.with(destination.toString()), + Replace.REASON.with(Message.of(entry.getKey()))); + } + }); } private Message getYouOrName(MVCommandIssuer issuer, Player player) { @@ -131,6 +134,7 @@ private void teleportMultiplePlayers(MVCommandIssuer issuer, Player[] players, safetyTeleporter.to(destination) .by(issuer) .checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety()) + .passengerMode(config.getPassengerMode()) .teleport(List.of(players)) .onSuccessCount(successCount -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS, Replace.PLAYER.with(successCount + " players"), diff --git a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java index 9823547e6..11d961afb 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java @@ -12,6 +12,7 @@ import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.event.EventPriority; import org.bukkit.plugin.PluginManager; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; @@ -28,6 +29,8 @@ import org.mvplugins.multiverse.core.config.migration.VersionMigrator; import org.mvplugins.multiverse.core.config.migration.action.SetMigratorAction; import org.mvplugins.multiverse.core.destination.DestinationsProvider; +import org.mvplugins.multiverse.core.teleportation.PassengerMode; +import org.mvplugins.multiverse.core.teleportation.PassengerModes; import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat; @Service @@ -231,6 +234,31 @@ public boolean getUseFinerTeleportPermissions() { return configHandle.get(configNodes.useFinerTeleportPermissions); } + /** + * Sets the passenger mode + * + * @param passengerMode The passenger mode + * @return The set result + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public Try setPassengerMode(PassengerModes passengerMode) { + return configHandle.set(configNodes.passengerMode, passengerMode); + } + + /** + * Gets the passenger mode + * + * @return The passenger mode + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public PassengerMode getPassengerMode() { + return configHandle.get(configNodes.passengerMode); + } + /** * {@inheritDoc} */ @@ -632,10 +660,11 @@ public boolean isShowingDonateMessage() { } /** - * Gets the underlying config file object + * Gets the underlying config file object. For internal use only. * * @return The config file */ + @ApiStatus.Internal public FileConfiguration getConfig() { return configHandle.getConfig(); } 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 b6ebffdfa..d483c2333 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java @@ -4,7 +4,7 @@ import io.vavr.control.Try; import jakarta.inject.Inject; import jakarta.inject.Provider; -import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; import org.bukkit.event.EventPriority; import org.bukkit.plugin.PluginManager; @@ -19,12 +19,13 @@ import org.mvplugins.multiverse.core.config.node.NodeGroup; import org.mvplugins.multiverse.core.config.node.functions.NodeStringParser; import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer; +import org.mvplugins.multiverse.core.destination.DestinationInstance; import org.mvplugins.multiverse.core.destination.DestinationsProvider; -import org.mvplugins.multiverse.core.destination.core.WorldDestination; import org.mvplugins.multiverse.core.dynamiclistener.EventPriorityMapper; import org.mvplugins.multiverse.core.event.MVDebugModeEvent; import org.mvplugins.multiverse.core.exceptions.MultiverseException; import org.mvplugins.multiverse.core.permissions.PermissionUtils; +import org.mvplugins.multiverse.core.teleportation.PassengerModes; import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat; import java.util.Collection; @@ -183,6 +184,20 @@ private N node(N node) { .name("use-finer-teleport-permissions") .build()); + final ConfigNode passengerMode = node(ConfigNode.builder("teleport.passenger-mode", PassengerModes.class) + .comment("") + .comment("Configures how passengers and vehicles are handled when an entity is teleported.") + .comment(" default: Server will handle passengers and vehicles, this usually means entities will not be teleported to a different world if they have passengers.") + .comment(" dismount_passengers: Passengers will be removed from the parent entity before the teleport.") + .comment(" dismount_vehicle: Vehicle will be removed and from the parent entity before the teleport.") + .comment(" dismount_all: All passengers and vehicles will be removed from the parent entity before the teleport.") + .comment(" retain_passengers: Passengers will teleport together with the parent entity.") + .comment(" retain_vehicle: Vehicles will teleport together with the parent entity.") + .comment(" retain_all: All passengers and vehicles will teleport together with the parent entity.") + .defaultValue(PassengerModes.DEFAULT) + .name("passenger-mode") + .build()); + final ConfigNode concurrentTeleportLimit = node(ConfigNode.builder("teleport.concurrent-teleport-limit", Integer.class) .comment("") .comment("Sets the maximum number of players allowed to be teleported at once with `/mv teleport` command") @@ -237,6 +252,7 @@ private N node(N node) { .defaultValue("") .name("first-spawn-location") .suggester(this::suggestDestinations) + .stringParser(this::parseDestinationString) .build()); final ConfigNode enableJoinDestination = node(ConfigNode.builder("spawn.enable-join-destination", Boolean.class) @@ -254,6 +270,7 @@ private N node(N node) { .defaultValue("") .name("join-destination") .suggester(this::suggestDestinations) + .stringParser(this::parseDestinationString) .build()); final ConfigNode defaultRespawnInOverworld = node(ConfigNode.builder("spawn.default-respawn-in-overworld", Boolean.class) @@ -532,15 +549,14 @@ private N node(N node) { .hidden() .build()); - // todo: Maybe combine with the similar method in MVCommandCompletion but that has permission checking - private Collection suggestDestinations(String input) { - return destinationsProvider.get().getDestinations().stream() - .flatMap(destination -> destination.suggestDestinations(Bukkit.getConsoleSender(), null) - .stream() - .map(packet -> destination instanceof WorldDestination - ? packet.destinationString() - : destination.getIdentifier() + ":" + packet.destinationString())) - .toList(); + private Collection suggestDestinations(CommandSender sender, String input) { + return destinationsProvider.get().suggestDestinationStrings(sender, input); + } + + private Try parseDestinationString(CommandSender sender, String input, Class type) { + return destinationsProvider.get().parseDestination(sender, input) + .map(DestinationInstance::toString) + .toTry(); } private static final class DimensionFormatNodeSerializer implements NodeSerializer { diff --git a/src/main/java/org/mvplugins/multiverse/core/config/handle/StringPropertyHandle.java b/src/main/java/org/mvplugins/multiverse/core/config/handle/StringPropertyHandle.java index 95639bd03..8b2fb6aab 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/handle/StringPropertyHandle.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/handle/StringPropertyHandle.java @@ -5,6 +5,9 @@ import io.vavr.control.Option; import io.vavr.control.Try; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -40,6 +43,7 @@ public Collection getAllPropertyNames() { /** * Get property names that can be modified. ADD and REMOVE actions can only be used on list nodes. + * * @param action The target action * @return The property names modifiable for the action. */ @@ -71,20 +75,47 @@ public Try> getPropertyType(@Nullable String name) { /** * Suggests property values for command auto-complete based on the input and action type. * - * @param name The name of the property. - * @param input The input value to suggest based on. - * @param action The modification action being performed. + * @param name The name of the property. + * @param input The input value to suggest based on. + * @param action The modification action being performed. * @return A collection of suggested values. */ public Collection getSuggestedPropertyValue( - @Nullable String name, @Nullable String input, @NotNull PropertyModifyAction action) { + @Nullable String name, + @Nullable String input, + @NotNull PropertyModifyAction action + ) { + return getSuggestedPropertyValue(name, input, action, Bukkit.getConsoleSender()); + } + + /** + * Suggests property values for command auto-complete based on the input and action type. + *
+ * Providing a sender gives contextual information such as sender name, permissions, or player location + * for better suggestions. + * + * @param name The name of the property. + * @param input The input value to suggest based on. + * @param action The modification action being performed. + * @param sender The sender context to use. + * @return A collection of suggested values + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public Collection getSuggestedPropertyValue( + @Nullable String name, + @Nullable String input, + @NotNull PropertyModifyAction action, + @NotNull CommandSender sender + ) { return switch (action) { case SET -> findNode(name, ValueNode.class) - .map(node -> node.suggest(input)) + .map(node -> node.suggest(sender, input)) .getOrElse(Collections.emptyList()); case ADD -> findNode(name, ListValueNode.class) - .map(node -> node.suggestItem(input)) + .map(node -> node.suggestItem(sender, input)) .getOrElse(Collections.emptyList()); case REMOVE -> findNode(name, ListValueNode.class) @@ -111,7 +142,7 @@ public Try getProperty(@Nullable String name) { /** * Sets the value of the specified property name. * - * @param name The name of the property. + * @param name The name of the property. * @param value The value to set. * @return A Try indicating success or failure. */ @@ -122,7 +153,7 @@ public Try setProperty(@Nullable String name, @Nullable Object value) { /** * Adds a value to a list property name. * - * @param name The name of the property. + * @param name The name of the property. * @param value The value to add. * @return A Try indicating success or failure. */ @@ -133,7 +164,7 @@ public Try addProperty(@Nullable String name, @Nullable Object value) { /** * Removes a value from a list property name. * - * @param name The name of the property. + * @param name The name of the property. * @param value The value to remove. * @return A Try indicating success or failure. */ @@ -154,9 +185,9 @@ public Try resetProperty(@Nullable String name) { /** * Modifies a property name based on the given action. * - * @param name The name of the property. - * @param value The new value (if applicable). - * @param action The modification action. + * @param name The name of the property. + * @param value The new value (if applicable). + * @param action The modification action. * @return A Try indicating success or failure. */ public Try modifyProperty( @@ -173,59 +204,128 @@ public Try modifyProperty( /** * Sets the property value from a string representation. * - * @param name The name of the property. + * @param name The name of the property. * @param value The string value to set. * @return A Try indicating success or failure. */ public Try setPropertyString(@Nullable String name, @Nullable String value) { + return setPropertyString(Bukkit.getConsoleSender(), name, value); + } + + /** + * Sets the property value from a string representation with a sender context. + * + * @param sender The sender context. + * @param name The name of the property. + * @param value The string value to set. + * @return A Try indicating success or failure. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public Try setPropertyString(@NotNull CommandSender sender, @Nullable String name, @Nullable String value) { return findNode(name, ValueNode.class) - .flatMap(node -> node.parseFromString(value) + .flatMap(node -> node.parseFromString(sender, value) .flatMap(parsedValue -> handle.set(node, parsedValue))); } /** * Adds a value to a list property using its string representation. * - * @param name The name of the property. + * @param name The name of the property. * @param value The string value to add. * @return A Try indicating success or failure. */ public Try addPropertyString(@Nullable String name, @Nullable String value) { + return addPropertyString(Bukkit.getConsoleSender(), name, value); + } + + /** + * Adds a value to a list property using its string representation with a sender context. + * + * @param sender The sender context. + * @param name The name of the property. + * @param value The string value to add. + * @return A Try indicating success or failure. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public Try addPropertyString(@NotNull CommandSender sender, @Nullable String name, @Nullable String value) { return findNode(name, ListValueNode.class) - .flatMap(node -> node.parseItemFromString(value) + .flatMap(node -> node.parseItemFromString(sender, value) .flatMap(parsedValue -> handle.add(node, parsedValue))); } /** * Removes a value from a list property using its string representation. * - * @param name The name of the property. + * @param name The name of the property. * @param value The string value to remove. * @return A Try indicating success or failure. */ public Try removePropertyString(@Nullable String name, @Nullable String value) { + return removePropertyString(Bukkit.getConsoleSender(), name, value); + } + + /** + * Removes a value from a list property using its string representation with a sender context. + * + * @param sender The sender context. + * @param name The name of the property. + * @param value The string value to remove. + * @return A Try indicating success or failure. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public Try removePropertyString(@NotNull CommandSender sender, @Nullable String name, @Nullable String value) { return findNode(name, ListValueNode.class) - .flatMap(node -> node.parseItemFromString(value) + .flatMap(node -> node.parseItemFromString(sender, value) .flatMap(parsedValue -> handle.remove(node, parsedValue))); } /** * Modifies a property using a string value based on the given action. * - * @param name The name of the property. - * @param value The string value (if applicable). - * @param action The modification action. + * @param name The name of the property. + * @param value The string value (if applicable). + * @param action The modification action. * @return A Try indicating success or failure. */ public Try modifyPropertyString( - @Nullable String name, @Nullable String value, @NotNull PropertyModifyAction action) { + @Nullable String name, + @Nullable String value, + @NotNull PropertyModifyAction action + ) { + return modifyPropertyString(Bukkit.getConsoleSender(), name, value, action); + } + + /** + * Modifies a property using a string value based on the given action with a sender context. + * + * @param sender The sender context. + * @param name The name of the property. + * @param value The string value (if applicable). + * @param action The modification action. + * @return A Try indicating success or failure. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public Try modifyPropertyString( + @NotNull CommandSender sender, + @Nullable String name, + @Nullable String value, + @NotNull PropertyModifyAction action + ) { if (action.isRequireValue() && (value == null)) { return Try.failure(new IllegalArgumentException("Value is required for PropertyModifyAction: " + action)); } return switch (action) { - case SET -> setPropertyString(name, value); - case ADD -> addPropertyString(name, value); - case REMOVE -> removePropertyString(name, value); + case SET -> setPropertyString(sender, name, value); + case ADD -> addPropertyString(sender, name, value); + case REMOVE -> removePropertyString(sender, name, value); case RESET -> resetProperty(name); default -> Try.failure(new IllegalArgumentException("Unknown action: " + action)); }; @@ -234,9 +334,9 @@ public Try modifyPropertyString( /** * Finds a configuration node by name and type. * - * @param name The name of the node. - * @param type The expected class type of the node. - * @param The type of node. + * @param name The name of the node. + * @param type The expected class type of the node. + * @param The type of node. * @return A Try containing the found node or a failure if not found. */ private Try findNode(@Nullable String name, @NotNull Class type) { diff --git a/src/main/java/org/mvplugins/multiverse/core/config/node/ConfigNode.java b/src/main/java/org/mvplugins/multiverse/core/config/node/ConfigNode.java index 81cdd2d35..30e77df34 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/node/ConfigNode.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/node/ConfigNode.java @@ -8,9 +8,14 @@ import io.vavr.control.Option; import io.vavr.control.Try; +import org.apache.logging.log4j.util.Strings; +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.config.node.functions.SenderNodeStringParser; +import org.mvplugins.multiverse.core.config.node.functions.SenderNodeSuggester; import org.mvplugins.multiverse.core.config.node.serializer.DefaultSerializerProvider; import org.mvplugins.multiverse.core.config.node.functions.DefaultStringParserProvider; import org.mvplugins.multiverse.core.config.node.functions.DefaultSuggesterProvider; @@ -42,6 +47,7 @@ public class ConfigNode extends ConfigHeaderNode implements ValueNode { protected final @Nullable String name; protected final @NotNull Class type; + protected final @NotNull String[] aliases; protected @Nullable Supplier defaultValue; protected @Nullable NodeSuggester suggester; protected @Nullable NodeStringParser stringParser; @@ -54,6 +60,7 @@ protected ConfigNode( @NotNull String[] comments, @Nullable String name, @NotNull Class type, + @NotNull String[] aliases, @Nullable Supplier defaultValue, @Nullable NodeSuggester suggester, @Nullable NodeStringParser stringParser, @@ -63,6 +70,7 @@ protected ConfigNode( super(path, comments); this.name = name; this.type = type; + this.aliases = aliases; this.defaultValue = defaultValue; this.suggester = (suggester != null) ? suggester @@ -93,6 +101,14 @@ protected ConfigNode( return type; } + /** + * {@inheritDoc} + */ + @Override + public @NotNull String[] getAliases() { + return aliases; + } + /** * {@inheritDoc} */ @@ -115,6 +131,17 @@ protected ConfigNode( return Collections.emptyList(); } + /** + * {@inheritDoc} + */ + @Override + public @NotNull Collection suggest(@NotNull CommandSender sender, @Nullable String input) { + if (suggester != null && suggester instanceof SenderNodeSuggester senderSuggester) { + return senderSuggester.suggest(sender, input); + } + return suggest(input); + } + /** * {@inheritDoc} */ @@ -126,6 +153,17 @@ protected ConfigNode( return Try.failure(new UnsupportedOperationException("No string parser for type " + type.getName())); } + /** + * {@inheritDoc} + */ + @Override + public @NotNull Try parseFromString(@NotNull CommandSender sender, @Nullable String input) { + if (stringParser != null && stringParser instanceof SenderNodeStringParser senderStringParser) { + return senderStringParser.parse(sender, input, type); + } + return parseFromString(input); + } + /** * {@inheritDoc} */ @@ -164,6 +202,7 @@ public static class Builder> extends Confi protected @Nullable String name; protected @NotNull final Class type; + protected @NotNull String[] aliases = Strings.EMPTY_ARRAY; protected @Nullable Supplier defaultValue; protected @Nullable NodeSuggester suggester; protected @Nullable NodeStringParser stringParser; @@ -243,6 +282,20 @@ protected Builder(@NotNull String path, @NotNull Class type) { return name(null); } + /** + * Sets the aliases for this node. Aliases are alternative identifiers for referencing the node. + * + * @param aliases The aliases to set for this node. + * @return This builder. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public @NotNull B aliases(@NotNull String... aliases) { + this.aliases = aliases; + return self(); + } + /** * Sets the suggester for this node. * @@ -254,6 +307,20 @@ protected Builder(@NotNull String path, @NotNull Class type) { return self(); } + /** + * Sets the suggester for this node with sender context. + * + * @param suggester The suggester for this node. + * @return This builder. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public @NotNull B suggester(@NotNull SenderNodeSuggester suggester) { + this.suggester = suggester; + return self(); + } + /** * Sets the string parser for this node. * @@ -265,6 +332,20 @@ protected Builder(@NotNull String path, @NotNull Class type) { return self(); } + /** + * Sets the string parser for this node with sender context. + * + * @param stringParser The string parser for this node. + * @return This builder. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public @NotNull B stringParser(@NotNull SenderNodeStringParser stringParser) { + this.stringParser = stringParser; + return self(); + } + /** * Sets the serializer for this node. * @@ -304,7 +385,7 @@ protected Builder(@NotNull String path, @NotNull Class type) { @Override public @NotNull ConfigNode build() { return new ConfigNode<>(path, comments.toArray(new String[0]), - name, type, defaultValue, suggester, stringParser, serializer, validator, onSetValue); + name, type, aliases, defaultValue, suggester, stringParser, serializer, validator, onSetValue); } } } diff --git a/src/main/java/org/mvplugins/multiverse/core/config/node/ListConfigNode.java b/src/main/java/org/mvplugins/multiverse/core/config/node/ListConfigNode.java index 45ca1fb1a..138c2fed6 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/node/ListConfigNode.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/node/ListConfigNode.java @@ -5,7 +5,6 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; @@ -13,6 +12,8 @@ import io.vavr.Value; import io.vavr.control.Try; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -20,6 +21,8 @@ import org.mvplugins.multiverse.core.config.node.functions.DefaultSuggesterProvider; import org.mvplugins.multiverse.core.config.node.functions.NodeStringParser; import org.mvplugins.multiverse.core.config.node.functions.NodeSuggester; +import org.mvplugins.multiverse.core.config.node.functions.SenderNodeStringParser; +import org.mvplugins.multiverse.core.config.node.functions.SenderNodeSuggester; import org.mvplugins.multiverse.core.config.node.serializer.DefaultSerializerProvider; import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer; import org.mvplugins.multiverse.core.utils.REPatterns; @@ -58,6 +61,7 @@ protected ListConfigNode( @NotNull String[] comments, @Nullable String name, @NotNull Class> type, + @NotNull String[] aliases, @Nullable Supplier> defaultValueSupplier, @Nullable NodeSuggester suggester, @Nullable NodeStringParser> stringParser, @@ -70,7 +74,7 @@ protected ListConfigNode( @Nullable NodeSerializer itemSerializer, @Nullable Function> itemValidator, @Nullable BiConsumer onSetItemValue) { - super(path, comments, name, type, defaultValueSupplier, suggester, stringParser, serializer, + super(path, comments, name, type, aliases, defaultValueSupplier, suggester, stringParser, serializer, validator, onSetValue); this.itemType = itemType; this.itemSuggester = itemSuggester != null @@ -110,12 +114,12 @@ private void setDefaults() { } private void setDefaultSuggester() { - this.suggester = input -> { - if (input == null) { - return itemSuggester.suggest(null); - } - return StringFormatter.addonToCommaSeperated(input, itemSuggester.suggest(input)); - }; + if (itemSuggester instanceof SenderNodeSuggester senderItemSuggester) { + this.suggester = (SenderNodeSuggester)(sender, input) -> + StringFormatter.addonToCommaSeperated(input, senderItemSuggester.suggest(sender, input)); + } else { + this.suggester = input -> StringFormatter.addonToCommaSeperated(input, itemSuggester.suggest(input)); + } } private void setDefaultStringParser() { @@ -194,6 +198,17 @@ private void setDefaultOnSetValue() { return Collections.emptyList(); } + /** + * {@inheritDoc} + */ + @Override + public @NotNull Collection suggestItem(@NotNull CommandSender sender, @Nullable String input) { + if (itemSuggester != null && itemSuggester instanceof SenderNodeSuggester senderSuggester) { + return senderSuggester.suggest(sender, input); + } + return suggestItem(input); + } + /** * {@inheritDoc} */ @@ -205,6 +220,17 @@ private void setDefaultOnSetValue() { return Try.failure(new UnsupportedOperationException("No item string parser for type " + itemType)); } + /** + * {@inheritDoc} + */ + @Override + public @NotNull Try parseItemFromString(@NotNull CommandSender sender, @Nullable String input) { + if (itemStringParser != null && itemStringParser instanceof SenderNodeStringParser senderStringParser) { + return senderStringParser.parse(sender, input, itemType); + } + return parseItemFromString(input); + } + /** * {@inheritDoc} */ @@ -267,6 +293,20 @@ protected Builder(@NotNull String path, @NotNull Class itemType) { return self(); } + /** + * Sets the suggester for an individual item in the list with sender context. + * + * @param itemSuggester The suggester. + * @return This builder. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public @NotNull B itemSuggester(@NotNull SenderNodeSuggester itemSuggester) { + this.itemSuggester = itemSuggester; + return self(); + } + /** * Sets the string parser for an individual item in the list. * @@ -278,6 +318,20 @@ protected Builder(@NotNull String path, @NotNull Class itemType) { return self(); } + /** + * Sets the string parser for an individual item in the list with sender context. + * + * @param itemStringParser The string parser. + * @return This builder. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public @NotNull B itemStringParser(@NotNull SenderNodeStringParser itemStringParser) { + this.itemStringParser = itemStringParser; + return self(); + } + /** * Sets the serializer for an individual item in the list. * @@ -321,6 +375,7 @@ protected Builder(@NotNull String path, @NotNull Class itemType) { comments.toArray(new String[0]), name, type, + aliases, defaultValue, suggester, stringParser, diff --git a/src/main/java/org/mvplugins/multiverse/core/config/node/ListValueNode.java b/src/main/java/org/mvplugins/multiverse/core/config/node/ListValueNode.java index 9b9eff851..cb5d0671f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/node/ListValueNode.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/node/ListValueNode.java @@ -4,6 +4,8 @@ import java.util.List; import io.vavr.control.Try; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,6 +33,21 @@ public interface ListValueNode extends ValueNode> { */ @NotNull Collection suggestItem(@Nullable String input); + /** + * Suggests possible string values for this node. Use contextural information from the sender such as + * sender name, permissions, or player location for better suggestions. + * + * @param sender The sender context. + * @param input The input string. + * @return A collection of possible string values + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + default @NotNull Collection suggestItem(@NotNull CommandSender sender, @Nullable String input) { + return suggestItem(input); + } + /** * Parses the given string into a value of type {@link I}. Used for property set by user input. * @@ -39,6 +56,20 @@ public interface ListValueNode extends ValueNode> { */ @NotNull Try parseItemFromString(@Nullable String input); + /** + * Parses the given string into a value of type {@link I}. Used for property set by user input. + * + * @param sender The sender context. + * @param input The string to parse. + * @return The parsed value, or given exception if parsing failed. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + default @NotNull Try parseItemFromString(@NotNull CommandSender sender, @Nullable String input) { + return parseItemFromString(input); + } + /** * Gets the serializer for this node. * diff --git a/src/main/java/org/mvplugins/multiverse/core/config/node/NodeGroup.java b/src/main/java/org/mvplugins/multiverse/core/config/node/NodeGroup.java index 7b3117615..779bb012b 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/node/NodeGroup.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/node/NodeGroup.java @@ -1,9 +1,11 @@ package org.mvplugins.multiverse.core.config.node; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import io.github.townyadvanced.commentedconfiguration.setting.CommentedNode; @@ -16,6 +18,7 @@ */ public class NodeGroup implements Collection { private final Collection nodes; + private final List nodeNames; private final Map nodesMap; /** @@ -23,6 +26,7 @@ public class NodeGroup implements Collection { */ public NodeGroup() { this.nodes = new ArrayList<>(); + this.nodeNames = new ArrayList<>(); this.nodesMap = new HashMap<>(); } @@ -34,18 +38,27 @@ public NodeGroup() { public NodeGroup(@NotNull Collection nodes) { this.nodes = nodes; this.nodesMap = new HashMap<>(nodes.size()); + this.nodeNames = new ArrayList<>(nodes.size()); nodes.forEach(this::addNodeIndex); } private void addNodeIndex(@NotNull Node node) { - if (node instanceof ValueNode) { - ((ValueNode) node).getName().peek(name -> nodesMap.put(name, node)); + if (node instanceof ValueNode valueNode) { + valueNode.getName().peek(name -> { + nodeNames.add(name); + nodesMap.put(name, node); + Arrays.stream(valueNode.getAliases()).forEach(alias -> nodesMap.put(alias, node)); + }); } } private void removeNodeIndex(@NotNull Node node) { - if (node instanceof ValueNode) { - ((ValueNode) node).getName().peek(nodesMap::remove); + if (node instanceof ValueNode valueNode) { + valueNode.getName().peek(name -> { + nodeNames.remove(name); + nodesMap.remove(name); + Arrays.stream(valueNode.getAliases()).forEach(alias -> nodesMap.remove(alias, node)); + }); } } @@ -55,7 +68,7 @@ private void removeNodeIndex(@NotNull Node node) { * @return The names of all nodes in this group. */ public @NotNull Collection getNames() { - return nodesMap.keySet(); + return nodeNames; } /** diff --git a/src/main/java/org/mvplugins/multiverse/core/config/node/ValueNode.java b/src/main/java/org/mvplugins/multiverse/core/config/node/ValueNode.java index 51db14a68..aaf62fa73 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/node/ValueNode.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/node/ValueNode.java @@ -4,6 +4,9 @@ import io.vavr.control.Option; import io.vavr.control.Try; +import org.apache.logging.log4j.util.Strings; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -12,7 +15,7 @@ public interface ValueNode extends Node { /** - * Gets the name of this node. Used for identifying the node from user input. + * Gets the name of this node. Used for identifying the node from user input. This must be unique within a node group. * * @return An {@link Option} containing the name of this node, or {@link Option.None} if the node has no name. */ @@ -25,6 +28,19 @@ public interface ValueNode extends Node { */ @NotNull Class getType(); + /** + * Gets the aliases of this node. Serves as shorter or legacy alternatives the {@link #getName()} and must be + * unique within a node group. + * + * @return The aliases of this node. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + default @NotNull String[] getAliases() { + return Strings.EMPTY_ARRAY; + } + /** * Gets the default value with type {@link T} of the node. * @@ -40,6 +56,19 @@ public interface ValueNode extends Node { */ @NotNull Collection suggest(@Nullable String input); + /** + * Suggests possible string values for this node. Use contextural information from the sender such as + * sender name, permissions, or player location for better suggestions. + * + * @param sender The sender context. + * @param input The input string. + * @return A collection of possible string values + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + @NotNull Collection suggest(@NotNull CommandSender sender, @Nullable String input); + /** * Parses the given string into a value of type {@link T}. Used for property set by user input. * @@ -48,6 +77,21 @@ public interface ValueNode extends Node { */ @NotNull Try parseFromString(@Nullable String input); + /** + * Parses the given string into a value of type {@link T} with context from the sender. + * Used for property set by user input. + * + * @param sender The sender context. + * @param input The string to parse. + * @return The parsed value, or given exception if parsing failed. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + default @NotNull Try parseFromString(@NotNull CommandSender sender, @Nullable String input) { + return parseFromString(input); + } + /** * Gets the serializer for this node. * diff --git a/src/main/java/org/mvplugins/multiverse/core/config/node/functions/NodeSuggester.java b/src/main/java/org/mvplugins/multiverse/core/config/node/functions/NodeSuggester.java index 7a72c15db..9ec8c3371 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/node/functions/NodeSuggester.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/node/functions/NodeSuggester.java @@ -6,7 +6,8 @@ import org.jetbrains.annotations.Nullable; /** - * A function that suggests possible values for a node value. + * A function that suggests possible values for a node value. These suggestions must be able to be used to parse the + * value from string with {@link NodeStringParser}. */ @FunctionalInterface public interface NodeSuggester { diff --git a/src/main/java/org/mvplugins/multiverse/core/config/node/functions/SenderNodeStringParser.java b/src/main/java/org/mvplugins/multiverse/core/config/node/functions/SenderNodeStringParser.java new file mode 100644 index 000000000..b008dd287 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/config/node/functions/SenderNodeStringParser.java @@ -0,0 +1,35 @@ +package org.mvplugins.multiverse.core.config.node.functions; + +import io.vavr.control.Try; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A function that parses a string into a node value object of type {@link T} with contextual information from the sender. + * + * @param The type of the object to parse. + */ +@ApiStatus.AvailableSince("5.1") +public interface SenderNodeStringParser extends NodeStringParser { + /** + * Parses a string into a node value object of type {@link T} with contextual information from the sender. + * This ties in with {@link SenderNodeSuggester} that provides suggestions based on the sender context. + * + * @param sender The sender context. + * @param string The string to parse. + * @param type The type of the object to parse. + * @return The parsed object, or {@link Try.Failure} if the string could not be parsed. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + @NotNull Try parse(@NotNull CommandSender sender, @Nullable String string, @NotNull Class type); + + @Override + default @NotNull Try parse(@Nullable String string, @NotNull Class type) { + return parse(Bukkit.getConsoleSender(), string, type); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/config/node/functions/SenderNodeSuggester.java b/src/main/java/org/mvplugins/multiverse/core/config/node/functions/SenderNodeSuggester.java new file mode 100644 index 000000000..19d56d962 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/config/node/functions/SenderNodeSuggester.java @@ -0,0 +1,36 @@ +package org.mvplugins.multiverse.core.config.node.functions; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +/** + * A function that suggests possible values for a node value with sender contextual information. + * + * @since 5.1 + */ +@ApiStatus.AvailableSince("5.1") +@FunctionalInterface +public interface SenderNodeSuggester extends NodeSuggester { + + /** + * Suggests possible values for a node value. Generated based on the current user input and sender contextual information. + * + * @param sender The sender context. + * @param input The current partial user input + * @return The possible values. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + @NotNull Collection suggest(@NotNull CommandSender sender, @Nullable String input); + + @Override + default @NotNull Collection suggest(@Nullable String input) { + return suggest(Bukkit.getConsoleSender(), input); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/Destination.java b/src/main/java/org/mvplugins/multiverse/core/destination/Destination.java index a8c2bb368..08e986cda 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/Destination.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/Destination.java @@ -2,7 +2,9 @@ import java.util.Collection; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jvnet.hk2.annotations.Contract; @@ -11,6 +13,9 @@ /** * A destination is a location that can be teleported to. + *
+ * Please ensure you implement at least one of {@link #getDestinationInstance(CommandSender, String)} or + * {@link #getDestinationInstance(String)} to prevent a stack overflow. * * @param The type of the destination * @param The type of the destination instance @@ -18,13 +23,13 @@ @Contract public interface Destination, T extends DestinationInstance, F extends FailureReason> { /** - * Returns the identifier or prefix that is required for this destination. + * Returns the identifier or prefix required for this destination. * *

Portals have a prefix of "p" for example and OpenWarp (third party plugin) uses "ow". This is derived from a - * hash and cannot have duplicate values. Read that as your plugin cannot use 'p' because it's already used. + * hash and cannot have duplicate values. Means that your plugin cannot use 'p' because it's already used. * Please check the wiki when adding a custom destination!

* - * @return The identifier or prefix that is required for this destination. + * @return The identifier or prefix required for this destination. */ @NotNull String getIdentifier(); @@ -33,8 +38,36 @@ public interface Destination, T extends Destinati * * @param destinationParams The destination parameters. ex: p:MyPortal:nw * @return The destination instance, or null if the destination parameters are invalid. + * + * @deprecated Use {@link #getDestinationInstance(CommandSender, String)} instead. This method will no longer be + * called by {@link DestinationsProvider}. + */ + @Deprecated(forRemoval = true, since = "5.1") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") + default @NotNull Attempt getDestinationInstance(@NotNull String destinationParams) { + return getDestinationInstance(Bukkit.getConsoleSender(), destinationParams); + } + + /** + * Returns the destination instance for the given destination parameters with sender context. This allows + * for shorthands such as getting location or name from sender. If no sender context is available, use + * {@link Bukkit#getConsoleSender()} will be defaulted by {@link DestinationsProvider}. + *
+ * Note that the resulting {@link DestinationInstance} should be (de)serializable without the original sender context. + *
+ * For example, the parsable string with sender context `e:@here` should return a {@link DestinationInstance} that + * is serialized to `e:world:x,y,z:p:y`, which can be deserialized without the original sender context. + * + * @param sender The sender context. + * @param destinationParams The destination parameters. ex: p:MyPortal:nw + * @return The destination instance, or null if the destination parameters are invalid. + * + * @since 5.1 */ - @NotNull Attempt getDestinationInstance(@NotNull String destinationParams); + @ApiStatus.AvailableSince("5.1") + default Attempt getDestinationInstance(@NotNull CommandSender sender, @NotNull String destinationParams) { + return getDestinationInstance(destinationParams); + } /** * Returns a list of possible destinations for the given destination parameters. This packet's destination diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/DestinationSuggestionPacket.java b/src/main/java/org/mvplugins/multiverse/core/destination/DestinationSuggestionPacket.java index fafc9b6ae..7d496011a 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/DestinationSuggestionPacket.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/DestinationSuggestionPacket.java @@ -1,5 +1,8 @@ package org.mvplugins.multiverse.core.destination; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.core.destination.core.WorldDestination; + /** * Data of a possible destination for tab completion and permission checking * @@ -8,4 +11,19 @@ * @param finerPermissionSuffix The finer permission suffix */ public record DestinationSuggestionPacket(Destination destination, String destinationString, String finerPermissionSuffix) { + + /** + * Gets a parsable string representation of the destination that is most likely valid for + * {@link DestinationsProvider#parseDestination(String)}. + * + * @return The parsable string + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public String parsableString() { + return destination instanceof WorldDestination + ? destinationString + : destination.getIdentifier() + ":" + destinationString; + } } diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/DestinationsProvider.java b/src/main/java/org/mvplugins/multiverse/core/destination/DestinationsProvider.java index f1cd37dd6..2ed15f9f4 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/DestinationsProvider.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/DestinationsProvider.java @@ -7,7 +7,9 @@ import co.aikar.locales.MessageKey; import co.aikar.locales.MessageKeyProvider; import jakarta.inject.Inject; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jvnet.hk2.annotations.Service; @@ -50,8 +52,25 @@ public void registerDestination(@NotNull Destination destination) { * @param destinationString The destination string. * @return The destination object, or null if invalid format. */ - @SuppressWarnings("unchecked,rawtypes") public @NotNull Attempt, FailureReason> parseDestination(@NotNull String destinationString) { + return this.parseDestination(Bukkit.getConsoleSender(), destinationString); + } + + /** + * Converts a destination string to a destination object with sender context. + * + * @param sender The target sender context. + * @param destinationString The destination string. + * @return The destination object, or null if invalid format. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + @SuppressWarnings("unchecked,rawtypes") + public @NotNull Attempt, FailureReason> parseDestination( + @NotNull CommandSender sender, + @NotNull String destinationString + ) { String[] items = destinationString.split(SEPARATOR, 2); String idString = items[0]; @@ -73,7 +92,7 @@ public void registerDestination(@NotNull Destination destination) { replace("{ids}").with(String.join(", ", this.destinationMap.keySet()))); } - return destination.getDestinationInstance(destinationParams); + return destination.getDestinationInstance(sender, destinationParams); } /** @@ -95,12 +114,36 @@ public void registerDestination(@NotNull Destination destination) { return this.destinationMap.values(); } + /** + * Gets suggestions for possible parsable destinations. + * + * @param sender The target sender context. + * @param destinationParams The current user input. + * @return A collection of destination suggestions. + */ public @NotNull Collection suggestDestinations(@NotNull CommandSender sender, @Nullable String destinationParams) { return this.getDestinations().stream() .flatMap(destination -> destination.suggestDestinations(sender, destinationParams).stream()) .toList(); } + /** + * Gets suggestions for possible parsable destinations. + * + * @param sender The target sender context. + * @param destinationParams The current user input. + * @return A collection of destination suggestions in parsable string format. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public @NotNull Collection suggestDestinationStrings(@NotNull CommandSender sender, @Nullable String destinationParams) { + return suggestDestinations(sender, destinationParams) + .stream() + .map(DestinationSuggestionPacket::parsableString) + .toList(); + } + public enum ParseFailureReason implements FailureReason { INVALID_DESTINATION_ID(MVCorei18n.DESTINATION_PARSE_FAILUREREASON_INVALIDDESTINATIONID), ; diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/AnchorDestination.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/AnchorDestination.java index 15a559590..51e9af722 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/AnchorDestination.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/AnchorDestination.java @@ -45,7 +45,10 @@ public final class AnchorDestination implements Destination getDestinationInstance(@NotNull String destinationParams) { + public @NotNull Attempt getDestinationInstance( + @NotNull CommandSender sender, + @NotNull String destinationParams + ) { return this.anchorManager.getAnchor(destinationParams) .fold( () -> Attempt.failure(InstanceFailureReason.ANCHOR_NOT_FOUND, replace("{anchor}").with(destinationParams)), diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/BedDestination.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/BedDestination.java index d3c8ace02..7527152cc 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/BedDestination.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/BedDestination.java @@ -45,7 +45,10 @@ public final class BedDestination implements Destination getDestinationInstance(@NotNull String destinationParams) { + public @NotNull Attempt getDestinationInstance( + @NotNull CommandSender sender, + @NotNull String destinationParams + ) { Player player = PlayerFinder.get(destinationParams); if (player == null && !OWN_BED_STRING.equals(destinationParams)) { return Attempt.failure(InstanceFailureReason.PLAYER_NOT_FOUND, Replace.PLAYER.with(destinationParams)); diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/CannonDestination.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/CannonDestination.java index 50191375e..901bd2079 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/CannonDestination.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/CannonDestination.java @@ -51,7 +51,10 @@ public final class CannonDestination implements Destination getDestinationInstance(@NotNull String destinationParams) { + public @NotNull Attempt getDestinationInstance( + @NotNull CommandSender sender, + @NotNull String destinationParams + ) { String[] params = REPatterns.COLON.split(destinationParams); if (params.length != 5) { return Attempt.failure(InstanceFailureReason.INVALID_FORMAT); diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestination.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestination.java index 570537493..e1b0b4337 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestination.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestination.java @@ -1,6 +1,7 @@ package org.mvplugins.multiverse.core.destination.core; import java.util.Collection; +import java.util.stream.Stream; import co.aikar.locales.MessageKey; import co.aikar.locales.MessageKeyProvider; @@ -8,7 +9,9 @@ import jakarta.inject.Inject; import org.bukkit.Location; import org.bukkit.World; +import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jvnet.hk2.annotations.Service; @@ -17,7 +20,6 @@ import org.mvplugins.multiverse.core.destination.Destination; import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket; import org.mvplugins.multiverse.core.locale.MVCorei18n; -import org.mvplugins.multiverse.core.locale.message.MessageReplacement; import org.mvplugins.multiverse.core.utils.REPatterns; import org.mvplugins.multiverse.core.utils.result.Attempt; import org.mvplugins.multiverse.core.utils.result.FailureReason; @@ -67,9 +69,19 @@ public ExactDestination(CoreConfig config, WorldManager worldManager, WorldEntry * {@inheritDoc} */ @Override - public @NotNull Attempt getDestinationInstance(@NotNull String destinationParams) { + public @NotNull Attempt getDestinationInstance( + @NotNull CommandSender sender, + @NotNull String destinationParams + ) { String[] items = REPatterns.COLON.split(destinationParams); if (items.length < 2) { + if (items[0].equals("@here")) { + return getLocationFromSender(sender) + .map(location -> Attempt.success( + new ExactDestinationInstance(this, new UnloadedWorldLocation(location)) + )) + .getOrElse(() -> Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT)); // todo: specific failure reason for this case + } return Attempt.failure(InstanceFailureReason.INVALID_FORMAT); } @@ -118,19 +130,61 @@ private Option getLoadedMultiverseWorld(String worldName) : worldManager.getLoadedWorld(worldName); } + private Option getLocationFromSender(CommandSender sender) { + if (sender instanceof Entity entity) { + return Option.of(entity.getLocation()); + } + if (sender instanceof BlockCommandSender blockSender) { + return Option.of(blockSender.getBlock().getLocation()); + } + return Option.none(); + } + /** * {@inheritDoc} */ @Override public @NotNull Collection suggestDestinations( @NotNull CommandSender sender, @Nullable String destinationParams) { - return worldManager.getLoadedWorlds().stream() + Stream stream = worldManager.getLoadedWorlds().stream() .filter(world -> worldEntryCheckerProvider.forSender(sender) .canAccessWorld(world) .isSuccess()) .map(world -> - new DestinationSuggestionPacket(this, world.getTabCompleteName() + ":", world.getName())) - .toList(); + new DestinationSuggestionPacket(this, world.getTabCompleteName() + ":", world.getName())); + + Location location = getLocationFromSender(sender).getOrNull(); + if (location != null) { + var herePacket = new DestinationSuggestionPacket( + this, + "@here", + location.getWorld().getName() + ); + var locationPacket = new DestinationSuggestionPacket( + this, + "%s:%.2f,%.2f,%.2f".formatted( + location.getWorld().getName(), + location.getX(), + location.getY(), + location.getZ() + ), + location.getWorld().getName() + ); + var locationPacketPW = new DestinationSuggestionPacket( + this, + "%s:%.2f,%.2f,%.2f:%.2f:%.2f".formatted( + location.getWorld().getName(), + location.getX(), + location.getY(), + location.getZ(), + location.getPitch(), + location.getYaw() + ), + location.getWorld().getName() + ); + stream = Stream.concat(stream, Stream.of(herePacket, locationPacket, locationPacketPW)); + } + return stream.toList(); } public enum InstanceFailureReason implements FailureReason { diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/PlayerDestination.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/PlayerDestination.java index fac0edb8a..b09d3c151 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/PlayerDestination.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/PlayerDestination.java @@ -43,7 +43,10 @@ public final class PlayerDestination implements Destination getDestinationInstance(@NotNull String destinationParams) { + public @NotNull Attempt getDestinationInstance( + @NotNull CommandSender sender, + @NotNull String destinationParams + ) { Player player = PlayerFinder.get(destinationParams); if (player == null) { return Attempt.failure(InstanceFailureReason.PLAYER_NOT_FOUND, Replace.PLAYER.with(destinationParams)); diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/WorldDestination.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/WorldDestination.java index a0bcde4a6..7c86c9d83 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/WorldDestination.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/WorldDestination.java @@ -60,7 +60,10 @@ public final class WorldDestination implements Destination getDestinationInstance(@NotNull String destinationParams) { + public @NotNull Attempt getDestinationInstance( + @NotNull CommandSender sender, + @NotNull String destinationParams + ) { String[] items = REPatterns.COLON.split(destinationParams, 3); String worldName = items[0]; MultiverseWorld world = getMultiverseWorld(worldName); 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 0bfdc0bcd..a210f73cb 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 @@ -8,6 +8,7 @@ import org.bukkit.ChatColor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; /** * Filter content and text based on regex matching. @@ -68,7 +69,7 @@ public boolean checkMatch(String value) { if (!hasValidRegex()) { return false; } - String text = ChatColor.stripColor(String.valueOf(value)).toLowerCase(); + String text = ChatTextFormatter.removeColor(String.valueOf(value)).toLowerCase(); try { return regexPattern.matcher(text).find(); } catch (PatternSyntaxException ignored) { 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 9ca64e737..f2de2659c 100644 --- a/src/main/java/org/mvplugins/multiverse/core/listeners/MVChatListener.java +++ b/src/main/java/org/mvplugins/multiverse/core/listeners/MVChatListener.java @@ -5,7 +5,6 @@ import jakarta.inject.Inject; import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.bukkit.ChatColor; import org.bukkit.entity.Player; import org.bukkit.event.player.AsyncPlayerChatEvent; import org.jvnet.hk2.annotations.Service; @@ -15,6 +14,7 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod; import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancelled; import org.mvplugins.multiverse.core.dynamiclistener.annotations.SkipIfEventExist; +import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; import org.mvplugins.multiverse.core.world.WorldManager; /** @@ -87,7 +87,7 @@ void asyncPlayerChat(AsyncPlayerChatEvent event) { String prefixChatFormat = config.getPrefixChatFormat(); prefixChatFormat = prefixChatFormat.replace("%world%", worldName).replace("%chat%", chat); - prefixChatFormat = ChatColor.translateAlternateColorCodes('&', prefixChatFormat); + prefixChatFormat = ChatTextFormatter.colorize(prefixChatFormat); event.setFormat(prefixChatFormat); } 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 81948be68..1d1b0b247 100644 --- a/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java +++ b/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java @@ -181,6 +181,10 @@ public enum MVCorei18n implements MessageKeyProvider { SETSPAWN_DESCRIPTION, SETSPAWN_LOCATION_DESCRIPTION, SETSPAWN_WORLD_DESCRIPTION, + SETSPAWN_UNSAFE, + SETSPAWN_SUCCESS, + SETSPAWN_FAILED, + SETSPAWN_NOTMVWORLD, // /mv spawn SPAWN_DESCRIPTION, diff --git a/src/main/java/org/mvplugins/multiverse/core/locale/message/MessageReplacement.java b/src/main/java/org/mvplugins/multiverse/core/locale/message/MessageReplacement.java index 1868b5ceb..c54595f89 100644 --- a/src/main/java/org/mvplugins/multiverse/core/locale/message/MessageReplacement.java +++ b/src/main/java/org/mvplugins/multiverse/core/locale/message/MessageReplacement.java @@ -105,6 +105,7 @@ public enum Replace { DESTINATION(replace("{destination}")), ERROR(replace("{error}")), GAMERULE(replace("{gamerule}")), + LOCATION(replace("{location}")), NAME(replace("{name}")), PLAYER(replace("{player}")), REASON(replace("{reason}")), diff --git a/src/main/java/org/mvplugins/multiverse/core/module/MultiverseModule.java b/src/main/java/org/mvplugins/multiverse/core/module/MultiverseModule.java index 44ab2984f..bcd8840b2 100644 --- a/src/main/java/org/mvplugins/multiverse/core/module/MultiverseModule.java +++ b/src/main/java/org/mvplugins/multiverse/core/module/MultiverseModule.java @@ -108,8 +108,8 @@ protected void shutdownDependencyInjection() { * * @deprecated Use {@link #registerDynamicListeners(Class)} with the new DynamicListener API. */ - @Deprecated(since = "5.0.0", forRemoval = true) - @ApiStatus.ScheduledForRemoval(inVersion = "6.0.0") + @Deprecated(since = "5.0", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") protected void registerEvents(Class listenerClass) { var pluginManager = getServer().getPluginManager(); Try.run(() -> serviceLocator.getAllServices(listenerClass).forEach( 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 8f9da96cc..487eb43e8 100644 --- a/src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissionsChecker.java +++ b/src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissionsChecker.java @@ -349,8 +349,8 @@ public String toString() { return scope; } - public static Scope getApplicableScope(CommandSender sender, Entity entity) { - if (sender instanceof Entity senderEntity && senderEntity.equals(entity)) { + public static Scope getApplicableScope(CommandSender teleporter, Entity entity) { + if (teleporter instanceof Entity senderEntity && senderEntity.equals(entity)) { return Scope.SELF; } return Scope.OTHER; diff --git a/src/main/java/org/mvplugins/multiverse/core/teleportation/AsyncSafetyTeleporterAction.java b/src/main/java/org/mvplugins/multiverse/core/teleportation/AsyncSafetyTeleporterAction.java index 1f34c54cc..1e70de926 100644 --- a/src/main/java/org/mvplugins/multiverse/core/teleportation/AsyncSafetyTeleporterAction.java +++ b/src/main/java/org/mvplugins/multiverse/core/teleportation/AsyncSafetyTeleporterAction.java @@ -11,6 +11,7 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.plugin.PluginManager; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.mvplugins.multiverse.core.MultiverseCore; @@ -20,6 +21,7 @@ import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate; import org.mvplugins.multiverse.core.utils.result.Attempt; +import java.util.ArrayList; import java.util.List; /** @@ -35,6 +37,7 @@ public final class AsyncSafetyTeleporterAction { private final @NotNull Either> locationOrDestination; private boolean checkSafety; + private PassengerMode passengerMode = PassengerModes.DEFAULT; private @Nullable CommandSender teleporter = null; AsyncSafetyTeleporterAction( @@ -58,18 +61,32 @@ public final class AsyncSafetyTeleporterAction { * Sets whether to check for safe location before teleport. * * @param checkSafety Whether to check for safe location - * @return A new {@link AsyncSafetyTeleporterAction} to be chained + * @return The same {@link AsyncSafetyTeleporterAction} to be chained */ public AsyncSafetyTeleporterAction checkSafety(boolean checkSafety) { this.checkSafety = checkSafety; return this; } + /** + * Sets the passenger mode + * + * @param passengerMode The passenger mode + * @return The same {@link AsyncSafetyTeleporterAction} to be chained + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public AsyncSafetyTeleporterAction passengerMode(@NotNull PassengerMode passengerMode) { + this.passengerMode = passengerMode; + return this; + } + /** * Sets the teleporter. * * @param issuer The issuer - * @return A new {@link AsyncSafetyTeleporterAction} to be chained + * @return The same {@link AsyncSafetyTeleporterAction} to be chained */ public AsyncSafetyTeleporterAction by(@NotNull BukkitCommandIssuer issuer) { return by(issuer.getIssuer()); @@ -79,7 +96,7 @@ public AsyncSafetyTeleporterAction by(@NotNull BukkitCommandIssuer issuer) { * Sets the teleporter. * * @param teleporter The teleporter - * @return A new {@link AsyncSafetyTeleporterAction} to be chained + * @return The same {@link AsyncSafetyTeleporterAction} to be chained */ public AsyncSafetyTeleporterAction by(@NotNull CommandSender teleporter) { this.teleporter = teleporter; @@ -94,24 +111,31 @@ public AsyncSafetyTeleporterAction by(@NotNull CommandSender teleporter) { * @return A list of async futures that represent the teleportation result of each entity */ public AsyncAttemptsAggregate teleport(@NotNull List teleportees) { - return AsyncAttemptsAggregate.allOf(teleportees.stream().map(this::teleport).toList()); + return AsyncAttemptsAggregate.allOfAggregate(teleportees.stream().map(this::teleportSingle).toList()); } /** - * Teleports one entity + * Teleports one parent entity. Multiple entities may be teleported depending on {@link #passengerMode(PassengerMode)}. * * @param teleportee The entity to teleport - * @return An async future that represents the teleportation result + * @return A list of async future that represents the teleportation result + * + * @since 5.1 */ - public AsyncAttempt teleport(@NotNull Entity teleportee) { + @ApiStatus.AvailableSince("5.1") + public AsyncAttemptsAggregate teleportSingle(@NotNull Entity teleportee) { var localTeleporter = this.teleporter == null ? teleportee : this.teleporter; - return AsyncAttempt.fromAttempt(getLocation(teleportee).mapAttempt(this::doSafetyCheck)) + + return getLocation(teleportee).mapAttempt(this::doSafetyCheck) .onSuccess(() -> { if (teleportee instanceof Player player) { this.teleportQueue.addToQueue(localTeleporter, player); } }) - .mapAsyncAttempt(location -> doAsyncTeleport(teleportee, location)) + .transform( + location -> doAsyncTeleport(teleportee, location), + failure -> AsyncAttemptsAggregate.allOf(AsyncAttempt.failure(failure)) + ) .thenRun(() -> { if (teleportee instanceof Player player) { this.teleportQueue.popFromQueue(player.getName()); @@ -119,6 +143,21 @@ public AsyncAttempt teleport(@NotNull Entity telepo }); } + /** + * Teleports one entity + * + * @param teleportee The entity to teleport + * @return An async future that represents the teleportation result + * + * @deprecated Use {@link #teleportSingle(Entity)} instead, as teleport of single entity may result in multiple + * teleports due to vehicle and passengers based on {@link #passengerMode(PassengerMode)}. + */ + @Deprecated(forRemoval = true, since = "5.1") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") + public AsyncAttempt teleport(@NotNull Entity teleportee) { + return teleportSingle(teleportee).getAttempts().get(0); + } + private Attempt getLocation(@NotNull Entity teleportee) { return this.locationOrDestination.fold( this::parseLocation, @@ -160,9 +199,61 @@ private Attempt doSafetyCheck(@NotNull Location return Attempt.success(safeLocation); } - private AsyncAttempt doAsyncTeleport( + private AsyncAttemptsAggregate doAsyncTeleport( + @NotNull Entity teleportee, + @NotNull Location location + ) { + if (passengerMode.isDismountVehicle() && teleportee.isInsideVehicle()) { + Entity vehicle = teleportee.getVehicle(); + if (vehicle != null) { + return doVehicleTeleport(teleportee, location, vehicle); + } + } + + List passengers = teleportee.getPassengers(); + if (passengerMode.isDismountPassengers() && !passengers.isEmpty()) { + passengers.forEach(teleportee::removePassenger); + if (passengerMode.isPassengersFollow()) { + return doPassengersTeleport(teleportee, location, passengers); + } + } + + return AsyncAttemptsAggregate.allOf(doSingleTeleport(teleportee, location)); + } + + private AsyncAttemptsAggregate doVehicleTeleport( + @NotNull Entity teleportee, + @NotNull Location location, + @NotNull Entity vehicle + ) { + if (passengerMode.isVehicleFollow()) { + return doPassengersTeleport(vehicle, location, vehicle.getPassengers()); + } + teleportee.leaveVehicle(); + return doAsyncTeleport(teleportee, location); + } + + private AsyncAttemptsAggregate doPassengersTeleport( + @NotNull Entity teleportee, + @NotNull Location location, + @NotNull List passengers + ) { + List toTeleport = new ArrayList<>(passengers); + toTeleport.addFirst(teleportee); + + return AsyncAttemptsAggregate.allOfAggregate(toTeleport.stream() + .map(passenger -> doAsyncTeleport(passenger, location)) + .toList()) + .onSuccess(() -> Bukkit.getScheduler().runTask(multiverseCore, () -> { + passengers.forEach(teleportee::addPassenger); + Logging.finer("Mounted %d passengers to %s", passengers.size(), teleportee.getName()); + })); + } + + private AsyncAttempt doSingleTeleport( @NotNull Entity teleportee, - @NotNull Location location) { + @NotNull Location location + ) { return AsyncAttempt.of(PaperLib.teleportAsync(teleportee, location), exception -> { Logging.warning("Failed to teleport %s to %s: %s", teleportee.getName(), location, exception.getMessage()); diff --git a/src/main/java/org/mvplugins/multiverse/core/teleportation/PassengerMode.java b/src/main/java/org/mvplugins/multiverse/core/teleportation/PassengerMode.java new file mode 100644 index 000000000..587a6c37d --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/teleportation/PassengerMode.java @@ -0,0 +1,60 @@ +package org.mvplugins.multiverse.core.teleportation; + +import org.jetbrains.annotations.ApiStatus; + +/** + * Defines how passengers and vehicles on an entity should be handled when the entity is teleported. + * + * @since 5.1 + */ +@ApiStatus.AvailableSince("5.1") +public interface PassengerMode { + /** + * Defines whether the passengers should be removed from the parent entity when the parent entity is teleported. + *
+ * Teleports between worlds may fail if passengers are not removed. + * + * @return Whether the passengers should be removed + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + boolean isDismountPassengers(); + + /** + * Defines whether the passengers should follow the parent entity when the parent entity is teleported. + *
+ * This only applies of passengers are removed from the parent entity, i.e. {@link #isDismountPassengers()} is true. + * + * @return Whether the passengers should follow + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + boolean isPassengersFollow(); + + /** + * Defines whether the entity should dismount from the vehicle when the entity is teleported. + *
+ * Teleport between worlds may fail if entity is not dismounted. + * + * @return Whether the entity should dismount + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + boolean isDismountVehicle(); + + /** + * Defines whether the vehicle and other passengers on it should follow the target entity on the vehicle when + * the target entity is teleported. + *
+ * This only applies if the entity is dismounted from the vehicle, i.e. {@link #isDismountVehicle()} is true. + * + * @return Whether the vehicle should follow + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + boolean isVehicleFollow(); +} diff --git a/src/main/java/org/mvplugins/multiverse/core/teleportation/PassengerModes.java b/src/main/java/org/mvplugins/multiverse/core/teleportation/PassengerModes.java new file mode 100644 index 000000000..d0c90d510 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/teleportation/PassengerModes.java @@ -0,0 +1,114 @@ +package org.mvplugins.multiverse.core.teleportation; + +import org.jetbrains.annotations.ApiStatus; + +/** + * Enum shorthands for {@link PassengerMode} to define common passenger modes configurations. + * + * @since 5.1 + */ +@ApiStatus.AvailableSince("5.1") +public enum PassengerModes implements PassengerMode { + /** + * All passengers and vehicles are handled by the server. This usually means that entities with passengers + * or in vehicles will not be able to teleport to a different world until they manually dismount. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + DEFAULT(false, false, false, false), + + /** + * All passengers will be removed from the parent entity before the teleport. Passengers will be left behind. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + DISMOUNT_PASSENGERS(true, false, false, false), + + /** + * Parent entity will be dismounted from the vehicle before the teleport. Vehicle will be left behind. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + DISMOUNT_VEHICLE(true, false, true, false), + + /** + * All passengers and vehicles will be removed from the parent entity before the teleport. Passengers and vehicles + * will be left behind. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + DISMOUNT_ALL(true, false, true, false), + + /** + * All passengers will teleport together with the parent entity. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + RETAIN_PASSENGERS(true, true, false, false), + + /** + * All vehicles will teleport together with the parent entity. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + RETAIN_VEHICLE(false, false, true, true), + + /** + * All passengers and vehicles will teleport together with the parent entity. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + RETAIN_ALL(true, true, true, true), + ; + + private final boolean dismountPassengers; + private final boolean passengersFollow; + private final boolean dismountVehicle; + private final boolean vehicleFollow; + + PassengerModes(boolean dismountPassengers, boolean mountPassengers, boolean dismountVehicle, boolean mountVehicle) { + this.dismountPassengers = dismountPassengers; + this.passengersFollow = mountPassengers; + this.dismountVehicle = dismountVehicle; + this.vehicleFollow = mountVehicle; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isDismountPassengers() { + return dismountPassengers; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPassengersFollow() { + return passengersFollow; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isDismountVehicle() { + return dismountVehicle; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isVehicleFollow() { + return vehicleFollow; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/MinecraftTimeFormatter.java b/src/main/java/org/mvplugins/multiverse/core/utils/MinecraftTimeFormatter.java new file mode 100644 index 000000000..81c888f90 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/MinecraftTimeFormatter.java @@ -0,0 +1,73 @@ +package org.mvplugins.multiverse.core.utils; + +import io.vavr.control.Try; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.ApiStatus; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * Utility class for formatting Minecraft time to real-world time format. + * + * @since 5.1 + */ +@ApiStatus.AvailableSince("5.1") +public final class MinecraftTimeFormatter { + + private static final double TIME_MULTIPLIER = 3.6; + private static final long DAY_SECONDS = 24 * 60 * 60; + private static final long START_OFFSET = 6 * 60 * 60; + + /** + * Formats Minecraft time to 12-hour format. + * + * @param time The Minecraft time to format. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static String format12h(long time) { + return formatTime(time, "hh:mm a"); + } + + /** + * Formats Minecraft time to 24-hour format. + * + * @param time The Minecraft time to format. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static String format24h(long time) { + return formatTime(time, "HH:mm"); + } + + /** + * Formats Minecraft time to the specified format. + * See DateTimeFormatter documentation + * for available patterns. + * + * @param time The Minecraft time to format. + * @param format The format string for the time. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static String formatTime(long time, String format) { + // Convert Minecraft time to real-world time + long realTime = (long) ((time * TIME_MULTIPLIER) + START_OFFSET) % DAY_SECONDS; // Minecraft ticks to seconds + + // Convert seconds to LocalTime + LocalTime localTime = LocalTime.ofSecondOfDay(realTime); + + return Try.of(() -> DateTimeFormatter.ofPattern(format)) + .map(localTime::format) + .getOrElse("invalid time format: " + format); + } + + private MinecraftTimeFormatter() { + // No instantiation + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/result/AsyncAttemptsAggregate.java b/src/main/java/org/mvplugins/multiverse/core/utils/result/AsyncAttemptsAggregate.java index cca2008bf..2ba5e65db 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/result/AsyncAttemptsAggregate.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/result/AsyncAttemptsAggregate.java @@ -1,5 +1,8 @@ package org.mvplugins.multiverse.core.utils.result; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -36,7 +39,39 @@ public static AsyncAttemptsAggregate allOf(Li * @return An instance of {@link AsyncAttemptsAggregate}. */ public static AsyncAttemptsAggregate allOf(AsyncAttempt... attempts) { - return new AsyncAttemptsAggregate<>(List.of(attempts)); + return allOf(List.of(attempts)); + } + + /** + * Combines multiple {@link AsyncAttemptsAggregate} lists into a single one. + * + * @param attempts The asynchronous attempts aggregates to combine. + * @param The type of the successful result. + * @param The type representing failure reasons. + * @return An instance of {@link AsyncAttemptsAggregate}. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static AsyncAttemptsAggregate allOfAggregate(List> attempts) { + return new AsyncAttemptsAggregate<>(attempts.stream() + .flatMap(a -> a.attempts.stream()) + .toList()); + } + + /** + * Combines multiple {@link AsyncAttemptsAggregate} varargs arrays into a single one. + * + * @param attempts The asynchronous attempts aggregates to combine. + * @param The type of the successful result. + * @param The type representing failure reasons. + * @return An instance of {@link AsyncAttemptsAggregate}. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static AsyncAttemptsAggregate allOfAggregate(AsyncAttemptsAggregate... attempts) { + return allOfAggregate(List.of(attempts)); } /** @@ -47,22 +82,40 @@ public static AsyncAttemptsAggregate allOf(As * @return An instance of {@link AsyncAttemptsAggregate} with no attempts. */ public static AsyncAttemptsAggregate emptySuccess() { - return new AsyncAttemptsAggregate<>(CompletableFuture.completedFuture(AttemptsAggregate.emptySuccess())); + return new AsyncAttemptsAggregate<>( + Collections.emptyList(), + CompletableFuture.completedFuture(AttemptsAggregate.emptySuccess()) + ); } + private final List> attempts; private final CompletableFuture> future; private AsyncAttemptsAggregate(List> attempts) { - future = CompletableFuture.allOf(attempts.stream().map(AsyncAttempt::getFuture).toArray(CompletableFuture[]::new)) + this.attempts = attempts; + this.future = CompletableFuture.allOf(attempts.stream().map(AsyncAttempt::getFuture).toArray(CompletableFuture[]::new)) .thenApply(v -> AttemptsAggregate.allOf(attempts.stream() .map(AsyncAttempt::getFuture) .map(CompletableFuture::join).toList())); } - private AsyncAttemptsAggregate(CompletableFuture> future) { + private AsyncAttemptsAggregate(List> attempts, CompletableFuture> future) { + this.attempts = attempts; this.future = future; } + /** + * Gets an immutable copy of the list of asynchronous attempts that this aggregate represents. + * + * @return The list of asynchronous attempts. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public List> getAttempts() { + return attempts.stream().toList(); + } + /** * Executes the provided {@link Runnable} if there are successful attempts. * @@ -70,8 +123,7 @@ private AsyncAttemptsAggregate(CompletableFuture> future * @return A new {@link AsyncAttemptsAggregate} instance. */ public AsyncAttemptsAggregate onSuccess(Runnable runnable) { - return new AsyncAttemptsAggregate<>( - future.thenApply(aggregate -> aggregate.onSuccess(runnable))); + return newFuture(future.thenApply(aggregate -> aggregate.onSuccess(runnable))); } /** @@ -81,8 +133,7 @@ public AsyncAttemptsAggregate onSuccess(Runnable runnable) { * @return A new {@link AsyncAttemptsAggregate} instance. */ public AsyncAttemptsAggregate onFailure(Runnable runnable) { - return new AsyncAttemptsAggregate<>( - future.thenApply(aggregate -> aggregate.onFailure(runnable))); + return newFuture(future.thenApply(aggregate -> aggregate.onFailure(runnable))); } /** @@ -92,8 +143,7 @@ public AsyncAttemptsAggregate onFailure(Runnable runnable) { * @return A new {@link AsyncAttemptsAggregate} instance. */ public AsyncAttemptsAggregate onSuccess(Consumer>> successConsumer) { - return new AsyncAttemptsAggregate<>( - future.thenApply(aggregate -> aggregate.onSuccess(successConsumer))); + return newFuture(future.thenApply(aggregate -> aggregate.onSuccess(successConsumer))); } /** @@ -103,8 +153,7 @@ public AsyncAttemptsAggregate onSuccess(Consumer>> succ * @return A new {@link AsyncAttemptsAggregate} instance. */ public AsyncAttemptsAggregate onFailure(Consumer>> failureConsumer) { - return new AsyncAttemptsAggregate<>( - future.thenApply(aggregate -> aggregate.onFailure(failureConsumer))); + return newFuture(future.thenApply(aggregate -> aggregate.onFailure(failureConsumer))); } /** @@ -114,8 +163,7 @@ public AsyncAttemptsAggregate onFailure(Consumer>> fail * @return A new {@link AsyncAttemptsAggregate} instance. */ public AsyncAttemptsAggregate onSuccessCount(Consumer successConsumer) { - return new AsyncAttemptsAggregate<>( - future.thenApply(aggregate -> aggregate.onSuccessCount(successConsumer))); + return newFuture(future.thenApply(aggregate -> aggregate.onSuccessCount(successConsumer))); } /** @@ -125,7 +173,26 @@ public AsyncAttemptsAggregate onSuccessCount(Consumer successCons * @return A new {@link AsyncAttemptsAggregate} instance. */ public AsyncAttemptsAggregate onFailureCount(Consumer> failureConsumer) { - return new AsyncAttemptsAggregate<>( - future.thenApply(aggregate -> aggregate.onFailureCount(failureConsumer))); + return newFuture(future.thenApply(aggregate -> aggregate.onFailureCount(failureConsumer))); + } + + /** + * Executes an action after all async attempts are completed. + * + * @param runnable The action to execute after all async attempts are completed. + * @return A new {@link AsyncAttemptsAggregate} instance. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public AsyncAttemptsAggregate thenRun(Runnable runnable) { + return newFuture(future.thenApply(aggregate -> { + runnable.run(); + return aggregate; + })); + } + + private AsyncAttemptsAggregate newFuture(CompletableFuture> future) { + return new AsyncAttemptsAggregate<>(attempts, future); } } 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 2c414278c..795d9a4b5 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 @@ -5,6 +5,9 @@ import java.util.function.Supplier; import io.vavr.control.Either; +import io.vavr.control.Try; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.core.exceptions.MultiverseException; import org.mvplugins.multiverse.core.locale.message.Message; import org.mvplugins.multiverse.core.locale.message.MessageReplacement; @@ -117,6 +120,29 @@ default boolean isFailure() { return this instanceof Failure; } + /** + * Converts this {@link Attempt} instance to an equivalent {@link Try} representation. Defaults to a + * {@link MultiverseException} with failure message if this is a failure attempt. + * + * @return A {@link Try} instance representing the result of this {@code Attempt}. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + Try toTry(); + + /** + * Converts this attempt to a {@code Try} instance. If this attempt represents a failure, the + * provided exception supplier will be invoked to create a failed try. + * + * @param throwableFunction A function that provides a throwable in case of failure. + * @return The {@link Try} instance corresponding to this attempt. + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + Try toTry(Function, Throwable> throwableFunction); + default Attempt thenRun(Runnable runnable) { runnable.run(); return this; @@ -219,6 +245,25 @@ default Attempt transform(UF failureReason) { } } + /** + * Maps attempt result to another value. + * + * @param successMapper Action taken if the attempt is a success + * @param failureMapper Action taken if the attempt is a failure + * @param The transformed value type + * @return The transformed value + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + default U transform(Function successMapper, Function failureMapper) { + if (this instanceof Success) { + return successMapper.apply(get()); + } else { + return failureMapper.apply(getFailureReason()); + } + } + /** * Calls either the failure or success function depending on the result type. * @@ -333,6 +378,16 @@ public T getOrThrow(Function, X> exceptionSu return value; } + @Override + public Try toTry() { + return Try.success(value); + } + + @Override + public Try toTry(Function, Throwable> throwableFunction) { + return Try.success(value); + } + @Override public F getFailureReason() { throw new UnsupportedOperationException("No failure reason as attempt is a success"); @@ -386,6 +441,16 @@ public T getOrThrow(Function, X> exceptionSu throw exceptionSupplier.apply(this); } + @Override + public Try toTry() { + return Try.failure(new MultiverseException(message)); + } + + @Override + public Try toTry(Function, Throwable> throwableFunction) { + return Try.failure(throwableFunction.apply(this)); + } + @Override public F getFailureReason() { return failureReason; 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 new file mode 100644 index 000000000..f553010af --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/text/AdventureTextFormatter.java @@ -0,0 +1,40 @@ +package org.mvplugins.multiverse.core.utils.text; + +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; + +final class AdventureTextFormatter implements TextFormatter { + @Override + public void sendFormattedMessage(CommandSender sender, String message) { + sender.sendMessage(colorize(message)); + } + + @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)); + } + + @Override + public String colorize(String message) { + return LegacyComponentSerializer.legacySection().serialize(toAmpComponent(message)); + } + + private TextComponent toAmpComponent(String message) { + return LegacyComponentSerializer.legacyAmpersand().deserialize(message); + } + + private TextComponent toSectionComponent(String message) { + return LegacyComponentSerializer.legacySection().deserialize(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 new file mode 100644 index 000000000..f6251658f --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/text/ChatColorTextFormatter.java @@ -0,0 +1,31 @@ +package org.mvplugins.multiverse.core.utils.text; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +final class ChatColorTextFormatter implements TextFormatter { + @Override + public void sendFormattedMessage(CommandSender sender, String message) { + sender.sendMessage(colorize(message)); + } + + @Override + public String removeColor(String message) { + return ChatColor.stripColor(colorize(message)); + } + + @Override + public String removeAmpColor(String message) { + return removeColor(message); + } + + @Override + public String removeSectionColor(String message) { + return ChatColor.stripColor(message); + } + + @Override + public String colorize(String message) { + 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 new file mode 100644 index 000000000..43b752d66 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/text/ChatTextFormatter.java @@ -0,0 +1,89 @@ +package org.mvplugins.multiverse.core.utils.text; + +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.core.utils.ReflectHelper; + +/** + * Utility class to format chat messages. Uses Kyori Adventure if available for better format support such as &#rrggbb + * hex codes. Falls back to bukkit's ChatColor if Kyori Adventure is not available. + * + * @since 5.1 + */ +@ApiStatus.AvailableSince("5.1") +public final class ChatTextFormatter { + + private static final TextFormatter wrapper; + + static { + if (ReflectHelper.hasClass("net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer")) { + wrapper = new AdventureTextFormatter(); + } else { + wrapper = new ChatColorTextFormatter(); + } + } + + /** + * Sends message with color formatting applied. + * + * @param sender The sender to send the message to + * @param message The message to send + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static void sendFormattedMessage(CommandSender sender, String message) { + wrapper.sendFormattedMessage(sender, message); + } + + /** + * Remove all color formatting from the message. + * + * @param message The text to remove color from + * @return The text with color formatting removed + * + * @since 5.1 + */ + public static String removeColor(String message) { + return wrapper.removeColor(message); + } + + /** + * Removes & color formatting from the message. + * + * @param message The text to remove color from + * @return The text with color formatting removed + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static String removeAmpColor(String message) { + return wrapper.removeAmpColor(message); + } + + /** + * Removes ยง color formatting from the message. + * + * @param message The text to remove color from + * @return The text with color formatting removed + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static String removeSectionColor(String message) { + return wrapper.removeSectionColor(message); + } + + /** + * Applies color formatting to the message. + * + * @param message The text to apply color to + * @return The text with color formatting + * + * @since 5.1 + */ + @ApiStatus.AvailableSince("5.1") + public static String colorize(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 new file mode 100644 index 000000000..6a66b0bbb --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/text/TextFormatter.java @@ -0,0 +1,15 @@ +package org.mvplugins.multiverse.core.utils.text; + +import org.bukkit.command.CommandSender; + +interface TextFormatter { + void sendFormattedMessage(CommandSender sender, String message); + + String removeColor(String message); + + String removeAmpColor(String message); + + String removeSectionColor(String message); + + String colorize(String message); +} diff --git a/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java b/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java index 6071d206d..2609df770 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java @@ -5,7 +5,6 @@ import com.google.common.base.Strings; import io.vavr.control.Try; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.Difficulty; import org.bukkit.GameMode; import org.bukkit.Location; @@ -16,6 +15,7 @@ import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle; +import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; import org.mvplugins.multiverse.core.world.location.SpawnLocation; import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig; @@ -137,7 +137,7 @@ public String getColourlessAlias() { } void updateColourlessAlias() { - colourlessAlias = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', getAliasOrName())); + colourlessAlias = ChatTextFormatter.removeColor(getAliasOrName()); } /** 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 c85229db7..2ff002136 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java @@ -49,6 +49,7 @@ import org.mvplugins.multiverse.core.utils.result.Attempt; import org.mvplugins.multiverse.core.utils.result.FailureReason; import org.mvplugins.multiverse.core.utils.FileUtils; +import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; import org.mvplugins.multiverse.core.world.biomeprovider.BiomeProviderFactory; import org.mvplugins.multiverse.core.world.entity.EntityPurger; import org.mvplugins.multiverse.core.world.generators.GeneratorProvider; @@ -899,9 +900,10 @@ public Option getUnloadedWorldByNameOrAlias(@Nullable String wo } private Option getUnloadedWorldByAlias(@Nullable String alias) { + String colourlessAlias = ChatTextFormatter.removeColor(alias); return Option.ofOptional(worldsMap.values().stream() .filter(world -> !world.isLoaded()) - .filter(world -> world.getColourlessAlias().equalsIgnoreCase(ChatColor.stripColor(alias))) + .filter(world -> world.getColourlessAlias().equalsIgnoreCase(colourlessAlias)) .findFirst()); } diff --git a/src/main/resources/multiverse-core_en.properties b/src/main/resources/multiverse-core_en.properties index f73dba199..2e1e7df40 100644 --- a/src/main/resources/multiverse-core_en.properties +++ b/src/main/resources/multiverse-core_en.properties @@ -164,6 +164,10 @@ mv-core.root.help=&aSee &f/mv help&a for commands available. mv-core.setspawn.description=Sets the spawn location of the specified world mv-core.setspawn.location.description=Location of the new spawn mv-core.setspawn.world.description=Target world to set spawn of (defaults to player's current world) +mv-core.setspawn.unsafe=&cThe new spawn location is unsafe! If this is intentional, you can disable safety checks with &6--unsafe&c flag. +mv-core.setspawn.success=&aSuccessfully set spawn in &6{world}&a to &6{location}&a! +mv-core.setspawn.failed=&cFailed to set spawn in &6{world}&c to &6{location}&c. {error} +mv-core.setspawn.notmvworld=&cUnable to set spawn for &c{world} as it is not a Multiverse world. # /mv spawn mv-core.spawn.description=Teleports the specified player to the spawn of the world they are in diff --git a/src/test/java/org/mvplugins/multiverse/core/commands/AbstractCommandTest.kt b/src/test/java/org/mvplugins/multiverse/core/commands/AbstractCommandTest.kt index c30ce057c..9f5b8b408 100644 --- a/src/test/java/org/mvplugins/multiverse/core/commands/AbstractCommandTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/commands/AbstractCommandTest.kt @@ -1,6 +1,5 @@ package org.mvplugins.multiverse.core.commands -import org.bukkit.ChatColor import org.bukkit.permissions.PermissionAttachment import org.mockbukkit.mockbukkit.command.ConsoleCommandSenderMock import org.mockbukkit.mockbukkit.entity.PlayerMock @@ -8,6 +7,7 @@ import org.mvplugins.multiverse.core.TestWithMockBukkit import org.mvplugins.multiverse.core.command.MVCommandManager import org.mvplugins.multiverse.core.locale.PluginLocales import org.mvplugins.multiverse.core.locale.message.Message +import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter import org.mvplugins.multiverse.core.world.WorldManager import org.mvplugins.multiverse.core.world.options.CreateWorldOptions import kotlin.test.BeforeTest @@ -47,8 +47,8 @@ abstract class AbstractCommandTest : TestWithMockBukkit() { fun assertCommandOutput(message : Message) { assertEquals( - ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&',message.formatted(locales))), - ChatColor.stripColor(player.nextMessage()) + ChatTextFormatter.removeColor(message.formatted(locales)), + ChatTextFormatter.removeColor(player.nextMessage()) ) } } diff --git a/src/test/java/org/mvplugins/multiverse/core/commands/VersionCommandTest.kt b/src/test/java/org/mvplugins/multiverse/core/commands/VersionCommandTest.kt index 2e9e58a7f..e61ee1c2f 100644 --- a/src/test/java/org/mvplugins/multiverse/core/commands/VersionCommandTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/commands/VersionCommandTest.kt @@ -6,6 +6,7 @@ import org.bukkit.ChatColor import org.mvplugins.multiverse.core.locale.MVCorei18n import org.mvplugins.multiverse.core.locale.message.Message import org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace +import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -15,7 +16,7 @@ class VersionCommandTest : AbstractCommandTest() { @Test fun `Run version command as console`() { assertTrue(Bukkit.dispatchCommand(console, "mv version")) - val output = ChatColor.stripColor(console.nextMessage()) + val output = ChatTextFormatter.removeColor(console.nextMessage()) assertEquals("Multiverse Core Version v" + multiverseCore.getDescription().getVersion(), output) } diff --git a/src/test/java/org/mvplugins/multiverse/core/config/ConfigTest.kt b/src/test/java/org/mvplugins/multiverse/core/config/ConfigTest.kt index c01db57d5..9e9e90887 100644 --- a/src/test/java/org/mvplugins/multiverse/core/config/ConfigTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/config/ConfigTest.kt @@ -81,8 +81,8 @@ class ConfigTest : TestWithMockBukkit() { assertTrue(config.stringPropertyHandle.setProperty("enforce-access", true).isSuccess) assertEquals(true, config.stringPropertyHandle.getProperty("enforce-access").get()) - assertTrue(config.stringPropertyHandle.setProperty("first-spawn-location", "world2").isSuccess) - assertEquals("world2", config.stringPropertyHandle.getProperty("first-spawn-location").get()) + assertTrue(config.stringPropertyHandle.setProperty("chat-prefix-format", "%world% %chat%").isSuccess) + assertEquals("%world% %chat%", config.stringPropertyHandle.getProperty("chat-prefix-format").get()) assertTrue(config.stringPropertyHandle.setProperty("global-debug", 1).isSuccess) assertEquals(1, config.stringPropertyHandle.getProperty("global-debug").get()) @@ -93,8 +93,8 @@ class ConfigTest : TestWithMockBukkit() { assertTrue(config.stringPropertyHandle.setPropertyString("enforce-access", "true").isSuccess) assertEquals(true, config.stringPropertyHandle.getProperty("enforce-access").get()) - assertTrue(config.stringPropertyHandle.setPropertyString("first-spawn-location", "world2").isSuccess) - assertEquals("world2", config.stringPropertyHandle.getProperty("first-spawn-location").get()) + assertTrue(config.stringPropertyHandle.setPropertyString("chat-prefix-format", "%world% %chat%").isSuccess) + assertEquals("%world% %chat%", config.stringPropertyHandle.getProperty("chat-prefix-format").get()) assertTrue(config.stringPropertyHandle.setPropertyString("global-debug", "1").isSuccess) assertEquals(1, config.stringPropertyHandle.getProperty("global-debug").get()) diff --git a/src/test/resources/configs/fresh_config.yml b/src/test/resources/configs/fresh_config.yml index 66898898b..aefac74bd 100644 --- a/src/test/resources/configs/fresh_config.yml +++ b/src/test/resources/configs/fresh_config.yml @@ -11,6 +11,7 @@ world: teleport: use-finer-teleport-permissions: true + passenger-mode: default concurrent-teleport-limit: 50 teleport-intercept: true safe-location-horizontal-search-radius: 3