added
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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%"
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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%"
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user