Merge pull request #11 from amyavi/cleanups

This commit is contained in:
Kaboom 2025-03-08 22:19:10 +02:00 committed by GitHub
commit fac2dfe8f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 282 additions and 75 deletions

View file

@ -1,10 +1,5 @@
name: Maven CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
on: [push, pull_request]
jobs:
build:
@ -31,3 +26,4 @@ jobs:
with:
name: CommandSpy
path: target/CommandSpy.jar
compression-level: 0

2
.gitignore vendored
View file

@ -6,3 +6,5 @@ target/
.classpath
.project
*.iml
.theia/
run/

View file

@ -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

16
pom.xml
View file

@ -15,7 +15,7 @@
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.18.2-R0.1-SNAPSHOT</version>
<version>1.20.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
@ -30,10 +30,22 @@
<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>
<version>3.1.2</version>
<version>3.6.0</version>
<executions>
<execution>
<id>checkstyle</id>

View file

@ -0,0 +1,127 @@
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);
}
}
}

View file

@ -1,120 +1,188 @@
package pw.kaboom.commandspy;
import java.util.Set;
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;
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;
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;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.UUID;
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"));
//noinspection DataFlowIssue
this.getCommand("commandspy").setExecutor(this);
this.getServer().getPluginManager().registerEvents(this, this);
// Save the state every 30 seconds
Bukkit.getScheduler().runTaskTimerAsynchronously(this, this.config::trySave, 600L, 600L);
}
private void enableCommandSpy(final Player player) {
config.set(player.getUniqueId().toString(), true);
saveConfig();
player.sendMessage(Component.text("Successfully enabled CommandSpy"));
@Override
public void onDisable() {
this.config.trySave();
}
private void disableCommandSpy(final Player player) {
config.set(player.getUniqueId().toString(), null);
saveConfig();
player.sendMessage(Component.text("Successfully disabled CommandSpy"));
private void updateCommandSpyState(final @NotNull Player target,
final @NotNull CommandSender source, final boolean state) {
this.config.setCommandSpyState(target.getUniqueId(), state);
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())
);
}
}
private NamedTextColor getTextColor(final Player player) {
if (config.contains(player.getUniqueId().toString())) {
if (this.config.getCommandSpyState(player.getUniqueId())) {
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) {
sender.sendMessage(Component.text("Command has to be run by a player"));
return true;
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("<command>", 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("<command>", label)))
);
return true;
}
}
final Player player = (Player) sender;
if (target == null) {
if (!(sender instanceof final Player player)) {
sender.sendMessage(Component.text("Command has to be run by a player"));
return true;
}
if (args.length >= 1 && "on".equalsIgnoreCase(args[0])) {
enableCommandSpy(player);
return true;
target = player;
}
if ((args.length >= 1 && "off".equalsIgnoreCase(args[0]))
|| config.contains(player.getUniqueId().toString())) {
disableCommandSpy(player);
return true;
if (state == null) {
state = !this.config.getCommandSpyState(target.getUniqueId());
}
enableCommandSpy(player);
this.updateCommandSpyState(target, sender, state);
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)
.append(Component.text(": "))
.append(Component.text(event.getMessage()));
.append(Component.text(": "))
.append(Component.text(event.getMessage()));
for (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
@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:"));
.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);
.append(Component.text("\n "))
.append(line);
}
for (String uuidString : config.getKeys(false)) {
final UUID uuid = UUID.fromString(uuidString);
final Player recipient = Bukkit.getPlayer(uuid);
this.config.broadcastSpyMessage(message);
}
if (recipient == null) {
continue;
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;
}
recipient.sendMessage(message);
}
}
}

View file

@ -1,13 +1,13 @@
name: CommandSpy
main: pw.kaboom.commandspy.Main
description: Plugin that allows you to spy on players' commands.
api-version: 1.13
api-version: '1.20'
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
usage: '/<command> [player] [on|enable|off|disable]'

View file

@ -5,4 +5,6 @@
<suppressions>
<suppress checks="Javadoc" files="."/>
</suppressions>
<suppress checks="MagicNumber" files="."/>
<suppress checks="MethodLength" files="."/>
</suppressions>