new file: .gitignore

new file:   README.md
	new file:   database/schema.sql
	new file:   paper-plugin/pom.xml
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/PaperLoggerPlugin.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/commands/MCLoggerCommand.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/database/DatabaseManager.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/BlockListener.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/EntityListener.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/InventoryListener.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/LuckPermsListener.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerChatCommandListener.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerDeathListener.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerMiscListener.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerSessionListener.java
	new file:   paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/WorldListener.java
	new file:   paper-plugin/src/main/resources/config.yml
	new file:   paper-plugin/src/main/resources/plugin.yml
	new file:   paper-plugin/target/classes/config.yml
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/PaperLoggerPlugin.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand$RsConsumer.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager$ThrowingRunnable.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/BlockListener.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/EntityListener.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/InventoryListener.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/LuckPermsListener.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerChatCommandListener.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerDeathListener.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerMiscListener.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerSessionListener.class
	new file:   paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/WorldListener.class
	new file:   paper-plugin/target/classes/plugin.yml
	new file:   paper-plugin/target/maven-archiver/pom.properties
	new file:   paper-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
	new file:   paper-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
	new file:   paper-plugin/target/mclogger-paper-1.0.0.jar
	new file:   paper-plugin/target/original-mclogger-paper-1.0.0.jar
	new file:   velocity-plugin/pom.xml
	new file:   velocity-plugin/src/main/java/de/simolzimol/mclogger/velocity/VelocityLoggerPlugin.java
	new file:   velocity-plugin/src/main/java/de/simolzimol/mclogger/velocity/database/VelocityDatabaseManager.java
	new file:   velocity-plugin/src/main/java/de/simolzimol/mclogger/velocity/listeners/VelocityEventListener.java
	new file:   velocity-plugin/src/main/resources/velocity-config.yml
	new file:   velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/VelocityLoggerPlugin.class
	new file:   velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/database/VelocityDatabaseManager$ThrowingRunnable.class
	new file:   velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/database/VelocityDatabaseManager.class
	new file:   velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/listeners/VelocityEventListener.class
	new file:   velocity-plugin/target/classes/velocity-config.yml
	new file:   velocity-plugin/target/classes/velocity-plugin.json
	new file:   velocity-plugin/target/maven-archiver/pom.properties
	new file:   velocity-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
	new file:   velocity-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
	new file:   velocity-plugin/target/mclogger-velocity-1.0.0.jar
	new file:   velocity-plugin/target/original-mclogger-velocity-1.0.0.jar
	new file:   web/Dockerfile
	new file:   web/app.py
	new file:   web/blueprints/__init__.py
	new file:   web/blueprints/auth.py
	new file:   web/blueprints/group_admin.py
	new file:   web/blueprints/panel.py
	new file:   web/blueprints/site_admin.py
	new file:   web/config.py
	new file:   web/crypto.py
	new file:   web/docker-compose.yml
	new file:   web/panel_db.py
	new file:   web/requirements.txt
	new file:   web/static/css/style.css
	new file:   web/static/js/main.js
	new file:   web/templates/_pagination.html
	new file:   web/templates/admin/base.html
	new file:   web/templates/admin/dashboard.html
	new file:   web/templates/admin/group_edit.html
	new file:   web/templates/admin/group_members.html
	new file:   web/templates/admin/groups.html
	new file:   web/templates/admin/user_edit.html
	new file:   web/templates/admin/users.html
	new file:   web/templates/auth/admin_login.html
	new file:   web/templates/auth/login.html
	new file:   web/templates/base.html
	new file:   web/templates/blocks.html
	new file:   web/templates/chat.html
	new file:   web/templates/commands.html
	new file:   web/templates/dashboard.html
	new file:   web/templates/deaths.html
	new file:   web/templates/group_admin/base.html
	new file:   web/templates/group_admin/dashboard.html
	new file:   web/templates/group_admin/database.html
	new file:   web/templates/group_admin/member_edit.html
	new file:   web/templates/group_admin/members.html
	new file:   web/templates/login.html
	new file:   web/templates/panel/blocks.html
	new file:   web/templates/panel/chat.html
	new file:   web/templates/panel/commands.html
	new file:   web/templates/panel/dashboard.html
	new file:   web/templates/panel/deaths.html
	new file:   web/templates/panel/no_db.html
	new file:   web/templates/panel/perms.html
	new file:   web/templates/panel/player_detail.html
	new file:   web/templates/panel/players.html
	new file:   web/templates/panel/proxy.html
	new file:   web/templates/panel/server_events.html
	new file:   web/templates/panel/sessions.html
	new file:   web/templates/perms.html
	new file:   web/templates/player_detail.html
	new file:   web/templates/players.html
	new file:   web/templates/proxy.html
	new file:   web/templates/server_events.html
	new file:   web/templates/sessions.html
