diff --git a/pom.xml b/pom.xml
index 61c531f..fae898a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,8 +5,8 @@
master
- 17
- 17
+ 21
+ 21
true
UTF-8
@@ -15,7 +15,7 @@
io.papermc.paper
paper-api
- 1.20.4-R0.1-SNAPSHOT
+ 1.21.8-R0.1-SNAPSHOT
provided
diff --git a/src/main/java/pw/kaboom/commandspy/Main.java b/src/main/java/pw/kaboom/commandspy/Main.java
index a5d4f40..81949a2 100644
--- a/src/main/java/pw/kaboom/commandspy/Main.java
+++ b/src/main/java/pw/kaboom/commandspy/Main.java
@@ -1,10 +1,17 @@
package pw.kaboom.commandspy;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
+import io.papermc.paper.command.brigadier.Commands;
+import io.papermc.paper.command.brigadier.MessageComponentSerializer;
+import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -14,19 +21,30 @@
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
+import pw.kaboom.commandspy.command.PlayerOrUUIDArgumentType;
+import pw.kaboom.commandspy.command.StateArgumentType;
import java.io.File;
-import java.util.UUID;
+import java.util.List;
+
+import static io.papermc.paper.command.brigadier.Commands.*;
+import static pw.kaboom.commandspy.command.PlayerOrUUIDArgumentType.getPlayer;
+import static pw.kaboom.commandspy.command.StateArgumentType.getState;
+
+public final class Main extends JavaPlugin implements Listener {
+ public static final SimpleCommandExceptionType ERROR_NOT_PLAYER =
+ new SimpleCommandExceptionType(MessageComponentSerializer.message()
+ .serialize(Component.translatable("permissions.requires.player")));
-public final class Main extends JavaPlugin implements CommandExecutor, Listener {
private CommandSpyState config;
@Override
public void onEnable() {
this.config = new CommandSpyState(new File(this.getDataFolder(), "state.bin"));
- //noinspection DataFlowIssue
- this.getCommand("commandspy").setExecutor(this);
+
+ this.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS,
+ event -> this.registerCommands(event.registrar()));
this.getServer().getPluginManager().registerEvents(this, this);
// Save the state every 30 seconds
@@ -38,12 +56,50 @@ public void onDisable() {
this.config.trySave();
}
- private void updateCommandSpyState(final @NotNull Player target,
- final @NotNull CommandSender source, final boolean state) {
+ private void registerCommands(final Commands registrar) {
+ final LiteralCommandNode commandSpyCommand =
+ literal("commandspy")
+ .requires(
+ Commands.restricted(
+ ctx -> ctx.getSender().hasPermission("commandspy.command")
+ )
+ )
+ .then(
+ argument("state", new StateArgumentType())
+ .executes(ctx -> updateState(ctx, null, getState(ctx, "state")))
+ )
+ .then(
+ argument("target", new PlayerOrUUIDArgumentType())
+ .then(
+ argument("state", new StateArgumentType())
+ .executes(
+ ctx -> updateState(
+ ctx, getPlayer(ctx, "target"), getState(ctx, "state")
+ )
+ )
+ )
+ .executes(ctx -> updateState(ctx, getPlayer(ctx, "target"), null))
+ )
+ .executes(ctx -> updateState(ctx, null, null))
+ .build();
+
+ registrar.register(commandSpyCommand,
+ "Allows you to spy on players' commands", List.of("c", "cs", "cspy"));
+ }
+
+ private int updateState(final @NotNull CommandContext ctx,
+ Player target, Boolean state) throws CommandSyntaxException {
+ final CommandSender source = ctx.getSource().getSender();
+ if (target == null) {
+ if (!(source instanceof final Player player)) throw ERROR_NOT_PLAYER.create();
+ target = player;
+ }
+
+ if (state == null) state = !this.config.getCommandSpyState(target.getUniqueId());
+
this.config.setCommandSpyState(target.getUniqueId(), state);
final Component stateString = Component.text(state ? "enabled" : "disabled");
-
target.sendMessage(Component.empty()
.append(Component.text("Successfully "))
.append(stateString)
@@ -54,9 +110,10 @@ private void updateCommandSpyState(final @NotNull Player target,
.append(Component.text("Successfully "))
.append(stateString)
.append(Component.text(" CommandSpy for "))
- .append(target.name())
- );
+ .append(target.name()));
}
+
+ return Command.SINGLE_SUCCESS;
}
private NamedTextColor getTextColor(final Player player) {
@@ -67,67 +124,6 @@ private NamedTextColor getTextColor(final Player player) {
return NamedTextColor.AQUA;
}
- @Override
- public boolean onCommand(final @NotNull CommandSender sender, final @NotNull Command cmd,
- final @NotNull String label, final String[] args) {
-
- Player target = null;
- Boolean state = null;
-
- switch (args.length) {
- case 0 -> {
- }
- case 1, 2 -> {
- // Get the last argument as a state. Fail if we have 2 arguments.
- state = getState(args[args.length - 1]);
- if (state != null && args.length == 1) {
- break;
- } else if (state == null && args.length == 2) {
- sender.sendMessage(Component.text("Usage: ", NamedTextColor.RED)
- .append(Component.text(cmd.getUsage().replace("", label)))
- );
- return true;
- }
-
- // Get the first argument as a player. Fail if it can't be found.
- target = getPlayer(args[0]);
- if (target != null) {
- break;
- }
-
- sender.sendMessage(Component.empty()
- .append(Component.text("Player \""))
- .append(Component.text(args[0]))
- .append(Component.text("\" not found"))
- );
- return true;
- }
- default -> {
- sender.sendMessage(Component.text("Usage: ", NamedTextColor.RED)
- .append(Component.text(cmd.getUsage().replace("", label)))
- );
- return true;
- }
- }
-
- if (target == null) {
- if (!(sender instanceof final Player player)) {
- sender.sendMessage(Component.text("Command has to be run by a player"));
- return true;
- }
-
- target = player;
- }
-
- if (state == null) {
- state = !this.config.getCommandSpyState(target.getUniqueId());
- }
-
- this.updateCommandSpyState(target, sender, state);
-
- return true;
- }
-
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
void onPlayerCommandPreprocess(final PlayerCommandPreprocessEvent event) {
final Player player = event.getPlayer();
@@ -155,34 +151,4 @@ void onSignChange(final SignChangeEvent event) {
this.config.broadcastSpyMessage(message);
}
-
- private static Player getPlayer(final String arg) {
- final Player player = Bukkit.getPlayer(arg);
- if (player != null) {
- return player;
- }
-
- final UUID uuid;
- try {
- uuid = UUID.fromString(arg);
- } catch (final IllegalArgumentException ignored) {
- return null;
- }
-
- return Bukkit.getPlayer(uuid);
- }
-
- private static Boolean getState(final String arg) {
- switch (arg) {
- case "on", "enable" -> {
- return true;
- }
- case "off", "disable" -> {
- return false;
- }
- default -> {
- return null;
- }
- }
- }
}
diff --git a/src/main/java/pw/kaboom/commandspy/command/PlayerOrUUIDArgumentType.java b/src/main/java/pw/kaboom/commandspy/command/PlayerOrUUIDArgumentType.java
new file mode 100644
index 0000000..46716d7
--- /dev/null
+++ b/src/main/java/pw/kaboom/commandspy/command/PlayerOrUUIDArgumentType.java
@@ -0,0 +1,56 @@
+package pw.kaboom.commandspy.command;
+
+import com.mojang.brigadier.StringReader;
+import com.mojang.brigadier.arguments.ArgumentType;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
+import io.papermc.paper.command.brigadier.argument.ArgumentTypes;
+import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
+import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public final class PlayerOrUUIDArgumentType implements
+ CustomArgumentType<@NotNull Player, @NotNull EntitySelectorArgumentResolver> {
+
+ // We lie to clients and tell them we're an entity selector so that UUIDs don't show up as red.
+ @Override
+ public @NotNull ArgumentType getNativeType() {
+ return ArgumentTypes.entity();
+ }
+
+ @Override
+ public Player parse(final @NotNull StringReader reader) {
+ throw new IllegalStateException("method should never be called as we implement override");
+ }
+
+ @Override
+ public Player parse(final @NotNull StringReader reader, final S source)
+ throws CommandSyntaxException {
+ if (!(source instanceof final CommandSourceStack stack))
+ throw new IllegalStateException("source was not a CommandSourceStack");
+
+ final int cursor = reader.getCursor();
+ final String string = reader.readString();
+
+ try {
+ final UUID uuid = UUID.fromString(string);
+ final Player player = Bukkit.getPlayer(uuid);
+
+ if (player != null) return player;
+ } catch (final IllegalArgumentException ignored) {
+ }
+
+ reader.setCursor(cursor);
+ return ArgumentTypes.player().parse(reader).resolve(stack).getFirst();
+ }
+
+ public static Player getPlayer(final CommandContext context,
+ final String name) {
+ return context.getArgument(name, Player.class);
+ }
+}
diff --git a/src/main/java/pw/kaboom/commandspy/command/StateArgumentType.java b/src/main/java/pw/kaboom/commandspy/command/StateArgumentType.java
new file mode 100644
index 0000000..5785117
--- /dev/null
+++ b/src/main/java/pw/kaboom/commandspy/command/StateArgumentType.java
@@ -0,0 +1,67 @@
+package pw.kaboom.commandspy.command;
+
+import com.mojang.brigadier.arguments.ArgumentType;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
+import io.papermc.paper.command.brigadier.MessageComponentSerializer;
+import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
+import net.kyori.adventure.text.Component;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+
+public final class StateArgumentType implements
+ CustomArgumentType.Converted<@NotNull Boolean, @NotNull String> {
+
+ private static final Collection VALUES = Arrays.asList("on", "enable",
+ "off", "disable");
+
+ private static final DynamicCommandExceptionType ERROR_INVALID_VALUE =
+ new DynamicCommandExceptionType(o ->
+ MessageComponentSerializer.message()
+ .serialize(Component.translatable("argument.enum.invalid")
+ .arguments(Component.text(o.toString()))));
+
+ @Override
+ public @NotNull ArgumentType getNativeType() {
+ return StringArgumentType.word();
+ }
+
+ @Override
+ public Boolean convert(final String s) throws CommandSyntaxException {
+ switch (s) {
+ case "on", "enable" -> {
+ return true;
+ }
+ case "off", "disable" -> {
+ return false;
+ }
+
+ default -> throw ERROR_INVALID_VALUE.create(s);
+ }
+ }
+
+ @Override
+ public @NotNull CompletableFuture listSuggestions(
+ final @NotNull CommandContext context, final @NotNull SuggestionsBuilder builder) {
+ for (final String value: VALUES) {
+ if (!value.startsWith(builder.getRemainingLowerCase())) continue;
+
+ builder.suggest(value);
+ }
+
+ return builder.buildFuture();
+ }
+
+ public static boolean getState(final CommandContext context,
+ final String name) {
+ return context.getArgument(name, Boolean.class);
+ }
+}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 9ba5483..9678ad5 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -1,13 +1,6 @@
name: CommandSpy
main: pw.kaboom.commandspy.Main
description: Plugin that allows you to spy on players' commands.
-api-version: '1.20'
+api-version: '1.21'
version: master
folia-supported: true
-
-commands:
- commandspy:
- aliases: [ c, cs, cspy ]
- description: Allows you to spy on players' commands
- permission: commandspy.command
- usage: '/ [player] [on|enable|off|disable]'