diff --git a/bungee/base/build.gradle.kts b/bungee/base/build.gradle.kts index 6e8e9033..dcd0123c 100644 --- a/bungee/base/build.gradle.kts +++ b/bungee/base/build.gradle.kts @@ -8,7 +8,7 @@ dependencies { } relocate("net.kyori") -relocate("cloud.commandframework") +relocate("org.incendo.cloud") relocate("io.leangen.geantyref") // used in cloud // these dependencies are already present on the platform diff --git a/bungee/base/src/main/java/org/geysermc/floodgate/bungee/module/BungeePlatformModule.java b/bungee/base/src/main/java/org/geysermc/floodgate/bungee/module/BungeePlatformModule.java index 4b60cbe9..af5160f9 100644 --- a/bungee/base/src/main/java/org/geysermc/floodgate/bungee/module/BungeePlatformModule.java +++ b/bungee/base/src/main/java/org/geysermc/floodgate/bungee/module/BungeePlatformModule.java @@ -25,29 +25,28 @@ package org.geysermc.floodgate.bungee.module; -import cloud.commandframework.CommandManager; -import cloud.commandframework.bungee.BungeeCommandManager; -import cloud.commandframework.execution.CommandExecutionCoordinator; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Factory; import jakarta.inject.Named; import jakarta.inject.Singleton; -import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.plugin.Plugin; import org.geysermc.floodgate.core.connection.audience.FloodgateCommandPreprocessor; +import org.geysermc.floodgate.core.connection.audience.FloodgateSenderMapper; import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.bungee.BungeeCommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; @Factory public final class BungeePlatformModule { @Bean @Singleton public CommandManager commandManager(CommandUtil commandUtil, Plugin plugin) { - CommandManager commandManager = new BungeeCommandManager<>( + var commandManager = new BungeeCommandManager<>( plugin, - CommandExecutionCoordinator.simpleCoordinator(), - commandUtil::getUserAudience, - audience -> (CommandSender) audience.source() + ExecutionCoordinator.simpleCoordinator(), + new FloodgateSenderMapper<>(commandUtil) ); commandManager.registerCommandPreProcessor(new FloodgateCommandPreprocessor<>(commandUtil)); return commandManager; diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/LinkAccountCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/LinkAccountCommand.java index 480f5d56..599b0b1f 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/LinkAccountCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/LinkAccountCommand.java @@ -25,11 +25,8 @@ package org.geysermc.floodgate.core.command; -import cloud.commandframework.ArgumentDescription; -import cloud.commandframework.Command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.arguments.standard.StringArgument; -import cloud.commandframework.context.CommandContext; +import static org.incendo.cloud.parser.standard.StringParser.stringParser; + import io.micronaut.context.annotation.Secondary; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -40,7 +37,6 @@ import org.geysermc.floodgate.core.api.SimpleFloodgateApi; import org.geysermc.floodgate.core.command.util.Permission; import org.geysermc.floodgate.core.config.FloodgateConfig; import org.geysermc.floodgate.core.connection.audience.ProfileAudience; -import org.geysermc.floodgate.core.connection.audience.ProfileAudienceArgument; import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.connection.audience.UserAudience.PlayerAudience; import org.geysermc.floodgate.core.link.CommonPlayerLink; @@ -50,6 +46,10 @@ import org.geysermc.floodgate.core.platform.command.FloodgateCommand; import org.geysermc.floodgate.core.platform.command.TranslatableMessage; import org.geysermc.floodgate.core.util.Constants; import org.geysermc.floodgate.core.util.Utils; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.Description; @Singleton @Secondary @@ -59,20 +59,19 @@ public final class LinkAccountCommand implements FloodgateCommand { @Inject FloodgateLogger logger; @Override - public Command buildCommand(CommandManager commandManager) { + public Command buildCommand(CommandManager commandManager) { return commandManager.commandBuilder("linkaccount", - ArgumentDescription.of("Link your Java account with your Bedrock account")) + Description.of("Link your Java account with your Bedrock account")) .senderType(PlayerAudience.class) .permission(Permission.COMMAND_LINK.get()) - .argument(ProfileAudienceArgument.of("player", true)) - .argument(StringArgument.optional("code")) + .argument(ProfileAudience.ofAnyUsernameBoth("player")) + .optional("code", stringParser()) .handler(this::execute) .build(); } - @Override - public void execute(CommandContext context) { - UserAudience sender = context.getSender(); + public void execute(CommandContext context) { + UserAudience sender = context.sender(); //todo make this less hacky if (link instanceof GlobalPlayerLinking) { diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/TestCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/TestCommand.java index b8a9c901..7442a61e 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/TestCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/TestCommand.java @@ -25,38 +25,39 @@ package org.geysermc.floodgate.core.command; -import cloud.commandframework.Command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.arguments.standard.StringArgument; -import cloud.commandframework.arguments.standard.UUIDArgument; -import cloud.commandframework.context.CommandContext; +import static org.incendo.cloud.parser.standard.StringParser.stringParser; +import static org.incendo.cloud.parser.standard.UUIDParser.uuidParser; + import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.util.concurrent.ExecutionException; import org.geysermc.floodgate.core.config.FloodgateConfig; import org.geysermc.floodgate.core.connection.audience.UserAudience; +import org.geysermc.floodgate.core.connection.audience.UserAudience.ConsoleAudience; import org.geysermc.floodgate.core.link.CommonPlayerLink; import org.geysermc.floodgate.core.platform.command.FloodgateCommand; import org.geysermc.floodgate.core.util.Constants; import org.geysermc.floodgate.core.util.Utils; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; @Singleton public class TestCommand implements FloodgateCommand { @Inject CommonPlayerLink link; @Override - public Command buildCommand(CommandManager commandManager) { + public Command buildCommand(CommandManager commandManager) { return commandManager.commandBuilder("floodgate-test") - .senderType(UserAudience.ConsoleAudience.class) + .senderType(ConsoleAudience.class) + .required("xuid", stringParser()) + .required("uuid", uuidParser()) + .required("name", stringParser()) .handler(this::execute) - .argument(StringArgument.of("xuid")) - .argument(UUIDArgument.of("uuid")) - .argument(StringArgument.of("name")) .build(); } - @Override - public void execute(CommandContext context) { + public void execute(CommandContext context) { try { link.addLink(context.get("uuid"), context.get("name"), Utils.getJavaUuid(context.get("xuid"))).get(); } catch (InterruptedException | ExecutionException e) { diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/UnlinkAccountCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/UnlinkAccountCommand.java index 32697b55..509e4116 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/UnlinkAccountCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/UnlinkAccountCommand.java @@ -25,10 +25,6 @@ package org.geysermc.floodgate.core.command; -import cloud.commandframework.ArgumentDescription; -import cloud.commandframework.Command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.context.CommandContext; import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.Getter; @@ -41,24 +37,27 @@ import org.geysermc.floodgate.core.link.GlobalPlayerLinking; import org.geysermc.floodgate.core.platform.command.FloodgateCommand; import org.geysermc.floodgate.core.platform.command.TranslatableMessage; import org.geysermc.floodgate.core.util.Constants; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.Description; @Singleton public final class UnlinkAccountCommand implements FloodgateCommand { @Inject CommonPlayerLink link; @Override - public Command buildCommand(CommandManager commandManager) { + public Command buildCommand(CommandManager commandManager) { return commandManager.commandBuilder("unlinkaccount", - ArgumentDescription.of("Unlink your Java account from your Bedrock account")) + Description.of("Unlink your Java account from your Bedrock account")) .senderType(PlayerAudience.class) .permission(Permission.COMMAND_UNLINK.get()) .handler(this::execute) .build(); } - @Override - public void execute(CommandContext context) { - UserAudience sender = context.getSender(); + public void execute(CommandContext context) { + UserAudience sender = context.sender(); //todo make this less hacky if (link instanceof GlobalPlayerLinking) { diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/WhitelistCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/WhitelistCommand.java index f62c1ce0..282029ae 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/WhitelistCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/WhitelistCommand.java @@ -25,10 +25,6 @@ package org.geysermc.floodgate.core.command; -import cloud.commandframework.ArgumentDescription; -import cloud.commandframework.Command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.context.CommandContext; import io.micronaut.http.client.exceptions.HttpClientResponseException; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -39,13 +35,15 @@ import org.geysermc.floodgate.core.command.util.Permission; import org.geysermc.floodgate.core.config.FloodgateConfig; import org.geysermc.floodgate.core.config.ProxyFloodgateConfig; import org.geysermc.floodgate.core.connection.audience.ProfileAudience; -import org.geysermc.floodgate.core.connection.audience.ProfileAudienceArgument; import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.http.xbox.XboxClient; import org.geysermc.floodgate.core.platform.command.CommandUtil; import org.geysermc.floodgate.core.platform.command.FloodgateCommand; import org.geysermc.floodgate.core.platform.command.TranslatableMessage; -import org.geysermc.floodgate.core.platform.util.PlayerType; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.Description; @Singleton public class WhitelistCommand implements FloodgateCommand { @@ -56,23 +54,23 @@ public class WhitelistCommand implements FloodgateCommand { @Override public Command buildCommand(CommandManager commandManager) { Command.Builder builder = commandManager.commandBuilder("fwhitelist", - ArgumentDescription.of("Easy way to whitelist Bedrock players")) + Description.of("Easy way to whitelist Bedrock players")) .permission(Permission.COMMAND_WHITELIST.get()); commandManager.command(builder .literal("add", "a") - .argument(ProfileAudienceArgument.of("player", true, true, PlayerType.ONLY_BEDROCK)) + .argument(ProfileAudience.ofAnyIdentifierBedrock("player")) .handler(context -> performCommand(context, true))); return builder .literal("remove", "r") - .argument(ProfileAudienceArgument.of("player", true, true, PlayerType.ONLY_BEDROCK)) + .argument(ProfileAudience.ofAnyIdentifierBedrock("player")) .handler(context -> performCommand(context, false)) .build(); } public void performCommand(CommandContext context, boolean add) { - UserAudience sender = context.getSender(); + UserAudience sender = context.sender(); ProfileAudience profile = context.get("player"); UUID uuid = profile.uuid(); String name = profile.username(); @@ -181,11 +179,6 @@ public class WhitelistCommand implements FloodgateCommand { }); } - @Override - public void execute(CommandContext context) { - // ignored, all the logic is in the other method - } - @Override public boolean shouldRegister(FloodgateConfig config) { // currently only Spigot (our only non-Proxy platform) has a whitelist build-in. diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/main/FirewallCheckSubcommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/main/FirewallCheckSubcommand.java index 941d3b2b..342f10d1 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/main/FirewallCheckSubcommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/main/FirewallCheckSubcommand.java @@ -27,7 +27,6 @@ package org.geysermc.floodgate.core.command.main; import static org.geysermc.floodgate.core.util.Constants.COLOR_CHAR; -import cloud.commandframework.context.CommandContext; import com.google.gson.JsonElement; import it.unimi.dsi.fastutil.Pair; import jakarta.inject.Inject; @@ -41,6 +40,7 @@ import org.geysermc.floodgate.core.util.Constants; import org.geysermc.floodgate.core.util.HttpClient; import org.geysermc.floodgate.core.util.HttpClient.HttpResponse; import org.geysermc.floodgate.core.util.Utils; +import org.incendo.cloud.context.CommandContext; final class FirewallCheckSubcommand extends FloodgateSubCommand { @Inject HttpClient httpProvider; @@ -67,7 +67,7 @@ final class FirewallCheckSubcommand extends FloodgateSubCommand { @Override public void execute(CommandContext context) { - UserAudience sender = context.getSender(); + UserAudience sender = context.sender(); executeChecks( globalApiCheck(sender) ).whenComplete((response, $) -> diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/main/MainCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/main/MainCommand.java index 810c2c06..7ccc1348 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/main/MainCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/main/MainCommand.java @@ -26,12 +26,8 @@ package org.geysermc.floodgate.core.command.main; import static org.geysermc.floodgate.core.util.Constants.COLOR_CHAR; +import static org.incendo.cloud.description.Description.description; -import cloud.commandframework.ArgumentDescription; -import cloud.commandframework.Command; -import cloud.commandframework.Command.Builder; -import cloud.commandframework.CommandManager; -import cloud.commandframework.context.CommandContext; import jakarta.inject.Singleton; import java.util.Locale; import org.geysermc.floodgate.core.command.util.Permission; @@ -39,6 +35,11 @@ import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.platform.command.FloodgateCommand; import org.geysermc.floodgate.core.platform.command.FloodgateSubCommand; import org.geysermc.floodgate.core.platform.command.SubCommands; +import org.incendo.cloud.Command; +import org.incendo.cloud.Command.Builder; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.Description; @Singleton public final class MainCommand extends SubCommands implements FloodgateCommand { @@ -46,14 +47,14 @@ public final class MainCommand extends SubCommands implements FloodgateCommand { public Command buildCommand(CommandManager commandManager) { Builder builder = commandManager.commandBuilder( "floodgate", - ArgumentDescription.of("A set of Floodgate related actions in one command")) + Description.of("A set of Floodgate related actions in one command")) .senderType(UserAudience.class) .permission(Permission.COMMAND_MAIN.get()) .handler(this::execute); for (FloodgateSubCommand subCommand : subCommands()) { commandManager.command(builder - .literal(subCommand.name().toLowerCase(Locale.ROOT), subCommand.description()) + .literal(subCommand.name().toLowerCase(Locale.ROOT), description(subCommand.description())) .permission(subCommand.permission().get()) .handler(subCommand::execute) ); @@ -63,12 +64,11 @@ public final class MainCommand extends SubCommands implements FloodgateCommand { return builder.build(); } - @Override public void execute(CommandContext context) { StringBuilder helpMessage = new StringBuilder("Available subcommands are:\n"); for (FloodgateSubCommand subCommand : subCommands()) { - if (context.getSender().hasPermission(subCommand.permission().get())) { + if (context.sender().hasPermission(subCommand.permission().get())) { helpMessage.append('\n').append(COLOR_CHAR).append('b') .append(subCommand.name().toLowerCase(Locale.ROOT)) .append(COLOR_CHAR).append("f - ").append(COLOR_CHAR).append('7') @@ -76,6 +76,6 @@ public final class MainCommand extends SubCommands implements FloodgateCommand { } } - context.getSender().sendMessage(helpMessage.toString()); + context.sender().sendMessage(helpMessage.toString()); } } diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/main/VersionSubcommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/main/VersionSubcommand.java index 3915aeee..39ffd289 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/main/VersionSubcommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/main/VersionSubcommand.java @@ -27,7 +27,6 @@ package org.geysermc.floodgate.core.command.main; import static org.geysermc.floodgate.core.util.Constants.COLOR_CHAR; -import cloud.commandframework.context.CommandContext; import com.google.gson.JsonElement; import jakarta.inject.Inject; import org.geysermc.floodgate.api.logger.FloodgateLogger; @@ -37,6 +36,7 @@ import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.platform.command.FloodgateSubCommand; import org.geysermc.floodgate.core.util.Constants; import org.geysermc.floodgate.core.util.HttpClient; +import org.incendo.cloud.context.CommandContext; public class VersionSubcommand extends FloodgateSubCommand { @Inject HttpClient httpClient; @@ -64,7 +64,7 @@ public class VersionSubcommand extends FloodgateSubCommand { @Override public void execute(CommandContext context) { - UserAudience sender = context.getSender(); + UserAudience sender = context.sender(); sender.sendMessage(String.format( COLOR_CHAR + "7You're currently on " + COLOR_CHAR + "b%s" + COLOR_CHAR + "7 (branch: " + COLOR_CHAR + "b%s" + COLOR_CHAR + "7)\n" + diff --git a/core/src/main/java/org/geysermc/floodgate/core/connection/audience/FloodgateCommandPreprocessor.java b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/FloodgateCommandPreprocessor.java index 71411705..5980581e 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/connection/audience/FloodgateCommandPreprocessor.java +++ b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/FloodgateCommandPreprocessor.java @@ -25,14 +25,14 @@ package org.geysermc.floodgate.core.connection.audience; -import cloud.commandframework.execution.preprocessor.CommandPreprocessingContext; -import cloud.commandframework.execution.preprocessor.CommandPreprocessor; import lombok.RequiredArgsConstructor; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.incendo.cloud.execution.preprocessor.CommandPreprocessingContext; +import org.incendo.cloud.execution.preprocessor.CommandPreprocessor; /** - * Command preprocessor which decorated incoming {@link cloud.commandframework.context.CommandContext} + * Command preprocessor which decorated incoming {@link org.incendo.cloud.context.CommandContext} * with Floodgate specific objects * * @param Command sender type @@ -44,6 +44,6 @@ public final class FloodgateCommandPreprocessor implements CommandPreprocesso @Override public void accept(@NonNull CommandPreprocessingContext context) { - context.getCommandContext().store("CommandUtil", commandUtil); + context.commandContext().store("CommandUtil", commandUtil); } } diff --git a/core/src/main/java/org/geysermc/floodgate/core/connection/audience/FloodgateSenderMapper.java b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/FloodgateSenderMapper.java new file mode 100644 index 00000000..33c9ff43 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/FloodgateSenderMapper.java @@ -0,0 +1,18 @@ +package org.geysermc.floodgate.core.connection.audience; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.incendo.cloud.SenderMapper; + +public record FloodgateSenderMapper(CommandUtil commandUtil) implements SenderMapper { + @Override + public @NonNull UserAudience map(@NonNull T base) { + return commandUtil.getUserAudience(base); + } + + @SuppressWarnings("unchecked") + @Override + public @NonNull T reverse(@NonNull UserAudience mapped) { + return (T) mapped.source(); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/connection/audience/InvalidPlayerIdentifierException.java b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/InvalidPlayerIdentifierException.java new file mode 100644 index 00000000..f071887c --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/InvalidPlayerIdentifierException.java @@ -0,0 +1,18 @@ +package org.geysermc.floodgate.core.connection.audience; + +import java.io.Serial; +import org.checkerframework.checker.nullness.qual.NonNull; + +public final class InvalidPlayerIdentifierException extends IllegalArgumentException { + @Serial + private static final long serialVersionUID = -6500019324607183855L; + + public InvalidPlayerIdentifierException(@NonNull String message) { + super(message); + } + + @Override + public @NonNull Throwable fillInStackTrace() { + return this; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudience.java b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudience.java index 7b99ba55..b06607f9 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudience.java +++ b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudience.java @@ -25,18 +25,80 @@ package org.geysermc.floodgate.core.connection.audience; +import static org.incendo.cloud.parser.standard.StringParser.quotedStringParser; + +import java.util.ArrayList; import java.util.UUID; -import lombok.Getter; -import lombok.experimental.Accessors; +import java.util.concurrent.CompletableFuture; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.geysermc.floodgate.core.platform.util.PlayerType; +import org.geysermc.floodgate.core.util.BrigadierUtils; +import org.incendo.cloud.component.CommandComponent; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.suggestion.Suggestion; -@Getter @Accessors(fluent = true) -public final class ProfileAudience { - private final @Nullable UUID uuid; - private final @Nullable String username; +public record ProfileAudience(@Nullable UUID uuid, @Nullable String username) { + public static CommandComponent.Builder ofAnyIdentifierBedrock(String name) { + return of(name, true, true, PlayerType.ONLY_BEDROCK); + } - public ProfileAudience(@Nullable UUID uuid, @Nullable String username) { - this.uuid = uuid; - this.username = username; + public static CommandComponent.Builder ofAnyUsernameBoth(String name) { + return of(name, false, true, PlayerType.ALL_PLAYERS); + } + + private static CommandComponent.Builder of(String name, boolean allowUuid, boolean allowOffline, PlayerType limitTo) { + return CommandComponent.builder() + .name(name) + .parser(quotedStringParser().flatMapSuccess(ProfileAudience.class, (context, input) -> { + CommandUtil commandUtil = context.get("CommandUtil"); + + ProfileAudience profileAudience; + if (input.length() > 16) { + // This must be a UUID. + if (!allowUuid) { + return ArgumentParseResult.failureFuture( + new InvalidPlayerIdentifierException("UUID is not allowed here")); + } + + if (input.length() != 32 && input.length() != 36) { + // Neither UUID without dashes nor with dashes. + return ArgumentParseResult.failureFuture( + new InvalidPlayerIdentifierException("Expected player name/UUID")); + } + + try { + // We only want to make sure the UUID is valid here. + Object player = commandUtil.getPlayerByUuid(UUID.fromString(input), limitTo); + profileAudience = commandUtil.getProfileAudience(player, allowOffline); + } catch (final IllegalArgumentException ignored) { + return ArgumentParseResult.failureFuture( + new InvalidPlayerIdentifierException("Invalid UUID '" + input + "'")); + } + } else { + // This is a username. + Object player = commandUtil.getPlayerByUsername(input, limitTo); + profileAudience = commandUtil.getProfileAudience(player, allowOffline); + } + + if (profileAudience == null) { + return ArgumentParseResult.failureFuture( + new InvalidPlayerIdentifierException("Invalid player '" + input + "'")); + } + + return ArgumentParseResult.successFuture(profileAudience); + })) + .suggestionProvider((context, input) -> { + CommandUtil commandUtil = context.get("CommandUtil"); + + var quoted = input.remainingInput().startsWith("\""); + + var suggestions = new ArrayList(); + for (final String player : commandUtil.getOnlineUsernames(limitTo)) { + suggestions.add(Suggestion.simple(BrigadierUtils.escapeIfRequired(player, quoted))); + } + + return CompletableFuture.completedFuture(suggestions); + }); } } diff --git a/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudienceArgument.java b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudienceArgument.java deleted file mode 100644 index 41d08192..00000000 --- a/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudienceArgument.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Floodgate - */ - -package org.geysermc.floodgate.core.connection.audience; - -import cloud.commandframework.arguments.CommandArgument; -import cloud.commandframework.arguments.parser.ArgumentParseResult; -import cloud.commandframework.arguments.parser.ArgumentParser; -import cloud.commandframework.context.CommandContext; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Queue; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.floodgate.core.platform.command.CommandUtil; -import org.geysermc.floodgate.core.platform.util.PlayerType; - -public class ProfileAudienceArgument extends CommandArgument { - private ProfileAudienceArgument(@NonNull String name, ProfileAudienceParser parser) { - super(true, name, parser, ProfileAudience.class); - } - - public static ProfileAudienceArgument of( - String name, - boolean allowUuid, - boolean allowOffline, - PlayerType limitTo) { - return new ProfileAudienceArgument(name, - new ProfileAudienceParser(allowUuid, allowOffline, limitTo)); - } - - public static ProfileAudienceArgument of( - String name, - boolean allowOffline, - PlayerType limitTo) { - return of(name, false, allowOffline, limitTo); - } - - public static ProfileAudienceArgument ofOnline(String name, PlayerType limitTo) { - return of(name, false, false, limitTo); - } - - public static ProfileAudienceArgument ofOnline(String name, boolean allowUuid) { - return of(name, allowUuid, false, PlayerType.ALL_PLAYERS); - } - - public static CommandArgument ofOnline(String name) { - return of(name, false, false, PlayerType.ALL_PLAYERS); - } - - public static ProfileAudienceArgument of(String name, boolean allowOffline) { - return of(name, false, allowOffline, PlayerType.ALL_PLAYERS); - } - - @RequiredArgsConstructor - public static final class ProfileAudienceParser - implements ArgumentParser { - - private final boolean allowUuid; - private final boolean allowOffline; - private final PlayerType limitTo; - - @Override - public @NonNull ArgumentParseResult parse( - @NonNull CommandContext<@NonNull UserAudience> commandContext, - @NonNull Queue<@NonNull String> inputQueue) { - CommandUtil commandUtil = commandContext.get("CommandUtil"); - - String input = inputQueue.poll(); - if (input == null || input.length() < 3) { - return ArgumentParseResult.failure( - new NullPointerException("Expected player name/UUID")); - } - - if (input.startsWith("\"")) { - if (input.endsWith("\"")) { - // Remove quotes from both sides of this string - input = input.substring(1); - input = input.substring(0, input.length() - 1); - } else { - // Multi-line - StringBuilder builder = new StringBuilder(input); - while (!inputQueue.isEmpty()) { - String string = inputQueue.remove(); - builder.append(' ').append(string); - if (string.endsWith("\"")) { - break; - } - } - - if (builder.lastIndexOf("\"") != builder.length() - 1) { - return ArgumentParseResult.failure( - new InvalidPlayerIdentifierException("Malformed string provided; " + - "no end quotes found!")); - } - - builder.deleteCharAt(0); - builder.deleteCharAt(builder.length() - 1); - input = builder.toString(); - } - } - - ProfileAudience profileAudience; - - if (input.length() > 16) { - // This must be a UUID. - if (!allowUuid) { - return ArgumentParseResult.failure( - new InvalidPlayerIdentifierException("UUID is not allowed here")); - } - - if (input.length() != 32 && input.length() != 36) { - // Neither UUID without dashes nor with dashes. - return ArgumentParseResult.failure( - new InvalidPlayerIdentifierException("Expected player name/UUID")); - } - - try { - // We only want to make sure the UUID is valid here. - Object player = commandUtil.getPlayerByUuid(UUID.fromString(input), limitTo); - profileAudience = commandUtil.getProfileAudience(player, allowOffline); - } catch (final IllegalArgumentException ignored) { - return ArgumentParseResult.failure( - new InvalidPlayerIdentifierException("Invalid UUID '" + input + "'")); - } - } else { - // This is a username. - Object player = commandUtil.getPlayerByUsername(input, limitTo); - profileAudience = commandUtil.getProfileAudience(player, allowOffline); - } - - if (profileAudience == null) { - return ArgumentParseResult.failure( - new InvalidPlayerIdentifierException("Invalid player '" + input + "'")); - } - - return ArgumentParseResult.success(profileAudience); - } - - @Override - public @NonNull List suggestions( - @NonNull CommandContext commandContext, - @NonNull String input) { - CommandUtil commandUtil = commandContext.get("CommandUtil"); - String trimmedInput = input.trim(); - - if (trimmedInput.isEmpty()) { - return Collections.unmodifiableList(commandUtil.getOnlineUsernames(limitTo)); - } - - String lowercaseInput = input.toLowerCase(Locale.ROOT); - List profileSuggestions = new ArrayList<>(); - - for (final String player : commandUtil.getOnlineUsernames(limitTo)) { - if (player.toLowerCase(Locale.ROOT).startsWith(lowercaseInput)) { - profileSuggestions.add(player); - } - } - - return Collections.unmodifiableList(profileSuggestions); - } - - @Override - public boolean isContextFree() { - return true; - } - } - - public static final class InvalidPlayerIdentifierException extends IllegalArgumentException { - private static final long serialVersionUID = -6500019324607183855L; - - public InvalidPlayerIdentifierException(@NonNull String message) { - super(message); - } - - @Override - public @NonNull Throwable fillInStackTrace() { - return this; - } - } -} diff --git a/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateCommand.java b/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateCommand.java index fce04e59..ba3163c8 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateCommand.java @@ -25,11 +25,10 @@ package org.geysermc.floodgate.core.platform.command; -import cloud.commandframework.Command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.context.CommandContext; import org.geysermc.floodgate.core.config.FloodgateConfig; import org.geysermc.floodgate.core.connection.audience.UserAudience; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; /** The base class for every Floodgate command. */ public interface FloodgateCommand { @@ -39,14 +38,7 @@ public interface FloodgateCommand { * @param commandManager the manager to create a command * @return the command to register */ - Command buildCommand(CommandManager commandManager); - - /** - * Called when the command created in {@link #buildCommand(CommandManager)} is executed. - * - * @param context the context of the executed command - */ - void execute(CommandContext context); + Command buildCommand(CommandManager commandManager); /** * Called by the CommandRegister to check if the command should be added given the config. diff --git a/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateSubCommand.java b/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateSubCommand.java index 78110507..7a920a98 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateSubCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateSubCommand.java @@ -25,9 +25,9 @@ package org.geysermc.floodgate.core.platform.command; -import cloud.commandframework.context.CommandContext; import org.geysermc.floodgate.core.command.util.Permission; import org.geysermc.floodgate.core.connection.audience.UserAudience; +import org.incendo.cloud.context.CommandContext; public abstract class FloodgateSubCommand { public abstract Class parent(); diff --git a/core/src/main/java/org/geysermc/floodgate/core/register/CommandRegister.java b/core/src/main/java/org/geysermc/floodgate/core/register/CommandRegister.java index e03c10f8..ff2df2d0 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/register/CommandRegister.java +++ b/core/src/main/java/org/geysermc/floodgate/core/register/CommandRegister.java @@ -25,13 +25,13 @@ package org.geysermc.floodgate.core.register; -import cloud.commandframework.CommandManager; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.util.Set; import org.geysermc.floodgate.core.config.FloodgateConfig; import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.platform.command.FloodgateCommand; +import org.incendo.cloud.CommandManager; /** * This class is responsible for registering commands to the command register of the platform that diff --git a/core/src/main/java/org/geysermc/floodgate/core/util/BrigadierUtils.java b/core/src/main/java/org/geysermc/floodgate/core/util/BrigadierUtils.java new file mode 100644 index 00000000..4b63e2f7 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/util/BrigadierUtils.java @@ -0,0 +1,51 @@ +package org.geysermc.floodgate.core.util; + +/* +Code taken from Brigadier's StringArgumentType and StringReader + */ +public final class BrigadierUtils { + public static boolean isAllowedInUnquotedString(char c) { + return c >= '0' && c <= '9' || + c >= 'A' && c <= 'Z' || + c >= 'a' && c <= 'z' || + c == '_' || c == '-' || + c == '.' || c == '+'; + } + + public static String escapeIfRequired(String input) { + for (final char c : input.toCharArray()) { + if (!isAllowedInUnquotedString(c)) { + return "\"" + input + "\""; + } + } + return input; + } + + public static String escapeIfRequired(String input, boolean quoted) { + if (quoted) { + return escape(input); + } + + for (final char c : input.toCharArray()) { + if (!isAllowedInUnquotedString(c)) { + return "\"" + input + "\""; + } + } + return input; + } + + private static String escape(final String input) { + final StringBuilder result = new StringBuilder("\""); + + for (int i = 0; i < input.length(); i++) { + final char c = input.charAt(i); + if (c == '\\' || c == '"') { + result.append('\\'); + } + result.append(c); + } + + result.append("\""); + return result.toString(); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a8d7a93e..e0234bb2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # parent micronaut-gradle = "4.3.2" -lombok = "8.0.1" +lombok = "8.4" # api cumulus = "2.0.0-SNAPSHOT" @@ -14,8 +14,7 @@ base-api = "feature-floodgate-merge-1.1.0-SNAPSHOT" configurate = "4.2.0-GeyserMC-SNAPSHOT" database-utils = "1.0-SNAPSHOT" fastutil = "8.5.3" -java-websocket = "1.5.2" -cloud = "1.5.0" +cloud = "2.0.0-beta.2" snakeyaml = "2.0" bstats = "3.0.2" @@ -33,7 +32,7 @@ velocity = "3.2.0-SNAPSHOT" indra = "3.1.3" shadow = "8.1.1" gradle-idea-ext = "1.1.7" -checkerframework = "3.19.0" +checkerframework = "3.42.0" [libraries] # indirectly included @@ -56,7 +55,7 @@ database-utils-mongo = { module = "org.geysermc.databaseutils:database-mongo", v database-utils-ap = { module = "org.geysermc.databaseutils:ap", version.ref = "database-utils" } fastutil-short-object-maps = { module = "com.nukkitx.fastutil:fastutil-short-object-maps", version.ref = "fastutil" } fastutil-int-object-maps = { module = "com.nukkitx.fastutil:fastutil-int-object-maps", version.ref = "fastutil" } -cloud-core = { module = "cloud.commandframework:cloud-core", version.ref = "cloud" } +cloud-core = { module = "org.incendo:cloud-core", version.ref = "cloud" } snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" } bstats = { module = "org.bstats:bstats-base", version.ref = "bstats" } @@ -78,15 +77,15 @@ junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher # bungee bungee = { module = "com.github.SpigotMC.BungeeCord:bungeecord-proxy", version.ref = "bungee" } -cloud-bungee = { module = "cloud.commandframework:cloud-bungee", version.ref = "cloud" } +cloud-bungee = { module = "org.incendo:cloud-bungee", version.ref = "cloud" } # spigot paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper" } -cloud-bukkit = { module = "cloud.commandframework:cloud-bukkit", version.ref = "cloud" } +cloud-paper = { module = "org.incendo:cloud-paper", version.ref = "cloud" } authlib = { module = "com.mojang:authlib", version.ref = "authlib" } # velocity -cloud-velocity = { module = "cloud.commandframework:cloud-velocity", version.ref = "cloud" } +cloud-velocity = { module = "org.incendo:cloud-velocity", version.ref = "cloud" } velocity-api = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" } # buildSrc diff --git a/spigot/base/build.gradle.kts b/spigot/base/build.gradle.kts index d28f8575..e197a707 100644 --- a/spigot/base/build.gradle.kts +++ b/spigot/base/build.gradle.kts @@ -4,13 +4,13 @@ dependencies { annotationProcessor(libs.micronaut.inject.java) compileOnlyApi(projects.isolation) - implementation(libs.cloud.bukkit) + implementation(libs.cloud.paper) compileOnlyApi(libs.paper.api) } relocate("net.kyori") -relocate("cloud.commandframework") +relocate("org.incendo.cloud") relocate("io.leangen.geantyref") // used in cloud // hack to make (old versions? of) Paper work relocate("it.unimi") diff --git a/spigot/base/src/main/java/org/geysermc/floodgate/spigot/command/SpigotCommandManager.java b/spigot/base/src/main/java/org/geysermc/floodgate/spigot/command/SpigotCommandManager.java index 5255993f..e7954276 100644 --- a/spigot/base/src/main/java/org/geysermc/floodgate/spigot/command/SpigotCommandManager.java +++ b/spigot/base/src/main/java/org/geysermc/floodgate/spigot/command/SpigotCommandManager.java @@ -25,23 +25,23 @@ package org.geysermc.floodgate.spigot.command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.bukkit.BukkitCommandManager; -import cloud.commandframework.execution.CommandExecutionCoordinator; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Factory; import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.SneakyThrows; import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.geysermc.floodgate.core.command.util.Permission; import org.geysermc.floodgate.core.connection.audience.FloodgateCommandPreprocessor; +import org.geysermc.floodgate.core.connection.audience.FloodgateSenderMapper; import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.paper.PaperCommandManager; @Factory public class SpigotCommandManager { @@ -49,11 +49,10 @@ public class SpigotCommandManager { @SneakyThrows @Singleton public CommandManager commandManager(CommandUtil commandUtil, JavaPlugin plugin) { - CommandManager commandManager = new BukkitCommandManager<>( + var commandManager = new PaperCommandManager<>( plugin, - CommandExecutionCoordinator.simpleCoordinator(), - commandUtil::getUserAudience, - audience -> (CommandSender) audience.source() + ExecutionCoordinator.simpleCoordinator(), + new FloodgateSenderMapper<>(commandUtil) ); commandManager.registerCommandPreProcessor(new FloodgateCommandPreprocessor<>(commandUtil)); return commandManager; diff --git a/velocity/base/build.gradle.kts b/velocity/base/build.gradle.kts index d01938d9..88ad2c49 100644 --- a/velocity/base/build.gradle.kts +++ b/velocity/base/build.gradle.kts @@ -9,7 +9,7 @@ dependencies { implementation(libs.cloud.velocity) } -relocate("cloud.commandframework") +relocate("org.incendo.cloud") relocate("io.leangen.geantyref") // used in cloud relocate("org.yaml.snakeyaml") diff --git a/velocity/base/src/main/java/org/geysermc/floodgate/velocity/module/VelocityPlatformModule.java b/velocity/base/src/main/java/org/geysermc/floodgate/velocity/module/VelocityPlatformModule.java index 20a2f6e5..1663ca1f 100644 --- a/velocity/base/src/main/java/org/geysermc/floodgate/velocity/module/VelocityPlatformModule.java +++ b/velocity/base/src/main/java/org/geysermc/floodgate/velocity/module/VelocityPlatformModule.java @@ -25,10 +25,6 @@ package org.geysermc.floodgate.velocity.module; -import cloud.commandframework.CommandManager; -import cloud.commandframework.execution.CommandExecutionCoordinator; -import cloud.commandframework.velocity.VelocityCommandManager; -import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.proxy.ProxyServer; import io.micronaut.context.annotation.Bean; @@ -36,8 +32,12 @@ import io.micronaut.context.annotation.Factory; import jakarta.inject.Named; import jakarta.inject.Singleton; import org.geysermc.floodgate.core.connection.audience.FloodgateCommandPreprocessor; +import org.geysermc.floodgate.core.connection.audience.FloodgateSenderMapper; import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.velocity.VelocityCommandManager; @Factory public class VelocityPlatformModule { @@ -48,12 +48,11 @@ public class VelocityPlatformModule { ProxyServer proxy, PluginContainer container ) { - CommandManager commandManager = new VelocityCommandManager<>( + var commandManager = new VelocityCommandManager<>( container, proxy, - CommandExecutionCoordinator.simpleCoordinator(), - commandUtil::getUserAudience, - audience -> (CommandSource) audience.source() + ExecutionCoordinator.simpleCoordinator(), + new FloodgateSenderMapper<>(commandUtil) ); commandManager.registerCommandPreProcessor(new FloodgateCommandPreprocessor<>(commandUtil)); return commandManager;