This commit is contained in:
SimolZimol
2026-04-01 01:36:01 +02:00
commit b918dadb0c
109 changed files with 9196 additions and 0 deletions

117
velocity-plugin/pom.xml Normal file
View File

@@ -0,0 +1,117 @@
<?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-velocity</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>MCLogger-Velocity</name>
<description>Comprehensive Minecraft proxy event logger for Velocity with MariaDB storage</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>
</repositories>
<dependencies>
<!-- Velocity API -->
<dependency>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>3.3.0-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 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<plugins>
<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.velocity.lib.hikari</shadedPattern>
</relocation>
<relocation>
<pattern>org.mariadb</pattern>
<shadedPattern>de.simolzimol.mclogger.velocity.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>
<!-- Annotation Processor für Velocity -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>3.3.0-SNAPSHOT</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,121 @@
package de.simolzimol.mclogger.velocity;
import com.google.inject.Inject;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import de.simolzimol.mclogger.velocity.database.VelocityDatabaseManager;
import de.simolzimol.mclogger.velocity.listeners.VelocityEventListener;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import org.slf4j.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
/**
* MCLogger Velocity Proxy Plugin entry point
*
* @author SimolZimol
* @version 1.0.0
*/
@Plugin(
id = "mclogger-velocity",
name = "MCLogger-Velocity",
version = "1.0.0",
description = "Comprehensive proxy event logger with MariaDB storage",
authors = {"SimolZimol"},
url = "https://github.com/SimolZimol/MCLogger"
)
public class VelocityLoggerPlugin {
private final ProxyServer server;
private final Logger logger;
private final Path dataDirectory;
private VelocityDatabaseManager db;
@Inject
public VelocityLoggerPlugin(ProxyServer server, Logger logger,
@DataDirectory Path dataDirectory) {
this.server = server;
this.logger = logger;
this.dataDirectory = dataDirectory;
}
@Subscribe
public void onInitialize(ProxyInitializeEvent event) {
// Load configuration
ConfigurationNode cfg = loadConfig();
if (cfg == null) {
logger.error("[MCLogger] Failed to load configuration!");
return;
}
// Database connection
db = new VelocityDatabaseManager(this);
if (!db.connect(cfg)) {
logger.error("[MCLogger] No database connection plugin inactive.");
return;
}
// Register plugin messaging channel incoming from Paper backends
server.getChannelRegistrar().register(MinecraftChannelIdentifier.from("mclogger:logged"));
// Register listeners
server.getEventManager().register(this, new VelocityEventListener(this));
// Log proxy start
Map<String, Object> details = new HashMap<>();
details.put("velocity_version", server.getVersion().getVersion());
details.put("max_players", server.getConfiguration().getShowMaxPlayers());
db.insertProxyEvent("proxy_start", null, null, null, null, null, details);
logger.info("[MCLogger] Velocity plugin started! Proxy: " + db.getProxyName());
}
@Subscribe
public void onShutdown(ProxyShutdownEvent event) {
if (db != null && db.isConnected()) {
Map<String, Object> details = new HashMap<>();
details.put("online_players", server.getPlayerCount());
db.insertProxyEvent("proxy_stop", null, null, null, null, null, details);
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
db.disconnect();
}
logger.info("[MCLogger] Velocity plugin shut down.");
}
private ConfigurationNode loadConfig() {
try {
if (!Files.exists(dataDirectory)) {
Files.createDirectories(dataDirectory);
}
Path configFile = dataDirectory.resolve("config.yml");
if (!Files.exists(configFile)) {
try (InputStream in = getClass().getResourceAsStream("/velocity-config.yml")) {
if (in != null) Files.copy(in, configFile);
}
}
YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
.path(configFile)
.build();
return loader.load();
} catch (IOException e) {
logger.error("[MCLogger] Error loading configuration: " + e.getMessage());
return null;
}
}
public ProxyServer getServer() { return server; }
public Logger getLogger() { return logger; }
public VelocityDatabaseManager getDb() { return db; }
}

