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