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 6a0493785..71acc4e50 100644 --- a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandCompletions.java +++ b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandCompletions.java @@ -27,6 +27,7 @@ import org.bukkit.Material; import org.bukkit.World; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.entity.SpawnCategory; import org.jetbrains.annotations.NotNull; @@ -43,9 +44,9 @@ 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; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; import org.mvplugins.multiverse.core.world.MultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; @@ -100,7 +101,7 @@ public class MVCommandCompletions extends PaperCommandCompletions { registerAsyncCompletion("mvworlds", this::suggestMVWorlds); registerAsyncCompletion("mvworldpropsname", this::suggestMVWorldPropsName); registerAsyncCompletion("mvworldpropsvalue", this::suggestMVWorldPropsValue); - registerAsyncCompletion("playersarray", this::suggestPlayersArray); + registerCompletion("playersarray", this::suggestPlayersArray); // getting online players cannot be async registerStaticCompletion("propsmodifyaction", suggestEnums(PropertyModifyAction.class)); registerStaticCompletion("spawncategories", suggestEnums(SpawnCategory.class)); registerAsyncCompletion("spawncategorypropsname", this::suggestSpawnCategoryPropsName); @@ -224,18 +225,22 @@ private Collection suggestDestinations(BukkitCommandCompletionContext co // Most likely console did not specify a player return Collections.emptyList(); } - if (context.hasConfig("othersOnly") && (players.length == 1 && players[0].equals(context.getIssuer().getIssuer()))) { + CommandSender sender = context.getIssuer().getIssuer(); + if (context.hasConfig("othersOnly") && (players.length == 1 && players[0].equals(sender))) { return Collections.emptyList(); } - return suggestDestinationsWithPerms(context.getIssuer().getIssuer(), players, context.getInput()); + return suggestDestinationsWithPerms(sender, Arrays.asList(players), context.getInput()); }) .getOrElse(Collections.emptyList()); } - private Collection suggestDestinationsWithPerms(CommandSender teleporter, Player[] players, String deststring) { + private Collection suggestDestinationsWithPerms(CommandSender teleporter, List teleportees, String deststring) { return destinationsProvider.suggestDestinations(teleporter, deststring).stream() + .filter(packet -> !config.getSimplifiedDestinationTabCompletion() + || packet.destination() instanceof WorldDestination + || deststring.startsWith(packet.destination().getIdentifier() + ":")) .filter(packet -> corePermissionsChecker - .checkDestinationPacketPermission(teleporter, Arrays.asList(players), packet)) + .checkDestinationPacketPermission(teleporter, teleportees, packet)) .map(DestinationSuggestionPacket::parsableString) .toList(); } 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 e08c6699c..c7831fe02 100644 --- a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java +++ b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java @@ -278,7 +278,12 @@ private IssuerAwareContextBuilder playerArrayContextBuilder() { }) .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.")); + .inputOnlyFailMessage((context, input) -> { + if (PlayerFinder.isSelector(input)) { + return Message.of("No player(s) matched selector: " + input + "."); + } + return Message.of("Player(s) " + input + " not found."); + }); } private PlayerLocation parsePlayerLocation(BukkitCommandExecutionContext context) { 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 index a9c1057b7..7df5fe70d 100644 --- 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 @@ -123,48 +123,8 @@ public IssuerAwareContextBuilder inputOnlyFailMessage(BiFunction 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)); - }; + validateRequiredVariables(); + return context -> resolveValue(context, GenericIssuerAwareValue::new).value; } /** @@ -178,49 +138,66 @@ public IssuerAwareContextResolver generateCont */ @ApiStatus.AvailableSince("5.1") public IssuerAwareContextResolver generateContext(BiFunction createValue) { - // todo: This is a copy and paste from above + validateRequiredVariables(); + return context -> resolveValue(context, createValue); + } + private void validateRequiredVariables() { 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", ""); + private I resolveValue(BukkitCommandExecutionContext context, BiFunction createValue) { + 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); - } + 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(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); + } + + int maxArgForAware = context.getFlagValue("maxArgForAware", Integer.MAX_VALUE); + long argLengthWithoutFlags = context.getArgs().stream() + .takeWhile(value -> !value.startsWith("--") && !value.isEmpty()) + .count(); + + if (resolve.equals("issuerAware") && argLengthWithoutFlags <= maxArgForAware) { + if (issuer.isPlayer()) { + Player player = issuer.getPlayer(); + result = fromPlayer.apply(context, player); + if (result != null) { + return createValue.apply(true, result); } - throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer)); + 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)); + } + + private static class GenericIssuerAwareValue extends IssuerAwareValue { + private final T value; - throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer)); - }; + public GenericIssuerAwareValue(boolean byIssuer, T value) { + super(byIssuer); + this.value = value; + } } } 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 8030c1792..544b60abf 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java @@ -19,7 +19,10 @@ import org.mvplugins.multiverse.core.command.MVCommandIssuer; import org.mvplugins.multiverse.core.command.context.issueraware.PlayerArrayValue; +import org.mvplugins.multiverse.core.command.flag.CommandFlag; +import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager; import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags; +import org.mvplugins.multiverse.core.command.flags.PageFilterFlags; import org.mvplugins.multiverse.core.command.flags.UnsafeFlags; import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.destination.DestinationInstance; @@ -35,14 +38,14 @@ final class TeleportCommand extends CoreCommand { private final CoreConfig config; private final CorePermissionsChecker permissionsChecker; private final AsyncSafetyTeleporter safetyTeleporter; - private final UnsafeFlags flags; + private final Flags flags; @Inject TeleportCommand( @NotNull CoreConfig config, @NotNull CorePermissionsChecker permissionsChecker, @NotNull AsyncSafetyTeleporter safetyTeleporter, - @NotNull UnsafeFlags flags + @NotNull Flags flags ) { this.config = config; this.permissionsChecker = permissionsChecker; @@ -54,14 +57,14 @@ final class TeleportCommand extends CoreCommand { @Subcommand("teleport|tp") @CommandPermission("@mvteleport") @CommandCompletion("@playersarray:checkPermissions=@mvteleportother|@destinations:byIssuerForArg=arg1 " - + "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME + " " - + "@flags:notByIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME) + + "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + Flags.NAME + " " + + "@flags:notByIssuerForArg=arg1,groupName=" + Flags.NAME) @Syntax("[player] [--unsafe]") @Description("{@@mv-core.teleport.description}") void onTeleportCommand( MVCommandIssuer issuer, - @Flags("resolve=issuerAware") + @co.aikar.commands.annotation.Flags("resolve=issuerAware,maxArgForAware=1") @Syntax("[player]") @Description("{@@mv-core.teleport.player.description}") PlayerArrayValue playersValue, @@ -103,16 +106,21 @@ private void teleportSinglePlayer(MVCommandIssuer issuer, Player player, .checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety()) .passengerMode(config.getPassengerMode()) .teleportSingle(player) - .onSuccess(() -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS, - Replace.PLAYER.with(getYouOrName(issuer, player)), - Replace.DESTINATION.with(destination.toString()))) + .onSuccess(() -> { + if (parsedFlags.hasFlag(flags.silent)) { + return; + } + issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS, + Replace.PLAYER.with(getYouOrName(issuer, player)), + Replace.DESTINATION.with(destination.getDisplayMessage())); + }) .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.DESTINATION.with(destination.getDisplayMessage()), Replace.REASON.with(Message.of(entry.getKey()))); } }); @@ -136,18 +144,38 @@ private void teleportMultiplePlayers(MVCommandIssuer issuer, Player[] players, .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"), - Replace.DESTINATION.with(destination.toString()))) + .onSuccessCount(successCount -> { + if (parsedFlags.hasFlag(flags.silent)) { + return; + } + issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS, + Replace.PLAYER.with(successCount + " players"), + Replace.DESTINATION.with(destination.getDisplayMessage())); + }) .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(entry.getValue() + " players"), - Replace.DESTINATION.with(destination.toString()), + Replace.DESTINATION.with(destination.getDisplayMessage()), Replace.REASON.with(Message.of(entry.getKey()))); } }); } + + @Service + private static final class Flags extends UnsafeFlags { + + private static final String NAME = "mvteleport"; + + @Inject + private Flags(@NotNull CommandFlagsManager flagsManager) { + super(NAME, flagsManager); + } + + private final CommandFlag silent = flag(CommandFlag.builder("--silent") + .addAlias("-s") + .build()); + } } 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 70bfb706e..c9f2fbb9a 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java @@ -8,16 +8,13 @@ import com.dumptruckman.minecraft.util.Logging; import io.vavr.control.Try; import jakarta.inject.Inject; -import jakarta.inject.Provider; 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; import org.mvplugins.multiverse.core.MultiverseCore; -import org.mvplugins.multiverse.core.command.MVCommandManager; import org.mvplugins.multiverse.core.command.queue.ConfirmMode; import org.mvplugins.multiverse.core.config.handle.CommentedConfigurationHandle; import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle; @@ -28,7 +25,6 @@ import org.mvplugins.multiverse.core.config.migration.action.MoveMigratorAction; 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; @@ -539,6 +535,16 @@ public boolean getResolveAliasName() { return configHandle.get(configNodes.resolveAliasName); } + @ApiStatus.AvailableSince("5.4") + public Try setSimplifiedDestinationTabCompletion(boolean simplifiedDestinationTabCompletion) { + return configHandle.set(configNodes.simplifiedDestinationTabCompletion, simplifiedDestinationTabCompletion); + } + + @ApiStatus.AvailableSince("5.4") + public boolean getSimplifiedDestinationTabCompletion() { + return configHandle.get(configNodes.simplifiedDestinationTabCompletion); + } + /** * {@inheritDoc} */ 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 221378785..c3e1a6b61 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java @@ -414,6 +414,15 @@ private N node(N node) { .name("resolve-alias-name") .build()); + final ConfigNode simplifiedDestinationTabCompletion = node(ConfigNode.builder("command.simplified-destination-tab-completion", Boolean.class) + .comment("") + .comment("If this is set to true, Multiverse will only suggest simple destination formats in tab completion.") + .comment("This means only world names will be suggested without other destination type such as `e:worldname:x,y,z` or `p:playername`.") + .comment("Note: This DOES NOT prevent players from using the destinations, as that is controlled by permissions.") + .defaultValue(false) + .name("simplified-destination-tab-completion") + .build()); + final ConfigNode confirmMode = node(ConfigNode.builder("command.confirm-mode", ConfirmMode.class) .comment("") .comment("This config option defines whether `/mv confirm` is needed before running a DANGEROUS action.") diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/DestinationInstance.java b/src/main/java/org/mvplugins/multiverse/core/destination/DestinationInstance.java index e2c04ab7d..63d3d2d26 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/DestinationInstance.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/DestinationInstance.java @@ -4,8 +4,9 @@ import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.util.Vector; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -import org.mvplugins.multiverse.core.utils.result.FailureReason; +import org.mvplugins.multiverse.core.locale.message.Message; import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation; /** @@ -79,6 +80,20 @@ protected DestinationInstance(@NotNull T destination) { */ public abstract @NotNull Option getFinerPermissionSuffix(); + /** + * Gets a user-friendly display text for this destination instance. This is used when displaying the destination + * to the player. By default, this returns the same as {@link #toString()}. Override this method to provide a more + * user-friendly display text with colors, formatting and localization support. + * + * @return The display message. + * @since 5.4 + */ + @ApiStatus.AvailableSince("5.4") + @NotNull + public Message getDisplayMessage() { + return Message.of(this.toString()); + } + /** * Serialises the destination instance to a savable string. * diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/AnchorDestinationInstance.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/AnchorDestinationInstance.java index 92b291426..806f2020c 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/AnchorDestinationInstance.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/AnchorDestinationInstance.java @@ -7,6 +7,7 @@ import org.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.core.destination.DestinationInstance; +import org.mvplugins.multiverse.core.locale.message.Message; /** * Destination instance implementation for the {@link AnchorDestination}. @@ -63,6 +64,15 @@ public boolean checkTeleportSafety() { return Option.of(anchorName); } + /** + * {@inheritDoc} + */ + @Override + public @NotNull Message getDisplayMessage() { + //TODO Localize + return Message.of("anchor '" + anchorName + "'"); + } + /** * {@inheritDoc} */ 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 7527152cc..06683edc1 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 @@ -6,7 +6,7 @@ import co.aikar.locales.MessageKey; import co.aikar.locales.MessageKeyProvider; -import org.bukkit.Bukkit; +import jakarta.inject.Inject; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -20,8 +20,7 @@ import org.mvplugins.multiverse.core.utils.PlayerFinder; import org.mvplugins.multiverse.core.utils.result.Attempt; import org.mvplugins.multiverse.core.utils.result.FailureReason; - -import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace; +import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker; /** * {@link Destination} implementation for beds. @@ -30,7 +29,11 @@ public final class BedDestination implements Destination { static final String OWN_BED_STRING = "playerbed"; - BedDestination() { + private final ConcurrentPlayerWorldTracker worldTracker; + + @Inject + BedDestination(@NotNull ConcurrentPlayerWorldTracker worldTracker) { + this.worldTracker = worldTracker; } /** @@ -62,8 +65,8 @@ public final class BedDestination implements Destination suggestDestinations( @NotNull CommandSender sender, @Nullable String destinationParams) { - List collect = Bukkit.getOnlinePlayers().stream() - .map(player -> new DestinationSuggestionPacket(this, player.getName(), player.getName())) + List collect = worldTracker.getOnlinePlayers().stream() + .map(player -> new DestinationSuggestionPacket(this, player, player)) .collect(Collectors.toList()); if (sender instanceof Player) { collect.add(new DestinationSuggestionPacket(this, OWN_BED_STRING, OWN_BED_STRING)); diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/BedDestinationInstance.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/BedDestinationInstance.java index 5516ac1ce..5144c9245 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/BedDestinationInstance.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/BedDestinationInstance.java @@ -9,12 +9,13 @@ import org.jetbrains.annotations.Nullable; import org.mvplugins.multiverse.core.destination.DestinationInstance; +import org.mvplugins.multiverse.core.locale.message.Message; /** * Destination instance implementation for the {@link BedDestination}. */ public final class BedDestinationInstance extends DestinationInstance { - private final Player player; + private final @Nullable Player player; /** * Constructor. @@ -64,6 +65,15 @@ public boolean checkTeleportSafety() { return Option.of(player != null ? player.getName() : BedDestination.OWN_BED_STRING); } + /** + * {@inheritDoc} + */ + @Override + public @NotNull Message getDisplayMessage() { + //TODO Localize + return Message.of(player == null ? "your bed/respawn point" : player.getName() + "'s bed/respawn point"); + } + /** * {@inheritDoc} */ 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 c37969d05..ddc040f20 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 @@ -23,14 +23,11 @@ import org.mvplugins.multiverse.core.locale.MVCorei18n; import org.mvplugins.multiverse.core.utils.REPatterns; import org.mvplugins.multiverse.core.utils.position.EntityPosition; -import org.mvplugins.multiverse.core.utils.position.PositionNumber; -import org.mvplugins.multiverse.core.utils.position.VectorPosition; import org.mvplugins.multiverse.core.utils.result.Attempt; import org.mvplugins.multiverse.core.utils.result.FailureReason; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider; -import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation; import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.*; @@ -68,6 +65,7 @@ public ExactDestination(CoreConfig config, WorldManager worldManager, WorldEntry public @NotNull ExactDestinationInstance fromLocation(@NotNull Location location) { return new ExactDestinationInstance( this, + worldManager, location.getWorld().getName(), EntityPosition.ofLocation(location) ); @@ -88,6 +86,7 @@ public ExactDestination(CoreConfig config, WorldManager worldManager, WorldEntry .map(location -> Attempt.success( new ExactDestinationInstance( this, + worldManager, location.getWorld().getName(), EntityPosition.ofLocation(location) ) @@ -112,7 +111,7 @@ public ExactDestination(CoreConfig config, WorldManager worldManager, WorldEntry return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e)); } - return Attempt.success(new ExactDestinationInstance(this, worldName, position)); + return Attempt.success(new ExactDestinationInstance(this, worldManager, worldName, position)); } //TODO: Extract to a world finder class diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestinationInstance.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestinationInstance.java index 3eebadfd8..a5c08c81c 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestinationInstance.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestinationInstance.java @@ -9,14 +9,16 @@ import org.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.core.destination.DestinationInstance; +import org.mvplugins.multiverse.core.locale.message.Message; import org.mvplugins.multiverse.core.utils.position.EntityPosition; -import org.mvplugins.multiverse.core.utils.position.VectorPosition; -import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation; +import org.mvplugins.multiverse.core.world.MultiverseWorld; +import org.mvplugins.multiverse.core.world.WorldManager; /** * Destination instance implementation for the {@link ExactDestination}. */ public final class ExactDestinationInstance extends DestinationInstance { + private final WorldManager worldManager; private final String worldName; private final EntityPosition position; @@ -28,9 +30,11 @@ public final class ExactDestinationInstance extends DestinationInstance { + + private final ConcurrentPlayerWorldTracker playerWorldTracker; + /** * Creates a new instance of the PlayerDestination. */ - PlayerDestination() { + @Inject + PlayerDestination(@NotNull ConcurrentPlayerWorldTracker playerWorldTracker) { + this.playerWorldTracker = playerWorldTracker; } /** @@ -60,8 +65,8 @@ public final class PlayerDestination implements Destination suggestDestinations( @NotNull CommandSender sender, @Nullable String destinationParams) { - return Bukkit.getOnlinePlayers().stream() - .map(p -> new DestinationSuggestionPacket(this, p.getName(), p.getName())) + return playerWorldTracker.getOnlinePlayers().stream() + .map(player -> new DestinationSuggestionPacket(this, player, player)) .toList(); } diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/PlayerDestinationInstance.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/PlayerDestinationInstance.java index 83bbbd5b5..67abd08af 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/PlayerDestinationInstance.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/PlayerDestinationInstance.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.core.destination.DestinationInstance; +import org.mvplugins.multiverse.core.locale.message.Message; import java.util.UUID; @@ -66,6 +67,14 @@ public boolean checkTeleportSafety() { return Option.of(playerName); } + /** + * {@inheritDoc} + */ + @Override + public @NotNull Message getDisplayMessage() { + return Message.of(playerName); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/WorldDestinationInstance.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/WorldDestinationInstance.java index ad5ba3ce3..1ab20524e 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/WorldDestinationInstance.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/WorldDestinationInstance.java @@ -8,12 +8,9 @@ import org.jetbrains.annotations.Nullable; import org.mvplugins.multiverse.core.destination.DestinationInstance; -import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; +import org.mvplugins.multiverse.core.locale.message.Message; import org.mvplugins.multiverse.core.world.MultiverseWorld; -import java.lang.ref.Reference; -import java.lang.ref.WeakReference; - /** * Destination instance implementation for the {@link WorldDestination}. */ @@ -87,6 +84,14 @@ public boolean checkTeleportSafety() { return Option.of(world.getName()); } + /** + * {@inheritDoc} + */ + @Override + public @NotNull Message getDisplayMessage() { + return Message.of(world.getAliasOrName()); + } + /** * {@inheritDoc} */ 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 948c990af..2c3c5ad0c 100644 --- a/src/main/java/org/mvplugins/multiverse/core/listeners/MVChatListener.java +++ b/src/main/java/org/mvplugins/multiverse/core/listeners/MVChatListener.java @@ -16,6 +16,7 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.SkipIfEventExist; import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; import org.mvplugins.multiverse.core.world.WorldManager; +import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker; /** * Multiverse's Listener for players. @@ -24,16 +25,17 @@ final class MVChatListener implements CoreListener { private final CoreConfig config; private final WorldManager worldManager; - private final MVPlayerListener playerListener; + private final ConcurrentPlayerWorldTracker playerWorldTracker; @Inject MVChatListener( CoreConfig config, WorldManager worldManager, - MVPlayerListener playerListener) { + ConcurrentPlayerWorldTracker playerWorldTracker + ) { this.config = config; this.worldManager = worldManager; - this.playerListener = playerListener; + this.playerWorldTracker = playerWorldTracker; } @EventClass("io.papermc.paper.event.player.AsyncChatEvent") @@ -94,12 +96,9 @@ void asyncPlayerChat(AsyncPlayerChatEvent event) { } private String getWorldName(Player player) { - String world = playerListener.getPlayerWorld().get(player.getName()); - if (world == null) { - world = player.getWorld().getName(); - playerListener.getPlayerWorld().put(player.getName(), world); - } - return this.worldManager.getLoadedWorld(world) + String worldName = playerWorldTracker.getPlayerWorld(player.getName()) + .getOrElse(() -> player.getWorld().getName()); + return this.worldManager.getLoadedWorld(worldName) .map(mvworld -> mvworld.isHidden() ? "" : mvworld.getAliasOrName()) .getOrElse(""); } diff --git a/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java b/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java index a0db9a621..9d459e9bc 100644 --- a/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java +++ b/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java @@ -74,8 +74,6 @@ final class MVPlayerListener implements CoreListener { private final CorePermissionsChecker corePermissionsChecker; private final AsyncSafetyTeleporter asyncSafetyTeleporter; - private final Map playerWorld = new ConcurrentHashMap<>(); - @Inject MVPlayerListener( MultiverseCore plugin, @@ -120,15 +118,6 @@ private PluginLocales getLocales() { return getCommandManager().getLocales(); } - /** - * Gets the map of player and the world name they are in. - * - * @return the playerWorld-map - */ - Map getPlayerWorld() { - return playerWorld; - } - /** * This method is called when a player respawns. * @@ -272,7 +261,6 @@ private void teleportToDestinationOnJoin(Player player, DestinationInstance { + registerPermissionWithWildcards(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE)); + }).onSuccess(ignore -> { + Logging.fine("Successfully registered base permissions"); + }).onFailure(e -> { + Logging.fine("Failed to register base permissions: %s", e.getMessage()); + }); } public Try addWorldPermissions(@NotNull MultiverseWorld world) { return Try.run(() -> { - pluginManager.addPermission(new Permission( + registerPermissionWithWildcards(new Permission( concatPermission(WORLD_ACCESS, world.getName()), PermissionDefault.OP)); - pluginManager.addPermission(new Permission( + registerPermissionWithWildcards(new Permission( concatPermission(WORLD_EXEMPT, world.getName()), PermissionDefault.OP)); - pluginManager.addPermission(new Permission( + registerPermissionWithWildcards(new Permission( concatPermission(GAMEMODE_BYPASS, world.getName()), PermissionDefault.FALSE)); - pluginManager.addPermission(new Permission( + registerPermissionWithWildcards(new Permission( concatPermission(PLAYERLIMIT_BYPASS, world.getName()), PermissionDefault.FALSE)); - pluginManager.addPermission(new Permission( + registerPermissionWithWildcards(new Permission( concatPermission(SPAWN, world.getName()), PermissionDefault.OP)); - pluginManager.addPermission(new Permission( + registerPermissionWithWildcards(new Permission( concatPermission(SPAWN, "self", world.getName()), PermissionDefault.OP)); - pluginManager.addPermission(new Permission( + registerPermissionWithWildcards(new Permission( concatPermission(SPAWN, "other", world.getName()), PermissionDefault.OP)); Logging.fine("Successfully registered permissions for world %s", world.getName()); - }); + }).onFailure(e -> Logging.fine("Failed to register permissions for world %s: %s", + world.getName(), e.getMessage())); } public Try removeWorldPermissions(@NotNull MultiverseWorld world) { @@ -90,20 +97,23 @@ public Try removeWorldPermissions(@NotNull MultiverseWorld world) { pluginManager.removePermission(concatPermission(WORLD_EXEMPT, world.getName())); pluginManager.removePermission(concatPermission(GAMEMODE_BYPASS, world.getName())); pluginManager.removePermission(concatPermission(PLAYERLIMIT_BYPASS, world.getName())); + pluginManager.removePermission(concatPermission(SPAWN, world.getName())); pluginManager.removePermission(concatPermission(SPAWN, "self", world.getName())); pluginManager.removePermission(concatPermission(SPAWN, "other", world.getName())); Logging.fine("Successfully removed permissions for world %s", world.getName()); - }); + }).onFailure(e -> Logging.fine("Failed to remove permissions for world %s: %s", + world.getName(), e.getMessage())); } public Try addDestinationPermissions(@NotNull Destination destination) { return Try.run(() -> { - pluginManager.addPermission(new Permission( + registerPermissionWithWildcards(new Permission( concatPermission(TELEPORT, "self", destination.getIdentifier()), PermissionDefault.OP)); - pluginManager.addPermission(new Permission( + registerPermissionWithWildcards(new Permission( concatPermission(TELEPORT, "other", destination.getIdentifier()), PermissionDefault.OP)); Logging.fine("Successfully registered permissions for destination %s", destination.getIdentifier()); - }); + }).onFailure(e -> Logging.fine("Failed to register permissions for destination %s: %s", + destination.getIdentifier(), e.getMessage())); } public Try removeDestinationPermissions(@NotNull Destination destination) { @@ -111,6 +121,7 @@ public Try removeDestinationPermissions(@NotNull Destination destination) pluginManager.removePermission(concatPermission(TELEPORT, "self", destination.getIdentifier())); pluginManager.removePermission(concatPermission(TELEPORT, "other", destination.getIdentifier())); Logging.fine("Successfully removed permissions for destination %s", destination.getIdentifier()); - }); + }).onFailure(e -> Logging.fine("Failed to remove permissions for destination %s: %s", + destination.getIdentifier(), e.getMessage())); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/permissions/PermissionUtils.java b/src/main/java/org/mvplugins/multiverse/core/permissions/PermissionUtils.java index 97a707cbb..55cd65a05 100644 --- a/src/main/java/org/mvplugins/multiverse/core/permissions/PermissionUtils.java +++ b/src/main/java/org/mvplugins/multiverse/core/permissions/PermissionUtils.java @@ -1,8 +1,14 @@ package org.mvplugins.multiverse.core.permissions; import com.dumptruckman.minecraft.util.Logging; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Arrays; public final class PermissionUtils { @@ -20,6 +26,37 @@ public static void setDebugPermissions(boolean debugPermissions) { PermissionUtils.debugPermissions = debugPermissions; } + /** + * Registers a permission along with all its wildcard parents. + *
+ * For example, registering "mv.bypass.joinlocation" will also register "mv.*" and "mv.bypass.*" as parents. + * + * @param permission The permission to register. + * + * @since 5.4 + */ + @ApiStatus.AvailableSince("5.4") + public static void registerPermissionWithWildcards(Permission permission) { + Bukkit.getServer().getPluginManager().addPermission(permission); + String[] split = permission.getName().split("\\."); + StringBuilder prefix = new StringBuilder(); + // Skip the last element since it's the actual permission + Arrays.stream(Arrays.copyOfRange(split, 0, split.length - 1)).forEach(s -> { + prefix.append(s).append("."); + Permission perm = getOrAddPermission(prefix + "*"); + permission.addParent(perm, true); + }); + } + + private static Permission getOrAddPermission(String permission) { + Permission perm = Bukkit.getServer().getPluginManager().getPermission(permission); + if (perm == null) { + perm = new Permission(permission, PermissionDefault.FALSE); + Bukkit.getServer().getPluginManager().addPermission(perm); + } + return perm; + } + /** * Joins permissions with a dot. * diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/PlayerFinder.java b/src/main/java/org/mvplugins/multiverse/core/utils/PlayerFinder.java index 17d57d6fb..699875fef 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/PlayerFinder.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/PlayerFinder.java @@ -3,7 +3,6 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; -import java.util.regex.Pattern; import java.util.stream.Collectors; import com.dumptruckman.minecraft.util.Logging; @@ -11,6 +10,7 @@ import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -112,7 +112,16 @@ public final class PlayerFinder { return playerResults; } - private static boolean isSelector(@NotNull String playerIdentifier) { + /** + * Check if the player identifier is a selector. + * + * @param playerIdentifier An identifier of name, UUID or selector. + * @return True if the identifier is a selector, else false. + * + * @since 5.4 + */ + @ApiStatus.AvailableSince("5.4") + public static boolean isSelector(@NotNull String playerIdentifier) { return VANILLA_SELECTORS.stream().anyMatch(playerIdentifier::startsWith); } 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 3e5783cab..553a0c6b6 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 @@ -107,18 +107,14 @@ static Attempt failure(F failureReason, Messa * * @return Whether this attempt is a success. */ - default boolean isSuccess() { - return this instanceof Success; - } + boolean isSuccess(); /** * Returns whether this attempt is a failure. * * @return Whether this attempt is a failure. */ - default boolean isFailure() { - return this instanceof Failure; - } + boolean isFailure(); /** * Converts this {@link Attempt} instance to an equivalent {@link Try} representation. Defaults to a @@ -143,19 +139,25 @@ default boolean isFailure() { @ApiStatus.AvailableSince("5.1") Try toTry(Function, Throwable> throwableFunction); + /** + * Runs the given runnable after this attempt regardless of success or failure. + * + * @param runnable The runnable. + * @return This attempt. + */ default Attempt thenRun(Runnable runnable) { runnable.run(); return this; } - default Attempt thenAccept(Consumer> consumer) { - if (this instanceof Success) { - consumer.accept(Either.left(get())); - } else { - consumer.accept(Either.right(getFailureReason())); - } - return this; - } + /** + * Accepts either the value or the failure reason depending on the result type. + * This will run regardless of success or failure. + * + * @param consumer The consumer with either the value or the failure reason. + * @return This attempt. + */ + Attempt thenAccept(Consumer> consumer); /** * Peeks at the value if this is a success attempt. @@ -163,12 +165,7 @@ default Attempt thenAccept(Consumer> consumer) { * @param consumer The consumer with the value. * @return This attempt. */ - default Attempt peek(Consumer consumer) { - if (this instanceof Success) { - consumer.accept(get()); - } - return this; - } + Attempt peek(Consumer consumer); /** * Maps the value to another value if this is a success attempt. @@ -177,13 +174,7 @@ default Attempt peek(Consumer consumer) { * @param The type of the new value. * @return The new attempt. */ - default Attempt map(Function mapper) { - if (this instanceof Success) { - return new Success<>(mapper.apply(get())); - } else { - return new Failure<>((Failure) this); - } - } + Attempt map(Function mapper); /** * Maps the value to another attempt if this is a success attempt. @@ -192,13 +183,7 @@ default Attempt map(Function mapper) { * @param The type of the new value. * @return The new attempt. */ - default Attempt map(Supplier mapper) { - if (this instanceof Success) { - return new Success<>(mapper.get()); - } else { - return new Failure<>((Failure) this); - } - } + Attempt map(Supplier mapper); /** * Maps the value to another attempt with the same fail reason if this is a success attempt. @@ -207,13 +192,7 @@ default Attempt map(Supplier mapper) { * @param The type of the new value. * @return The new attempt. */ - default Attempt mapAttempt(Function> mapper) { - if (this instanceof Success) { - return mapper.apply(get()); - } else { - return new Failure<>((Failure) this); - } - } + Attempt mapAttempt(Function> mapper); /** * Maps the value to another attempt with the same fail reason if this is a success attempt. @@ -222,13 +201,7 @@ default Attempt mapAttempt(Function> mapper) * @param The type of the new value. * @return The new attempt. */ - default Attempt mapAttempt(Supplier> mapper) { - if (this instanceof Success) { - return mapper.get(); - } else { - return new Failure<>((Failure) this); - } - } + Attempt mapAttempt(Supplier> mapper); /** * Maps to another attempt with a different fail reason. @@ -237,13 +210,7 @@ default Attempt mapAttempt(Supplier> mapper) { * @param The type of the new fail reason. * @return The new attempt. */ - default Attempt transform(UF failureReason) { - if (this instanceof Success) { - return new Success<>(get()); - } else { - return new Failure<>(failureReason, getFailureMessage(), (Failure) this); - } - } + Attempt transform(UF failureReason); /** * Maps attempt result to another value. @@ -256,13 +223,7 @@ default Attempt transform(UF failureReason) { * @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()); - } - } + U transform(Function successMapper, Function failureMapper); /** * Calls either the failure or success function depending on the result type. @@ -272,13 +233,7 @@ default U transform(Function successMapper, Function failureMapp * @param The type of the new value. * @return The result of the function. */ - default N fold(Function, N> failureMapper, Function successMapper) { - if (this instanceof Success) { - return successMapper.apply(get()); - } else { - return failureMapper.apply((Failure) this); - } - } + N fold(Function, N> failureMapper, Function successMapper); /** * Calls the given runnable if this is a success attempt. @@ -286,12 +241,7 @@ default N fold(Function, N> failureMapper, Function succ * @param runnable The runnable. * @return This attempt. */ - default Attempt onSuccess(Runnable runnable) { - if (this instanceof Success) { - runnable.run(); - } - return this; - } + Attempt onSuccess(Runnable runnable); /** * Calls the given consumer if this is a success attempt. @@ -299,12 +249,7 @@ default Attempt onSuccess(Runnable runnable) { * @param consumer The consumer with the value. * @return This attempt. */ - default Attempt onSuccess(Consumer consumer) { - if (this instanceof Success) { - consumer.accept(get()); - } - return this; - } + Attempt onSuccess(Consumer consumer); /** * Calls the given consumer if this is a failure attempt. @@ -312,12 +257,7 @@ default Attempt onSuccess(Consumer consumer) { * @param runnable The runnable. * @return This attempt. */ - default Attempt onFailure(Runnable runnable) { - if (this instanceof Failure) { - runnable.run(); - } - return this; - } + Attempt onFailure(Runnable runnable); /** * Calls the given consumer if this is a failure attempt. @@ -325,12 +265,7 @@ default Attempt onFailure(Runnable runnable) { * @param consumer The consumer with the failure instance. * @return This attempt. */ - default Attempt onFailure(Consumer> consumer) { - if (this instanceof Failure) { - consumer.accept((Failure) this); - } - return this; - } + Attempt onFailure(Consumer> consumer); /** * Calls the given runnable if this is a failure attempt. @@ -338,12 +273,7 @@ default Attempt onFailure(Consumer> consumer) { * @param consumer The consumer with the failure reason. * @return This attempt. */ - default Attempt onFailureReason(Consumer consumer) { - if (this instanceof Failure) { - consumer.accept(getFailureReason()); - } - return this; - } + Attempt onFailureReason(Consumer consumer); /** * Represents a successful attempt with a value. @@ -378,6 +308,26 @@ public T getOrThrow(Function, X> exceptionSu return value; } + @Override + public F getFailureReason() { + throw new UnsupportedOperationException("No failure reason as attempt is a success"); + } + + @Override + public Message getFailureMessage() { + throw new UnsupportedOperationException("No failure message as attempt is a success"); + } + + @Override + public boolean isSuccess() { + return true; + } + + @Override + public boolean isFailure() { + return false; + } + @Override public Try toTry() { return Try.success(value); @@ -389,13 +339,83 @@ public Try toTry(Function, Throwable> throwableFunction) { } @Override - public F getFailureReason() { - throw new UnsupportedOperationException("No failure reason as attempt is a success"); + public Attempt thenAccept(Consumer> consumer) { + consumer.accept(Either.left(value)); + return this; } @Override - public Message getFailureMessage() { - throw new UnsupportedOperationException("No failure message as attempt is a success"); + public Attempt peek(Consumer consumer) { + consumer.accept(value); + return this; + } + + @Override + public Attempt map(Function mapper) { + return new Success<>(mapper.apply(value)); + } + + @Override + public Attempt map(Supplier mapper) { + return new Success<>(mapper.get()); + } + + @Override + public Attempt mapAttempt(Function> mapper) { + return mapper.apply(value); + } + + @Override + public Attempt mapAttempt(Supplier> mapper) { + return mapper.get(); + } + + @Override + public Attempt transform(UF failureReason) { + return changeFailureType(); + } + + @Override + public U transform(Function successMapper, Function failureMapper) { + return successMapper.apply(value); + } + + @Override + public N fold(Function, N> failureMapper, Function successMapper) { + return successMapper.apply(value); + } + + @Override + public Attempt onSuccess(Runnable runnable) { + runnable.run(); + return this; + } + + @Override + public Attempt onSuccess(Consumer consumer) { + consumer.accept(value); + return this; + } + + @Override + public Attempt onFailure(Runnable runnable) { + return this; + } + + @Override + public Attempt onFailure(Consumer> consumer) { + return this; + } + + @Override + public Attempt onFailureReason(Consumer consumer) { + return this; + } + + private Attempt changeFailureType() { + @SuppressWarnings("unchecked") + Attempt mappedSuccess = (Attempt) this; + return mappedSuccess; } @Override @@ -451,6 +471,26 @@ public T getOrThrow(Function, X> exceptionSu throw exceptionSupplier.apply(this); } + @Override + public F getFailureReason() { + return failureReason; + } + + @Override + public Message getFailureMessage() { + return message; + } + + @Override + public boolean isSuccess() { + return false; + } + + @Override + public boolean isFailure() { + return true; + } + @Override public Try toTry() { return Try.failure(new MultiverseException(message)); @@ -462,13 +502,83 @@ public Try toTry(Function, Throwable> throwableFunction) { } @Override - public F getFailureReason() { - return failureReason; + public Attempt thenAccept(Consumer> consumer) { + consumer.accept(Either.right(failureReason)); + return this; } @Override - public Message getFailureMessage() { - return message; + public Attempt peek(Consumer consumer) { + return this; + } + + @Override + public Attempt map(Function mapper) { + return changeValueType(); + } + + @Override + public Attempt map(Supplier mapper) { + return changeValueType(); + } + + @Override + public Attempt mapAttempt(Function> mapper) { + return changeValueType(); + } + + @Override + public Attempt mapAttempt(Supplier> mapper) { + return changeValueType(); + } + + @Override + public Attempt transform(UF failureReason) { + return new Failure<>(failureReason, getFailureMessage(), this); + } + + @Override + public U transform(Function successMapper, Function failureMapper) { + return failureMapper.apply(failureReason); + } + + @Override + public N fold(Function, N> failureMapper, Function successMapper) { + return failureMapper.apply(this); + } + + @Override + public Attempt onSuccess(Runnable runnable) { + return this; + } + + @Override + public Attempt onSuccess(Consumer consumer) { + return this; + } + + @Override + public Attempt onFailure(Runnable runnable) { + runnable.run(); + return this; + } + + @Override + public Attempt onFailure(Consumer> consumer) { + consumer.accept(this); + return this; + } + + @Override + public Attempt onFailureReason(Consumer consumer) { + consumer.accept(failureReason); + return this; + } + + private Attempt changeValueType() { + @SuppressWarnings("unchecked") + Attempt mappedFailure = (Attempt) this; + return mappedFailure; } @Override diff --git a/src/main/java/org/mvplugins/multiverse/core/world/helpers/ConcurrentPlayerWorldTracker.java b/src/main/java/org/mvplugins/multiverse/core/world/helpers/ConcurrentPlayerWorldTracker.java new file mode 100644 index 000000000..8c9c66300 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/world/helpers/ConcurrentPlayerWorldTracker.java @@ -0,0 +1,90 @@ +package org.mvplugins.multiverse.core.world.helpers; + +import io.vavr.control.Option; +import jakarta.inject.Inject; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.MultiverseCore; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Tracks which players are in which worlds, using a thread-safe map. + * This allows async access to online players list and the world they are in. + * + * @since 5.4 + */ +@ApiStatus.AvailableSince("5.4") +@Service +public final class ConcurrentPlayerWorldTracker implements Listener { + + private final Map playerWorldMap; + + @Inject + ConcurrentPlayerWorldTracker(@NotNull MultiverseCore plugin) { + this.playerWorldMap = new ConcurrentHashMap<>(); + plugin.getServer().getPluginManager().registerEvents(this, plugin); + } + + /** + * Get an unmodifiable collection of all online player names on the server. + * + * @return Unmodifiable collection of online player names. + * + * @since 5.4 + */ + @ApiStatus.AvailableSince("5.4") + @NotNull + @UnmodifiableView + public Collection getOnlinePlayers() { + return Collections.unmodifiableCollection(playerWorldMap.keySet()); + } + + /** + * Get the world name a player is currently in. + * + * @param playerName Name of the player. + * @return World name the player is in, or null if the player is not online. + * + * @since 5.4 + */ + @ApiStatus.AvailableSince("5.4") + @NotNull + public Option getPlayerWorld(String playerName) { + return Option.of(playerWorldMap.get(playerName)); + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onPlayerJoin(PlayerJoinEvent event) { + setPlayerWorld(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onPlayerChangedWorld(@NotNull PlayerChangedWorldEvent event) { + setPlayerWorld(event.getPlayer()); + } + + private void setPlayerWorld(Player player) { + String playerName = player.getName(); + String worldName = player.getWorld().getName(); + playerWorldMap.put(playerName, worldName); + } + + @EventHandler + private void onPlayerQuit(@NotNull PlayerQuitEvent event) { + String playerName = event.getPlayer().getName(); + playerWorldMap.remove(playerName); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/world/options/CloneWorldOptions.java b/src/main/java/org/mvplugins/multiverse/core/world/options/CloneWorldOptions.java index a92909181..ca2238a7f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/options/CloneWorldOptions.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/options/CloneWorldOptions.java @@ -94,7 +94,6 @@ public boolean keepWorldConfig() { return keepWorldConfig; } - /** * Sets whether to keep the world border of the world during cloning. * diff --git a/src/test/resources/configs/fresh_config.yml b/src/test/resources/configs/fresh_config.yml index 0df83a617..b258b7de4 100644 --- a/src/test/resources/configs/fresh_config.yml +++ b/src/test/resources/configs/fresh_config.yml @@ -41,6 +41,7 @@ messaging: command: resolve-alias-name: true + simplified-destination-tab-completion: false confirm-mode: enable use-confirm-otp: true confirm-timeout: 30