View File

@@ -0,0 +1,262 @@
package de.simolzimol.mclogger.velocity.database;
import com.google.gson.Gson;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import de.simolzimol.mclogger.velocity.VelocityLoggerPlugin;
import org.spongepowered.configurate.ConfigurationNode;
import java.sql.*;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
/**
* MariaDB manager for the Velocity plugin.
* Writes proxy events asynchronously to the database.
*
* @author SimolZimol
*/
public class VelocityDatabaseManager {
private final VelocityLoggerPlugin plugin;
private HikariDataSource dataSource;
private String proxyName;
private static final Gson GSON = new Gson();
public VelocityDatabaseManager(VelocityLoggerPlugin plugin) {
this.plugin = plugin;
}
// --------------------------------------------------------
// Connection
// --------------------------------------------------------
public boolean connect(ConfigurationNode cfg) {
proxyName = cfg.node("proxy", "name").getString("proxy-01");
HikariConfig hk = new HikariConfig();
hk.setDriverClassName("de.simolzimol.mclogger.velocity.lib.mariadb.jdbc.Driver");
hk.setJdbcUrl(String.format("jdbc:mariadb://%s:%d/%s?useSSL=%b&autoReconnect=true&characterEncoding=UTF-8",
cfg.node("database", "host").getString("localhost"),
cfg.node("database", "port").getInt(3306),
cfg.node("database", "database").getString("mclogger"),
cfg.node("database", "ssl").getBoolean(false)));
hk.setUsername(cfg.node("database", "username").getString("root"));
hk.setPassword(cfg.node("database", "password").getString(""));
hk.setMaximumPoolSize(cfg.node("database", "pool-size").getInt(5));
hk.setMinimumIdle(1);
hk.setConnectionTimeout(30_000);
hk.setIdleTimeout(600_000);
hk.setPoolName("MCLogger-Velocity");
try {
dataSource = new HikariDataSource(hk);
registerProxy(cfg);
plugin.getLogger().info("MCLogger-Velocity: Database connected - Proxy: " + proxyName);
return true;
} catch (Exception e) {
plugin.getLogger().error("MCLogger-Velocity: Database connection failed!", e);
return false;
}
}
public void disconnect() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
}
}
private void registerProxy(ConfigurationNode cfg) throws SQLException {
try (Connection con = dataSource.getConnection();
PreparedStatement ps = con.prepareStatement(
"INSERT INTO servers (server_name, server_type) VALUES (?,?) " +
"ON DUPLICATE KEY UPDATE last_seen = CURRENT_TIMESTAMP(3)")) {
ps.setString(1, proxyName);
ps.setString(2, "velocity");
ps.executeUpdate();
}
}
Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
// --------------------------------------------------------
// Async helper
// --------------------------------------------------------
private void asyncExec(ThrowingRunnable r) {
CompletableFuture.runAsync(() -> {
try {
r.run();
} catch (Exception e) {
plugin.getLogger().warn("MCLogger-Velocity: DB error: " + e.getMessage());
}
});
}
@FunctionalInterface
interface ThrowingRunnable {
void run() throws Exception;
}
// --------------------------------------------------------
// Players
// --------------------------------------------------------
public void upsertPlayer(String uuid, String username, String ip) {
asyncExec(() -> {
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(
"INSERT INTO players (uuid, username, ip_address) VALUES (?,?,?) " +
"ON DUPLICATE KEY UPDATE username=VALUES(username), ip_address=VALUES(ip_address), last_seen=CURRENT_TIMESTAMP(3)")) {
ps.setString(1, uuid);
ps.setString(2, username);
ps.setString(3, ip);
ps.executeUpdate();
}
});
}
// --------------------------------------------------------
// Proxy Events (central table)
// --------------------------------------------------------
public void insertProxyEvent(String type, String playerUuid, String playerName,
String fromServer, String toServer,
String ip, Map<String, Object> details) {
asyncExec(() -> {
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(
"INSERT INTO proxy_events (event_type, player_uuid, player_name, proxy_name, from_server, to_server, ip_address, details) " +
"VALUES (?,?,?,?,?,?,?,?)")) {
ps.setString(1, type);
ps.setString(2, playerUuid);
ps.setString(3, playerName);
ps.setString(4, proxyName);
ps.setString(5, fromServer);
ps.setString(6, toServer);
ps.setString(7, ip);
ps.setString(8, details != null ? GSON.toJson(details) : null);
ps.executeUpdate();
}
});
}
// --------------------------------------------------------
// Chat & Commands (shared tables)
// --------------------------------------------------------
public void insertChat(String uuid, String name, String message) {
insertChatWithServer(uuid, name, proxyName, message);
}
public void insertChatWithServer(String uuid, String name, String serverName, String message) {
asyncExec(() -> {
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(
"INSERT INTO player_chat (player_uuid, player_name, server_name, message, channel) VALUES (?,?,?,?,?)")) {
ps.setString(1, uuid);
ps.setString(2, name);
ps.setString(3, serverName);
ps.setString(4, message);
ps.setString(5, "global");
ps.executeUpdate();
}
});
}
public void insertCommand(String uuid, String name, String command) {
insertCommandWithServer(uuid, name, proxyName, command);
}
public void insertCommandWithServer(String uuid, String name, String serverName, String command) {
asyncExec(() -> {
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(
"INSERT INTO player_commands (player_uuid, player_name, server_name, command) VALUES (?,?,?,?)")) {
ps.setString(1, uuid);
ps.setString(2, name);
ps.setString(3, serverName);
ps.setString(4, command);
ps.executeUpdate();
}
});
}
// --------------------------------------------------------
// Sessions
// --------------------------------------------------------
/** Opens a new session row. server_name is updated on disconnect to the last known backend. */
public void insertSessionLogin(String uuid, String name, String ip, String clientBrand) {
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, proxyName);
ps.setString(4, ip);
ps.setString(5, country);
ps.setString(6, clientBrand);
ps.executeUpdate();
}
});
}
/** Closes the open session, sets the actual backend server name and duration. */
public void closeSession(String uuid, String lastServer, long durationSec) {
asyncExec(() -> {
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(
"UPDATE player_sessions SET logout_time=CURRENT_TIMESTAMP(3), duration_sec=?, server_name=? " +
"WHERE player_uuid=? AND logout_time IS NULL ORDER BY login_time DESC LIMIT 1")) {
ps.setLong(1, durationSec);
ps.setString(2, lastServer);
ps.setString(3, uuid);
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();
}
});
}
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);
com.google.gson.JsonObject obj = new com.google.gson.Gson().fromJson(sb.toString(), com.google.gson.JsonObject.class);
if (obj != null && obj.has("country")) return obj.get("country").getAsString();
}
} catch (Exception ignored) {}
return "Unknown";
}
// --------------------------------------------------------
// Getter
// --------------------------------------------------------
public String getProxyName() { return proxyName; }
public boolean isConnected() { return dataSource != null && !dataSource.isClosed(); }
}

