diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61c2119..0dd2b6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,11 +18,11 @@ jobs: - name: Check out repository uses: actions/checkout@v6 - - name: Set up Java 21 + - name: Set up Java 25 uses: actions/setup-java@v5 with: distribution: temurin - java-version: "21" + java-version: "25" cache: gradle - name: Validate Gradle Wrapper diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a37f16..5beef0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,11 +16,11 @@ jobs: - name: Check out repository uses: actions/checkout@v6 - - name: Set up Java 21 + - name: Set up Java 25 uses: actions/setup-java@v5 with: distribution: temurin - java-version: "21" + java-version: "25" cache: gradle - name: Validate Gradle Wrapper diff --git a/README.md b/README.md index abe2516..16b9ee9 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,18 @@ [![CI](https://img.shields.io/github/actions/workflow/status/minecraft-gilde/importer/ci.yml?branch=main&label=build)](https://github.com/minecraft-gilde/importer/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/minecraft-gilde/importer?label=release&cacheSeconds=300)](https://github.com/minecraft-gilde/importer/releases) [![License](https://img.shields.io/github/license/minecraft-gilde/importer)](LICENSE) -![Java](https://img.shields.io/badge/Java-21-orange) -![Paper](https://img.shields.io/badge/Paper-1.21.x-blue) +![Java](https://img.shields.io/badge/Java-25-orange) +![Paper](https://img.shields.io/badge/Paper-26.1-blue) ![Folia](https://img.shields.io/badge/Folia-supported-brightgreen) [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.minecraft-gilde.de) Dieses Repository enthält ein Java-Plugin für den Import von Minecraft-Stats auf Paper/Folia. +## Voraussetzungen + +- Java 25 (Build und Runtime) +- Paper/Folia 26.1 + ## Dokumentation Die ausführliche Projektdokumentation liegt unter: @@ -30,7 +35,7 @@ Windows PowerShell: Ergebnis-JAR: -- `build/libs/stats-importer-plugin-1.0.0.jar` +- `build/libs/stats-importer-plugin-.jar` ## Konfiguration @@ -45,7 +50,7 @@ Wichtig: - `import.safety.*`: Mindestwerte gegen falsche/leere Stats-Pfade - `import.safety.max-parse-errors`: Grenze für kaputte Stats-JSONs vor Snapshot-Veröffentlichung - `import.retention.keep-runs`: Aufbewahrung alter Snapshot-Runs -- `import.stats-dir`: `auto` nutzt den Standardpfad des Servers (`/stats`) +- `import.stats-dir`: `auto` nutzt den Paper/Folia-26.1-Standardpfad (`/players/stats`) - `import.usercache-path`: `auto` nutzt `/usercache.json` - `import.banned-players-path`: `auto` nutzt `/banned-players.json` - `import.worker-threads`: Anzahl paralleler Threads für die Stat-Berechnung diff --git a/build.gradle.kts b/build.gradle.kts index bb221f1..a5449ca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,30 +4,38 @@ plugins { } group = "de.gilde" -version = providers.gradleProperty("releaseVersion").orElse("1.0.4").get() +val pluginVersion = providers.gradleProperty("releaseVersion").orElse("1.0.5").get() +version = pluginVersion repositories { mavenCentral() - maven("https://repo.papermc.io/repository/maven-public/") + maven { + name = "papermc" + url = uri("https://repo.papermc.io/repository/maven-public/") + } } dependencies { - compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT") + compileOnly("io.papermc.paper:paper-api:26.1.2.build.+") implementation("com.zaxxer:HikariCP:7.0.2") implementation("org.mariadb.jdbc:mariadb-java-client:3.5.8") implementation("com.fasterxml.jackson.core:jackson-databind:2.21.3") testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") - testImplementation("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT") + testImplementation("io.papermc.paper:paper-api:26.1.2.build.+") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(25)) +} + tasks.processResources { filesMatching(listOf("plugin.yml", "paper-plugin.yml")) { expand( mapOf( - "version" to project.version + "version" to pluginVersion ) ) } @@ -35,7 +43,7 @@ tasks.processResources { tasks.withType().configureEach { options.encoding = "UTF-8" - options.release.set(21) + options.release.set(25) } tasks.shadowJar { diff --git a/docs/README.md b/docs/README.md index 5049758..d25bbca 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,7 +20,7 @@ Sie richtet sich an zwei Zielgruppen: ## Kurzübersicht -`StatsImporter` importiert Minecraft-Statistiken aus `world/stats/*.json` in MariaDB und materialisiert daraus Metrikwerte für Leaderboards. +`StatsImporter` importiert Minecraft-Statistiken aus `world/players/stats/*.json` in MariaDB und materialisiert daraus Metrikwerte für Leaderboards. Kernmerkmale: diff --git a/docs/architektur.md b/docs/architektur.md index a89b487..e48432e 100644 --- a/docs/architektur.md +++ b/docs/architektur.md @@ -4,7 +4,7 @@ Das Plugin läuft innerhalb eines Paper/Folia Servers und übernimmt ETL-Aufgaben: -- Input: `world/stats/.json` + `usercache.json` + `banned-players.json` +- Input: `world/players/stats/.json` + `usercache.json` + `banned-players.json` - Verarbeitung: Filter, Normierung, Metrikberechnung, Hash-Vergleich - Output: Tabellen `player_profile`, `player_known`, `player_ban`, `player_stats`, `metric_value` (plus optional `metric_award`) diff --git a/docs/betrieb.md b/docs/betrieb.md index 38b994c..e8db13f 100644 --- a/docs/betrieb.md +++ b/docs/betrieb.md @@ -2,11 +2,11 @@ ## Voraussetzungen -- Java 21 (Build und Runtime) -- Paper/Folia (API `1.21`) +- Java 25 (Build und Runtime) +- Paper/Folia (API `26.1.2`) - MariaDB/MySQL-kompatible Datenbank (für Schema siehe `docs/datenbank.md`) - Schreibrechte für Plugin-Datenordner -- Leserechte auf `world/stats` und `usercache.json` +- Leserechte auf `world/players/stats` und `usercache.json` ## Build diff --git a/docs/datenbank.md b/docs/datenbank.md index c5b8443..7d390be 100644 --- a/docs/datenbank.md +++ b/docs/datenbank.md @@ -41,7 +41,7 @@ Ban-Zeitpunkte aus `banned-players.json` werden mit dem dort enthaltenen Offset - Name, Suchfeld `name_lc`, Quelleninfos, `last_seen` - `player_known` - persistente Known-Players-Quelle (run-unabhängig) - - wird aus `stats/*.json`, `usercache.json`, `banned-players.json` per Upsert gepflegt + - wird aus `players/stats/*.json`, `usercache.json`, `banned-players.json` per Upsert gepflegt - Name ist immer belegt (bei fehlender Quelle: deterministischer Fallback aus UUID) - Resolver-Metadatum `name_checked_at` steuert Refresh für Mojang-Rechecks - Felder für Statuslogik: `seen_in_stats`, `seen_in_usercache`, `seen_in_bans`, `first_seen`, `last_seen` diff --git a/docs/entwicklung.md b/docs/entwicklung.md index e709cd4..034664c 100644 --- a/docs/entwicklung.md +++ b/docs/entwicklung.md @@ -102,7 +102,7 @@ Bei Durchsatzproblemen iterativ anpassen: Die CI (`.github/workflows/ci.yml`) prüft aktuell: - Checkout -- Java 21 Setup +- Java 25 Setup - Gradle Wrapper Validation - `./gradlew build --no-daemon` diff --git a/docs/gesamtstruktur.md b/docs/gesamtstruktur.md index 0cfd901..6b608cf 100644 --- a/docs/gesamtstruktur.md +++ b/docs/gesamtstruktur.md @@ -4,7 +4,7 @@ Dieses Projekt ist der technische Kern für den Import und die Aufbereitung der ## Systemüberblick -1. Der Minecraft-Server liefert Rohdaten aus `world/stats/*.json`, `usercache.json` und `banned-players.json`. +1. Der Minecraft-Server liefert Rohdaten aus `world/players/stats/*.json`, `usercache.json` und `banned-players.json`. 2. Das Plugin `StatsImporter` verarbeitet diese Daten und schreibt sie nach MariaDB. 3. Die Website nutzt die aufbereiteten Daten über die Statistik-API. 4. Das Frontend zeigt darauf basierend Ranglisten, Spielersuche und Profile an. diff --git a/docs/konfiguration.md b/docs/konfiguration.md index d015b78..e2ea409 100644 --- a/docs/konfiguration.md +++ b/docs/konfiguration.md @@ -25,7 +25,7 @@ Default-Werte im Repository: | Key | Default | Bedeutung | |---|---:|---| -| `import.stats-dir` | `auto` | Pfad zu `world/stats`; `auto/default/standard/leer` wird automatisch aufgelöst | +| `import.stats-dir` | `auto` | Pfad zu `world/players/stats`; `auto/default/standard/leer` wird automatisch aufgelöst | | `import.usercache-path` | `auto` | Pfad zu `usercache.json`; bei `auto` wird `/usercache.json` genutzt | | `import.banned-players-path` | `auto` | Pfad zu `banned-players.json`; bei `auto` wird `/banned-players.json` genutzt | diff --git a/docs/schnittstellen.md b/docs/schnittstellen.md index 64fbd2d..68dc417 100644 --- a/docs/schnittstellen.md +++ b/docs/schnittstellen.md @@ -6,13 +6,13 @@ Dieses Kapitel beschreibt die fachlichen und technischen Schnittstellen des Impo Der Importer verarbeitet diese Dateien aus der Minecraft-Serverumgebung: -- `world/stats/.json` +- `world/players/stats/.json` - `usercache.json` - `banned-players.json` ### Erwartete Eigenschaften -- `stats/*.json` enthält pro Spieler UUID-basierte Rohstatistiken +- `players/stats/*.json` enthält pro Spieler UUID-basierte Rohstatistiken - `usercache.json` dient als primäre Namensquelle - `banned-players.json` liefert Ban-Snapshotdaten für die Anzeige diff --git a/src/main/java/de/gilde/statsimporter/importer/ImportCoordinator.java b/src/main/java/de/gilde/statsimporter/importer/ImportCoordinator.java index 56253eb..7b20689 100644 --- a/src/main/java/de/gilde/statsimporter/importer/ImportCoordinator.java +++ b/src/main/java/de/gilde/statsimporter/importer/ImportCoordinator.java @@ -193,13 +193,12 @@ private void runImport(String reason, boolean ignoreHashOverride, boolean dryRun String retentionNote = ""; Long candidateRunId = null; - Path statsDir = resolveStatsDir(); - Path usercachePath = resolveUsercachePath(); - Path bannedPlayersPath = resolveBannedPlayersPath(); - WorldAgeSnapshot worldAgeSnapshot = null; - try { - worldAgeSnapshot = resolveWorldAgeSnapshot(); + ServerSnapshot serverSnapshot = resolveServerSnapshot(); + Path statsDir = serverSnapshot.statsDir(); + Path usercachePath = serverSnapshot.usercachePath(); + Path bannedPlayersPath = serverSnapshot.bannedPlayersPath(); + WorldAgeSnapshot worldAgeSnapshot = serverSnapshot.worldAgeSnapshot(); if (!Files.isDirectory(statsDir)) { throw new IllegalStateException("stats-dir not found: " + statsDir); } @@ -2344,6 +2343,31 @@ private UUID parseUuidFromStatsFilename(Path file) { } } + private ServerSnapshot resolveServerSnapshot() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + try { + plugin.getServer().getGlobalRegionScheduler().execute(plugin, () -> { + try { + future.complete(captureServerSnapshot()); + } catch (Throwable throwable) { + future.completeExceptionally(throwable); + } + }); + } catch (RuntimeException ex) { + future.completeExceptionally(ex); + } + return future.get(30, TimeUnit.SECONDS); + } + + private ServerSnapshot captureServerSnapshot() { + return new ServerSnapshot( + resolveStatsDir(), + resolveUsercachePath(), + resolveBannedPlayersPath(), + resolveWorldAgeSnapshot() + ); + } + private Path resolveStatsDir() { String configured = settings.statsDirectory(); if (!isAuto(configured)) { @@ -2351,26 +2375,38 @@ private Path resolveStatsDir() { } Path worldContainer = plugin.getServer().getWorldContainer().toPath(); - List worldStats = plugin.getServer().getWorlds().stream() - .map(world -> world.getWorldFolder().toPath().resolve("stats")) + List worldFolders = plugin.getServer().getWorlds().stream() + .map(world -> world.getWorldFolder().toPath()) .distinct() .collect(Collectors.toList()); + List statsCandidates = statsDirCandidates(worldContainer, worldFolders); - for (Path candidate : worldStats) { + for (Path candidate : statsCandidates) { if (Files.isDirectory(candidate)) { return candidate; } } - Path vanillaDefault = worldContainer.resolve("world").resolve("stats"); - if (Files.isDirectory(vanillaDefault)) { - return vanillaDefault; + return statsCandidates.get(0); + } + + static List statsDirCandidates(Path worldContainer, List worldFolders) { + List candidates = new ArrayList<>(); + for (Path worldFolder : worldFolders) { + addStatsDirCandidates(candidates, worldFolder); } + addStatsDirCandidates(candidates, worldContainer.resolve("world")); + return candidates; + } - if (!worldStats.isEmpty()) { - return worldStats.get(0); + private static void addStatsDirCandidates(List candidates, Path worldFolder) { + addDistinctPath(candidates, worldFolder.resolve("players").resolve("stats")); + } + + private static void addDistinctPath(List candidates, Path candidate) { + if (!candidates.contains(candidate)) { + candidates.add(candidate); } - return vanillaDefault; } private Path resolveUsercachePath() { @@ -2460,6 +2496,14 @@ private record WorldAgeSnapshot( ) { } + private record ServerSnapshot( + Path statsDir, + Path usercachePath, + Path bannedPlayersPath, + WorldAgeSnapshot worldAgeSnapshot + ) { + } + private record BanEntry( UUID uuid, String name, diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index bb39162..2dcd035 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -3,7 +3,7 @@ enabled: true # Intervall für Timer-Imports in Sekunden. interval-seconds: 14400 - # Pfad zum Stats-Ordner (auto = automatisch erkennen). + # Pfad zum Stats-Ordner (auto = Paper/Folia 26.1 /players/stats). stats-dir: "auto" # Pfad zu usercache.json (auto = aus Server-Verzeichnis). usercache-path: "auto" diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index 6013921..a2e62cf 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -1,7 +1,7 @@ name: StatsImporter main: de.gilde.statsimporter.ImporterPlugin version: ${version} -api-version: "1.21" +api-version: "26.1.2" folia-supported: true author: CFPlusPlus description: Importiert Minecraft-Statistiken in eine MariaDB für Ranglisten-Metriken. diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 3b90066..0139bdd 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: StatsImporter main: de.gilde.statsimporter.ImporterPlugin version: ${version} -api-version: "1.21" +api-version: "26.1.2" folia-supported: true author: CFPlusPlus commands: diff --git a/src/test/java/de/gilde/statsimporter/importer/ImportCoordinatorTest.java b/src/test/java/de/gilde/statsimporter/importer/ImportCoordinatorTest.java new file mode 100644 index 0000000..87c8835 --- /dev/null +++ b/src/test/java/de/gilde/statsimporter/importer/ImportCoordinatorTest.java @@ -0,0 +1,45 @@ +package de.gilde.statsimporter.importer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ImportCoordinatorTest { + + @Test + void statsDirCandidatesUsePaper261PlayerStatsLayout() { + Path worldContainer = Path.of("server"); + Path overworld = worldContainer.resolve("world"); + Path nether = worldContainer.resolve("world_nether"); + + List candidates = ImportCoordinator.statsDirCandidates( + worldContainer, + List.of(overworld, nether) + ); + + assertEquals( + List.of( + overworld.resolve("players").resolve("stats"), + nether.resolve("players").resolve("stats") + ), + candidates + ); + } + + @Test + void statsDirCandidatesFallBackToDefaultWorldWhenNoWorldIsLoaded() { + Path worldContainer = Path.of("server"); + Path defaultWorld = worldContainer.resolve("world"); + + List candidates = ImportCoordinator.statsDirCandidates(worldContainer, List.of()); + + assertEquals( + List.of( + defaultWorld.resolve("players").resolve("stats") + ), + candidates + ); + } +}