From 020275f3785c46e0a3d29b1f9e318d08d73fc94e Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Sun, 23 Jun 2024 12:41:24 -0300
Subject: [PATCH 01/14] style: match file structure with Extras

---
 .gitignore       | 2 ++
 README.md        | 6 +++---
 suppressions.xml | 4 +++-
 3 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/.gitignore b/.gitignore
index f845591..d3ee570 100755
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@ target/
 .classpath
 .project
 *.iml
+.theia/
+run/
\ No newline at end of file
diff --git a/README.md b/README.md
index 57d3662..bb7b021 100644
--- a/README.md
+++ b/README.md
@@ -6,9 +6,9 @@ The plugin is created for the Kaboom server.
 
 ## Commands
 
-| Command | Alias | Permission | Description |
-| ------- | ----- | ---------- | ----------- |
-|/commandspy | /c, /cs, /cspy | commandspy.command | Allows you to spy on players' commands|
+| Command     | Alias          | Permission         | Description                            |
+|-------------|----------------|--------------------|----------------------------------------|
+| /commandspy | /c, /cs, /cspy | commandspy.command | Allows you to spy on players' commands |
 
 ## Compiling
 
diff --git a/suppressions.xml b/suppressions.xml
index 8990963..30ced2c 100644
--- a/suppressions.xml
+++ b/suppressions.xml
@@ -5,4 +5,6 @@
 
 <suppressions>
     <suppress checks="Javadoc" files="."/>
-</suppressions>
+    <suppress checks="MagicNumber" files="."/>
+    <suppress checks="MethodLength" files="."/>
+</suppressions>
\ No newline at end of file

From d5c402cfc7cc9242f647b99416be938956964383 Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Sun, 23 Jun 2024 12:51:26 -0300
Subject: [PATCH 02/14] chore: bump used actions, checkstyle & paper

---
 .github/workflows/main.yml    | 9 +++++----
 pom.xml                       | 4 ++--
 src/main/resources/plugin.yml | 5 ++---
 3 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d5644a3..3af4259 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -11,14 +11,14 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v3
-    - uses: actions/setup-java@v3
+    - uses: actions/checkout@v4
+    - uses: actions/setup-java@v4
       with:
         distribution: 'temurin'
         java-version: 17
 
     - name: Cache maven packages to speed up build
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
         path: ~/.m2
         key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
@@ -27,7 +27,8 @@ jobs:
     - name: Build with Maven
       run: mvn -B package --file pom.xml
 
-    - uses: actions/upload-artifact@v3
+    - uses: actions/upload-artifact@v4
       with:
         name: CommandSpy
         path: target/CommandSpy.jar
+        compression-level: 0
diff --git a/pom.xml b/pom.xml
index 8c686a8..15ddd18 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,7 +15,7 @@
         <dependency>
             <groupId>io.papermc.paper</groupId>
             <artifactId>paper-api</artifactId>
-            <version>1.18.2-R0.1-SNAPSHOT</version>
+            <version>1.19.4-R0.1-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
     </dependencies>
@@ -33,7 +33,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>3.1.2</version>
+                <version>3.4.0</version>
                 <executions>
                     <execution>
                         <id>checkstyle</id>
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index f95cb20..af086de 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -1,13 +1,12 @@
 name: CommandSpy
 main: pw.kaboom.commandspy.Main
 description: Plugin that allows you to spy on players' commands.
-api-version: 1.13
+api-version: '1.19'
 version: master
 folia-supported: true
 
 commands:
    commandspy:
-      aliases: [c,cs,cspy]
+      aliases: [ c, cs, cspy ]
       description: Allows you to spy on players' commands
-      usage: /commandspy
       permission: commandspy.command

From 130f06fe5ac02c71cb14730b2fc14f3cf1c314c4 Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Sun, 23 Jun 2024 12:54:02 -0300
Subject: [PATCH 03/14] fix: use MONITOR priority for the command event

This hides commands from players that are being controlled with
iControlU.
---
 src/main/java/pw/kaboom/commandspy/Main.java | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/src/main/java/pw/kaboom/commandspy/Main.java b/src/main/java/pw/kaboom/commandspy/Main.java