View File

@@ -0,0 +1,232 @@
package de.simolzimol.mclogger.velocity.listeners;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.command.CommandExecuteEvent;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.event.player.PlayerChatEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.proxy.Player;
import de.simolzimol.mclogger.velocity.VelocityLoggerPlugin;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* All Velocity event listeners:
* - Login / Disconnect
* - Server switch
* - Chat
* - Commands
*
* @author SimolZimol
*/
public class VelocityEventListener {
private final VelocityLoggerPlugin plugin;
/** Login timestamps for session duration calculation */
private final Map<UUID, Long> loginTimes = new ConcurrentHashMap<>();
/** Commands already logged by a Paper backend Velocity skips these */
private final Set<String> paperLoggedCommands = ConcurrentHashMap.newKeySet();
public VelocityEventListener(VelocityLoggerPlugin plugin) {
this.plugin = plugin;
}
// --------------------------------------------------------
// Login
// --------------------------------------------------------
@Subscribe(order = PostOrder.LAST)
public void onLogin(PostLoginEvent event) {
Player p = event.getPlayer();
loginTimes.put(p.getUniqueId(), System.currentTimeMillis());
String ip = p.getRemoteAddress() != null
? p.getRemoteAddress().getAddress().getHostAddress() : "unknown";
// Upsert player in DB
plugin.getDb().upsertPlayer(p.getUniqueId().toString(), p.getUsername(), ip);
// Open session row (server_name is filled in on disconnect with last known backend)
String clientBrand = p.getClientBrand() != null ? p.getClientBrand() : "unknown";
plugin.getDb().insertSessionLogin(p.getUniqueId().toString(), p.getUsername(), ip, clientBrand);
Map<String, Object> details = new HashMap<>();
details.put("ip", ip);
details.put("client_brand", p.getClientBrand() != null ? p.getClientBrand() : "unknown");
details.put("protocol", p.getProtocolVersion().getProtocol());
details.put("ping", p.getPing());
details.put("online_mode", !p.isOnlineMode() ? "offline/bedrock" : "java");
plugin.getDb().insertProxyEvent(
"login",
p.getUniqueId().toString(),
p.getUsername(),
null, null, ip, details
);
plugin.getLogger().info("[MCLogger] LOGIN: " + p.getUsername() + " (" + ip + ")");
}
// --------------------------------------------------------
// Disconnect
// --------------------------------------------------------
@Subscribe(order = PostOrder.LAST)
public void onDisconnect(DisconnectEvent event) {
Player p = event.getPlayer();
UUID uuid = p.getUniqueId();
Long loginTimeMs = loginTimes.remove(uuid);
long durationSec = loginTimeMs != null ? (System.currentTimeMillis() - loginTimeMs) / 1000 : 0;
String lastServer = p.getCurrentServer()
.map(s -> s.getServerInfo().getName())
.orElse(plugin.getDb().getProxyName());
// Close the session opened in onLogin
plugin.getDb().closeSession(uuid.toString(), lastServer, durationSec);
Map<String, Object> details = new HashMap<>();
details.put("reason", event.getLoginStatus().name());
details.put("session_duration_sec", durationSec);
details.put("current_server", lastServer);
plugin.getDb().insertProxyEvent(
"disconnect",
uuid.toString(),
p.getUsername(),
p.getCurrentServer().map(s -> s.getServerInfo().getName()).orElse(null),
null, null, details
);
plugin.getLogger().info("[MCLogger] DISCONNECT: " + p.getUsername() +
" (duration: " + durationSec + "s, reason: " + event.getLoginStatus().name() + ")");
}
// --------------------------------------------------------
// Server switch (after switching)
// --------------------------------------------------------
@Subscribe(order = PostOrder.LAST)
public void onServerConnected(ServerConnectedEvent event) {
Player p = event.getPlayer();
String from = event.getPreviousServer().map(s -> s.getServerInfo().getName()).orElse(null);
String to = event.getServer().getServerInfo().getName();
Map<String, Object> details = new HashMap<>();
details.put("ping", p.getPing());
plugin.getDb().insertProxyEvent(
"server_switch",
p.getUniqueId().toString(),
p.getUsername(),
from, to, null, details
);
plugin.getLogger().info("[MCLogger] SERVER-SWITCH: " + p.getUsername() +
" | " + (from != null ? from : "joining") + "" + to);
}
// --------------------------------------------------------
// Server connect attempt (before switching)
// --------------------------------------------------------
@Subscribe(order = PostOrder.LAST)
public void onServerPreConnect(ServerPreConnectEvent event) {
if (event.getResult().getServer().isEmpty()) return;
Player p = event.getPlayer();
Map<String, Object> details = new HashMap<>();
details.put("target_server", event.getResult().getServer().get().getServerInfo().getName());
plugin.getDb().insertProxyEvent(
"server_switch",
p.getUniqueId().toString(),
p.getUsername(),
p.getCurrentServer().map(s -> s.getServerInfo().getName()).orElse(null),
event.getResult().getServer().get().getServerInfo().getName(),
null, details
);
}
// --------------------------------------------------------
// Commands only proxy-native commands (e.g. /server, /glist)
// Commands Paper logs with position and notifies proxy; Velocity is fallback.
// --------------------------------------------------------
/**
* Receives plugin messages from Paper backends.
* When Paper has logged a command, it sends the key here so Velocity skips it.
*/
@Subscribe
public void onPluginMessage(PluginMessageEvent event) {
if (!event.getIdentifier().getId().equals("mclogger:logged")) return;
// Prevent Velocity from forwarding this internal message to the client
event.setResult(PluginMessageEvent.ForwardResult.handled());
String key = new String(event.getData(), StandardCharsets.UTF_8);
paperLoggedCommands.add(key);
}
/**
* Schedules command logging with a 300 ms delay.
* If a Paper backend signals it already logged the command, the task is cancelled.
* On servers without the Paper plugin, Velocity logs it as fallback.
*/
@Subscribe(order = PostOrder.LAST)
public void onCommand(CommandExecuteEvent event) {
if (!(event.getCommandSource() instanceof Player)) return;
Player p = (Player) event.getCommandSource();
String rawCommand = event.getCommand();
// Key format matches what Paper sends: uuid|/command args
String key = p.getUniqueId() + "|/" + rawCommand;
String serverName = p.getCurrentServer()
.map(s -> s.getServerInfo().getName())
.orElse(plugin.getDb().getProxyName());
plugin.getServer().getScheduler()
.buildTask(plugin, () -> {
// If Paper already sent us a confirmation, skip avoid duplicate
if (paperLoggedCommands.remove(key)) return;
plugin.getDb().insertCommandWithServer(
p.getUniqueId().toString(),
p.getUsername(),
serverName,
"/" + rawCommand
);
})
.delay(Duration.ofMillis(300))
.schedule();
}
// Note: Chat is logged at proxy level (see onChat below).
// --------------------------------------------------------
// Chat logged at proxy level with correct backend server name
// --------------------------------------------------------
@Subscribe(order = PostOrder.LAST)
public void onChat(PlayerChatEvent event) {
if (!event.getResult().isAllowed()) return;
Player p = event.getPlayer();
String serverName = p.getCurrentServer()
.map(s -> s.getServerInfo().getName())
.orElse(plugin.getDb().getProxyName());
plugin.getDb().insertChatWithServer(
p.getUniqueId().toString(),
p.getUsername(),
serverName,
event.getMessage()
);
}
}

