This commit is contained in:
2026-06-20 12:05:20 -04:00
commit 88cf021c3b
25 changed files with 984 additions and 0 deletions
View File
+48
View File
@@ -0,0 +1,48 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bitnix</groupId>
<artifactId>DirtSpy</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>DirtSpy</name>
<description>Admin review plugin for collecting realistic player/client/network stats on Paper.</description>
<properties>
<java.version>21</java.version>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -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;
}
}
}
@@ -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<String> 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<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
List<String> 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;
}
}
@@ -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);
}
}
}
@@ -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<UUID, PlayerRecord> 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<UUID, PlayerRecord> 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<String> getAltNamesOnIp(String ip, UUID exclude) {
List<PlayerRecord> matching = new ArrayList<>();
for (Map.Entry<UUID, PlayerRecord> 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<String> names = new ArrayList<>();
for (PlayerRecord record : matching) {
names.add(record.getName() == null ? "unknown" : record.getName());
}
return names;
}
public void clear() {
records.clear();
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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();
}
}
}
+35
View File
@@ -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 <player|reload>"
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%"
+25
View File
@@ -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 <player|reload>
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
Binary file not shown.
Binary file not shown.
Binary file not shown.
+35
View File
@@ -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 <player|reload>"
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%"
+25
View File
@@ -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 <player|reload>
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
+5
View File
@@ -0,0 +1,5 @@
#Generated by Maven
#Sat Jun 13 16:22:51 EDT 2026
artifactId=DirtSpy
groupId=com.bitnix
version=1.0-SNAPSHOT
@@ -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
@@ -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