Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -30,7 +35,7 @@ Windows PowerShell:

Ergebnis-JAR:

- `build/libs/stats-importer-plugin-1.0.0.jar`
- `build/libs/stats-importer-plugin-<version>.jar`

## Konfiguration

Expand All @@ -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 (`<world>/stats`)
- `import.stats-dir`: `auto` nutzt den Paper/Folia-26.1-Standardpfad (`<world>/players/stats`)
- `import.usercache-path`: `auto` nutzt `<server-root>/usercache.json`
- `import.banned-players-path`: `auto` nutzt `<server-root>/banned-players.json`
- `import.worker-threads`: Anzahl paralleler Threads für die Stat-Berechnung
Expand Down
20 changes: 14 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,46 @@ 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
)
)
}
}

tasks.withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
options.release.set(21)
options.release.set(25)
}

tasks.shadowJar {
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion docs/architektur.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Das Plugin läuft innerhalb eines Paper/Folia Servers und übernimmt ETL-Aufgaben:

- Input: `world/stats/<uuid>.json` + `usercache.json` + `banned-players.json`
- Input: `world/players/stats/<uuid>.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`)

Expand Down
6 changes: 3 additions & 3 deletions docs/betrieb.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/datenbank.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion docs/entwicklung.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
2 changes: 1 addition & 1 deletion docs/gesamtstruktur.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/konfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<worldContainer>/usercache.json` genutzt |
| `import.banned-players-path` | `auto` | Pfad zu `banned-players.json`; bei `auto` wird `<worldContainer>/banned-players.json` genutzt |

Expand Down
4 changes: 2 additions & 2 deletions docs/schnittstellen.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<uuid>.json`
- `world/players/stats/<uuid>.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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -2344,33 +2343,70 @@ private UUID parseUuidFromStatsFilename(Path file) {
}
}

private ServerSnapshot resolveServerSnapshot() throws Exception {
CompletableFuture<ServerSnapshot> 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)) {
return Path.of(configured);
}

Path worldContainer = plugin.getServer().getWorldContainer().toPath();
List<Path> worldStats = plugin.getServer().getWorlds().stream()
.map(world -> world.getWorldFolder().toPath().resolve("stats"))
List<Path> worldFolders = plugin.getServer().getWorlds().stream()
.map(world -> world.getWorldFolder().toPath())
.distinct()
.collect(Collectors.toList());
List<Path> 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<Path> statsDirCandidates(Path worldContainer, List<Path> worldFolders) {
List<Path> 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<Path> candidates, Path worldFolder) {
addDistinctPath(candidates, worldFolder.resolve("players").resolve("stats"));
}

private static void addDistinctPath(List<Path> candidates, Path candidate) {
if (!candidates.contains(candidate)) {
candidates.add(candidate);
}
return vanillaDefault;
}

private Path resolveUsercachePath() {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <world>/players/stats).
stats-dir: "auto"
# Pfad zu usercache.json (auto = aus Server-Verzeichnis).
usercache-path: "auto"
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/paper-plugin.yml
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Path> 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<Path> candidates = ImportCoordinator.statsDirCandidates(worldContainer, List.of());

assertEquals(
List.of(
defaultWorld.resolve("players").resolve("stats")
),
candidates
);
}
}