View File

@@ -0,0 +1,17 @@
# ============================================================
# MCLogger Velocity Konfiguration
# Author: SimolZimol
# ============================================================
proxy:
# Eindeutiger Name dieses Proxies (wird in der DB gespeichert)
name: "proxy-01"
database:
host: "localhost"
port: 3306
database: "mclogger"
username: "mclogger"
password: "change_me_please"
ssl: false
pool-size: 5

View File

@@ -0,0 +1,17 @@
# ============================================================
# MCLogger Velocity Konfiguration
# Author: SimolZimol
# ============================================================
proxy:
# Eindeutiger Name dieses Proxies (wird in der DB gespeichert)
name: "proxy-01"
database:
host: "localhost"
port: 3306
database: "mclogger"
username: "mclogger"
password: "change_me_please"
ssl: false
pool-size: 5

View File

@@ -0,0 +1 @@
{"id":"mclogger-velocity","name":"MCLogger-Velocity","version":"1.0.0","description":"Comprehensive proxy event logger with MariaDB storage","url":"https://github.com/SimolZimol/MCLogger","authors":["SimolZimol"],"dependencies":[],"main":"de.simolzimol.mclogger.velocity.VelocityLoggerPlugin"}

View File

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

View File

@@ -0,0 +1,5 @@
de\simolzimol\mclogger\velocity\database\VelocityDatabaseManager.class
velocity-plugin.json
de\simolzimol\mclogger\velocity\database\VelocityDatabaseManager$ThrowingRunnable.class
de\simolzimol\mclogger\velocity\listeners\VelocityEventListener.class
de\simolzimol\mclogger\velocity\VelocityLoggerPlugin.class

View File

@@ -0,0 +1,3 @@
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\velocity-plugin\src\main\java\de\simolzimol\mclogger\velocity\database\VelocityDatabaseManager.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\velocity-plugin\src\main\java\de\simolzimol\mclogger\velocity\listeners\VelocityEventListener.java
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\velocity-plugin\src\main\java\de\simolzimol\mclogger\velocity\VelocityLoggerPlugin.java

Binary file not shown.