diff --git a/src/main/java/cc/fascinated/Aetheria.java b/src/main/java/cc/fascinated/Aetheria.java index 0b6065b..ed3c003 100644 --- a/src/main/java/cc/fascinated/Aetheria.java +++ b/src/main/java/cc/fascinated/Aetheria.java @@ -3,9 +3,10 @@ package cc.fascinated; import cc.fascinated.account.AccountManager; import cc.fascinated.chat.ChatManager; import cc.fascinated.command.CommandManager; +import cc.fascinated.commandspy.CommandSpyManager; +import cc.fascinated.commandspy.command.CommandSpyCommand; import cc.fascinated.event.EventManager; import cc.fascinated.metrics.MetricManager; -import cc.fascinated.misc.PlayerVersionWarning; import cc.fascinated.motd.MotdManager; import cc.fascinated.placeholder.PlaceholderManager; import cc.fascinated.playercolor.PlayerColorManager; @@ -47,6 +48,6 @@ public class Aetheria extends JavaPlugin { new PlayerColorManager(); new ChatManager(); new MotdManager(); - new PlayerVersionWarning(); + new CommandSpyManager(); } } \ No newline at end of file diff --git a/src/main/java/cc/fascinated/account/Account.java b/src/main/java/cc/fascinated/account/Account.java index 2122623..9553746 100644 --- a/src/main/java/cc/fascinated/account/Account.java +++ b/src/main/java/cc/fascinated/account/Account.java @@ -1,8 +1,10 @@ package cc.fascinated.account; import cc.fascinated.Aetheria; +import cc.fascinated.config.Lang; import cc.fascinated.playercolor.PlayerColor; import cc.fascinated.utils.Style; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; @@ -16,7 +18,7 @@ import org.bukkit.entity.Player; import java.io.File; import java.util.UUID; -@Getter @Log4j2 +@Getter @Log4j2 @EqualsAndHashCode public class Account { /** @@ -24,9 +26,18 @@ public class Account { */ private static final String playerColorProfileId = "playerColorProfile"; + /** + * The last hashcode of the account. This is used to check if any changes + * have been made to the account. If no changes have been made, + * the account will not be saved this is to prevent unnecessary saving. + */ + @EqualsAndHashCode.Exclude + private int lastHash; + /** * The UUID of the player. */ + @EqualsAndHashCode.Exclude private final UUID uuid; /** @@ -42,11 +53,13 @@ public class Account { /** * The file for this account. */ + @EqualsAndHashCode.Exclude private final File file; /** * The configuration for this account. */ + @EqualsAndHashCode.Exclude private final FileConfiguration config; /** @@ -89,9 +102,25 @@ public class Account { // Load profiles this.playerColorProfile = new PlayerColor(this, this.getProfileSection(playerColorProfileId)); + this.lastHash = this.hashCode(); + //log.info("Loaded account for " + this.uuid); } + /** + * Save a profile to the configuration. + * + * @param profile the profile to save + * @param key the key to save the profile under + */ + private void saveProfile(Profile profile, String key) { + profile.save(config.getConfigurationSection(key) == null ? config.createSection(key) : config.getConfigurationSection(key)); + } + + private ConfigurationSection getProfileSection(String key) { + return this.config.getConfigurationSection(key); + } + /** * Get the name of the player. * @@ -138,21 +167,49 @@ public class Account { } /** - * Save a profile to the configuration. + * Send a message to the player with a prefix. * - * @param profile the profile to save - * @param key the key to save the profile under + * @param prefix the prefix to use + * @param message the message to send */ - private void saveProfile(Profile profile, String key) { - profile.save(config.getConfigurationSection(key) == null ? config.createSection(key) : config.getConfigurationSection(key)); + public void sendMessage(String prefix, Component message) { + Component prefixComponent = Style.getMiniMessage().deserialize(Lang.PREFIX_FORMAT.getAsString() + .replace("%prefix%", prefix.toUpperCase()) // Always ensure the prefix is uppercase + ); + this.sendMessage(prefixComponent.append(message)); } - private ConfigurationSection getProfileSection(String key) { - return this.config.getConfigurationSection(key); + /** + * Send a message to the player with a prefix. + * + * @param prefix the prefix to use + * @param message the message to send + */ + public void sendMessage(String prefix, String message) { + this.sendMessage(prefix, Style.getMiniMessage().deserialize(message)); } + /** + * Check if the player is an operator. + * + * @return if the player is an operator + */ + public boolean isOp() { + return getPlayer().isOp(); + } + + /** + * Saves the account to disk. + * + * @param saveProfiles if the profiles should be saved + * @return true if the account was saved, false otherwise + */ @SneakyThrows - public void save(boolean saveProfiles) { + public boolean save(boolean saveProfiles) { + if (this.lastHash == this.hashCode()) { + return false; // No changes have been made + } + this.config.set("firstJoin", this.firstJoin); this.config.set("lastLogin", this.lastLogin); @@ -160,6 +217,17 @@ public class Account { this.saveProfile(this.playerColorProfile, playerColorProfileId); } - this.config.save(this.file); + this.config.save(this.file); // Save the account to disk + this.lastHash = this.hashCode(); // Update the last hash + return true; + } + + /** + * Saves the account and profiles to disk. + * + * @return true if the account was saved, false otherwise + */ + public boolean save() { + return this.save(true); } } diff --git a/src/main/java/cc/fascinated/account/AccountManager.java b/src/main/java/cc/fascinated/account/AccountManager.java index a8f2046..3f95a44 100644 --- a/src/main/java/cc/fascinated/account/AccountManager.java +++ b/src/main/java/cc/fascinated/account/AccountManager.java @@ -1,12 +1,16 @@ package cc.fascinated.account; import cc.fascinated.Aetheria; +import cc.fascinated.account.command.SaveAccountsCommand; +import cc.fascinated.command.CommandManager; import cc.fascinated.config.Config; import cc.fascinated.config.Lang; +import cc.fascinated.playercolor.PlayerColor; import cc.fascinated.utils.DiscordWebhook; import cc.fascinated.utils.Manager; import cc.fascinated.utils.Priority; import cc.fascinated.utils.Style; +import com.viaversion.viaversion.api.Via; import lombok.extern.log4j.Log4j2; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -14,7 +18,6 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; import java.io.IOException; @@ -30,13 +33,15 @@ public class AccountManager extends Manager implements Listener { private static final Map ACCOUNTS = new HashMap<>(); public AccountManager() { + CommandManager.registerCommand(new SaveAccountsCommand()); + Aetheria.INSTANCE.getServer().getPluginManager().registerEvents(this, Aetheria.INSTANCE); for (Player player : Bukkit.getOnlinePlayers()) { loadAccount(player.getUniqueId()); } Bukkit.getAsyncScheduler().runAtFixedRate(Aetheria.INSTANCE, (task) -> { - this.saveAccounts(); + saveAccounts(); }, SAVE_INTERVAL, SAVE_INTERVAL, TimeUnit.MINUTES); } @@ -78,13 +83,17 @@ public class AccountManager extends Manager implements Listener { /** * Save all accounts to disk. */ - private void saveAccounts() { - long before = System.currentTimeMillis(); + public static void saveAccounts() { + long before = System.currentTimeMillis(), + saved = 0; // The amount of accounts that were saved log.info("Saving accounts..."); for (Account account : ACCOUNTS.values()) { - account.save(true); // Save the account + boolean didSave = account.save(); // Save the account + if (didSave) { + saved++; + } } - log.info("Saved {} accounts. ({}ms)", ACCOUNTS.size(), System.currentTimeMillis() - before); + log.info("Saved {}/{} accounts. ({}ms)", saved, ACCOUNTS.size(), System.currentTimeMillis() - before); } @EventHandler @@ -93,15 +102,15 @@ public class AccountManager extends Manager implements Listener { if (isAccountLoaded(uuid)) { // Account already loaded return; } - loadAccount(uuid); + loadAccount(uuid); // Load the account into memory } @Override public void onPlayerJoin(Account account, PlayerJoinEvent event) { - Player player = event.getPlayer(); String joinMessage = Lang.JOIN_MESSAGE.getAsString(); + PlayerColor playerColorProfile = account.getPlayerColorProfile(); - if (!player.hasPlayedBefore()) { + if (!account.getPlayer().hasPlayedBefore()) { joinMessage = Lang.FIRST_JOIN_MESSAGE.getAsString(); // Send a notification to the discord log channel @@ -110,8 +119,8 @@ public class AccountManager extends Manager implements Listener { DiscordWebhook discordWebhook = new DiscordWebhook(Config.DISCORD_LOG_WEBHOOK.getAsString()); DiscordWebhook.EmbedObject embed = new DiscordWebhook.EmbedObject(); embed.setTitle("New Player Joined"); - embed.addField("Name", player.getName(), true); - embed.addField("UUID", player.getUniqueId().toString(), true); + embed.addField("Name", account.getName(), true); + embed.addField("UUID", account.getUuid().toString(), true); discordWebhook.addEmbed(embed); try { @@ -122,25 +131,30 @@ public class AccountManager extends Manager implements Listener { }); } event.joinMessage(Style.getMiniMessage().deserialize(joinMessage - .replace("%player%", player.getName()) - .replace("player-color", account.getPlayerColorProfile().getColor().toString()) + .replace("%player%", account.getName()) + .replace("player-color", playerColorProfile.getColor().toString()) )); + + // Check if the player is using an outdated version and send a warning message + int version = Via.getAPI().getPlayerVersion(account.getUuid()); + if (version < Config.VERSION_WARNING_VERSION.getAsInt()) { + account.sendMessage(Style.getMiniMessage().deserialize(Config.VERSION_WARNING_MESSAGE.getAsString())); + } } @Override public void onPlayerQuit(Account account, PlayerQuitEvent event) { - account.save(true); // Save the account - - ACCOUNTS.remove(account.getUuid()); event.quitMessage(Style.getMiniMessage().deserialize(Lang.QUIT_MESSAGE.getAsString() .replace("%player%", event.getPlayer().getName()) .replace("player-color", account.getPlayerColorProfile().getColor().toString()) )); + account.save(); // Save the account + ACCOUNTS.remove(account.getUuid()); // Remove the account from the cache } @Override public void onAetheriaDisable() { - this.saveAccounts(); // Save the accounts to disk + saveAccounts(); // Save the accounts to disk ACCOUNTS.clear(); // Remove the accounts from the cache } } diff --git a/src/main/java/cc/fascinated/account/Profile.java b/src/main/java/cc/fascinated/account/Profile.java index b3674d9..ccce70c 100644 --- a/src/main/java/cc/fascinated/account/Profile.java +++ b/src/main/java/cc/fascinated/account/Profile.java @@ -1,12 +1,10 @@ package cc.fascinated.account; -import cc.fascinated.command.Command; -import cc.fascinated.utils.Manager; import lombok.Getter; import org.bukkit.configuration.ConfigurationSection; @Getter -public abstract class Profile extends Manager { +public abstract class Profile { /** * The account this profile is for. diff --git a/src/main/java/cc/fascinated/account/command/SaveAccountsCommand.java b/src/main/java/cc/fascinated/account/command/SaveAccountsCommand.java new file mode 100644 index 0000000..eee5243 --- /dev/null +++ b/src/main/java/cc/fascinated/account/command/SaveAccountsCommand.java @@ -0,0 +1,20 @@ +package cc.fascinated.account.command; + +import cc.fascinated.account.Account; +import cc.fascinated.account.AccountManager; +import cc.fascinated.command.Command; +import cc.fascinated.config.Lang; + +public class SaveAccountsCommand extends Command { + + public SaveAccountsCommand() { + super("saveaccounts", "command.aetheria.saveaccounts"); + } + + @Override + public void execute(Account account, String[] args) { + account.sendMessage(Lang.SAVE_ACCOUNTS_COMMAND_SAVING.getAsString()); + AccountManager.saveAccounts(); + account.sendMessage(Lang.SAVE_ACCOUNTS_COMMAND_SAVED.getAsString()); + } +} diff --git a/src/main/java/cc/fascinated/commandspy/CommandSpyManager.java b/src/main/java/cc/fascinated/commandspy/CommandSpyManager.java new file mode 100644 index 0000000..b3695bb --- /dev/null +++ b/src/main/java/cc/fascinated/commandspy/CommandSpyManager.java @@ -0,0 +1,72 @@ +package cc.fascinated.commandspy; + +import cc.fascinated.account.Account; +import cc.fascinated.command.CommandManager; +import cc.fascinated.commandspy.command.CommandSpyCommand; +import cc.fascinated.config.Lang; +import cc.fascinated.utils.Manager; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.util.ArrayList; +import java.util.List; + +public class CommandSpyManager extends Manager { + + /** + * The list of accounts that have command spy enabled. + */ + private static final List COMMAND_SPY_ENABLED = new ArrayList<>(); + + public CommandSpyManager() { + CommandManager.registerCommand(new CommandSpyCommand()); + } + + /** + * Toggles command spy for the specified account. + * + * @param account the account + * @return true if command spy is enabled, false otherwise + */ + public static boolean toggleCommandSpy(Account account) { + if (COMMAND_SPY_ENABLED.contains(account)) { // Disable command spy + COMMAND_SPY_ENABLED.remove(account); + return false; + } + COMMAND_SPY_ENABLED.add(account); // Enable command spy + return true; + } + + @Override + public void onCommandPreProcess(Account account, PlayerCommandPreprocessEvent event) { + for (Account commandSpyAccount : COMMAND_SPY_ENABLED) { + commandSpyAccount.sendMessage(Lang.COMMAND_SPY_FORMAT.getAsString() + .replace("player-color", account.getPlayerColorProfile().getColor().toString()) + .replace("%player%", account.getName()) + .replace("%command%", event.getMessage()) + ); + } + } + + @Override + public void onPlayerJoin(Account account, PlayerJoinEvent event) { + if (!account.isOp()) { + return; + } + COMMAND_SPY_ENABLED.add(account); // Add account to command spy list + } + + @Override + public void onPlayerQuit(Account account, PlayerQuitEvent event) { + if (!COMMAND_SPY_ENABLED.contains(account)) { + return; + } + COMMAND_SPY_ENABLED.remove(account); // Remove account from command spy list + } + + @Override + public void onAetheriaDisable() { + COMMAND_SPY_ENABLED.clear(); // Clear command spy list + } +} diff --git a/src/main/java/cc/fascinated/commandspy/command/CommandSpyCommand.java b/src/main/java/cc/fascinated/commandspy/command/CommandSpyCommand.java new file mode 100644 index 0000000..974a701 --- /dev/null +++ b/src/main/java/cc/fascinated/commandspy/command/CommandSpyCommand.java @@ -0,0 +1,23 @@ +package cc.fascinated.commandspy.command; + +import cc.fascinated.account.Account; +import cc.fascinated.command.Command; +import cc.fascinated.commandspy.CommandSpyManager; +import cc.fascinated.config.Lang; + +public class CommandSpyCommand extends Command { + + public CommandSpyCommand() { + super("commandspy", "aetheria.command.commandspy"); + } + + @Override + public void execute(Account account, String[] args) { + boolean enabled = CommandSpyManager.toggleCommandSpy(account); + if (enabled) { + account.sendMessage(Lang.COMMAND_SPY_ENABLED.getAsString()); + return; + } + account.sendMessage(Lang.COMMAND_SPY_DISABLED.getAsString()); + } +} diff --git a/src/main/java/cc/fascinated/config/Lang.java b/src/main/java/cc/fascinated/config/Lang.java index 1ed61cb..5bf4a06 100644 --- a/src/main/java/cc/fascinated/config/Lang.java +++ b/src/main/java/cc/fascinated/config/Lang.java @@ -12,6 +12,7 @@ import java.util.List; public enum Lang { PREFIX("prefix"), + PREFIX_FORMAT("prefix-format"), ADMIN_PREFIX("admin-prefix"), HELP_COMMAND("help-command"), GIT_COMMAND("git-command"), @@ -23,12 +24,17 @@ public enum Lang { VOTE_COMMAND_HEADER("vote-command.header"), VOTE_COMMAND_FORMAT("vote-command.format"), VOTE_COMMAND_LINKS("vote-command.links"), + SAVE_ACCOUNTS_COMMAND_SAVING("save-accounts-command.saving"), + SAVE_ACCOUNTS_COMMAND_SAVED("save-accounts-command.saved"), BLOCKED_MESSAGE("blocked-message"), BLOCKED_MESSAGE_ALERT("blocked-message-alert"), CHAT_FORMAT("chat-format"), FIRST_JOIN_MESSAGE("first-join-message"), JOIN_MESSAGE("join-message"), - QUIT_MESSAGE("quit-message"); + QUIT_MESSAGE("quit-message"), + COMMAND_SPY_ENABLED("command-spy.toggled-on"), + COMMAND_SPY_DISABLED("command-spy.toggled-off"), + COMMAND_SPY_FORMAT("command-spy.format"); /** * The path of the lang in the lang.yml file. diff --git a/src/main/java/cc/fascinated/event/EventListener.java b/src/main/java/cc/fascinated/event/EventListener.java index 1e44db6..a4e3c74 100644 --- a/src/main/java/cc/fascinated/event/EventListener.java +++ b/src/main/java/cc/fascinated/event/EventListener.java @@ -3,6 +3,8 @@ package cc.fascinated.event; import cc.fascinated.account.Account; import cc.fascinated.utils.Priority; import io.papermc.paper.event.player.AsyncChatEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; @@ -19,5 +21,7 @@ public interface EventListener { default void onPlayerLogin(Account account, PlayerLoginEvent event) {} default void onPlayerQuit(Account account, PlayerQuitEvent event) {} default void onAsyncChat(Account account, AsyncChatEvent event) {} + default void onCommandPreProcess(Account account, PlayerCommandPreprocessEvent event) {} default void onServerListPing(ServerListPingEvent event) {} + default void onEntityDeath(EntityDeathEvent event) {} } diff --git a/src/main/java/cc/fascinated/event/EventManager.java b/src/main/java/cc/fascinated/event/EventManager.java index e77c88e..4686d8b 100644 --- a/src/main/java/cc/fascinated/event/EventManager.java +++ b/src/main/java/cc/fascinated/event/EventManager.java @@ -7,6 +7,8 @@ import io.papermc.paper.event.player.AsyncChatEvent; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; @@ -89,10 +91,27 @@ public class EventManager implements Listener { } } + @EventHandler + public void onCommandPreProcess(PlayerCommandPreprocessEvent event) { + Player player = event.getPlayer(); + Account account = AccountManager.getAccount(player.getUniqueId()); + + for (EventListener listener : LISTENERS) { + listener.onCommandPreProcess(account, event); + } + } + @EventHandler public void onServerListPing(ServerListPingEvent event) { for (EventListener listener : LISTENERS) { listener.onServerListPing(event); } } + + @EventHandler + public void onEntityDeath(EntityDeathEvent event) { + for (EventListener listener : LISTENERS) { + listener.onEntityDeath(event); + } + } } diff --git a/src/main/java/cc/fascinated/misc/PlayerVersionWarning.java b/src/main/java/cc/fascinated/misc/PlayerVersionWarning.java deleted file mode 100644 index 883cd24..0000000 --- a/src/main/java/cc/fascinated/misc/PlayerVersionWarning.java +++ /dev/null @@ -1,19 +0,0 @@ -package cc.fascinated.misc; - -import cc.fascinated.account.Account; -import cc.fascinated.config.Config; -import cc.fascinated.utils.Manager; -import cc.fascinated.utils.Style; -import com.viaversion.viaversion.api.Via; -import org.bukkit.event.player.PlayerJoinEvent; - -public class PlayerVersionWarning extends Manager { - - @Override - public void onPlayerJoin(Account account, PlayerJoinEvent event) { - int version = Via.getAPI().getPlayerVersion(account.getUuid()); - if (version < Config.VERSION_WARNING_VERSION.getAsInt()) { - account.sendMessage(Style.getMiniMessage().deserialize(Config.VERSION_WARNING_MESSAGE.getAsString())); - } - } -} diff --git a/src/main/java/cc/fascinated/playercolor/PlayerColor.java b/src/main/java/cc/fascinated/playercolor/PlayerColor.java index a87c8f2..ac3ec5c 100644 --- a/src/main/java/cc/fascinated/playercolor/PlayerColor.java +++ b/src/main/java/cc/fascinated/playercolor/PlayerColor.java @@ -2,13 +2,14 @@ package cc.fascinated.playercolor; import cc.fascinated.account.Account; import cc.fascinated.account.Profile; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.scoreboard.Team; -@Getter @Setter +@Getter @Setter @EqualsAndHashCode(callSuper = false) public class PlayerColor extends Profile { /** diff --git a/src/main/java/cc/fascinated/utils/Style.java b/src/main/java/cc/fascinated/utils/Style.java index 1877f68..d350475 100644 --- a/src/main/java/cc/fascinated/utils/Style.java +++ b/src/main/java/cc/fascinated/utils/Style.java @@ -2,6 +2,7 @@ package cc.fascinated.utils; import cc.fascinated.config.Lang; import lombok.Getter; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; @@ -31,4 +32,20 @@ public class Style { .build() ).build(); } + + /** + * Get the prefix for a class. + * + * @param clazz the class + * @return the prefix + */ + public static Component getPrefix(Class clazz) { + String className = clazz.getSimpleName() + .replace("manager", "") // PlayerColorManager -> PlayerColor + .replace("command", "") // PlayerColorCommand -> PlayerColor + .toUpperCase(); + return miniMessage.deserialize(Lang.PREFIX_FORMAT.getAsString() + .replace("%class%", className) + ); + } } diff --git a/src/main/resources/lang.yml b/src/main/resources/lang.yml index f496217..6567443 100644 --- a/src/main/resources/lang.yml +++ b/src/main/resources/lang.yml @@ -1,4 +1,5 @@ prefix: "AETHERIA » " +prefix-format: "%class% » " admin-prefix: "[ADMIN] " chat-format: "<<%chatcolor%>%name%> %message%" @@ -9,6 +10,11 @@ first-join-message: "%player% joined for the first time!" join-message: "%player% joined the game" quit-message: "%player% left the game" +command-spy: + format: "[CS] %player% ran: %command%" + toggled-on: "Command spy is now enabled" + toggled-off: "Command spy is now disabled" + help-command: - "Commands:" - "/kill - Kills you" @@ -39,3 +45,6 @@ vote-command: - "https://servers-minecraft.net/server-aetheria.24701" - "https://topminecraftservers.org/vote/33565" - "https://best-minecraft-servers.co/server-aetheria.16373/vote" +save-accounts-command: + saving: "Saving accounts..." + saved: "Accounts saved!" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4115d3e..5485871 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -27,4 +27,10 @@ commands: usage: "/vote" git: description: "Shows the git information" - usage: "/git" \ No newline at end of file + usage: "/git" + saveaccounts: + description: "Saves the accounts" + usage: "/saveaccounts" + commandspy: + description: "Toggles command spy" + usage: "/commandspy" \ No newline at end of file