commit 88cf021c3b4e13b8e144d408fe21f817f705682f Author: Xelara Networks Date: Sat Jun 20 12:05:20 2026 -0400 added diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6e28d7d --- /dev/null +++ b/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + com.bitnix + DirtSpy + 1.0-SNAPSHOT + jar + + DirtSpy + Admin review plugin for collecting realistic player/client/network stats on Paper. + + + 21 + 21 + UTF-8 + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + + + + io.papermc.paper + paper-api + 1.21.1-R0.1-SNAPSHOT + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + + + + diff --git a/src/main/java/com/bitnix/dirtspy/ClientBrandListener.java b/src/main/java/com/bitnix/dirtspy/ClientBrandListener.java new file mode 100644 index 0000000..8701009 --- /dev/null +++ b/src/main/java/com/bitnix/dirtspy/ClientBrandListener.java @@ -0,0 +1,119 @@ +package com.bitnix.dirtspy; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; + +import java.nio.charset.StandardCharsets; + +public class ClientBrandListener implements PluginMessageListener { + + private final DirtSpyPlugin plugin; + + public ClientBrandListener(DirtSpyPlugin plugin) { + this.plugin = plugin; + } + + @Override + public void onPluginMessageReceived(String channel, Player player, byte[] message) { + if (player == null || message == null) { + return; + } + + if (!"minecraft:brand".equals(channel)) { + return; + } + + if (!plugin.getConfig().getBoolean("settings.track-client-brand", true)) { + return; + } + + String brand = decodeBrand(message); + PlayerRecord record = plugin.getPlayerDataManager().getOrCreate(player.getUniqueId()); + record.setName(player.getName()); + record.setClientBrand(brand); + record.setLastSeen(System.currentTimeMillis()); + } + + private String decodeBrand(byte[] data) { + try { + VarIntResult lenResult = readVarInt(data, 0); + if (lenResult == null) { + return sanitize(fallbackDecode(data)); + } + + int length = lenResult.value; + int start = lenResult.nextIndex; + + if (length < 0 || start < 0 || start + length > data.length) { + return sanitize(fallbackDecode(data)); + } + + String decoded = new String(data, start, length, StandardCharsets.UTF_8); + return sanitize(decoded); + } catch (Throwable ignored) { + return sanitize(fallbackDecode(data)); + } + } + + private String fallbackDecode(byte[] data) { + try { + return new String(data, StandardCharsets.UTF_8); + } catch (Throwable ignored) { + return "unknown"; + } + } + + private String sanitize(String input) { + if (input == null) { + return "unknown"; + } + + String cleaned = input + .replace("\u0000", "") + .replaceAll("\\p{Cntrl}", "") + .trim(); + + if (cleaned.isEmpty()) { + return "unknown"; + } + + if (cleaned.length() > 64) { + cleaned = cleaned.substring(0, 64); + } + + return cleaned; + } + + private VarIntResult readVarInt(byte[] data, int offset) { + int numRead = 0; + int result = 0; + byte read; + + do { + if (offset + numRead >= data.length) { + return null; + } + + read = data[offset + numRead]; + int value = read & 0b01111111; + result |= (value << (7 * numRead)); + + numRead++; + if (numRead > 5) { + return null; + } + } while ((read & 0b10000000) != 0); + + return new VarIntResult(result, offset + numRead); + } + + private static class VarIntResult { + private final int value; + private final int nextIndex; + + private VarIntResult(int value, int nextIndex) { + this.value = value; + this.nextIndex = nextIndex; + } + } +} diff --git a/src/main/java/com/bitnix/dirtspy/DirtSpyCommand.java b/src/main/java/com/bitnix/dirtspy/DirtSpyCommand.java new file mode 100644 index 0000000..8920f1b --- /dev/null +++ b/src/main/java/com/bitnix/dirtspy/DirtSpyCommand.java @@ -0,0 +1,124 @@ +package com.bitnix.dirtspy; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class DirtSpyCommand implements CommandExecutor, TabCompleter { + + private final DirtSpyPlugin plugin; + + public DirtSpyCommand(DirtSpyPlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("dirtspy.use")) { + sender.sendMessage(plugin.message("messages.no-permission")); + return true; + } + + if (args.length != 1) { + sender.sendMessage(plugin.message("messages.prefix") + plugin.message("messages.usage")); + return true; + } + + if (args[0].equalsIgnoreCase("reload")) { + if (!sender.hasPermission("dirtspy.reload")) { + sender.sendMessage(plugin.message("messages.prefix") + plugin.message("messages.no-permission")); + return true; + } + + plugin.reloadPlugin(); + sender.sendMessage(plugin.message("messages.prefix") + plugin.message("messages.reloaded")); + return true; + } + + if (!sender.hasPermission("dirtspy.view")) { + sender.sendMessage(plugin.message("messages.prefix") + plugin.message("messages.no-permission")); + return true; + } + + String targetName = args[0]; + PlayerRecord record = plugin.getPlayerDataManager().getByName(targetName); + + if (record == null) { + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayerIfCached(targetName); + if (offlinePlayer != null && offlinePlayer.getUniqueId() != null) { + record = plugin.getPlayerDataManager().getOrCreate(offlinePlayer.getUniqueId()); + if (record.getName() == null) { + record.setName(offlinePlayer.getName() == null ? targetName : offlinePlayer.getName()); + } + } + } + + if (record == null || record.getName() == null) { + sender.sendMessage(plugin.message("messages.prefix") + plugin.message("messages.player-not-found")); + return true; + } + + List alts = plugin.getPlayerDataManager().getAltNamesOnIp(record.getLastIp(), record.getUuid()); + + sender.sendMessage(plugin.message("messages.prefix") + plugin.message("messages.header").replace("%player%", record.getName())); + sender.sendMessage(format("messages.line-name", safe(record.getName()))); + sender.sendMessage(format("messages.line-uuid", String.valueOf(record.getUuid()))); + sender.sendMessage(format("messages.line-first-seen", formatTime(record.getFirstSeen()))); + sender.sendMessage(format("messages.line-last-seen", formatTime(record.getLastSeen()))); + sender.sendMessage(format("messages.line-joins", String.valueOf(record.getJoinCount()))); + sender.sendMessage(format("messages.line-current-ip", safe(record.getCurrentIp()))); + sender.sendMessage(format("messages.line-last-ip", safe(record.getLastIp()))); + sender.sendMessage(format("messages.line-locale", safe(record.getLocale()))); + sender.sendMessage(format("messages.line-brand", safe(record.getClientBrand()))); + sender.sendMessage(format("messages.line-view-distance", String.valueOf(record.getClientViewDistance()))); + sender.sendMessage(format("messages.line-last-world", safe(record.getLastWorld()))); + sender.sendMessage(format("messages.line-last-gamemode", safe(record.getLastGamemode()))); + sender.sendMessage(format("messages.line-last-join", formatTime(record.getLastJoinTime()))); + sender.sendMessage(format("messages.line-last-quit", formatTime(record.getLastQuitTime()))); + sender.sendMessage(format("messages.line-total-playtime", String.valueOf(record.getTotalTrackedPlaytimeSeconds()))); + sender.sendMessage(format("messages.line-alt-count", String.valueOf(alts.size()))); + sender.sendMessage(format("messages.line-alts", alts.isEmpty() ? "none" : String.join(", ", alts))); + sender.sendMessage(format("messages.line-online", String.valueOf(record.isOnline()))); + return true; + } + + private String format(String path, String value) { + return plugin.message("messages.prefix") + plugin.message(path).replace("%value%", value); + } + + private String safe(String input) { + return input == null || input.isBlank() ? "unknown" : input; + } + + private String formatTime(long millis) { + if (millis <= 0L) { + return "never"; + } + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(millis)); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + List completions = new ArrayList<>(); + if (args.length == 1) { + if ("reload".startsWith(args[0].toLowerCase())) { + completions.add("reload"); + } + + Bukkit.getOnlinePlayers().forEach(player -> { + if (player.getName().toLowerCase().startsWith(args[0].toLowerCase())) { + completions.add(player.getName()); + } + }); + } + return completions; + } +} diff --git a/src/main/java/com/bitnix/dirtspy/DirtSpyPlugin.java b/src/main/java/com/bitnix/dirtspy/DirtSpyPlugin.java new file mode 100644 index 0000000..807b2b4 --- /dev/null +++ b/src/main/java/com/bitnix/dirtspy/DirtSpyPlugin.java @@ -0,0 +1,93 @@ +package com.bitnix.dirtspy; + +import org.bukkit.Bukkit; +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.java.JavaPlugin; + +public class DirtSpyPlugin extends JavaPlugin { + + private PlayerDataManager playerDataManager; + private SaveTask saveTask; + private ClientBrandListener clientBrandListener; + + @Override + public void onEnable() { + saveDefaultConfig(); + + this.playerDataManager = new PlayerDataManager(this); + this.playerDataManager.load(); + + DirtSpyCommand commandExecutor = new DirtSpyCommand(this); + PluginCommand command = getCommand("dirtspy"); + if (command != null) { + command.setExecutor(commandExecutor); + command.setTabCompleter(commandExecutor); + } else { + getLogger().severe("Command 'dirtspy' is missing from plugin.yml"); + } + + Bukkit.getPluginManager().registerEvents(new PlayerListener(this), this); + + this.clientBrandListener = new ClientBrandListener(this); + getServer().getMessenger().registerIncomingPluginChannel(this, "minecraft:brand", clientBrandListener); + + long intervalSeconds = getConfig().getLong("settings.save-interval-seconds", 300L); + if (intervalSeconds > 0) { + this.saveTask = new SaveTask(this); + this.saveTask.runTaskTimer(this, intervalSeconds * 20L, intervalSeconds * 20L); + } + + getLogger().info("DirtSpy enabled."); + } + + @Override + public void onDisable() { + if (saveTask != null) { + saveTask.cancel(); + saveTask = null; + } + + if (clientBrandListener != null) { + getServer().getMessenger().unregisterIncomingPluginChannel(this, "minecraft:brand", clientBrandListener); + clientBrandListener = null; + } + + if (getConfig().getBoolean("settings.save-on-disable", true) && playerDataManager != null) { + playerDataManager.save(); + } + + if (playerDataManager != null) { + playerDataManager.clear(); + playerDataManager = null; + } + + getLogger().info("DirtSpy disabled."); + } + + public PlayerDataManager getPlayerDataManager() { + return playerDataManager; + } + + public String color(String input) { + return input == null ? "" : input.replace("&", "ยง"); + } + + public String message(String path) { + return color(getConfig().getString(path, "")); + } + + public void reloadPlugin() { + reloadConfig(); + + if (saveTask != null) { + saveTask.cancel(); + saveTask = null; + } + + long intervalSeconds = getConfig().getLong("settings.save-interval-seconds", 300L); + if (intervalSeconds > 0) { + this.saveTask = new SaveTask(this); + this.saveTask.runTaskTimer(this, intervalSeconds * 20L, intervalSeconds * 20L); + } + } +} diff --git a/src/main/java/com/bitnix/dirtspy/PlayerDataManager.java b/src/main/java/com/bitnix/dirtspy/PlayerDataManager.java new file mode 100644 index 0000000..8d58ead --- /dev/null +++ b/src/main/java/com/bitnix/dirtspy/PlayerDataManager.java @@ -0,0 +1,146 @@ +package com.bitnix.dirtspy; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class PlayerDataManager { + + private final DirtSpyPlugin plugin; + private final File dataFile; + private final Map records = new HashMap<>(); + + public PlayerDataManager(DirtSpyPlugin plugin) { + this.plugin = plugin; + this.dataFile = new File(plugin.getDataFolder(), "players.yml"); + } + + public void load() { + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + if (!dataFile.exists()) { + try { + dataFile.createNewFile(); + } catch (IOException e) { + plugin.getLogger().severe("Could not create players.yml: " + e.getMessage()); + } + } + + YamlConfiguration config = YamlConfiguration.loadConfiguration(dataFile); + ConfigurationSection playersSection = config.getConfigurationSection("players"); + if (playersSection == null) { + return; + } + + for (String key : playersSection.getKeys(false)) { + try { + UUID uuid = UUID.fromString(key); + ConfigurationSection section = playersSection.getConfigurationSection(key); + if (section == null) { + continue; + } + + PlayerRecord record = new PlayerRecord(uuid); + record.setName(section.getString("name", "unknown")); + record.setFirstSeen(section.getLong("first-seen", 0L)); + record.setLastSeen(section.getLong("last-seen", 0L)); + record.setJoinCount(section.getInt("join-count", 0)); + record.setCurrentIp(section.getString("current-ip", "unknown")); + record.setLastIp(section.getString("last-ip", "unknown")); + record.setLocale(section.getString("locale", "unknown")); + record.setClientBrand(section.getString("client-brand", "unknown")); + record.setClientViewDistance(section.getInt("client-view-distance", -1)); + record.setLastWorld(section.getString("last-world", "unknown")); + record.setLastGamemode(section.getString("last-gamemode", "unknown")); + record.setLastJoinTime(section.getLong("last-join-time", 0L)); + record.setLastQuitTime(section.getLong("last-quit-time", 0L)); + record.setTotalTrackedPlaytimeSeconds(section.getLong("total-tracked-playtime-seconds", 0L)); + record.setOnline(section.getBoolean("online", false)); + + records.put(uuid, record); + } catch (IllegalArgumentException ex) { + plugin.getLogger().warning("Skipping invalid UUID in players.yml: " + key); + } + } + } + + public void save() { + YamlConfiguration config = new YamlConfiguration(); + + for (Map.Entry entry : records.entrySet()) { + String base = "players." + entry.getKey(); + PlayerRecord record = entry.getValue(); + + config.set(base + ".name", record.getName()); + config.set(base + ".first-seen", record.getFirstSeen()); + config.set(base + ".last-seen", record.getLastSeen()); + config.set(base + ".join-count", record.getJoinCount()); + config.set(base + ".current-ip", record.getCurrentIp()); + config.set(base + ".last-ip", record.getLastIp()); + config.set(base + ".locale", record.getLocale()); + config.set(base + ".client-brand", record.getClientBrand()); + config.set(base + ".client-view-distance", record.getClientViewDistance()); + config.set(base + ".last-world", record.getLastWorld()); + config.set(base + ".last-gamemode", record.getLastGamemode()); + config.set(base + ".last-join-time", record.getLastJoinTime()); + config.set(base + ".last-quit-time", record.getLastQuitTime()); + config.set(base + ".total-tracked-playtime-seconds", record.getTotalTrackedPlaytimeSeconds()); + config.set(base + ".online", record.isOnline()); + } + + try { + config.save(dataFile); + } catch (IOException e) { + plugin.getLogger().severe("Could not save players.yml: " + e.getMessage()); + } + } + + public PlayerRecord getOrCreate(UUID uuid) { + return records.computeIfAbsent(uuid, PlayerRecord::new); + } + + public PlayerRecord getByName(String name) { + for (PlayerRecord record : records.values()) { + if (record.getName() != null && record.getName().equalsIgnoreCase(name)) { + return record; + } + } + return null; + } + + public List getAltNamesOnIp(String ip, UUID exclude) { + List matching = new ArrayList<>(); + for (Map.Entry entry : records.entrySet()) { + if (entry.getKey().equals(exclude)) { + continue; + } + + PlayerRecord record = entry.getValue(); + if (record.getLastIp() != null && record.getLastIp().equalsIgnoreCase(ip)) { + matching.add(record); + } + } + + matching.sort(Comparator.comparing(record -> record.getName() == null ? "" : record.getName(), String.CASE_INSENSITIVE_ORDER)); + + List names = new ArrayList<>(); + for (PlayerRecord record : matching) { + names.add(record.getName() == null ? "unknown" : record.getName()); + } + return names; + } + + public void clear() { + records.clear(); + } +} diff --git a/src/main/java/com/bitnix/dirtspy/PlayerListener.java b/src/main/java/com/bitnix/dirtspy/PlayerListener.java new file mode 100644 index 0000000..00c74c0 --- /dev/null +++ b/src/main/java/com/bitnix/dirtspy/PlayerListener.java @@ -0,0 +1,130 @@ +package com.bitnix.dirtspy; + +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerLocaleChangeEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.net.InetSocketAddress; + +public class PlayerListener implements Listener { + + private final DirtSpyPlugin plugin; + + public PlayerListener(DirtSpyPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + PlayerRecord record = plugin.getPlayerDataManager().getOrCreate(player.getUniqueId()); + + long now = System.currentTimeMillis(); + + record.setName(player.getName()); + if (record.getFirstSeen() <= 0L) { + record.setFirstSeen(now); + } + record.setLastSeen(now); + record.setJoinCount(record.getJoinCount() + 1); + record.setLastJoinTime(now); + record.setOnline(true); + + if (plugin.getConfig().getBoolean("settings.track-ip-address", true)) { + InetSocketAddress address = player.getAddress(); + String ip = (address != null && address.getAddress() != null) + ? address.getAddress().getHostAddress() + : "unknown"; + record.setCurrentIp(ip); + record.setLastIp(ip); + } + + if (plugin.getConfig().getBoolean("settings.track-locale", true)) { + try { + record.setLocale(player.locale().toString()); + } catch (Throwable ignored) { + record.setLocale("unknown"); + } + } + + if (plugin.getConfig().getBoolean("settings.track-client-view-distance", true)) { + try { + record.setClientViewDistance(player.getClientViewDistance()); + } catch (Throwable ignored) { + record.setClientViewDistance(-1); + } + } + + try { + record.setLastWorld(player.getWorld().getName()); + } catch (Throwable ignored) { + record.setLastWorld("unknown"); + } + + GameMode gameMode = player.getGameMode(); + record.setLastGamemode(gameMode == null ? "unknown" : gameMode.name()); + + if (plugin.getConfig().getBoolean("settings.log-joins-to-console", true)) { + plugin.getLogger().info("Tracked join for " + player.getName() + + " | IP=" + record.getLastIp() + + " | locale=" + record.getLocale() + + " | vd=" + record.getClientViewDistance()); + } + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + PlayerRecord record = plugin.getPlayerDataManager().getOrCreate(player.getUniqueId()); + + long now = System.currentTimeMillis(); + + record.setName(player.getName()); + record.setLastSeen(now); + record.setLastQuitTime(now); + record.setOnline(false); + + if (record.getLastJoinTime() > 0L && now >= record.getLastJoinTime()) { + long seconds = (now - record.getLastJoinTime()) / 1000L; + record.setTotalTrackedPlaytimeSeconds(record.getTotalTrackedPlaytimeSeconds() + seconds); + } + + record.setCurrentIp("offline"); + + try { + record.setLastWorld(player.getWorld().getName()); + } catch (Throwable ignored) { + record.setLastWorld("unknown"); + } + + GameMode gameMode = player.getGameMode(); + record.setLastGamemode(gameMode == null ? "unknown" : gameMode.name()); + } + + @EventHandler + public void onLocaleChange(PlayerLocaleChangeEvent event) { + if (!plugin.getConfig().getBoolean("settings.track-locale", true)) { + return; + } + + PlayerRecord record = plugin.getPlayerDataManager().getOrCreate(event.getPlayer().getUniqueId()); + record.setLocale(String.valueOf(event.locale())); + record.setLastSeen(System.currentTimeMillis()); + } + + @EventHandler + public void onWorldChange(PlayerChangedWorldEvent event) { + Player player = event.getPlayer(); + PlayerRecord record = plugin.getPlayerDataManager().getOrCreate(player.getUniqueId()); + record.setLastWorld(player.getWorld().getName()); + record.setLastSeen(System.currentTimeMillis()); + + GameMode gameMode = player.getGameMode(); + record.setLastGamemode(gameMode == null ? "unknown" : gameMode.name()); + } +} diff --git a/src/main/java/com/bitnix/dirtspy/PlayerRecord.java b/src/main/java/com/bitnix/dirtspy/PlayerRecord.java new file mode 100644 index 0000000..287e512 --- /dev/null +++ b/src/main/java/com/bitnix/dirtspy/PlayerRecord.java @@ -0,0 +1,165 @@ +package com.bitnix.dirtspy; + +import java.util.UUID; + +public class PlayerRecord { + + private final UUID uuid; + private String name; + private long firstSeen; + private long lastSeen; + private int joinCount; + private String currentIp; + private String lastIp; + private String locale; + private String clientBrand; + private int clientViewDistance; + private String lastWorld; + private String lastGamemode; + private long lastJoinTime; + private long lastQuitTime; + private long totalTrackedPlaytimeSeconds; + private boolean online; + + public PlayerRecord(UUID uuid) { + this.uuid = uuid; + this.firstSeen = 0L; + this.lastSeen = 0L; + this.joinCount = 0; + this.currentIp = "unknown"; + this.lastIp = "unknown"; + this.locale = "unknown"; + this.clientBrand = "unknown"; + this.clientViewDistance = -1; + this.lastWorld = "unknown"; + this.lastGamemode = "unknown"; + this.lastJoinTime = 0L; + this.lastQuitTime = 0L; + this.totalTrackedPlaytimeSeconds = 0L; + this.online = false; + } + + public UUID getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getFirstSeen() { + return firstSeen; + } + + public void setFirstSeen(long firstSeen) { + this.firstSeen = firstSeen; + } + + public long getLastSeen() { + return lastSeen; + } + + public void setLastSeen(long lastSeen) { + this.lastSeen = lastSeen; + } + + public int getJoinCount() { + return joinCount; + } + + public void setJoinCount(int joinCount) { + this.joinCount = joinCount; + } + + public String getCurrentIp() { + return currentIp; + } + + public void setCurrentIp(String currentIp) { + this.currentIp = currentIp; + } + + public String getLastIp() { + return lastIp; + } + + public void setLastIp(String lastIp) { + this.lastIp = lastIp; + } + + public String getLocale() { + return locale; + } + + public void setLocale(String locale) { + this.locale = locale; + } + + public String getClientBrand() { + return clientBrand; + } + + public void setClientBrand(String clientBrand) { + this.clientBrand = clientBrand; + } + + public int getClientViewDistance() { + return clientViewDistance; + } + + public void setClientViewDistance(int clientViewDistance) { + this.clientViewDistance = clientViewDistance; + } + + public String getLastWorld() { + return lastWorld; + } + + public void setLastWorld(String lastWorld) { + this.lastWorld = lastWorld; + } + + public String getLastGamemode() { + return lastGamemode; + } + + public void setLastGamemode(String lastGamemode) { + this.lastGamemode = lastGamemode; + } + + public long getLastJoinTime() { + return lastJoinTime; + } + + public void setLastJoinTime(long lastJoinTime) { + this.lastJoinTime = lastJoinTime; + } + + public long getLastQuitTime() { + return lastQuitTime; + } + + public void setLastQuitTime(long lastQuitTime) { + this.lastQuitTime = lastQuitTime; + } + + public long getTotalTrackedPlaytimeSeconds() { + return totalTrackedPlaytimeSeconds; + } + + public void setTotalTrackedPlaytimeSeconds(long totalTrackedPlaytimeSeconds) { + this.totalTrackedPlaytimeSeconds = totalTrackedPlaytimeSeconds; + } + + public boolean isOnline() { + return online; + } + + public void setOnline(boolean online) { + this.online = online; + } +} diff --git a/src/main/java/com/bitnix/dirtspy/SaveTask.java b/src/main/java/com/bitnix/dirtspy/SaveTask.java new file mode 100644 index 0000000..476406b --- /dev/null +++ b/src/main/java/com/bitnix/dirtspy/SaveTask.java @@ -0,0 +1,19 @@ +package com.bitnix.dirtspy; + +import org.bukkit.scheduler.BukkitRunnable; + +public class SaveTask extends BukkitRunnable { + + private final DirtSpyPlugin plugin; + + public SaveTask(DirtSpyPlugin plugin) { + this.plugin = plugin; + } + + @Override + public void run() { + if (plugin.getPlayerDataManager() != null) { + plugin.getPlayerDataManager().save(); + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..90b36cc --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,35 @@ +settings: + save-on-disable: true + save-interval-seconds: 300 + track-ip-address: true + track-locale: true + track-client-view-distance: true + track-client-brand: true + log-joins-to-console: true + +messages: + prefix: "&8[&6DirtSpy&8] " + no-permission: "&cYou do not have permission." + player-only: "&cOnly players can use this command." + usage: "&eUsage: /dirtspy " + reloaded: "&aDirtSpy config reloaded." + player-not-found: "&cPlayer not found." + header: "&6DirtSpy report for &e%player%" + line-name: "&7Name: &f%value%" + line-uuid: "&7UUID: &f%value%" + line-first-seen: "&7First Seen: &f%value%" + line-last-seen: "&7Last Seen: &f%value%" + line-joins: "&7Join Count: &f%value%" + line-current-ip: "&7Current IP: &f%value%" + line-last-ip: "&7Last IP: &f%value%" + line-locale: "&7Locale: &f%value%" + line-brand: "&7Client Brand: &f%value%" + line-view-distance: "&7Client View Distance: &f%value%" + line-last-world: "&7Last World: &f%value%" + line-last-gamemode: "&7Last Gamemode: &f%value%" + line-last-join: "&7Last Join: &f%value%" + line-last-quit: "&7Last Quit: &f%value%" + line-total-playtime: "&7Tracked Playtime: &f%value% sec" + line-alt-count: "&7Accounts on Last IP: &f%value%" + line-alts: "&7Alt Names: &f%value%" + line-online: "&7Online: &f%value%" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..47aefa6 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,25 @@ +name: DirtSpy +version: 1.0-SNAPSHOT +main: com.bitnix.dirtspy.DirtSpyPlugin +api-version: '1.21' +author: bitnix +description: Collects realistic client, connection, and behavior stats for admin review. + +commands: + dirtspy: + description: View DirtSpy info and reload the plugin + usage: /dirtspy + permission: dirtspy.use + +permissions: + dirtspy.use: + description: Allows use of DirtSpy commands + default: op + + dirtspy.reload: + description: Allows reloading DirtSpy config + default: op + + dirtspy.view: + description: Allows viewing collected DirtSpy data + default: op diff --git a/target/DirtSpy-1.0-SNAPSHOT.jar b/target/DirtSpy-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..9c0038b Binary files /dev/null and b/target/DirtSpy-1.0-SNAPSHOT.jar differ diff --git a/target/classes/com/bitnix/dirtspy/ClientBrandListener$VarIntResult.class b/target/classes/com/bitnix/dirtspy/ClientBrandListener$VarIntResult.class new file mode 100644 index 0000000..9333633 Binary files /dev/null and b/target/classes/com/bitnix/dirtspy/ClientBrandListener$VarIntResult.class differ diff --git a/target/classes/com/bitnix/dirtspy/ClientBrandListener.class b/target/classes/com/bitnix/dirtspy/ClientBrandListener.class new file mode 100644 index 0000000..6e74ab6 Binary files /dev/null and b/target/classes/com/bitnix/dirtspy/ClientBrandListener.class differ diff --git a/target/classes/com/bitnix/dirtspy/DirtSpyCommand.class b/target/classes/com/bitnix/dirtspy/DirtSpyCommand.class new file mode 100644 index 0000000..b4342ce Binary files /dev/null and b/target/classes/com/bitnix/dirtspy/DirtSpyCommand.class differ diff --git a/target/classes/com/bitnix/dirtspy/DirtSpyPlugin.class b/target/classes/com/bitnix/dirtspy/DirtSpyPlugin.class new file mode 100644 index 0000000..70e5fd4 Binary files /dev/null and b/target/classes/com/bitnix/dirtspy/DirtSpyPlugin.class differ diff --git a/target/classes/com/bitnix/dirtspy/PlayerDataManager.class b/target/classes/com/bitnix/dirtspy/PlayerDataManager.class new file mode 100644 index 0000000..49ca177 Binary files /dev/null and b/target/classes/com/bitnix/dirtspy/PlayerDataManager.class differ diff --git a/target/classes/com/bitnix/dirtspy/PlayerListener.class b/target/classes/com/bitnix/dirtspy/PlayerListener.class new file mode 100644 index 0000000..c3ca817 Binary files /dev/null and b/target/classes/com/bitnix/dirtspy/PlayerListener.class differ diff --git a/target/classes/com/bitnix/dirtspy/PlayerRecord.class b/target/classes/com/bitnix/dirtspy/PlayerRecord.class new file mode 100644 index 0000000..b231cab Binary files /dev/null and b/target/classes/com/bitnix/dirtspy/PlayerRecord.class differ diff --git a/target/classes/com/bitnix/dirtspy/SaveTask.class b/target/classes/com/bitnix/dirtspy/SaveTask.class new file mode 100644 index 0000000..9688972 Binary files /dev/null and b/target/classes/com/bitnix/dirtspy/SaveTask.class differ diff --git a/target/classes/config.yml b/target/classes/config.yml new file mode 100644 index 0000000..90b36cc --- /dev/null +++ b/target/classes/config.yml @@ -0,0 +1,35 @@ +settings: + save-on-disable: true + save-interval-seconds: 300 + track-ip-address: true + track-locale: true + track-client-view-distance: true + track-client-brand: true + log-joins-to-console: true + +messages: + prefix: "&8[&6DirtSpy&8] " + no-permission: "&cYou do not have permission." + player-only: "&cOnly players can use this command." + usage: "&eUsage: /dirtspy " + reloaded: "&aDirtSpy config reloaded." + player-not-found: "&cPlayer not found." + header: "&6DirtSpy report for &e%player%" + line-name: "&7Name: &f%value%" + line-uuid: "&7UUID: &f%value%" + line-first-seen: "&7First Seen: &f%value%" + line-last-seen: "&7Last Seen: &f%value%" + line-joins: "&7Join Count: &f%value%" + line-current-ip: "&7Current IP: &f%value%" + line-last-ip: "&7Last IP: &f%value%" + line-locale: "&7Locale: &f%value%" + line-brand: "&7Client Brand: &f%value%" + line-view-distance: "&7Client View Distance: &f%value%" + line-last-world: "&7Last World: &f%value%" + line-last-gamemode: "&7Last Gamemode: &f%value%" + line-last-join: "&7Last Join: &f%value%" + line-last-quit: "&7Last Quit: &f%value%" + line-total-playtime: "&7Tracked Playtime: &f%value% sec" + line-alt-count: "&7Accounts on Last IP: &f%value%" + line-alts: "&7Alt Names: &f%value%" + line-online: "&7Online: &f%value%" diff --git a/target/classes/plugin.yml b/target/classes/plugin.yml new file mode 100644 index 0000000..47aefa6 --- /dev/null +++ b/target/classes/plugin.yml @@ -0,0 +1,25 @@ +name: DirtSpy +version: 1.0-SNAPSHOT +main: com.bitnix.dirtspy.DirtSpyPlugin +api-version: '1.21' +author: bitnix +description: Collects realistic client, connection, and behavior stats for admin review. + +commands: + dirtspy: + description: View DirtSpy info and reload the plugin + usage: /dirtspy + permission: dirtspy.use + +permissions: + dirtspy.use: + description: Allows use of DirtSpy commands + default: op + + dirtspy.reload: + description: Allows reloading DirtSpy config + default: op + + dirtspy.view: + description: Allows viewing collected DirtSpy data + default: op diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..65ed439 --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,5 @@ +#Generated by Maven +#Sat Jun 13 16:22:51 EDT 2026 +artifactId=DirtSpy +groupId=com.bitnix +version=1.0-SNAPSHOT diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..e38c818 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,8 @@ +com/bitnix/dirtspy/DirtSpyCommand.class +com/bitnix/dirtspy/ClientBrandListener$VarIntResult.class +com/bitnix/dirtspy/PlayerRecord.class +com/bitnix/dirtspy/DirtSpyPlugin.class +com/bitnix/dirtspy/PlayerListener.class +com/bitnix/dirtspy/ClientBrandListener.class +com/bitnix/dirtspy/SaveTask.class +com/bitnix/dirtspy/PlayerDataManager.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..b4bb862 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,7 @@ +/home/bitnix/Desktop/DirtSpy/src/main/java/com/bitnix/dirtspy/ClientBrandListener.java +/home/bitnix/Desktop/DirtSpy/src/main/java/com/bitnix/dirtspy/DirtSpyCommand.java +/home/bitnix/Desktop/DirtSpy/src/main/java/com/bitnix/dirtspy/DirtSpyPlugin.java +/home/bitnix/Desktop/DirtSpy/src/main/java/com/bitnix/dirtspy/PlayerDataManager.java +/home/bitnix/Desktop/DirtSpy/src/main/java/com/bitnix/dirtspy/PlayerListener.java +/home/bitnix/Desktop/DirtSpy/src/main/java/com/bitnix/dirtspy/PlayerRecord.java +/home/bitnix/Desktop/DirtSpy/src/main/java/com/bitnix/dirtspy/SaveTask.java