diff --git a/docs/event-modi-plan.md b/docs/event-modi-plan.md new file mode 100644 index 00000000..f4ec7bb7 --- /dev/null +++ b/docs/event-modi-plan.md @@ -0,0 +1,714 @@ +# Event-Modi für Titan — Recherche und Plan + +**Stand:** 2026-08-21 +**Betrifft:** Titan (Lobby), perspektivisch alle OLF-Minestom-Projekte +**Baut auf:** `docs/olf-minestom-project-standard.md` (Extension-Modell, Abschnitt 5) + +--- + +## 0. Der Befund, der alles andere steuert + +Bevor es um Architektur geht: **Eine umdekorierte Lobby bewegt keine Spielerzahlen.** + +Das ist keine Meinung. Deep Rock Galactic betreibt beides — kosmetische +Saison-Events *und* inhaltliche Seasons — im selben Spiel, mit derselben +Spielerschaft. Die Zahlen (SteamCharts, von mir stichprobenartig direkt am +Primärwert geprüft): + +| Deep Rock Galactic | Veränderung ggü. Vormonat | +|---|---| +| Oktober 2022 (kosmetisches Event) | **−0,37 %** | +| Oktober 2023 (kosmetisches Event) | **−12,16 %** | +| Oktober 2024 (kosmetisches Event) | **−4,92 %** | +| November 2022 (Season 03, Inhalt) | **+154,37 %** | +| März 2023 (Season 04, Inhalt) | **+58,79 %** | +| Juni 2024 (Season 05, Inhalt) | **+141,50 %** | + +Dead by Daylight liefert dasselbe Bild als zweites, unabhängiges +Within-Game-Experiment: Halloween-Oktober über zehn Jahre im Median **+1 bis ++3 %**; der Juni mit Jahrestag *und* neuem Kapitel dagegen sechsmal in Folge nie +unter **+20 %**, teils +53 %. Destiny 2 ist in fünf von sechs Oktobern sogar +**negativ** — die eine Ausnahme war der Monat, in dem bezahlter Inhalt erschien. + +**Zwei Zusatzbefunde, die genauso wichtig sind:** + +- **Nachhaltigkeit.** Vom Zugewinn eines Spitzenmonats sind nach einem Monat + 40–55 % weg, nach drei Monaten rund 80 %. Bei kosmetischen Events bleibt am + Ende ≈ 0 %, teils unterschreitet es die Ausgangslage (Destiny 2 lag im Dezember + 2024 17 % *unter* dem Wert vor dem Oktober-Event). Bei Inhalt bleibt ein Boden + von 20–35 % über der alten Baseline — sechs Monate und länger. +- **Kalender-Saisonalität ist genauso groß wie ein Deko-Event.** Der September + ist in fünf voneinander unabhängigen Spielen verlässlich der Jahrestiefpunkt + (−15 bis −20 %), Dezember/Januar der Hochpunkt. Ein Dezember-Effekt muss diese + Grundlinie erst schlagen, bevor man ihn dem Event gutschreibt. + +### Was daraus folgt — und was nicht + +**Nicht:** „Lasst die Winter-Lobby sein." Deko ist Markenpflege, und die hat +ihren eigenen Wert: ein Server, der sichtbar gepflegt wird, wirkt lebendig. + +**Sondern:** Der Aufwand für die Deko muss **klein und wiederholbar** sein. Jede +Personenstunde, die in ein einmaliges Winter-Setup fließt, fehlt beim Inhalt — +und nur Inhalt hebt den Boden. Der ganze folgende Plan ist darauf ausgelegt, die +Deko-Seite auf Stunden statt Wochen zu drücken und die frei werdende Zeit in +Spielbares zu lenken. + +> **Belegqualität.** Die Steam-Zahlen sind gemessene Telemetrie und für +> Within-Game-Vergleiche sauber. Für Minecraft-Server existieren **keine** +> vergleichbaren öffentlichen Daten — Hypixel-Zahlen kursieren nur als +> Community-Behauptungen ohne Primärquelle, und `minecraft-stats.com` ist eine +> kaputte Vorlagenseite, die unausgefüllte Platzhalter ausliefert. Ebenso gibt es +> **keine** peer-reviewte Literatur, die den Effekt saisonaler Events auf +> Spielerbindung misst. Die Übertragung auf Titan ist begründet, aber sie ist +> eine Übertragung. + +--- + +## 1. Ist-Zustand: Titan hat bereits ein Event-System + +Es ist nur weder benannt noch abgesichert. + +| Baustein | Zustand | +|---|---| +| Welt-Auswahl | `System.getProperty("TITAN_LOBBY_MAP", "world")` in `MapPool` | +| `halloween` | bereits verdrahtet — als Default im `setup`-Modul | +| Feature-Flags | Togglz, `TitanFeatures` mit 5 Navigator-Konstanten | +| Flag-Reload | funktioniert: `FileBasedStateRepository(File)` delegiert mit 1000 ms | +| Resource Packs | ungenutzt; Aves liefert `ResourcePackHandler` + `ResourcePackCondition` | +| Items | `Items`, statisch und hart verdrahtet | +| Navigator-Layout | im Code (`NavigationHelper`) | +| `app.json` | flach, ohne Saison-Bezug | + +Der heutige Mechanismus ist also: **eine Welt pro Saison, Auswahl beim Start per +JVM-Argument.** Kein Zeitplan, kein automatischer Wechsel, kein Fallback. + +### Das ist kein Provisorium — es ist der Branchenstandard + +Wichtig für die Planung: Der naheliegende Wunsch „eine Welt mit saisonalen +Overlays statt mehrerer Welten" ist technisch **nicht umsetzbar**, und zwar aus +zwei unabhängigen Gründen: + +- In Minestom teilen sich `SharedInstance`s die Chunks ihres Parent-Containers. + Zwei SharedInstances können **nicht** unterschiedliche Blöcke zeigen. +- Resource-Pack-`overlays` werden ausschließlich über das *Pack-Format des + Clients* ausgewählt. Es gibt kein Feld, kein Paket und keine Option, mit der + ein Server ein Overlay aktiviert. + +Entsprechend arbeiten alle nachprüfbaren Netzwerke mit getrennten Welten. +CubeCraft baut pro Saison einen neuen Hub und recycelt ihn über Jahre; Hypixel +reskinnt **sieben** Lobbys pro Event. Titans Ansatz ist damit richtig — er braucht +nur einen Zeitplan, einen Fallback und einen Rollout-Weg. + +### Der Fehler, der beim ersten echten Event zuschlägt + +```java +private void peekMap() { + Check.argCondition(this.referenceList.isEmpty(), "The map list is empty"); + if (this.referenceList.size() == 1) { + this.selectedMap = this.referenceList.getFirst(); // Property ignoriert + return; + } + this.selectedMap = ...filter(...).findFirst().orElseThrow(); // ohne Meldung +} +``` + +Zwei Verhaltensweisen, die zusammen eine Falle bilden: + +1. **Bei genau einer Welt wird die Property stillschweigend ignoriert.** Lokal + fällt ein Tippfehler in `TITAN_LOBBY_MAP` deshalb nie auf. +2. **Bei mehreren Welten wirft `orElseThrow()` ohne Meldung.** Der Server startet + nicht, und im Log steht eine `NoSuchElementException` ohne den Namen der + gesuchten Welt. + +Genau das Szenario „Winter-Welt nicht mitdeployt" endet damit in einem +Startabbruch, dessen Ursache man erst suchen muss. **Das ist Aufgabe 1 des +Plans**, unabhängig von allem anderen. + +--- + +## 2. Leitentscheidung der Architektur + +**Saisonaler Content ist bei euch ein Deployment-Problem, kein Feature-Flag-Problem.** + +Feature-Flags existieren, weil Deployment teuer ist. Bei euch ist es das nicht: +CloudNet-Services sind kurzlebig und neustartbar, und das Zielbild aus dem +OLF-Standard lädt Fachlogik ohnehin als Extension-Jars. Damit fällt die +Hauptbegründung für Flags als Content-Träger weg. + +Ein Halloween-Event ist außerdem nicht *ein* Feature, sondern zwölf: Deko, +Mob-Skins, Sounds, Scoreboard-Titel, Navigator-Icons, Drops. Wer das flaggt, hat +nach drei Jahren keine 15, sondern 150 Konstanten in `TitanFeatures` — und +N boolesche Flags ergeben 2^N Konfigurationen, von denen nie jemand mehr als drei +testet. + +### Die Aufteilung + +| Werkzeug | Wofür | Nicht wofür | +|---|---|---| +| **Season-Pack** (Extension-Jar) | alles Saisonale: Code, Listener, Deko-Logik | Werte, die sich pro Saison nur ändern | +| **Season-Daten** (JSON im Pack) | Zeitfenster, Items, Texte, Positionen, Loot | Kontrollfluss | +| **Togglz** | **ein** Flag pro Saison: Zeitfenster-Gate + Notausschalter | ein Flag pro Sub-Feature | + +**Die Grenze zwischen Daten und Code:** Braucht es einen neuen *Wert* oder ein +neues *Verb*? „Der Kürbis ist orange" ist ein Wert → JSON. „Kürbisse explodieren, +wenn jemand drübergeht" ist ein Verb → Code. + +Sobald das JSON `if`, `when` oder `onTick` bekommt, habt ihr eine schlechte +Programmiersprache gebaut — ohne Typprüfung, ohne Debugger, ohne Stacktrace. Das +gilt ausdrücklich auch für Togglz' `ScriptEngineActivationStrategy`: die Bedingung +wird unprüfbar, *und* Nashorn ist seit JDK 15 nicht mehr im JDK. + +**Der Java-25-Ausweg — Daten, aber typisiert:** + +```java +public sealed interface SeasonEffect + permits ReplaceItem, PrefixTitle, PlaceDecoration, AmbientSound { } + +public record ReplaceItem(NavigatorSlot slot, Material material, String nameKey) + implements SeasonEffect { } +``` + +Jackson deserialisiert polymorph, ein `switch` über die versiegelte Hierarchie ist +erschöpfend. Ein neuer Effekt-Typ, den niemand behandelt, bricht den **Compile**, +nicht die Produktion. + +### Zeitfenster: was Togglz nicht kann + +`ReleaseDateActivationStrategy` hat exakt zwei Parameter, `PARAM_DATE` und +`PARAM_TIME` — **kein Enddatum, keine Zeitzone** (im Jar per `javap` geprüft). +Für „16.10. bis 08.11." ist sie unbrauchbar. + +Der Ersatz ist klein: `ActivationStrategy` hat vier Methoden. Eine eigene +`SeasonWindowActivationStrategy` mit `from`, `to`, `zone`, registriert über +`META-INF/services`, ersetzt jedes handgeschriebene `if (LocalDate.now()...)` im +Projekt. Rund 60 Zeilen. + +**Zeitzone einmal festlegen:** `Europe/Berlin` ist die Redaktionszeitzone. +Fenster als `2026-10-16T00:00` + `zone`, beim Laden **einmal** zu `Instant` +auflösen, danach nur noch `Instant` vergleichen. Nie `LocalDateTime.now()`, nie +`ZoneOffset` speichern (`+02:00` ist im November falsch). + +**`Clock` injizieren** statt `Instant.now()` in der Fläche. Das erledigt drei +Dinge auf einmal: deterministische Tests ohne DST-Zufall, lokale Vorschau per +`-Dtitan.clock.offset=P45D`, und keine Tests, die im März grün und im November rot +sind. + +### Vorschau ohne Flag-Umlegen + +Das naheliegende „Flag kurz anschalten, gucken, ausschalten" ist für alle Spieler +sichtbar und hinterlässt Drift zwischen den Nodes. Stattdessen eine Preview-Ebene +im Gate, in dieser Reihenfolge: + +```java +boolean active(SeasonId id, Player p) { + if (killSwitch.isActive(id)) return false; // Ops, höchste Priorität + if (p.hasPermission("titan.season.preview")) return true; // Team sieht es vorab + return window(id).contains(clock.instant()); // Zeitfenster +} +``` + +Der Preview-Zweig läuft über LuckPerms, das ihr schon habt — kein zweites +Berechtigungssystem. + +--- + +## 3. Termine 2026 + +Aus ~25 Datenpunkten der Event-Historie von Dead by Daylight, Team Fortress 2, +Destiny 2 und Overwatch: + +- **Modalwert der Laufzeit: exakt 21 Tage.** Spanne 11–28, praktisch nichts über + vier Wochen. Der Trend geht zu kürzer. +- **Halloween beginnt 2–3 Wochen vor dem 31.10. und endet zwischen dem 1. und + 7. November. Kein einziges Event endet am 31.10.** Der Zahltag liegt mitten + drin, nie am Rand. +- **Winter beginnt 1–2,5 Wochen vor dem 25.12. und endet in der ersten + Januarwoche.** Alle überspannen Weihnachten *und* Neujahr. + +Dazu die deutschen Schulferien 2026 (KMK/schulferien.org) — für ein +deutschsprachiges Netzwerk der wichtigere Faktor: + +- **Herbstferien streuen über einen ganzen Monat: 05.10.–06.11.** Die maximale + Überlappung liegt bei **19.–24.10. mit 11 von 16 Ländern**. An Halloween selbst + sind nur noch etwa drei Länder frei, und **Bayern hat erst 02.–06.11. Ferien**. + Es gibt **keinen einzigen bundesweit gemeinsamen Herbstferientag.** +- **Weihnachten dagegen: 23.12.–01.01. sind alle 16 Länder gleichzeitig frei.** + +| | Halloween 2026 | Winter 2026/27 | +|---|---|---| +| **Live ab** | Freitag, **16.10.** | Dienstag, **01.12.** | +| **Ende** | Sonntag, **08.11.** | Mittwoch, **06.01.** | +| Laufzeit | ~3,5 Wochen | Deko 5 Wochen, Kernprogramm ~3 | +| Warum | trifft das 11-Länder-Fenster 19.–24.10., Ende nimmt Bayern mit | einziges bundesweites Fenster | + +**Wenn die Zeit nur für ein gutes Event reicht: Winter.** Es ist das einzige mit +einem bundesweiten Ferienfenster. + +--- + +## 3a. Die Terminfalle, die zwei Netzwerke zweimal getroffen hat + +**Plattform-Migrationen fressen Saison-Events.** CubeCraft hat das zweimal in +Folge öffentlich dokumentiert. Der Project Lead zu Halloween 2022: + +> „Because we're updating our networks to 1.19, all of our resources are focused +> on getting that out soon, so this Halloween event is a little simplified." + +Und im Frühjahr 2023 dasselbe noch einmal — das Event kam im Mai statt im März, +weil das Entwicklerteam vollständig in einer Bedrock-Migration steckte. Halloween +war dort die einzige Ausnahme von einem viermonatigen Netzwerk-Freeze. + +**Für Titan ist das unmittelbar relevant:** Der OLF-Standard sieht in Phase 4 und 5 +eine Bootstrap-Extraktion und die Umstellung auf Extension-Jars vor — inklusive +Wechsel des Extension-Frameworks. Fällt diese Umstellung in den Oktober oder +November, ist das Event entweder abgespeckt oder verschoben. + +**Konsequenz für die Reihenfolge:** Entweder die Extension-Migration ist **vor +Anfang Oktober** abgeschlossen, oder Schritt 3 dieses Plans (Season-Pack als +Extension) wird für 2026 **übersprungen** — dann gibt es die Winter-Lobby als +reinen Weltentausch über `TITAN_LOBBY_MAP`, so wie heute, und das Pack-Modell +kommt 2027. Beides ist vertretbar. Beides gleichzeitig im Oktober ist es nicht. + +--- + +## 4. Umsetzung in vier Schritten + +Jeder Schritt ist für sich nutzbar und einzeln mergebar. + +### Schritt 1 — Die Falle entschärfen (halber Tag) + +Vor allem anderen, weil es das Deployment jedes künftigen Events betrifft. + +- `MapPool.peekMap()`: bei fehlender Zielwelt eine Meldung werfen, die den + gesuchten Namen und die gefundenen Welten nennt. +- Den Sonderfall „genau eine Welt" abschaffen — die Property gilt immer, oder sie + gilt nie. Ein stiller Sonderfall, der lokal anders wirkt als in Produktion, ist + schlimmer als ein harter Fehler. +- **Fallback statt Absturz:** Ist die Event-Welt nicht da, mit `world` starten und + eine Warnung loggen. Ein Server, der ohne Deko läuft, ist besser als einer, der + nicht startet. + +**Fertig, wenn:** ein falsch gesetztes `TITAN_LOBBY_MAP` den Server startet, eine +verständliche Warnung schreibt und die Standardwelt lädt. + +### Schritt 2 — Zeitfenster und Gate (1–2 Tage) + +- `SeasonWindowActivationStrategy` (`from`, `to`, `zone`) schreiben und über + `META-INF/services` registrieren. +- `SeasonGate` als einzige Fassade vor Togglz, in der Reihenfolge + Kill-Switch → Preview-Permission → Zeitfenster. **Kein anderer Code ruft + `FeatureContext` direkt.** +- `Clock` in den Composition Root. +- Ein Command `/season` für Status und Notausschalter — die Togglz-Admin-Konsole + ist Servlet-basiert und im Minestom-Prozess nicht nutzbar. + +**Fertig, wenn:** ein Zeitfenster in der Konfiguration steht, das Team es per +Permission vorab sieht, und der Wechsel ohne Neustart passiert. + +### Schritt 3 — Erstes Season-Pack (Winter zuerst) + +Als eigenes Extension-Modul nach OLF-L5-01/L5-03: `extension.json` generiert, +alles vom Host Gelieferte `compileOnly`. + +- Contract `SeasonalContent` ins `:api`-Modul: `id()`, `window()`, `activate()`, + **`deactivate()`**. +- Registrierung über die `ServiceRegistry` des Extension-Frameworks, **nicht** über + `ServiceLoader` — der ist über Classloader-Grenzen fragil, genau das Problem, um + das die `ThreadHelper`-Krücke heute herumarbeitet. +- **`deactivate()` ist Pflicht, nicht optional.** Ohne symmetrisches Abräumen + bleiben Deko-Blöcke, Boss-Bars und Scheduler-Tasks nach Saisonende stehen, und + niemand findet die Ursache — das Pack ist ja „aus". +- Saisonale Optik als **Decorator** um bestehende Bausteine (`NavigationHelper` + bekommt andere Items und Namen), nicht als zweite Implementierung. Bei zwei + gleichzeitigen Packs entscheidet ein **Prioritätsfeld im Manifest**, nicht die + Ladereihenfolge. + +**Fertig, wenn:** der Host ohne Season-Pack sauber startet und das Pack sich durch +Löschen einer Zeile in `settings.gradle.kts` restlos entfernen lässt. Geht das +nicht, ist ein `if` in den Kern gewandert. + +### Schritt 3b — Rollout: CloudNet kann das schon + +Für den Saisonwechsel ist **kein eigener Code nötig**. CloudNet bringt alles mit: + +| Mechanismus | Wofür | +|---|---| +| Templates werden beim Service-Start kopiert | neues Template auf den Lobby-Task → jeder neu startende Service ist winterlich | +| Smart-Modul (`autoStopTimeByUnusedServiceInSeconds`, `minNonFullServices`) | alte Lobbys laufen leer und beenden sich → **Drain-Rollout statt Big Bang** | +| `templateInstaller: INSTALL_RANDOM_ONCE` | eingebaute Map-Rotation; funktioniert genauso für „30 % Wintervariante" | +| `includes` (Download beim Prepare) | Saisonwelt aus Object Storage ziehen statt ins Template committen | +| `maintenance: true` auf einem Canary-Task | Staged Rollout: erst Staff, dann alle | + +**Eine harte Grenze:** Das Deployment saisonaler Inhalte darf **niemals** einen +Neustart der Komponente erfordern, die Joins, Queueing oder Lobby-Transfers +abwickelt. Hypixels Dev-Blog beschreibt genau diesen Fehler: Ihr Monolith brauchte +60–80 Sekunden Boot, in denen niemand joinen, die Lobby wechseln oder Befehle +nutzen konnte — laut eigener Aussage *„the cause of nearly every network drop over +the past year"*. Saisonales gehört in einen Leaf-Service, nie in Node oder Bridge. + +**Und: keine Lockstep-Migration erzwingen.** Alte und neue Lobby-Version müssen +während des Rollouts koexistieren können. Hypixel musste 2017 sichtbar unfertige +Lobbys ausrollen, weil ihr Backend-Wechsel alle Lobbys gleichzeitig verlangte — +mit dem eingestandenen Ergebnis „some lesser quality lobbies". + +### Schritt 3c — Das Event-Ende ist der teuerste Moment + +Das am häufigsten unterschätzte Detail. Hypixels Events-Team über den +Spooky-Festival-Kollaps, öffentlich: + +> „It only ever breaks for a few minutes? I think that's to be expected for 10,000 +> players all warping to the hub." … „I don't know if there's any cheap solution +> to thousands of players all connecting to different servers." + +Ein Event, das für alle gleichzeitig endet, erzeugt einen synchronen +Massen-Transfer. **Sie haben das nie gelöst.** Bei eurer Größenordnung ist das +beherrschbar, aber nur wenn man es einplant: Ende staffeln, oder die Event-Lobby +nach Eventende weiterlaufen lassen und nur den Zustrom umlenken. + +**Ergänzend, das Belohnungs-Ende:** Nicht ausgegebene Event-Währung nach +Fristende **automatisch** in reguläre Währung umwandeln (Overwatch-2-Muster, +Abschnitt 6) — keine Abholfrist, keine Verfallsregel. MultiVersus gab nach +Eventende nur zwei Tage zum Einlösen bereits verdienter Belohnungen; das ist die +Sorte Detail, die Support-Tickets erzeugt. + +### Schritt 3d — Wenn ein Event wiederkommt: Smoketest + +Hypixel, Holidays 2025, aus dem Forum: + +> „It looks like a bug from last year is back, where Santa Says wins don't count +> towards your /myposition or the wins leaderboard." + +Kein Staff-Reply. **Code, der elf Monate dunkel liegt, wird von nichts getestet** — +er kompiliert, aber niemand führt ihn aus. Zum Reaktivieren einer Saison gehört +deshalb ein Durchlauf der Kernpfade, bevor das Event live geht. Das ist genau der +Grund für die `deactivate()`-Pflicht und den Time-Bomb-Test aus Abschnitt 5. + +### Schritt 4 — Messung (ein Nachmittag) + +Ohne das ist nach dem Event nichts aussagbar. + +**Wichtig vorweg:** *Plan (Player Analytics)* listet Minestom nicht und ist +unbrauchbar; *bStats* misst Plugin-Verbreitung für Autoren, nicht Serverbetrieb. +Beides scheidet aus. Ihr braucht ein eigenes Join-Log — und habt die +Einstiegspunkte mit `PlayerSpawnListener` und `PlayerConfigurationListener` +bereits. + +Eine Zeile `timestamp;uuid` pro Join. Daraus fallen mit `sort`/`uniq` heraus: + +| Kennzahl | Aussage | +|---|---| +| **Reaktivierte Spieler** — UUIDs im Event, die 30 Tage davor nicht da waren | der eigentliche Zweck eines Events | +| **Post-Event-Retention** — von den Neuzugängen nach 14 und 28 Tagen noch da | ob etwas hängen bleibt | +| Unique Spieler pro Tag | Grundlinie | +| Neue Spieler (erstmals gesehen) | Wachstum | + +Zwei Disziplinen, ohne die die Zahlen wertlos sind: + +- **Baseline 14 Tage vor dem Event erheben**, auf Wochentage ausgerichtet. +- **Personenstunden mitzählen.** „Hat sich gelohnt" ist ein Bruch; ohne Nenner + habt ihr nur den Zähler. + +**Falls Prometheus:** keine Pro-Spieler-Metriken — das erzeugt eine Zeitreihe je +gesehenem Spieler und ist eine Kardinalitätsbombe. Uniques gehören in eine +Tabelle. *(Der Grafana-MCP-Zugang dieser Session war defekt — `mcp-grafana` nicht +im PATH. Falls die Auswertung dort landen soll, muss das repariert werden.)* + +**Confounder, der ins Auswertungsdokument gehört:** Weil die Herbstferien 2026 +über den 05.10.–06.11. streuen und das Event mitten drin liegt, lässt sich der +Event-Effekt im ersten Jahr **nicht** vom Ferieneffekt trennen. Erst der +Jahresvergleich ab 2027 hilft. + +--- + +## 4a. Der eigentliche Engpass: der Weg vom Build in den Server + +Der Engpass ist erfahrungsgemäß **nicht das Bauen, sondern die Integration.** Der +einzige direkte Beleg dazu stammt von einem Hypixel-Builder: seine abgenommene Map +*„wasn't added to the game until I fully joined the build team months later"* — +Build fertig, Auslieferung wartete auf Entwicklerkapazität. Wynncraft nennt +Bewerbern drei Monate Review-Latenz. + +**Handoff-Format: Anvil-Weltordner, nicht Schematic.** Schematics verlieren zu +viel: `.schem` speichert kein Licht und keine Heightmaps, und **Entities sind auf +beiden Seiten opt-in** (`//copy -e` *und* `//paste -e`). Vergisst ein Builder das, +fehlen Item Frames, Paintings, Armor Stands und Display Entities vollständig. +`.litematic` ist client-only und wird serverseitig von nichts gelesen. + +**Metadaten neben die Welt.** Das einzige gut dokumentierte Schema kommt von +BlueDragonMC, einem quelloffenen Minestom-Netzwerk: eine `config.yml` **im +Weltordner neben `region/`**, alles unter `world:` genamespaced, mit `name`, +`description`, `author`, `spawnpoints`, `additionalLocations`. Direkt übernehmbar. +Für alles, was in situ gesetzt werden muss, ist euer Guira-Setup-Flow der richtige +Weg — er schreibt das JSON, statt dass jemand Koordinaten abtippt. + +**Entschieden: Falco statt Polar.** Eine frühere Fassung dieses Dokuments empfahl +einen Wechsel auf das Polar-Format. Diese Empfehlung ist **hinfällig** — Titan +setzt auf die eigene Engine **Falco** (`falco-anvil`, `falco-light`), die das +Anvil-Format behält. + +Das ist die bessere Entscheidung, und zwar aus einem Grund, der im +Polar-Vergleich untergegangen wäre: Polar speichert **keine Entities**, was NPCs, +Item Frames und Display Entities aus der Welt in Konfiguration verschoben hätte. +Falco liest weiter Anvil und braucht diesen Umbau nicht. Dazu kommt, dass ein +Lesefehler bei Falco eine Ausnahme wirft, statt den Chunk als „nicht vorhanden" +zu melden — bei einer gebauten Lobby ist genau das der Unterschied zwischen einem +Fehler und einem stillschweigend neu generierten, leeren Stück Welt. + +Anforderungen und Abnahmekriterien dazu stehen in +[`spec-lobby-saison-events.md`](spec-lobby-saison-events.md), Stufe 1. + +**Zu klären bleibt die Lizenzfrage:** Falco steht unter AGPL-3.0 ohne +Linking-Ausnahme, Titan veröffentlicht unter Apache-2.0. Siehe Spec, Abschnitt 7. + +## 4b. Resource Packs, falls saisonal gewünscht + +Aves liefert `ResourcePackHandler` und `ResourcePackCondition`, Titan nutzt beides +nicht. Falls saisonale Optik über Packs laufen soll, ist das Modell seit 1.20.3: +**großes Base-Pack mit stabiler UUID + kleines Season-Delta mit eigener UUID.** +`resource_pack_pop` entfernt gezielt eine UUID, ohne die anderen anzufassen — der +große Base-Pack bleibt im Client-Cache. + +Vier Fallen, alle im Minestom-Quelltext bzw. Protokoll belegt: + +1. **Nie `replace(true)`** für den Saisonwechsel. Minestom implementiert das als + Pop-all-plus-Push — nicht atomar, der Client lädt zweimal. +2. **`ConnectionManager.doConfiguration()` macht `packFuture.join()` ohne + Timeout.** Ein Client, der nie antwortet, parkt seinen Config-Thread unbegrenzt. + Eigenen Timeout einbauen. +3. **CustomModelData merged nicht.** Ein Season-Pack, das ein Item anfasst, das + das Base-Pack schon customized, ersetzt dessen Datei ganz — alle Basis-Varianten + verschwinden still. Item-IDs zwischen Base und Season **disjunkt** halten. + (Atlanten und Sprachdateien mergen dagegen problemlos.) +4. **Geyser lügt.** Bedrock-Spieler melden über Geyser `SUCCESSFULLY_LOADED`, ohne + je etwas erhalten zu haben. Am Pack-Status ist das nicht erkennbar — explizit + auf Bedrock prüfen. + +Der Client cached außerdem **nach URL, nicht nach Hash** — also +inhaltsadressierte Dateinamen (`pack-.zip`) verwenden und SHA-1 immer +mitsenden. + +## 5. Damit es im dritten Jahr nicht kippt + +Der eigentliche Vorteil von Packs gegenüber Flags: Aufräumen ist ein +`settings.gradle.kts`-Diff statt eines Refactorings. + +1. **Jahreszahl im Namen ist Pflicht** — `season-halloween-2026`, nie `halloween`. + Erzwingt die bewusste Entscheidung „wiederverwenden oder neu bauen?" statt + stillem Verhaltensdrift. +2. **`until` im Manifest ist Pflichtfeld.** Kein Pack ohne Enddatum. +3. **Time-Bomb-Test:** ein JUnit-Test, der fehlschlägt, wenn ein registriertes Pack + sein `until` + 12 Monate überschritten hat. Passt in dieselbe ArchUnit-Schicht, + die der OLF-Standard ohnehin vorsieht. +4. **Obergrenze auf `TitanFeatures`** — heute 5 Konstanten, Limit bei 12, CI bricht + darüber. Zwingt zu „welches alte Flag räume ich weg, damit ich das neue anlegen + darf". +5. **Archivieren statt löschen:** Pack nach `seasons/archive/` und aus + `settings.gradle.kts`. Nächstes Jahr wieder aufnehmbar, aber solange nicht + kompiliert, nicht getestet, nicht mitmigriert. Tragekosten null. + +--- + +## 6. Kommunikation + +**Discord ist Hub, nicht Reichweite.** Die JIM-Studie 2025 (n = 1.200, +repräsentativ, 12–19 Jahre) misst Discord bei **20 % regelmäßiger Nutzung** — +32 % der Jungen, aber nur **7 % der Mädchen**. Zum Vergleich: Snapchat 56 %, +TikTok 53 %, Twitch 11 %. + +Der ermutigende Teil derselben Studie: **Minecraft ist Lieblingsspiel Nr. 1**, mit +rund 25 % bei *offener* Frage ohne Antwortvorgaben — „in allen Befragungsgruppen +an erster Stelle", unabhängig von Alter, Geschlecht und Schulform. Die Zielgruppe +ist da; sie ist nur nicht auf Discord. + +Reichweite kommt aus **Serverlisten** (serverliste.net führt mit Abstand das +höchste Volumen) und aus kurzen Clips. **X/Twitter lohnt nicht** — 0,12 % +Engagement gegen 3,73 % bei TikTok. + +**Beteiligung realistisch ansetzen** (Nielsens 90-9-1): Ein Build-Contest liegt im +1-%-Tier — bei 200 Aktiven sind das 2–10 Einreichungen. Screenshot-Aktionen und +Abstimmungen liegen im 9-%-Tier, also 20–60. Wer mit 50 Build-Einreichungen +plant, plant falsch. + +### Zwei Fehlerbilder, die ihr umgehen könnt + +**Halo Infinite, „Winter Contingency" 2021:** zehn Belohnungsstufen, **eine pro +Tag**, über 14 Tage zwischen Weihnachten und Neujahr. Wer alles wollte, musste an +10 von 14 Feiertagen spielen. Schlimmer: Teilnahme brachte keine kleineren +Belohnungen, sondern **kaputte** — ein Schulterpolster ohne Gegenstück. + +Ein klassischer 24-Türchen-Adventskalender ist strukturell derselbe Mechanismus. +Drei Änderungen machen ihn sicher: + +1. **Kumulativ statt tagesgebunden** — 15 von 24 Tagen da gewesen = 15 Türchen, + nachholbar. +2. **Teilbelohnungen müssen für sich funktionieren.** +3. **Beim Start sagen, ob es das nächstes Jahr wieder gibt.** + +**Overwatch 2, Winter Wonderland 2023** — das Muster fürs Eventende, direkt +kopierbar: nicht ausgegebene Event-Währung wurde nach Fristende **automatisch in +reguläre Währung umgewandelt**, vorab im Ankündigungsposten angekündigt. Kein +Löschen, keine Verfallsfrist, keine Abholpflicht. + +--- + +## 7. Was ausdrücklich nicht getan werden sollte + +1. **Ein Flag pro saisonalem Sub-Feature.** Ein Flag pro *Saison*. Die Sub-Features + leben und sterben gemeinsam. +2. **`if (HALLOWEEN.isActive())` verstreut im Kerncode.** Die Entscheidung fällt + einmal im Composition Root; danach wird ein Objekt injiziert, kein Boolean + geprüft. +3. **Vererbung für Saison-Varianten** (`HalloweenLobby extends Lobby`). + Funktioniert für die erste Saison, kollabiert bei zwei gleichzeitigen. +4. **Ein fettes `Theme`-Interface** mit Farben, Items, Sounds, Mobs und Nachrichten. + Jedes neue Event zwingt alle alten Themes zu einer neuen Methode. Stattdessen + kleine, unabhängig registrierbare Beiträge. +5. **Content-Pack ohne `deactivate()`.** +6. **Pack, das ein anderes Pack direkt referenziert.** Verstößt gegen OLF-L2-01, + funktioniert im Test, bricht im Deployment. +7. **Pack bündelt, was der Host liefert** (Minestom, `:common`, Togglz-Core). + Zwei Kopien in zwei Classloadern → `NoSuchMethodError`, teuer zu finden. +8. **Wochenlanges Deko-Bauen.** Siehe Abschnitt 0 — der Hebel liegt beim Inhalt. +9. **Ein Einmal-Spektakel als erstes Event.** Höchste Kosten, kürzeste Wirkung — + die Kategorie, an der Epic das Format eingestellt hat (7b). +10. **Mit hoher Frequenz starten.** Sie lässt sich nicht mehr senken (7b). +11. **Weiter als drei Monate durchplanen.** Überlebt den Kontakt mit der Realität + nicht (7b). +12. **Am Vortag fertig werden.** Liveops gehört Wochen vorher fertig und durch + einen Test — Winter heißt Mitte November, nicht 30.11. + +--- + +## 7a. Ein Saison-Primitiv statt jährlich neuer Mechaniken + +**Das ist die Antwort auf „wie schiebe ich am schnellsten weitere Funktionen +nach".** Und sie ist ungewöhnlich gut belegt. + +Die schärfste Formulierung stammt von InnoGames über ihr Saison-Event „Forge of +Vulcan" in der sechsten Iteration: + +> „The team doesn't rebuild it every quarter… **The event currency and 21-day +> runtime have been constant throughout — the stability and habit players need, +> leaving everything else open to change.**" + +Und im selben Bericht: Vergleicht man die erste Fassung mit der heutigen, findet +man *„very few similarities between them"*. + +**Daraus die Architekturregel: Währung, Laufzeit und Kalenderslot sind das +Produkt. Alles andere ist die Saison.** Genau diese drei Dinge gehören in +stabilen Code; alles darüber gehört in die austauschbaren Saison-Daten. + +Drei unabhängige Bestätigungen: + +- **Fortnite Winterfest** ist bis in die Werbetexte hinein derselbe Event: die + Lodge als fester Ort, 14 Geschenke pro Tag unverändert von 2019 bis 2023, der + Kamin-Loop identisch — und 2019 und 2021 wörtlich dieselbe Werbezeile. Neue + Inhalte kommen per Unvaulting alter Items, nicht durch Neubau. +- **Genshin Impact** hat Wiederverwendung sogar kategorisiert: eine Wiki-Kategorie + „Recurring Events" mit rund 65 Einträgen. Ley Line Overflow lief **21×**, + Marvelous Merchandise 9×, Windtrace 5× — letzteres 2025 als „Snowtrace" + umgeskinnt. Das Template bleibt, das Balancing wird versioniert. +- **Belka Games** (Clockmaker, neun Jahre alt), unverblümt: *„Don't be shy about + simply reusing successful old events. In just 2–3 months, your player base will + refresh significantly."* + +**Die Grenze** — Javier Barnes (ehemals Socialpoint/Tilting Point): Templates sind +der Schlüssel zu planbarem Aufwand, aber *„a single kind shouldn't run too often +with the same exact experience and rewards."* Gleiche Mechanik ja, gleiche +Belohnungen nein. + +CubeCraft macht in der Minecraft-Welt exakt dasselbe: seit Jahren dieselbe +Hub-Suche, nur umgethemt — 20 Kürbisse, 30 Trick-or-Treats, 20 Blumen, +Strandbälle, 20 Schneemann-Teile. Eine Mechanik, fünf Saisons. + +## 7b. Wie viele Events pro Jahr — und die Falle dahinter + +**Empfehlung: zwei große plus zwei kleine im ersten Jahr.** Das ist jetzt belegt +statt geschätzt: + +- **InnoGames** hat die Kehrtwende dokumentiert: *„I used to firmly believe that + you needed an event every day, but… synergy and rhythm are more important than + sheer coverage."* Heutige Zielgröße für große Beats: **drei bis vier pro Jahr**, + je 4–8 Wochen — mit einem Vollzeitteam. +- **Fortnite** fährt seit neun Jahren exakt **zwei** unstrittige Jahresanker: + Fortnitemares und Winterfest, lückenlos 2017–2025. +- **Genshin** hält seit 2020 exakt 42 Tage pro Version mit einem Flagship-Event + pro Version. + +Die einzige harte Zahl zu Teamgröße kommt von der GDC 2017: Space Ape fuhr +*wöchentliche* Events mit einem Vierer-Team — allerdings mit fertiger +Live-Ops-Tooling-Infrastruktur in einem 100-Personen-Studio. **Vier Leute schaffen +wöchentliche Events erst, nachdem das Tooling steht.** Genau deshalb steht in +diesem Plan das Tooling (Schritte 1–3) vor der Frequenz. + +### Drei Fallstricke, die teuer sind + +**Kadenz ist ein Einwegversprechen.** Epic stellte Fortnite-Updates von +wöchentlich auf zweiwöchentlich um, um Crunch zu lindern — *„But the situation did +not improve."* Aus zwölf Interviews bei Polygon: *„Crunch never ends in a live +service game like that."* Frequenz lässt sich leicht erhöhen und praktisch nicht +mehr senken, ohne dass es als Rückschritt gelesen wird. **Fangt niedrig an.** + +**Das Einmal-Spektakel tötet das Format.** Donald Mustard zur Einstellung der +Fortnite-Live-Events, gefragt nach dem Grund: *„It was resources."* Und: *„the +event team was starting to be needed to work on other stuff."* Der Kontrast ist +messbar — Galactus war **zehn Minuten Inhalt** mit rund sechs Monaten Vorlauf, +gegen 2–4 Wochen Laufzeit bei jedem wiederkehrenden Feiertagsevent. Für ein Team +eurer Größe ist das Einmal-Spektakel die teuerste denkbare Kategorie. + +**Drei Monate Planungshorizont, nicht mehr.** Grant Shonkwiler, ehemals +Fortnite-Producer bei Epic: *„a feature level schedule for the next 3 months. Why +3 months? Because I have found anything more than that is a waste of time, it +won't survive contact with players OR devs."* Ergänzend Barnes: *„Liveops should +be finished weeks before they go live, and should go through QA. The cost of being +late on a liveops or having bugs is huge."* + +Auf eure Termine übersetzt: Winter geht am 01.12. live, ist also **Mitte November +fertig** — nicht am 30.11. + +## 8. Offene Punkte + +1. **Lizenzfrage Falco ↔ Titan.** Falco ist AGPL-3.0 ohne Linking-Ausnahme, + Titan veröffentlicht unter Apache-2.0. Ein Minecraft-Server ist ein + Netzwerkdienst — genau der AGPL-§13-Fall. Zu entscheiden, **bevor** der + Falco-Umstieg beginnt. Siehe Spec, Abschnitt 7. +2. **Saisonale Resource Packs — ja oder nein?** Aves bringt die Bausteine mit. + Die Entscheidung betrifft auch den Umgang mit Spielern ohne Pack und mit + Bedrock-Spielern (siehe 4b) und gehört vor Schritt 3 getroffen. +3. **Wird `titan-api` publiziert?** Der `SeasonalContent`-Contract gehört ins + `:api`-Modul. Das trägt derzeit kein `maven-publish` (siehe + `olf-minestom-project-standard.md`, offener Punkt 3). +4. **Wann läuft die Extension-Migration?** Siehe Abschnitt 3a — die Antwort + entscheidet, ob Schritt 3 dieses Plans für 2026 stattfindet oder auf 2027 + verschoben wird. + +### Wo die Recherche nichts hergab + +Ehrlichkeitshalber, damit niemand diese Fragen für beantwortet hält: Kein Netzwerk +hat je CDN-Wahl, Packgrößen, Downloadzeiten oder Spieler-Abbruchquoten +veröffentlicht. Zu Mineplex-Postmortems und zur Technik von Hive und GommeHD gibt +es nichts Verifizierbares — die entsprechenden Seiten blockieren automatisierte +Abrufe. Und es existiert **kein** öffentliches Datenbankschema eines großen +Netzwerks; die Cosmetics-Empfehlungen stammen aus quelloffenen Plugins und einem +Minestom-Backend, nicht von einem Netzwerk eurer Zielgröße. + +--- + +## Quellen + +**Gemessene Telemetrie** (Monatswerte durchschnittlicher gleichzeitiger Spieler): +[Deep Rock Galactic](https://steamcharts.com/app/548430) · [Dead by Daylight](https://steamcharts.com/app/381210) · [Destiny 2](https://steamcharts.com/app/1085660) · [Team Fortress 2](https://steamcharts.com/app/440) · [Warframe](https://steamcharts.com/app/230410) · [Sea of Thieves](https://steamcharts.com/app/1172620) · [Rust](https://steamcharts.com/app/252490) + +**Zielgruppe:** [JIM-Studie 2025, mpfs](https://mpfs.de/studie/jim-studie-2025/) ([PDF](https://mpfs.de/app/uploads/2025/11/JIM_2025_PDF_barrierearm.pdf)) + +**Ferien:** [schulferien.org 2026/27](https://www.schulferien.org/deutschland/ferien/2026-2027/) · [KMK](https://www.kmk.org/service/ferien.html) + +**Fehlerbilder:** [Kotaku zu Halo Winter Contingency](https://kotaku.com/halo-infinite-s-winter-event-unfortunately-demands-a-bi-1848254025) · [Blizzard zu Winter Wonderland 2023](https://overwatch.blizzard.com/en-us/news/24033785/a-flurry-of-fun-returns-to-overwatch-2-winter-wonderland-begins-december-19/) + +**Beteiligung:** [Nielsen, 90-9-1](https://www.nngroup.com/articles/participation-inequality/) + +**Architektur:** [Martin Fowler, Feature Toggles](https://martinfowler.com/articles/feature-toggles.html) · [Togglz Activation Strategies](https://www.togglz.org/documentation/activation-strategies) · [Togglz State Repositories](https://www.togglz.org/documentation/repositories) + +**Verknappung (Kaufabsicht, nicht Bindung):** [Scarcity tactics in marketing, J. Retailing 2022](https://doi.org/10.1016/j.jretai.2022.06.003) · [Meta-analysis on product scarcity, Psych. & Marketing 2023](https://doi.org/10.1002/mar.21816) — letztere findet, dass **rein zeitliche** Verknappung der *schwächste* Hebel ist; soziale Sichtbarkeit wirkt stärker. + +**Im Code verifiziert:** `MapPool.peekMap()`, `TitanFeatures`, `SingletonFeatureManagerProvider`, sowie `togglz-core-4.6.2.jar` per `javap` (`ReleaseDateActivationStrategy` ohne End-/Zeitzonenparameter; `FileBasedStateRepository(File)` delegiert mit 1000 ms — der Reload funktioniert bereits). diff --git a/docs/olf-minestom-project-standard.md b/docs/olf-minestom-project-standard.md new file mode 100644 index 00000000..fbc1ca20 --- /dev/null +++ b/docs/olf-minestom-project-standard.md @@ -0,0 +1,1018 @@ +# OLF Minestom Project Standard + +**Status:** Vorschlag zur Review +**Datum:** 2026-08-20 +**Geltungsbereich:** Alle OneLiteFeather-Minestom-Projekte (Server, Extensions, Libraries) +**Erster Anwender:** Titan (Migrationsplan in Anhang B) + +--- + +## 0. Warum es dieses Dokument gibt + +OneLiteFeather betreibt inzwischen vier vergleichbare Minestom-Server-Projekte — +ManisGame, Cygnus, Tamias und Titan — und eine Reihe von Libraries (Aves, Xerus, +Coris, Guira, pica, Cyano). Die Libraries folgen einer konsistenten, über Jahre +gewachsenen Handschrift. Die Server-Projekte tun das unterschiedlich stark. + +Der Unterschied ist messbar, nicht Geschmackssache: + +| Kennzahl (nur `src/main`) | Titan | ManisGame | Cygnus | Tamias | Aves | pica | Xerus | +|---|---:|---:|---:|---:|---:|---:|---:| +| Java-Dateien | 74 | 359 | 239 | 181 | 99 | 38 | 34 | +| davon mit `@author` | **2** | 152 | 121 | 89 | 78 | 18 | 33 | +| davon mit `@since` | **3** | 157 | 122 | 89 | 81 | 18 | 33 | +| `package-info.java` | **0** | 77 | 54 | 35 | 14 | 11 | 0 | +| `@ApiStatus`-Nutzung | **0** | 9 | 0 | 3 | 8 | 5 | 1 | +| `sealed`-Nutzung | 3 | 10 | 5 | 10 | 14 | 7 | 0 | +| öffentliche `*Impl` im API-Modul | 0 | 0 | — | — | — | — | — | +| Convention-Plugin (`buildSrc`) | **nein** | ja (3) | ja (1) | ja (1) | — | — | — | + +Titan ist damit nicht "schlechter geschrieben", sondern **ohne durchgesetzte +Konvention gewachsen**. Es ist das einzige der vier Server-Projekte ohne +`buildSrc`. + +**Eine Warnung zu dieser Tabelle.** Zahlen zeigen, wo man hinschauen soll — sie +ersetzen das Hinschauen nicht. Eine frühere Fassung dieses Dokuments las Titans +vier `*Impl`-Dateien im `:api`-Modul als Verstoß gegen die API/Impl-Trennung. Ein +Blick in den Code widerlegte das: alle vier sind package-private und hinter +Factory-Methoden verborgen — es ist eines der saubersten APIs im Bestand (siehe +OLF-L2-02). Wer diesen Standard anwendet, prüft jeden Zählwert am Code, bevor er +daraus eine Aufgabe ableitet. + +Dieses Dokument leitet die Konvention aus dem ab, was in den Referenzprojekten +nachweislich funktioniert, macht sie maschinell prüfbar und beschreibt, wie Titan +als erstes Projekt darauf migriert. + +**Die Referenzprojekte und wofür sie stehen:** + +| Projekt | Referenz für | +|---|---| +| **ManisGame** | Modultopologie (`shared:*` + Server-Varianten), Convention-Plugin-Hierarchie, API-Modul-Disziplin | +| **Cygnus** | Bootstrap-Extraktion, echte Minestom-Extension-Jars, `package-info`-Disziplin | +| **Tamias** | Schlanke Drei-Modul-Topologie (`common`/`game`/`setup`), feine Paketierung | +| **Aves / Xerus / pica / Coris** | Klassen-Patterns: Interface + Factory, Builder, `sealed`, Javadoc-Konvention | + +### Die drei Leitentscheidungen + +1. **Deployment:** Ein schlanker Host-Prozess lädt alle Fachlogik als Extensions. + Butterfly und die Lobby-Logik werden Extension-Jars, kein Fat-Jar-Inhalt. +2. **Reichweite:** Org-weiter Standard. Titan ist der erste Anwender, nicht der + einzige Adressat. +3. **Durchsetzung:** Was prüfbar ist, wird geprüft. Regeln landen als + `buildSrc`-Convention (Checkstyle, ArchUnit, Jacoco), nicht als Wunsch im Wiki. + +### Regel-IDs + +Jede Regel hat eine ID der Form `OLF-L-`. ArchUnit-Testmethoden +und Review-Kommentare referenzieren diese ID, damit eine Beanstandung immer auf +eine nachlesbare Begründung zeigt statt auf persönlichen Geschmack. + +--- + +## 1. Ebene L1 — Projekt + +### OLF-L1-01: `buildSrc` ist Pflicht, Build-Logik wird nicht kopiert + +**Regel.** Jedes Multi-Modul-Projekt hat ein `buildSrc` mit einer +Convention-Plugin-**Hierarchie**: ein Basis-Plugin mit Toolchain, Compiler-Flags +und Test-Konfiguration, darauf aufbauend je ein Plugin pro Modulrolle. Toolchain, +Test-Konfiguration, Qualitäts-Gates und der Publishing-Block stehen dort +**einmal**. Jedes Modul wendet genau ein Rollen-Plugin an. + +**Begründung.** Titan hat heute keinen `buildSrc`. Der `publishing { ... }`-Block +mit POM-Metadaten, Lizenz, Developer und SCM ist in `app`, `setup` und `bridge` +**dreimal wortgleich** kopiert, die Repository-Definition mit +Credential-Handling ebenso. Die Java-Toolchain 25 ist **fünfmal** dupliziert, die +Test-Konfiguration `useJUnitPlatform()` + `-Dminestom.inside-test=true` **viermal**. +Jede Änderung an einer dieser Stellen ist heute eine Änderung an vier bis fünf +Dateien — mit der üblichen Folge, dass eine vergessen wird. Cygnus zeigt die +Gegenprobe: `cygnus.java-conventions.gradle.kts`, ein einziger Ort. + +**Ziel-Layout.** ManisGame lebt die Hierarchie bereits vor und ist die Vorlage: + +``` +buildSrc/src/main/kotlin/ + titan.java-conventions.gradle.kts Basis: Toolchain, Compiler, Test, Jacoco + titan.library-conventions.gradle.kts ← java-conventions + `java-library` + titan.application-conventions.gradle.kts ← java-conventions + `application` + shadow + titan.extension-conventions.gradle.kts ← java-conventions + Processor, compileOnly + titan.quality-conventions.gradle.kts Checkstyle, ArchUnit, Nullability-Gate + titan.publish-conventions.gradle.kts POM, Lizenz, Developer, SCM, Repository +``` + +**Referenz** (ManisGame — Vererbung in Reinform, das Rollen-Plugin enthält nur +noch den Unterschied): + +```kotlin +// manis.library-conventions.gradle.kts +plugins { + id("manis.java-conventions") + `java-library` +} + +// manis.application-conventions.gradle.kts +plugins { + id("manis.java-conventions") + application +} +``` + +**Referenz** (Cygnus, gekürzt — die Basis, auf der `titan.java-conventions` aufsetzt): + +```kotlin +plugins { + java + jacoco +} + +java { + toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } +} + +tasks.withType().configureEach { + options.release.set(25) + options.encoding = "UTF-8" + options.compilerArgs.addAll(listOf("-Xlint:deprecation", "-Xlint:unchecked")) +} + +tasks.withType().configureEach { + jvmArgs("-Dminestom.inside-test=true") + finalizedBy(tasks.matching { it.name == "jacocoTestReport" }) + useJUnitPlatform() + testLogging { events("passed", "skipped", "failed") } +} +``` + +**Durchsetzung.** Ein Modul-`build.gradle.kts`, das `java { toolchain { ... } }` +oder einen `publishing`-Block selbst deklariert, statt das Convention-Plugin +anzuwenden, wird im Review abgelehnt. Perspektivisch prüfbar über einen +`checkNoInlineToolchain`-Task, der die Buildfiles nach diesen Blöcken greppt. + +**Warnendes Beispiel.** Tamias hat ein `buildSrc` mit +`tamias.java-conventions.gradle.kts` — wendet es in `game/build.gradle.kts` aber +nicht an; dort stehen nur `alias(libs.plugins.shadow)` und `application`. Das +Convention-Plugin allein genügt also nicht: ohne Prüfung driften Module wieder +auseinander, und der `buildSrc` wird zur Dekoration. Genau deshalb steht in +diesem Standard bei jeder Regel eine Durchsetzungsangabe. + +--- + +### OLF-L1-02: Version-Catalog im `settings.gradle.kts`, Versionen über BOM + +**Regel.** Abhängigkeiten werden im Inline-Version-Catalog des +`settings.gradle.kts` deklariert (nicht in `libs.versions.toml` — das ist die +OLF-Abweichung vom Gradle-Default). Konkrete Versionen kommen wo immer möglich +aus einer BOM (`mycelium-bom` für Libraries, `aonyx-bom` für Minestom-Anwendungen, +`manis-bom` für Spiele) über `.withoutVersion()`. + +**Begründung.** `.withoutVersion()` bedeutet: die BOM entscheidet, ein +Renovate-Update an der BOM zieht das ganze Ökosystem nach. Eine hart notierte +Version im Catalog hebelt das aus und muss pro Projekt gepflegt werden. Titan hat +aktuell 12 harte Versionen im Catalog, von denen mindestens `guava` und +`kotlin-stdlib-jdk8` nur deshalb dort stehen, weil sie transitive Lücken stopfen. +Solche Einträge brauchen einen Kommentar, der die Lücke benennt — Titan macht das +bereits vorbildlich, das ist beizubehalten. + +**Regel im Detail.** Eine harte Version im Catalog ist zulässig, wenn: +1. die BOM sie nicht führt, **und** +2. ein Kommentar direkt darüber erklärt, warum sie nötig ist und wann sie + entfallen kann. + +--- + +### OLF-L1-03: Ein Modul, ein Zweck, ein Artefakt + +**Regel.** Jedes Gradle-Modul produziert genau ein Artefakt mit genau einer Rolle. +Die zulässigen Rollen sind: `bootstrap` (ausführbarer Host), `api` (Contracts), +`common` (geteilte Domäne), `extension` (Fachlogik als Extension-Jar). + +**Begründung.** Titans `:api`-Modul verletzt das doppelt: es enthält sowohl +Contracts (`Deliver`, `DeliverComponent`, `DeliverType`) als auch vier +Implementierungen (`ServerBuilderImpl`, `ServerDeliverComponentImpl`, +`TaskBuilderImpl`, `TaskComponentImpl`). Siehe OLF-L2-02. + +**Zwei erprobte Zuschnitte.** OLF hat bereits zwei funktionierende Topologien — +die Wahl richtet sich nach der Projektgröße, nicht nach Geschmack: + +*Tamias (schlank, 181 Dateien)* — drei Module, für Projekte mit einer Server-Rolle: + +``` +:common geteilte Domäne (area, config, event, explosion, map, ground, ...) +:game Spiel-Logik, application +:setup Setup-Modus, application +``` + +*ManisGame (ausgebaut, 359 Dateien)* — geteilte Basis fein geschnitten, darauf +mehrere Server-Varianten: + +``` +shared:api reine Contracts, 0 Impl, compileOnly(minestom) +shared:common geteilte Domäne +shared:database Persistenz +shared:queue Messaging +shared:cloud CloudNet-Anbindung +shared:scare, shared:day Fach-Features +extensions:lobby Server-Variante (application + mainClass) +extensions:game Server-Variante +extensions:setup Server-Variante +``` + +**Empfehlung für Titan.** Der Tamias-Zuschnitt plus ein `:api`-Modul und die +Extension-Aufteilung aus Abschnitt 5 — Titan hat mit 74 Dateien nicht die Größe, +die ManisGames Sieben-Wege-Split rechtfertigt. Wächst Titan, ist der Weg von +Tamias- zu ManisGame-Topologie additiv: `:common` wird aufgeteilt, die Rollen +bleiben. + +**Namenswarnung.** ManisGames Verzeichnis `extensions/` enthält **keine** +Minestom-Extensions — es sind eigenständige Server-Anwendungen mit `mainClass` +und `shadowJar`; im gesamten Repository existiert keine `extension.json`. Für +echte Minestom-Extension-Jars sind Cygnus' und Titans `:bridge`-Module die +Referenz (Abschnitt 5). Wer ManisGame als Vorlage nimmt, darf die beiden +Bedeutungen von "Extension" nicht vermischen. + +--- + +### OLF-L1-04: Release Please, Renovate-Preset, OLF-Workflows + +**Regel.** Jedes Repository nutzt Release Please für Versionierung und Changelog, +das zentrale OneLiteFeather-Renovate-Preset und die reusable Workflows aus +`OneLiteFeatherNET/workflows`. + +**Begründung.** Titan und Cygnus erfüllen das bereits. Butterfly nutzt noch +`.releaserc.json` (semantic-release) — das ist die einzige verbleibende Abweichung +im betrachteten Umfeld und im Zuge der Extension-Umstellung mitzuziehen. + +--- + +## 2. Ebene L2 — Modul + +### OLF-L2-01: Eine Abhängigkeitsrichtung, keine Zyklen + +**Regel.** Abhängigkeiten fließen ausschließlich in eine Richtung: + +``` +extension ──▶ common ──▶ api + │ ▲ + └─────────────────────┘ +bootstrap ──▶ api (nur Contracts, nie common, nie extension) +``` + +Verboten: `common → extension`, `api → common`, `extension → extension`, +`bootstrap → common`. + +**Begründung.** Extensions laufen in eigenen Classloadern. Eine Extension, die +eine andere direkt referenziert, funktioniert im Test und bricht im Deployment — +oder erzwingt eine Ladereihenfolge, die niemand dokumentiert hat. Kommunikation +zwischen Extensions läuft über Events oder die `ServiceRegistry`, nie über +direkte Typen. Titan macht das an der kritischsten Stelle bereits richtig: +`TitanPermissionBridge` reicht nur JDK-Typen über die Classloader-Grenze. + +**Durchsetzung.** ArchUnit in `olf.quality-conventions`: + +```java +@ArchTest +static final ArchRule modules_respect_dependency_direction = + layeredArchitecture().consideringOnlyDependenciesInLayers() + .layer("api").definedBy("..titan.api..") + .layer("common").definedBy("..titan.common..") + .layer("extension").definedBy("..titan.lobby..", "..titan.setup..", "..titan.bridge..") + .whereLayer("extension").mayNotBeAccessedByAnyLayer() + .whereLayer("common").mayOnlyBeAccessedByLayers("extension") + .as("OLF-L2-01"); +``` + +--- + +### OLF-L2-02: `:api` enthält Contracts, niemals Implementierungen + +**Regel.** Das öffentliche API besteht aus Interfaces, Records als reine +Wertträger, Enums und Konstanten. Implementierungen dürfen im selben Modul liegen, +müssen dann aber **package-private** sein und über Factory-Methoden am Interface +erreicht werden. Öffentliche `*Impl`-Typen sind verboten. + +**Positivbeispiel: Titans `Deliver`-API.** Entgegen dem, was die reine Dateizählung +nahelegt, ist dieses API bereits vorbildlich gebaut — es ist die interne Vorlage +für diese Regel: + +```java +public sealed interface DeliverComponent permits DeliverComponent.TaskComponent, + DeliverComponent.ServerDeliverComponent, TaskComponentImpl, ServerDeliverComponentImpl { + + DeliverType type(); + UUID playerId(); + + static TaskBuilder taskBuilder() { return new TaskBuilderImpl(); } + static ServerBuilder serverBuilder() { return new ServerBuilderImpl(); } + + sealed interface TaskComponent extends DeliverComponent permits TaskComponentImpl { + String taskName(); + } + + sealed interface Builder> { + T playerId(UUID playerId); + default T player(Player player) { return playerId(player.getUuid()); } + DeliverComponent build(); + } +} +``` + +Alle vier `*Impl` sind package-private (`final class TaskBuilderImpl`, +`record TaskComponentImpl`), der Contract wird ausschließlich über +Factory-Methoden betreten, und der Builder nutzt self-typed Generics. Ein +Konsument des Moduls sieht die Implementierungen nicht. + +**Warum `permits` die Impls nennen darf.** Die `permits`-Klausel eines `sealed` +Typs muss jeden direkten Subtyp benennen — das ist eine Sprachanforderung, kein +Leck. Sichtbarkeit entscheidet über die Kapselung, nicht die Erwähnung im +`permits`. Ein `permits`-Eintrag auf einen package-private Typ ist genau richtig. + +**Was hier trotzdem zu tun bleibt.** Die Einträge `TaskComponentImpl` und +`ServerDeliverComponentImpl` in der **Wurzel**-`permits`-Klausel sind redundant, +weil beide Typen bereits über `TaskComponent` bzw. `ServerDeliverComponent` +erreichbar sind. Aufräumen ist kosmetisch, kein Regelverstoß. + +Der reale Mangel liegt woanders: `:api` trägt kein `maven-publish`. Publiziert +werden nur `titan-app`, `titan-setup` und `titan-bridge`. Das erklärte Ziel — ein +Contract, den andere Projekte konsumieren — ist damit nicht erreichbar; wer ihn +will, müsste den Fat-Jar der Lobby ziehen. Siehe offener Punkt 3. + +**Die Gegenprobe.** ManisGames `shared:api` enthält bei 359 Projektdateien +**null** `*Impl`-Klassen und deklariert Minestom konsequent als `compileOnly` — +das API-Modul zieht also nicht einmal eine Laufzeitabhängigkeit auf den Server +nach: + +```kotlin +plugins { id("manis.library-conventions") } + +dependencies { + api(platform(libs.manis.bom)) + compileOnly(libs.minestom) // API beschreibt, sie führt nicht aus + implementation(libs.caffeine) +} +``` + +Dass das bei fast fünffacher Projektgröße durchgehalten wird, entkräftet das +übliche Gegenargument, saubere API-Grenzen seien nur bei kleinen Projekten +praktikabel. + +**Korrekte Form** — `sealed` erlaubt genau die Sub-Contracts, die Implementierung +liegt in `:common` und wird über eine Factory-Methode erreicht (Vorbild: Aves +`MapEntry`): + +```java +public sealed interface DeliverComponent + permits DeliverComponent.TaskComponent, DeliverComponent.ServerDeliverComponent { + + @Contract(pure = true, value = "_, _ -> new") + static ServerDeliverComponent server(String name, UUID id) { ... } + + DeliverType type(); +} +``` + +**Durchsetzung.** + +```java +@ArchTest +static final ArchRule api_has_no_implementations = + noClasses().that().resideInAPackage("..titan.api..") + .should().haveSimpleNameEndingWith("Impl") + .as("OLF-L2-02"); +``` + +--- + +### OLF-L2-03: Ein Paketwurzel-Präfix pro Projekt + +**Regel.** Alle Typen eines Projekts liegen unterhalb genau eines +Wurzelpakets: `net.onelitefeather.`. + +**Begründung.** Titans `:api`-Modul hat heute **zwei** Wurzeln: +`net.onelitefeather.deliver` (6 Klassen) und `net.onelitefeather.titan.api.deliver` +(1 Klasse) — wobei die eine Klasse aus der anderen Wurzel importiert. Das ist +nicht nur unordentlich, es macht auch jede paketbasierte Regel (ArchUnit, +Checkstyle, `@NotNullByDefault`) unzuverlässig, weil sie eine der beiden Wurzeln +übersieht. + +**Durchsetzung.** + +```java +@ArchTest +static final ArchRule single_root_package = + classes().should().resideInAPackage("net.onelitefeather.titan..") + .as("OLF-L2-03"); +``` + +--- + +### OLF-L2-04: Reuse vor Reimplementierung + +**Regel.** Bevor eine Abstraktion in `:common` neu entsteht, ist zu prüfen, ob +Aves, Xerus, Coris, Guira oder pica sie bereits liefert. Eine bewusste +Eigenimplementierung braucht einen Kommentar, der begründet, warum die +Library-Variante nicht passt. + +**Begründung.** Das ist der teuerste Verstoß in Titan. Titan importiert Aves +bereits (`net.theevilreaper.aves.map.BaseMap`, `GsonFileHandler`, +`PositionGsonAdapter`) — und baut die Kernabstraktion daneben trotzdem selbst: + +| Titan (Eigenbau) | Aves (vorhanden) | +|---|---| +| `final class MapProvider` | `interface MapProvider` + `abstract class AbstractMapProvider` | +| `record MapEntry(Path path)` | `sealed interface MapEntry permits BaseMapEntry` mit `of(...)`-Factories | +| `Function, List>` | `@FunctionalInterface PathFilter` | +| `MapPool`, `LobbyMap`, `LobbyMapBuilder` | `BaseMap`, `BaseMapBuilder`, `ChunkLoaderFactory` | + +Titans `MapProvider` ist eine `final class` ohne Interface — also weder +mockbar noch erweiterbar, obwohl Aves genau dafür `AbstractMapProvider` als +Erweiterungspunkt anbietet. Dass es anders geht, zeigt ManisGames +`SetupExtension`: es importiert `net.theevilreaper.aves.map.provider.MapProvider` +direkt und leitet für den Sonderfall `SetupMapProvider` ab, statt daneben etwas +Eigenes zu bauen. + +**Der org-weite Fall: `ThreadHelper`.** Diese Klasse existiert **viermal** im +OneLiteFeather-Bestand, drei Kopien davon byte-identisch: + +| Ort | Inhalt | +|---|---| +| `titan/common/utils/ThreadHelper.java` | identisch | +| `butterfly/minestom/feature/ThreadHelper.java` | identisch | +| `butterfly/bukkit/utils/ThreadHelper.java` | identisch | +| `manis/common/util/ThreadHelper.java` | leicht abweichende Variante | + +Alle vier lösen dasselbe Problem: den Context-Classloader für einen +`ServiceLoader`-Aufruf temporär umzubiegen. Das ist keine Titan-Nachlässigkeit, +sondern eine org-weite Lücke — es fehlt schlicht der Ort, an den solcher Code +gehört. Dieselbe Geschichte bei `SingletonFeatureManagerProvider` +(Titan + Butterfly). + +**Konsequenz für den Standard.** Querschnittscode dieser Art gehört in eine +Library (Aves für Allgemeines, Butterfly für LuckPerms-/Feature-Flag-Nahes), nicht +in das jeweilige `common` des Projekts. Die Regel lautet daher konkret: **Wird +eine Hilfsklasse in einem zweiten Projekt gebraucht, wird sie nicht kopiert, +sondern in die passende Library gehoben.** + +**Durchsetzung.** Nicht automatisierbar, aber im Review verbindlich: Eine neue +Klasse in `:common`, deren Name einem Aves-/Xerus-Typ entspricht +(`*Provider`, `*Entry`, `*Pool`, `*Builder`, `Kit*`, `Team*`, `Phase*`), erfordert +die explizite Begründung im Klassen-Javadoc. + +--- + +### OLF-L2-05: Kein statischer Zustand über Modulgrenzen + +**Regel.** Statische Felder mit veränderlichem Zustand sind auf Modulebene +verboten. Muss Zustand eine Classloader-Grenze queren, geschieht das über einen +Holder, der ausschließlich JDK-Typen in seiner Signatur führt. + +**Begründung.** Voraussetzung für das Extension-Modell (Abschnitt 4). Titan +erfüllt das an der schwierigsten Stelle bereits: `TitanPermissionBridge` reicht +`(UUID, String) -> boolean` über die Grenze zwischen Anwendungs- und +Extension-Classloader — kein LuckPerms-Typ, kein CloudNet-Typ. Diese Lösung ist +die Blaupause, nicht die Ausnahme. + +**Bekannte Abweichung.** `SingletonFeatureManagerProvider` hält ein statisches, +lazy initialisiertes `FeatureManager`-Feld. Das ist durch das Togglz-SPI +vorgegeben (`FeatureManagerProvider` wird per ServiceLoader instanziiert) und +bleibt zulässig — aber genau deshalb gehört die Klasse nach Butterfly und nicht +in beide Projekte kopiert. + +--- + +## 3. Ebene L3 — Paket + +### OLF-L3-01: `package-info.java` mit `@NotNullByDefault` in jedem Paket + +**Regel.** Jedes Paket mit mindestens einem öffentlichen Typ hat eine +`package-info.java`. Nullability wird dort einmal deklariert, nicht pro Parameter +verteilt. + +```java +@NotNullByDefault +package net.onelitefeather.titan.common.map; + +import org.jetbrains.annotations.NotNullByDefault; +``` + +Innerhalb eines so annotierten Pakets wird nur noch `@Nullable` gesetzt — als +bewusste Ausnahme. Ein `@NotNull` dort ist dann redundant und wird entfernt. + +**Begründung.** Das ist die durchgängigste Konvention der Referenzprojekte: +Cygnus 54 Pakete, Aves 14, pica 11, Coris 8, Guira 4. Titan hat **null** — und +stattdessen 24 Dateien mit handverteilten `@NotNull`. Handverteilte Annotationen +sind unvollständig per Konstruktion: sie stehen dort, wo jemand daran gedacht hat. +Die Paket-Variante kehrt den Default um, wodurch die Lücke sichtbar statt still +wird. + +**Durchsetzung.** Nicht selbst schreiben — Falcos +`PublicApiTest.everyPublishedPackageDeclaresNullness` übernehmen. Die Annotation +wird dort bewusst **als String** gematcht, damit die Regel nicht davon abhängt, +dass das Annotations-Artefakt zur Importzeit auflösbar ist; ein Paket ganz ohne +`package-info` fällt ebenfalls durch — der Fall, um den es eigentlich geht: + +```java +private static final ArchCondition IN_NOT_NULL_BY_DEFAULT_PACKAGE = + new ArchCondition<>("reside in a @NotNullByDefault package") { + @Override + public void check(JavaClass item, ConditionEvents events) { + Optional> info = item.getPackage().tryGetPackageInfo(); + boolean ok = info.isPresent() + && item.getPackage().isAnnotatedWith( + "org.jetbrains.annotations.NotNullByDefault"); + events.add(new SimpleConditionEvent(item, ok, + "package " + item.getPackageName() + " has no @NotNullByDefault package-info")); + } + }; + +@ArchTest +static final ArchRule everyPublishedPackageDeclaresNullness = classes() + .that().resideInAnyPackage(PUBLISHED) + .and(not(PACKAGE_INFO)) + .should(IN_NOT_NULL_BY_DEFAULT_PACKAGE) + .because("not one bare @NotNull exists in the three modules; the non-nullability of the " + + "whole API rests on the three package-info lines"); +``` + +--- + +### OLF-L3-02: Fachliche Paketnamen, kein `utils`/`helper`-Sammelbecken + +**Regel.** Pakete werden nach Fachlichkeit geschnitten, nicht nach technischer +Kategorie. `util` ist zulässig für echte, zustandslose Querschnittsfunktionen +(Aves hat `util/vector`, `util/collection`, `util/functional` — jeweils fachlich +scharf). Ein `utils`-Paket als Ablage für alles, was sonst nirgends passt, ist es +nicht. + +**Begründung.** Titans `common/utils` enthält heute sieben Typen ohne +gemeinsamen Nenner: + +| Klasse | gehört fachlich nach | +|---|---| +| `Cancelable` | `common/event` | +| `CloudNetEnvironment` | `common/bootstrap` | +| `Items` | `common/item` (oder ersetzbar durch Aves `item`) | +| `SingletonFeatureManagerProvider` | Butterfly (Duplikat, siehe OLF-L2-04) | +| `Tags` | `common/tag` (zusammen mit `tags/PosTagSerializer`) | +| `ThreadHelper` | Butterfly (Duplikat) | +| `TitanFeatures` | `common/feature` | + +Ein `utils`-Paket wächst monoton: es hat keine Definition, also kann nichts +hineinpassen und nichts wieder hinaus. Dasselbe gilt für `common/helper` +(`BlockHandlerHelper`, `SitHelper`) — siehe OLF-L4-04. + +**Durchsetzung.** + +```java +@ArchTest +static final ArchRule no_generic_util_package = + noClasses().should().resideInAnyPackage("..utils", "..helper", "..misc", "..common.impl") + .as("OLF-L3-02"); +``` + +--- + +### OLF-L3-03: Events liegen im `event`-Unterpaket ihres Fachpakets + +**Regel.** Ein Event gehört in das `event`-Unterpaket der Fachlichkeit, die es +auslöst — nicht in ein globales Event-Paket. + +**Begründung.** Das Muster ist in allen sechs geprüften Referenzprojekten +identisch: Coris `door/event`, `floor/event`; Xerus `kit/event`, `team/event`; +pica `dialog/event`; Cygnus `map/event`, `page/event`, `player/event`, `view/event`; +Tamias `common/map/event`, `game/round/event`; ManisGame `api/door/event`, +`lobby/event`, `setup/event/dialog`, `hud/event`. + +Der Vorteil ist Lokalität: wer das Feature liest, sieht seine Events, ohne ein +Sammelpaket zu durchsuchen. Tamias treibt es am weitesten und spiegelt die +Event-Struktur auch bei den Listenern (`game/listener/area`, `listener/round`, +`listener/team`) — bei 181 Dateien ist das der Unterschied zwischen Navigieren +und Suchen. + +Titan hat aktuell ein flaches `common/event` mit einer Klasse — bei der +derzeitigen Größe unkritisch, aber beim Ausbau die falsche Weiche. Titans +`app/listener` mit 14 Listenern in einem flachen Paket ist bereits jetzt der Fall, +in dem sich die Tamias-Untergliederung lohnen würde (`listener/sit`, +`listener/elytra`, `listener/player`). + +--- + +## 4. Ebene L4 — Klasse + +### OLF-L4-01: Javadoc-Header auf jedem öffentlichen Typ + +**Regel.** Jeder öffentliche Typ trägt einen Javadoc-Block mit Beschreibung und +den Tags `@author`, `@version`, `@since`. `@version` wird bei jeder +API-relevanten Änderung erhöht, `@since` nie. + +**Referenz** (Aves `MapProvider`): + +```java +/** + * The {@link MapProvider} interface is responsible for managing the available maps. + * It will load all maps data from the given path and store them. + * It would not load the map itself over a {@link AnvilLoader} instance. + * This behavior is handled by another class. + * + * @author theEvilReaper + * @version 1.1.0 + * @since 1.6.0 + */ +public interface MapProvider { +``` + +**Begründung.** `@since` ist der einzige Weg, ohne `git blame` zu erkennen, ob +ein Typ zum stabilen Kern gehört oder letzte Woche entstand — bei einer Library, +die andere Projekte konsumieren, ist das der Unterschied zwischen "kann ich mich +darauf verlassen" und "muss ich nachfragen". Zahlen siehe Abschnitt 0. + +**Durchsetzung.** Checkstyle `JavadocType` mit +`allowedAnnotations=""` und `authorFormat`/`versionFormat` gesetzt, plus +`MissingJavadocType` auf `scope=public`. + +--- + +### OLF-L4-02: Interface, dann `Abstract`/`Base`, dann Factory-Methode + +**Regel.** Ein Typ, den andere Module benutzen, wird als Interface eingeführt. +Wiederverwendbare Teilimplementierung heißt `Abstract*` oder `Base*`. Die +Instanziierung läuft über eine statische Factory-Methode auf dem Interface, nicht +über einen öffentlichen Konstruktor. + +**Referenz** (Aves `MapEntry` — Interface, `sealed`, Factory, `@Contract`): + +```java +public sealed interface MapEntry permits BaseMapEntry { + + String MAP_FILE = "map.json"; + + @Contract(pure = true, value = "_ -> new") + static MapEntry of(Path directoryRoot) { + return new BaseMapEntry(directoryRoot, MAP_FILE); + } + + @Contract(pure = true, value = "_, _ -> new") + static MapEntry of(Path directoryRoot, String mapFileNaming) { + return new BaseMapEntry(directoryRoot, mapFileNaming); + } +} +``` + +**Begründung.** Die Factory-Methode ist der Punkt, an dem die Implementierung +wechseln kann, ohne dass ein Aufrufer es merkt — der Kern des Dependency +Inversion Principle. `@Contract(pure = true, value = "_ -> new")` teilt der IDE +und statischen Analyse mit, dass der Aufruf nebenwirkungsfrei ist und ein neues +Objekt liefert. + +Titan hat dieses Muster bereits an mehreren Stellen (`MapProvider.create(...)`, +`AppConfigProvider.create(...)`, `DeliverProvider.create()`, `Titan.instance()`), +aber jeweils ohne Interface — die Factory liefert die konkrete `final class` +zurück. Damit fehlt genau die Austauschbarkeit, für die die Factory da ist, und +Tests müssen die echte Klasse konstruieren statt eine Test-Implementierung +einzusetzen. + +--- + +### OLF-L4-03: Builder für Objekte mit mehr als drei optionalen Feldern + +**Regel.** Konfigurationsobjekte mit mehr als drei optionalen Feldern bekommen +einen Builder. Der Builder ist eine eigene Klasse `*Builder` und wird über eine +Factory-Methode am Zieltyp erreicht. + +**Begründung.** Aves nutzt das durchgängig (8 Builder-Klassen: `InventoryBuilder`, +`PageableInventoryBuilder`, `BaseMapBuilder`, …), pica ebenso (7). Titan hat mit +`AppConfigBuilder` und `LobbyMapBuilder` bereits zwei — die Konvention ist also +schon da und muss nur festgeschrieben werden. `AppConfig` mit seinen elf +Zugriffsmethoden ist genau der Fall, für den die Regel existiert. + +--- + +### OLF-L4-04: Statische Helfer werden Services + +**Regel.** Eine Klasse mit ausschließlich statischen Methoden und mehr als +~50 Zeilen Fachlogik wird ein Service mit Interface und Instanz. Rein +funktionale, zustandslose Umrechnungen (Vektor-Mathematik, Farbkonvertierung) +dürfen statisch bleiben — dann aber als `final class` mit privatem Konstruktor. + +**Begründung.** `SitHelper` in Titan hat 102 Zeilen statische Sitz-Logik, gegen +die 378 Zeilen Testcode anschreiben — das schlechteste Test-zu-Code-Verhältnis im +Projekt, weil jeder Testfall den globalen Zustand selbst herstellen muss, statt +eine konfigurierte Instanz zu bekommen. Statische +Fachlogik lässt sich weder ersetzen noch in einem anderen Kontext anders +konfigurieren — sie ist der Gegenentwurf zu OLF-L4-02. `NavigationHelper` macht +es bereits besser (`NavigationHelper.instance(deliver)` mit injizierter +Abhängigkeit) und ist die Vorlage für `SitHelper`. + +`BlockHandlerHelper.registerAll()` ist der zulässige Gegenfall: eine einmalige +Registrierung ohne Zustand und ohne Variantenbedarf. + +--- + +### OLF-L4-05: `final class` + privater Konstruktor für echte Utilities + +**Regel.** Nicht instanziierbare Klassen sind `final` und haben einen privaten +Konstruktor ohne Rumpf. + +**Referenz** (Cygnus `ServiceBootstrap`): + +```java +public final class ServiceBootstrap { + private ServiceBootstrap() { + } +} +``` + +**Durchsetzung.** Checkstyle `HideUtilityClassConstructor` + `FinalClass`. + +--- + +### OLF-L4-06: `@ApiStatus` für nicht-stabile API + +**Regel.** Öffentliche Typen, die noch nicht stabil sind, tragen +`@ApiStatus.Experimental`. Typen, die technisch öffentlich sein müssen, aber +nicht zur API gehören, tragen `@ApiStatus.Internal`. + +**Begründung.** Aves nutzt das in 8, pica in 5 Dateien; Coris markiert seine +Kern-Interfaces bewusst als `@ApiStatus.Experimental`. Ohne diese Markierung ist +jede öffentliche Klasse implizit ein Versprechen. Titan nutzt `@ApiStatus` +bislang gar nicht — bei einem Modul namens `:api`, das publiziert wird, ist das +eine Lücke. + +--- + +### OLF-L4-07: Ein Test pro Fachklasse, Cyano für Minestom-Integration + +**Regel.** Jede Klasse mit Fachlogik hat eine Testklasse gleichen Namens mit +Suffix `Test` im spiegelbildlichen Paket. Tests, die einen laufenden +Minestom-Kontext brauchen (Player, Instance, Pakete), nutzen Cyano und heißen +`*IntegrationTest`. + +**Begründung.** Cygnus zeigt die Spiegelstruktur konsequent — zu +`common/page/PageFactory.java` gehört `common/page/PageFactoryTest.java`, zu +`player/PermissionAwarePlayer.java` gehört +`player/PermissionAwarePlayerIntegrationTest.java`. Titan folgt dem in `:common` +und `:app` bereits gut; die Lücke ist `:api` (7 Klassen, 0 Tests), `:setup` +(7 Klassen, 0 Tests) und `:bridge` (1 Klasse, 0 Tests). + +**Durchsetzung.** Jacoco-Coverage-Gate in `olf.java-conventions`, zunächst als +`violationRules` mit einer Schwelle, die den Ist-Stand nicht unterschreitet, und +dann schrittweise angehoben. Ein Gate, das sofort auf 80 % springt, wird +umgangen statt erfüllt. + +--- + +### OLF-L4-08: Ein Logger pro Klasse, keine `System.out` + +**Regel.** `private static final Logger LOGGER = LoggerFactory.getLogger(X.class);` +— SLF4J, keine Ausnahme. Jede Anwendung deklariert genau ein SLF4J-Binding als +`runtimeOnly`. + +**Begründung.** Cygnus dokumentiert die Falle im Buildfile: *"SLF4J needs a +binding at runtime; without one it falls back to NOP and the server logs nothing +at all."* — und deklariert entsprechend `libs.slf4j.api` als `implementation` und +`libs.slf4j.simple` als `runtimeOnly`. Titan deklariert **weder API noch Binding** +in irgendeinem Buildfile, nutzt `LoggerFactory` aber in `MapProvider` und +`MapPool`. Beides kommt derzeit transitiv über Minestom herein. Das funktioniert, +solange Minestom es mitbringt — und hört ohne Vorwarnung auf, wenn ein +Minestom-Update die Abhängigkeit umstellt. Der Ausfall ist dabei still: SLF4J +fällt auf NOP zurück und protokolliert einfach nichts mehr. + +--- + +## 5. Extension-Modell + +### 5.1 Zielbild + +Der Host-Prozess enthält keine Fachlogik. Er initialisiert Minestom, startet den +`ExtensionBootstrap` und übergibt. + +``` +app-titan.jar Host: main(), ServiceBootstrap, Minestom, Auth +└─ extensions/ + ├─ butterfly-minestom.jar Chat-Format, Prefix, LuckPerms-Anbindung + ├─ titan-lobby.jar Sit, Elytra, Tickle, Navigation, Commands + ├─ titan-setup.jar Map-Setup-Modus + ├─ titan-bridge.jar CloudNet -> LuckPerms + └─ luckperms.jar JarInJar-Loader +``` + +**Was das löst.** + +- Butterfly wird nicht mehr in den Fat-Jar geshadet. Die byte-identischen Kopien + `ThreadHelper` und `SingletonFeatureManagerProvider` in Titans `common/utils` + entfallen ersatzlos (OLF-L2-04). +- Lobby-Features werden einzeln deaktivierbar, ohne Rebuild des Hosts. +- Der Host wird testbar, weil er nichts Fachliches mehr tut. +- `:setup` und `:lobby` teilen denselben Host, statt zwei getrennte + `application`-Module mit je eigenem `Titan.java` zu sein. + +### 5.2 OLF-L5-01: `extension.json` wird generiert, nicht gepflegt + +**Regel.** Extension-Metadaten entstehen über den Annotation Processor +`minestom-extensions-processor`, nicht als handgepflegte Ressource. + +**Begründung.** Titan pflegt `bridge/src/main/resources/extension.json` von Hand +und stanzt die Version über `ReplaceTokens` und einen `@version@`-Platzhalter +ein — inklusive `inputs.properties`-Boilerplate im Buildfile. Cygnus lässt den +Processor die Datei erzeugen und übergibt nur die Version als Compiler-Argument: + +```kotlin +compileOnly(libs.minestom.extensions.processor) +annotationProcessor(libs.minestom.extensions.processor) +// ... +options.compilerArgs.add("-Aminestom.extension.version=${rootProject.version}") +``` + +Der Processor validiert dabei, dass der Entrypoint existiert und die richtige +Basisklasse erweitert — die handgepflegte Variante fällt erst zur Laufzeit auf. + +### 5.3 OLF-L5-02: OLF-Fork statt archiviertes Upstream + +**Regel.** Extension-fähige Projekte nutzen `net.onelitefeather:minestom-extensions` +(über `minestom-extensions-bom`). + +**Begründung.** Titan nutzt heute `dev.hollowcube:minestom-ce-extensions:1.2.0`. +Das Upstream-Repository `hollow-cube/minestom-ce-extensions` ist archiviert; der +OneLiteFeather-Fork (bei Cygnus in Version 2.1.1 im Einsatz) hat identische +Paketnamen, wird gepflegt und liefert zusätzlich den Annotation Processor aus +OLF-L5-01. Beim Wechsel entfällt außerdem die JitPack-Proxy-Repository-Definition, +die Titan nur wegen `DependencyGetter` braucht. + +### 5.4 OLF-L5-03: Alles, was der Host liefert, ist `compileOnly` + +**Regel.** Ein Extension-Modul deklariert Minestom, das Extension-Framework, +CloudNet und `:common` als `compileOnly`. Gebündelt wird nur, was ausschließlich +diese Extension braucht. + +**Begründung.** Titans `:bridge` macht das bereits vollständig richtig und ist +die Vorlage für die neuen Extension-Module. Wird eine vom Host gelieferte +Bibliothek mitgebündelt, existiert sie zur Laufzeit zweimal in zwei +Classloadern — die Fehlerbilder daraus (`NoSuchMethodError`, `ClassCastException` +zwischen identisch benannten Typen) sind teuer zu diagnostizieren. + +--- + +## 6. Anhang A — Checkstyle- und ArchUnit-Regelabbildung + +> **Die Durchsetzungsschicht existiert bei OLF bereits.** Falco hat ein eigenes, +> nie publiziertes Modul `falco-archunit` mit **41 ArchUnit-Regeln** in fünf +> Testklassen, die als normale JUnit-Tests laufen. Mehrere davon sind exakt die +> Regeln dieses Dokuments — `everyPublishedPackageDeclaresNullness` ist OLF-L3-01, +> `publishedModulesOnlyUseDeclaredDependencies` ist OLF-L2-01, +> `publicApiIsMarkedExperimental` ist OLF-L4-06, `publicClassesAreFinal` und +> `utilityClassesHideTheirConstructor` sind OLF-L4-05, `noMutableStaticFields` +> ist OLF-L2-05, `onlySlf` / `loggerFieldShape` / `noPrintStackTrace` sind +> OLF-L4-08. +> +> Die Tabelle unten ist damit **keine Konstruktionsanleitung mehr, sondern eine +> Abbildung auf vorhandenen Code.** Falcos Implementierung wird kopiert, nicht +> nachgebaut. Ihr Stil ist ebenfalls zu übernehmen: jede Regel endet auf +> `.because(...)` mit der echten Begründung, Ausnahmen (Records, Enums, +> Throwables, Minestom-Subklassen) stehen in der Regel statt in einer +> Unterdrückungsliste, und das Regel-Javadoc benennt die Grenze der Regel +> ausdrücklich — die Nullability-Regel etwa prüft, *dass* der Default deklariert +> ist, nicht dass eine Signatur ihn einhält. + +| Regel-ID | Werkzeug | Konkret | +|---|---|---| +| OLF-L1-01 | Review + Grep-Task | Kein `toolchain`/`publishing`-Block im Modul-Buildfile | +| OLF-L2-01 | ArchUnit | `layeredArchitecture()` | +| OLF-L2-02 | ArchUnit | `noClasses().that().arePublic().and().resideInAPackage("..api..").should().haveSimpleNameEndingWith("Impl")` — die Sichtbarkeit ist der Punkt, nicht der Name | +| OLF-L2-03 | ArchUnit | `classes().should().resideInAPackage("net.onelitefeather.titan..")` | +| OLF-L2-05 | ArchUnit | `fields().that().areStatic().and().arePublic().should().beFinal()`, plus Ausnahmeliste für SPI-Holder | +| OLF-L3-01 | ArchUnit | Paket-Annotation `@NotNullByDefault` vorhanden | +| OLF-L3-02 | ArchUnit | `noClasses().resideInAnyPackage("..utils", "..helper", "..misc")` | +| OLF-L4-01 | Checkstyle | `MissingJavadocType`, `JavadocType` mit `authorFormat` | +| OLF-L4-05 | Checkstyle | `HideUtilityClassConstructor`, `FinalClass` | +| OLF-L4-07 | Jacoco | `violationRules`, Schwelle schrittweise angehoben | +| OLF-L4-08 | Checkstyle | `RegexpSinglelineJava` auf `System\.(out\|err)` | + +**Bewusst nicht automatisiert.** OLF-L1-02 (BOM-Nutzung), OLF-L1-03 (Modulrolle), +OLF-L1-04 (Release-Tooling), OLF-L2-04 (Reuse vor Reimplementierung), +OLF-L3-03 (Event-Platzierung), OLF-L4-02 bis OLF-L4-04 (Pattern-Wahl), +OLF-L4-06 (`@ApiStatus`) und OLF-L5-01 bis OLF-L5-03 (Extension-Setup) sind +Entwurfsentscheidungen, die ein Werkzeug nicht beurteilen kann. Sie gelten im +Review — die Regel-ID im PR-Kommentar ersetzt die Diskussion über Geschmack. + +**Einführungsstrategie.** Alle Gates starten als Warnung mit einer Baseline, die +den Ist-Stand einschließt. Pro Migrationsphase (Anhang B) wird die Baseline für +den berührten Bereich entfernt und das Gate dort scharf geschaltet. Ein Gate, das +beim Einschalten 400 Verstöße meldet, wird abgeschaltet statt behoben. + +--- + +## 7. Anhang B — Migrationsplan Titan + +Fünf Phasen. Jede ist für sich lauffähig, testbar und mergebar; keine Phase +setzt voraus, dass eine spätere bereits begonnen wurde. + +### Phase 1 — `buildSrc` einführen (kein Produktionscode betroffen) + +- `buildSrc` mit `olf.java-conventions`, `olf.publish-conventions` anlegen +- Toolchain (5×), Test-Konfiguration (4×) und Publishing-Block (3×) dorthin + zusammenführen +- Checkstyle/ArchUnit als Warnung mit Voll-Baseline aktivieren +- `slf4j-api` als `implementation` und ein Binding als `runtimeOnly` explizit + deklarieren, statt sich auf Minestoms transitive Abhängigkeit zu verlassen + (OLF-L4-08) +- **Prüfbar:** `./gradlew build` liefert byte-gleiche Artefakte wie vorher + +### Phase 2 — Paket- und API-Hygiene + +- `net.onelitefeather.deliver` nach `net.onelitefeather.titan.api.deliver` + verschieben (OLF-L2-03) +- Redundante `permits`-Einträge (`TaskComponentImpl`, + `ServerDeliverComponentImpl`) aus der Wurzelklausel von `DeliverComponent` + entfernen — kosmetisch, kein Verstoß (OLF-L2-02). Die vier `*Impl` bleiben, wo + sie sind: package-private hinter Factory-Methoden ist das Zielbild. +- `package-info.java` mit `@NotNullByDefault` in allen Paketen anlegen, + redundante `@NotNull` entfernen (OLF-L3-01) +- Javadoc-Header auf öffentlichen Typen ergänzen (OLF-L4-01) +- **Prüfbar:** ArchUnit-Regeln L2-02, L2-03, L3-01 ohne Baseline grün + +### Phase 3 — Reuse statt Eigenbau + +- `common/utils` und `common/helper` fachlich auflösen (OLF-L3-02, Tabelle in + Abschnitt 3) +- `app/listener` nach Fachlichkeit untergliedern (OLF-L3-03) +- `ThreadHelper` und `SingletonFeatureManagerProvider` in Titan löschen und aus + Butterfly beziehen (OLF-L2-04). **Vorbedingung:** Butterfly muss sie als + konsumierbares Artefakt bereitstellen — heute liegen sie dort in + `minestom/feature` und `bukkit/utils` doppelt und gehören plattformneutral nach + `butterfly:api`. Diese Konsolidierung ist ein eigener Butterfly-PR und blockiert + die übrigen Punkte dieser Phase nicht. +- Titans `MapProvider`/`MapEntry`/`MapPool` auf Aves' `AbstractMapProvider`, + `MapEntry.of(...)` und `PathFilter` umstellen. (`LobbyMap` erbt bereits von + `BaseMap` und hat bereits `sealed interface Builder permits LobbyMapBuilder` + plus Factory — dort ist nichts zu tun.) +- `GsonFileHandler` → `ModernGsonFileHandler`: die von `MapProvider` genutzte + Variante ist in Aves `@Deprecated(since = "1.9.0", forRemoval = true)` +- `SitHelper` zum Service mit Interface machen, `NavigationHelper` als Vorlage + (OLF-L4-04) +- **Prüfbar:** Bestehende Tests grün, `common`-Klassenzahl sinkt + +### Phase 4 — Bootstrap extrahieren + +- `:bootstrap`-Modul anlegen; aus `TitanApplication.main()` werden + `ServiceBootstrap` (Bind-Host/Port, Working-Dir, Konsole), + `VelocityAuthResolver` und `LuckPermsBootstrap` +- Cygnus' `ServiceBootstrap` ist die Vorlage — inklusive Tests + (`ServiceBootstrapTest`, `StopCommandTest`) +- Der Reflection-Hack für `ExtensionBootstrap(MinecraftServer)` wird beim + Wechsel auf den OLF-Fork (OLF-L5-02) geprüft; bietet der Fork einen + öffentlichen Konstruktor, entfällt er ersatzlos +- `app/Titan.java` und `setup/Titan.java` teilen sich die extrahierte + Registrierungslogik +- **Prüfbar:** Bootstrap-Klassen haben Tests; `main()` unter 30 Zeilen + +### Phase 5 — Extension-Split + +- Wechsel auf `net.onelitefeather:minestom-extensions` (OLF-L5-02) +- `:app` wird zu `:lobby` als Extension-Modul, `:setup` ebenso; beide mit + `compileOnly` auf Host-Abhängigkeiten (OLF-L5-03) +- `extension.json` überall über Annotation Processor (OLF-L5-01) +- Butterfly als Extension deployen statt shaden; Butterfly-Repo auf Release + Please migrieren (OLF-L1-04) +- AOT-Cache-Training auf den Host-Jar plus Extensions anpassen +- **Prüfbar:** Host startet ohne Extensions; Lobby-Features erscheinen erst mit + `titan-lobby.jar` in `extensions/` + +### Was ausdrücklich nicht Teil des Plans ist + +- Kein Umschreiben funktionierender Fachlogik ohne Regelbezug. Sit-, Elytra- und + Tickle-Verhalten bleiben identisch; sie ziehen nur um. +- Kein Wechsel des Formatierungswerkzeugs. Titans Spotless-Setup mit + `header.java` und dem Eclipse-Profil bleibt; Checkstyle prüft Struktur, nicht + Formatierung. (`.editorconfig` aus Cygnus/Butterfly kann ergänzt werden, ist + aber keine Regel.) +- Keine Änderung am CloudNet-Deployment oder an der LuckPerms-Einbindung über + das hinaus, was Phase 4 und 5 ohnehin berühren. + +--- + +## 8. Offene Punkte + +1. **Reflection-Hack im Bootstrap.** Ob `ExtensionBootstrap` im OLF-Fork einen + öffentlichen Konstruktor mit `MinecraftServer` anbietet, ist noch zu prüfen. + Falls nicht, ist ein Upstream-PR gegen den Fork der saubere Weg — der Hack + bleibt sonst dauerhaft in Phase 4 stehen. +2. **Coverage-Schwelle.** Der Startwert für das Jacoco-Gate ist noch zu + bestimmen. Vorschlag: Ist-Stand pro Modul messen, minus 5 Prozentpunkte + Puffer, danach pro Phase anheben. +3. **`:api` als publiziertes Artefakt.** Titan publiziert derzeit `titan-app`, + `titan-setup` und `titan-bridge`, aber nicht `:api`. Wenn `:api` ein echter + Contract für andere Projekte werden soll, gehört es publiziert — und braucht + dann `@ApiStatus`-Disziplin (OLF-L4-06) und eine Kompatibilitätszusage. + ManisGame versioniert sein `shared:api` bewusst eigenständig + (`version = "0.5.0"` im Modul-Buildfile, unabhängig vom Root) — das ist die + Vorlage, falls Titans `:api` publiziert werden soll. +4. **`ThreadHelper` org-weit konsolidieren.** Vier Kopien in drei Projekten + (OLF-L2-04). Der Zielort ist zu entscheiden: `butterfly:api` (plattformneutral, + nah an den bestehenden Nutzern) oder Aves (allgemeiner, aber + theEvilReaper-Hoheit). Diese Entscheidung betrifft nicht nur Titan und sollte + vor Phase 3 fallen. +5. **Verbindlichkeit für Bestandsprojekte.** Dieses Dokument beschreibt einen + Standard, den derzeit kein Projekt vollständig erfüllt — Tamias wendet sein + eigenes Convention-Plugin nicht überall an, Cygnus nutzt kein `@ApiStatus`, + Xerus keine `package-info`. Zu klären ist, ob der Standard nur für neue + Projekte und berührten Code gilt (Empfehlung) oder ob Bestandsprojekte + nachziehen müssen. diff --git a/docs/rollout-log.md b/docs/rollout-log.md new file mode 100644 index 00000000..ee34c5d4 --- /dev/null +++ b/docs/rollout-log.md @@ -0,0 +1,43 @@ +# Rollout-Log + +Nachweis darüber, welches Feature wann auf welcher Freigabestufe stand. Erfüllt +NFR-011 aus [`spec-lobby-saison-events.md`](spec-lobby-saison-events.md). + +**Diese Datei wird bei jedem Stufenwechsel ergänzt, nicht überschrieben.** Alte +Zeilen bleiben stehen — der Verlauf ist der Zweck. + +## Die Stufen + +| Stufe | Wer sieht es | Berechtigung | +|---|---|---| +| `intern` | nur das Team | `titan.feature.internal` | +| `premium` | Team **und** Premium-Spieler | `titan.feature.premium` | +| `ga` | alle Spieler | — | +| `aus` | niemand (Notausschalter) | — | + +Der Notausschalter schlägt jede Stufe und jedes Zeitfenster. Die Prüfreihenfolge +ist in US-3.07 festgelegt: erst Notausschalter, dann Stufe, dann Zeitfenster. + +## Verlauf + +| Datum | Feature | von → nach | Grund | Verantwortlich | +|---|---|---|---|---| +| — | — | — | noch kein Eintrag | — | + +## Wie ein Eintrag entsteht + +Ein Stufenwechsel wird an zwei Stellen festgehalten, und beide sind Pflicht: + +1. **Hier**, als neue Zeile mit Datum, Feature, Übergang, Grund und + verantwortlicher Person. +2. **Im Log der Anwendung**, automatisch beim Wechsel (US-3.09). + +Der Grund ist das Feld, das später zählt. „Auf premium gehoben" ist keine +Begründung; „interne Prüfung ohne Befund über zwei Wochen" ist eine. + +## Rücknahmen + +Eine Rücknahme (etwa `ga` → `aus`) bekommt eine eigene Zeile mit dem Grund und, +sobald bekannt, einen Verweis auf die Ursache. Zeilen werden nicht gelöscht, +wenn ein Feature später erneut freigegeben wird — gerade der zweite Anlauf ist +die interessante Information. diff --git a/docs/spec-lobby-saison-events.md b/docs/spec-lobby-saison-events.md new file mode 100644 index 00000000..d9cee2af --- /dev/null +++ b/docs/spec-lobby-saison-events.md @@ -0,0 +1,378 @@ +# Anforderungen: Titan-Lobby — Saison, Zeit und gestufte Auslieferung + +**Verantwortlichkeiten:** Konzept: @TheMeinerLP · Anforderungen gepflegt von: @TheMeinerLP +**Stand:** 21.08.2026 +**Projekttyp:** Technisches Projekt (Rolle „Als Entwickler/Betreiber", ergänzt um Spielersicht wo das Verhalten sichtbar ist) +**Baut auf:** [`olf-minestom-project-standard.md`](olf-minestom-project-standard.md) · [`event-modi-plan.md`](event-modi-plan.md) + +--- + +## 1. Kontext & Ausgangslage + +Titan ist der Lobby-Server des OneLiteFeather-Netzwerks auf Minestom. Er lädt +Welten heute über Minestoms `AnvilLoader` und wählt die aktive Lobby-Welt über +die System-Property `TITAN_LOBBY_MAP`. Ein Wert `halloween` ist im `setup`-Modul +bereits als Default verdrahtet — der Mechanismus für saisonale Lobbys existiert +also im Ansatz, aber ohne Zeitplan, ohne Rückfallebene und ohne Möglichkeit, +etwas zuerst nur einem Teil der Spieler zu zeigen. + +Gleichzeitig ist mit **Falco** eine eigene Engine entstanden, die Minestoms +Chunk-Loader und Lichtsystem ersetzt und deren `falco-instance` einen eigenen +Entladepfad mitbringt. Titan soll darauf wechseln — sowohl weil die eigene Engine +gepflegt wird, als auch weil die Lichtsteuerung Voraussetzung für die geplante +Echtzeit-Tageszeit ist. + +Das Vorhaben bündelt sechs Anforderungen, die technisch dieselbe Grundlage +brauchen: + +1. Falco als Chunk-Loader und Lichtsystem statt Minestoms Standardimplementierung +2. Saisonale Lobbys für Halloween und Winter, vorbereitet für weitere Spielmodi +3. Jahreszeiten und eine an Berlin gekoppelte Echtzeit-Tageszeit +4. Gestufte Auslieferung: interne Tests → Lite-Spieler → allgemeine Freigabe +5. Anzeige der Build-Server für berechtigte Teammitglieder +6. Portale zum Serverwechsel (optional, spätere Stufe) + +Resource Packs sind ausdrücklich **später** vorgesehen und in dieser Fassung nur +als Ausbaustufe umrissen, nicht ausspezifiziert. + +### Was der Research dazu beigetragen hat + +Zwei Befunde aus [`event-modi-plan.md`](event-modi-plan.md) prägen diese +Anforderungen und sind der Grund für einige Zuschnitte, die sonst willkürlich +wirken würden: + +- **Deko allein bewegt keine Spielerzahlen** (Deep Rock Galactic: kosmetische + Oktober-Events −0,37 % / −12,16 % / −4,92 %; inhaltliche Seasons +154 % / + +59 % / +142 %). Deshalb ist der Aufwand für saisonale Optik in dieser Spec + strikt auf „Daten statt Code" gedeckelt. +- **Wiederverwendung ist die Architektur, nicht die Sparmaßnahme.** InnoGames zu + ihrem Saison-Event in der sechsten Iteration: Währung und Laufzeit blieben + konstant, „leaving everything else open to change". Übertragen: Zeitfenster, + Zielgruppen-Stufen und Welt-Auswahl sind stabiler Code; alles Saisonale sind + austauschbare Daten. + +--- + +## 2. Ziele & Nicht-Ziele + +**Ziele:** + +* Titan lädt Welten über Falco statt über Minestoms `AnvilLoader` und steuert + Licht über `falco-light`. +* Eine saisonale Lobby lässt sich ohne Codeänderung ausrollen — neue Welt, neue + Konfiguration, fertig. +* Die Lobby zeigt Jahreszeit und Tageszeit passend zu Berlin, ohne dass jemand + etwas von Hand umstellt. +* Jedes Feature kann zuerst intern, dann für Lite-Spieler, dann für alle + freigegeben werden — über **einen** Mechanismus, nicht drei. +* Berechtigte Teammitglieder sehen die Build-Server im Navigator; alle anderen + sehen sie nicht. +* Der jeweils erreichte Rollout-Stand ist dokumentiert und nachvollziehbar. + +**Nicht-Ziele:** + +* **Kein** Resource-Pack-System in dieser Stufe (Stufe 6 umreißt es, mehr nicht). +* **Kein** Cosmetics- oder Belohnungssystem. +* **Keine** neuen Minispiele — die Lobby wird darauf *vorbereitet*, sie bekommt + sie nicht. +* **Keine** eigene Wetter-Simulation. Jahreszeit steuert Optik und Daten, nicht + Niederschlagslogik. +* **Kein** A/B-Testing. Die Rollout-Stufen sind sequenziell, nicht parallel. + +--- + +## 3. Stakeholder & Rollen + +| Rolle | Person | Interesse | +|---|---|---| +| Maintainer Titan | @TheMeinerLP | Architektur, Reviews, Deployment | +| Maintainer Falco | @TheMeinerLP | API-Stabilität der Engine, AGPL-Frage | +| Buildteam | (offen) | Liefert Saison-Welten, braucht klaren Übergabeweg | +| Betreiber | @TheMeinerLP | CloudNet-Templates, Rollout, Notausschalter | +| Lite-Spieler | — | Frühzugang als Gegenleistung für den Rang | +| Spieler | — | Funktionierende Lobby, keine halben Features | + +--- + +## 4. Ausbaustufen-Übersicht + +| Stufe | Kurzbeschreibung | Priorität | +|---|---|---| +| **Stufe 1** | Fundament: Falco-Umstieg, Welt-Auswahl absichern | Must | +| **Stufe 2** | Zeit: Jahreszeiten + Echtzeit-Tageszeit Berlin | Must | +| **Stufe 3** | Freigabe-Stufen: intern → Lite → GA | Must | +| **Stufe 4** | Saison-Pakete: Halloween und Winter als Daten | Should | +| **Stufe 5** | Build-Server im Navigator | Should | +| **Stufe 6** | Resource Packs pro Saison | Could | +| **Stufe 7** | Portale zum Serverwechsel | Could | + +Die Reihenfolge ist keine Empfehlung, sondern eine Abhängigkeitskette: Stufe 3 +braucht Stufe 1 (Gate greift auf die Welt-Auswahl zu), Stufe 4 braucht Stufe 2 +und 3. + +--- + +## 5. Nicht-funktionale Anforderungen + +| ID | Kategorie | Anforderung (EARS) | Priorität | +|---|---|---|---| +| NFR-001 | Kompatibilität | The Lobby shall mit der Minestom-Version kompatibel bleiben, die der `aonyx-bom` vorgibt. | Must | +| NFR-002 | Betrieb | If eine konfigurierte Saison-Welt beim Start nicht vorhanden ist, then shall die Lobby mit der Standardwelt starten und eine Warnung mit dem gesuchten Weltnamen protokollieren. | Must | +| NFR-003 | Betrieb | The Lobby shall ohne installiertes Saison-Paket vollständig funktionsfähig starten. | Must | +| NFR-004 | Betrieb | The Ausrollen einer Saison shall keinen Neustart der CloudNet-Node oder der Bridge erfordern. | Must | +| NFR-005 | Sicherheit | If ein Spieler ohne die erforderliche Berechtigung ein Build-Server-Ziel anwählt, then shall die Lobby den Wechsel verweigern und das Ziel gar nicht erst anzeigen. | Must | +| NFR-006 | Korrektheit | The Zeitsteuerung shall Zeitzonen- und Sommerzeitwechsel für `Europe/Berlin` korrekt behandeln, ohne dass ein Neustart nötig wird. | Must | +| NFR-007 | Testbarkeit | The Zeitsteuerung shall über eine injizierte `Clock` testbar sein, sodass Tests kein Warten auf reale Zeit benötigen. | Must | +| NFR-008 | Performance | While die Lobby läuft, shall das Setzen der Tageszeit keine spürbare Tick-Verzögerung verursachen (Aktualisierung höchstens einmal pro Sekunde, nicht pro Tick). | Should | +| NFR-009 | Wartbarkeit | The Anzahl der Konstanten in `TitanFeatures` shall zwölf nicht überschreiten; der Build shall bei Überschreitung fehlschlagen. | Should | +| NFR-010 | Wartbarkeit | If ein Saison-Paket sein hinterlegtes Enddatum um mehr als zwölf Monate überschritten hat, then shall der Build fehlschlagen. | Should | +| NFR-011 | Dokumentation | The Rollout-Stand jedes Features shall in einem versionierten Dokument nachvollziehbar sein (Stufe, Datum, Verantwortlicher). | Must | +| NFR-012 | Lizenz | The Lobby shall keine Abhängigkeit einbinden, deren Lizenz mit der veröffentlichten Lizenz von Titan unvereinbar ist. | Must | +| NFR-013 | Betrieb | When ein Saison-Zeitfenster endet, shall der Übergang gestaffelt erfolgen, sodass nicht alle Spieler gleichzeitig transferiert werden. | Should | + +--- + +## 6. User Stories + +### Stufe 1 — Fundament + +| ID | Story | Akzeptanzkriterium (EARS) | Schnittstelle | Priorität | Status | +|---|---|---|---|---|---| +| US-1.01 | Als Betreiber möchte ich Welten über Falco laden, damit wir unsere eigene Engine nutzen und Ladefehler nicht als „Chunk fehlt" durchgehen. | When eine Instanz erzeugt wird, shall die Lobby einen `FalcoAnvilLoader` als `ChunkLoader` setzen statt Minestoms `AnvilLoader`. | `net.onelitefeather.falco.anvil.FalcoAnvilLoader(Path, Key)` | Must | offen | +| US-1.02 | Als Betreiber möchte ich, dass ein Lesefehler den Chunk nicht stillschweigend neu generiert, damit gebaute Welten nicht überschrieben werden. | If ein Chunk nicht gelesen werden kann, then shall der Ladevorgang eine Ausnahme werfen und der Chunk shall nicht neu generiert werden. | `AnvilFault`, `ChunkDataException` | Must | offen | +| US-1.03 | Als Betreiber möchte ich Licht über `falco-light` steuern, damit die Lobby vollständig ausgeleuchtet ist und die Tageszeit später korrekt wirkt. | When ein Chunk geladen wird, shall die Lobby dessen Licht über `ChunkLightService` berechnen. | `ChunkLightService`, `ChunkLightScheduler` | Must | offen | +| US-1.04 | Als Betreiber möchte ich bei falsch gesetzter Welt-Property eine verständliche Meldung, damit ein Tippfehler kein Rätsel ist. | If die über `TITAN_LOBBY_MAP` benannte Welt nicht existiert, then shall die Lobby den gesuchten Namen und die gefundenen Welten protokollieren und mit der Standardwelt starten. | `MapPool.peekMap()` | Must | offen | +| US-1.05 | Als Entwickler möchte ich, dass die Welt-Property immer gilt, damit sich lokal und in Produktion nichts unterschiedlich verhält. | The Welt-Auswahl shall die Property unabhängig von der Anzahl vorhandener Welten auswerten. | `MapPool.peekMap()` | Must | offen | +| US-1.06 | Als Betreiber möchte ich Welten pro Saison als eigenes Verzeichnis ablegen, damit der Wechsel ohne Codeänderung möglich ist. | The Lobby shall die aktive Welt aus einem Verzeichnis unter `worlds/` laden, dessen Name konfigurierbar ist. | `worlds//` | Must | offen | + +### Stufe 2 — Jahreszeiten und Echtzeit-Tageszeit + +**Entwurfsentscheidung: beide Abbildungen sind Strategien.** Sowohl die +Uhrzeit-Abbildung als auch die Jahreszeitgrenzen werden als +Strategy-Pattern gebaut. Das erlaubt, Variante A und B gegeneinander zu testen +und B später nachzuliefern, ohne den Aufrufcode anzufassen. Details in +Abschnitt 6a. + +| ID | Story | Akzeptanzkriterium (EARS) | Schnittstelle | Priorität | Status | +|---|---|---|---|---|---| +| US-2.01 | Als Spieler möchte ich, dass die Lobby-Tageszeit meiner echten Tageszeit entspricht, damit sich die Welt lebendig anfühlt. | While die Lobby läuft, shall die Tageszeit der Instanz der aktuellen Uhrzeit in `Europe/Berlin` entsprechen. | `Instance#setTime`, `setTimeRate(0)` | Must | offen | +| US-2.02 | Als Entwickler möchte ich, dass der eingebaute Tageszyklus abgeschaltet ist, damit unsere Zeitsteuerung nicht gegen Minestom arbeitet. | The Lobby shall die Zeitrate der Instanz auf 0 setzen und die Zeit ausschließlich selbst setzen. | `Instance#setTimeRate` | Must | offen | +| US-2.03 | Als Entwickler möchte ich die Zeitquelle austauschen können, damit Tests deterministisch sind. | The Zeitsteuerung shall ihre Zeit aus einer injizierten `java.time.Clock` beziehen und nicht aus `Instant.now()`. | `java.time.Clock` | Must | offen | +| US-2.04 | Als Betreiber möchte ich, dass Sommerzeit korrekt behandelt wird, damit im Oktober nichts um eine Stunde verrutscht. | When die Sommerzeitumstellung in `Europe/Berlin` stattfindet, shall die Lobby-Tageszeit ohne Neustart korrekt weiterlaufen. | `ZoneId.of("Europe/Berlin")` | Must | offen | +| US-2.05 | Als Entwickler möchte ich die Uhrzeit-Abbildung austauschen können, damit wir lineare und astronomische Variante vergleichen können, ohne den Aufrufcode zu ändern. | The Zeitsteuerung shall die Abbildung von Realzeit auf Spielzeit über eine austauschbare Strategie beziehen. | `DayTimeStrategy` | Must | offen | +| US-2.06 | Als Betreiber möchte ich die lineare Abbildung als Standard, damit die Stufe ohne astronomische Berechnung nutzbar ist. | The Lobby shall ohne abweichende Konfiguration die lineare Abbildung verwenden. | `LinearDayTimeStrategy` | Must | offen | +| US-2.07 | Als Betreiber möchte ich später auf die astronomische Abbildung wechseln können, damit im Dezember spät hell wird. | Where die astronomische Strategie konfiguriert ist, shall die Lobby Sonnenauf- und -untergang für Berlin auf die Spielzeit abbilden. | `SolarDayTimeStrategy` | Could | offen | +| US-2.08 | Als Entwickler möchte ich beide Abbildungen gegen dieselben Testfälle prüfen, damit der Vergleich belastbar ist. | The Testsuite shall beide Strategien gegen denselben Satz fester Zeitpunkte prüfen. | Testfall je Strategie | Should | offen | +| US-2.09 | Als Spieler möchte ich, dass die Lobby die aktuelle Jahreszeit widerspiegelt, damit sie sich über das Jahr verändert. | The Lobby shall die aktuelle Jahreszeit aus dem Datum in `Europe/Berlin` ableiten und als Zustand bereitstellen. | `Season`-Enum | Must | offen | +| US-2.10 | Als Entwickler möchte ich die Jahreszeitgrenzen austauschen können, damit wir meteorologische und astronomische Grenzen vergleichen können. | The Jahreszeit-Ermittlung shall über eine austauschbare Strategie erfolgen. | `SeasonBoundaryStrategy` | Must | offen | +| US-2.11 | Als Betreiber möchte ich meteorologische Grenzen als Standard, weil sie auf feste Monatsanfänge fallen und keine Berechnung brauchen. | The Lobby shall ohne abweichende Konfiguration meteorologische Jahreszeitgrenzen verwenden (1.3., 1.6., 1.9., 1.12.). | `MeteorologicalSeasonStrategy` | Must | offen | +| US-2.12 | Als Betreiber möchte ich auf astronomische Grenzen wechseln können, damit die Jahreszeit zu den Sonnenwenden passt. | Where die astronomische Strategie konfiguriert ist, shall die Lobby die Jahreszeit anhand von Tagundnachtgleichen und Sonnenwenden bestimmen. | `AstronomicalSeasonStrategy` | Could | offen | +| US-2.13 | Als Betreiber möchte ich eine Jahreszeit fest vorgeben können, damit ein Event unabhängig vom Kalender laufen kann. | Where eine Jahreszeit fest konfiguriert ist, shall die Lobby diese verwenden und keine Strategie befragen. | `FixedSeasonStrategy` | Should | offen | +| US-2.14 | Als Betreiber möchte ich, dass die Zeitaktualisierung günstig ist, damit sie den Tick nicht belastet. | The Zeitsteuerung shall die Tageszeit höchstens einmal pro Sekunde aktualisieren. | Scheduler | Should | offen | + +### Stufe 3 — Freigabe-Stufen + +| ID | Story | Akzeptanzkriterium (EARS) | Schnittstelle | Priorität | Status | +|---|---|---|---|---|---| +| US-3.01 | Als Entwickler möchte ich ein Feature zuerst nur intern sehen, damit wir es prüfen können, bevor es jemand anders sieht. | Where ein Feature auf Stufe „intern" steht, shall die Lobby es ausschließlich Spielern mit der Berechtigung `titan.feature.internal` zeigen. | `FeatureGate`, LuckPerms | Must | offen | +| US-3.02 | Als Betreiber möchte ich ein Feature auf Lite-Spieler ausweiten, damit wir es unter Last prüfen und Lite einen Vorteil hat. | Where ein Feature auf Stufe „lite" steht, shall die Lobby es Spielern der LuckPerms-Gruppe `lite` **und** Spielern mit `titan.feature.internal` zeigen. | `FeatureGate`, LuckPerms-Gruppe `lite` | Must | offen | +| US-3.03 | Als Betreiber möchte ich ein Feature allgemein freigeben, damit alle es sehen. | Where ein Feature auf Stufe „ga" steht, shall die Lobby es allen Spielern zeigen. | `FeatureGate` | Must | offen | +| US-3.04 | Als Betreiber möchte ich ein Feature sofort abschalten können, damit ein Fehler nicht bis zum nächsten Deployment sichtbar bleibt. | If der Notausschalter eines Features gesetzt ist, then shall die Lobby es unabhängig von Stufe und Zeitfenster niemandem zeigen. | Togglz-Flag | Must | offen | +| US-3.05 | Als Betreiber möchte ich, dass die Abschaltung ohne Neustart wirkt, damit die Reaktionszeit kurz ist. | When die Flag-Datei geändert wird, shall die Änderung innerhalb von zwei Sekunden wirksam sein. | `FileBasedStateRepository` | Must | offen | +| US-3.06 | Als Betreiber möchte ich Freigaben zeitlich planen, damit ein Event ohne Nachtschicht startet. | Where für ein Feature ein Zeitfenster konfiguriert ist, shall die Lobby es nur innerhalb dieses Fensters aktivieren. | eigene `ActivationStrategy` | Must | offen | +| US-3.07 | Als Entwickler möchte ich, dass die Prüfreihenfolge festgelegt ist, damit das Verhalten vorhersagbar bleibt. | The Freigabeprüfung shall in dieser Reihenfolge auswerten: Notausschalter, dann Berechtigungsstufe, dann Zeitfenster. | `FeatureGate` | Must | offen | +| US-3.08 | Als Betreiber möchte ich den aktuellen Stand im Spiel abfragen, damit ich nicht ins Log schauen muss. | When ein berechtigtes Teammitglied `/season status` ausführt, shall die Lobby je Feature Stufe, Zeitfenster und Notausschalter-Zustand ausgeben. | Command | Should | offen | +| US-3.09 | Als Betreiber möchte ich jeden Stufenwechsel dokumentiert haben, damit nachvollziehbar ist, wann was freigegeben wurde. | When ein Feature die Stufe wechselt, shall der Wechsel mit Zeitpunkt, alter und neuer Stufe protokolliert werden. | Log + `docs/rollout-log.md` | Must | offen | + +### Stufe 4 — Saison-Pakete + +| ID | Story | Akzeptanzkriterium (EARS) | Schnittstelle | Priorität | Status | +|---|---|---|---|---|---| +| US-4.01 | Als Betreiber möchte ich eine Saison als eigenes Paket ausliefern, damit sie ohne Kernänderung kommt und geht. | The Lobby shall Saison-Inhalte aus einem separat deploybaren Paket laden. | `SeasonalContent`-Contract | Should | offen | +| US-4.02 | Als Entwickler möchte ich, dass ein Paket sich vollständig zurückbaut, damit nach Saisonende keine Reste bleiben. | When ein Saison-Paket deaktiviert wird, shall es alle von ihm gesetzten Blöcke, Anzeigen und geplanten Aufgaben entfernen. | `SeasonalContent#deactivate` | Must | offen | +| US-4.03 | Als Betreiber möchte ich saisonale Werte ohne Neubau ändern, damit Textänderungen kein Deployment brauchen. | The Saison-Inhalte shall Materialien, Texte, Positionen und Zeitfenster aus einer Konfigurationsdatei beziehen. | JSON im Paket | Should | offen | +| US-4.04 | Als Entwickler möchte ich, dass ein unbekannter Effekt-Typ beim Übersetzen auffällt, nicht im Betrieb. | If eine Saison-Konfiguration einen unbekannten Effekt-Typ enthält, then shall das Laden mit einer benannten Fehlermeldung fehlschlagen. | `sealed interface SeasonEffect` | Should | offen | +| US-4.05 | Als Betreiber möchte ich bei zwei gleichzeitigen Paketen eine feste Reihenfolge, damit das Ergebnis nicht von der Ladereihenfolge abhängt. | Where mehrere Saison-Pakete gleichzeitig aktiv sind, shall die Lobby sie nach einem im Paket hinterlegten Prioritätswert anwenden. | Paket-Manifest | Should | offen | +| US-4.06 | Als Betreiber möchte ich, dass ein Paket nicht ein anderes voraussetzt, damit Deployment-Reihenfolgen egal sind. | The Saison-Pakete shall einander nicht direkt referenzieren. | ArchUnit-Regel | Must | offen | +| US-4.07 | Als Betreiber möchte ich vor dem Livegang sehen, wie es aussieht, ohne dass Spieler es sehen. | Where ein Spieler die Berechtigung `titan.season.preview` hat, shall die Lobby ihm Saison-Inhalte auch außerhalb des Zeitfensters zeigen. | `FeatureGate` | Should | offen | +| US-4.08 | Als Betreiber möchte ich beim Reaktivieren einer alten Saison Gewissheit, dass sie noch funktioniert. | Before eine Saison erneut aktiviert wird, shall ein Testlauf ihrer Kernpfade erfolgreich durchlaufen sein. | Testfall je Paket | Should | offen | + +### Stufe 5 — Build-Server im Navigator + +| ID | Story | Akzeptanzkriterium (EARS) | Schnittstelle | Priorität | Status | +|---|---|---|---|---|---| +| US-5.01 | Als Teammitglied möchte ich die Build-Server im Navigator sehen, damit ich ohne Befehl dorthin komme. | Where ein Spieler die Berechtigung `titan.navigator.buildserver` hat, shall der Navigator die verfügbaren Build-Server als Ziele anzeigen. | `NavigationHelper` | Should | offen | +| US-5.02 | Als Betreiber möchte ich, dass Spieler ohne Berechtigung diese Ziele gar nicht sehen, damit ihre Existenz nicht verrät, dass es sie gibt. | If ein Spieler die Berechtigung nicht hat, then shall der Navigator die Build-Server-Einträge weder anzeigen noch ihren Platz freihalten. | `NavigationHelper` | Must | offen | +| US-5.03 | Als Betreiber möchte ich, dass die Berechtigung auch beim Wechsel geprüft wird, damit ein manipulierter Klick nichts bewirkt. | When ein Wechsel zu einem Build-Server angefordert wird, shall die Lobby die Berechtigung erneut prüfen, bevor sie den Spieler weiterleitet. | `Deliver` | Must | offen | +| US-5.04 | Als Teammitglied möchte ich sehen, welche Build-Server gerade laufen, damit ich nicht auf einen gestoppten klicke. | The Navigator shall nur Build-Server anzeigen, die zum Zeitpunkt des Öffnens als erreichbar gemeldet sind. | CloudNet-Dienstliste | Should | offen | + +### Stufe 6 — Resource Packs (später) + +Bewusst grob gehalten. Ausspezifizierung erst, wenn Stufe 1–4 stehen. + +| ID | Story | Akzeptanzkriterium (EARS) | Schnittstelle | Priorität | Status | +|---|---|---|---|---|---| +| US-6.01 | Als Spieler möchte ich saisonale Texturen sehen, ohne bei jedem Serverwechsel neu zu laden. | The Lobby shall ein Basis-Paket und ein Saison-Paket mit getrennten Kennungen ausliefern. | `resource_pack_push` | Could | offen | +| US-6.02 | Als Betreiber möchte ich beim Saisonwechsel nur das Saison-Paket tauschen. | When die Saison wechselt, shall die Lobby ausschließlich das Saison-Paket entfernen und ersetzen, nicht alle Pakete. | `resource_pack_pop(uuid)` | Could | offen | +| US-6.03 | Als Betreiber möchte ich, dass ein hängender Client den Verbindungsaufbau nicht blockiert. | If ein Client nicht innerhalb einer konfigurierten Frist auf die Paketanfrage antwortet, then shall die Lobby fortfahren statt zu warten. | eigener Timeout | Could | offen | +| US-6.04 | Als Betreiber möchte ich Bedrock-Spieler korrekt behandeln, weil deren gemeldeter Status nicht zutrifft. | If ein Spieler über Geyser verbunden ist, then shall die Lobby ihn nicht anhand des gemeldeten Paketstatus bewerten. | Bedrock-Erkennung | Could | offen | + +### Stufe 7 — Portale (optional) + +| ID | Story | Akzeptanzkriterium (EARS) | Schnittstelle | Priorität | Status | +|---|---|---|---|---|---| +| US-7.01 | Als Spieler möchte ich durch ein Portal auf einen anderen Server wechseln, statt einen Navigator zu öffnen. | When ein Spieler einen als Portal definierten Bereich betritt, shall die Lobby ihn an den hinterlegten Zielserver weiterleiten. | `Deliver`, Bereichsprüfung | Could | offen | +| US-7.02 | Als Betreiber möchte ich Portale ohne Codeänderung definieren. | The Portale shall aus einer Konfigurationsdatei mit Bereich und Zielserver geladen werden. | JSON | Could | offen | +| US-7.03 | Als Betreiber möchte ich, dass ein Portal mit unerreichbarem Ziel den Spieler nicht ins Leere schickt. | If der Zielserver eines Portals nicht erreichbar ist, then shall die Lobby den Spieler an Ort und Stelle lassen und ihm eine Meldung anzeigen. | `Deliver` | Could | offen | +| US-7.04 | Als Betreiber möchte ich, dass Portale denselben Berechtigungsregeln folgen wie der Navigator. | Where ein Portal ein berechtigungspflichtiges Ziel hat, shall dieselbe Prüfung gelten wie für das entsprechende Navigator-Ziel. | `FeatureGate` | Could | offen | + +--- + +## 7. Offene Fragen / Risiken + +| Frage/Risiko | Auswirkung | Verantwortlich | Status | +|---|---|---|---| +| ~~Lizenzkonflikt Falco ↔ Titan~~ — **entschieden am 21.08.2026: Titan wechselt auf AGPL-3.0.** `LICENSE`, `header.java`, alle 97 getrackten Java-Header sowie die POM-Angaben in `app`, `setup` und `bridge` sind umgestellt. | Erledigt. Der Blocker für Stufe 1 ist damit weg. | @TheMeinerLP | **erledigt** | +| **Zustimmung der Mitautoren zum Lizenzwechsel.** Titan hat neben @TheMeinerLP weitere Urheber: theEvilReaper (~70 Commits), Joltras (~18), Yannick Lamprecht (2), dazu OLF-Organisationsaccounts. Ein Wechsel von Apache-2.0 auf AGPL-3.0 ändert die Bedingungen, unter denen deren Beiträge weitergegeben werden. | Ohne dokumentierte Zustimmung bleibt der Wechsel rechtlich angreifbar. Praktisch lösbar durch eine kurze schriftliche Bestätigung der Mitautoren, im Repository abgelegt. | @TheMeinerLP | **offen** | +| Falco ist als `@ApiStatus.Experimental` markiert; Signaturen können sich in einer Minor-Version ändern. | Umstellungsaufwand bei Falco-Updates | @TheMeinerLP | offen | +| `falco-instance` kann kein `SharedInstance` tragen. | Falls die Lobby mehrere Instanzen auf denselben Chunks braucht, entfällt dieses Modul (`falco-anvil` und `falco-light` sind davon nicht betroffen). | @TheMeinerLP | offen | +| Zeitpunkt der Extension-Umstellung aus dem OLF-Standard. | Fällt sie in Oktober/November, kollidiert sie mit dem Event — siehe `event-modi-plan.md`, Abschnitt 3a | @TheMeinerLP | offen | + +--- + +## 6a. Entwurf: zwei Strategien für Zeit und Jahreszeit + +**Entschieden:** Uhrzeit-Abbildung und Jahreszeitgrenzen werden beide als +Strategy-Pattern gebaut. Der Aufrufcode kennt nur die Schnittstelle; welche +Ausprägung läuft, entscheidet die Konfiguration. Damit lassen sich beide Varianten +gegeneinander testen, und die aufwendigere kommt später — ohne Umbau. + +### Tageszeit + +```java +/** + * Bildet einen Zeitpunkt der Realwelt auf die Tageszeit einer Minecraft-Welt ab. + * + * @author TheMeinerLP + * @version 1.0.0 + * @since 1.11.0 + */ +public interface DayTimeStrategy { + + /** Ein voller Minecraft-Tag in Ticks. */ + int TICKS_PER_DAY = 24_000; + + /** + * @param instant der Zeitpunkt, für den die Tageszeit gilt + * @param zone die Zeitzone, gegen die gerechnet wird + * @return die Tageszeit in Ticks, im Bereich [0, {@value #TICKS_PER_DAY}) + */ + @Contract(pure = true) + long ticksAt(Instant instant, ZoneId zone); +} +``` + +| Ausprägung | Verhalten | Stufe | +|---|---|---| +| `LinearDayTimeStrategy` | 24 reale Stunden gleichmäßig auf 24 000 Ticks; 12:00 Uhr ergibt Mittag | **Standard**, Stufe 2 | +| `SolarDayTimeStrategy` | Sonnenauf- und -untergang für Berlin auf die Spielzeit gelegt; im Dezember spät hell | Could, später | + +Die lineare Variante ist bewusst der Standard: Sie liefert den Nutzen fast +vollständig und hat keinen Berechnungsfehler, den man übersehen könnte. Die +astronomische ist die Verfeinerung — in Berlin schwankt der Sonnenaufgang +zwischen etwa 4:45 Uhr im Juni und 8:15 Uhr im Dezember, und genau das macht sie +sichtbar. + +**Was der Vergleich prüfen muss** (US-2.08): Beide Ausprägungen werden gegen +denselben Satz fester Zeitpunkte getestet — Sonnenwenden, Tagundnachtgleichen, +beide Sommerzeitumstellungen und ein gewöhnlicher Tag. Da die Zeitquelle eine +injizierte `Clock` ist, braucht kein Test reale Zeit. + +### Jahreszeit + +```java +/** + * Bestimmt, welche Jahreszeit an einem Datum gilt. + * + * @author TheMeinerLP + * @version 1.0.0 + * @since 1.11.0 + */ +public interface SeasonBoundaryStrategy { + + /** + * @param date das Datum in der Redaktionszeitzone + * @return die an diesem Tag geltende Jahreszeit + */ + @Contract(pure = true) + Season seasonAt(LocalDate date); +} +``` + +| Ausprägung | Grenzen | Stufe | +|---|---|---| +| `MeteorologicalSeasonStrategy` | feste Monatsanfänge: 1.3., 1.6., 1.9., 1.12. | **Standard**, Stufe 2 | +| `AstronomicalSeasonStrategy` | Tagundnachtgleichen und Sonnenwenden (um den 20./21.) | Could, später | +| `FixedSeasonStrategy` | gibt immer dieselbe Jahreszeit zurück | Should — Tests, Vorschau, Events außerhalb des Kalenders | + +Meteorologisch ist der Standard, weil die Grenzen auf feste Kalendertage fallen +und keine Berechnung brauchen. Der Unterschied zur astronomischen Variante +beträgt rund drei Wochen — spürbar, aber kein Grund, die Berechnung in Stufe 2 zu +ziehen. + +`FixedSeasonStrategy` ist nicht nur ein Testhilfsmittel: Sie ist der Weg, ein +Winter-Event im August vorzuführen, ohne an der Systemuhr zu drehen. + +### Warum Strategy und nicht Konfigurationsschalter + +Ein `if (astronomisch) … else …` an der Abbildungsstelle hätte denselben Effekt +und wäre kürzer. Drei Gründe sprechen dagegen: + +1. **Der Vergleich ist der Zweck.** Beide Varianten sollen gegeneinander getestet + werden. Als eigene Typen sind sie einzeln instanziierbar und einzeln testbar; + als Zweig einer Bedingung nicht. +2. **Die astronomische Variante bringt eigene Abhängigkeiten mit** (Sonnenstand, + geografische Position). Die gehören in ihre Klasse, nicht in den gemeinsamen + Pfad. +3. **`FixedSeasonStrategy` fällt gratis ab.** Bei einer Bedingung wäre die + Vorschaufunktion ein dritter Zweig; als Strategie ist sie eine zehnzeilige + Klasse. + +Beide Schnittstellen sind bewusst zustandslos und `@Contract(pure = true)` — sie +bekommen den Zeitpunkt übergeben, statt selbst auf die Uhr zu sehen. Die +`Clock` sitzt in der aufrufenden Zeitsteuerung (US-2.03), nicht in den Strategien. + +--- + +## 8. Abnahmekriterien + +- [x] Die Lizenzfrage Falco ↔ Titan ist entschieden: Titan steht unter AGPL-3.0 (21.08.2026). +- [ ] Die Zustimmung der Mitautoren zum Lizenzwechsel liegt schriftlich vor und ist im Repository abgelegt. +- [ ] Die Lobby lädt Welten über `FalcoAnvilLoader`; Minestoms `AnvilLoader` wird nicht mehr verwendet. +- [ ] Ein Lesefehler an einem Chunk führt zu einer Ausnahme, nicht zu einem neu generierten Chunk. +- [ ] Ein falsch gesetztes `TITAN_LOBBY_MAP` startet die Lobby mit der Standardwelt und protokolliert den gesuchten Namen. +- [ ] Die Tageszeit der Lobby entspricht der Uhrzeit in Berlin, auch über eine Sommerzeitumstellung hinweg. +- [ ] Die Zeitsteuerung ist mit einer festen `Clock` testbar; ein Test prüft Winter im Sommer. +- [ ] Ein Feature lässt sich nacheinander auf intern, lite und ga stellen, ohne dass Code geändert wird. +- [ ] Der Notausschalter wirkt innerhalb von zwei Sekunden und schlägt Stufe und Zeitfenster. +- [ ] Ein Spieler ohne `titan.navigator.buildserver` sieht die Build-Server nicht und kann sie auch durch einen manipulierten Klick nicht erreichen. +- [ ] Die Lobby startet ohne Saison-Paket vollständig funktionsfähig. +- [ ] Ein Saison-Paket lässt sich entfernen, ohne dass Reste in der Welt zurückbleiben. +- [ ] Der Rollout-Stand jedes Features ist in `docs/rollout-log.md` nachvollziehbar. + +--- + +## 9. Was diese Spec bewusst offen lässt + +Damit niemand diese Punkte für vergessen hält: + +- **Der Inhalt der Saisons.** Diese Spec beschreibt das Gerüst, nicht das + Halloween-Event selbst. Was Spieler tun können, ist ein eigenes Konzept — und + laut Research der Teil, der tatsächlich Wirkung hat. +- **Das Übergabeformat vom Buildteam.** Die Empfehlung aus dem Research (eine + `config.yml` im Weltordner, wie BlueDragonMC es macht) ist noch nicht als + Anforderung formuliert, weil das Buildteam dazu gehört werden sollte. +- **Cosmetics und Belohnungen.** Ausdrücklich Nicht-Ziel. +- **Die Frage, ob `titan-api` publiziert wird.** Betrifft den + `SeasonalContent`-Contract aus Stufe 4; siehe + [`olf-minestom-project-standard.md`](olf-minestom-project-standard.md).