index 7aab4d7..8e7e48c 100644
--- a/src/main/java/pw/kaboom/commandspy/Main.java
+++ b/src/main/java/pw/kaboom/commandspy/Main.java
@@ -1,6 +1,5 @@
 package pw.kaboom.commandspy;
 
-import java.util.Set;
 import java.util.UUID;
 
 import org.bukkit.Bukkit;
@@ -11,10 +10,10 @@ import org.bukkit.command.ConsoleCommandSender;
 import org.bukkit.configuration.file.FileConfiguration;
 import org.bukkit.entity.Player;
 import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
 import org.bukkit.event.Listener;
 import org.bukkit.event.block.SignChangeEvent;
 import org.bukkit.event.player.PlayerCommandPreprocessEvent;
-import org.bukkit.plugin.Plugin;
 import org.bukkit.plugin.java.JavaPlugin;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.format.NamedTextColor;
@@ -71,12 +70,8 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
         return true;
     }
 
-    @EventHandler
+    @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
     void onPlayerCommandPreprocess(final PlayerCommandPreprocessEvent event) {
-        if (event.isCancelled()) {
-            return;
-        }
-
         final Player player = event.getPlayer();
         final NamedTextColor color = getTextColor(player);
         final Component message = Component.text(player.getName(), color)

From 1ca2ffc91aea7c593f32d771ba754cfb0fbbfc43 Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Sun, 23 Jun 2024 13:07:12 -0300
Subject: [PATCH 04/14] perf: opt-out of paper plugin remapping

Since we don't use NMS, this is harmless. Saves about 100ms of startup
time on first boot on my machine
---
 pom.xml | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/pom.xml b/pom.xml
index 15ddd18..6dfe236 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,18 @@
     <build>
         <finalName>${project.artifactId}</finalName>
         <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.4.2</version>
+                <configuration>
+                    <archive>
+                        <manifestEntries>
+                            <paperweight-mappings-namespace>mojang</paperweight-mappings-namespace>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>

From 3de6d8d6c96d69ee03c54c25e7f5d0dca8b56340 Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Sun, 23 Jun 2024 13:18:37 -0300
Subject: [PATCH 05/14] style: misc style fixes

Finalizes everything, adds @NotNull to onCommand parameters, and uses
pattern matching for the onCommand instanceof Player check.
---
 src/main/java/pw/kaboom/commandspy/Main.java | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/main/java/pw/kaboom/commandspy/Main.java b/src/main/java/pw/kaboom/commandspy/Main.java
index 8e7e48c..10692b4 100644
--- a/src/main/java/pw/kaboom/commandspy/Main.java
+++ b/src/main/java/pw/kaboom/commandspy/Main.java
@@ -6,7 +6,6 @@ import org.bukkit.Bukkit;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandSender;
-import org.bukkit.command.ConsoleCommandSender;
 import org.bukkit.configuration.file.FileConfiguration;
 import org.bukkit.entity.Player;
 import org.bukkit.event.EventHandler;
@@ -17,6 +16,7 @@ import org.bukkit.event.player.PlayerCommandPreprocessEvent;
 import org.bukkit.plugin.java.JavaPlugin;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.format.NamedTextColor;
+import org.jetbrains.annotations.NotNull;
 
 public final class Main extends JavaPlugin implements CommandExecutor, Listener {
     private FileConfiguration config;
@@ -44,19 +44,18 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
         if (config.contains(player.getUniqueId().toString())) {
             return NamedTextColor.YELLOW;
         }
+
         return NamedTextColor.AQUA;
     }
 
     @Override
-    public boolean onCommand(final CommandSender sender, final Command cmd, final String label,
-                             final String[] args) {
-        if (sender instanceof ConsoleCommandSender) {
+    public boolean onCommand(final @NotNull CommandSender sender, final @NotNull Command cmd,
+                             final @NotNull String label, final String[] args) {
+        if (!(sender instanceof final Player player)) {
             sender.sendMessage(Component.text("Command has to be run by a player"));
             return true;
         }
 
-        final Player player = (Player) sender;
-
         if (args.length >= 1 && "on".equalsIgnoreCase(args[0])) {
             enableCommandSpy(player);
             return true;
@@ -78,31 +77,32 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
             .append(Component.text(": "))
             .append(Component.text(event.getMessage()));
 
-        for (String uuidString : config.getKeys(false)) {
+        for (final String uuidString : config.getKeys(false)) {
             final UUID uuid = UUID.fromString(uuidString);
             final Player recipient = Bukkit.getPlayer(uuid);
 
             if (recipient == null) {
                 continue;
             }
+
             recipient.sendMessage(message);
         }
     }
 
-    @EventHandler
+    @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
     void onSignChange(final SignChangeEvent event) {
         final Player player = event.getPlayer();
         final NamedTextColor color = getTextColor(player);
         Component message = Component.text(player.getName(), color)
             .append(Component.text(" created a sign with contents:"));
 
-        for (Component line : event.lines()) {
+        for (final Component line : event.lines()) {
             message = message
                 .append(Component.text("\n "))
                 .append(line);
         }
 
-        for (String uuidString : config.getKeys(false)) {
+        for (final String uuidString : config.getKeys(false)) {
             final UUID uuid = UUID.fromString(uuidString);
             final Player recipient = Bukkit.getPlayer(uuid);
 

From 7635411849bcb686516e413a9c995ad49636b32a Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Sun, 23 Jun 2024 16:22:21 -0300
Subject: [PATCH 06/14] perf: rewrite config logic; use binary format

This was done so spamming CommandSpy commands wouldn't lag the server as
much, and having a bunch of players turn on CommandSpy wouldn't fill up
the server storage.
---
 .../pw/kaboom/commandspy/CommandSpyState.java | 126 ++++++++++++++++++
 src/main/java/pw/kaboom/commandspy/Main.java  |  48 +++----
 2 files changed, 145 insertions(+), 29 deletions(-)
 create mode 100644 src/main/java/pw/kaboom/commandspy/CommandSpyState.java

diff --git a/src/main/java/pw/kaboom/commandspy/CommandSpyState.java b/src/main/java/pw/kaboom/commandspy/CommandSpyState.java
new file mode 100644
index 0000000..97e8542
--- /dev/null
+++ b/src/main/java/pw/kaboom/commandspy/CommandSpyState.java
@@ -0,0 +1,126 @@
+package pw.kaboom.commandspy;
+
+import com.google.common.io.Files;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.StampedLock;
+import java.util.stream.Collectors;
+
+public final class CommandSpyState {
+    private static final Logger LOGGER = JavaPlugin.getPlugin(Main.class).getSLF4JLogger();
+
+    private final ObjectOpenHashSet<UUID> users = new ObjectOpenHashSet<>();
+    private final StampedLock usersLock = new StampedLock();
+    private final AtomicBoolean dirty = new AtomicBoolean();
+    private final File file;
+
+    public CommandSpyState(final @NotNull File file) {
+        this.file = file;
+
+        try {
+            this.load();
+        } catch (final FileNotFoundException exception) {
+            try {
+                this.save(); // Create file if it doesn't exist
+            } catch (IOException ignored) {}
+        } catch (final IOException exception) {
+            LOGGER.error("Failed to load state file:", exception);
+        }
+    }
+
+    private void load() throws IOException {
+        final InputStream reader = new BufferedInputStream(new FileInputStream(this.file));
+
+        int read;
+        final ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
+
+        // Loop until we read less than 16 bytes
+        while ((read = reader.readNBytes(buffer.array(), 0, 16)) == 16) {
+             this.users.add(new UUID(buffer.getLong(0), buffer.getLong(8)));
+        }
+
+        reader.close();
+        if (read != 0) {
+            throw new IOException("Found " + read + " bytes extra whilst reading file");
+        }
+    }
+
+    private void save() throws IOException {
+        Files.createParentDirs(this.file);
+        final OutputStream writer = new BufferedOutputStream(new FileOutputStream(this.file));
+        final ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
+
+        final long stamp = this.usersLock.readLock();
+        for (final UUID uuid: this.users) {
+            buffer.putLong(0, uuid.getMostSignificantBits());
+            buffer.putLong(8, uuid.getLeastSignificantBits());
+            writer.write(buffer.array());
+        }
+        this.usersLock.unlockRead(stamp);
+
+        writer.flush();
+        writer.close();
+    }
+
+    public void trySave() {
+        // If the state is not dirty, then we don't need to do anything.
+        if (!this.dirty.compareAndExchange(true, false)) {
+            return;
+        }
+
+        try {
+            this.save();
+        } catch (final IOException exception) {
+            LOGGER.error("Failed to save state file:", exception);
+        }
+    }
+
+    public boolean getCommandSpyState(final @NotNull UUID playerUUID) {
+        final long stamp = this.usersLock.readLock();
+        final boolean result = this.users.contains(playerUUID);
+        this.usersLock.unlockRead(stamp);
+
+        return result;
+    }
+
+    public void setCommandSpyState(final @NotNull UUID playerUUID, final boolean state) {
+        final long stamp = this.usersLock.writeLock();
+
+        final boolean dirty;
+        if (state) {
+            dirty = this.users.add(playerUUID);
+        } else {
+            dirty = this.users.remove(playerUUID);
+        }
+
+        this.usersLock.unlockWrite(stamp);
+        if (dirty) {
+            this.dirty.set(true);
+        }
+    }
+
+    public void broadcastSpyMessage(final @NotNull Component message) {
+        // Raw access here, so we can get more performance by not locking/unlocking over and over
+        final long stamp = this.usersLock.readLock();
+        final Collection<Player> players = Bukkit.getOnlinePlayers()
+                .stream()
+                .filter(p -> this.users.contains(p.getUniqueId()))
+                .collect(Collectors.toUnmodifiableSet());
+        this.usersLock.unlockRead(stamp);
+
+        for (final Player recipient : players) {
+            recipient.sendMessage(message);
+        }
+    }
+}
diff --git a/src/main/java/pw/kaboom/commandspy/Main.java b/src/main/java/pw/kaboom/commandspy/Main.java
index 10692b4..9679dc5 100644
--- a/src/main/java/pw/kaboom/commandspy/Main.java
+++ b/src/main/java/pw/kaboom/commandspy/Main.java
@@ -1,12 +1,11 @@
 package pw.kaboom.commandspy;
 
-import java.util.UUID;
+import java.io.File;
 
 import org.bukkit.Bukkit;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandSender;
-import org.bukkit.configuration.file.FileConfiguration;
 import org.bukkit.entity.Player;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.EventPriority;
@@ -19,29 +18,36 @@ import net.kyori.adventure.text.format.NamedTextColor;
 import org.jetbrains.annotations.NotNull;
 
 public final class Main extends JavaPlugin implements CommandExecutor, Listener {
-    private FileConfiguration config;
+    private CommandSpyState config;
 
     @Override
     public void onEnable() {
-        config = getConfig();
+        this.config = new CommandSpyState(new File(this.getDataFolder(), "state.bin"));
+
         this.getCommand("commandspy").setExecutor(this);
         this.getServer().getPluginManager().registerEvents(this, this);
+
+        // Save the config every 30 seconds
+        Bukkit.getScheduler().runTaskTimerAsynchronously(this, this.config::trySave, 600L, 600L);
+    }
+
+    @Override
+    public void onDisable() {
+        this.config.trySave();
     }
 
     private void enableCommandSpy(final Player player) {
-        config.set(player.getUniqueId().toString(), true);
-        saveConfig();
+        this.config.setCommandSpyState(player.getUniqueId(), true);
         player.sendMessage(Component.text("Successfully enabled CommandSpy"));
     }
 
     private void disableCommandSpy(final Player player) {
-        config.set(player.getUniqueId().toString(), null);
-        saveConfig();
+        this.config.setCommandSpyState(player.getUniqueId(), false);
         player.sendMessage(Component.text("Successfully disabled CommandSpy"));
     }
 
     private NamedTextColor getTextColor(final Player player) {
-        if (config.contains(player.getUniqueId().toString())) {
+        if (this.config.getCommandSpyState(player.getUniqueId())) {
             return NamedTextColor.YELLOW;
         }
 
@@ -61,7 +67,7 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
             return true;
         }
         if ((args.length >= 1 && "off".equalsIgnoreCase(args[0]))
-                || config.contains(player.getUniqueId().toString())) {
+                || this.config.getCommandSpyState(player.getUniqueId())) {
             disableCommandSpy(player);
             return true;
         }
@@ -73,20 +79,12 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
     void onPlayerCommandPreprocess(final PlayerCommandPreprocessEvent event) {
         final Player player = event.getPlayer();
         final NamedTextColor color = getTextColor(player);
+
         final Component message = Component.text(player.getName(), color)
             .append(Component.text(": "))
             .append(Component.text(event.getMessage()));
 
-        for (final String uuidString : config.getKeys(false)) {
-            final UUID uuid = UUID.fromString(uuidString);
-            final Player recipient = Bukkit.getPlayer(uuid);
-
-            if (recipient == null) {
-                continue;
-            }
-
-            recipient.sendMessage(message);
-        }
+        this.config.broadcastSpyMessage(message);
     }
 
     @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
@@ -102,14 +100,6 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
                 .append(line);
         }
 
-        for (final String uuidString : config.getKeys(false)) {
-            final UUID uuid = UUID.fromString(uuidString);
-            final Player recipient = Bukkit.getPlayer(uuid);
-
-            if (recipient == null) {
-                continue;
-            }
-            recipient.sendMessage(message);
-        }
+        this.config.broadcastSpyMessage(message);
     }
 }

From c6351f3f5447f61a71f2e14fce16e60770e688a1 Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Sun, 23 Jun 2024 18:42:10 -0300
Subject: [PATCH 07/14] feat: allow players to disable/enable cspy for others

---
 src/main/java/pw/kaboom/commandspy/Main.java | 124 +++++++++++++++----
 src/main/resources/plugin.yml                |   1 +
 2 files changed, 101 insertions(+), 24 deletions(-)

diff --git a/src/main/java/pw/kaboom/commandspy/Main.java b/src/main/java/pw/kaboom/commandspy/Main.java
index 9679dc5..62da442 100644
--- a/src/main/java/pw/kaboom/commandspy/Main.java
+++ b/src/main/java/pw/kaboom/commandspy/Main.java
@@ -1,6 +1,7 @@
 package pw.kaboom.commandspy;
 
 import java.io.File;
+import java.util.UUID;
 
 import org.bukkit.Bukkit;
 import org.bukkit.command.Command;
@@ -27,7 +28,7 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
         this.getCommand("commandspy").setExecutor(this);
         this.getServer().getPluginManager().registerEvents(this, this);
 
-        // Save the config every 30 seconds
+        // Save the state every 30 seconds
         Bukkit.getScheduler().runTaskTimerAsynchronously(this, this.config::trySave, 600L, 600L);
     }
 
@@ -36,14 +37,26 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
         this.config.trySave();
     }
 
-    private void enableCommandSpy(final Player player) {
-        this.config.setCommandSpyState(player.getUniqueId(), true);
-        player.sendMessage(Component.text("Successfully enabled CommandSpy"));
-    }
+    private void updateCommandSpyState(final @NotNull Player target,
+                                       final @NotNull CommandSender source, final boolean state) {
+        this.config.setCommandSpyState(target.getUniqueId(), state);
 
-    private void disableCommandSpy(final Player player) {
-        this.config.setCommandSpyState(player.getUniqueId(), false);
-        player.sendMessage(Component.text("Successfully disabled CommandSpy"));
+        final Component stateString = Component.text(state ? "enabled" : "disabled");
+
+        target.sendMessage(Component.empty()
+                .append(Component.text("Successfully "))
+                .append(stateString)
+                .append(Component.text(" CommandSpy.")));
+
+        if (source != target) {
+            source.sendMessage(Component.empty()
+                    .append(Component.text("Successfully "))
+                    .append(stateString)
+                    .append(Component.text(" CommandSpy for "))
+                    .append(target.name())
+                    .append(Component.text("."))
+            );
+        }
     }
 
     private NamedTextColor getTextColor(final Player player) {
@@ -57,21 +70,54 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
     @Override
     public boolean onCommand(final @NotNull CommandSender sender, final @NotNull Command cmd,
                              final @NotNull String label, final String[] args) {
-        if (!(sender instanceof final Player player)) {
-            sender.sendMessage(Component.text("Command has to be run by a player"));
-            return true;
+
+        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) {
+                    return false;
+                }
+
+                // 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 -> {
+                return false;
+            }
         }
 
-        if (args.length >= 1 && "on".equalsIgnoreCase(args[0])) {
-            enableCommandSpy(player);
-            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 ((args.length >= 1 && "off".equalsIgnoreCase(args[0]))
-                || this.config.getCommandSpyState(player.getUniqueId())) {
-            disableCommandSpy(player);
-            return true;
+
+        if (state == null) {
+            state = !this.config.getCommandSpyState(target.getUniqueId());
         }
-        enableCommandSpy(player);
+
+        this.updateCommandSpyState(target, sender, state);
+
         return true;
     }
 
@@ -81,8 +127,8 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
         final NamedTextColor color = getTextColor(player);
 
         final Component message = Component.text(player.getName(), color)
-            .append(Component.text(": "))
-            .append(Component.text(event.getMessage()));
+                .append(Component.text(": "))
+                .append(Component.text(event.getMessage()));
 
         this.config.broadcastSpyMessage(message);
     }
@@ -92,14 +138,44 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
         final Player player = event.getPlayer();
         final NamedTextColor color = getTextColor(player);
         Component message = Component.text(player.getName(), color)
-            .append(Component.text(" created a sign with contents:"));
+                .append(Component.text(" created a sign with contents:"));
 
         for (final Component line : event.lines()) {
             message = message
-                .append(Component.text("\n "))
-                .append(line);
+                    .append(Component.text("\n "))
+                    .append(line);
         }
 
         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/resources/plugin.yml b/src/main/resources/plugin.yml
index af086de..6a93917 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -10,3 +10,4 @@ commands:
       aliases: [ c, cs, cspy ]
       description: Allows you to spy on players' commands
       permission: commandspy.command
+      usage: 'Usage: /<command> [player] [on|enable|off|disable]'

From 74db21dea145ec7b2011b1b4f01cc91ecd1832d4 Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Sun, 23 Jun 2024 22:18:51 -0300
Subject: [PATCH 08/14] fix: use correct usage; color usage in red

---
 src/main/java/pw/kaboom/commandspy/Main.java | 10 ++++++++--
 src/main/resources/plugin.yml                |  2 +-
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/main/java/pw/kaboom/commandspy/Main.java b/src/main/java/pw/kaboom/commandspy/Main.java
index 62da442..01f7536 100644
--- a/src/main/java/pw/kaboom/commandspy/Main.java
+++ b/src/main/java/pw/kaboom/commandspy/Main.java
@@ -82,7 +82,10 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
                 if (state != null && args.length == 1) {
                     break;
                 } else if (state == null && args.length == 2) {
-                    return false;
+                    sender.sendMessage(Component.text("Usage: ", NamedTextColor.RED)
+                            .append(Component.text(cmd.getUsage().replace("<command>", label)))
+                    );
+                    return true;
                 }
 
                 // Get the first argument as a player. Fail if it can't be found.
@@ -99,7 +102,10 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
                 return true;
             }
             default -> {
-                return false;
+                sender.sendMessage(Component.text("Usage: ", NamedTextColor.RED)
+                        .append(Component.text(cmd.getUsage().replace("<command>", label)))
+                );
+                return true;
             }
         }
 
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 6a93917..7a39943 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -10,4 +10,4 @@ commands:
       aliases: [ c, cs, cspy ]
       description: Allows you to spy on players' commands
       permission: commandspy.command
-      usage: 'Usage: /<command> [player] [on|enable|off|disable]'
+      usage: '/<command> [player] [on|enable|off|disable]'

From dbf9796791c1e21e92f1e2626ac1fa96215fe4d8 Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 11:48:00 -0300
Subject: [PATCH 09/14] feat: run build action on every branch

---
 .github/workflows/main.yml | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3af4259..45948ae 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,10 +1,5 @@
 name: Maven CI
-
-on:
-  push:
-    branches: [ master ]
-  pull_request:
-    branches: [ master ]
+on: [push, pull_request]
 
 jobs:
   build:

From 876ec6ae447b80d37f7f52c28f535c261a4678a3 Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 16:45:57 -0300
Subject: [PATCH 10/14] chore: bump paper to 1.20.4; fix warnings

We should probably keep kaboom plugins' API version synced with the main
server
---
 pom.xml                                      | 2 +-
 src/main/java/pw/kaboom/commandspy/Main.java | 1 +
 src/main/resources/plugin.yml                | 2 +-
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index 6dfe236..494839c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,7 +15,7 @@
         <dependency>
             <groupId>io.papermc.paper</groupId>
             <artifactId>paper-api</artifactId>
-            <version>1.19.4-R0.1-SNAPSHOT</version>
+            <version>1.20.4-R0.1-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
     </dependencies>
diff --git a/src/main/java/pw/kaboom/commandspy/Main.java b/src/main/java/pw/kaboom/commandspy/Main.java
index 01f7536..111587f 100644
--- a/src/main/java/pw/kaboom/commandspy/Main.java
+++ b/src/main/java/pw/kaboom/commandspy/Main.java
@@ -25,6 +25,7 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
     public void onEnable() {
         this.config = new CommandSpyState(new File(this.getDataFolder(), "state.bin"));
 
+        //noinspection DataFlowIssue
         this.getCommand("commandspy").setExecutor(this);
         this.getServer().getPluginManager().registerEvents(this, this);
 
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 7a39943..9ba5483 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -1,7 +1,7 @@
 name: CommandSpy
 main: pw.kaboom.commandspy.Main
 description: Plugin that allows you to spy on players' commands.
-api-version: '1.19'
+api-version: '1.20'
 version: master
 folia-supported: true
 

From 91787dec900e3e89d58cdf60d0d18a2280a3a105 Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Tue, 25 Jun 2024 22:31:31 -0300
Subject: [PATCH 11/14] fix: consistency, maven warning

---
 pom.xml                                          |  3 +--
 .../pw/kaboom/commandspy/CommandSpyState.java    |  7 ++++---
 src/main/java/pw/kaboom/commandspy/Main.java     | 16 ++++++++--------
 3 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/pom.xml b/pom.xml
index 494839c..090e76e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,8 +5,7 @@
     <version>master</version>
 
     <properties>
-        <maven.compiler.source>17</maven.compiler.source>
-        <maven.compiler.target>17</maven.compiler.target>
+        <maven.compiler.release>17</maven.compiler.release>
         <maven.test.skip>true</maven.test.skip>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
diff --git a/src/main/java/pw/kaboom/commandspy/CommandSpyState.java b/src/main/java/pw/kaboom/commandspy/CommandSpyState.java
index 97e8542..676ba89 100644
--- a/src/main/java/pw/kaboom/commandspy/CommandSpyState.java
+++ b/src/main/java/pw/kaboom/commandspy/CommandSpyState.java
@@ -33,7 +33,8 @@ public final class CommandSpyState {
         } catch (final FileNotFoundException exception) {
             try {
                 this.save(); // Create file if it doesn't exist
-            } catch (IOException ignored) {}
+            } catch (IOException ignored) {
+            }
         } catch (final IOException exception) {
             LOGGER.error("Failed to load state file:", exception);
         }
@@ -47,7 +48,7 @@ public final class CommandSpyState {
 
         // Loop until we read less than 16 bytes
         while ((read = reader.readNBytes(buffer.array(), 0, 16)) == 16) {
-             this.users.add(new UUID(buffer.getLong(0), buffer.getLong(8)));
+            this.users.add(new UUID(buffer.getLong(0), buffer.getLong(8)));
         }
 
         reader.close();
@@ -62,7 +63,7 @@ public final class CommandSpyState {
         final ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
 
         final long stamp = this.usersLock.readLock();
-        for (final UUID uuid: this.users) {
+        for (final UUID uuid : this.users) {
             buffer.putLong(0, uuid.getMostSignificantBits());
             buffer.putLong(8, uuid.getLeastSignificantBits());
             writer.write(buffer.array());
diff --git a/src/main/java/pw/kaboom/commandspy/Main.java b/src/main/java/pw/kaboom/commandspy/Main.java
index 111587f..a5d4f40 100644
--- a/src/main/java/pw/kaboom/commandspy/Main.java
+++ b/src/main/java/pw/kaboom/commandspy/Main.java
@@ -1,8 +1,7 @@
 package pw.kaboom.commandspy;
 
-import java.io.File;
-import java.util.UUID;
-
+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;
@@ -14,10 +13,11 @@ import org.bukkit.event.Listener;
 import org.bukkit.event.block.SignChangeEvent;
 import org.bukkit.event.player.PlayerCommandPreprocessEvent;
 import org.bukkit.plugin.java.JavaPlugin;
-import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.format.NamedTextColor;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.File;
+import java.util.UUID;
+
 public final class Main extends JavaPlugin implements CommandExecutor, Listener {
     private CommandSpyState config;
 
@@ -47,7 +47,7 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
         target.sendMessage(Component.empty()
                 .append(Component.text("Successfully "))
                 .append(stateString)
-                .append(Component.text(" CommandSpy.")));
+                .append(Component.text(" CommandSpy")));
 
         if (source != target) {
             source.sendMessage(Component.empty()
@@ -55,7 +55,6 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
                     .append(stateString)
                     .append(Component.text(" CommandSpy for "))
                     .append(target.name())
-                    .append(Component.text("."))
             );
         }
     }
@@ -76,7 +75,8 @@ public final class Main extends JavaPlugin implements CommandExecutor, Listener
         Boolean state = null;
 
         switch (args.length) {
-            case 0 -> {}
+            case 0 -> {
+            }
             case 1, 2 -> {
                 // Get the last argument as a state. Fail if we have 2 arguments.
                 state = getState(args[args.length - 1]);

From d4ca1c84ad5dc65003d9532128dbd633ff4ae210 Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Tue, 25 Jun 2024 22:34:09 -0300
Subject: [PATCH 12/14] fix: maven skill issue

---
 pom.xml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/pom.xml b/pom.xml
index 090e76e..7e7e78e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,6 +5,8 @@
     <version>master</version>
 
     <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
         <maven.compiler.release>17</maven.compiler.release>
         <maven.test.skip>true</maven.test.skip>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

From 2a5a9da5672afb060bbdee6f4d9db6d633fde98c Mon Sep 17 00:00:00 2001
From: amy <144570677+amyavi@users.noreply.github.com>
Date: Fri, 27 Sep 2024 21:08:11 -0300
Subject: [PATCH 13/14] chore: bump checkstyle version

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 7e7e78e..66bc096 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,7 +46,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>3.4.0</version>
+                <version>3.5.0</version>
                 <executions>
                     <execution>
                         <id>checkstyle</id>

From 94c09b61f83ae4ac42578c7ff97d1dc7a980412d Mon Sep 17 00:00:00 2001
From: amyavi <144570677+amyavi@users.noreply.github.com>
Date: Sat, 21 Dec 2024 15:41:34 -0300
Subject: [PATCH 14/14] chore: bump checkstyle version

---
 pom.xml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index 66bc096..61c531f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,6 @@
     <properties>
         <maven.compiler.source>17</maven.compiler.source>
         <maven.compiler.target>17</maven.compiler.target>
-        <maven.compiler.release>17</maven.compiler.release>
         <maven.test.skip>true</maven.test.skip>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
@@ -46,7 +45,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>3.5.0</version>
+                <version>3.6.0</version>
                 <executions>
                     <execution>
                         <id>checkstyle</id>