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 a1e9acdf3..7dfd49f30 100644 --- a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java +++ b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java @@ -115,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())); } 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 c4f9fcc6d..dc1930b89 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/ModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/ModifyCommand.java @@ -80,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), 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 e14b4bf82..d483c2333 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java @@ -19,6 +19,7 @@ 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.dynamiclistener.EventPriorityMapper; import org.mvplugins.multiverse.core.event.MVDebugModeEvent; @@ -251,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) @@ -268,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) @@ -550,6 +553,12 @@ private Collection suggestDestinations(CommandSender sender, String inpu 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 { private static final DimensionFormatNodeSerializer INSTANCE = new DimensionFormatNodeSerializer(); 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/DestinationsProvider.java b/src/main/java/org/mvplugins/multiverse/core/destination/DestinationsProvider.java index b4368c8f9..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,6 +7,7 @@ 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; @@ -51,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]; @@ -74,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); } /** 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 f04984698..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 @@ -5,13 +5,13 @@ import co.aikar.locales.MessageKey; import co.aikar.locales.MessageKeyProvider; -import com.dumptruckman.minecraft.util.Logging; import io.vavr.control.Option; 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.Player; +import org.bukkit.entity.Entity; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jvnet.hk2.annotations.Service; @@ -69,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); } @@ -120,6 +130,16 @@ 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} */ @@ -132,30 +152,37 @@ private Option getLoadedMultiverseWorld(String worldName) .isSuccess()) .map(world -> new DestinationSuggestionPacket(this, world.getTabCompleteName() + ":", world.getName())); - if (sender instanceof Player player) { - var playerLocation = new DestinationSuggestionPacket( + + 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( - player.getWorld().getName(), - player.getLocation().getX(), - player.getLocation().getY(), - player.getLocation().getZ() + location.getWorld().getName(), + location.getX(), + location.getY(), + location.getZ() ), - player.getWorld().getName() + location.getWorld().getName() ); - var playerLocationPW = new DestinationSuggestionPacket( + var locationPacketPW = new DestinationSuggestionPacket( this, "%s:%.2f,%.2f,%.2f:%.2f:%.2f".formatted( - player.getWorld().getName(), - player.getLocation().getX(), - player.getLocation().getY(), - player.getLocation().getZ(), - player.getLocation().getPitch(), - player.getLocation().getYaw() + location.getWorld().getName(), + location.getX(), + location.getY(), + location.getZ(), + location.getPitch(), + location.getYaw() ), - player.getWorld().getName() + location.getWorld().getName() ); - stream = Stream.concat(stream, Stream.of(playerLocation, playerLocationPW)); + stream = Stream.concat(stream, Stream.of(herePacket, locationPacket, locationPacketPW)); } return stream.toList(); } 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/utils/result/Attempt.java b/src/main/java/org/mvplugins/multiverse/core/utils/result/Attempt.java index e92d15dbc..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,7 +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; @@ -118,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; @@ -353,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"); @@ -406,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/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())