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:
SimolZimol
2026-04-01 01:36:01 +02:00
commit b918dadb0c
109 changed files with 9196 additions and 0 deletions

113
paper-plugin/pom.xml Normal file
View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.simolzimol</groupId>
<artifactId>mclogger-paper</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>MCLogger-Paper</name>
<description>Comprehensive Minecraft event logger</description>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>lucko</id>
<url>https://repo.lucko.me/</url>
</repository>
</repositories>
<dependencies>
<!-- Paper API 1.21 -->
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- HikariCP Connection Pooling -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.1.0</version>
</dependency>
<!-- MariaDB JDBC Driver -->
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.4.1</version>
</dependency>
<!-- Gson für JSON-Serialisierung -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
<scope>provided</scope>
</dependency>
<!-- LuckPerms API (optional / soft-depend) -->
<dependency>
<groupId>net.luckperms</groupId>
<artifactId>api</artifactId>
<version>5.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<plugins>
<!-- Shade: alle Abhängigkeiten in eine JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<relocations>
<relocation>
<pattern>com.zaxxer.hikari</pattern>
<shadedPattern>de.simolzimol.mclogger.lib.hikari</shadedPattern>
</relocation>
<relocation>
<pattern>org.mariadb</pattern>
<shadedPattern>de.simolzimol.mclogger.lib.mariadb</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/LICENSE*</exclude>
<exclude>META-INF/NOTICE*</exclude>
<exclude>META-INF/DEPENDENCIES</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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; }
}

View File

@@ -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));
}
}

View File

@@ -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(); }
}

View File

@@ -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]
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}

View File

@@ -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
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,28 @@
# ============================================================
# MCLogger Paper Configuration
# Author: SimolZimol
# ============================================================
server:
# Unique name of this server (stored in the DB)
name: "survival-01"
database:
host: "localhost"
port: 3306
database: "mclogger"
username: "mclogger"
password: "change_me_please"
ssl: false
# Number of concurrent database connections
pool-size: 10
logging:
# Log block events (can be disabled under high traffic)
blocks: true
# Log entity spawns (WARNING: can produce a huge number of entries!)
entity-spawns: false
# Log inventory clicks (except crafting, enchanting)
inventory-clicks: true
# Only log destructive events (break/explode) = lower volume
blocks-break-only: false

View File

@@ -0,0 +1,59 @@
name: MCLogger
version: '1.0.0'
main: de.simolzimol.mclogger.paper.PaperLoggerPlugin
api-version: '1.20'
description: Minecraft event logger
author: SimolZimol
softdepend:
- LuckPerms
commands:
mclogger:
description: MCLogger admin command
usage: /mclogger <help|reload|status|chat|commands|deaths|sessions|blocks|perms>
permission: mclogger.use
aliases:
- mcl
- mlog
permissions:
mclogger.use:
description: Access to MCLogger commands
default: op
children:
mclogger.admin: true
mclogger.view.chat: true
mclogger.view.commands: true
mclogger.view.deaths: true
mclogger.view.sessions: true
mclogger.view.blocks: true
mclogger.view.perms: true
mclogger.admin:
description: Full access to MCLogger (reload, status)
default: op
mclogger.view.chat:
description: View chat logs in-game
default: op
mclogger.view.commands:
description: View command logs in-game
default: op
mclogger.view.deaths:
description: View death logs in-game
default: op
mclogger.view.sessions:
description: View session logs in-game
default: op
mclogger.view.blocks:
description: View block logs in-game
default: op
mclogger.view.perms:
description: View LuckPerms permission changes in-game
default: op

View File

@@ -0,0 +1,28 @@
# ============================================================
# MCLogger Paper Configuration
# Author: SimolZimol
# ============================================================
server:
# Unique name of this server (stored in the DB)
name: "survival-01"
database:
host: "localhost"
port: 3306
database: "mclogger"
username: "mclogger"
password: "change_me_please"
ssl: false
# Number of concurrent database connections
pool-size: 10
logging:
# Log block events (can be disabled under high traffic)
blocks: true
# Log entity spawns (WARNING: can produce a huge number of entries!)
entity-spawns: false
# Log inventory clicks (except crafting, enchanting)
inventory-clicks: true
# Only log destructive events (break/explode) = lower volume
blocks-break-only: false

View File

@@ -0,0 +1,59 @@
name: MCLogger
version: '1.0.0'
main: de.simolzimol.mclogger.paper.PaperLoggerPlugin
api-version: '1.20'
description: Minecraft event logger
author: SimolZimol
softdepend:
- LuckPerms
commands:
mclogger:
description: MCLogger admin command
usage: /mclogger <help|reload|status|chat|commands|deaths|sessions|blocks|perms>
permission: mclogger.use
aliases:
- mcl
- mlog
permissions:
mclogger.use:
description: Access to MCLogger commands
default: op
children:
mclogger.admin: true
mclogger.view.chat: true
mclogger.view.commands: true
mclogger.view.deaths: true
mclogger.view.sessions: true
mclogger.view.blocks: true
mclogger.view.perms: true
mclogger.admin:
description: Full access to MCLogger (reload, status)
default: op
mclogger.view.chat:
description: View chat logs in-game
default: op
mclogger.view.commands:
description: View command logs in-game
default: op
mclogger.view.deaths:
description: View death logs in-game
default: op
mclogger.view.sessions:
description: View session logs in-game
default: op
mclogger.view.blocks:
description: View block logs in-game
default: op
mclogger.view.perms:
description: View LuckPerms permission changes in-game
default: op

View File

@@ -0,0 +1,3 @@
artifactId=mclogger-paper
groupId=de.simolzimol
version=1.0.0

View File

@@ -0,0 +1,16 @@
de\simolzimol\mclogger\paper\commands\MCLoggerCommand.class
de\simolzimol\mclogger\paper\database\DatabaseManager$ThrowingRunnable.class
de\simolzimol\mclogger\paper\PaperLoggerPlugin.class
de\simolzimol\mclogger\paper\listeners\InventoryListener.class
de\simolzimol\mclogger\paper\database\DatabaseManager.class
de\simolzimol\mclogger\paper\listeners\LuckPermsListener$1.class
de\simolzimol\mclogger\paper\listeners\EntityListener.class
de\simolzimol\mclogger\paper\listeners\PlayerMiscListener.class
de\simolzimol\mclogger\paper\listeners\PlayerChatCommandListener.class
de\simolzimol\mclogger\paper\listeners\BlockListener.class
de\simolzimol\mclogger\paper\listeners\PlayerDeathListener.class
de\simolzimol\mclogger\paper\listeners\LuckPermsListener.class
de\simolzimol\mclogger\paper\listeners\WorldListener.class
de\simolzimol\mclogger\paper\listeners\PlayerSessionListener.class
de\simolzimol\mclogger\paper\commands\MCLoggerCommand$RsConsumer.class
de\simolzimol\mclogger\paper\listeners\InventoryListener$1.class

View File

@@ -0,0 +1,12 @@
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\commands\MCLoggerCommand.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\database\DatabaseManager.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\BlockListener.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\EntityListener.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\InventoryListener.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\LuckPermsListener.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerChatCommandListener.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerDeathListener.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerMiscListener.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerSessionListener.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\WorldListener.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\PaperLoggerPlugin.java

Binary file not shown.

Binary file not shown.