new file: .gitignore
new file: README.md new file: database/schema.sql new file: paper-plugin/pom.xml new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/PaperLoggerPlugin.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/commands/MCLoggerCommand.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/database/DatabaseManager.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/BlockListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/EntityListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/InventoryListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/LuckPermsListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerChatCommandListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerDeathListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerMiscListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerSessionListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/WorldListener.java new file: paper-plugin/src/main/resources/config.yml new file: paper-plugin/src/main/resources/plugin.yml new file: paper-plugin/target/classes/config.yml new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/PaperLoggerPlugin.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand$RsConsumer.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager$ThrowingRunnable.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/BlockListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/EntityListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/InventoryListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/LuckPermsListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerChatCommandListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerDeathListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerMiscListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerSessionListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/WorldListener.class new file: paper-plugin/target/classes/plugin.yml new file: paper-plugin/target/maven-archiver/pom.properties new file: paper-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file: paper-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file: paper-plugin/target/mclogger-paper-1.0.0.jar new file: paper-plugin/target/original-mclogger-paper-1.0.0.jar new file: velocity-plugin/pom.xml new file: velocity-plugin/src/main/java/de/simolzimol/mclogger/velocity/VelocityLoggerPlugin.java new file: velocity-plugin/src/main/java/de/simolzimol/mclogger/velocity/database/VelocityDatabaseManager.java new file: velocity-plugin/src/main/java/de/simolzimol/mclogger/velocity/listeners/VelocityEventListener.java new file: velocity-plugin/src/main/resources/velocity-config.yml new file: velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/VelocityLoggerPlugin.class new file: velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/database/VelocityDatabaseManager$ThrowingRunnable.class new file: velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/database/VelocityDatabaseManager.class new file: velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/listeners/VelocityEventListener.class new file: velocity-plugin/target/classes/velocity-config.yml new file: velocity-plugin/target/classes/velocity-plugin.json new file: velocity-plugin/target/maven-archiver/pom.properties new file: velocity-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file: velocity-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file: velocity-plugin/target/mclogger-velocity-1.0.0.jar new file: velocity-plugin/target/original-mclogger-velocity-1.0.0.jar new file: web/Dockerfile new file: web/app.py new file: web/blueprints/__init__.py new file: web/blueprints/auth.py new file: web/blueprints/group_admin.py new file: web/blueprints/panel.py new file: web/blueprints/site_admin.py new file: web/config.py new file: web/crypto.py new file: web/docker-compose.yml new file: web/panel_db.py new file: web/requirements.txt new file: web/static/css/style.css new file: web/static/js/main.js new file: web/templates/_pagination.html new file: web/templates/admin/base.html new file: web/templates/admin/dashboard.html new file: web/templates/admin/group_edit.html new file: web/templates/admin/group_members.html new file: web/templates/admin/groups.html new file: web/templates/admin/user_edit.html new file: web/templates/admin/users.html new file: web/templates/auth/admin_login.html new file: web/templates/auth/login.html new file: web/templates/base.html new file: web/templates/blocks.html new file: web/templates/chat.html new file: web/templates/commands.html new file: web/templates/dashboard.html new file: web/templates/deaths.html new file: web/templates/group_admin/base.html new file: web/templates/group_admin/dashboard.html new file: web/templates/group_admin/database.html new file: web/templates/group_admin/member_edit.html new file: web/templates/group_admin/members.html new file: web/templates/login.html new file: web/templates/panel/blocks.html new file: web/templates/panel/chat.html new file: web/templates/panel/commands.html new file: web/templates/panel/dashboard.html new file: web/templates/panel/deaths.html new file: web/templates/panel/no_db.html new file: web/templates/panel/perms.html new file: web/templates/panel/player_detail.html new file: web/templates/panel/players.html new file: web/templates/panel/proxy.html new file: web/templates/panel/server_events.html new file: web/templates/panel/sessions.html new file: web/templates/perms.html new file: web/templates/player_detail.html new file: web/templates/players.html new file: web/templates/proxy.html new file: web/templates/server_events.html new file: web/templates/sessions.html
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package de.simolzimol.mclogger.paper;
|
||||
|
||||
import de.simolzimol.mclogger.paper.commands.MCLoggerCommand;
|
||||
import de.simolzimol.mclogger.paper.database.DatabaseManager;
|
||||
import de.simolzimol.mclogger.paper.listeners.*;
|
||||
import net.luckperms.api.LuckPerms;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* MCLogger – Paper Plugin entry point
|
||||
*
|
||||
* @author SimolZimol
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class PaperLoggerPlugin extends JavaPlugin {
|
||||
|
||||
private static PaperLoggerPlugin instance;
|
||||
private DatabaseManager db;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
|
||||
saveDefaultConfig();
|
||||
|
||||
// Establish database connection
|
||||
db = new DatabaseManager(this);
|
||||
if (!db.connect()) {
|
||||
getLogger().severe("[MCLogger] Could not connect to database – disabling plugin.");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Register plugin messaging channel towards the Velocity proxy
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, "mclogger:logged");
|
||||
|
||||
// Register all listeners
|
||||
registerListeners();
|
||||
|
||||
// LuckPerms listener (soft-depend)
|
||||
registerLuckPermsListener();
|
||||
|
||||
// Register /mclogger command
|
||||
MCLoggerCommand cmdHandler = new MCLoggerCommand(this);
|
||||
PluginCommand cmd = getCommand("mclogger");
|
||||
if (cmd != null) {
|
||||
cmd.setExecutor(cmdHandler);
|
||||
cmd.setTabCompleter(cmdHandler);
|
||||
}
|
||||
|
||||
// Server-Start Event loggen
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("version", getServer().getVersion());
|
||||
details.put("bukkit_version", getServer().getBukkitVersion());
|
||||
details.put("online_mode", getServer().getOnlineMode());
|
||||
details.put("max_players", getServer().getMaxPlayers());
|
||||
db.insertServerEvent("server_start", "Server started", details);
|
||||
|
||||
getLogger().info("[MCLogger] started successfully! Server: " + db.getServerName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (db != null) {
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("online_players", getServer().getOnlinePlayers().size());
|
||||
db.insertServerEvent("server_stop", "Server stopping", details);
|
||||
// Short delay to let async tasks finish
|
||||
try { Thread.sleep(500); } catch (InterruptedException ignored) {}
|
||||
db.disconnect();
|
||||
}
|
||||
getLogger().info("[MCLogger] disabled.");
|
||||
}
|
||||
|
||||
private void registerListeners() {
|
||||
PluginManager pm = getServer().getPluginManager();
|
||||
pm.registerEvents(new PlayerSessionListener(this), this);
|
||||
pm.registerEvents(new PlayerChatCommandListener(this), this);
|
||||
pm.registerEvents(new PlayerDeathListener(this), this);
|
||||
pm.registerEvents(new PlayerMiscListener(this), this);
|
||||
pm.registerEvents(new BlockListener(this), this);
|
||||
pm.registerEvents(new EntityListener(this), this);
|
||||
pm.registerEvents(new InventoryListener(this), this);
|
||||
pm.registerEvents(new WorldListener(this), this);
|
||||
getLogger().info("[MCLogger] All listeners registered.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the LuckPerms listener only if LuckPerms is actually loaded (softdepend).
|
||||
*/
|
||||
private void registerLuckPermsListener() {
|
||||
if (getServer().getPluginManager().getPlugin("LuckPerms") == null) {
|
||||
getLogger().info("[MCLogger] LuckPerms not found – permission logging disabled.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
RegisteredServiceProvider<LuckPerms> provider =
|
||||
getServer().getServicesManager().getRegistration(LuckPerms.class);
|
||||
if (provider != null) {
|
||||
new LuckPermsListener(this, provider.getProvider());
|
||||
} else {
|
||||
getLogger().warning("[MCLogger] LuckPerms service not available.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().log(Level.WARNING, "[MCLogger] Error registering LuckPerms listener.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static PaperLoggerPlugin getInstance() { return instance; }
|
||||
public DatabaseManager getDb() { return db; }
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
package de.simolzimol.mclogger.paper.commands;
|
||||
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* In-game command class for /mclogger.
|
||||
*
|
||||
* Sub-commands:
|
||||
* /mclogger help – Help (mclogger.use)
|
||||
* /mclogger status – DB status (mclogger.admin)
|
||||
* /mclogger reload – Reload config (mclogger.admin)
|
||||
* /mclogger chat <player> [page] – Chat log (mclogger.view.chat)
|
||||
* /mclogger commands <player> [page] – Command log (mclogger.view.commands)
|
||||
* /mclogger deaths <player> [page] – Death log (mclogger.view.deaths)
|
||||
* /mclogger sessions <player> [page] – Session log (mclogger.view.sessions)
|
||||
* /mclogger blocks <x> <y> <z> [page]– Block log (mclogger.view.blocks)
|
||||
* /mclogger perms player <name> [page] – Perms by target player (mclogger.view.perms)
|
||||
* /mclogger perms actor <name> [page] – Perms by actor (mclogger.view.perms)
|
||||
* /mclogger perms group <name> [page] – Perms by group (mclogger.view.perms)
|
||||
* /mclogger perms all [page] – All perm events (mclogger.view.perms)
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class MCLoggerCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private static final int PAGE_SIZE = 8;
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
|
||||
public MCLoggerCommand(PaperLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Tab-Completion
|
||||
// --------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command cmd,
|
||||
@NotNull String label, String[] args) {
|
||||
if (!sender.hasPermission("mclogger.use")) return List.of();
|
||||
|
||||
if (args.length == 1) {
|
||||
List<String> subs = new ArrayList<>(List.of("help", "status", "reload",
|
||||
"chat", "commands", "deaths", "sessions", "blocks", "perms"));
|
||||
String partial = args[0].toLowerCase();
|
||||
subs.removeIf(s -> !s.startsWith(partial));
|
||||
return subs;
|
||||
}
|
||||
if (args.length == 2 && args[0].equalsIgnoreCase("perms")) {
|
||||
return List.of("player", "actor", "group", "all").stream()
|
||||
.filter(s -> s.startsWith(args[1].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
if (args.length == 3 && args[0].equalsIgnoreCase("perms")
|
||||
&& (args[1].equalsIgnoreCase("player") || args[1].equalsIgnoreCase("actor"))) {
|
||||
return plugin.getServer().getOnlinePlayers().stream()
|
||||
.map(p -> p.getName())
|
||||
.filter(n -> n.toLowerCase().startsWith(args[2].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (args.length == 2) {
|
||||
String sub = args[0].toLowerCase();
|
||||
return switch (sub) {
|
||||
case "chat", "commands", "deaths", "sessions", "perms" ->
|
||||
plugin.getServer().getOnlinePlayers().stream()
|
||||
.map(p -> p.getName())
|
||||
.filter(n -> n.toLowerCase().startsWith(args[1].toLowerCase()))
|
||||
.toList();
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Command handler
|
||||
// --------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd,
|
||||
@NotNull String label, String[] args) {
|
||||
if (!sender.hasPermission("mclogger.use")) {
|
||||
send(sender, NamedTextColor.RED, "You don't have permission to use this command.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.length == 0) {
|
||||
handleHelp(sender);
|
||||
return true;
|
||||
}
|
||||
|
||||
return switch (args[0].toLowerCase()) {
|
||||
case "help" -> { handleHelp(sender); yield true; }
|
||||
case "status" -> { handleStatus(sender); yield true; }
|
||||
case "reload" -> { handleReload(sender); yield true; }
|
||||
case "chat" -> { handleChat(sender, args); yield true; }
|
||||
case "commands" -> { handleCommands(sender, args); yield true; }
|
||||
case "deaths" -> { handleDeaths(sender, args); yield true; }
|
||||
case "sessions" -> { handleSessions(sender, args); yield true; }
|
||||
case "blocks" -> { handleBlocks(sender, args); yield true; }
|
||||
case "perms" -> { handlePerms(sender, args); yield true; }
|
||||
default -> { handleHelp(sender); yield true; }
|
||||
};
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Sub-commands
|
||||
// --------------------------------------------------------
|
||||
|
||||
private void handleHelp(CommandSender s) {
|
||||
sendHeader(s, "MCLogger Help");
|
||||
sendLine(s, "/mcl help", "This help");
|
||||
if (s.hasPermission("mclogger.admin")) {
|
||||
sendLine(s, "/mcl status", "Database status");
|
||||
sendLine(s, "/mcl reload", "Reload configuration");
|
||||
}
|
||||
if (s.hasPermission("mclogger.view.chat"))
|
||||
sendLine(s, "/mcl chat <player> [page]", "Chat history");
|
||||
if (s.hasPermission("mclogger.view.commands"))
|
||||
sendLine(s, "/mcl commands <player> [page]", "Command history");
|
||||
if (s.hasPermission("mclogger.view.deaths"))
|
||||
sendLine(s, "/mcl deaths <player> [page]", "Death history");
|
||||
if (s.hasPermission("mclogger.view.sessions"))
|
||||
sendLine(s, "/mcl sessions <player> [page]", "Session history");
|
||||
if (s.hasPermission("mclogger.view.blocks"))
|
||||
sendLine(s, "/mcl blocks <x> <y> <z> [page]","Block log at position");
|
||||
if (s.hasPermission("mclogger.view.perms"))
|
||||
sendLine(s, "/mcl perms <player> [page]", "LuckPerms changes");
|
||||
}
|
||||
|
||||
private void handleStatus(CommandSender s) {
|
||||
requirePerm(s, "mclogger.admin", () -> {
|
||||
boolean ok = plugin.getDb().isConnected();
|
||||
send(s, ok ? NamedTextColor.GREEN : NamedTextColor.RED,
|
||||
"Database connection: " + (ok ? "✔ CONNECTED" : "✘ DISCONNECTED"));
|
||||
send(s, NamedTextColor.AQUA, "Server: " + plugin.getDb().getServerName()
|
||||
+ " | ID: " + plugin.getDb().getServerId());
|
||||
});
|
||||
}
|
||||
|
||||
private void handleReload(CommandSender s) {
|
||||
requirePerm(s, "mclogger.admin", () -> {
|
||||
plugin.reloadConfig();
|
||||
send(s, NamedTextColor.GREEN, "Configuration reloaded.");
|
||||
});
|
||||
}
|
||||
|
||||
// -- Chat -----------------------------------------------
|
||||
|
||||
private void handleChat(CommandSender s, String[] args) {
|
||||
requirePerm(s, "mclogger.view.chat", () -> {
|
||||
if (args.length < 2) { send(s, NamedTextColor.YELLOW, "Usage: /mcl chat <player> [page]"); return; }
|
||||
String player = args[1];
|
||||
int page = parsePage(args, 2);
|
||||
runQuery(s,
|
||||
"SELECT timestamp, message FROM player_chat " +
|
||||
"WHERE player_name = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?",
|
||||
player, page,
|
||||
rs -> {
|
||||
sendHeader(s, "Chat: " + player + " (page " + page + ")");
|
||||
while (rs.next()) {
|
||||
send(s, NamedTextColor.GRAY, "[" + fmtTs(rs.getString("timestamp")) + "] "
|
||||
+ rs.getString("message"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -- Commands -------------------------------------------
|
||||
|
||||
private void handleCommands(CommandSender s, String[] args) {
|
||||
requirePerm(s, "mclogger.view.commands", () -> {
|
||||
if (args.length < 2) { send(s, NamedTextColor.YELLOW, "Usage: /mcl commands <player> [page]"); return; }
|
||||
String player = args[1];
|
||||
int page = parsePage(args, 2);
|
||||
runQuery(s,
|
||||
"SELECT timestamp, command FROM player_commands " +
|
||||
"WHERE player_name = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?",
|
||||
player, page,
|
||||
rs -> {
|
||||
sendHeader(s, "Commands: " + player + " (page " + page + ")");
|
||||
while (rs.next()) {
|
||||
send(s, NamedTextColor.GRAY, "[" + fmtTs(rs.getString("timestamp")) + "] "
|
||||
+ rs.getString("command"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -- Deaths ---------------------------------------------
|
||||
|
||||
private void handleDeaths(CommandSender s, String[] args) {
|
||||
requirePerm(s, "mclogger.view.deaths", () -> {
|
||||
if (args.length < 2) { send(s, NamedTextColor.YELLOW, "Usage: /mcl deaths <player> [page]"); return; }
|
||||
String player = args[1];
|
||||
int page = parsePage(args, 2);
|
||||
runQuery(s,
|
||||
"SELECT timestamp, death_message, world, x, y, z FROM player_deaths " +
|
||||
"WHERE player_name = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?",
|
||||
player, page,
|
||||
rs -> {
|
||||
sendHeader(s, "Deaths: " + player + " (page " + page + ")");
|
||||
while (rs.next()) {
|
||||
send(s, NamedTextColor.RED,
|
||||
"[" + fmtTs(rs.getString("timestamp")) + "] " + rs.getString("death_message")
|
||||
+ " @ " + rs.getString("world") + " "
|
||||
+ (int)rs.getDouble("x") + "/"
|
||||
+ (int)rs.getDouble("y") + "/"
|
||||
+ (int)rs.getDouble("z"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -- Sessions -------------------------------------------
|
||||
|
||||
private void handleSessions(CommandSender s, String[] args) {
|
||||
requirePerm(s, "mclogger.view.sessions", () -> {
|
||||
if (args.length < 2) { send(s, NamedTextColor.YELLOW, "Usage: /mcl sessions <player> [page]"); return; }
|
||||
String player = args[1];
|
||||
int page = parsePage(args, 2);
|
||||
runQuery(s,
|
||||
"SELECT login_time, logout_time, duration_sec, ip_address, country, server_name FROM player_sessions " +
|
||||
"WHERE player_name = ? ORDER BY login_time DESC LIMIT ? OFFSET ?",
|
||||
player, page,
|
||||
rs -> {
|
||||
sendHeader(s, "Sessions: " + player + " (page " + page + ")");
|
||||
while (rs.next()) {
|
||||
long dur = rs.getLong("duration_sec");
|
||||
String durStr = dur > 0 ? formatDuration(dur) : "ongoing";
|
||||
String country = rs.getString("country");
|
||||
String countryStr = (country != null && !country.isEmpty()) ? " [" + country + "]" : "";
|
||||
send(s, NamedTextColor.AQUA,
|
||||
"[" + fmtTs(rs.getString("login_time")) + "] "
|
||||
+ rs.getString("server_name") + " " + durStr
|
||||
+ " IP: " + rs.getString("ip_address") + countryStr);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -- Blocks ---------------------------------------------
|
||||
|
||||
private void handleBlocks(CommandSender s, String[] args) {
|
||||
requirePerm(s, "mclogger.view.blocks", () -> {
|
||||
if (args.length < 4) { send(s, NamedTextColor.YELLOW, "Usage: /mcl blocks <x> <y> <z> [page]"); return; }
|
||||
int bx, by, bz;
|
||||
try { bx = Integer.parseInt(args[1]); by = Integer.parseInt(args[2]); bz = Integer.parseInt(args[3]); }
|
||||
catch (NumberFormatException ex) { send(s, NamedTextColor.RED, "Coordinates must be integers."); return; }
|
||||
int page = parsePage(args, 4);
|
||||
runQuery(s,
|
||||
"SELECT timestamp, player_name, event_type, block_type FROM block_events " +
|
||||
"WHERE x = ? AND y = ? AND z = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?",
|
||||
bx, by, bz, page,
|
||||
rs -> {
|
||||
sendHeader(s, "Blocks @ " + bx + "/" + by + "/" + bz + " (page " + page + ")");
|
||||
while (rs.next()) {
|
||||
send(s, NamedTextColor.GOLD,
|
||||
"[" + fmtTs(rs.getString("timestamp")) + "] "
|
||||
+ rs.getString("player_name") + " "
|
||||
+ rs.getString("event_type") + " "
|
||||
+ rs.getString("block_type"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -- Perms ----------------------------------------------
|
||||
|
||||
private void handlePerms(CommandSender s, String[] args) {
|
||||
requirePerm(s, "mclogger.view.perms", () -> {
|
||||
if (args.length < 2) {
|
||||
send(s, NamedTextColor.YELLOW,
|
||||
"Usage: /mcl perms <player|actor|group|all> [name] [page]");
|
||||
return;
|
||||
}
|
||||
String type = args[1].toLowerCase();
|
||||
switch (type) {
|
||||
case "all" -> {
|
||||
int page = parsePage(args, 2);
|
||||
runQueryParams(s,
|
||||
"SELECT timestamp, event_type, player_name, actor_name, target_type, action, server_name FROM plugin_events " +
|
||||
"WHERE plugin_name = 'LuckPerms' ORDER BY timestamp DESC LIMIT ? OFFSET ?",
|
||||
List.of(), page,
|
||||
rs -> {
|
||||
sendHeader(s, "All Permission Events (page " + page + ")");
|
||||
while (rs.next()) printPermRow(s, rs);
|
||||
});
|
||||
}
|
||||
case "player" -> {
|
||||
if (args.length < 3) { send(s, NamedTextColor.YELLOW, "Usage: /mcl perms player <name> [page]"); return; }
|
||||
String name = args[2]; int page = parsePage(args, 3);
|
||||
runQueryParams(s,
|
||||
"SELECT timestamp, event_type, player_name, actor_name, target_type, action, server_name FROM plugin_events " +
|
||||
"WHERE plugin_name = 'LuckPerms' AND (player_name = ? OR player_name LIKE ?) ORDER BY timestamp DESC LIMIT ? OFFSET ?",
|
||||
List.of(name, name + "@%"), page,
|
||||
rs -> {
|
||||
sendHeader(s, "Perms for player " + name + " (page " + page + ")");
|
||||
while (rs.next()) printPermRow(s, rs);
|
||||
});
|
||||
}
|
||||
case "actor" -> {
|
||||
if (args.length < 3) { send(s, NamedTextColor.YELLOW, "Usage: /mcl perms actor <name> [page]"); return; }
|
||||
String name = args[2]; int page = parsePage(args, 3);
|
||||
runQueryParams(s,
|
||||
"SELECT timestamp, event_type, player_name, actor_name, target_type, action, server_name FROM plugin_events " +
|
||||
"WHERE plugin_name = 'LuckPerms' AND (actor_name = ? OR actor_name LIKE ?) ORDER BY timestamp DESC LIMIT ? OFFSET ?",
|
||||
List.of(name, name + "@%"), page,
|
||||
rs -> {
|
||||
sendHeader(s, "Perms by actor " + name + " (page " + page + ")");
|
||||
while (rs.next()) printPermRow(s, rs);
|
||||
});
|
||||
}
|
||||
case "group" -> {
|
||||
if (args.length < 3) { send(s, NamedTextColor.YELLOW, "Usage: /mcl perms group <name> [page]"); return; }
|
||||
String name = args[2]; int page = parsePage(args, 3);
|
||||
runQueryParams(s,
|
||||
"SELECT timestamp, event_type, player_name, actor_name, target_type, action, server_name FROM plugin_events " +
|
||||
"WHERE plugin_name = 'LuckPerms' AND target_type = 'group' AND target_id = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?",
|
||||
List.of(name), page,
|
||||
rs -> {
|
||||
sendHeader(s, "Perms for group " + name + " (page " + page + ")");
|
||||
while (rs.next()) printPermRow(s, rs);
|
||||
});
|
||||
}
|
||||
default -> send(s, NamedTextColor.YELLOW,
|
||||
"Usage: /mcl perms <player|actor|group|all> [name] [page]");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void printPermRow(CommandSender s, ResultSet rs) throws SQLException {
|
||||
String srv = rs.getString("server_name");
|
||||
String srvStr = (srv != null && !srv.isEmpty()) ? " [" + srv + "]" : "";
|
||||
send(s, NamedTextColor.LIGHT_PURPLE,
|
||||
"[" + fmtTs(rs.getString("timestamp")) + "]" + srvStr + " "
|
||||
+ rs.getString("actor_name") + " → " + rs.getString("action")
|
||||
+ (rs.getString("player_name") != null ? " (player: " + rs.getString("player_name") + ")" : ""));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// DB helper infrastructure (sync – acceptable for short admin queries)
|
||||
// --------------------------------------------------------
|
||||
|
||||
@FunctionalInterface
|
||||
interface RsConsumer { void accept(ResultSet rs) throws SQLException; }
|
||||
|
||||
/** Executes a paginated query asynchronously (acceptable for admin commands). */
|
||||
private void runQuery(CommandSender s, String sql, String player, int page, RsConsumer consumer) {
|
||||
int offset = (page - 1) * PAGE_SIZE;
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try (Connection con = plugin.getDb().getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(sql)) {
|
||||
ps.setString(1, player);
|
||||
ps.setInt(2, PAGE_SIZE);
|
||||
ps.setInt(3, offset);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
try { consumer.accept(rs); }
|
||||
catch (SQLException ex) { send(s, NamedTextColor.RED, "Database error: " + ex.getMessage()); }
|
||||
});
|
||||
} catch (SQLException ex) {
|
||||
send(s, NamedTextColor.RED, "Database error: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Overload for queries with a variable list of leading params before LIMIT/OFFSET. */
|
||||
private void runQueryParams(CommandSender s, String sql, List<String> params, int page, RsConsumer consumer) {
|
||||
int offset = (page - 1) * PAGE_SIZE;
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try (Connection con = plugin.getDb().getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(sql)) {
|
||||
int i = 1;
|
||||
for (String p : params) ps.setString(i++, p);
|
||||
ps.setInt(i++, PAGE_SIZE);
|
||||
ps.setInt(i, offset);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
try { consumer.accept(rs); }
|
||||
catch (SQLException ex) { send(s, NamedTextColor.RED, "Database error: " + ex.getMessage()); }
|
||||
});
|
||||
} catch (SQLException ex) {
|
||||
send(s, NamedTextColor.RED, "Database error: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Overload for block queries with 3 coordinates. */
|
||||
private void runQuery(CommandSender s, String sql,
|
||||
int bx, int by, int bz, int page, RsConsumer consumer) {
|
||||
int offset = (page - 1) * PAGE_SIZE;
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try (Connection con = plugin.getDb().getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(sql)) {
|
||||
ps.setInt(1, bx);
|
||||
ps.setInt(2, by);
|
||||
ps.setInt(3, bz);
|
||||
ps.setInt(4, PAGE_SIZE);
|
||||
ps.setInt(5, offset);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
try { consumer.accept(rs); }
|
||||
catch (SQLException ex) { send(s, NamedTextColor.RED, "Database error: " + ex.getMessage()); }
|
||||
});
|
||||
} catch (SQLException ex) {
|
||||
send(s, NamedTextColor.RED, "Database error: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Helper methods
|
||||
// --------------------------------------------------------
|
||||
|
||||
private void requirePerm(CommandSender s, String perm, Runnable action) {
|
||||
if (!s.hasPermission(perm)) {
|
||||
send(s, NamedTextColor.RED, "No permission: " + perm);
|
||||
} else {
|
||||
action.run();
|
||||
}
|
||||
}
|
||||
|
||||
private int parsePage(String[] args, int idx) {
|
||||
if (args.length > idx) {
|
||||
try { return Math.max(1, Integer.parseInt(args[idx])); }
|
||||
catch (NumberFormatException ignored) {}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static final DateTimeFormatter EU_TS = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");
|
||||
private static final DateTimeFormatter DB_TS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private String fmtTs(String raw) {
|
||||
if (raw == null) return "-";
|
||||
try {
|
||||
String s = raw.contains(".") ? raw.substring(0, raw.indexOf('.')) : raw;
|
||||
return LocalDateTime.parse(s, DB_TS).format(EU_TS);
|
||||
} catch (Exception e) {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
private String formatDuration(long seconds) {
|
||||
long h = seconds / 3600, m = (seconds % 3600) / 60, sec = seconds % 60;
|
||||
return String.format("%02d:%02d:%02d", h, m, sec);
|
||||
}
|
||||
|
||||
private void sendHeader(CommandSender s, String title) {
|
||||
s.sendMessage(Component.text("══ " + title + " ══")
|
||||
.color(NamedTextColor.GOLD)
|
||||
.decorate(TextDecoration.BOLD));
|
||||
}
|
||||
|
||||
private void sendLine(CommandSender s, String cmd, String desc) {
|
||||
s.sendMessage(Component.text(" " + cmd + " ")
|
||||
.color(NamedTextColor.YELLOW)
|
||||
.append(Component.text("– " + desc).color(NamedTextColor.GRAY)));
|
||||
}
|
||||
|
||||
private void send(CommandSender s, NamedTextColor color, String text) {
|
||||
s.sendMessage(Component.text(text).color(color));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
package de.simolzimol.mclogger.paper.database;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Manages the MariaDB connection pool and provides
|
||||
* helper methods for writing all log events.
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class DatabaseManager {
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
private HikariDataSource dataSource;
|
||||
private String serverName;
|
||||
private int serverId = -1;
|
||||
private static final Gson GSON = new GsonBuilder().create();
|
||||
|
||||
public DatabaseManager(PaperLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Connection Lifecycle
|
||||
// --------------------------------------------------------
|
||||
|
||||
public boolean connect() {
|
||||
FileConfiguration cfg = plugin.getConfig();
|
||||
serverName = cfg.getString("server.name", "default");
|
||||
|
||||
HikariConfig hk = new HikariConfig();
|
||||
hk.setDriverClassName("de.simolzimol.mclogger.lib.mariadb.jdbc.Driver");
|
||||
hk.setJdbcUrl(String.format("jdbc:mariadb://%s:%d/%s?useSSL=%b&autoReconnect=true&characterEncoding=UTF-8",
|
||||
cfg.getString("database.host", "localhost"),
|
||||
cfg.getInt("database.port", 3306),
|
||||
cfg.getString("database.database", "mclogger"),
|
||||
cfg.getBoolean("database.ssl", false)));
|
||||
hk.setUsername(cfg.getString("database.username", "root"));
|
||||
hk.setPassword(cfg.getString("database.password", ""));
|
||||
hk.setMaximumPoolSize(cfg.getInt("database.pool-size", 10));
|
||||
hk.setMinimumIdle(2);
|
||||
hk.setConnectionTimeout(30_000);
|
||||
hk.setIdleTimeout(600_000);
|
||||
hk.setMaxLifetime(1_800_000);
|
||||
hk.setPoolName("MCLogger-Paper");
|
||||
hk.addDataSourceProperty("cachePrepStmts", "true");
|
||||
hk.addDataSourceProperty("prepStmtCacheSize", "250");
|
||||
hk.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
|
||||
|
||||
try {
|
||||
dataSource = new HikariDataSource(hk);
|
||||
initializeTables();
|
||||
registerServer(cfg);
|
||||
plugin.getLogger().info("[MCLogger] Database connected - Server: " + serverName + " (ID: " + serverId + ")");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "[MCLogger] Database connection failed!", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeTables() throws SQLException {
|
||||
String[] ddl = {
|
||||
// servers
|
||||
"CREATE TABLE IF NOT EXISTS servers (" +
|
||||
" id INT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" server_name VARCHAR(100) NOT NULL," +
|
||||
" server_type ENUM('paper','velocity') NOT NULL DEFAULT 'paper'," +
|
||||
" ip_address VARCHAR(45)," +
|
||||
" mc_version VARCHAR(100)," +
|
||||
" first_seen TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" last_seen TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)," +
|
||||
" UNIQUE KEY uq_server_name (server_name)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// players
|
||||
"CREATE TABLE IF NOT EXISTS players (" +
|
||||
" uuid VARCHAR(36) PRIMARY KEY," +
|
||||
" username VARCHAR(16) NOT NULL," +
|
||||
" display_name VARCHAR(64)," +
|
||||
" ip_address VARCHAR(45)," +
|
||||
" locale VARCHAR(20)," +
|
||||
" first_seen TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" last_seen TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)," +
|
||||
" total_playtime_sec BIGINT DEFAULT 0," +
|
||||
" is_op TINYINT(1) DEFAULT 0," +
|
||||
" INDEX idx_username (username)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// player_sessions
|
||||
"CREATE TABLE IF NOT EXISTS player_sessions (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" player_uuid VARCHAR(36) NOT NULL," +
|
||||
" player_name VARCHAR(16) NOT NULL," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" login_time TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" logout_time TIMESTAMP(3) NULL DEFAULT NULL," +
|
||||
" duration_sec INT," +
|
||||
" ip_address VARCHAR(45)," +
|
||||
" country VARCHAR(100)," +
|
||||
" client_version VARCHAR(20)," +
|
||||
" INDEX idx_ps_uuid (player_uuid)," +
|
||||
" INDEX idx_ps_server (server_name)," +
|
||||
" INDEX idx_ps_login (login_time)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// player_chat
|
||||
"CREATE TABLE IF NOT EXISTS player_chat (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" player_uuid VARCHAR(36)," +
|
||||
" player_name VARCHAR(16)," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" world VARCHAR(100)," +
|
||||
" message TEXT NOT NULL," +
|
||||
" channel VARCHAR(50) DEFAULT 'global'," +
|
||||
" INDEX idx_chat_uuid (player_uuid)," +
|
||||
" INDEX idx_chat_timestamp (timestamp)," +
|
||||
" INDEX idx_chat_server (server_name)," +
|
||||
" FULLTEXT INDEX ft_message (message)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// player_commands
|
||||
"CREATE TABLE IF NOT EXISTS player_commands (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" player_uuid VARCHAR(36)," +
|
||||
" player_name VARCHAR(16)," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" world VARCHAR(100)," +
|
||||
" command TEXT NOT NULL," +
|
||||
" x DOUBLE," +
|
||||
" y DOUBLE," +
|
||||
" z DOUBLE," +
|
||||
" INDEX idx_cmd_uuid (player_uuid)," +
|
||||
" INDEX idx_cmd_timestamp (timestamp)," +
|
||||
" INDEX idx_cmd_server (server_name)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// block_events
|
||||
"CREATE TABLE IF NOT EXISTS block_events (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" event_type ENUM('break','place','ignite','burn','explode','fade','grow','dispense') NOT NULL," +
|
||||
" player_uuid VARCHAR(36)," +
|
||||
" player_name VARCHAR(16)," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" world VARCHAR(100) NOT NULL," +
|
||||
" x INT NOT NULL," +
|
||||
" y INT NOT NULL," +
|
||||
" z INT NOT NULL," +
|
||||
" block_type VARCHAR(100) NOT NULL," +
|
||||
" block_data VARCHAR(255)," +
|
||||
" tool VARCHAR(100)," +
|
||||
" is_silk TINYINT(1) DEFAULT 0," +
|
||||
" INDEX idx_be_player (player_uuid)," +
|
||||
" INDEX idx_be_timestamp (timestamp)," +
|
||||
" INDEX idx_be_world (world)," +
|
||||
" INDEX idx_be_type (event_type)," +
|
||||
" INDEX idx_be_server (server_name)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// player_deaths
|
||||
"CREATE TABLE IF NOT EXISTS player_deaths (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" player_uuid VARCHAR(36) NOT NULL," +
|
||||
" player_name VARCHAR(16) NOT NULL," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" world VARCHAR(100)," +
|
||||
" x DOUBLE," +
|
||||
" y DOUBLE," +
|
||||
" z DOUBLE," +
|
||||
" death_message TEXT," +
|
||||
" cause VARCHAR(100)," +
|
||||
" killer_uuid VARCHAR(36)," +
|
||||
" killer_name VARCHAR(100)," +
|
||||
" killer_type VARCHAR(100)," +
|
||||
" exp_level INT," +
|
||||
" items_lost JSON," +
|
||||
" INDEX idx_deaths_uuid (player_uuid)," +
|
||||
" INDEX idx_deaths_timestamp (timestamp)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// entity_events
|
||||
"CREATE TABLE IF NOT EXISTS entity_events (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" event_type ENUM('spawn','death','damage','tame','breed','transform','explode') NOT NULL," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" world VARCHAR(100)," +
|
||||
" x DOUBLE," +
|
||||
" y DOUBLE," +
|
||||
" z DOUBLE," +
|
||||
" entity_type VARCHAR(100) NOT NULL," +
|
||||
" entity_uuid VARCHAR(36)," +
|
||||
" entity_name VARCHAR(100)," +
|
||||
" player_uuid VARCHAR(36)," +
|
||||
" player_name VARCHAR(16)," +
|
||||
" cause VARCHAR(100)," +
|
||||
" damage DOUBLE," +
|
||||
" details JSON," +
|
||||
" INDEX idx_ee_timestamp (timestamp)," +
|
||||
" INDEX idx_ee_type (event_type)," +
|
||||
" INDEX idx_ee_entity (entity_type)," +
|
||||
" INDEX idx_ee_player (player_uuid)," +
|
||||
" INDEX idx_ee_server (server_name)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// player_teleports
|
||||
"CREATE TABLE IF NOT EXISTS player_teleports (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" player_uuid VARCHAR(36) NOT NULL," +
|
||||
" player_name VARCHAR(16) NOT NULL," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" from_world VARCHAR(100)," +
|
||||
" from_x DOUBLE," +
|
||||
" from_y DOUBLE," +
|
||||
" from_z DOUBLE," +
|
||||
" to_world VARCHAR(100)," +
|
||||
" to_x DOUBLE," +
|
||||
" to_y DOUBLE," +
|
||||
" to_z DOUBLE," +
|
||||
" cause VARCHAR(100)," +
|
||||
" INDEX idx_tp_uuid (player_uuid)," +
|
||||
" INDEX idx_tp_timestamp (timestamp)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// inventory_events
|
||||
"CREATE TABLE IF NOT EXISTS inventory_events (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" event_type ENUM('pickup','drop','click','craft','enchant','anvil','trade') NOT NULL," +
|
||||
" player_uuid VARCHAR(36) NOT NULL," +
|
||||
" player_name VARCHAR(16) NOT NULL," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" world VARCHAR(100)," +
|
||||
" x DOUBLE," +
|
||||
" y DOUBLE," +
|
||||
" z DOUBLE," +
|
||||
" item_type VARCHAR(100)," +
|
||||
" item_amount INT," +
|
||||
" item_meta JSON," +
|
||||
" slot INT," +
|
||||
" inventory_type VARCHAR(100)," +
|
||||
" INDEX idx_inv_uuid (player_uuid)," +
|
||||
" INDEX idx_inv_timestamp (timestamp)," +
|
||||
" INDEX idx_inv_type (event_type)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// player_stats
|
||||
"CREATE TABLE IF NOT EXISTS player_stats (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" event_type VARCHAR(100) NOT NULL," +
|
||||
" player_uuid VARCHAR(36) NOT NULL," +
|
||||
" player_name VARCHAR(16) NOT NULL," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" old_value VARCHAR(255)," +
|
||||
" new_value VARCHAR(255)," +
|
||||
" details JSON," +
|
||||
" INDEX idx_pst_uuid (player_uuid)," +
|
||||
" INDEX idx_pst_timestamp (timestamp)," +
|
||||
" INDEX idx_pst_type (event_type)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// world_events
|
||||
"CREATE TABLE IF NOT EXISTS world_events (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" event_type VARCHAR(100) NOT NULL," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" world VARCHAR(100)," +
|
||||
" x DOUBLE," +
|
||||
" y DOUBLE," +
|
||||
" z DOUBLE," +
|
||||
" details JSON," +
|
||||
" INDEX idx_we_timestamp (timestamp)," +
|
||||
" INDEX idx_we_world (world)," +
|
||||
" INDEX idx_we_type (event_type)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// server_events
|
||||
"CREATE TABLE IF NOT EXISTS server_events (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" event_type VARCHAR(100) NOT NULL," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" message TEXT," +
|
||||
" details JSON," +
|
||||
" INDEX idx_se_timestamp (timestamp)," +
|
||||
" INDEX idx_se_type (event_type)," +
|
||||
" INDEX idx_se_server (server_name)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// proxy_events
|
||||
"CREATE TABLE IF NOT EXISTS proxy_events (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" event_type ENUM('login','disconnect','server_switch','command','chat','kick','proxy_start','proxy_stop') NOT NULL," +
|
||||
" player_uuid VARCHAR(36)," +
|
||||
" player_name VARCHAR(16)," +
|
||||
" proxy_name VARCHAR(100)," +
|
||||
" from_server VARCHAR(100)," +
|
||||
" to_server VARCHAR(100)," +
|
||||
" ip_address VARCHAR(45)," +
|
||||
" details JSON," +
|
||||
" INDEX idx_pe_uuid (player_uuid)," +
|
||||
" INDEX idx_pe_timestamp (timestamp)," +
|
||||
" INDEX idx_pe_type (event_type)," +
|
||||
" INDEX idx_pe_proxy (proxy_name)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// sign_edits
|
||||
"CREATE TABLE IF NOT EXISTS sign_edits (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" player_uuid VARCHAR(36)," +
|
||||
" player_name VARCHAR(16)," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" world VARCHAR(100)," +
|
||||
" x INT," +
|
||||
" y INT," +
|
||||
" z INT," +
|
||||
" line1 VARCHAR(255)," +
|
||||
" line2 VARCHAR(255)," +
|
||||
" line3 VARCHAR(255)," +
|
||||
" line4 VARCHAR(255)," +
|
||||
" INDEX idx_sign_uuid (player_uuid)," +
|
||||
" INDEX idx_sign_timestamp (timestamp)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// plugin_events
|
||||
"CREATE TABLE IF NOT EXISTS plugin_events (" +
|
||||
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
||||
" timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)," +
|
||||
" event_type VARCHAR(100) NOT NULL," +
|
||||
" plugin_name VARCHAR(100)," +
|
||||
" server_name VARCHAR(100)," +
|
||||
" player_uuid VARCHAR(36)," +
|
||||
" player_name VARCHAR(16)," +
|
||||
" actor_uuid VARCHAR(36)," +
|
||||
" actor_name VARCHAR(64)," +
|
||||
" target_type VARCHAR(50)," +
|
||||
" target_id VARCHAR(100)," +
|
||||
" action VARCHAR(255)," +
|
||||
" details JSON," +
|
||||
" INDEX idx_plev_uuid (player_uuid)," +
|
||||
" INDEX idx_plev_timestamp (timestamp)," +
|
||||
" INDEX idx_plev_type (event_type)," +
|
||||
" INDEX idx_plev_plugin (plugin_name)," +
|
||||
" INDEX idx_plev_actor (actor_uuid)" +
|
||||
") ENGINE=InnoDB",
|
||||
|
||||
// view
|
||||
"CREATE OR REPLACE VIEW v_recent_activity AS" +
|
||||
" SELECT 'chat' AS source, timestamp, player_name, server_name, message AS detail FROM player_chat WHERE timestamp >= NOW() - INTERVAL 24 HOUR" +
|
||||
" UNION ALL" +
|
||||
" SELECT 'command' AS source, timestamp, player_name, server_name, command AS detail FROM player_commands WHERE timestamp >= NOW() - INTERVAL 24 HOUR" +
|
||||
" UNION ALL" +
|
||||
" SELECT 'block' AS source, timestamp, player_name, server_name, CONCAT(event_type,' ',block_type,' at ',world,' ',x,',',y,',',z) AS detail FROM block_events WHERE timestamp >= NOW() - INTERVAL 24 HOUR" +
|
||||
" UNION ALL" +
|
||||
" SELECT 'death' AS source, timestamp, player_name, server_name, death_message AS detail FROM player_deaths WHERE timestamp >= NOW() - INTERVAL 24 HOUR" +
|
||||
" ORDER BY timestamp DESC"
|
||||
};
|
||||
|
||||
try (Connection con = dataSource.getConnection();
|
||||
Statement st = con.createStatement()) {
|
||||
for (String sql : ddl) {
|
||||
st.execute(sql);
|
||||
}
|
||||
// Widen mc_version in case the table was created with the old VARCHAR(20)
|
||||
st.execute("ALTER TABLE servers MODIFY COLUMN mc_version VARCHAR(100)");
|
||||
// Add country column to existing tables
|
||||
st.execute("ALTER TABLE player_sessions ADD COLUMN IF NOT EXISTS country VARCHAR(100) AFTER ip_address");
|
||||
}
|
||||
plugin.getLogger().info("[MCLogger] Database tables verified/created.");
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
if (dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void registerServer(FileConfiguration cfg) throws SQLException {
|
||||
String version = plugin.getServer().getMinecraftVersion();
|
||||
try (Connection con = dataSource.getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO servers (server_name, server_type, ip_address, mc_version) VALUES (?,?,?,?) " +
|
||||
"ON DUPLICATE KEY UPDATE last_seen = CURRENT_TIMESTAMP(3), mc_version = VALUES(mc_version)",
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
ps.setString(1, serverName);
|
||||
ps.setString(2, "paper");
|
||||
ps.setString(3, cfg.getString("database.host", "localhost"));
|
||||
ps.setString(4, version);
|
||||
ps.executeUpdate();
|
||||
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||
if (rs.next()) serverId = rs.getInt(1);
|
||||
}
|
||||
}
|
||||
if (serverId == -1) {
|
||||
try (Connection con = dataSource.getConnection();
|
||||
PreparedStatement ps = con.prepareStatement("SELECT id FROM servers WHERE server_name = ?")) {
|
||||
ps.setString(1, serverName);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (rs.next()) serverId = rs.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Connection getConnection() throws SQLException {
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Async insert helper
|
||||
// --------------------------------------------------------
|
||||
|
||||
private void asyncExec(ThrowingRunnable r) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
r.run();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "[MCLogger] DB write error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ThrowingRunnable {
|
||||
void run() throws Exception;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Players
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void upsertPlayer(String uuid, String username, String displayName,
|
||||
String ip, String locale, boolean isOp) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO players (uuid, username, display_name, ip_address, locale, is_op) VALUES (?,?,?,?,?,?) " +
|
||||
"ON DUPLICATE KEY UPDATE username=VALUES(username), display_name=VALUES(display_name), " +
|
||||
"ip_address=VALUES(ip_address), locale=VALUES(locale), is_op=VALUES(is_op), " +
|
||||
"last_seen=CURRENT_TIMESTAMP(3)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, username);
|
||||
ps.setString(3, displayName);
|
||||
ps.setString(4, ip);
|
||||
ps.setString(5, locale);
|
||||
ps.setBoolean(6, isOp);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Sessions
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertSessionLogin(String uuid, String name, String ip, String clientVersion) {
|
||||
asyncExec(() -> {
|
||||
String country = lookupCountry(ip);
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO player_sessions (player_uuid, player_name, server_name, ip_address, country, client_version) VALUES (?,?,?,?,?,?)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, name);
|
||||
ps.setString(3, serverName);
|
||||
ps.setString(4, ip);
|
||||
ps.setString(5, country);
|
||||
ps.setString(6, clientVersion);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Looks up the player's country via ip-api.com (free, no API key). Returns "Local" for LAN/private IPs. */
|
||||
private String lookupCountry(String ip) {
|
||||
if (ip == null || ip.equals("unknown") || ip.equals("::1")
|
||||
|| ip.startsWith("127.") || ip.startsWith("10.")
|
||||
|| ip.startsWith("192.168.") || ip.startsWith("172.")) {
|
||||
return "Local";
|
||||
}
|
||||
try {
|
||||
java.net.URL url = new java.net.URL("http://ip-api.com/json/" + ip + "?fields=country");
|
||||
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(3000);
|
||||
conn.setReadTimeout(3000);
|
||||
conn.setRequestMethod("GET");
|
||||
try (java.io.BufferedReader reader = new java.io.BufferedReader(
|
||||
new java.io.InputStreamReader(conn.getInputStream()))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) sb.append(line);
|
||||
JsonObject obj = GSON.fromJson(sb.toString(), JsonObject.class);
|
||||
if (obj != null && obj.has("country")) return obj.get("country").getAsString();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
public void closeSession(String uuid, long durationSec) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"UPDATE player_sessions SET logout_time=CURRENT_TIMESTAMP(3), duration_sec=? " +
|
||||
"WHERE player_uuid=? AND server_name=? AND logout_time IS NULL ORDER BY login_time DESC LIMIT 1")) {
|
||||
ps.setLong(1, durationSec);
|
||||
ps.setString(2, uuid);
|
||||
ps.setString(3, serverName);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"UPDATE players SET total_playtime_sec = total_playtime_sec + ? WHERE uuid = ?")) {
|
||||
ps.setLong(1, durationSec);
|
||||
ps.setString(2, uuid);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Chat
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertChat(String uuid, String name, String world, String message, String channel) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO player_chat (player_uuid, player_name, server_name, world, message, channel) VALUES (?,?,?,?,?,?)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, name);
|
||||
ps.setString(3, serverName);
|
||||
ps.setString(4, world);
|
||||
ps.setString(5, message);
|
||||
ps.setString(6, channel);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Commands
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertCommand(String uuid, String name, String world,
|
||||
String command, double x, double y, double z) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO player_commands (player_uuid, player_name, server_name, world, command, x, y, z) VALUES (?,?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, name);
|
||||
ps.setString(3, serverName);
|
||||
ps.setString(4, world);
|
||||
ps.setString(5, command);
|
||||
ps.setDouble(6, x);
|
||||
ps.setDouble(7, y);
|
||||
ps.setDouble(8, z);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Block Events
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertBlockEvent(String type, String uuid, String name,
|
||||
String world, int x, int y, int z,
|
||||
String blockType, String blockData, String tool, boolean silkTouch) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO block_events (event_type, player_uuid, player_name, server_name, world, x, y, z, block_type, block_data, tool, is_silk) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, type);
|
||||
ps.setString(2, uuid);
|
||||
ps.setString(3, name);
|
||||
ps.setString(4, serverName);
|
||||
ps.setString(5, world);
|
||||
ps.setInt(6, x);
|
||||
ps.setInt(7, y);
|
||||
ps.setInt(8, z);
|
||||
ps.setString(9, blockType);
|
||||
ps.setString(10, blockData);
|
||||
ps.setString(11, tool);
|
||||
ps.setBoolean(12, silkTouch);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Deaths
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertDeath(String uuid, String name, String world, double x, double y, double z,
|
||||
String msg, String cause, String killerUuid, String killerName,
|
||||
String killerType, int expLevel, Map<String, Object> itemsLost) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO player_deaths (player_uuid, player_name, server_name, world, x, y, z, " +
|
||||
"death_message, cause, killer_uuid, killer_name, killer_type, exp_level, items_lost) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, name);
|
||||
ps.setString(3, serverName);
|
||||
ps.setString(4, world);
|
||||
ps.setDouble(5, x);
|
||||
ps.setDouble(6, y);
|
||||
ps.setDouble(7, z);
|
||||
ps.setString(8, msg);
|
||||
ps.setString(9, cause);
|
||||
ps.setString(10, killerUuid);
|
||||
ps.setString(11, killerName);
|
||||
ps.setString(12, killerType);
|
||||
ps.setInt(13, expLevel);
|
||||
ps.setString(14, itemsLost != null ? GSON.toJson(itemsLost) : null);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Entity Events
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertEntityEvent(String type, String world, double x, double y, double z,
|
||||
String entityType, String entityUuid, String entityName,
|
||||
String playerUuid, String playerName, String cause,
|
||||
double damage, Map<String, Object> details) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO entity_events (event_type, server_name, world, x, y, z, entity_type, entity_uuid, entity_name, player_uuid, player_name, cause, damage, details) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, type);
|
||||
ps.setString(2, serverName);
|
||||
ps.setString(3, world);
|
||||
ps.setDouble(4, x);
|
||||
ps.setDouble(5, y);
|
||||
ps.setDouble(6, z);
|
||||
ps.setString(7, entityType);
|
||||
ps.setString(8, entityUuid);
|
||||
ps.setString(9, entityName);
|
||||
ps.setString(10, playerUuid);
|
||||
ps.setString(11, playerName);
|
||||
ps.setString(12, cause);
|
||||
ps.setDouble(13, damage);
|
||||
ps.setString(14, details != null ? GSON.toJson(details) : null);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Teleports
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertTeleport(String uuid, String name,
|
||||
String fromWorld, double fx, double fy, double fz,
|
||||
String toWorld, double tx, double ty, double tz,
|
||||
String cause) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO player_teleports (player_uuid, player_name, server_name, from_world, from_x, from_y, from_z, to_world, to_x, to_y, to_z, cause) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, name);
|
||||
ps.setString(3, serverName);
|
||||
ps.setString(4, fromWorld);
|
||||
ps.setDouble(5, fx);
|
||||
ps.setDouble(6, fy);
|
||||
ps.setDouble(7, fz);
|
||||
ps.setString(8, toWorld);
|
||||
ps.setDouble(9, tx);
|
||||
ps.setDouble(10, ty);
|
||||
ps.setDouble(11, tz);
|
||||
ps.setString(12, cause);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Inventory Events
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertInventoryEvent(String type, String uuid, String name,
|
||||
String world, double x, double y, double z,
|
||||
String item, int amount, Map<String, Object> meta,
|
||||
int slot, String invType) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO inventory_events (event_type, player_uuid, player_name, server_name, world, x, y, z, item_type, item_amount, item_meta, slot, inventory_type) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, type);
|
||||
ps.setString(2, uuid);
|
||||
ps.setString(3, name);
|
||||
ps.setString(4, serverName);
|
||||
ps.setString(5, world);
|
||||
ps.setDouble(6, x);
|
||||
ps.setDouble(7, y);
|
||||
ps.setDouble(8, z);
|
||||
ps.setString(9, item);
|
||||
ps.setInt(10, amount);
|
||||
ps.setString(11, meta != null ? GSON.toJson(meta) : null);
|
||||
ps.setInt(12, slot);
|
||||
ps.setString(13, invType);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Player Stats (Gamemode, Level, etc.)
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertPlayerStat(String type, String uuid, String name,
|
||||
String oldVal, String newVal, Map<String, Object> details) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO player_stats (event_type, player_uuid, player_name, server_name, old_value, new_value, details) VALUES (?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, type);
|
||||
ps.setString(2, uuid);
|
||||
ps.setString(3, name);
|
||||
ps.setString(4, serverName);
|
||||
ps.setString(5, oldVal);
|
||||
ps.setString(6, newVal);
|
||||
ps.setString(7, details != null ? GSON.toJson(details) : null);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// World Events
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertWorldEvent(String type, String world, Double x, Double y, Double z, Map<String, Object> details) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO world_events (event_type, server_name, world, x, y, z, details) VALUES (?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, type);
|
||||
ps.setString(2, serverName);
|
||||
ps.setString(3, world);
|
||||
if (x != null) ps.setDouble(4, x); else ps.setNull(4, Types.DOUBLE);
|
||||
if (y != null) ps.setDouble(5, y); else ps.setNull(5, Types.DOUBLE);
|
||||
if (z != null) ps.setDouble(6, z); else ps.setNull(6, Types.DOUBLE);
|
||||
ps.setString(7, details != null ? GSON.toJson(details) : null);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Server Events
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertServerEvent(String type, String message, Map<String, Object> details) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO server_events (event_type, server_name, message, details) VALUES (?,?,?,?)")) {
|
||||
ps.setString(1, type);
|
||||
ps.setString(2, serverName);
|
||||
ps.setString(3, message);
|
||||
ps.setString(4, details != null ? GSON.toJson(details) : null);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Sign Edits
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertSignEdit(String uuid, String name, String world,
|
||||
int x, int y, int z,
|
||||
String l1, String l2, String l3, String l4) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO sign_edits (player_uuid, player_name, server_name, world, x, y, z, line1, line2, line3, line4) VALUES (?,?,?,?,?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, name);
|
||||
ps.setString(3, serverName);
|
||||
ps.setString(4, world);
|
||||
ps.setInt(5, x);
|
||||
ps.setInt(6, y);
|
||||
ps.setInt(7, z);
|
||||
ps.setString(8, l1);
|
||||
ps.setString(9, l2);
|
||||
ps.setString(10, l3);
|
||||
ps.setString(11, l4);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Plugin Events (LuckPerms etc.)
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Writes a generic plugin event (e.g. LuckPerms permission change).
|
||||
*
|
||||
* @param eventType Type of event (e.g. "luckperms_grant", "luckperms_revoke")
|
||||
* @param pluginName Name of the triggering plugin (e.g. "LuckPerms")
|
||||
* @param playerUuid UUID of the affected player (may be null)
|
||||
* @param playerName Name of the affected player
|
||||
* @param actorUuid UUID of the actor (console UUID = 00000000-...)
|
||||
* @param actorName Display name of the actor
|
||||
* @param targetType "user" or "group"
|
||||
* @param targetId UUID or group name
|
||||
* @param action Description of the action (e.g. "permission.node set to true")
|
||||
* @param details Additional details as Map
|
||||
*/
|
||||
public void insertPluginEvent(String eventType, String pluginName,
|
||||
String playerUuid, String playerName,
|
||||
String actorUuid, String actorName,
|
||||
String targetType, String targetId,
|
||||
String action, Map<String, Object> details) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO plugin_events " +
|
||||
"(event_type, plugin_name, server_name, player_uuid, player_name, " +
|
||||
" actor_uuid, actor_name, target_type, target_id, action, details) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, eventType);
|
||||
ps.setString(2, pluginName);
|
||||
ps.setString(3, serverName);
|
||||
if (playerUuid != null) ps.setString(4, playerUuid); else ps.setNull(4, Types.VARCHAR);
|
||||
if (playerName != null) ps.setString(5, playerName); else ps.setNull(5, Types.VARCHAR);
|
||||
if (actorUuid != null) ps.setString(6, actorUuid); else ps.setNull(6, Types.VARCHAR);
|
||||
if (actorName != null) ps.setString(7, actorName); else ps.setNull(7, Types.VARCHAR);
|
||||
if (targetType != null) ps.setString(8, targetType); else ps.setNull(8, Types.VARCHAR);
|
||||
if (targetId != null) ps.setString(9, targetId); else ps.setNull(9, Types.VARCHAR);
|
||||
if (action != null) ps.setString(10, action); else ps.setNull(10, Types.VARCHAR);
|
||||
ps.setString(11, details != null ? GSON.toJson(details) : null);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Getter
|
||||
// --------------------------------------------------------
|
||||
|
||||
public String getServerName() { return serverName; }
|
||||
public int getServerId() { return serverId; }
|
||||
public boolean isConnected() { return dataSource != null && !dataSource.isClosed(); }
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package de.simolzimol.mclogger.paper.listeners;
|
||||
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.Sign;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.*;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Logs all block-related events: breaking, placing,
|
||||
* burning, exploding, igniting, signs, etc.
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class BlockListener implements Listener {
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
|
||||
public BlockListener(PaperLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Block Break
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBreak(BlockBreakEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Block b = event.getBlock();
|
||||
ItemStack tool = p.getInventory().getItemInMainHand();
|
||||
|
||||
boolean silkTouch = tool.hasItemMeta()
|
||||
&& tool.getItemMeta() != null
|
||||
&& tool.getItemMeta().hasEnchant(Enchantment.SILK_TOUCH);
|
||||
|
||||
String toolName = tool.getType().isAir() ? "HAND" : tool.getType().name();
|
||||
String blockData = b.getBlockData().getAsString();
|
||||
|
||||
plugin.getDb().insertBlockEvent(
|
||||
"break",
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
b.getWorld().getName(),
|
||||
b.getX(), b.getY(), b.getZ(),
|
||||
b.getType().name(),
|
||||
blockData,
|
||||
toolName,
|
||||
silkTouch
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Block Place
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlace(BlockPlaceEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Block b = event.getBlockPlaced();
|
||||
|
||||
plugin.getDb().insertBlockEvent(
|
||||
"place",
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
b.getWorld().getName(),
|
||||
b.getX(), b.getY(), b.getZ(),
|
||||
b.getType().name(),
|
||||
b.getBlockData().getAsString(),
|
||||
null, false
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Block Burn
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBurn(BlockBurnEvent event) {
|
||||
Block b = event.getBlock();
|
||||
plugin.getDb().insertBlockEvent(
|
||||
"burn",
|
||||
null, null,
|
||||
b.getWorld().getName(),
|
||||
b.getX(), b.getY(), b.getZ(),
|
||||
b.getType().name(),
|
||||
b.getBlockData().getAsString(),
|
||||
null, false
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Block Ignite
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onIgnite(BlockIgniteEvent event) {
|
||||
Block b = event.getBlock();
|
||||
Player p = event.getPlayer();
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("cause", event.getCause().name());
|
||||
|
||||
plugin.getDb().insertBlockEvent(
|
||||
"ignite",
|
||||
p != null ? p.getUniqueId().toString() : null,
|
||||
p != null ? p.getName() : null,
|
||||
b.getWorld().getName(),
|
||||
b.getX(), b.getY(), b.getZ(),
|
||||
b.getType().name(),
|
||||
b.getBlockData().getAsString(),
|
||||
null, false
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Block Explosion
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onExplode(BlockExplodeEvent event) {
|
||||
Block b = event.getBlock();
|
||||
plugin.getDb().insertBlockEvent(
|
||||
"explode",
|
||||
null, null,
|
||||
b.getWorld().getName(),
|
||||
b.getX(), b.getY(), b.getZ(),
|
||||
b.getType().name(),
|
||||
b.getBlockData().getAsString(),
|
||||
null, false
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Sign Edit
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onSign(SignChangeEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Block b = event.getBlock();
|
||||
|
||||
String[] lines = new String[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
lines[i] = event.line(i) != null
|
||||
? PlainTextComponentSerializer.plainText().serialize(event.line(i))
|
||||
: "";
|
||||
}
|
||||
|
||||
plugin.getDb().insertSignEdit(
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
b.getWorld().getName(),
|
||||
b.getX(), b.getY(), b.getZ(),
|
||||
lines[0], lines[1], lines[2], lines[3]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package de.simolzimol.mclogger.paper.listeners;
|
||||
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.entity.*;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Logs entity events: spawns, deaths, damage, taming, breeding, explosions.
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class EntityListener implements Listener {
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
|
||||
public EntityListener(PaperLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Entity Spawn
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onSpawn(EntitySpawnEvent event) {
|
||||
Entity e = event.getEntity();
|
||||
// Skip redundant entities (projectiles etc.) to reduce DB load
|
||||
if (e instanceof Item || e instanceof ExperienceOrb || e instanceof Arrow) return;
|
||||
|
||||
String playerUuid = null, playerName = null;
|
||||
if (e instanceof Player p) {
|
||||
playerUuid = p.getUniqueId().toString();
|
||||
playerName = p.getName();
|
||||
}
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("spawn_reason", e instanceof LivingEntity le ? "natural" : "other");
|
||||
details.put("has_ai", e instanceof Mob mob && mob.hasAI());
|
||||
|
||||
plugin.getDb().insertEntityEvent(
|
||||
"spawn",
|
||||
e.getWorld().getName(),
|
||||
e.getLocation().getX(), e.getLocation().getY(), e.getLocation().getZ(),
|
||||
e.getType().name(),
|
||||
e.getUniqueId().toString(),
|
||||
e.customName() != null ? PlainTextComponentSerializer.plainText().serialize(e.customName()) : null,
|
||||
playerUuid, playerName,
|
||||
null, 0, details
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Entity Death
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onDeath(EntityDeathEvent event) {
|
||||
LivingEntity e = event.getEntity();
|
||||
if (e instanceof Player) return; // Player deaths are handled by PlayerDeathListener
|
||||
|
||||
Player killer = e.getKiller();
|
||||
String killerUuid = killer != null ? killer.getUniqueId().toString() : null;
|
||||
String killerName = killer != null ? killer.getName() : null;
|
||||
|
||||
String cause = e.getLastDamageCause() != null
|
||||
? e.getLastDamageCause().getCause().name() : "UNKNOWN";
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("drops", event.getDrops().size());
|
||||
details.put("exp", event.getDroppedExp());
|
||||
|
||||
plugin.getDb().insertEntityEvent(
|
||||
"death",
|
||||
e.getWorld().getName(),
|
||||
e.getLocation().getX(), e.getLocation().getY(), e.getLocation().getZ(),
|
||||
e.getType().name(),
|
||||
e.getUniqueId().toString(),
|
||||
e.customName() != null ? PlainTextComponentSerializer.plainText().serialize(e.customName()) : null,
|
||||
killerUuid, killerName,
|
||||
cause, 0, details
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Entity Damage (player-inflicted only)
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onDamage(EntityDamageByEntityEvent event) {
|
||||
if (!(event.getDamager() instanceof Player p)) return;
|
||||
if (event.getEntity() instanceof Player) return; // PvP separat nicht nötig
|
||||
Entity target = event.getEntity();
|
||||
|
||||
plugin.getDb().insertEntityEvent(
|
||||
"damage",
|
||||
target.getWorld().getName(),
|
||||
target.getLocation().getX(), target.getLocation().getY(), target.getLocation().getZ(),
|
||||
target.getType().name(),
|
||||
target.getUniqueId().toString(),
|
||||
target.customName() != null ? PlainTextComponentSerializer.plainText().serialize(target.customName()) : null,
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
event.getCause().name(),
|
||||
event.getFinalDamage(),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Entity Tame
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onTame(EntityTameEvent event) {
|
||||
if (!(event.getOwner() instanceof Player p)) return;
|
||||
Entity e = event.getEntity();
|
||||
|
||||
plugin.getDb().insertEntityEvent(
|
||||
"tame",
|
||||
e.getWorld().getName(),
|
||||
e.getLocation().getX(), e.getLocation().getY(), e.getLocation().getZ(),
|
||||
e.getType().name(),
|
||||
e.getUniqueId().toString(),
|
||||
null,
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
null, 0, null
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Entity Breed
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBreed(EntityBreedEvent event) {
|
||||
Entity e = event.getEntity();
|
||||
Player breeder = event.getBreeder() instanceof Player pl ? pl : null;
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("mother", event.getMother().getType().name());
|
||||
details.put("father", event.getFather().getType().name());
|
||||
if (event.getBredWith() != null) details.put("bred_with", event.getBredWith().getType().name());
|
||||
|
||||
plugin.getDb().insertEntityEvent(
|
||||
"breed",
|
||||
e.getWorld().getName(),
|
||||
e.getLocation().getX(), e.getLocation().getY(), e.getLocation().getZ(),
|
||||
e.getType().name(),
|
||||
e.getUniqueId().toString(),
|
||||
null,
|
||||
breeder != null ? breeder.getUniqueId().toString() : null,
|
||||
breeder != null ? breeder.getName() : null,
|
||||
null, 0, details
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Explosion (Creeper, TNT, etc.)
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onExplode(EntityExplodeEvent event) {
|
||||
Entity e = event.getEntity();
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("blocks_destroyed", event.blockList().size());
|
||||
details.put("yield", event.getYield());
|
||||
|
||||
plugin.getDb().insertEntityEvent(
|
||||
"explode",
|
||||
e.getWorld().getName(),
|
||||
e.getLocation().getX(), e.getLocation().getY(), e.getLocation().getZ(),
|
||||
e.getType().name(),
|
||||
e.getUniqueId().toString(),
|
||||
null,
|
||||
null, null,
|
||||
null, 0, details
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// PvP – Damage between players
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPvP(EntityDamageByEntityEvent event) {
|
||||
if (!(event.getEntity() instanceof Player victim)) return;
|
||||
if (!(event.getDamager() instanceof Player attacker)) return;
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("cause", event.getCause().name());
|
||||
details.put("damage", event.getFinalDamage());
|
||||
details.put("victim_health_after",
|
||||
Math.max(0, victim.getHealth() - event.getFinalDamage()));
|
||||
|
||||
plugin.getDb().insertPlayerStat(
|
||||
"pvp_hit",
|
||||
attacker.getUniqueId().toString(),
|
||||
attacker.getName(),
|
||||
attacker.getName(),
|
||||
victim.getName(),
|
||||
details
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package de.simolzimol.mclogger.paper.listeners;
|
||||
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.inventory.*;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Logs inventory events: item pickup, item drop,
|
||||
* crafting, enchanting, trading, etc.
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class InventoryListener implements Listener {
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
|
||||
public InventoryListener(PaperLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Item Pickup
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPickup(EntityPickupItemEvent event) {
|
||||
if (!(event.getEntity() instanceof Player p)) return;
|
||||
ItemStack item = event.getItem().getItemStack();
|
||||
|
||||
plugin.getDb().insertInventoryEvent(
|
||||
"pickup",
|
||||
p.getUniqueId().toString(), p.getName(),
|
||||
p.getWorld().getName(),
|
||||
p.getLocation().getX(), p.getLocation().getY(), p.getLocation().getZ(),
|
||||
item.getType().name(), item.getAmount(),
|
||||
buildItemMeta(item), -1,
|
||||
"player"
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Item Drop
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onDrop(PlayerDropItemEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
ItemStack item = event.getItemDrop().getItemStack();
|
||||
|
||||
plugin.getDb().insertInventoryEvent(
|
||||
"drop",
|
||||
p.getUniqueId().toString(), p.getName(),
|
||||
p.getWorld().getName(),
|
||||
p.getLocation().getX(), p.getLocation().getY(), p.getLocation().getZ(),
|
||||
item.getType().name(), item.getAmount(),
|
||||
buildItemMeta(item), -1,
|
||||
"player"
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Inventory Clicks (player inventories only)
|
||||
// -------------------------------------------------------
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (!(event.getWhoClicked() instanceof Player p)) return;
|
||||
if (event.getCurrentItem() == null) return;
|
||||
// Only crafting, enchanting, anvil -> trade, etc.
|
||||
InventoryType type = event.getInventory().getType();
|
||||
if (type == InventoryType.PLAYER || type == InventoryType.CREATIVE) return; // too much noise
|
||||
|
||||
ItemStack item = event.getCurrentItem();
|
||||
String invTypeName = type.name();
|
||||
String logType = switch (type) {
|
||||
case CRAFTING, WORKBENCH -> "craft";
|
||||
case ENCHANTING -> "enchant";
|
||||
case ANVIL -> "anvil";
|
||||
case MERCHANT -> "trade";
|
||||
default -> "click";
|
||||
};
|
||||
|
||||
plugin.getDb().insertInventoryEvent(
|
||||
logType,
|
||||
p.getUniqueId().toString(), p.getName(),
|
||||
p.getWorld().getName(),
|
||||
p.getLocation().getX(), p.getLocation().getY(), p.getLocation().getZ(),
|
||||
item.getType().name(), item.getAmount(),
|
||||
buildItemMeta(item),
|
||||
event.getSlot(),
|
||||
invTypeName
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Helper method: item meta as Map
|
||||
// -------------------------------------------------------
|
||||
private Map<String, Object> buildItemMeta(ItemStack item) {
|
||||
if (!item.hasItemMeta()) return null;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
if (meta.hasDisplayName() && meta.displayName() != null) {
|
||||
m.put("name", PlainTextComponentSerializer.plainText().serialize(meta.displayName()));
|
||||
}
|
||||
if (meta.hasEnchants()) {
|
||||
Map<String, Integer> enc = new HashMap<>();
|
||||
meta.getEnchants().forEach((e, lvl) -> enc.put(e.getKey().getKey(), lvl));
|
||||
m.put("enchants", enc);
|
||||
}
|
||||
if (meta.hasLore() && meta.lore() != null) {
|
||||
m.put("lore_lines", meta.lore().size());
|
||||
}
|
||||
if (meta.isUnbreakable()) m.put("unbreakable", true);
|
||||
return m.isEmpty() ? null : m;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package de.simolzimol.mclogger.paper.listeners;
|
||||
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import net.luckperms.api.LuckPerms;
|
||||
import net.luckperms.api.event.EventBus;
|
||||
import net.luckperms.api.event.log.LogPublishEvent;
|
||||
import net.luckperms.api.actionlog.Action;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Listens for LuckPerms log events and stores all permission changes.
|
||||
* Only registered when LuckPerms is loaded (softdepend).
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class LuckPermsListener {
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
|
||||
public LuckPermsListener(PaperLoggerPlugin plugin, LuckPerms luckPerms) {
|
||||
this.plugin = plugin;
|
||||
registerEvents(luckPerms.getEventBus());
|
||||
plugin.getLogger().info("[MCLogger] LuckPerms listener registered.");
|
||||
}
|
||||
|
||||
private void registerEvents(EventBus bus) {
|
||||
bus.subscribe(plugin, LogPublishEvent.class, this::onLogPublish);
|
||||
}
|
||||
|
||||
/**
|
||||
* LogPublishEvent is fired by LuckPerms whenever an action is written to
|
||||
* the action log (grant, revoke, group create/delete, track
|
||||
* add/remove, meta set, etc.).
|
||||
*/
|
||||
private void onLogPublish(LogPublishEvent event) {
|
||||
Action entry = event.getEntry();
|
||||
|
||||
String actorUuid = entry.getSource().getUniqueId().toString();
|
||||
String actorName = entry.getSource().getName();
|
||||
String targetType = formatTargetType(entry.getTarget().getType());
|
||||
String targetId = formatTargetId(entry.getTarget());
|
||||
String actionStr = entry.getDescription();
|
||||
|
||||
// Derive player UUID if the target is a user
|
||||
String playerUuid = null;
|
||||
String playerName = null;
|
||||
if (entry.getTarget().getType() == Action.Target.Type.USER) {
|
||||
playerUuid = entry.getTarget().getUniqueId()
|
||||
.map(UUID::toString).orElse(null);
|
||||
playerName = entry.getTarget().getName();
|
||||
}
|
||||
|
||||
// Derive event type from the action description
|
||||
String eventType = deriveEventType(actionStr);
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("actor_uuid", actorUuid);
|
||||
details.put("actor_name", actorName);
|
||||
details.put("target_type", targetType);
|
||||
details.put("target_id", targetId);
|
||||
details.put("action_raw", actionStr);
|
||||
details.put("timestamp_luckperms", entry.getTimestamp().getEpochSecond());
|
||||
|
||||
plugin.getDb().insertPluginEvent(
|
||||
eventType,
|
||||
"LuckPerms",
|
||||
playerUuid,
|
||||
playerName,
|
||||
actorUuid,
|
||||
actorName,
|
||||
targetType,
|
||||
targetId,
|
||||
actionStr,
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Helper methods
|
||||
// --------------------------------------------------------
|
||||
|
||||
private String formatTargetType(Action.Target.Type type) {
|
||||
return switch (type) {
|
||||
case USER -> "user";
|
||||
case GROUP -> "group";
|
||||
case TRACK -> "track";
|
||||
};
|
||||
}
|
||||
|
||||
private String formatTargetId(Action.Target target) {
|
||||
if (target.getUniqueId().isPresent()) {
|
||||
return target.getUniqueId().get().toString();
|
||||
}
|
||||
return target.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an action description to a compact event type.
|
||||
* Beispiele:
|
||||
* "permission set permission.node to true" -> luckperms_permission_set
|
||||
* "permission unset permission.node" -> luckperms_permission_unset
|
||||
* "parent add groupname" -> luckperms_parent_add
|
||||
* "group create groupname" -> luckperms_group_create
|
||||
*/
|
||||
private String deriveEventType(String action) {
|
||||
if (action == null) return "luckperms_unknown";
|
||||
String lower = action.toLowerCase();
|
||||
if (lower.startsWith("permission set")) return "luckperms_permission_set";
|
||||
if (lower.startsWith("permission unset")) return "luckperms_permission_unset";
|
||||
if (lower.startsWith("parent add")) return "luckperms_parent_add";
|
||||
if (lower.startsWith("parent remove")) return "luckperms_parent_remove";
|
||||
if (lower.startsWith("meta set")) return "luckperms_meta_set";
|
||||
if (lower.startsWith("meta unset")) return "luckperms_meta_unset";
|
||||
if (lower.startsWith("group create")) return "luckperms_group_create";
|
||||
if (lower.startsWith("group delete")) return "luckperms_group_delete";
|
||||
if (lower.startsWith("track create")) return "luckperms_track_create";
|
||||
if (lower.startsWith("track delete")) return "luckperms_track_delete";
|
||||
if (lower.startsWith("track add")) return "luckperms_track_add";
|
||||
if (lower.startsWith("track remove")) return "luckperms_track_remove";
|
||||
return "luckperms_action";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package de.simolzimol.mclogger.paper.listeners;
|
||||
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Logs commands executed by players.
|
||||
* Chat is logged by the Velocity proxy with correct server context.
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class PlayerChatCommandListener implements Listener {
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
|
||||
public PlayerChatCommandListener(PaperLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command – logs with world/position, then notifies Velocity proxy.
|
||||
* Velocity will skip its own fallback log once it receives the plugin message.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onCommand(PlayerCommandPreprocessEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
String cmd = event.getMessage(); // includes leading '/'
|
||||
plugin.getDb().insertCommand(
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
p.getWorld().getName(),
|
||||
cmd,
|
||||
p.getLocation().getX(),
|
||||
p.getLocation().getY(),
|
||||
p.getLocation().getZ()
|
||||
);
|
||||
// Notify Velocity proxy so it skips duplicate fallback logging
|
||||
String key = p.getUniqueId() + "|" + cmd;
|
||||
p.sendPluginMessage(plugin, "mclogger:logged", key.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* World change (e.g. Nether / End portal)
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onWorldChange(PlayerChangedWorldEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("from_world", event.getFrom().getName());
|
||||
details.put("to_world", p.getWorld().getName());
|
||||
plugin.getDb().insertPlayerStat(
|
||||
"world_change",
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
event.getFrom().getName(),
|
||||
p.getWorld().getName(),
|
||||
details
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package de.simolzimol.mclogger.paper.listeners;
|
||||
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.EntityEffect;
|
||||
import org.bukkit.entity.*;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Logs player deaths with full context:
|
||||
* - cause of death, killer, lost items, XP level
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class PlayerDeathListener implements Listener {
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
|
||||
public PlayerDeathListener(PaperLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onDeath(PlayerDeathEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
|
||||
String deathMsg = event.deathMessage() != null
|
||||
? PlainTextComponentSerializer.plainText().serialize(event.deathMessage())
|
||||
: "No death message";
|
||||
|
||||
// Cause of death
|
||||
String cause = "UNKNOWN";
|
||||
if (p.getLastDamageCause() != null) {
|
||||
cause = p.getLastDamageCause().getCause().name();
|
||||
}
|
||||
|
||||
// Determine killer
|
||||
String killerUuid = null, killerName = null, killerType = null;
|
||||
Entity killer = p.getKiller();
|
||||
if (killer != null) {
|
||||
killerType = killer.getType().name();
|
||||
killerName = killer instanceof Player
|
||||
? ((Player) killer).getName()
|
||||
: (killer.customName() != null
|
||||
? PlainTextComponentSerializer.plainText().serialize(killer.customName())
|
||||
: killer.getType().name());
|
||||
killerUuid = killer.getUniqueId().toString();
|
||||
} else if (p.getLastDamageCause() != null) {
|
||||
killerType = p.getLastDamageCause().getCause().name();
|
||||
}
|
||||
|
||||
// Serialize lost items
|
||||
Map<String, Object> itemsLost = new LinkedHashMap<>();
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (ItemStack item : event.getDrops()) {
|
||||
if (item == null) continue;
|
||||
Map<String, Object> itemMap = new HashMap<>();
|
||||
itemMap.put("type", item.getType().name());
|
||||
itemMap.put("amount", item.getAmount());
|
||||
if (item.hasItemMeta() && item.getItemMeta().hasDisplayName()) {
|
||||
itemMap.put("name", PlainTextComponentSerializer.plainText()
|
||||
.serialize(item.getItemMeta().displayName()));
|
||||
}
|
||||
items.add(itemMap);
|
||||
}
|
||||
itemsLost.put("items", items);
|
||||
|
||||
plugin.getDb().insertDeath(
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
p.getWorld().getName(),
|
||||
p.getLocation().getX(),
|
||||
p.getLocation().getY(),
|
||||
p.getLocation().getZ(),
|
||||
deathMsg,
|
||||
cause,
|
||||
killerUuid,
|
||||
killerName,
|
||||
killerType,
|
||||
p.getLevel(),
|
||||
itemsLost
|
||||
);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onRespawn(PlayerRespawnEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("is_anchor_spawn", event.isAnchorSpawn());
|
||||
details.put("is_bed_spawn", event.isBedSpawn());
|
||||
details.put("world", event.getRespawnLocation().getWorld().getName());
|
||||
plugin.getDb().insertPlayerStat(
|
||||
"respawn",
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
null, null, details
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package de.simolzimol.mclogger.paper.listeners;
|
||||
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.*;
|
||||
import org.bukkit.event.player.PlayerBedEnterEvent;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Miscellaneous player events: teleport, gamemode, bed, level,
|
||||
* arrow-shooting (as projectile), hand-swap, sleeping, etc.
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class PlayerMiscListener implements Listener {
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
|
||||
public PlayerMiscListener(PaperLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/** Teleport (all causes) */
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onTeleport(PlayerTeleportEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
if (event.getFrom().equals(event.getTo())) return; // no movement
|
||||
|
||||
String cause = event.getCause().name();
|
||||
// Only log relevant causes (COMMAND, PLUGIN, NETHER_PORTAL, etc.)
|
||||
plugin.getDb().insertTeleport(
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
event.getFrom().getWorld().getName(),
|
||||
event.getFrom().getX(), event.getFrom().getY(), event.getFrom().getZ(),
|
||||
event.getTo().getWorld().getName(),
|
||||
event.getTo().getX(), event.getTo().getY(), event.getTo().getZ(),
|
||||
cause
|
||||
);
|
||||
}
|
||||
|
||||
/** Gamemode change */
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onGameMode(PlayerGameModeChangeEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("cause", event.getCause().name());
|
||||
plugin.getDb().insertPlayerStat(
|
||||
"gamemode_change",
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
p.getGameMode().name(),
|
||||
event.getNewGameMode().name(),
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
/** Level change (XP) */
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onLevelChange(PlayerLevelChangeEvent event) {
|
||||
if (Math.abs(event.getNewLevel() - event.getOldLevel()) == 0) return;
|
||||
Player p = event.getPlayer();
|
||||
plugin.getDb().insertPlayerStat(
|
||||
"level_change",
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
String.valueOf(event.getOldLevel()),
|
||||
String.valueOf(event.getNewLevel()),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/** Enter bed */
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBedEnter(PlayerBedEnterEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("result", event.getBedEnterResult().name());
|
||||
details.put("world", event.getBed().getWorld().getName());
|
||||
details.put("x", event.getBed().getX());
|
||||
details.put("y", event.getBed().getY());
|
||||
details.put("z", event.getBed().getZ());
|
||||
plugin.getDb().insertPlayerStat("bed_enter", p.getUniqueId().toString(), p.getName(), null, null, details);
|
||||
}
|
||||
|
||||
/** Leave bed */
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onBedLeave(PlayerBedLeaveEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("world", event.getBed().getWorld().getName());
|
||||
plugin.getDb().insertPlayerStat("bed_leave", p.getUniqueId().toString(), p.getName(), null, null, details);
|
||||
}
|
||||
|
||||
/** Item held change (hand slot) */
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onItemHeld(PlayerItemHeldEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("from_slot", event.getPreviousSlot());
|
||||
details.put("to_slot", event.getNewSlot());
|
||||
String newItem = p.getInventory().getItem(event.getNewSlot()) != null
|
||||
? p.getInventory().getItem(event.getNewSlot()).getType().name() : "EMPTY";
|
||||
details.put("new_item", newItem);
|
||||
plugin.getDb().insertPlayerStat("item_held_change", p.getUniqueId().toString(), p.getName(),
|
||||
String.valueOf(event.getPreviousSlot()), String.valueOf(event.getNewSlot()), details);
|
||||
}
|
||||
|
||||
/** Swap hands (F key) */
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onSwapHands(PlayerSwapHandItemsEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
if (event.getMainHandItem() != null) details.put("main_hand", event.getMainHandItem().getType().name());
|
||||
if (event.getOffHandItem() != null) details.put("off_hand", event.getOffHandItem().getType().name());
|
||||
plugin.getDb().insertPlayerStat("swap_hands", p.getUniqueId().toString(), p.getName(), null, null, details);
|
||||
}
|
||||
|
||||
/** Fishing activity */
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onFish(PlayerFishEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("state", event.getState().name());
|
||||
if (event.getCaught() != null) details.put("caught", event.getCaught().getType().name());
|
||||
details.put("exp", event.getExpToDrop());
|
||||
plugin.getDb().insertPlayerStat("fish", p.getUniqueId().toString(), p.getName(), null, null, details);
|
||||
}
|
||||
|
||||
/** Player interaction with entity */
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onInteractEntity(PlayerInteractEntityEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("entity_type", event.getRightClicked().getType().name());
|
||||
details.put("entity_uuid", event.getRightClicked().getUniqueId().toString());
|
||||
details.put("hand", event.getHand().name());
|
||||
plugin.getDb().insertPlayerStat("interact_entity", p.getUniqueId().toString(), p.getName(), null, null, details);
|
||||
}
|
||||
|
||||
/** OP status changed */
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onOp(PlayerToggleSneakEvent event) {
|
||||
// used for OP-change – updated via upsertPlayer on the next join
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package de.simolzimol.mclogger.paper.listeners;
|
||||
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Handles login, logout and basic player session data.
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class PlayerSessionListener implements Listener {
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
/** Stores login timestamp per UUID for session duration calculation. */
|
||||
private final Map<UUID, Long> loginTimes = new ConcurrentHashMap<>();
|
||||
|
||||
public PlayerSessionListener(PaperLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login event: player has authenticated and is connecting.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onLogin(PlayerLoginEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
String ip = event.getAddress() != null ? event.getAddress().getHostAddress() : "unknown";
|
||||
|
||||
plugin.getDb().upsertPlayer(
|
||||
p.getUniqueId().toString(),
|
||||
p.getName(),
|
||||
p.getDisplayName().length() > 64 ? p.getDisplayName().substring(0, 64) : p.getDisplayName(),
|
||||
ip,
|
||||
p.locale().toLanguageTag(),
|
||||
p.isOp()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join event: player appears in-game.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
loginTimes.put(p.getUniqueId(), System.currentTimeMillis());
|
||||
|
||||
String ip = p.getAddress() != null
|
||||
? p.getAddress().getAddress().getHostAddress() : "unknown";
|
||||
|
||||
// Session is opened by the Velocity proxy on PostLoginEvent
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("join_message", event.joinMessage() != null ? event.joinMessage().toString() : "");
|
||||
details.put("first_join", !p.hasPlayedBefore());
|
||||
details.put("gamemode", p.getGameMode().name());
|
||||
plugin.getDb().insertServerEvent("player_join",
|
||||
p.getName() + " joined the server", details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quit event: player leaves the server.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
UUID uuid = p.getUniqueId();
|
||||
Long storedLogin = loginTimes.remove(uuid);
|
||||
long durationSec = storedLogin != null ? (System.currentTimeMillis() - storedLogin) / 1000 : 0;
|
||||
|
||||
// Session is closed by the Velocity proxy on DisconnectEvent
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("reason", event.getReason().name());
|
||||
details.put("playtime_sec", durationSec);
|
||||
plugin.getDb().insertServerEvent("player_quit",
|
||||
p.getName() + " left the server", details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick event: player was kicked.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onKick(PlayerKickEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("reason", event.getReason());
|
||||
details.put("cause", event.getCause().name());
|
||||
plugin.getDb().insertServerEvent("player_kick",
|
||||
p.getName() + " was kicked: " + event.getReason(), details);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package de.simolzimol.mclogger.paper.listeners;
|
||||
|
||||
import de.simolzimol.mclogger.paper.PaperLoggerPlugin;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.weather.ThunderChangeEvent;
|
||||
import org.bukkit.event.weather.WeatherChangeEvent;
|
||||
import org.bukkit.event.world.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Logs world events: weather, thunder, portal creation,
|
||||
* tree/mushroom growth, chunk load, etc.
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class WorldListener implements Listener {
|
||||
|
||||
private final PaperLoggerPlugin plugin;
|
||||
|
||||
public WorldListener(PaperLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onWeather(WeatherChangeEvent event) {
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("to_storm", event.toWeatherState());
|
||||
plugin.getDb().insertWorldEvent(
|
||||
"weather_change",
|
||||
event.getWorld().getName(),
|
||||
null, null, null,
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onThunder(ThunderChangeEvent event) {
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("to_thunder", event.toThunderState());
|
||||
plugin.getDb().insertWorldEvent(
|
||||
"thunder_change",
|
||||
event.getWorld().getName(),
|
||||
null, null, null,
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPortal(PortalCreateEvent event) {
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("reason", event.getReason().name());
|
||||
details.put("blocks", event.getBlocks().size());
|
||||
if (event.getEntity() != null) {
|
||||
details.put("entity", event.getEntity().getType().name());
|
||||
}
|
||||
plugin.getDb().insertWorldEvent(
|
||||
"portal_create",
|
||||
event.getWorld().getName(),
|
||||
event.getBlocks().isEmpty() ? null : (double) event.getBlocks().get(0).getLocation().getBlockX(),
|
||||
event.getBlocks().isEmpty() ? null : (double) event.getBlocks().get(0).getLocation().getBlockY(),
|
||||
event.getBlocks().isEmpty() ? null : (double) event.getBlocks().get(0).getLocation().getBlockZ(),
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onStructureGrow(StructureGrowEvent event) {
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("species", event.getSpecies().name());
|
||||
details.put("natural", event.isFromBonemeal());
|
||||
details.put("player", event.getPlayer() != null ? event.getPlayer().getName() : null);
|
||||
plugin.getDb().insertWorldEvent(
|
||||
"structure_grow",
|
||||
event.getWorld().getName(),
|
||||
(double) event.getLocation().getBlockX(),
|
||||
(double) event.getLocation().getBlockY(),
|
||||
(double) event.getLocation().getBlockZ(),
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onWorldLoad(WorldLoadEvent event) {
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("environment", event.getWorld().getEnvironment().name());
|
||||
details.put("seed", event.getWorld().getSeed());
|
||||
plugin.getDb().insertWorldEvent("world_load", event.getWorld().getName(), null, null, null, details);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onWorldUnload(WorldUnloadEvent event) {
|
||||
plugin.getDb().insertWorldEvent("world_unload", event.getWorld().getName(), null, null, null, null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user