diff --git a/README.md b/README.md index 186600b..47cf791 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,557 @@ -
+# 🎄 Adventskalender – Event-Kalender & Geburtstage für Minecraft -# ❄️ Minecraft Adventskalender ❄️ +Ein Plugin für deinen Minecraft-Server, das Spielern jeden Tag ein Geschenk gibt. +Es hat **drei Kalender** und ein **Geburtstags-Geschenk**: -*Bringe festliche Stimmung auf deinen Server mit einem hochgradig anpassbaren System* +| Was | Wann | Türchen | +|---|---|---| +| 🎄 **Adventskalender** | ab 1. Dezember | 24 | +| 🎃 **Halloween-Kalender** | ab 1. Oktober | 31 | +| 🐰 **Oster-Kalender** | 13 Tage vor Ostersonntag bis Ostersonntag | 14 | +| 🎂 **Geburtstags-Geschenk** | am Geburtstag des Spielers + 2 Tage danach | 1 pro Jahr | -![Version](https://img.shields.io/badge/Version-1.18.x--1.21.x-green.svg) -![Java](https://img.shields.io/badge/Java-17+-orange.svg) -![Update Checker](https://img.shields.io/badge/Update--Checker-Klickbar-blue.svg) -![Type](https://img.shields.io/badge/Type-Event-red.svg) - -
+Du musst **nichts programmieren**. Plugin hineinkopieren, Server starten, fertig. +Alles Weitere (Belohnungen, Texte, Zeiten) kannst du in normalen Textdateien ändern. --- -## Über Minecraft Adventskalender +## Inhaltsverzeichnis -Das **Minecraft Adventskalender Plugin** ist die ultimative Lösung für deinen Server im Dezember! Es bietet volle Kontrolle über Belohnungen, Sprache und Funktionen. Dank des neuen klickbaren Update-Checkers und dem erweiterten Admin-Test-System war die Verwaltung eines Events noch nie so einfach. Perfekt für Community-Events, um die Spielerbindung in der Weihnachtszeit zu stärken! +1. [Was brauche ich?](#1-was-brauche-ich) +2. [Installation in 5 Schritten](#2-installation-in-5-schritten) +3. [So benutzen Spieler das Plugin](#3-so-benutzen-spieler-das-plugin) +4. [Befehle für Admins](#4-befehle-für-admins) +5. [Rechte (Permissions)](#5-rechte-permissions) +6. [Die Dateien des Plugins](#6-die-dateien-des-plugins) +7. [Belohnungen ändern](#7-belohnungen-ändern) +8. [Kalender ein- und ausschalten oder anpassen](#8-kalender-ein--und-ausschalten-oder-anpassen) +9. [Geburtstage einstellen](#9-geburtstage-einstellen) +10. [Texte und Sprache ändern](#10-texte-und-sprache-ändern) +11. [Alles testen, ohne auf den Dezember zu warten](#11-alles-testen-ohne-auf-den-dezember-zu-warten) +12. [Jeder darf nur einmal – oder der ganze Server nur einmal?](#12-jeder-darf-nur-einmal--oder-der-ganze-server-nur-einmal) +13. [Mehrere Server mit einer Datenbank (MySQL)](#13-mehrere-server-mit-einer-datenbank-mysql) +14. [Platzhalter für Scoreboards, Hologramme & Co.](#14-platzhalter-für-scoreboards-hologramme--co) +15. [Plugin aktualisieren](#15-plugin-aktualisieren) +16. [Probleme und ihre Lösung](#16-probleme-und-ihre-lösung) +17. [Häufige Fragen](#17-häufige-fragen) --- -## ✨ Exklusive Features +## 1. Was brauche ich? -- **Vollständig Anpassbare Belohnungen:** Definiere für jeden Tag individuelle Items mit Menge, Name, Lore und Verzauberungen. -- **Smart Update-Checker:** Erhalte beim Joinen als Admin eine dezente, **klickbare Benachrichtigung**, falls eine neue Version verfügbar ist (erscheint verzögert nach 2 Sek.). -- **Simulations Modus (Admin-Test):** Teste alle Türchen vorab durch Datums-Simulation, ohne deine Systemzeit zu ändern. Inklusive einfachem Reset auf das Echtzeit-Datum. -- **Präzises Reset-System:** Setze einzelne Tage oder komplette Fortschritte zurück – wahlweise für einzelne Spieler oder global für den ganzen Server. -- **Immersive Effekte:** Festliche Sound- und Partikeleffekte beim Öffnen eines Türchens. -- **Optimierte Tab-Completion:** Intelligente Befehlsvorschläge mit korrekter numerischer Sortierung (1, 2, 3... statt 1, 10, 11...). +- Einen Minecraft-Server mit **Spigot** oder **Paper** (oder einem Fork davon) ab **Version 1.20**. +- **Java 21** auf dem Server. Ältere Java-Versionen starten das Plugin nicht. +- Optional: das Plugin **PlaceholderAPI**, wenn du Werte des Kalenders auf Scoreboards oder Hologrammen anzeigen willst. +- Optional: eine **MySQL-Datenbank**, wenn mehrere Server (z. B. mit BungeeCord) sich die Daten teilen sollen. Für einen einzelnen Server brauchst du das **nicht**. + +> Hinweis: Das Plugin wurde für Minecraft 1.20 gebaut. Auf neueren Versionen sollte es laufen, teste es aber vor dem Einsatz auf deinem Live-Server kurz aus. --- -## 🎄 Adventskalender GUI +## 2. Installation in 5 Schritten -
+1. **Server ausschalten** (oder einfach weitermachen, wenn du später neu startest). +2. Die Datei **`adventskalender-1.0.3.jar`** in den Ordner **`plugins`** deines Servers kopieren. +3. **Server starten.** +4. Das Plugin legt automatisch den Ordner **`plugins/Adventskalender`** an. Darin liegen jetzt die Dateien `config.yml`, `messages_de.yml` und `messages_en.yml`. +5. **Fertig.** Im Spiel `/ak` eingeben und der Kalender geht auf. -Kalender GUI 1 -Kalender GUI 2 - -*Die intuitive Kalender-Oberfläche mit allen 24 Türchen* - -
+Wenn du in der Konsole die Meldung **„Adventskalender erfolgreich gestartet!“** siehst, hat alles geklappt. --- -## ⚙️ Befehle & Permissions +## 3. So benutzen Spieler das Plugin -### ▶ Spieler-Befehle +### Kalender öffnen -**`/adventskalender`** (Aliase: `/ak`, `/advent`, `/kalender`) -- **Beschreibung:** Öffnet die Kalender-Oberfläche. -- **Berechtigung:** `adventskalender.use` +| Befehl | Was passiert | +|---|---| +| `/ak` | Öffnet den Kalender, der gerade läuft. Läuft keiner, wird der Adventskalender gezeigt (mit gesperrten Türchen). | +| `/ak halloween` | Öffnet den Halloween-Kalender | +| `/ak easter` | Öffnet den Oster-Kalender | +| `/ak advent` | Öffnet den Adventskalender | -### ▶ Admin-Befehle +Statt `/ak` funktionieren auch `/adventskalender`, `/advent` und `/kalender`. -**`/ak admin test <1-24 | reset>`** -- **Beschreibung:** Simuliert einen bestimmten Tag für Belohnungstests. Nutze `reset`, um zum echten Datum zurückzukehren. -- **Berechtigung:** `adventskalender.admin` +### Türchen öffnen -**`/ak admin resetday <1-24>`** -- **Beschreibung:** Setzt den Status eines spezifischen Tages für einen Spieler oder den gesamten Server zurück. -- **Berechtigung:** `adventskalender.admin` +- Die **Köpfe (Advent) bzw. Items (Halloween, Ostern)** in der Mitte des Menüs sind die Türchen. Die rot-grünen Glasscheiben am Rand sind nur Dekoration. +- Ein Türchen kannst du nur öffnen, wenn der **Tag erreicht** ist. Der 5. Dezember schaltet z. B. Türchen 1 bis 5 frei, Türchen 6 bleibt zu. +- **Klick auf ein Türchen** = du bekommst das Geschenk. Passt es nicht ins Inventar, fällt es vor deine Füße. +- Ein geöffnetes Türchen **glitzert** und kann nicht nochmal geöffnet werden. +- Hast du einen Tag **verpasst**, ist das kein Problem: Du kannst alte Türchen jederzeit nachholen, solange der Kalender läuft. -**`/ak admin reset `** -- **Beschreibung:** Löscht den kompletten Fortschritt (alle Tage) für einen Spieler oder global. -- **Berechtigung:** `adventskalender.admin` +### Geburtstags-Geschenk -**`/ak admin reload`** -- **Beschreibung:** Lädt Config, Sprachdateien und Datenbank-Verbindung im laufenden Betrieb neu. -- **Berechtigung:** `adventskalender.admin` +| Befehl | Was passiert | +|---|---| +| `/ak birthday set 15.03` | Trägt deinen Geburtstag ein (Tag.Monat, **ohne Jahr**) | +| `/ak birthday` | Zeigt deinen eingetragenen Geburtstag | +| `/ak birthday claim` | Holt dein Geschenk ab | + +- Du kannst das Geschenk **am Geburtstag und bis zu 2 Tage danach** abholen (Familie, Freunde, Feier – du hast Zeit). +- Es gibt **ein Geschenk pro Jahr**. +- Wenn du dich an deinem Geburtstag (oder danach) einloggst, bekommst du eine **anklickbare Nachricht**. Ein Klick darauf holt das Geschenk ab. +- Dein Geburtstag kann **nur einmal** eingetragen werden. Hast du dich vertippt, muss ein Admin das ändern (siehe unten). --- -## 📊 PlaceholderAPI (PAPI) +## 4. Befehle für Admins -- **`%ak_c%`** - Anzahl bereits geöffneter Türchen. -- **`%ak_n%`** - Nummer des nächsten verfügbaren Türchens. -- **`%ak_d%`** - Verbleibende Tage bis zum 25. Dezember. +Alle Admin-Befehle beginnen mit `/ak admin`. Du brauchst dafür das Recht `adventskalender.admin` (Operatoren haben es automatisch). -### Placeholder auf einem Schild +Die Teile in `[eckigen Klammern]` sind **optional**. Lässt du `[event]` weg, gilt der Befehl für **alle** Kalender. +Gültige Namen für `[event]`: `advent`, `halloween`, `easter`. -
+| Befehl | Was er macht | +|---|---| +| `/ak admin reload` | Lädt Config und Sprachdateien neu, ohne Neustart | +| `/ak admin test [event] ` | Tut so, als wäre heute der gewählte Tag (siehe [Testen](#11-alles-testen-ohne-auf-den-dezember-zu-warten)) | +| `/ak admin test [event] reset` | Beendet den Test, das echte Datum gilt wieder | +| `/ak admin reset [event] ` | Löscht **alle** geöffneten Türchen eines Spielers | +| `/ak admin reset [event] global` | Löscht **alle** Daten aller Spieler (**Vorsicht!**) | +| `/ak admin resetday [event] ` | Macht **ein** Türchen für einen Spieler wieder zu | +| `/ak admin resetday [event] global ` | Macht ein Türchen im Server-weiten Modus wieder frei | +| `/ak admin birthday set ` | Trägt den Geburtstag eines Spielers ein oder ändert ihn | +| `/ak admin birthday remove ` | Löscht den Geburtstag eines Spielers | +| `/ak admin birthday reset ` | Erlaubt dem Spieler, das Geburtstagsgeschenk **nochmal** abzuholen (zum Testen oder bei Problemen) | -Placeholder Beispiel +**Beispiele:** -*Ein Schild, das die Placeholder des Plugins anzeigt.* +``` +/ak admin test halloween 15 (Halloween-Kalender: so tun, als wäre der 15. Tag) +/ak admin test reset (alle Tests beenden) +/ak admin resetday advent Steve 3 (Steve darf Türchen 3 im Advent nochmal öffnen) +/ak admin reset halloween Steve (Steves Halloween-Daten löschen) +/ak admin birthday set Steve 24.12 (Steves Geburtstag auf den 24.12. setzen) +``` -
+> Mit **Tab** vervollständigt Minecraft die Namen und Zahlen für dich. --- -## 💬 Support & Community +## 5. Rechte (Permissions) -Du hast Fragen oder Feedback? Tritt unserem Discord bei! +Rechte vergibst du mit einem Rechte-Plugin (z. B. LuckPerms). -
- -[![Discord Support](https://img.shields.io/badge/Discord-Support-7289DA?style=for-the-badge&logo=discord)](https://discord.com/invite/FdRs4BRd8D) - -*Klicke auf den Button für direkten Support durch die Community!* - -
+| Recht | Wofür | Standard | +|---|---|---| +| `adventskalender.use` | Kalender öffnen und benutzen | **alle Spieler** | +| `adventskalender.birthday` | Geburtstag eintragen und Geschenk abholen | **alle Spieler** | +| `adventskalender.admin` | Alle Admin-Befehle und Update-Hinweis beim Beitritt | nur Operatoren | --- -## 🔧 Technische Details +## 6. Die Dateien des Plugins -- **Kompatibilität:** Paper, Spigot, Purpur (1.18.x - 1.21.x) -- **Smart Caching:** Minimale Serverlast durch asynchrone Datenbankaufrufe. -- **Join-Delay:** Update-Meldungen erscheinen erst nach 2 Sekunden, um den Chat-Spam beim Joinen zu umgehen. +Alles liegt in `plugins/Adventskalender/`: + +| Datei | Wofür | +|---|---| +| `config.yml` | **Einstellungen und Belohnungen.** Hier änderst du, was es zu gewinnen gibt und wann die Kalender laufen. | +| `messages_de.yml` | Alle **deutschen Texte** (Titel, Nachrichten, Namen der Geschenke) | +| `messages_en.yml` | Alle **englischen Texte** | +| `data.yml` | Hier speichert das Plugin, **wer was geöffnet hat** und die Geburtstage. Diese Datei **nicht löschen**, sonst fangen alle von vorne an. | + +### So bearbeitest du die Dateien richtig + +1. Öffne die Datei mit einem **Texteditor** (Notepad++, VS Code oder der Editor deines Hosters). **Nicht** mit Word. +2. Ändere nur die Werte, **nicht** die Einrückung am Zeilenanfang. Die Leerzeichen am Anfang sind wichtig! +3. Benutze **Leerzeichen**, keine Tabulatortaste. +4. Speichern und im Spiel `/ak admin reload` eingeben. +5. Wenn der Server nach einem Fehler die Datei nicht mehr lesen kann: Zeile prüfen, die du zuletzt geändert hast (fehlendes Leerzeichen, fehlende Anführungszeichen). --- -
+## 7. Belohnungen ändern -*Viper Plugins © 2026 - Effizienz und Magie für deinen Server* +Jedes Türchen hat in der `config.yml` einen Eintrag. Beispiel Advent, Tag 3: -
+```yaml +rewards: + 3: + material: DIAMOND_SWORD + amount: 1 + enchantments: + - type: SHARPNESS + level: 4 + - type: UNBREAKING + level: 2 +``` + +**Das bedeutet:** + +- `3:` – die Nummer des Türchens +- `material:` – welches Item es gibt (siehe unten) +- `amount:` – wie viele Stück +- `enchantments:` – Verzauberungen. Möchtest du keine, schreibe `enchantments: []` + +### Wo stehen die Belohnungen der anderen Kalender? + +- **Advent:** ganz oben unter `rewards:` +- **Halloween:** weiter unten unter `events:` → `halloween:` → `rewards:` +- **Ostern:** weiter unten unter `events:` → `easter:` → `rewards:` +- **Geburtstag:** ganz unten unter `birthday:` → `rewards:` + +### Item-Namen (`material`) + +Es sind die englischen Minecraft-Namen in **GROSSBUCHSTABEN** mit Unterstrich statt Leerzeichen. +Beispiele: `DIAMOND`, `GOLDEN_APPLE`, `NETHERITE_INGOT`, `ELYTRA`, `FIREWORK_ROCKET`. +Eine Liste findest du, wenn du im Internet nach „Spigot Material list“ suchst. + +### Verzauberungen (`type`) + +Auch hier die englischen Namen: `SHARPNESS`, `UNBREAKING`, `PROTECTION`, `EFFICIENCY`, `FORTUNE`, `LOOTING`, `MENDING` usw. +`level` ist die Stufe (z. B. `5` für Schärfe V). + +### Wenn du etwas falsch schreibst + +Das Plugin gibt dann eine **Warnung in der Konsole** aus (z. B. „Ungültiges Material …“). Der Spieler bekommt die Meldung, dass das Geschenk falsch eingestellt ist, und das **Türchen bleibt zu**. Er verliert also nichts. Korrigiere den Fehler und lade mit `/ak admin reload` neu. + +### Eigener Name und eigene Beschreibung der Geschenke + +Der Name und der Beschreibungstext, die im Menü über einem Türchen stehen, kommen **nicht** aus der `config.yml`, sondern aus der Sprachdatei. Siehe [Texte und Sprache ändern](#10-texte-und-sprache-ändern). --- -**Copyright © 2026 - M_Viper - Alle Rechte vorbehalten** +## 8. Kalender ein- und ausschalten oder anpassen -Die unbefugte Vervielfältigung, Verbreitung oder Weitergabe dieses Plugins ist strafbar und wird rechtlich verfolgt. \ No newline at end of file +In der `config.yml` ganz unten gibt es den Bereich `events:`. Dort steht jeder Kalender mit seinen Einstellungen: + +```yaml +events: + halloween: + enabled: true + days: 31 + start: "10-01" + grace_days: 0 + closed_material: PUMPKIN + opened_material: JACK_O_LANTERN +``` + +| Einstellung | Bedeutung | +|---|---| +| `enabled` | `true` = Kalender an, `false` = Kalender aus (die Daten bleiben erhalten) | +| `days` | Wie viele Türchen der Kalender hat (1 bis 36) | +| `start` | Wann der Kalender beginnt. Format `Monat-Tag`, z. B. `"10-01"` für den 1. Oktober | +| `grace_days` | Wie viele Tage der Kalender nach dem letzten Türchen noch offen bleibt, damit man verpasste Türchen nachholen kann | +| `closed_material` | Item für ein **geschlossenes** Türchen | +| `opened_material` | Item für ein **geöffnetes** Türchen | + +### Zeitpunkt für Ostern + +Ostern liegt jedes Jahr woanders. Deshalb gibt es dort eine Sonderschreibweise: + +```yaml +start: "easter-13" +``` + +Das heißt: **13 Tage vor Ostersonntag**. Das Plugin rechnet das Datum **jedes Jahr automatisch** aus, du musst nie wieder etwas ändern. +Weitere Beispiele: `easter+1` = Ostermontag, `easter-2` = Karfreitag. + +### Eigene Köpfe statt Items (für Fortgeschrittene) + +Möchtest du für Halloween oder Ostern hübsche Kopf-Texturen, kannst du bei einem Kalender zusätzlich diese Zeilen einfügen: + +```yaml + closed_texture: "http://textures.minecraft.net/texture/DEINE_TEXTUR" + opened_texture: "http://textures.minecraft.net/texture/DEINE_TEXTUR" +``` + +Passende Texturen findest du auf Seiten wie *minecraft-heads.com* (dort den Wert „Minecraft-URL“ kopieren). + +### Zeitzone + +Das Plugin richtet sich nach der Uhrzeit des Servers. Wenn dein Server in einem anderen Land steht und die Türchen zu früh oder zu spät aufgehen, trage deine Zeitzone ein: + +```yaml +general: + timezone: "Europe/Berlin" +``` + +--- + +## 9. Geburtstage einstellen + +Alles steht unten in der `config.yml` im Bereich `birthday:`: + +```yaml +birthday: + enabled: true + grace_days: 2 + allow_change: false + broadcast: true + commands: [] + rewards: + 1: + material: CAKE + amount: 1 + enchantments: [] +``` + +| Einstellung | Bedeutung | +|---|---| +| `enabled` | `false` schaltet die Geburtstags-Funktion komplett ab | +| `grace_days` | Wie viele Tage **nach** dem Geburtstag das Geschenk noch abholbar ist (Standard 2) | +| `allow_change` | `false` = Spieler können ihren Geburtstag nur **einmal** eintragen, Änderungen macht nur ein Admin. `true` = Spieler dürfen selbst ändern (siehe Hinweis) | +| `broadcast` | `true` = der ganze Server sieht „Steve feiert Geburtstag – alles Gute!“, wenn das Geschenk abgeholt wird | +| `commands` | Zusätzliche Befehle, die der Server ausführt (siehe unten) | +| `rewards` | Die Geschenke. Funktioniert genau wie bei den Kalendern, du kannst beliebig viele Einträge anlegen (1, 2, 3, …) | + +> **Warum ist `allow_change` standardmäßig aus?** +> Wäre es an, könnte ein Spieler sein Datum jeden Tag umstellen und sich ständig Geschenke holen. Auch mit `true` kann er pro Jahr nur **einmal** abholen. + +### Geld oder andere Belohnungen per Befehl + +Wenn du z. B. ein Wirtschafts-Plugin hast, kannst du Geld verschenken: + +```yaml + commands: + - "eco give %player% 500" + - "give %player% minecraft:cake 1" +``` + +`%player%` wird automatisch durch den Namen des Spielers ersetzt. Ohne Schrägstrich am Anfang schreiben. + +### Wichtige Hinweise + +- Das **Jahr** wird nie gespeichert, nur Tag und Monat. Das Plugin weiß also nie, wie alt jemand ist. +- Wer am **29. Februar** Geburtstag hat, feiert in Jahren ohne Schalttag am **28. Februar**. +- Wer am **31. Dezember** Geburtstag hat, kann bis in den Januar abholen. Das funktioniert korrekt. + +--- + +## 10. Texte und Sprache ändern + +### Sprache umstellen + +In der `config.yml` ganz oben: + +```yaml +general: + language: "de" +``` + +`"de"` = Deutsch, `"en"` = Englisch. Danach `/ak admin reload`. + +### Eigene Texte + +Öffne `messages_de.yml` (oder `messages_en.yml`). Dort stehen alle Texte. Farben schreibst du mit `&`: + +| Code | Farbe | Code | Farbe | +|---|---|---|---| +| `&0` | Schwarz | `&8` | Dunkelgrau | +| `&1` | Dunkelblau | `&9` | Blau | +| `&2` | Dunkelgrün | `&a` | Grün | +| `&3` | Türkis | `&b` | Hellblau | +| `&4` | Dunkelrot | `&c` | Rot | +| `&5` | Lila | `&d` | Pink | +| `&6` | Gold | `&e` | Gelb | +| `&7` | Grau | `&f` | Weiß | + +Formatierung: `&l` = **fett**, `&o` = *kursiv*, `&n` = unterstrichen, `&r` = zurücksetzen. + +### Platzhalter in Texten + +Diese Platzhalter dürfen in den Texten stehen und werden automatisch ersetzt: + +- `%day%` – die Nummer des Tages +- `%date%` – das Datum eines gesperrten Türchens +- `%player%` – der Spielername +- `%event%` – der Name des Kalenders +- `%days%` – die Anzahl Tage (bei Geburtstagen) + +### Namen und Beschreibung der Geschenke im Menü + +Beispiel für den Adventskalender in `messages_de.yml`: + +```yaml +rewards: + day_3: + name: "&cTag 3: Schwert des Helden" + lore: ["&7Ein mächtiges Schwert, um Monster zu jagen."] +``` + +- `name` ist die Überschrift des Türchens. +- `lore` sind die Zeilen darunter. Mehrere Zeilen schreibst du so: `["Zeile 1", "Zeile 2"]`. + +Für Halloween und Ostern stehen die Texte weiter unten in der Datei unter `events:` → `halloween:` bzw. `easter:`. +Wenn du ein Geschenk in der `config.yml` änderst, solltest du hier den Namen anpassen, sonst steht „Schwert des Helden“, obwohl es jetzt Diamanten gibt. + +> Wenn du neue Texte vermisst („Key error: …“ im Spiel), siehe [Plugin aktualisieren](#15-plugin-aktualisieren). + +--- + +## 11. Alles testen, ohne auf den Dezember zu warten + +Du musst nicht bis zum 1. Dezember warten, um alles auszuprobieren: + +1. `/ak admin test advent 10` – der Adventskalender denkt jetzt, es sei der 10. Dezember. +2. `/ak advent` – öffne ihn und probiere die Türchen 1 bis 10 aus. +3. Zum Zurücksetzen deiner Test-Türchen: `/ak admin reset advent DEINNAME` +4. Test beenden: `/ak admin test reset` + +Genauso für `halloween` und `easter`. Den Test-Tag **vergisst** das Plugin beim nächsten Neustart. Du kannst also nichts „vergessen auszuschalten“. + +**Geburtstag testen:** + +1. `/ak admin birthday set DEINNAME TT.MM` – statt TT.MM das **heutige** Datum eintragen (z. B. `24.12`) +2. `/ak birthday claim` – du bekommst das Geschenk +3. Nochmal testen: `/ak admin birthday reset DEINNAME` + +--- + +## 12. Jeder darf nur einmal – oder der ganze Server nur einmal? + +Es gibt zwei Betriebsarten: + +- **Normal (Standard):** Jeder Spieler hat seinen **eigenen** Kalender. Jeder darf jedes Türchen einmal öffnen. +- **Global:** Jedes Türchen darf **nur ein einziger Spieler** auf dem ganzen Server öffnen. Wer zuerst klickt, gewinnt. + +Umstellen in der `config.yml`: + +```yaml +calendar: + use_global_calendar: false # true = Global-Modus +``` + +Du kannst das auch nur für **einen** Kalender ändern, indem du bei diesem im Bereich `events:` die Zeile `global: true` oder `global: false` einfügst. + +--- + +## 13. Mehrere Server mit einer Datenbank (MySQL) + +Nur nötig, wenn du z. B. ein BungeeCord-Netzwerk hast und alle Server dieselben Daten benutzen sollen. Für **einen** Server einfach ignorieren. + +In der `config.yml`: + +```yaml +mysql: + enable: true + host: "localhost" + port: 3306 + database: "adventskalender" + user: "root" + password: "DEIN_PASSWORT" +``` + +- Die Datenbank (`adventskalender`) musst du vorher selbst anlegen. Die Tabellen darin erstellt das Plugin allein. +- Danach den Server **neu starten** (oder `/ak admin reload`). +- In der Konsole muss stehen: **„MySQL-Verbindung erfolgreich hergestellt!“** +- Klappt es nicht, benutzt das Plugin automatisch die `data.yml` als Notlösung und schreibt eine Warnung in die Konsole. +- Sollte die Datenbank später kurz ausfallen, bekommen Spieler eine Meldung und es wird **nichts** vergeben. Sobald die Datenbank wieder da ist, geht alles weiter. +- Alle Kalender und Geburtstage werden in der Datenbank gespeichert. Daten aus einer älteren Version werden dabei automatisch übernommen. + +> **Wichtig:** Daten aus der `data.yml` werden **nicht automatisch** in die Datenbank kopiert. Schaltest du MySQL ein, fangen die Spieler dort bei null an. + +--- + +## 14. Platzhalter für Scoreboards, Hologramme & Co. + +Wenn das Plugin **PlaceholderAPI** installiert ist, kannst du diese Werte überall anzeigen, wo PlaceholderAPI funktioniert: + +### Für das gerade laufende Kalender-Event + +| Platzhalter | Zeigt | +|---|---| +| `%ak_event%` | Name des laufenden Kalenders (`advent`, `halloween`, `easter`) | +| `%ak_day%` | Welcher Tag des Kalenders heute ist (0 = Kalender läuft nicht) | +| `%ak_claimed%` | Wie viele Türchen der Spieler schon geöffnet hat | +| `%ak_remaining%` | Wie viele Türchen noch offen sind | +| `%ak_has_claimed_today%` | „Ja“ oder „Nein“: Hat der Spieler das heutige Türchen schon geöffnet? | +| `%ak_is_global%` | „Global“ oder „Spieler-basiert“ | + +### Für einen bestimmten Kalender + +Schreibe den Namen des Kalenders vor den Platzhalter: + +- `%ak_halloween_claimed%` +- `%ak_easter_day%` +- `%ak_advent_remaining%` + +### Geburtstag + +| Platzhalter | Zeigt | +|---|---| +| `%ak_birthday%` | Eingetragenes Datum (z. B. `15.03.`) oder `-` | +| `%ak_birthday_active%` | „Ja“, wenn der Spieler gerade sein Geschenk abholen kann, sonst „Nein“ | + +--- + +## 15. Plugin aktualisieren + +1. Server **ausschalten**. +2. Die **alte `.jar`-Datei** aus dem `plugins`-Ordner löschen. +3. Die **neue `.jar`-Datei** hineinkopieren. +4. Server starten. + +Deine **Daten** (`data.yml`) und deine **Einstellungen** bleiben erhalten. + +### Neue Einstellungen sichtbar machen + +Deine alte `config.yml` und die Sprachdateien werden bei einem Update **nicht überschrieben**. Neue Funktionen laufen trotzdem mit den Standardwerten, du siehst die neuen Einstellungen aber nicht in der Datei. Willst du sie sehen und ändern: + +1. Server ausschalten. +2. `config.yml` umbenennen, z. B. in `config_alt.yml`. (Dasselbe mit den `messages_...yml`-Dateien, falls nötig.) +3. Server starten. Das Plugin erstellt eine frische, vollständige Datei. +4. Deine Änderungen aus `config_alt.yml` in die neue Datei übertragen. + +Die `data.yml` bleibt dabei **immer unangetastet**. + +### Update-Hinweis + +Erscheint eine neue Version auf SpigotMC, schreibt das Plugin eine Warnung in die Konsole. Admins bekommen beim Einloggen eine klickbare Nachricht mit dem Download-Link. + +--- + +## 16. Probleme und ihre Lösung + +| Problem | Lösung | +|---|---| +| **Plugin startet nicht** | Prüfe die Java-Version (**Java 21** nötig) und ob es Spigot/Paper ab 1.20 ist. Die genaue Fehlermeldung steht in der Server-Konsole. | +| **`/ak` tut nichts / „Unbekannter Befehl“** | Plugin wurde nicht geladen. Schau in die Konsole oder tippe `/plugins` und prüfe, ob „Adventskalender“ grün ist. | +| **„Dafür hast du keine Berechtigung“** | Der Spieler hat `adventskalender.use` (bzw. `adventskalender.admin`) nicht. Rechte prüfen. | +| **Alle Türchen sind gesperrt** | Es ist gerade nicht die Zeit des Kalenders. Prüfe das Datum oder teste mit `/ak admin test`. | +| **Im Menü steht „Key error: …“** | In deiner Sprachdatei fehlt ein Text. Siehe [Plugin aktualisieren](#15-plugin-aktualisieren), neue Sprachdatei erzeugen lassen. | +| **Ein Türchen lässt sich nicht öffnen, Meldung „falsch konfiguriert“** | In der `config.yml` ist bei diesem Tag ein Item- oder Verzauberungsname falsch geschrieben. Die Konsole sagt dir genau, wo. | +| **Eine Verzauberung fehlt am Geschenk** | Der Name in `type:` ist falsch (englisch und GROSS schreiben). Die Konsole zeigt eine Warnung. | +| **Kopf zeigt Steve statt Weihnachtsmotiv** | Der Server konnte die Textur nicht laden (Internet-/Mojang-Problem). Neustart versuchen. Bei Halloween/Ostern gibt es Items statt Köpfen, das ist normal. | +| **Meldung „Die Datenbank ist gerade nicht erreichbar“** | Nur mit MySQL. Prüfe Zugangsdaten und ob die Datenbank läuft. | +| **Nach dem Reload stimmt etwas nicht** | Server einmal komplett neu starten. | +| **Ein Spieler hat sich beim Geburtstag vertippt** | `/ak admin birthday set ` | +| **Spieler hat sein Geburtstagsgeschenk verloren / Fehler beim Abholen** | `/ak admin birthday reset `, dann darf er nochmal abholen. | +| **Kalender geht zur falschen Uhrzeit auf** | Zeitzone in der `config.yml` einstellen (siehe [Zeitzone](#zeitzone)). | + +Hilft nichts davon: Kopiere die **Fehlermeldung aus der Konsole** (nicht nur „geht nicht“) und frage auf der Seite des Plugins bei SpigotMC nach. + +--- + +## 17. Häufige Fragen + +**Kann ich Geschenke für jeden Tag selbst festlegen?** +Ja, siehe [Belohnungen ändern](#7-belohnungen-ändern). + +**Kann ich einen Kalender ausschalten, den ich nicht will?** +Ja: In der `config.yml` bei `events:` den Eintrag `enabled: false` setzen. + +**Kann ich mehr als 24, 31 oder 14 Türchen haben?** +Ja, bis zu 36 pro Kalender (Einstellung `days`). Du musst dann aber auch für jedes neue Türchen eine Belohnung eintragen und im Sprachfile einen Namen vergeben. + +**Was passiert, wenn der Kalender vorbei ist?** +Er bleibt noch `grace_days` Tage offen (Advent: bis zum 31.12., Ostern: 7 Tage), danach sind alle Türchen gesperrt, bis er im nächsten Jahr wieder startet. + +**Gehen die Türchen jedes Jahr von vorne los?** +Nein. Das Plugin merkt sich dauerhaft, was jemand geöffnet hat. Soll im nächsten Jahr wieder alles neu sein, benutze `/ak admin reset global` (oder `/ak admin reset advent global` nur für den Advent). **Achtung:** Das löscht die Daten für **alle**. + +**Sehen alle Spieler, wann ich Geburtstag habe?** +Nein. Das Datum sieht nur der Spieler selbst (und Admins in der Datenbank/Datei). Im Chat steht beim Abholen nur „Spieler feiert Geburtstag“, ohne Datum. + +**Funktioniert das Plugin mit Bedrock-Spielern (Geyser)?** +Das wurde nicht getestet. + +**Ich habe `data.yml` gelöscht, was jetzt?** +Dann haben alle Spieler wieder alle Türchen und Geburtstage sind weg. Spiele vorher eine Sicherung ein, wenn du eine hast. Mach vor Änderungen am Server grundsätzlich eine Sicherheitskopie des Plugin-Ordners. + +--- + +*Autor: M_Viper • Frohe Weihnachten, schaurig-schönes Halloween, frohe Ostern und alles Gute zum Geburtstag! 🎄🎃🐰🎂* diff --git a/pom.xml b/pom.xml index 4dcc34d..19e9423 100644 --- a/pom.xml +++ b/pom.xml @@ -1,77 +1,77 @@ - - - 4.0.0 - - de.mviper - adventskalender - 1.0.2 - jar - - - 21 - 21 - UTF-8 - - - - - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - - - placeholderapi - https://repo.extendedclip.com/content/repositories/placeholderapi/ - - - - - - org.spigotmc - spigot-api - 1.20.1-R0.1-SNAPSHOT - provided - - - me.clip - placeholderapi - 2.11.5 - provided - - - - - clean package - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - 21 - 21 - - - - org.apache.maven.plugins - maven-shade-plugin - 3.5.0 - - - package - - shade - - - - - - - - src/main/resources - true - - - + + + 4.0.0 + + de.mviper + adventskalender + 1.0.3 + jar + + + 21 + 21 + UTF-8 + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + placeholderapi + https://repo.extendedclip.com/content/repositories/placeholderapi/ + + + + + + org.spigotmc + spigot-api + 1.20.1-R0.1-SNAPSHOT + provided + + + me.clip + placeholderapi + 2.11.5 + provided + + + + + clean package + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + + + + src/main/resources + true + + + \ No newline at end of file diff --git a/src/main/java/de/mviper/adventskalender/AdventCalendarExpansion.java b/src/main/java/de/mviper/adventskalender/AdventCalendarExpansion.java index 0a54fd3..c37d479 100644 --- a/src/main/java/de/mviper/adventskalender/AdventCalendarExpansion.java +++ b/src/main/java/de/mviper/adventskalender/AdventCalendarExpansion.java @@ -1,69 +1,138 @@ -package de.mviper.adventskalender; - -import me.clip.placeholderapi.expansion.PlaceholderExpansion; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.time.LocalDate; -import java.time.Month; - -public class AdventCalendarExpansion extends PlaceholderExpansion { - - @Override - public @NotNull String getIdentifier() { - return "ak"; - } - - @Override - public @NotNull String getAuthor() { - return "M_Viper"; - } - - @Override - public @NotNull String getVersion() { - return "2.0.0"; - } - - @Override - public boolean persist() { - return true; // Expansion bleibt nach PAPI-Reload registriert - } - - @Override - public String onPlaceholderRequest(Player player, String identifier) { - if (player == null) return ""; - - // Aktuellen Tag ermitteln (Berücksichtigt den test_day aus der Config) - int testDay = Adventskalender.getInstance().getConfig().getInt("general.test_day", 0); - LocalDate date = LocalDate.now(); - int currentDay = (testDay > 0) ? testDay : (date.getMonth() == Month.DECEMBER ? date.getDayOfMonth() : 0); - - switch (identifier.toLowerCase()) { - - // %ak_claimed% - Anzahl der bereits geöffneten Türchen - case "claimed": - return String.valueOf(DataHandler.getClaimedDays(player).size()); - - // %ak_day% - Der aktuelle Tag des Adventskalenders (1-24) - case "day": - return String.valueOf(currentDay); - - // %ak_has_claimed_today% - Gibt "Ja" oder "Nein" zurück, ob heute schon geöffnet wurde - case "has_claimed_today": - if (currentDay == 0) return "Keine Adventszeit"; - return DataHandler.hasClaimed(player, currentDay) ? "Ja" : "Nein"; - - // %ak_remaining% - Wie viele Türchen sind noch offen (für diesen Spieler)? - case "remaining": - int claimedCount = DataHandler.getClaimedDays(player).size(); - return String.valueOf(24 - claimedCount); - - // %ak_is_global% - Zeigt an, ob der globale Modus aktiv ist - case "is_global": - boolean isGlobal = Adventskalender.getInstance().getConfig().getBoolean("calendar.use_global_calendar", false); - return isGlobal ? "Global" : "Spieler-basiert"; - } - - return null; // Placeholder unbekannt - } -} \ No newline at end of file +package de.mviper.adventskalender; + +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Platzhalter: %ak_claimed%, %ak_day%, %ak_has_claimed_today%, %ak_remaining%, %ak_is_global%, %ak_event% + * beziehen sich auf das laufende Event (sonst das erste aktivierte). Mit Event-Präfix, z.B. + * %ak_halloween_claimed%, gilt der Platzhalter für genau dieses Event. + */ +public class AdventCalendarExpansion extends PlaceholderExpansion { + + private static final long CACHE_MILLIS = 5000L; + + private record Cached(Set days, long time) {} + + // Kurzer Cache, damit Scoreboards/Hologramme die Datenbank nicht jede Sekunde abfragen + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + @Override + public @NotNull String getIdentifier() { + return "ak"; + } + + @Override + public @NotNull String getAuthor() { + return "M_Viper"; + } + + @Override + public @NotNull String getVersion() { + return Adventskalender.getInstance().getDescription().getVersion(); + } + + @Override + public boolean persist() { + return true; // Expansion bleibt nach PAPI-Reload registriert + } + + private Set claimedDays(Player player, CalendarEvent ev) { + long now = System.currentTimeMillis(); + String key = player.getUniqueId() + ":" + ev.getId(); + Cached c = cache.get(key); + if (c != null && now - c.time() < CACHE_MILLIS) return c.days(); + Set days = DataHandler.getViewClaimedDays(player, ev); + if (days == null) days = new HashSet<>(); + cache.put(key, new Cached(days, now)); + return days; + } + + private record CachedBirthday(Optional birthday, long time) {} + + private final ConcurrentHashMap birthdayCache = new ConcurrentHashMap<>(); + + private String birthdayPlaceholder(Player player, boolean activeOnly) { + long now = System.currentTimeMillis(); + CachedBirthday c = birthdayCache.get(player.getUniqueId()); + if (c == null || now - c.time() >= CACHE_MILLIS * 6) { + try { + c = new CachedBirthday(DataHandler.getBirthday(player.getUniqueId()), now); + } catch (DataHandler.DataException e) { + return activeOnly ? "Nein" : "-"; + } + birthdayCache.put(player.getUniqueId(), c); + } + if (c.birthday().isEmpty()) return activeOnly ? "Nein" : "-"; + DataHandler.Birthday b = c.birthday().get(); + if (!activeOnly) return BirthdayManager.format(b.month(), b.day()); + Integer year = BirthdayManager.activeYear(b.month(), b.day(), + java.time.LocalDate.now(Adventskalender.getInstance().getZone())); + return (year != null && b.claimedYear() < year) ? "Ja" : "Nein"; + } + + @Override + public String onPlaceholderRequest(Player player, String identifier) { + if (player == null) return ""; + + String id = identifier.toLowerCase(); + + // %ak_birthday% - eingetragener Geburtstag (TT.MM.) oder "-" + // %ak_birthday_active% - "Ja", wenn das Geburtstagsgeschenk gerade abholbar ist + if (id.equals("birthday") || id.equals("birthday_active")) { + return birthdayPlaceholder(player, id.equals("birthday_active")); + } + + CalendarEvent ev = null; + + // Optionales Event-Präfix, z.B. "halloween_claimed" + for (CalendarEvent candidate : EventManager.all()) { + if (id.startsWith(candidate.getId() + "_")) { + ev = candidate; + id = id.substring(candidate.getId().length() + 1); + break; + } + } + if (ev == null) ev = EventManager.defaultEvent(); + if (ev == null) return ""; + + int currentDay = ev.currentDay(); + + switch (id) { + + // %ak_event% - ID des Events + case "event": + return ev.getId(); + + // %ak_claimed% - Anzahl der bereits geöffneten Türchen + case "claimed": + return String.valueOf(claimedDays(player, ev).size()); + + // %ak_day% - Der aktuelle Tag des Kalenders + case "day": + return String.valueOf(currentDay); + + // %ak_has_claimed_today% - Gibt "Ja" oder "Nein" zurück, ob heute schon geöffnet wurde + case "has_claimed_today": + if (currentDay == 0) return "Keine Adventszeit"; + return claimedDays(player, ev).contains(currentDay) ? "Ja" : "Nein"; + + // %ak_remaining% - Wie viele Türchen sind noch offen? + case "remaining": + return String.valueOf(ev.getDays() - claimedDays(player, ev).size()); + + // %ak_is_global% - Zeigt an, ob der globale Modus aktiv ist + case "is_global": + return ev.isGlobal() ? "Global" : "Spieler-basiert"; + } + + return null; // Placeholder unbekannt + } +} diff --git a/src/main/java/de/mviper/adventskalender/AdventCommand.java b/src/main/java/de/mviper/adventskalender/AdventCommand.java index 07e2e8a..8ff9a41 100644 --- a/src/main/java/de/mviper/adventskalender/AdventCommand.java +++ b/src/main/java/de/mviper/adventskalender/AdventCommand.java @@ -1,160 +1,376 @@ -package de.mviper.adventskalender; - -import org.bukkit.Bukkit; -import org.bukkit.command.*; -import org.bukkit.entity.Player; -import java.util.*; -import java.util.stream.Collectors; - -public class AdventCommand implements CommandExecutor, TabCompleter { - - @Override - public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { - if (args.length > 0 && args[0].equalsIgnoreCase("admin")) { - if (!sender.hasPermission("adventskalender.admin")) { - sender.sendMessage(LanguageManager.getMessage("messages.no_permission")); - return true; - } - - // /ak admin reload - if (args.length == 2 && args[1].equalsIgnoreCase("reload")) { - Adventskalender.getInstance().reloadConfig(); - LanguageManager.setup(); - DataHandler.setup(); - sender.sendMessage(LanguageManager.getMessage("messages.admin.reload_success")); - return true; - } - - // /ak admin test - if (args.length == 3 && args[1].equalsIgnoreCase("test")) { - if (args[2].equalsIgnoreCase("reset")) { - Adventskalender.getInstance().getConfig().set("general.test_day", 0); - Adventskalender.getInstance().saveConfig(); - sender.sendMessage("§e[Admin] §7Test-Modus §cbeendet§7. Das echte Datum wird nun verwendet."); - return true; - } - - try { - int day = Integer.parseInt(args[2]); - if (day < 1 || day > 24) { - sender.sendMessage("§cBitte gib einen Tag zwischen 1 und 24 an (oder 'reset')."); - return true; - } - Adventskalender.getInstance().getConfig().set("general.test_day", day); - Adventskalender.getInstance().saveConfig(); - sender.sendMessage("§e[Admin] §7Test-Tag auf §6" + day + " §7gesetzt."); - } catch (NumberFormatException e) { - sender.sendMessage("§cBitte gib eine gültige Zahl (1-24) oder 'reset' an."); - } - return true; - } - - // /ak admin resetday - if (args.length == 4 && args[1].equalsIgnoreCase("resetday")) { - String target = args[2]; - try { - int day = Integer.parseInt(args[3]); - if (day < 1 || day > 24) throw new NumberFormatException(); - - if (target.equalsIgnoreCase("global")) { - DataHandler.GlobalDataManager.resetGlobalDay(day); - sender.sendMessage("§a[Admin] §7Tag §6" + day + " §7global zurückgesetzt."); - } else { - DataHandler.resetPlayerDay(target, day); - sender.sendMessage("§a[Admin] §7Tag §6" + day + " §7für §e" + target + " §7zurückgesetzt."); - } - } catch (NumberFormatException e) { - sender.sendMessage("§cTag muss zwischen 1 und 24 liegen."); - } - return true; - } - - // /ak admin reset - if (args.length >= 3 && args[1].equalsIgnoreCase("reset")) { - String baseTitle = LanguageManager.getString("gui.title"); - if (args[2].equalsIgnoreCase("global")) { - DataHandler.resetAll(); - for (Player online : Bukkit.getOnlinePlayers()) { - if (online.getOpenInventory().getTitle().contains(baseTitle)) online.closeInventory(); - } - sender.sendMessage("§a[Admin] §7Kompletter Reset durchgeführt."); - } else { - String targetName = args[2]; - Player target = Bukkit.getPlayer(targetName); - if (target != null) { - DataHandler.resetPlayer(target); - if (target.getOpenInventory().getTitle().contains(baseTitle)) target.closeInventory(); - sender.sendMessage("§a[Admin] §7Daten für §e" + target.getName() + " §7gelöscht."); - } else { - DataHandler.resetOfflinePlayer(targetName); - sender.sendMessage("§a[Admin] §7Offline-Daten für §e" + targetName + " §7gelöscht."); - } - } - return true; - } - - sender.sendMessage("§8§m---§r §6Advent Admin §8§m---"); - sender.sendMessage("§e/ak admin reload §7- Config neu laden"); - sender.sendMessage("§e/ak admin test <1-24/reset> §7- Datum simulieren"); - sender.sendMessage("§e/ak admin resetday §7- Tag löschen"); - sender.sendMessage("§e/ak admin reset §7- Alles löschen"); - return true; - } - - if (!(sender instanceof Player player)) return true; - if (!player.hasPermission("adventskalender.use")) { - player.sendMessage(LanguageManager.getMessage("messages.no_permission")); - return true; - } - - AdventInventory.open(player); - return true; - } - - @Override - public List onTabComplete(CommandSender s, Command c, String a, String[] args) { - if (args.length == 1) return Collections.singletonList("admin"); - - if (args.length == 2 && args[0].equalsIgnoreCase("admin")) - return Arrays.asList("reload", "test", "reset", "resetday"); - - // Tab-Completion für das Ziel (global oder Spielername) - if (args.length == 3 && (args[1].equalsIgnoreCase("reset") || args[1].equalsIgnoreCase("resetday"))) { - List options = new ArrayList<>(); - options.add("global"); - Bukkit.getOnlinePlayers().forEach(p -> options.add(p.getName())); - return options.stream() - .filter(opt -> opt.toLowerCase().startsWith(args[2].toLowerCase())) - .collect(Collectors.toList()); - } - - // Tab-Completion für die TAGE (Numerisch sortiert) - if ((args.length == 3 && args[1].equalsIgnoreCase("test")) || (args.length == 4 && args[1].equalsIgnoreCase("resetday"))) { - List options = new ArrayList<>(); - - // "reset" Option nur für den test command hinzufügen - if (args[1].equalsIgnoreCase("test")) { - options.add("reset"); - } - - for (int i = 1; i <= 24; i++) { - options.add(String.valueOf(i)); - } - - String currentInput = (args.length == 3) ? args[2] : args[3]; - - return options.stream() - .filter(opt -> opt.toLowerCase().startsWith(currentInput.toLowerCase())) - .sorted((s1, s2) -> { - // "reset" immer nach oben sortieren - if (s1.equalsIgnoreCase("reset")) return -1; - if (s2.equalsIgnoreCase("reset")) return 1; - // Restliche Zahlen numerisch vergleichen - return Integer.compare(Integer.parseInt(s1), Integer.parseInt(s2)); - }) - .collect(Collectors.toList()); - } - - return Collections.emptyList(); - } -} \ No newline at end of file +package de.mviper.adventskalender; + +import org.bukkit.Bukkit; +import org.bukkit.command.*; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import java.time.MonthDay; +import java.util.*; +import java.util.stream.Collectors; + +public class AdventCommand implements CommandExecutor, TabCompleter { + + private static final int MAX_DAYS = 36; + + @Override + public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { + Adventskalender plugin = Adventskalender.getInstance(); + + if (args.length > 0 && args[0].equalsIgnoreCase("admin")) { + if (!sender.hasPermission("adventskalender.admin")) { + sender.sendMessage(LanguageManager.getMessage("messages.no_permission")); + return true; + } + + // /ak admin reload + if (args.length == 2 && args[1].equalsIgnoreCase("reload")) { + plugin.reloadConfig(); + plugin.reloadSettings(); + LanguageManager.setup(); + DataHandler.setup(); + AdventInventory.initCache(); + sender.sendMessage(LanguageManager.getMessage("messages.admin.reload_success")); + return true; + } + + // /ak admin test [Event] + if ((args.length == 3 || args.length == 4) && args[1].equalsIgnoreCase("test")) { + CalendarEvent ev = null; + if (args.length == 4) { + ev = EventManager.getAny(args[2]); + if (ev == null) { + sendUnknownEvent(sender, args[2]); + return true; + } + } + String value = args[args.length - 1]; + List targets = ev != null ? List.of(ev) : EventManager.allConfigured(); + + if (value.equalsIgnoreCase("reset")) { + for (CalendarEvent t : targets) t.setTestDay(0); + sender.sendMessage(LanguageManager.getString("messages.admin.test_mode_off").replace("%event%", label(ev))); + return true; + } + + int day = parseDay(value, ev != null ? ev.getDays() : MAX_DAYS); + if (day == -1) { + sender.sendMessage(LanguageManager.getString("messages.admin.invalid_day")); + return true; + } + for (CalendarEvent t : targets) t.setTestDay(day); + sender.sendMessage(LanguageManager.getString("messages.admin.test_mode_on") + .replace("%day%", String.valueOf(day)).replace("%event%", label(ev))); + return true; + } + + // /ak admin resetday [Event] + if ((args.length == 4 || args.length == 5) && args[1].equalsIgnoreCase("resetday")) { + CalendarEvent ev = null; + int shift = 0; + if (args.length == 5) { + ev = EventManager.getAny(args[2]); + if (ev == null) { + sendUnknownEvent(sender, args[2]); + return true; + } + shift = 1; + } + String target = args[2 + shift]; + int day = parseDay(args[3 + shift], ev != null ? ev.getDays() : MAX_DAYS); + if (day == -1) { + sender.sendMessage(LanguageManager.getString("messages.admin.invalid_day")); + return true; + } + + final CalendarEvent event = ev; + runAsync(() -> { + if (target.equalsIgnoreCase("global")) { + DataHandler.resetGlobalDay(event, day); + reply(sender, LanguageManager.getString("messages.admin.reset_day_global") + .replace("%day%", String.valueOf(day)).replace("%event%", label(event))); + } else { + boolean found = DataHandler.resetPlayerDay(target, event, day); + reply(sender, found + ? LanguageManager.getString("messages.admin.reset_day_player") + .replace("%day%", String.valueOf(day)).replace("%player%", target).replace("%event%", label(event)) + : LanguageManager.getString("messages.admin.player_not_found").replace("%player%", target)); + } + }); + return true; + } + + // /ak admin reset [Event] + if (args.length >= 3 && args[1].equalsIgnoreCase("reset")) { + CalendarEvent ev = null; + String targetName = args[2]; + if (args.length >= 4) { + ev = EventManager.getAny(args[2]); + if (ev == null) { + sendUnknownEvent(sender, args[2]); + return true; + } + targetName = args[3]; + } + final CalendarEvent event = ev; + final String name = targetName; + + if (name.equalsIgnoreCase("global")) { + runAsync(() -> { + DataHandler.resetAll(event); + Bukkit.getScheduler().runTask(plugin, AdventInventory::closeAll); + reply(sender, LanguageManager.getString("messages.admin.reset_all").replace("%event%", label(event))); + }); + } else { + Player target = Bukkit.getPlayerExact(name); + if (target != null) { + runAsync(() -> { + DataHandler.resetPlayer(target, event); + Bukkit.getScheduler().runTask(plugin, () -> AdventInventory.closeFor(target)); + reply(sender, LanguageManager.getString("messages.admin.reset_player") + .replace("%player%", target.getName()).replace("%event%", label(event))); + }); + } else { + runAsync(() -> { + boolean found = DataHandler.resetOfflinePlayer(name, event); + reply(sender, found + ? LanguageManager.getString("messages.admin.reset_offline") + .replace("%player%", name).replace("%event%", label(event)) + : LanguageManager.getString("messages.admin.player_not_found").replace("%player%", name)); + }); + } + } + return true; + } + + // /ak admin birthday [TT.MM] + if (args.length >= 4 && args[1].equalsIgnoreCase("birthday")) { + String action = args[2].toLowerCase(); + String targetName = args[3]; + MonthDay date = null; + if (action.equals("set")) { + date = args.length == 5 ? BirthdayManager.parse(args[4]) : null; + if (date == null) { + sender.sendMessage(LanguageManager.getMessage("birthday.invalid_date", "birthday.prefix")); + return true; + } + } else if (!action.equals("remove") && !action.equals("reset")) { + for (String line : LanguageManager.getStringList("messages.admin.help")) sender.sendMessage(line); + return true; + } + + final MonthDay md = date; + runAsync(() -> { + @SuppressWarnings("deprecation") + OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); + if (!target.isOnline() && !target.hasPlayedBefore()) { + reply(sender, LanguageManager.getString("messages.admin.player_not_found").replace("%player%", targetName)); + return; + } + String shownName = target.getName() != null ? target.getName() : targetName; + try { + switch (action) { + case "set" -> { + DataHandler.setBirthday(target.getUniqueId(), shownName, md.getMonthValue(), md.getDayOfMonth(), false); + reply(sender, LanguageManager.getString("birthday.admin_set") + .replace("%player%", shownName).replace("%date%", BirthdayManager.format(md))); + } + case "remove" -> { + boolean removed = DataHandler.removeBirthday(target.getUniqueId(), shownName); + reply(sender, LanguageManager.getString(removed ? "birthday.admin_removed" : "birthday.admin_none") + .replace("%player%", shownName)); + } + default -> { + boolean reset = DataHandler.resetBirthdayClaim(target.getUniqueId(), shownName); + reply(sender, LanguageManager.getString(reset ? "birthday.admin_reset" : "birthday.admin_none") + .replace("%player%", shownName)); + } + } + } catch (DataHandler.DataException e) { + reply(sender, LanguageManager.getMessage("messages.db_error")); + } + }); + return true; + } + + for (String line : LanguageManager.getStringList("messages.admin.help")) { + sender.sendMessage(line); + } + return true; + } + + if (!(sender instanceof Player player)) { + sender.sendMessage(LanguageManager.getMessage("messages.only_player")); + return true; + } + if (!player.hasPermission("adventskalender.use")) { + player.sendMessage(LanguageManager.getMessage("messages.no_permission")); + return true; + } + + // /ak birthday [set | claim | remove] + if (args.length >= 1 && args[0].equalsIgnoreCase("birthday")) { + if (!player.hasPermission("adventskalender.birthday")) { + player.sendMessage(LanguageManager.getMessage("messages.no_permission")); + return true; + } + if (args.length == 1) { + BirthdayManager.handleInfo(player); + BirthdayManager.sendHelp(player); + } else if (args[1].equalsIgnoreCase("set") && args.length == 3) { + BirthdayManager.handleSet(player, args[2]); + } else if (args[1].equalsIgnoreCase("claim")) { + BirthdayManager.handleClaim(player); + } else if (args[1].equalsIgnoreCase("remove")) { + BirthdayManager.handleRemove(player); + } else { + BirthdayManager.sendHelp(player); + } + return true; + } + + // /ak [Event] + CalendarEvent event; + if (args.length >= 1) { + event = EventManager.get(args[0]); + if (event == null) { + sendUnknownEvent(player, args[0]); + return true; + } + } else { + event = EventManager.defaultEvent(); + if (event == null) { + player.sendMessage(LanguageManager.getMessage("messages.no_events")); + return true; + } + } + + AdventInventory.open(player, event); + return true; + } + + private void sendUnknownEvent(CommandSender sender, String given) { + String ids = EventManager.allConfigured().stream().map(CalendarEvent::getId).collect(Collectors.joining(", ")); + sender.sendMessage(LanguageManager.getMessage("messages.unknown_event") + .replace("%event%", given).replace("%events%", ids)); + } + + /** Tag zwischen 1 und max, sonst -1. */ + private static int parseDay(String value, int max) { + try { + int day = Integer.parseInt(value); + return (day >= 1 && day <= max) ? day : -1; + } catch (NumberFormatException e) { + return -1; + } + } + + private static String label(CalendarEvent ev) { + return ev != null ? ev.getId() : LanguageManager.getString("messages.admin.all_events"); + } + + private void runAsync(Runnable task) { + Bukkit.getScheduler().runTaskAsynchronously(Adventskalender.getInstance(), task); + } + + /** Sendet die Antwort im Main-Thread. */ + private void reply(CommandSender sender, String message) { + Bukkit.getScheduler().runTask(Adventskalender.getInstance(), () -> sender.sendMessage(message)); + } + + // ------------------------------------------------------------------ Tab-Completion + + private static List filter(Collection options, String prefix) { + return options.stream() + .filter(opt -> opt.toLowerCase().startsWith(prefix.toLowerCase())) + .collect(Collectors.toList()); + } + + private static List eventIds() { + return EventManager.allConfigured().stream().map(CalendarEvent::getId).collect(Collectors.toList()); + } + + private static List targets() { + List options = new ArrayList<>(); + options.add("global"); + Bukkit.getOnlinePlayers().forEach(p -> options.add(p.getName())); + return options; + } + + private static List dayOptions(int max) { + List days = new ArrayList<>(); + for (int i = 1; i <= max; i++) days.add(String.valueOf(i)); + return days; + } + + @Override + public List onTabComplete(CommandSender s, Command c, String a, String[] args) { + boolean admin = s.hasPermission("adventskalender.admin"); + + if (args.length == 1) { + List options = new ArrayList<>(EventManager.all().stream().map(CalendarEvent::getId).toList()); + if (BirthdayManager.isEnabled() && s.hasPermission("adventskalender.birthday")) options.add("birthday"); + if (admin) options.add("admin"); + return filter(options, args[0]); + } + + if (args[0].equalsIgnoreCase("birthday")) { + if (args.length == 2) return filter(Arrays.asList("set", "claim", "remove"), args[1]); + return Collections.emptyList(); + } + + if (!admin || !args[0].equalsIgnoreCase("admin")) return Collections.emptyList(); + + if (args.length == 2) return filter(Arrays.asList("reload", "test", "reset", "resetday", "birthday"), args[1]); + + if (args[1].equalsIgnoreCase("birthday")) { + if (args.length == 3) return filter(Arrays.asList("set", "remove", "reset"), args[2]); + if (args.length == 4) return filter(targets().stream().filter(t -> !t.equals("global")).collect(Collectors.toList()), args[3]); + return Collections.emptyList(); + } + + String sub = args[1].toLowerCase(); + String last = args[args.length - 1]; + CalendarEvent ev = EventManager.getAny(args[2]); + + switch (sub) { + case "test": { + if (args.length == 3) { + List options = new ArrayList<>(eventIds()); + options.add(0, "reset"); + options.addAll(dayOptions(MAX_DAYS)); + return filter(options, last); + } + if (args.length == 4 && ev != null) { + List options = new ArrayList<>(dayOptions(ev.getDays())); + options.add(0, "reset"); + return filter(options, last); + } + break; + } + case "reset": { + if (args.length == 3) { + List options = new ArrayList<>(eventIds()); + options.addAll(targets()); + return filter(options, last); + } + if (args.length == 4 && ev != null) return filter(targets(), last); + break; + } + case "resetday": { + if (args.length == 3) { + List options = new ArrayList<>(eventIds()); + options.addAll(targets()); + return filter(options, last); + } + if (args.length == 4) { + return ev != null ? filter(targets(), last) : filter(dayOptions(MAX_DAYS), last); + } + if (args.length == 5 && ev != null) return filter(dayOptions(ev.getDays()), last); + break; + } + default: + break; + } + return Collections.emptyList(); + } +} diff --git a/src/main/java/de/mviper/adventskalender/AdventInventory.java b/src/main/java/de/mviper/adventskalender/AdventInventory.java index b28f617..e97f824 100644 --- a/src/main/java/de/mviper/adventskalender/AdventInventory.java +++ b/src/main/java/de/mviper/adventskalender/AdventInventory.java @@ -1,154 +1,227 @@ -package de.mviper.adventskalender; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemFlag; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.inventory.meta.SkullMeta; -import org.bukkit.profile.PlayerProfile; -import org.bukkit.profile.PlayerTextures; -import java.net.MalformedURLException; -import java.net.URL; -import java.time.LocalDate; -import java.time.Month; -import java.util.*; - -public class AdventInventory { - - // Textur für das geöffnete Geschenk (Wreath) - private static final String OPEN_WREATH_URL = "http://textures.minecraft.net/texture/5e39248b341c87ce3e4294ac214c6f74468889cb1273ae7412906fd28db097e8"; - - // Texturen für die geschlossenen Tage (1-24) - private static final Map CLOSED_TEXTURES = new HashMap<>(); - private static final Map HEAD_CACHE = new HashMap<>(); - - static { - // Hier sind die URLs für die "Christmas Calendar #01-24 (red)" Reihe - CLOSED_TEXTURES.put(1, "http://textures.minecraft.net/texture/105b356be68ac56cfe0611b95027adf1ee67050e4cd66a0a99678fc2e25b6b0f"); - CLOSED_TEXTURES.put(2, "http://textures.minecraft.net/texture/40ec45e8ffb9cb714d1412bb9ee54fd1e7a3140c02e35ae0d3f308f1666ba126"); - CLOSED_TEXTURES.put(3, "http://textures.minecraft.net/texture/2466629a31aae22dba15a7b710a4517308656c19ba39a89ac2de2915a46e2823"); - CLOSED_TEXTURES.put(4, "http://textures.minecraft.net/texture/97a1956ed6f32e18b139f2d709355ac6945d0ab8fecd04d4c159d08006bfbe58"); - CLOSED_TEXTURES.put(5, "http://textures.minecraft.net/texture/855fad012c8844fc313510436b4919e002d93d6a0761f7bdaf78069683eee5e6"); - CLOSED_TEXTURES.put(6, "http://textures.minecraft.net/texture/c0400111ae026101077b59d8482c84a832c74f4f2105fc13b7c94f5b8000ea3"); - CLOSED_TEXTURES.put(7, "http://textures.minecraft.net/texture/a34505ed2567ea2efbab49977f7da8f1178db9ae53a0e99737286dc56e63636a"); - CLOSED_TEXTURES.put(8, "http://textures.minecraft.net/texture/a17ddf12485fb54cab9c9ffb55feea62a2d90b825772c6ea38561ce750cb6f83"); - CLOSED_TEXTURES.put(9, "http://textures.minecraft.net/texture/f75022feb11b736e50bc7d19d26d4ee00cecd6734a2138d2579f799b119b6734"); - CLOSED_TEXTURES.put(10, "http://textures.minecraft.net/texture/6b84208e4be2233fcd96e7f02d9613d5437f1d006236277c57d40652c5be2f23"); - - CLOSED_TEXTURES.put(11, "http://textures.minecraft.net/texture/19aa8a65ba29b4b7cbb9c66cf59fb537ba2c3013a958620b976b9f06248e8dda"); - CLOSED_TEXTURES.put(12, "http://textures.minecraft.net/texture/7dc1330b302e800c31c8dfefbd1b996016c34649f3a396749d3e048637364aad"); - CLOSED_TEXTURES.put(13, "http://textures.minecraft.net/texture/7e98a3884e0eac967c73a2bcbff7df62ab2fb327b4ceb9e636c74254866b5c7c"); - CLOSED_TEXTURES.put(14, "http://textures.minecraft.net/texture/3866643decb2d148999f9768dae89fbdb196b30d55f8846b1fbbe736bb7e5042"); - CLOSED_TEXTURES.put(15, "http://textures.minecraft.net/texture/744a95611950cc7f154c9f56ad472c1941376ec171efb8ba476e6cec4f866526"); - CLOSED_TEXTURES.put(16, "http://textures.minecraft.net/texture/1546959556eff4b8827ca50d8df686e8f20d5c843da689dddc48bff0a217efbe"); - CLOSED_TEXTURES.put(17, "http://textures.minecraft.net/texture/1f4fb00e824c97d9cfba4c44def27a79513978979515048f2e69eb5b7123c159"); - CLOSED_TEXTURES.put(18, "http://textures.minecraft.net/texture/9216b42018004a86081b5c1ee87dc8f12770f1859266e2cd359b75b44b5e7680"); - CLOSED_TEXTURES.put(19, "http://textures.minecraft.net/texture/93c7097291042c394267892c56feee9ba3936826ea5b2232f46cb5474f9cd937"); - CLOSED_TEXTURES.put(20, "http://textures.minecraft.net/texture/4f85a13977d01865a9d95d3279528285872551c36fd5231c7e3ab9897a7560fd"); - - CLOSED_TEXTURES.put(21, "http://textures.minecraft.net/texture/cdb512403f3610966d10bd3ca9fc61fdc5021268382f69719ac5679f3efd4c14"); - CLOSED_TEXTURES.put(22, "http://textures.minecraft.net/texture/cadaef824771250374fc3e82efe020ab765e665340cc2393f3b4ae8d6d18529c"); - CLOSED_TEXTURES.put(23, "http://textures.minecraft.net/texture/266f896c37a9d7fb857ff8c9f6f5e3540d17e1b893419f523eb199b71e71dde3"); - CLOSED_TEXTURES.put(24, "http://textures.minecraft.net/texture/f081f9274e4ef802453ac95381c00426e462440d3bb085fb3afefa6ded6a8d01"); - // Hinweis: In der Praxis füllst du hier alle 1-24 mit den URLs von Minecraft-Heads. - } - - public static void initCache() { - // Cache den Wreath-Kopf - HEAD_CACHE.put("OPENED", createBaseSkull(OPEN_WREATH_URL)); - - // Cache alle nummerierten geschlossenen Köpfe - for (int i = 1; i <= 24; i++) { - if (CLOSED_TEXTURES.containsKey(i)) { - HEAD_CACHE.put("CLOSED_" + i, createBaseSkull(CLOSED_TEXTURES.get(i))); - } - } - } - - public static void open(Player player) { - String title = " " + LanguageManager.getString("gui.title"); - Inventory inv = Bukkit.createInventory(null, 54, title); - - int testDay = Adventskalender.getInstance().getConfig().getInt("general.test_day", 0); - LocalDate date = LocalDate.now(); - int currentDay = (testDay > 0) ? testDay : (date.getMonth() == Month.DECEMBER ? date.getDayOfMonth() : 0); - - for (int i = 0; i < 54; i++) { - if (i < 9 || i >= 45 || i % 9 == 0 || (i + 1) % 9 == 0) { - Material m = (i % 2 == 0) ? Material.RED_STAINED_GLASS_PANE : Material.GREEN_STAINED_GLASS_PANE; - inv.setItem(i, createFiller(m)); - } else { - inv.setItem(i, createFiller(Material.WHITE_STAINED_GLASS_PANE)); - } - } - - int[] slots = {10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 39, 40, 41}; - - for (int day = 1; day <= 24; day++) { - boolean claimed = DataHandler.hasClaimed(player, day); - boolean isAvailable = currentDay >= day; - inv.setItem(slots[day - 1], createGiftItem(day, claimed, isAvailable)); - } - - player.openInventory(inv); - } - - private static ItemStack createGiftItem(int day, boolean claimed, boolean isAvailable) { - ItemStack item; - if (claimed) { - item = HEAD_CACHE.getOrDefault("OPENED", new ItemStack(Material.PLAYER_HEAD)).clone(); - } else { - item = HEAD_CACHE.getOrDefault("CLOSED_" + day, new ItemStack(Material.PLAYER_HEAD)).clone(); - } - - ItemMeta meta = item.getItemMeta(); - List lore = new ArrayList<>(); - lore.add(" "); - - if (claimed) { - meta.setDisplayName(LanguageManager.getString("rewards.day_" + day + ".name")); - meta.addEnchant(Enchantment.LUCK, 1, true); - meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); - lore.add("§8» §6Bereits abgeholt"); - lore.add("§aDu hast dieses Geschenk bereits geöffnet! ✔"); - } else { - if (!isAvailable) { - meta.setDisplayName("§c§lTag #" + day); - lore.add("§8» §7Noch verschlossen"); - lore.add("§7Komme am " + day + ". Dezember wieder!"); - } else { - meta.setDisplayName(LanguageManager.getString("rewards.day_" + day + ".name")); - lore.add("§8» §eKlicke zum Öffnen!"); - lore.addAll(LanguageManager.getStringList("rewards.day_" + day + ".lore")); - } - } - - meta.setLore(lore); - item.setItemMeta(meta); - return item; - } - - private static ItemStack createBaseSkull(String url) { - ItemStack item = new ItemStack(Material.PLAYER_HEAD); - SkullMeta meta = (SkullMeta) item.getItemMeta(); - try { - UUID uuid = UUID.nameUUIDFromBytes(url.getBytes()); - PlayerProfile profile = Bukkit.createPlayerProfile(uuid, "AdventHead"); - profile.getTextures().setSkin(new URL(url)); - meta.setOwnerProfile(profile); - item.setItemMeta(meta); - } catch (MalformedURLException e) { e.printStackTrace(); } - return item; - } - - private static ItemStack createFiller(Material material) { - ItemStack item = new ItemStack(material); - ItemMeta meta = item.getItemMeta(); - if (meta != null) { meta.setDisplayName(" "); item.setItemMeta(meta); } - return item; - } -} \ No newline at end of file +package de.mviper.adventskalender; + +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class AdventInventory { + + // Textur für das geöffnete Geschenk (Wreath) + private static final String OPEN_WREATH_URL = "http://textures.minecraft.net/texture/5e39248b341c87ce3e4294ac214c6f74468889cb1273ae7412906fd28db097e8"; + + // Texturen für die geschlossenen Tage (1-24) + private static final Map CLOSED_TEXTURES = new HashMap<>(); + private static final Map HEAD_CACHE = new HashMap<>(); + + static { + // Hier sind die URLs für die "Christmas Calendar #01-24 (red)" Reihe + CLOSED_TEXTURES.put(1, "http://textures.minecraft.net/texture/105b356be68ac56cfe0611b95027adf1ee67050e4cd66a0a99678fc2e25b6b0f"); + CLOSED_TEXTURES.put(2, "http://textures.minecraft.net/texture/40ec45e8ffb9cb714d1412bb9ee54fd1e7a3140c02e35ae0d3f308f1666ba126"); + CLOSED_TEXTURES.put(3, "http://textures.minecraft.net/texture/2466629a31aae22dba15a7b710a4517308656c19ba39a89ac2de2915a46e2823"); + CLOSED_TEXTURES.put(4, "http://textures.minecraft.net/texture/97a1956ed6f32e18b139f2d709355ac6945d0ab8fecd04d4c159d08006bfbe58"); + CLOSED_TEXTURES.put(5, "http://textures.minecraft.net/texture/855fad012c8844fc313510436b4919e002d93d6a0761f7bdaf78069683eee5e6"); + CLOSED_TEXTURES.put(6, "http://textures.minecraft.net/texture/c0400111ae026101077b59d8482c84a832c74f4f2105fc13b7c94f5b8000ea3"); + CLOSED_TEXTURES.put(7, "http://textures.minecraft.net/texture/a34505ed2567ea2efbab49977f7da8f1178db9ae53a0e99737286dc56e63636a"); + CLOSED_TEXTURES.put(8, "http://textures.minecraft.net/texture/a17ddf12485fb54cab9c9ffb55feea62a2d90b825772c6ea38561ce750cb6f83"); + CLOSED_TEXTURES.put(9, "http://textures.minecraft.net/texture/f75022feb11b736e50bc7d19d26d4ee00cecd6734a2138d2579f799b119b6734"); + CLOSED_TEXTURES.put(10, "http://textures.minecraft.net/texture/6b84208e4be2233fcd96e7f02d9613d5437f1d006236277c57d40652c5be2f23"); + + CLOSED_TEXTURES.put(11, "http://textures.minecraft.net/texture/19aa8a65ba29b4b7cbb9c66cf59fb537ba2c3013a958620b976b9f06248e8dda"); + CLOSED_TEXTURES.put(12, "http://textures.minecraft.net/texture/7dc1330b302e800c31c8dfefbd1b996016c34649f3a396749d3e048637364aad"); + CLOSED_TEXTURES.put(13, "http://textures.minecraft.net/texture/7e98a3884e0eac967c73a2bcbff7df62ab2fb327b4ceb9e636c74254866b5c7c"); + CLOSED_TEXTURES.put(14, "http://textures.minecraft.net/texture/3866643decb2d148999f9768dae89fbdb196b30d55f8846b1fbbe736bb7e5042"); + CLOSED_TEXTURES.put(15, "http://textures.minecraft.net/texture/744a95611950cc7f154c9f56ad472c1941376ec171efb8ba476e6cec4f866526"); + CLOSED_TEXTURES.put(16, "http://textures.minecraft.net/texture/1546959556eff4b8827ca50d8df686e8f20d5c843da689dddc48bff0a217efbe"); + CLOSED_TEXTURES.put(17, "http://textures.minecraft.net/texture/1f4fb00e824c97d9cfba4c44def27a79513978979515048f2e69eb5b7123c159"); + CLOSED_TEXTURES.put(18, "http://textures.minecraft.net/texture/9216b42018004a86081b5c1ee87dc8f12770f1859266e2cd359b75b44b5e7680"); + CLOSED_TEXTURES.put(19, "http://textures.minecraft.net/texture/93c7097291042c394267892c56feee9ba3936826ea5b2232f46cb5474f9cd937"); + CLOSED_TEXTURES.put(20, "http://textures.minecraft.net/texture/4f85a13977d01865a9d95d3279528285872551c36fd5231c7e3ab9897a7560fd"); + + CLOSED_TEXTURES.put(21, "http://textures.minecraft.net/texture/cdb512403f3610966d10bd3ca9fc61fdc5021268382f69719ac5679f3efd4c14"); + CLOSED_TEXTURES.put(22, "http://textures.minecraft.net/texture/cadaef824771250374fc3e82efe020ab765e665340cc2393f3b4ae8d6d18529c"); + CLOSED_TEXTURES.put(23, "http://textures.minecraft.net/texture/266f896c37a9d7fb857ff8c9f6f5e3540d17e1b893419f523eb199b71e71dde3"); + CLOSED_TEXTURES.put(24, "http://textures.minecraft.net/texture/f081f9274e4ef802453ac95381c00426e462440d3bb085fb3afefa6ded6a8d01"); + // Hinweis: In der Praxis füllst du hier alle 1-24 mit den URLs von Minecraft-Heads. + } + + // Spieler, die gerade einen Kalender geöffnet haben + private static final Set VIEWERS = ConcurrentHashMap.newKeySet(); + + /** Marker, an dem das Kalender-Inventar (und sein Event) erkannt wird statt am Titel. */ + public static class AdventHolder implements InventoryHolder { + private final CalendarEvent event; + private Inventory inventory; + + public AdventHolder(CalendarEvent event) { this.event = event; } + + public CalendarEvent getEvent() { return event; } + + @Override + public Inventory getInventory() { return inventory; } + } + + public static void initCache() { + HEAD_CACHE.clear(); + + // Advent: Wreath-Kopf und alle nummerierten geschlossenen Köpfe + HEAD_CACHE.put("OPENED", createBaseSkull(OPEN_WREATH_URL)); + for (int i = 1; i <= 24; i++) { + if (CLOSED_TEXTURES.containsKey(i)) { + HEAD_CACHE.put("CLOSED_" + i, createBaseSkull(CLOSED_TEXTURES.get(i))); + } + } + + // Andere Events: optionale eigene Kopf-Texturen aus der Config + for (CalendarEvent ev : EventManager.allConfigured()) { + if (ev.isAdvent()) continue; + if (!ev.getClosedTexture().isBlank()) HEAD_CACHE.put(ev.getId() + "_CLOSED", createBaseSkull(ev.getClosedTexture())); + if (!ev.getOpenedTexture().isBlank()) HEAD_CACHE.put(ev.getId() + "_OPENED", createBaseSkull(ev.getOpenedTexture())); + } + } + + public static void markClosed(Player player) { VIEWERS.remove(player.getUniqueId()); } + + public static void closeFor(Player player) { + if (VIEWERS.contains(player.getUniqueId())) player.closeInventory(); + } + + public static void closeAll() { + for (UUID id : new ArrayList<>(VIEWERS)) { + Player p = Bukkit.getPlayer(id); + if (p != null) p.closeInventory(); + } + VIEWERS.clear(); + } + + /** Lädt die Daten asynchron und öffnet das Inventar danach im Main-Thread. */ + public static void open(Player player, CalendarEvent event) { + Adventskalender plugin = Adventskalender.getInstance(); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Set claimedDays = DataHandler.getViewClaimedDays(player, event); + Bukkit.getScheduler().runTask(plugin, () -> { + if (!player.isOnline()) return; + if (claimedDays == null) { + player.sendMessage(LanguageManager.getMessage("messages.db_error", event)); + return; + } + build(player, event, claimedDays); + }); + }); + } + + private static void build(Player player, CalendarEvent event, Set claimedDays) { + String title = " " + LanguageManager.getString(event.titleKey()); + AdventHolder holder = new AdventHolder(event); + Inventory inv = Bukkit.createInventory(holder, 54, title); + holder.inventory = inv; + + int currentDay = event.currentDay(); + + for (int i = 0; i < 54; i++) { + int row = i / 9; + boolean border = row == 0 || row == 5 || (event.hasSideBorder() && (i % 9 == 0 || i % 9 == 8)); + if (border) { + Material m = (i % 2 == 0) ? Material.RED_STAINED_GLASS_PANE : Material.GREEN_STAINED_GLASS_PANE; + inv.setItem(i, createFiller(m)); + } else { + inv.setItem(i, createFiller(Material.WHITE_STAINED_GLASS_PANE)); + } + } + + for (int day = 1; day <= event.getDays(); day++) { + inv.setItem(event.slotForDay(day), createGiftItem(event, day, claimedDays.contains(day), currentDay >= day)); + } + + player.openInventory(inv); + // Nach openInventory, da ein vorher offenes Kalender-Inventar dabei geschlossen wird + VIEWERS.add(player.getUniqueId()); + } + + private static ItemStack baseItem(CalendarEvent event, int day, boolean claimed) { + if (event.isAdvent()) { + return claimed + ? HEAD_CACHE.getOrDefault("OPENED", new ItemStack(Material.PLAYER_HEAD)).clone() + : HEAD_CACHE.getOrDefault("CLOSED_" + day, new ItemStack(Material.PLAYER_HEAD)).clone(); + } + ItemStack head = HEAD_CACHE.get(event.getId() + (claimed ? "_OPENED" : "_CLOSED")); + ItemStack item = head != null ? head.clone() : new ItemStack(claimed ? event.getOpenedMaterial() : event.getClosedMaterial()); + item.setAmount(Math.max(1, Math.min(64, day))); // Die Stackgröße zeigt den Tag an + return item; + } + + private static ItemStack createGiftItem(CalendarEvent event, int day, boolean claimed, boolean isAvailable) { + ItemStack item = baseItem(event, day, claimed); + + ItemMeta meta = item.getItemMeta(); + List lore = new ArrayList<>(); + lore.add(" "); + String dayStr = String.valueOf(day); + + String rewardName = LanguageManager.has(event.nameKey(day)) + ? LanguageManager.getString(event.nameKey(day)) + : LanguageManager.getString("gui.default_name").replace("%day%", dayStr); + + if (claimed) { + meta.setDisplayName(rewardName); + // Glitzer-Effekt über eine beliebige Verzauberung, die per Key gesucht wird (versionsunabhängig) + Enchantment glow = Enchantment.getByKey(NamespacedKey.minecraft("luck_of_the_sea")); + if (glow != null) { + meta.addEnchant(glow, 1, true); + meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + } + lore.add(LanguageManager.getString("gui.claimed_status")); + lore.add(LanguageManager.getString("gui.claimed_hint")); + } else if (!isAvailable) { + meta.setDisplayName(LanguageManager.getString("gui.locked_name").replace("%day%", dayStr)); + lore.add(LanguageManager.getString("gui.locked_status")); + lore.add(LanguageManager.getString("gui.locked_hint") + .replace("%day%", dayStr).replace("%date%", event.formatDate(day))); + } else { + meta.setDisplayName(rewardName); + lore.add(LanguageManager.getString("gui.click_open")); + lore.addAll(LanguageManager.getStringList(event.loreKey(day))); + } + + meta.setLore(lore); + item.setItemMeta(meta); + return item; + } + + private static ItemStack createBaseSkull(String url) { + ItemStack item = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) item.getItemMeta(); + try { + UUID uuid = UUID.nameUUIDFromBytes(url.getBytes(StandardCharsets.UTF_8)); + PlayerProfile profile = Bukkit.createPlayerProfile(uuid, "AdventHead"); + PlayerTextures textures = profile.getTextures(); + textures.setSkin(URI.create(url).toURL()); + profile.setTextures(textures); + meta.setOwnerProfile(profile); + item.setItemMeta(meta); + } catch (Exception e) { + Adventskalender.getInstance().getLogger().warning("Kopf-Textur konnte nicht geladen werden (" + url + "): " + e.getMessage()); + } + return item; + } + + private static ItemStack createFiller(Material material) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { meta.setDisplayName(" "); item.setItemMeta(meta); } + return item; + } +} diff --git a/src/main/java/de/mviper/adventskalender/AdventListener.java b/src/main/java/de/mviper/adventskalender/AdventListener.java index fee86a4..bb77439 100644 --- a/src/main/java/de/mviper/adventskalender/AdventListener.java +++ b/src/main/java/de/mviper/adventskalender/AdventListener.java @@ -1,135 +1,163 @@ -package de.mviper.adventskalender; - -import net.md_5.bungee.api.chat.ClickEvent; -import net.md_5.bungee.api.chat.HoverEvent; -import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.api.chat.hover.content.Text; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.Sound; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import java.time.LocalDate; -import java.time.Month; -import java.util.List; -import java.util.Map; - -public class AdventListener implements Listener { - - @EventHandler - public void onJoin(PlayerJoinEvent event) { - Player player = event.getPlayer(); - - // Nachricht an Admins senden, wenn ein Update da ist - if (player.hasPermission("adventskalender.admin")) { - // Delay von 2 Sekunden (40 Ticks), damit die Nachricht nicht im Join-Spam untergeht - Bukkit.getScheduler().runTaskLater(Adventskalender.getInstance(), () -> { - if (Adventskalender.getInstance().isUpdateAvailable()) { - String latest = Adventskalender.getInstance().getLatestVersion(); - String current = Adventskalender.getInstance().getDescription().getVersion(); - - player.sendMessage("§7 "); - player.sendMessage("§8§m+-------------------------------------------+"); - player.sendMessage("§6§l ADVENTSKALENDER UPDATE"); - player.sendMessage("§7 "); - player.sendMessage("§7 Eine neue Version ist verfügbar: §a§l" + latest); - player.sendMessage("§7 Installierte Version: §c" + current); - player.sendMessage("§7 "); - - // Erstellung der klickbaren Nachricht - TextComponent message = new TextComponent("§e §nKLICKE HIER ZUM DOWNLOAD"); - message.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://www.spigotmc.org/resources/130974/")); - message.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("§7Öffnet die Spigot-Seite"))); - - player.spigot().sendMessage(message); - - player.sendMessage("§7 "); - player.sendMessage("§8§m+-------------------------------------------+"); - player.sendMessage("§7 "); - } - }, 40L); - } - } - - @EventHandler - public void onInventoryClick(InventoryClickEvent event) { - String baseTitle = LanguageManager.getString("gui.title"); - if (!event.getView().getTitle().contains(baseTitle)) return; - - event.setCancelled(true); - - if (!(event.getWhoClicked() instanceof Player player)) return; - - ItemStack clicked = event.getCurrentItem(); - if (clicked == null || clicked.getType() != Material.PLAYER_HEAD) return; - if (!clicked.hasItemMeta() || !clicked.getItemMeta().hasDisplayName()) return; - - String displayName = ChatColor.stripColor(clicked.getItemMeta().getDisplayName()); - int day; - try { - day = Integer.parseInt(displayName.replaceAll("[^0-9]", "")); - } catch (NumberFormatException e) { - return; - } - - int testDay = Adventskalender.getInstance().getConfig().getInt("general.test_day", 0); - LocalDate date = LocalDate.now(); - int currentDay = (testDay > 0) ? testDay : (date.getMonth() == Month.DECEMBER ? date.getDayOfMonth() : 0); - - if (currentDay >= day) { - if (DataHandler.hasClaimed(player, day)) { - player.sendMessage(LanguageManager.getMessage("messages.day_already_claimed")); - player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f); - return; - } - - giveReward(player, day); - DataHandler.setClaimed(player, day); - player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.5f); - - AdventInventory.open(player); - } else { - player.sendMessage(LanguageManager.getMessage("messages.day_not_available")); - player.playSound(player.getLocation(), Sound.BLOCK_CHEST_LOCKED, 1f, 1f); - } - } - - private void giveReward(Player player, int day) { - String path = "rewards." + day; - String matName = Adventskalender.getInstance().getConfig().getString(path + ".material", "COOKIE"); - int amount = Adventskalender.getInstance().getConfig().getInt(path + ".amount", 1); - - ItemStack reward = new ItemStack(Material.valueOf(matName.toUpperCase()), amount); - ItemMeta meta = reward.getItemMeta(); - - if (meta != null) { - List> enchants = Adventskalender.getInstance().getConfig().getMapList(path + ".enchantments"); - for (Map entry : enchants) { - String type = (String) entry.get("type"); - Object levelObj = entry.get("level"); - int level = (levelObj instanceof Integer) ? (int) levelObj : 1; - - Enchantment enc = Enchantment.getByName(type.toUpperCase()); - if (enc != null) { - meta.addEnchant(enc, level, true); - } - } - reward.setItemMeta(meta); - } - - if (player.getInventory().firstEmpty() == -1) { - player.getWorld().dropItemNaturally(player.getLocation(), reward); - player.sendMessage(LanguageManager.getMessage("messages.inventory_full")); - } else { - player.getInventory().addItem(reward); - } - player.sendMessage(LanguageManager.getMessage("messages.reward_received").replace("%day%", String.valueOf(day))); - } -} \ No newline at end of file +package de.mviper.adventskalender; + +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.inventory.ItemStack; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class AdventListener implements Listener { + + // Spieler, deren Klick gerade verarbeitet wird (verhindert Spam-Klicks) + private final Set busy = ConcurrentHashMap.newKeySet(); + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + + // Hinweis auf das Geburtstagsgeschenk (falls abholbar) + Bukkit.getScheduler().runTaskLater(Adventskalender.getInstance(), () -> { + if (player.isOnline()) BirthdayManager.checkOnJoin(player); + }, 80L); + + // Nachricht an Admins senden, wenn ein Update da ist + if (player.hasPermission("adventskalender.admin")) { + // Delay von 2 Sekunden (40 Ticks), damit die Nachricht nicht im Join-Spam untergeht + Bukkit.getScheduler().runTaskLater(Adventskalender.getInstance(), () -> { + if (!player.isOnline()) return; + if (Adventskalender.getInstance().isUpdateAvailable()) { + String latest = Adventskalender.getInstance().getLatestVersion(); + String current = Adventskalender.getInstance().getDescription().getVersion(); + + player.sendMessage("§7 "); + player.sendMessage("§8§m+-------------------------------------------+"); + player.sendMessage("§6§l ADVENTSKALENDER UPDATE"); + player.sendMessage("§7 "); + player.sendMessage("§7 Eine neue Version ist verfügbar: §a§l" + latest); + player.sendMessage("§7 Installierte Version: §c" + current); + player.sendMessage("§7 "); + + // Erstellung der klickbaren Nachricht + TextComponent message = new TextComponent("§e §nKLICKE HIER ZUM DOWNLOAD"); + message.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://www.spigotmc.org/resources/130974/")); + message.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("§7Öffnet die Spigot-Seite"))); + + player.spigot().sendMessage(message); + + player.sendMessage("§7 "); + player.sendMessage("§8§m+-------------------------------------------+"); + player.sendMessage("§7 "); + } + }, 40L); + } + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + if (event.getInventory().getHolder() instanceof AdventInventory.AdventHolder + && event.getPlayer() instanceof Player player) { + AdventInventory.markClosed(player); + } + } + + @EventHandler + public void onInventoryDrag(InventoryDragEvent event) { + if (event.getInventory().getHolder() instanceof AdventInventory.AdventHolder) { + event.setCancelled(true); + } + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getInventory().getHolder() instanceof AdventInventory.AdventHolder)) return; + + event.setCancelled(true); + + if (!(event.getWhoClicked() instanceof Player player)) return; + if (event.getClickedInventory() != event.getInventory()) return; + + CalendarEvent calendar = ((AdventInventory.AdventHolder) event.getInventory().getHolder()).getEvent(); + int day = calendar.dayForSlot(event.getRawSlot()); + if (day == -1) return; + + Adventskalender plugin = Adventskalender.getInstance(); + + if (calendar.currentDay() < day) { + player.sendMessage(LanguageManager.getMessage("messages.day_not_available", calendar)); + player.playSound(player.getLocation(), Sound.BLOCK_CHEST_LOCKED, 1f, 1f); + return; + } + + // Belohnung vorab prüfen: Bei Konfigurationsfehlern wird der Tag nicht verbraucht + ItemStack reward = buildReward(calendar, day); + if (reward == null) { + player.sendMessage(LanguageManager.getMessage("messages.reward_error", calendar).replace("%day%", String.valueOf(day))); + return; + } + + UUID id = player.getUniqueId(); + if (!busy.add(id)) return; + + // Datenbankzugriff async, Ergebnis wieder im Main-Thread verarbeiten + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + DataHandler.ClaimResult result = DataHandler.isReady() + ? DataHandler.setClaimed(player, calendar, day) + : DataHandler.ClaimResult.ERROR; + + Bukkit.getScheduler().runTask(plugin, () -> { + busy.remove(id); + + if (result == DataHandler.ClaimResult.SUCCESS && !player.isOnline()) { + // Spieler ist inzwischen weg: Tag wieder freigeben, damit die Belohnung nicht verloren geht + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + if (calendar.isGlobal()) { + DataHandler.resetGlobalDay(calendar, day); + } else { + DataHandler.resetPlayerDay(player.getName(), calendar, day); + } + }); + return; + } + if (!player.isOnline()) return; + + switch (result) { + case SUCCESS -> { + giveReward(player, calendar, day, reward); + player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.5f); + AdventInventory.open(player, calendar); + } + case ALREADY_CLAIMED -> { + player.sendMessage(LanguageManager.getMessage(calendar.isGlobal() ? "messages.global_already_claimed" : "messages.day_already_claimed", calendar)); + player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f); + } + case ERROR -> player.sendMessage(LanguageManager.getMessage("messages.db_error", calendar)); + } + }); + }); + } + + /** Baut das Belohnungs-Item aus der Config. Gibt null zurück, wenn das Material ungültig ist. */ + private ItemStack buildReward(CalendarEvent calendar, int day) { + return RewardUtil.build(calendar.rewardsPath() + "." + day, "Tag " + day + " (" + calendar.getId() + ")"); + } + + private void giveReward(Player player, CalendarEvent calendar, int day, ItemStack reward) { + if (RewardUtil.give(player, reward)) { + player.sendMessage(LanguageManager.getMessage("messages.inventory_full", calendar)); + } + player.sendMessage(LanguageManager.getMessage("messages.reward_received", calendar).replace("%day%", String.valueOf(day))); + } +} diff --git a/src/main/java/de/mviper/adventskalender/Adventskalender.java b/src/main/java/de/mviper/adventskalender/Adventskalender.java index 35898a9..bce8c3e 100644 --- a/src/main/java/de/mviper/adventskalender/Adventskalender.java +++ b/src/main/java/de/mviper/adventskalender/Adventskalender.java @@ -1,62 +1,103 @@ -package de.mviper.adventskalender; - -import org.bukkit.Bukkit; -import org.bukkit.NamespacedKey; -import org.bukkit.plugin.java.JavaPlugin; - -public final class Adventskalender extends JavaPlugin { - private static Adventskalender instance; - private NamespacedKey dataKey; - - // Update-Status Variablen - private boolean updateAvailable = false; - private String latestVersion = ""; - - @Override - public void onEnable() { - instance = this; - this.dataKey = new NamespacedKey(this, "claimed_days_v2"); - - saveDefaultConfig(); - LanguageManager.setup(); - - // Initialisierung der Daten (MySQL wird hier bevorzugt behandelt) - DataHandler.setup(); - - // Textur-Puffer füllen, um Steve-Köpfe zu vermeiden - AdventInventory.initCache(); - - getCommand("adventskalender").setExecutor(new AdventCommand()); - getServer().getPluginManager().registerEvents(new AdventListener(), this); - - // Update Checker Integration (ID: 130974) - new UpdateChecker(this, 130974).getVersion(version -> { - if (!this.getDescription().getVersion().equals(version)) { - this.updateAvailable = true; - this.latestVersion = version; - getLogger().warning("Eine neue Version (" + version + ") ist verfügbar!"); - getLogger().warning("Download: https://www.spigotmc.org/resources/130974/"); - } else { - getLogger().info("Das Plugin ist auf dem neuesten Stand."); - } - }); - - if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { - new AdventCalendarExpansion().register(); - } - - getLogger().info("Adventskalender erfolgreich gestartet!"); - } - - @Override - public void onDisable() { - MySQLManager.close(); - } - - public static Adventskalender getInstance() { return instance; } - public NamespacedKey getDataKey() { return dataKey; } - - // Getter für den Update-Status - public boolean isUpdateAvailable() { return updateAvailable; } - public String getLatestVersion() { return latestVersion; } -} \ No newline at end of file +package de.mviper.adventskalender; + +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.plugin.java.JavaPlugin; + +import java.time.DateTimeException; +import java.time.ZoneId; + +public final class Adventskalender extends JavaPlugin { + private static Adventskalender instance; + private NamespacedKey dataKey; + + // Update-Status Variablen + private volatile boolean updateAvailable = false; + private volatile String latestVersion = ""; + + @Override + public void onEnable() { + instance = this; + this.dataKey = new NamespacedKey(this, "claimed_days_v2"); + + saveDefaultConfig(); + reloadSettings(); + LanguageManager.setup(); + + // Initialisierung der Daten (MySQL wird hier bevorzugt behandelt) + DataHandler.setup(); + + // Textur-Puffer füllen, um Steve-Köpfe zu vermeiden + AdventInventory.initCache(); + + getCommand("adventskalender").setExecutor(new AdventCommand()); + getServer().getPluginManager().registerEvents(new AdventListener(), this); + + // Update Checker Integration (ID: 130974) + new UpdateChecker(this, 130974).getVersion(version -> { + String current = this.getDescription().getVersion(); + if (isNewer(version, current)) { + this.updateAvailable = true; + this.latestVersion = version; + getLogger().warning("Eine neue Version (" + version + ") ist verfügbar!"); + getLogger().warning("Download: https://www.spigotmc.org/resources/130974/"); + } else { + getLogger().info("Das Plugin ist auf dem neuesten Stand."); + } + }); + + if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { + new AdventCalendarExpansion().register(); + } + + getLogger().info("Adventskalender erfolgreich gestartet!"); + } + + @Override + public void onDisable() { + MySQLManager.close(); + } + + /** Liest die Werte, die aus der Config stammen, neu ein (auch nach /ak admin reload). */ + public void reloadSettings() { + EventManager.load(); + } + + public ZoneId getZone() { + String tz = getConfig().getString("general.timezone", ""); + if (tz != null && !tz.isBlank()) { + try { + return ZoneId.of(tz.trim()); + } catch (DateTimeException e) { + getLogger().warning("Ungültige Zeitzone '" + tz + "' in der config.yml - nutze Server-Zeitzone."); + } + } + return ZoneId.systemDefault(); + } + + /** true, wenn die entfernte Version numerisch neuer ist als die lokale. */ + private static boolean isNewer(String remote, String local) { + String[] r = remote.split("\\."); + String[] l = local.split("\\."); + for (int i = 0; i < Math.max(r.length, l.length); i++) { + int a = part(r, i); + int b = part(l, i); + if (a != b) return a > b; + } + return false; + } + + private static int part(String[] parts, int i) { + if (i >= parts.length) return 0; + String digits = parts[i].replaceAll("[^0-9]", ""); + if (digits.isEmpty() || digits.length() > 9) return 0; + return Integer.parseInt(digits); + } + + public static Adventskalender getInstance() { return instance; } + public NamespacedKey getDataKey() { return dataKey; } + + // Getter für den Update-Status + public boolean isUpdateAvailable() { return updateAvailable; } + public String getLatestVersion() { return latestVersion; } +} diff --git a/src/main/java/de/mviper/adventskalender/BirthdayManager.java b/src/main/java/de/mviper/adventskalender/BirthdayManager.java new file mode 100644 index 0000000..3efbcec --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/BirthdayManager.java @@ -0,0 +1,289 @@ +package de.mviper.adventskalender; + +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.MonthDay; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Spieler-Geburtstage: Der Spieler trägt sein Datum (ohne Jahr) ein und kann am Geburtstag + * sowie bis zu birthday.grace_days Tage danach einmal pro Jahr ein Geschenk abholen. + */ +public final class BirthdayManager { + + private BirthdayManager() {} + + // ------------------------------------------------------------ Config / Datum + + public static boolean isEnabled() { + return Adventskalender.getInstance().getConfig().getBoolean("birthday.enabled", true); + } + + public static int graceDays() { + return Math.max(0, Adventskalender.getInstance().getConfig().getInt("birthday.grace_days", 2)); + } + + private static boolean allowChange() { + return Adventskalender.getInstance().getConfig().getBoolean("birthday.allow_change", false); + } + + /** Erwartet TT.MM (auch 5.3 oder 05.03.). Gibt null bei ungültigen Daten zurück. */ + public static MonthDay parse(String input) { + if (input == null) return null; + String s = input.trim(); + if (s.endsWith(".")) s = s.substring(0, s.length() - 1); + String[] parts = s.split("\\."); + if (parts.length != 2) return null; + try { + return MonthDay.of(Integer.parseInt(parts[1].trim()), Integer.parseInt(parts[0].trim())); + } catch (NumberFormatException | DateTimeException e) { + return null; + } + } + + public static String format(int month, int day) { + return String.format("%02d.%02d.", day, month); + } + + public static String format(MonthDay md) { + return format(md.getMonthValue(), md.getDayOfMonth()); + } + + /** + * Das Jahr des Geburtstags, dessen Zeitfenster (Geburtstag bis +grace_days) heute läuft, sonst null. + * Berücksichtigt den Jahreswechsel (z.B. 31.12. + 2 Tage). Der 29.2. wird in Nichtschaltjahren zum 28.2. + */ + public static Integer activeYear(int month, int day, LocalDate today) { + MonthDay md = MonthDay.of(month, day); + for (int y = today.getYear() - 1; y <= today.getYear(); y++) { + LocalDate start = md.atYear(y); + if (!today.isBefore(start) && !today.isAfter(start.plusDays(graceDays()))) return y; + } + return null; + } + + private static String msg(String path) { + return LanguageManager.getMessage(path, "birthday.prefix"); + } + + private static LocalDate today() { + return LocalDate.now(Adventskalender.getInstance().getZone()); + } + + private static void async(Runnable task) { + Bukkit.getScheduler().runTaskAsynchronously(Adventskalender.getInstance(), task); + } + + private static void sync(Runnable task) { + Bukkit.getScheduler().runTask(Adventskalender.getInstance(), task); + } + + private static boolean checkEnabled(Player player) { + if (isEnabled()) return true; + player.sendMessage(msg("birthday.disabled")); + return false; + } + + // ------------------------------------------------------------ Spieler-Aktionen + + public static void sendHelp(Player player) { + for (String line : LanguageManager.getStringList("birthday.help")) { + player.sendMessage(line.replace("%days%", String.valueOf(graceDays()))); + } + } + + public static void handleInfo(Player player) { + if (!checkEnabled(player)) return; + UUID id = player.getUniqueId(); + async(() -> { + try { + Optional b = DataHandler.getBirthday(id); + sync(() -> { + if (b.isPresent()) { + player.sendMessage(msg("birthday.info") + .replace("%date%", format(b.get().month(), b.get().day()))); + } else { + player.sendMessage(msg("birthday.info_none")); + } + }); + } catch (DataHandler.DataException e) { + sync(() -> player.sendMessage(msg("messages.db_error"))); + } + }); + } + + public static void handleSet(Player player, String input) { + if (!checkEnabled(player)) return; + MonthDay md = parse(input); + if (md == null) { + player.sendMessage(msg("birthday.invalid_date")); + return; + } + UUID id = player.getUniqueId(); + String name = player.getName(); + async(() -> { + try { + boolean written = DataHandler.setBirthday(id, name, md.getMonthValue(), md.getDayOfMonth(), !allowChange()); + Optional current = written ? Optional.empty() : DataHandler.getBirthday(id); + sync(() -> { + if (written) { + player.sendMessage(msg("birthday.set_success") + .replace("%date%", format(md)).replace("%days%", String.valueOf(graceDays()))); + } else { + player.sendMessage(msg("birthday.already_set") + .replace("%date%", current.map(b -> format(b.month(), b.day())).orElse("?"))); + } + }); + } catch (DataHandler.DataException e) { + sync(() -> player.sendMessage(msg("messages.db_error"))); + } + }); + } + + public static void handleRemove(Player player) { + if (!checkEnabled(player)) return; + if (!allowChange()) { + player.sendMessage(msg("birthday.change_not_allowed")); + return; + } + UUID id = player.getUniqueId(); + async(() -> { + try { + boolean removed = DataHandler.removeBirthday(id, null); + sync(() -> player.sendMessage(msg(removed ? "birthday.removed" : "birthday.info_none"))); + } catch (DataHandler.DataException e) { + sync(() -> player.sendMessage(msg("messages.db_error"))); + } + }); + } + + public static void handleClaim(Player player) { + if (!checkEnabled(player)) return; + + // Belohnungen vorab bauen: Bei Konfigurationsfehlern wird nichts verbraucht + List items = new ArrayList<>(); + ConfigurationSection rewards = Adventskalender.getInstance().getConfig().getConfigurationSection("birthday.rewards"); + if (rewards != null) { + for (String key : rewards.getKeys(false)) { + ItemStack item = RewardUtil.build("birthday.rewards." + key, "Geburtstags-Belohnung " + key); + if (item == null) { + player.sendMessage(msg("birthday.reward_error")); + return; + } + items.add(item); + } + } + List commands = Adventskalender.getInstance().getConfig().getStringList("birthday.commands"); + if (items.isEmpty() && commands.isEmpty()) { + player.sendMessage(msg("birthday.reward_error")); + return; + } + + UUID id = player.getUniqueId(); + String name = player.getName(); + LocalDate today = today(); + + async(() -> { + String messageKey = null; + DataHandler.ClaimResult result = null; + Integer year = null; + try { + Optional b = DataHandler.getBirthday(id); + if (b.isEmpty()) { + messageKey = "birthday.not_set"; + } else { + year = activeYear(b.get().month(), b.get().day(), today); + if (year == null) { + String date = format(b.get().month(), b.get().day()); + sync(() -> player.sendMessage(msg("birthday.not_today") + .replace("%date%", date).replace("%days%", String.valueOf(graceDays())))); + return; + } + result = DataHandler.claimBirthday(id, year); + } + } catch (DataHandler.DataException e) { + result = DataHandler.ClaimResult.ERROR; + } + + final String key = messageKey; + final DataHandler.ClaimResult res = result; + sync(() -> { + if (key != null) { + player.sendMessage(msg(key)); + return; + } + if (res == DataHandler.ClaimResult.SUCCESS && !player.isOnline()) { + // Spieler ist inzwischen weg: Abholung zurücknehmen, damit das Geschenk nicht verloren geht + async(() -> DataHandler.resetBirthdayClaim(id, null)); + return; + } + if (!player.isOnline()) return; + switch (res) { + case SUCCESS -> giveRewards(player, items, commands); + case ALREADY_CLAIMED -> player.sendMessage(msg("birthday.already_claimed")); + default -> player.sendMessage(msg("messages.db_error")); + } + }); + }); + } + + private static void giveRewards(Player player, List items, List commands) { + boolean dropped = false; + for (ItemStack item : items) { + dropped |= RewardUtil.give(player, item); + } + for (String command : commands) { + String cmd = command.startsWith("/") ? command.substring(1) : command; + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd.replace("%player%", player.getName())); + } + if (dropped) player.sendMessage(msg("messages.inventory_full")); + player.sendMessage(msg("birthday.reward_received")); + player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1f, 1f); + + if (Adventskalender.getInstance().getConfig().getBoolean("birthday.broadcast", true)) { + Bukkit.broadcastMessage(msg("birthday.broadcast").replace("%player%", player.getName())); + } + } + + // ------------------------------------------------------------ Join-Hinweis + + /** Weist beim Beitritt auf das Geschenk hin, wenn es gerade abholbar ist. */ + public static void checkOnJoin(Player player) { + if (!isEnabled() || !player.hasPermission("adventskalender.birthday")) return; + UUID id = player.getUniqueId(); + LocalDate today = today(); + async(() -> { + try { + Optional b = DataHandler.getBirthday(id); + if (b.isEmpty()) return; + Integer year = activeYear(b.get().month(), b.get().day(), today); + if (year == null || b.get().claimedYear() >= year) return; + + sync(() -> { + if (!player.isOnline()) return; + TextComponent message = new TextComponent( + msg("birthday.join_notice")); + message.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/adventskalender birthday claim")); + message.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new Text(LanguageManager.getString("birthday.join_hover")))); + player.spigot().sendMessage(message); + }); + } catch (DataHandler.DataException ignored) { + // Beim Beitritt keine Fehlermeldung an den Spieler + } + }); + } +} diff --git a/src/main/java/de/mviper/adventskalender/CalendarEvent.java b/src/main/java/de/mviper/adventskalender/CalendarEvent.java new file mode 100644 index 0000000..00b9429 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/CalendarEvent.java @@ -0,0 +1,178 @@ +package de.mviper.adventskalender; + +import org.bukkit.Material; +import org.bukkit.configuration.file.FileConfiguration; + +import java.time.LocalDate; +import java.time.MonthDay; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Ein Kalender-Event (z.B. Advent, Halloween, Ostern), gelesen aus dem Abschnitt events. der config.yml. + */ +public final class CalendarEvent { + + public static final String ADVENT = "advent"; + private static final Pattern EASTER = Pattern.compile("(?i)easter\\s*([+-]\\s*\\d+)?"); + + private final String id; + private final boolean enabled; + private final int days; + private final int graceDays; + private final Boolean globalOverride; + private final MonthDay fixedStart; + private final Integer easterOffset; + private final Material closedMaterial; + private final Material openedMaterial; + private final String closedTexture; + private final String openedTexture; + private final int perRow; + private final int[] slots; + private volatile int testDay; + + public CalendarEvent(String id, FileConfiguration cfg, int testDay) { + this.id = id.toLowerCase(); + String p = "events." + id + "."; + + this.enabled = cfg.getBoolean(p + "enabled", true); + this.days = Math.max(1, Math.min(36, cfg.getInt(p + "days", 24))); + this.graceDays = Math.max(0, cfg.getInt(p + "grace_days", 0)); + Object g = cfg.get(p + "global"); + this.globalOverride = (g instanceof Boolean b) ? b : null; + this.closedMaterial = material(cfg.getString(p + "closed_material", "CHEST"), Material.CHEST); + this.openedMaterial = material(cfg.getString(p + "opened_material", "ENDER_CHEST"), Material.ENDER_CHEST); + this.closedTexture = cfg.getString(p + "closed_texture", ""); + this.openedTexture = cfg.getString(p + "opened_texture", ""); + this.testDay = testDay; + + String start = cfg.getString(p + "start", "12-01").trim(); + Matcher m = EASTER.matcher(start); + if (m.matches()) { + this.fixedStart = null; + this.easterOffset = m.group(1) == null ? 0 : Integer.parseInt(m.group(1).replaceAll("\\s", "")); + } else { + try { + this.fixedStart = MonthDay.parse("--" + start); + } catch (Exception e) { + throw new IllegalArgumentException("Ungültiger Startwert '" + start + "' (erwartet MM-dd oder easter-13)"); + } + this.easterOffset = null; + } + + // Slots: bis 28 Tage mit Rand links/rechts (7 pro Reihe), darüber 9 pro Reihe. Letzte Reihe zentriert. + this.perRow = days <= 28 ? 7 : 9; + int colStart = perRow == 7 ? 1 : 0; + this.slots = new int[days]; + int rows = (days + perRow - 1) / perRow; + int d = 0; + for (int r = 0; r < rows; r++) { + int n = Math.min(perRow, days - d); + int offset = (perRow - n) / 2; + for (int i = 0; i < n; i++) { + slots[d++] = (1 + r) * 9 + colStart + offset + i; + } + } + } + + private static Material material(String name, Material fallback) { + Material m = name == null ? null : Material.matchMaterial(name); + return (m != null && m.isItem() && !m.isAir()) ? m : fallback; + } + + // ------------------------------------------------------------- Datum + + /** Ostersonntag (Gaußsche Osterformel, gregorianisch). */ + public static LocalDate easter(int year) { + int a = year % 19, b = year / 100, c = year % 100; + int d = b / 4, e = b % 4, f = (b + 8) / 25, g = (b - f + 1) / 3; + int h = (19 * a + b - d - g + 15) % 30; + int i = c / 4, k = c % 4; + int l = (32 + 2 * e + 2 * i - h - k) % 7; + int m = (a + 11 * h + 22 * l) / 451; + int month = (h + l - 7 * m + 114) / 31; + int day = ((h + l - 7 * m + 114) % 31) + 1; + return LocalDate.of(year, month, day); + } + + private LocalDate startFor(int year) { + return easterOffset != null ? easter(year).plusDays(easterOffset) : fixedStart.atYear(year); + } + + /** Start des laufenden oder des nächsten Zeitfensters. */ + private LocalDate relevantStart(LocalDate today) { + for (int y = today.getYear() - 1; y <= today.getYear() + 1; y++) { + LocalDate s = startFor(y); + if (today.isBefore(s.plusDays(days + graceDays))) return s; + } + return startFor(today.getYear() + 1); + } + + /** Aktueller Tag (1..days), oder 0 außerhalb des Zeitraums. Berücksichtigt den Testmodus. */ + public int currentDay() { + if (testDay > 0) return Math.min(days, testDay); + LocalDate today = LocalDate.now(Adventskalender.getInstance().getZone()); + long offset = ChronoUnit.DAYS.between(relevantStart(today), today); + if (offset < 0) return 0; + return (int) Math.min(days, offset + 1); + } + + /** Kalenderdatum eines Türchens, formatiert für die Sprache aus der Config. */ + public String formatDate(int day) { + LocalDate today = LocalDate.now(Adventskalender.getInstance().getZone()); + LocalDate date = relevantStart(today).plusDays(day - 1); + String lang = Adventskalender.getInstance().getConfig().getString("general.language", "de"); + Locale locale = Locale.forLanguageTag(lang); + return date.format(DateTimeFormatter.ofPattern(lang.equalsIgnoreCase("de") ? "d. MMMM" : "MMMM d", locale)); + } + + // ----------------------------------------------------- Config-Pfade + + public boolean isAdvent() { return ADVENT.equals(id); } + + public String titleKey() { return isAdvent() ? "gui.title" : "events." + id + ".title"; } + + public String nameKey(int day) { return rewardLangKey(day) + ".name"; } + + public String loreKey(int day) { return rewardLangKey(day) + ".lore"; } + + private String rewardLangKey(int day) { + return (isAdvent() ? "rewards" : "events." + id + ".rewards") + ".day_" + day; + } + + public String rewardsPath() { return isAdvent() ? "rewards" : "events." + id + ".rewards"; } + + public boolean isGlobal() { + if (globalOverride != null) return globalOverride; + return Adventskalender.getInstance().getConfig().getBoolean("calendar.use_global_calendar", false); + } + + // ------------------------------------------------------------ Layout + + public int dayForSlot(int slot) { + for (int i = 0; i < slots.length; i++) { + if (slots[i] == slot) return i + 1; + } + return -1; + } + + public int slotForDay(int day) { return slots[day - 1]; } + + /** true, wenn die Randspalten links/rechts als Dekoration frei bleiben. */ + public boolean hasSideBorder() { return perRow == 7; } + + // ---------------------------------------------------------- Getter + + public String getId() { return id; } + public boolean isEnabled() { return enabled; } + public int getDays() { return days; } + public Material getClosedMaterial() { return closedMaterial; } + public Material getOpenedMaterial() { return openedMaterial; } + public String getClosedTexture() { return closedTexture; } + public String getOpenedTexture() { return openedTexture; } + public int getTestDay() { return testDay; } + public void setTestDay(int day) { this.testDay = day; } +} diff --git a/src/main/java/de/mviper/adventskalender/DataHandler.java b/src/main/java/de/mviper/adventskalender/DataHandler.java index 6020e1c..ab82407 100644 --- a/src/main/java/de/mviper/adventskalender/DataHandler.java +++ b/src/main/java/de/mviper/adventskalender/DataHandler.java @@ -1,241 +1,478 @@ -package de.mviper.adventskalender; - -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import java.io.File; -import java.io.IOException; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.HashSet; - -public class DataHandler { - - private static File file; - private static FileConfiguration dataConfig; - private static boolean mysqlActive = false; - - public static void setup() { - if (Adventskalender.getInstance().getConfig().getBoolean("mysql.enable")) { - MySQLManager.connect(); - if (MySQLManager.isConnected()) { - mysqlActive = true; - Adventskalender.getInstance().getLogger().info("MySQL-Verbindung steht. data.yml wird deaktiviert."); - return; - } else { - Adventskalender.getInstance().getLogger().warning("MySQL aktiviert, aber Verbindung fehlgeschlagen! Nutze data.yml als Backup."); - } - } - - if (!Adventskalender.getInstance().getDataFolder().exists()) { - Adventskalender.getInstance().getDataFolder().mkdirs(); - } - - file = new File(Adventskalender.getInstance().getDataFolder(), "data.yml"); - - if (!file.exists()) { - try { - file.createNewFile(); - } catch (IOException e) { - e.printStackTrace(); - } - } - dataConfig = YamlConfiguration.loadConfiguration(file); - } - - public static boolean hasClaimed(Player player, int day) { - if (isGlobal()) { - return GlobalDataManager.isDayClaimedGlobally(day); - } - - if (mysqlActive && MySQLManager.isConnected()) { - try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement("SELECT 1 FROM advent_players WHERE uuid = ? AND day = ?")) { - ps.setString(1, player.getUniqueId().toString()); - ps.setInt(2, day); - return ps.executeQuery().next(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - if (dataConfig != null) { - List claimed = dataConfig.getIntegerList("players." + player.getUniqueId() + ".claimed"); - return claimed.contains(day); - } - return false; - } - - public static void setClaimed(Player player, int day) { - if (isGlobal()) { - GlobalDataManager.setDayClaimedGlobally(day, player.getName()); - return; - } - - if (mysqlActive && MySQLManager.isConnected()) { - try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement("INSERT IGNORE INTO advent_players (uuid, day) VALUES (?, ?)")) { - ps.setString(1, player.getUniqueId().toString()); - ps.setInt(2, day); - ps.executeUpdate(); - return; - } catch (SQLException e) { - e.printStackTrace(); - } - } - - if (dataConfig != null) { - List claimed = dataConfig.getIntegerList("players." + player.getUniqueId() + ".claimed"); - if (!claimed.contains(day)) { - claimed.add(day); - dataConfig.set("players." + player.getUniqueId() + ".claimed", claimed); - dataConfig.set("players." + player.getUniqueId() + ".name", player.getName()); - save(); - } - } - } - - public static void resetPlayerDay(String playerName, int day) { - if (mysqlActive && MySQLManager.isConnected()) { - @SuppressWarnings("deprecation") - OfflinePlayer target = Bukkit.getOfflinePlayer(playerName); - try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement("DELETE FROM advent_players WHERE uuid = ? AND day = ?")) { - ps.setString(1, target.getUniqueId().toString()); - ps.setInt(2, day); - ps.executeUpdate(); - } catch (SQLException e) { e.printStackTrace(); } - } - if (dataConfig != null && dataConfig.getConfigurationSection("players") != null) { - for (String uuid : dataConfig.getConfigurationSection("players").getKeys(false)) { - if (playerName.equalsIgnoreCase(dataConfig.getString("players." + uuid + ".name"))) { - List claimed = dataConfig.getIntegerList("players." + uuid + ".claimed"); - claimed.remove(Integer.valueOf(day)); - dataConfig.set("players." + uuid + ".claimed", claimed); - save(); - break; - } - } - } - } - - public static Set getClaimedDays(Player player) { - if (mysqlActive && MySQLManager.isConnected()) { - Set days = new HashSet<>(); - try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement("SELECT day FROM advent_players WHERE uuid = ?")) { - ps.setString(1, player.getUniqueId().toString()); - ResultSet rs = ps.executeQuery(); - while (rs.next()) days.add(rs.getInt("day")); - return days; - } catch (SQLException e) { - e.printStackTrace(); - } - } - - if (dataConfig != null) { - return new HashSet<>(dataConfig.getIntegerList("players." + player.getUniqueId() + ".claimed")); - } - return new HashSet<>(); - } - - public static void resetPlayer(Player player) { - if (mysqlActive && MySQLManager.isConnected()) { - try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement("DELETE FROM advent_players WHERE uuid = ?")) { - ps.setString(1, player.getUniqueId().toString()); - ps.executeUpdate(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - if (dataConfig != null) { - dataConfig.set("players." + player.getUniqueId(), null); - save(); - } - } - - public static void resetOfflinePlayer(String name) { - if (dataConfig == null || dataConfig.getConfigurationSection("players") == null) return; - for (String uuid : dataConfig.getConfigurationSection("players").getKeys(false)) { - String savedName = dataConfig.getString("players." + uuid + ".name"); - if (name.equalsIgnoreCase(savedName)) { - dataConfig.set("players." + uuid, null); - save(); - break; - } - } - } - - public static void resetAll() { - if (mysqlActive && MySQLManager.isConnected()) { - try (PreparedStatement ps1 = MySQLManager.getConnection().prepareStatement("DELETE FROM advent_players"); - PreparedStatement ps2 = MySQLManager.getConnection().prepareStatement("DELETE FROM advent_global")) { - ps1.executeUpdate(); - ps2.executeUpdate(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - if (dataConfig != null) { - dataConfig.set("players", null); - dataConfig.set("global", null); - save(); - } - } - - private static boolean isGlobal() { - return Adventskalender.getInstance().getConfig().getBoolean("calendar.use_global_calendar", false); - } - - private static void save() { - if (dataConfig == null || file == null) return; - try { - dataConfig.save(file); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public static class GlobalDataManager { - public static boolean isDayClaimedGlobally(int day) { - if (mysqlActive && MySQLManager.isConnected()) { - try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement("SELECT 1 FROM advent_global WHERE day = ?")) { - ps.setInt(1, day); - return ps.executeQuery().next(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - return dataConfig != null && dataConfig.contains("global.day_" + day); - } - - public static void setDayClaimedGlobally(int day, String playerName) { - if (mysqlActive && MySQLManager.isConnected()) { - try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement("REPLACE INTO advent_global (day, player_name) VALUES (?, ?)")) { - ps.setInt(1, day); - ps.setString(2, playerName); - ps.executeUpdate(); - } catch (SQLException e) { - e.printStackTrace(); - } - } else if (dataConfig != null) { - dataConfig.set("global.day_" + day, playerName); - save(); - } - } - - public static void resetGlobalDay(int day) { - if (mysqlActive && MySQLManager.isConnected()) { - try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement("DELETE FROM advent_global WHERE day = ?")) { - ps.setInt(1, day); - ps.executeUpdate(); - } catch (SQLException e) { e.printStackTrace(); } - } else if (dataConfig != null) { - dataConfig.set("global.day_" + day, null); - DataHandler.save(); - } - } - } -} \ No newline at end of file +package de.mviper.adventskalender; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import java.io.File; +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +/** + * Alle Zugriffe sind synchronisiert, damit sie sicher aus Async-Tasks aufgerufen werden können. + * Pro Event werden die Daten getrennt gespeichert. Das Event "advent" nutzt weiterhin die + * bisherigen Pfade/Zeilen, damit bestehende Daten erhalten bleiben. + */ +public class DataHandler { + + public enum ClaimResult { SUCCESS, ALREADY_CLAIMED, ERROR } + + /** Geburtstag eines Spielers ohne Jahr. claimedYear = Jahr des zuletzt abgeholten Geschenks (0 = nie). */ + public record Birthday(int month, int day, int claimedYear) {} + + /** Wird bei Datenbankfehlern geworfen, damit Aufrufer "nicht vorhanden" und "Fehler" unterscheiden können. */ + public static class DataException extends RuntimeException { + public DataException(String message) { super(message); } + } + + private static File file; + private static FileConfiguration dataConfig; + private static boolean mysqlActive = false; + + public static synchronized void setup() { + mysqlActive = false; + dataConfig = null; + file = null; + MySQLManager.close(); + + if (Adventskalender.getInstance().getConfig().getBoolean("mysql.enable")) { + MySQLManager.connect(); + if (MySQLManager.isConnected()) { + mysqlActive = true; + Adventskalender.getInstance().getLogger().info("MySQL-Verbindung steht. data.yml wird deaktiviert."); + return; + } else { + Adventskalender.getInstance().getLogger().warning("MySQL aktiviert, aber Verbindung fehlgeschlagen! Nutze data.yml als Backup."); + } + } + + if (!Adventskalender.getInstance().getDataFolder().exists()) { + Adventskalender.getInstance().getDataFolder().mkdirs(); + } + + file = new File(Adventskalender.getInstance().getDataFolder(), "data.yml"); + + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + Adventskalender.getInstance().getLogger().severe("data.yml konnte nicht erstellt werden: " + e.getMessage()); + } + } + dataConfig = YamlConfiguration.loadConfiguration(file); + } + + /** false, wenn der Datenspeicher gerade nicht benutzbar ist (z.B. MySQL ausgefallen). */ + public static synchronized boolean isReady() { + if (mysqlActive) return MySQLManager.ensureConnected(); + return dataConfig != null; + } + + private static void logSql(SQLException e) { + Adventskalender.getInstance().getLogger().severe("Datenbankfehler: " + e.getMessage()); + } + + // ------------------------------------------------------------ Pfade / SQL-Helfer + + private static String playersPath(CalendarEvent ev) { + return ev.isAdvent() ? "players" : "events." + ev.getId() + ".players"; + } + + private static String globalPath(CalendarEvent ev) { + return ev.isAdvent() ? "global" : "events." + ev.getId() + ".global"; + } + + /** Ein Event, oder alle konfigurierten Events bei null. */ + private static List eventsOf(CalendarEvent ev) { + return ev != null ? List.of(ev) : EventManager.allConfigured(); + } + + private static int exec(String sql, Object... params) throws SQLException { + try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement(sql)) { + for (int i = 0; i < params.length; i++) ps.setObject(i + 1, params[i]); + return ps.executeUpdate(); + } + } + + private static Set queryDays(String sql, Object... params) throws SQLException { + Set days = new HashSet<>(); + try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement(sql)) { + for (int i = 0; i < params.length; i++) ps.setObject(i + 1, params[i]); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) days.add(rs.getInt("day")); + } + } + return days; + } + + // ------------------------------------------------------------------ Lesen + + /** Tage, die für den Spieler im GUI als geöffnet gelten (global oder pro Spieler). null = Fehler. */ + public static synchronized Set getViewClaimedDays(Player player, CalendarEvent ev) { + if (!isReady()) return null; + try { + return ev.isGlobal() ? loadGlobalDays(ev) : loadPlayerDays(player.getUniqueId().toString(), ev); + } catch (SQLException e) { + logSql(e); + return null; + } + } + + private static Set loadPlayerDays(String uuid, CalendarEvent ev) throws SQLException { + if (mysqlActive) { + return queryDays("SELECT day FROM advent_players WHERE uuid = ? AND event = ?", uuid, ev.getId()); + } + return new HashSet<>(dataConfig.getIntegerList(playersPath(ev) + "." + uuid + ".claimed")); + } + + private static Set loadGlobalDays(CalendarEvent ev) throws SQLException { + if (mysqlActive) { + return queryDays("SELECT day FROM advent_global WHERE event = ?", ev.getId()); + } + Set days = new HashSet<>(); + ConfigurationSection section = dataConfig.getConfigurationSection(globalPath(ev)); + if (section != null) { + for (String key : section.getKeys(false)) { + try { + days.add(Integer.parseInt(key.replace("day_", ""))); + } catch (NumberFormatException ignored) { + } + } + } + return days; + } + + // ---------------------------------------------------------------- Schreiben + + /** Markiert einen Tag atomar als abgeholt. Verhindert Doppelabholung auch bei gleichzeitigen Klicks. */ + public static synchronized ClaimResult setClaimed(Player player, CalendarEvent ev, int day) { + if (!isReady()) return ClaimResult.ERROR; + + if (ev.isGlobal()) { + return claimGlobal(ev, day, player.getName()); + } + + String uuid = player.getUniqueId().toString(); + if (mysqlActive) { + try { + int rows = exec("INSERT IGNORE INTO advent_players (uuid, event, day, name) VALUES (?, ?, ?, ?)", + uuid, ev.getId(), day, player.getName()); + return rows > 0 ? ClaimResult.SUCCESS : ClaimResult.ALREADY_CLAIMED; + } catch (SQLException e) { + logSql(e); + return ClaimResult.ERROR; + } + } + + String base = playersPath(ev) + "." + uuid; + List claimed = new ArrayList<>(dataConfig.getIntegerList(base + ".claimed")); + if (claimed.contains(day)) return ClaimResult.ALREADY_CLAIMED; + claimed.add(day); + dataConfig.set(base + ".claimed", claimed); + dataConfig.set(base + ".name", player.getName()); + return save() ? ClaimResult.SUCCESS : ClaimResult.ERROR; + } + + private static ClaimResult claimGlobal(CalendarEvent ev, int day, String playerName) { + if (mysqlActive) { + try { + int rows = exec("INSERT IGNORE INTO advent_global (event, day, player_name) VALUES (?, ?, ?)", + ev.getId(), day, playerName); + return rows > 0 ? ClaimResult.SUCCESS : ClaimResult.ALREADY_CLAIMED; + } catch (SQLException e) { + logSql(e); + return ClaimResult.ERROR; + } + } + String path = globalPath(ev) + ".day_" + day; + if (dataConfig.contains(path)) return ClaimResult.ALREADY_CLAIMED; + dataConfig.set(path, playerName); + return save() ? ClaimResult.SUCCESS : ClaimResult.ERROR; + } + + /** Setzt einen Tag für einen Spieler (per Name, auch offline) zurück. ev = null: in allen Events. */ + public static synchronized boolean resetPlayerDay(String playerName, CalendarEvent ev, int day) { + boolean found = false; + if (mysqlActive && MySQLManager.ensureConnected()) { + String uuid = lookupUuid(playerName); + try { + if (ev != null) { + found |= exec("DELETE FROM advent_players WHERE day = ? AND (name = ? OR uuid = ?) AND event = ?", + day, playerName, uuid, ev.getId()) > 0; + } else { + found |= exec("DELETE FROM advent_players WHERE day = ? AND (name = ? OR uuid = ?)", + day, playerName, uuid) > 0; + } + } catch (SQLException e) { + logSql(e); + } + } + if (dataConfig != null) { + for (CalendarEvent e : eventsOf(ev)) { + ConfigurationSection section = dataConfig.getConfigurationSection(playersPath(e)); + if (section == null) continue; + for (String uuid : section.getKeys(false)) { + String base = playersPath(e) + "." + uuid; + if (playerName.equalsIgnoreCase(dataConfig.getString(base + ".name"))) { + List claimed = new ArrayList<>(dataConfig.getIntegerList(base + ".claimed")); + found |= claimed.remove(Integer.valueOf(day)); + dataConfig.set(base + ".claimed", claimed); + save(); + break; + } + } + } + } + return found; + } + + /** Löscht alle Daten eines (online) Spielers. ev = null: in allen Events. */ + public static synchronized void resetPlayer(Player player, CalendarEvent ev) { + String uuid = player.getUniqueId().toString(); + if (mysqlActive && MySQLManager.ensureConnected()) { + try { + if (ev != null) { + exec("DELETE FROM advent_players WHERE uuid = ? AND event = ?", uuid, ev.getId()); + } else { + exec("DELETE FROM advent_players WHERE uuid = ?", uuid); + } + } catch (SQLException e) { + logSql(e); + } + } + if (dataConfig != null) { + for (CalendarEvent e : eventsOf(ev)) { + dataConfig.set(playersPath(e) + "." + uuid, null); + } + save(); + } + } + + /** Löscht alle Daten eines Spielers per Name (auch offline). ev = null: in allen Events. */ + public static synchronized boolean resetOfflinePlayer(String name, CalendarEvent ev) { + boolean found = false; + if (mysqlActive && MySQLManager.ensureConnected()) { + String uuid = lookupUuid(name); + try { + if (ev != null) { + found |= exec("DELETE FROM advent_players WHERE (name = ? OR uuid = ?) AND event = ?", + name, uuid, ev.getId()) > 0; + } else { + found |= exec("DELETE FROM advent_players WHERE name = ? OR uuid = ?", name, uuid) > 0; + } + } catch (SQLException e) { + logSql(e); + } + } + if (dataConfig != null) { + for (CalendarEvent e : eventsOf(ev)) { + ConfigurationSection section = dataConfig.getConfigurationSection(playersPath(e)); + if (section == null) continue; + for (String uuid : section.getKeys(false)) { + if (name.equalsIgnoreCase(dataConfig.getString(playersPath(e) + "." + uuid + ".name"))) { + dataConfig.set(playersPath(e) + "." + uuid, null); + found = true; + break; + } + } + } + save(); + } + return found; + } + + /** Löscht alle Spieler- und globalen Daten. ev = null: aller Events. */ + public static synchronized void resetAll(CalendarEvent ev) { + if (mysqlActive && MySQLManager.ensureConnected()) { + try { + if (ev != null) { + exec("DELETE FROM advent_players WHERE event = ?", ev.getId()); + exec("DELETE FROM advent_global WHERE event = ?", ev.getId()); + } else { + exec("DELETE FROM advent_players"); + exec("DELETE FROM advent_global"); + } + } catch (SQLException e) { + logSql(e); + } + } + if (dataConfig != null) { + for (CalendarEvent e : eventsOf(ev)) { + dataConfig.set(playersPath(e), null); + dataConfig.set(globalPath(e), null); + } + save(); + } + } + + /** Gibt einen global abgeholten Tag wieder frei. ev = null: in allen Events. */ + public static synchronized void resetGlobalDay(CalendarEvent ev, int day) { + if (mysqlActive && MySQLManager.ensureConnected()) { + try { + if (ev != null) { + exec("DELETE FROM advent_global WHERE day = ? AND event = ?", day, ev.getId()); + } else { + exec("DELETE FROM advent_global WHERE day = ?", day); + } + } catch (SQLException e) { + logSql(e); + } + } + if (dataConfig != null) { + for (CalendarEvent e : eventsOf(ev)) { + dataConfig.set(globalPath(e) + ".day_" + day, null); + } + save(); + } + } + + // ---------------------------------------------------------------- Geburtstage + + public static synchronized Optional getBirthday(UUID uuid) { + if (!isReady()) throw new DataException("Datenspeicher nicht verfügbar"); + if (mysqlActive) { + try (PreparedStatement ps = MySQLManager.getConnection().prepareStatement( + "SELECT month, day, claimed_year FROM advent_birthdays WHERE uuid = ?")) { + ps.setString(1, uuid.toString()); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return Optional.empty(); + return Optional.of(new Birthday(rs.getInt("month"), rs.getInt("day"), rs.getInt("claimed_year"))); + } + } catch (SQLException e) { + logSql(e); + throw new DataException(e.getMessage()); + } + } + String base = "birthdays." + uuid; + if (!dataConfig.contains(base + ".month")) return Optional.empty(); + return Optional.of(new Birthday(dataConfig.getInt(base + ".month"), dataConfig.getInt(base + ".day"), + dataConfig.getInt(base + ".claimed_year", 0))); + } + + /** Speichert den Geburtstag. onlyIfAbsent: nichts überschreiben. Gibt zurück, ob geschrieben wurde. */ + public static synchronized boolean setBirthday(UUID uuid, String name, int month, int day, boolean onlyIfAbsent) { + if (!isReady()) throw new DataException("Datenspeicher nicht verfügbar"); + if (mysqlActive) { + try { + if (onlyIfAbsent) { + return exec("INSERT IGNORE INTO advent_birthdays (uuid, name, month, day) VALUES (?, ?, ?, ?)", + uuid.toString(), name, month, day) > 0; + } + exec("INSERT INTO advent_birthdays (uuid, name, month, day) VALUES (?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE name = VALUES(name), month = VALUES(month), day = VALUES(day)", + uuid.toString(), name, month, day); + return true; + } catch (SQLException e) { + logSql(e); + throw new DataException(e.getMessage()); + } + } + String base = "birthdays." + uuid; + if (onlyIfAbsent && dataConfig.contains(base + ".month")) return false; + dataConfig.set(base + ".name", name); + dataConfig.set(base + ".month", month); + dataConfig.set(base + ".day", day); + if (!save()) throw new DataException("data.yml konnte nicht gespeichert werden"); + return true; + } + + /** Löscht einen Geburtstag per UUID oder Name (einer von beiden darf null sein). */ + public static synchronized boolean removeBirthday(UUID uuid, String name) { + if (!isReady()) throw new DataException("Datenspeicher nicht verfügbar"); + boolean found = false; + if (mysqlActive) { + try { + found = exec("DELETE FROM advent_birthdays WHERE uuid = ? OR name = ?", + uuid != null ? uuid.toString() : "", name != null ? name : "") > 0; + } catch (SQLException e) { + logSql(e); + throw new DataException(e.getMessage()); + } + return found; + } + ConfigurationSection section = dataConfig.getConfigurationSection("birthdays"); + if (section == null) return false; + for (String key : section.getKeys(false)) { + boolean match = (uuid != null && key.equals(uuid.toString())) + || (name != null && name.equalsIgnoreCase(dataConfig.getString("birthdays." + key + ".name"))); + if (match) { + dataConfig.set("birthdays." + key, null); + found = true; + break; + } + } + if (found) save(); + return found; + } + + /** Markiert das Geschenk für das Geburtstagsjahr atomar als abgeholt. */ + public static synchronized ClaimResult claimBirthday(UUID uuid, int year) { + if (!isReady()) return ClaimResult.ERROR; + if (mysqlActive) { + try { + int rows = exec("UPDATE advent_birthdays SET claimed_year = ? WHERE uuid = ? AND claimed_year < ?", + year, uuid.toString(), year); + return rows > 0 ? ClaimResult.SUCCESS : ClaimResult.ALREADY_CLAIMED; + } catch (SQLException e) { + logSql(e); + return ClaimResult.ERROR; + } + } + String path = "birthdays." + uuid + ".claimed_year"; + if (dataConfig.getInt(path, 0) >= year) return ClaimResult.ALREADY_CLAIMED; + dataConfig.set(path, year); + return save() ? ClaimResult.SUCCESS : ClaimResult.ERROR; + } + + /** Macht die Abholung rückgängig (Admin-Reset bzw. wenn der Spieler vor der Vergabe offline ging). */ + public static synchronized boolean resetBirthdayClaim(UUID uuid, String name) { + if (!isReady()) return false; + if (mysqlActive) { + try { + return exec("UPDATE advent_birthdays SET claimed_year = 0 WHERE uuid = ? OR name = ?", + uuid != null ? uuid.toString() : "", name != null ? name : "") > 0; + } catch (SQLException e) { + logSql(e); + return false; + } + } + ConfigurationSection section = dataConfig.getConfigurationSection("birthdays"); + if (section == null) return false; + for (String key : section.getKeys(false)) { + boolean match = (uuid != null && key.equals(uuid.toString())) + || (name != null && name.equalsIgnoreCase(dataConfig.getString("birthdays." + key + ".name"))); + if (match) { + dataConfig.set("birthdays." + key + ".claimed_year", 0); + return save(); + } + } + return false; + } + + @SuppressWarnings("deprecation") + private static String lookupUuid(String name) { + OfflinePlayer target = Bukkit.getOfflinePlayer(name); + return target.getUniqueId().toString(); + } + + private static boolean save() { + if (dataConfig == null || file == null) return false; + try { + dataConfig.save(file); + return true; + } catch (IOException e) { + Adventskalender.getInstance().getLogger().severe("data.yml konnte nicht gespeichert werden: " + e.getMessage()); + return false; + } + } +} diff --git a/src/main/java/de/mviper/adventskalender/EventManager.java b/src/main/java/de/mviper/adventskalender/EventManager.java new file mode 100644 index 0000000..64ff058 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/EventManager.java @@ -0,0 +1,82 @@ +package de.mviper.adventskalender; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Verwaltet alle konfigurierten Kalender-Events. */ +public final class EventManager { + + private static volatile Map events = new LinkedHashMap<>(); + + private EventManager() {} + + public static void load() { + Adventskalender plugin = Adventskalender.getInstance(); + FileConfiguration cfg = plugin.getConfig(); + int startTestDay = Math.max(0, cfg.getInt("general.test_day", 0)); + + // IDs aus der Config des Servers und den mitgelieferten Standardwerten (Standard zuerst) + Set ids = new LinkedHashSet<>(); + if (cfg.getDefaults() != null) { + ConfigurationSection def = cfg.getDefaults().getConfigurationSection("events"); + if (def != null) ids.addAll(def.getKeys(false)); + } + ConfigurationSection user = cfg.getConfigurationSection("events"); + if (user != null) ids.addAll(user.getKeys(false)); + if (ids.isEmpty()) ids.add(CalendarEvent.ADVENT); + + Map loaded = new LinkedHashMap<>(); + for (String id : ids) { + try { + CalendarEvent ev = new CalendarEvent(id, cfg, startTestDay); + loaded.put(ev.getId(), ev); + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("Event '" + id + "' wird übersprungen: " + e.getMessage()); + } + } + events = loaded; + } + + /** Alle aktivierten Events. */ + public static List all() { + List list = new ArrayList<>(); + for (CalendarEvent ev : events.values()) { + if (ev.isEnabled()) list.add(ev); + } + return Collections.unmodifiableList(list); + } + + /** Alle Events, auch deaktivierte (z.B. für Admin-Resets). */ + public static List allConfigured() { + return Collections.unmodifiableList(new ArrayList<>(events.values())); + } + + /** Aktiviertes Event per ID, sonst null. */ + public static CalendarEvent get(String id) { + CalendarEvent ev = id == null ? null : events.get(id.toLowerCase()); + return (ev != null && ev.isEnabled()) ? ev : null; + } + + /** Event per ID, auch wenn es deaktiviert ist. */ + public static CalendarEvent getAny(String id) { + return id == null ? null : events.get(id.toLowerCase()); + } + + /** Das Event, das gerade läuft; sonst das erste aktivierte. null, wenn keines aktiviert ist. */ + public static CalendarEvent defaultEvent() { + CalendarEvent first = null; + for (CalendarEvent ev : all()) { + if (first == null) first = ev; + if (ev.currentDay() > 0) return ev; + } + return first; + } +} diff --git a/src/main/java/de/mviper/adventskalender/LanguageManager.java b/src/main/java/de/mviper/adventskalender/LanguageManager.java index de8ce20..d38f359 100644 --- a/src/main/java/de/mviper/adventskalender/LanguageManager.java +++ b/src/main/java/de/mviper/adventskalender/LanguageManager.java @@ -1,35 +1,76 @@ -package de.mviper.adventskalender; - -import org.bukkit.ChatColor; -import org.bukkit.configuration.file.YamlConfiguration; -import java.io.File; -import java.util.List; -import java.util.stream.Collectors; - -public class LanguageManager { - private static YamlConfiguration cfg; - private static String prefix; - - public static void setup() { - String lang = Adventskalender.getInstance().getConfig().getString("general.language", "de"); - File f = new File(Adventskalender.getInstance().getDataFolder(), "messages_" + lang + ".yml"); - if (!f.exists()) Adventskalender.getInstance().saveResource("messages_" + lang + ".yml", false); - cfg = YamlConfiguration.loadConfiguration(f); - prefix = ChatColor.translateAlternateColorCodes('&', cfg.getString("prefix", "&6[Advent] ")); - } - - public static String getString(String path) { - String s = cfg.getString(path); - return s == null ? "§cKey error: " + path : ChatColor.translateAlternateColorCodes('&', s); - } - - public static String getMessage(String path) { - return prefix + getString(path); - } - - public static List getStringList(String path) { - return cfg.getStringList(path).stream() - .map(s -> ChatColor.translateAlternateColorCodes('&', s)) - .collect(Collectors.toList()); - } -} \ No newline at end of file +package de.mviper.adventskalender; + +import org.bukkit.ChatColor; +import org.bukkit.configuration.file.YamlConfiguration; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +public class LanguageManager { + private static YamlConfiguration cfg; + private static String prefix; + + public static void setup() { + Adventskalender plugin = Adventskalender.getInstance(); + String lang = plugin.getConfig().getString("general.language", "de").toLowerCase(); + if (!lang.matches("[a-z_]+")) lang = "de"; + + String resName = "messages_" + lang + ".yml"; + File f = new File(plugin.getDataFolder(), resName); + if (!f.exists() && plugin.getResource(resName) == null) { + plugin.getLogger().warning("Sprachdatei '" + resName + "' existiert nicht - nutze 'de'."); + resName = "messages_de.yml"; + f = new File(plugin.getDataFolder(), resName); + } + if (!f.exists()) plugin.saveResource(resName, false); + cfg = YamlConfiguration.loadConfiguration(f); + + // Fehlende Schlüssel (z.B. nach einem Update) aus der mitgelieferten Datei ergänzen + try (InputStream in = plugin.getResource(resName)) { + if (in != null) { + cfg.setDefaults(YamlConfiguration.loadConfiguration(new InputStreamReader(in, StandardCharsets.UTF_8))); + } + } catch (IOException ignored) { + } + + prefix = ChatColor.translateAlternateColorCodes('&', cfg.getString("prefix", "&6[Advent] ")); + } + + public static String getString(String path) { + String s = cfg.getString(path); + return s == null ? "§cKey error: " + path : ChatColor.translateAlternateColorCodes('&', s); + } + + public static boolean has(String path) { + return cfg.contains(path); + } + + public static String getMessage(String path) { + return prefix + getString(path); + } + + /** Nachricht mit dem Präfix aus prefixKey (z.B. "birthday.prefix"); fehlt er, gilt das Standard-Präfix. */ + public static String getMessage(String path, String prefixKey) { + return prefixOf(prefixKey) + getString(path); + } + + /** Nachricht mit dem Präfix des Events (z.B. [Halloween]); der Advent nutzt das Standard-Präfix. */ + public static String getMessage(String path, CalendarEvent event) { + return getMessage(path, event == null || event.isAdvent() ? null : "events." + event.getId() + ".prefix"); + } + + private static String prefixOf(String prefixKey) { + if (prefixKey == null || !cfg.contains(prefixKey)) return prefix; + return ChatColor.translateAlternateColorCodes('&', cfg.getString(prefixKey, "")); + } + + public static List getStringList(String path) { + return cfg.getStringList(path).stream() + .map(s -> ChatColor.translateAlternateColorCodes('&', s)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/de/mviper/adventskalender/MySQLManager.java b/src/main/java/de/mviper/adventskalender/MySQLManager.java index 3252a8a..a96df8e 100644 --- a/src/main/java/de/mviper/adventskalender/MySQLManager.java +++ b/src/main/java/de/mviper/adventskalender/MySQLManager.java @@ -1,66 +1,104 @@ -package de.mviper.adventskalender; - -import org.bukkit.configuration.file.FileConfiguration; -import java.sql.*; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -public class MySQLManager { - - private static Connection connection; - - public static void connect() { - FileConfiguration config = Adventskalender.getInstance().getConfig(); - String host = config.getString("mysql.host"); - int port = config.getInt("mysql.port"); - String database = config.getString("mysql.database"); - String user = config.getString("mysql.user"); - String password = config.getString("mysql.password"); - - try { - if (connection != null && !connection.isClosed()) return; - - synchronized (MySQLManager.class) { - Class.forName("com.mysql.jdbc.Driver"); - connection = DriverManager.getConnection("jdbc:mysql://" + host + ":" + port + "/" + database + "?autoReconnect=true&useSSL=false", user, password); - } - - createTables(); - Adventskalender.getInstance().getLogger().info("MySQL-Verbindung erfolgreich hergestellt!"); - } catch (Exception e) { - Adventskalender.getInstance().getLogger().severe("MySQL-Verbindung fehlgeschlagen: " + e.getMessage()); - } - } - - private static void createTables() { - try (Statement s = connection.createStatement()) { - // Tabelle für Einzelspieler-Daten - s.executeUpdate("CREATE TABLE IF NOT EXISTS advent_players (uuid VARCHAR(36), day INT, PRIMARY KEY(uuid, day))"); - // Tabelle für globalen Modus - s.executeUpdate("CREATE TABLE IF NOT EXISTS advent_global (day INT PRIMARY KEY, player_name VARCHAR(16))"); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public static boolean isConnected() { - try { - return connection != null && !connection.isClosed(); - } catch (SQLException e) { - return false; - } - } - - public static void close() { - try { - if (isConnected()) connection.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public static Connection getConnection() { - return connection; - } -} \ No newline at end of file +package de.mviper.adventskalender; + +import org.bukkit.configuration.file.FileConfiguration; +import java.sql.*; + +public class MySQLManager { + + private static Connection connection; + + /** Baut die Verbindung (neu) auf. Eine bestehende Verbindung wird vorher geschlossen. */ + public static synchronized void connect() { + FileConfiguration config = Adventskalender.getInstance().getConfig(); + String host = config.getString("mysql.host"); + int port = config.getInt("mysql.port"); + String database = config.getString("mysql.database"); + String user = config.getString("mysql.user"); + String password = config.getString("mysql.password"); + + close(); + try { + loadDriver(); + connection = DriverManager.getConnection( + "jdbc:mysql://" + host + ":" + port + "/" + database + "?useSSL=false&characterEncoding=utf8", user, password); + createTables(); + Adventskalender.getInstance().getLogger().info("MySQL-Verbindung erfolgreich hergestellt!"); + } catch (Exception e) { + connection = null; + Adventskalender.getInstance().getLogger().severe("MySQL-Verbindung fehlgeschlagen: " + e.getMessage()); + } + } + + private static void loadDriver() throws ClassNotFoundException { + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + } catch (ClassNotFoundException e) { + Class.forName("com.mysql.jdbc.Driver"); + } + } + + private static void createTables() { + try (Statement s = connection.createStatement()) { + // Tabelle für Einzelspieler-Daten + s.executeUpdate("CREATE TABLE IF NOT EXISTS advent_players (uuid VARCHAR(36), event VARCHAR(32) NOT NULL DEFAULT 'advent', day INT, name VARCHAR(32), PRIMARY KEY(uuid, event, day))"); + // Tabelle für globalen Modus + s.executeUpdate("CREATE TABLE IF NOT EXISTS advent_global (event VARCHAR(32) NOT NULL DEFAULT 'advent', day INT, player_name VARCHAR(32), PRIMARY KEY(event, day))"); + // Tabelle für Spieler-Geburtstage (ohne Jahr) + s.executeUpdate("CREATE TABLE IF NOT EXISTS advent_birthdays (uuid VARCHAR(36) PRIMARY KEY, name VARCHAR(32), month INT, day INT, claimed_year INT NOT NULL DEFAULT 0)"); + } catch (SQLException e) { + Adventskalender.getInstance().getLogger().severe("MySQL-Tabellen konnten nicht erstellt werden: " + e.getMessage()); + } + // Migrationen für Tabellen aus älteren Versionen (Fehler = Spalte existiert schon) + migrate("ALTER TABLE advent_players ADD COLUMN name VARCHAR(32)", null); + migrate("ALTER TABLE advent_players ADD COLUMN event VARCHAR(32) NOT NULL DEFAULT 'advent'", + "ALTER TABLE advent_players DROP PRIMARY KEY, ADD PRIMARY KEY (uuid, event, day)"); + migrate("ALTER TABLE advent_global ADD COLUMN event VARCHAR(32) NOT NULL DEFAULT 'advent'", + "ALTER TABLE advent_global DROP PRIMARY KEY, ADD PRIMARY KEY (event, day)"); + } + + /** Führt eine Migration aus; followUp läuft nur, wenn sie erfolgreich war. */ + private static void migrate(String sql, String followUp) { + try (Statement s = connection.createStatement()) { + s.executeUpdate(sql); + } catch (SQLException ignored) { + return; + } + if (followUp == null) return; + try (Statement s = connection.createStatement()) { + s.executeUpdate(followUp); + } catch (SQLException e) { + Adventskalender.getInstance().getLogger().severe("MySQL-Migration fehlgeschlagen: " + e.getMessage()); + } + } + + public static synchronized boolean isConnected() { + try { + return connection != null && !connection.isClosed(); + } catch (SQLException e) { + return false; + } + } + + /** Prüft die Verbindung und baut sie bei Bedarf neu auf. */ + public static synchronized boolean ensureConnected() { + try { + if (connection != null && connection.isValid(2)) return true; + } catch (SQLException ignored) { + } + connect(); + return connection != null; + } + + public static synchronized void close() { + try { + if (connection != null) connection.close(); + } catch (SQLException e) { + Adventskalender.getInstance().getLogger().warning("MySQL-Verbindung konnte nicht sauber geschlossen werden: " + e.getMessage()); + } + connection = null; + } + + public static synchronized Connection getConnection() { + return connection; + } +} diff --git a/src/main/java/de/mviper/adventskalender/RewardUtil.java b/src/main/java/de/mviper/adventskalender/RewardUtil.java new file mode 100644 index 0000000..9d6500d --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/RewardUtil.java @@ -0,0 +1,73 @@ +package de.mviper.adventskalender; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import java.util.List; +import java.util.Map; + +/** Gemeinsame Logik zum Bauen und Übergeben von Belohnungs-Items aus der Config. */ +public final class RewardUtil { + + private RewardUtil() {} + + /** + * Baut ein Item aus dem Config-Abschnitt (material, amount, enchantments). + * Gibt null zurück, wenn das Material ungültig ist. + */ + public static ItemStack build(String path, String label) { + Adventskalender plugin = Adventskalender.getInstance(); + String matName = plugin.getConfig().getString(path + ".material", "COOKIE"); + int amount = Math.max(1, plugin.getConfig().getInt(path + ".amount", 1)); + + Material material = Material.matchMaterial(matName); + if (material == null || !material.isItem() || material.isAir()) { + plugin.getLogger().warning("Ungültiges Material '" + matName + "' für " + label + " in der config.yml!"); + return null; + } + + ItemStack reward = new ItemStack(material, amount); + ItemMeta meta = reward.getItemMeta(); + + if (meta != null) { + List> enchants = plugin.getConfig().getMapList(path + ".enchantments"); + for (Map entry : enchants) { + Object typeObj = entry.get("type"); + if (typeObj == null) continue; + String type = String.valueOf(typeObj); + Object levelObj = entry.get("level"); + int level = (levelObj instanceof Number n) ? n.intValue() : 1; + + Enchantment enc = findEnchantment(type); + if (enc != null) { + meta.addEnchant(enc, level, true); + } else { + plugin.getLogger().warning("Unbekannte Verzauberung '" + type + "' für " + label + " in der config.yml!"); + } + } + reward.setItemMeta(meta); + } + return reward; + } + + /** Sucht eine Verzauberung über ihren Minecraft-Key (z.B. SHARPNESS), mit Fallback auf alte Bukkit-Namen. */ + @SuppressWarnings("deprecation") + private static Enchantment findEnchantment(String type) { + String name = type.trim(); + NamespacedKey key = NamespacedKey.fromString(name.toLowerCase()); + Enchantment enc = key != null ? Enchantment.getByKey(key) : null; + return enc != null ? enc : Enchantment.getByName(name.toUpperCase()); + } + + /** Legt das Item ins Inventar, Überschuss wird gedroppt. Gibt true zurück, wenn etwas gedroppt wurde. */ + public static boolean give(Player player, ItemStack reward) { + Map leftover = player.getInventory().addItem(reward); + for (ItemStack item : leftover.values()) { + player.getWorld().dropItemNaturally(player.getLocation(), item); + } + return !leftover.isEmpty(); + } +} diff --git a/src/main/java/de/mviper/adventskalender/UpdateChecker.java b/src/main/java/de/mviper/adventskalender/UpdateChecker.java index 8bd9e39..845af87 100644 --- a/src/main/java/de/mviper/adventskalender/UpdateChecker.java +++ b/src/main/java/de/mviper/adventskalender/UpdateChecker.java @@ -2,9 +2,9 @@ package de.mviper.adventskalender; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; -import java.io.IOException; import java.io.InputStream; -import java.net.URL; +import java.net.URI; +import java.net.URLConnection; import java.util.Scanner; import java.util.function.Consumer; @@ -20,14 +20,18 @@ public class UpdateChecker { public void getVersion(final Consumer consumer) { Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { - try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId).openStream(); - Scanner scann = new Scanner(is)) { - if (scann.hasNext()) { - consumer.accept(scann.next()); + try { + URLConnection connection = URI.create("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId).toURL().openConnection(); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + try (InputStream is = connection.getInputStream(); Scanner scann = new Scanner(is)) { + if (scann.hasNext()) { + consumer.accept(scann.next()); + } } - } catch (IOException e) { + } catch (Exception e) { plugin.getLogger().info("Update-Check fehlgeschlagen: " + e.getMessage()); } }); } -} \ No newline at end of file +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index e58782d..97df5b0 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -8,8 +8,12 @@ general: # Verfügbare Optionen: 'de', 'en' language: "de" -# Nur für Testzwecke: Simuliert einen Tag (0 = echtes Datum verwenden) - test_day: 0 + # Zeitzone für das Datum, z.B. "Europe/Berlin". Leer = Zeitzone des Servers. + timezone: "" + + # Nur für Testzwecke: Simuliert einen Tag beim Start (0 = echtes Datum verwenden) + # Mit /ak admin test kann der Tag zur Laufzeit geändert werden (wird nicht gespeichert). + test_day: 0 # --- Kalender-Einstellungen --- calendar: @@ -208,4 +212,275 @@ rewards: material: FIREWORK_ROCKET amount: 64 key: "rewards.day_24" - enchantments: [] \ No newline at end of file + enchantments: [] + +# ============================================================================= +# Events (Kalender) +# ============================================================================= +# Jedes Event hat einen eigenen Kalender, eigene Belohnungen und eigene Daten. +# Öffnen mit /ak (laufendes Event) oder /ak . +# +# enabled: Event an/aus +# days: Anzahl der Türchen (1-36) +# start: Startdatum im Format MM-dd (z.B. "10-01") ODER relativ zu Ostersonntag +# (z.B. "easter-13" = 13 Tage vor Ostersonntag, "easter+1" = Ostermontag). +# Das Datum wird jedes Jahr automatisch berechnet. +# grace_days: Wie viele Tage nach dem letzten Türchen der Kalender noch geöffnet bleibt. +# global: (optional) true/false überschreibt calendar.use_global_calendar nur für dieses Event. +# closed_material / opened_material: Items für geschlossene bzw. geöffnete Türchen. +# Die Stackgröße zeigt den Tag an. (Das Advent-Event nutzt eigene Köpfe.) +# closed_texture / opened_texture: (optional) Textur-URL eines Kopfes statt der Materialien. +# +# Die Texte (Titel, Namen, Lore) stehen in der Sprachdatei unter events.. +# Die Belohnungen des Advent-Events stehen weiterhin oben unter "rewards". +events: + advent: + enabled: true + days: 24 + start: "12-01" + grace_days: 7 + + halloween: + enabled: true + days: 31 + start: "10-01" + grace_days: 0 + closed_material: PUMPKIN + opened_material: JACK_O_LANTERN + rewards: + 1: + material: PUMPKIN_PIE + amount: 8 + enchantments: [] + 2: + material: JACK_O_LANTERN + amount: 4 + enchantments: [] + 3: + material: BONE + amount: 16 + enchantments: [] + 4: + material: SPIDER_EYE + amount: 8 + enchantments: [] + 5: + material: COBWEB + amount: 16 + enchantments: [] + 6: + material: BLACK_CANDLE + amount: 4 + enchantments: [] + 7: + material: SOUL_TORCH + amount: 16 + enchantments: [] + 8: + material: FERMENTED_SPIDER_EYE + amount: 4 + enchantments: [] + 9: + material: GHAST_TEAR + amount: 2 + enchantments: [] + 10: + material: SOUL_LANTERN + amount: 4 + enchantments: [] + 11: + material: PHANTOM_MEMBRANE + amount: 4 + enchantments: [] + 12: + material: REDSTONE + amount: 32 + enchantments: [] + 13: + material: GLOWSTONE_DUST + amount: 16 + enchantments: [] + 14: + material: BLAZE_ROD + amount: 4 + enchantments: [] + 15: + material: ENDER_PEARL + amount: 4 + enchantments: [] + 16: + material: SKELETON_SKULL + amount: 1 + enchantments: [] + 17: + material: ZOMBIE_HEAD + amount: 1 + enchantments: [] + 18: + material: CREEPER_HEAD + amount: 1 + enchantments: [] + 19: + material: GOLDEN_APPLE + amount: 3 + enchantments: [] + 20: + material: DIAMOND_SWORD + amount: 1 + enchantments: + - type: SMITE + level: 5 + - type: UNBREAKING + level: 3 + 21: + material: BOW + amount: 1 + enchantments: + - type: POWER + level: 4 + - type: INFINITY + level: 1 + 22: + material: NETHERITE_SCRAP + amount: 4 + enchantments: [] + 23: + material: WITHER_ROSE + amount: 2 + enchantments: [] + 24: + material: EXPERIENCE_BOTTLE + amount: 16 + enchantments: [] + 25: + material: TOTEM_OF_UNDYING + amount: 1 + enchantments: [] + 26: + material: NAME_TAG + amount: 2 + enchantments: [] + 27: + material: MUSIC_DISC_13 + amount: 1 + enchantments: [] + 28: + material: DIAMOND + amount: 8 + enchantments: [] + 29: + material: WITHER_SKELETON_SKULL + amount: 1 + enchantments: [] + 30: + material: NETHER_STAR + amount: 1 + enchantments: [] + 31: + material: NETHERITE_INGOT + amount: 3 + enchantments: [] + + easter: + enabled: true + days: 14 + start: "easter-13" + grace_days: 7 + closed_material: EGG + opened_material: TURTLE_EGG + rewards: + 1: + material: EGG + amount: 16 + enchantments: [] + 2: + material: CARROT + amount: 16 + enchantments: [] + 3: + material: GOLDEN_CARROT + amount: 8 + enchantments: [] + 4: + material: RABBIT_FOOT + amount: 2 + enchantments: [] + 5: + material: COOKIE + amount: 16 + enchantments: [] + 6: + material: HONEY_BOTTLE + amount: 4 + enchantments: [] + 7: + material: CAKE + amount: 1 + enchantments: [] + 8: + material: PINK_TULIP + amount: 8 + enchantments: [] + 9: + material: SADDLE + amount: 1 + enchantments: [] + 10: + material: GOLDEN_APPLE + amount: 2 + enchantments: [] + 11: + material: FIREWORK_ROCKET + amount: 16 + enchantments: [] + 12: + material: DIAMOND + amount: 5 + enchantments: [] + 13: + material: ENCHANTED_GOLDEN_APPLE + amount: 1 + enchantments: [] + 14: + material: GOLD_BLOCK + amount: 4 + enchantments: [] + +# ============================================================================= +# Geburtstage +# ============================================================================= +# Spieler tragen ihren Geburtstag (ohne Jahr) mit /ak birthday set ein und holen +# ihr Geschenk mit /ak birthday claim ab. Einmal pro Jahr, am Geburtstag und bis zu +# grace_days Tage danach. +birthday: + enabled: true + # Wie viele Tage nach dem Geburtstag das Geschenk noch abholbar ist. + grace_days: 2 + # Dürfen Spieler ihren Geburtstag nach dem Eintragen selbst ändern/löschen? + # false = nur Admins (/ak admin birthday set|remove ), verhindert Mehrfach-Abholung. + allow_change: false + # Nachricht an alle Spieler, wenn jemand sein Geschenk abholt. + broadcast: true + # Konsolenbefehle, die zusätzlich ausgeführt werden (%player% = Spielername), z.B. "eco give %player% 500" + commands: [] + rewards: + 1: + material: CAKE + amount: 1 + enchantments: [] + 2: + material: COOKIE + amount: 16 + enchantments: [] + 3: + material: GOLDEN_APPLE + amount: 3 + enchantments: [] + 4: + material: DIAMOND + amount: 5 + enchantments: [] + 5: + material: FIREWORK_ROCKET + amount: 16 + enchantments: [] diff --git a/src/main/resources/messages_de.yml b/src/main/resources/messages_de.yml index 8d9b0fd..6a75642 100644 --- a/src/main/resources/messages_de.yml +++ b/src/main/resources/messages_de.yml @@ -9,6 +9,13 @@ prefix: "&6[Adventskalender] &r" gui: # Der Titel des Adventskalender-Inventars. title: "&6✦ Adventskalender 2026 ✦" + default_name: "&6Türchen #%day%" + click_open: "&8» &eKlicke zum Öffnen!" + locked_name: "&c&lTag #%day%" + locked_status: "&8» &7Noch verschlossen" + locked_hint: "&7Komme am %date% wieder!" + claimed_status: "&8» &6Bereits abgeholt" + claimed_hint: "&aDu hast dieses Geschenk bereits geöffnet! ✔" # --- Nachrichten an Spieler --- # Verfügbare Platzhalter: @@ -22,16 +29,34 @@ messages: day_not_available: "&cDieses Türchen ist noch fest verschlossen. Gedulde dich noch etwas!" day_already_claimed: "&7Du hast dieses Geschenk bereits abgeholt." global_already_claimed: "&cDieses Geschenk wurde heute bereits von jemand anderem abgeholt!" + no_events: "&cEs ist derzeit kein Kalender aktiv." + unknown_event: "&cUnbekanntes Event '%event%'. Verfügbar: &e%events%" + db_error: "&cDie Datenbank ist gerade nicht erreichbar. Bitte versuche es später erneut." + reward_error: "&cDie Belohnung für Tag %day% ist falsch konfiguriert. Bitte melde das einem Admin." # --- Admin-Nachrichten --- admin: + all_events: "alle Events" reload_success: "&aKonfiguration und Sprachdateien wurden erfolgreich neu geladen." - test_mode_on: "&e[Admin] &7Der Test-Modus wurde auf Tag &6%day% &7gesetzt." - test_mode_off: "&e[Admin] &7Test-Modus deaktiviert. Es wird wieder das reale Datum genutzt." + test_mode_on: "&e[Admin] &7Test-Modus (%event%): Tag &6%day% &7gesetzt." + test_mode_off: "&e[Admin] &7Test-Modus (%event%) deaktiviert. Es wird wieder das reale Datum genutzt." player_not_found: "&cDer Spieler '%player%' wurde nicht gefunden." invalid_day: "&cUngültiger Tag. Bitte wähle eine Zahl zwischen 1 und 24." + reset_day_global: "&a[Admin] &7Tag &6%day% &7global zurückgesetzt (%event%)." + reset_day_player: "&a[Admin] &7Tag &6%day% &7für &e%player% &7zurückgesetzt (%event%)." + reset_all: "&a[Admin] &7Kompletter Reset durchgeführt (%event%)." + reset_player: "&a[Admin] &7Daten für &e%player% &7gelöscht (%event%)." + reset_offline: "&a[Admin] &7Offline-Daten für &e%player% &7gelöscht (%event%)." + help: + - "&8&m---&r &6Advent Admin &8&m---" + - "&e/ak admin reload &7- Config neu laden" + - "&e/ak admin test [event] &7- Datum simulieren" + - "&e/ak admin resetday [event] &7- Tag löschen" + - "&e/ak admin reset [event] &7- Alles löschen" + - "&7Ohne [event] gilt der Befehl für alle Events." + - "&e/ak admin birthday [TT.MM] &7- Geburtstage verwalten" -# --- Belohnungs-Texte im GUI --- +# --- Belohnungs-Texte im GUI (Advent) --- # Diese Schlüssel werden vom AdventInventory genutzt, um die Items im Menü zu benennen. rewards: day_1: @@ -105,4 +130,178 @@ rewards: lore: ["&7Nicht nur für den Ackerbau..."] day_24: name: "&cTag 24: Das große Finale!" - lore: ["&7Frohe Weihnachten!", "&7Feiere mit einem riesigen Feuerwerk!"] \ No newline at end of file + lore: ["&7Frohe Weihnachten!", "&7Feiere mit einem riesigen Feuerwerk!"] + +# --- Weitere Events: Titel und Belohnungs-Texte --- +events: + halloween: + prefix: "&6[Halloween] &r" + title: "&6✦ Halloween-Kalender ✦" + rewards: + day_1: + name: "&6Kürbiskuchen" + lore: ["&7Süßes oder Saures?"] + day_2: + name: "&6Leuchtende Kürbisse" + lore: ["&7Gegen die Dunkelheit."] + day_3: + name: "&fKnochenvorrat" + lore: ["&7Für den treuen Wolf."] + day_4: + name: "&cSpinnenaugen" + lore: ["&7Zutat für düstere Tränke."] + day_5: + name: "&fSpinnennetze" + lore: ["&7Gruselige Deko für dein Haus."] + day_6: + name: "&8Schwarze Kerzen" + lore: ["&7Für das perfekte Ritual."] + day_7: + name: "&bSeelenfackeln" + lore: ["&7Blaues Licht in der Nacht."] + day_8: + name: "&5Gegorene Spinnenaugen" + lore: ["&7Nur für mutige Brauer."] + day_9: + name: "&fGeisterträne" + lore: ["&7Ein Hauch vom Jenseits."] + day_10: + name: "&bSeelenlaternen" + lore: ["&7Sie leuchten für verlorene Seelen."] + day_11: + name: "&7Phantom-Membranen" + lore: ["&7Beute der nächtlichen Jagd."] + day_12: + name: "&cHexenstaub" + lore: ["&7Funkelt unheimlich rot."] + day_13: + name: "&eZauberstaub" + lore: ["&7Ein Hauch von Magie."] + day_14: + name: "&6Lohenruten" + lore: ["&7Heiß und gefährlich."] + day_15: + name: "&5Enderperlen" + lore: ["&7Verschwinde wie ein Geist."] + day_16: + name: "&fSkelettschädel" + lore: ["&7Ein Totenkopf als Deko."] + day_17: + name: "&2Zombiekopf" + lore: ["&7Er starrt dich an..."] + day_18: + name: "&aCreeperkopf" + lore: ["&7Zzzzisch... nur Deko."] + day_19: + name: "&6Goldäpfel" + lore: ["&7Stärkung nach der Nacht."] + day_20: + name: "&bGeisterjäger-Schwert" + lore: ["&7Schlag zu gegen Untote."] + day_21: + name: "&fKnochenbogen" + lore: ["&7Schieß die Geister weg."] + day_22: + name: "&8Netherit-Schrott" + lore: ["&7Aus den Tiefen des Nethers."] + day_23: + name: "&8Wither-Rosen" + lore: ["&7Schön und tödlich."] + day_24: + name: "&aErfahrungsfläschchen" + lore: ["&7Schnell Level sammeln."] + day_25: + name: "&dTotem des Unvergänglichen" + lore: ["&7Gib dem Tod keine Chance."] + day_26: + name: "&eGrusel-Namensschilder" + lore: ["&7Benenne deine Kreaturen."] + day_27: + name: "&fSchallplatte - 13" + lore: ["&7Unheimliche Klänge."] + day_28: + name: "&bDiamanten" + lore: ["&7Ein glänzender Schatz."] + day_29: + name: "&8Wither-Skelettschädel" + lore: ["&7Beschwört nichts Gutes."] + day_30: + name: "&eNetherstern" + lore: ["&7Das Herz der Nacht."] + day_31: + name: "&c&lHalloween-Finale!" + lore: ["&7Netherit zum Gruseln - frohes Halloween!"] + easter: + prefix: "&e[Ostern] &r" + title: "&e✦ Oster-Kalender ✦" + rewards: + day_1: + name: "&fOstereier" + lore: ["&7Frisch vom Hasen."] + day_2: + name: "&6Möhren" + lore: ["&7Des Hasen Lieblingsessen."] + day_3: + name: "&eGoldene Karotten" + lore: ["&7Ein edler Snack."] + day_4: + name: "&fHasenpfote" + lore: ["&7Bringt Glück!"] + day_5: + name: "&6Schoko-Kekse" + lore: ["&7Süße Osterleckerei."] + day_6: + name: "&eHonigflaschen" + lore: ["&7Süß wie der Frühling."] + day_7: + name: "&dOsterkuchen" + lore: ["&7Mit viel Liebe gebacken."] + day_8: + name: "&dFrühlingsblumen" + lore: ["&7Ein bunter Strauß."] + day_9: + name: "&6Osterhasen-Sattel" + lore: ["&7Hoppel los!"] + day_10: + name: "&6Goldäpfel" + lore: ["&7Eine Belohnung für Eiersucher."] + day_11: + name: "&cFrühlingsfeuerwerk" + lore: ["&7Lass es knallen!"] + day_12: + name: "&bDiamanten" + lore: ["&7Im Osternest versteckt."] + day_13: + name: "&6Verzauberter Goldapfel" + lore: ["&7Der Preis fürs Eiersuchen."] + day_14: + name: "&e&lDas goldene Osterei!" + lore: ["&7Frohe Ostern!"] + +# --- Geburtstage --- +birthday: + prefix: "&6[Geburtstag] &r" + disabled: "&cGeburtstags-Geschenke sind deaktiviert." + help: + - "&8&m---&r &6Geburtstag &8&m---" + - "&e/ak birthday set &7- Geburtstag eintragen (z.B. 15.03)" + - "&e/ak birthday claim &7- Geschenk abholen (am Geburtstag und %days% Tage danach)" + invalid_date: "&cUngültiges Datum. Bitte nutze das Format &eTT.MM &c(z.B. 15.03)." + set_success: "&aDein Geburtstag wurde auf den &e%date% &agesetzt. Am Tag selbst und %days% Tage danach kannst du dir mit &e/ak birthday claim &adein Geschenk abholen!" + already_set: "&cDu hast deinen Geburtstag bereits eingetragen (&e%date%&c). Bei Fehlern wende dich an einen Admin." + info: "&7Dein Geburtstag: &e%date%" + info_none: "&7Du hast noch keinen Geburtstag eingetragen: &e/ak birthday set " + removed: "&7Dein Geburtstag wurde gelöscht." + change_not_allowed: "&cDu kannst deinen Geburtstag nicht selbst ändern. Bitte wende dich an einen Admin." + not_set: "&cDu hast noch keinen Geburtstag eingetragen. Nutze &e/ak birthday set &c." + not_today: "&cDein Geburtstag (&e%date%&c) ist gerade nicht. Das Geschenk gibt es am Tag selbst und %days% Tage danach." + already_claimed: "&7Du hast dein Geburtstagsgeschenk dieses Jahr schon abgeholt." + reward_received: "&6&lAlles Gute zum Geburtstag! &eViel Spaß mit deinem Geschenk!" + broadcast: "&6%player% &efeiert Geburtstag - alles Gute!" + join_notice: "&6&lAlles Gute zum Geburtstag! &eKlicke hier, um dein Geschenk abzuholen!" + join_hover: "&7Führt /ak birthday claim aus" + reward_error: "&cDas Geburtstagsgeschenk ist falsch konfiguriert. Bitte melde das einem Admin." + admin_set: "&a[Admin] &7Geburtstag von &e%player% &7auf &6%date% &7gesetzt." + admin_removed: "&a[Admin] &7Geburtstag von &e%player% &7gelöscht." + admin_reset: "&a[Admin] &7Geschenk-Abholung von &e%player% &7zurückgesetzt." + admin_none: "&cFür &e%player% &cist kein Geburtstag eingetragen." diff --git a/src/main/resources/messages_en.yml b/src/main/resources/messages_en.yml index 10c9154..9fabd95 100644 --- a/src/main/resources/messages_en.yml +++ b/src/main/resources/messages_en.yml @@ -9,6 +9,13 @@ prefix: "&6[Advent] &r" gui: # The title of the Advent Calendar inventory. title: "&6✦ Advent Calendar 2026 ✦" + default_name: "&6Door #%day%" + click_open: "&8» &eClick to open!" + locked_name: "&c&lDay #%day%" + locked_status: "&8» &7Still locked" + locked_hint: "&7Come back on %date%!" + claimed_status: "&8» &6Already claimed" + claimed_hint: "&aYou have already opened this gift! ✔" # --- Messages to Players --- # Available placeholders: @@ -22,14 +29,32 @@ messages: day_not_available: "&cThis door is still locked. You'll have to wait a bit longer!" day_already_claimed: "&7You have already claimed this gift." global_already_claimed: "&cThis gift has already been claimed by someone else today!" + no_events: "&cNo calendar is active right now." + unknown_event: "&cUnknown event '%event%'. Available: &e%events%" + db_error: "&cThe database is currently unavailable. Please try again later." + reward_error: "&cThe reward for day %day% is misconfigured. Please report this to an admin." # --- Admin Messages --- admin: + all_events: "all events" reload_success: "&aConfiguration and language files have been reloaded." - test_mode_on: "&e[Admin] &7Test mode set to day &6%day%&7." - test_mode_off: "&e[Admin] &7Test mode disabled. Using real-time date again." + test_mode_on: "&e[Admin] &7Test mode (%event%) set to day &6%day%&7." + test_mode_off: "&e[Admin] &7Test mode (%event%) disabled. Using real-time date again." player_not_found: "&cThe player '%player%' was not found." invalid_day: "&cInvalid day. Please choose a number between 1 and 24." + reset_day_global: "&a[Admin] &7Day &6%day% &7reset globally (%event%)." + reset_day_player: "&a[Admin] &7Day &6%day% &7reset for &e%player%&7 (%event%)." + reset_all: "&a[Admin] &7Full reset completed (%event%)." + reset_player: "&a[Admin] &7Data for &e%player% &7deleted (%event%)." + reset_offline: "&a[Admin] &7Offline data for &e%player% &7deleted (%event%)." + help: + - "&8&m---&r &6Advent Admin &8&m---" + - "&e/ak admin reload &7- Reload config" + - "&e/ak admin test [event] &7- Simulate date" + - "&e/ak admin resetday [event] &7- Reset a day" + - "&e/ak admin reset [event] &7- Reset everything" + - "&7Without [event] the command applies to all events." + - "&e/ak admin birthday [DD.MM] &7- Manage birthdays" # --- Reward Texts in GUI --- rewards: @@ -104,4 +129,178 @@ rewards: lore: ["&7Not just for farming..."] day_24: name: "&cDay 24: The Grand Finale!" - lore: ["&7Merry Christmas!", "&7Celebrate with a huge firework!"] \ No newline at end of file + lore: ["&7Merry Christmas!", "&7Celebrate with a huge firework!"] + +# --- More events: titles and reward texts --- +events: + halloween: + prefix: "&6[Halloween] &r" + title: "&6✦ Halloween Calendar ✦" + rewards: + day_1: + name: "&6Pumpkin Pie" + lore: ["&7Trick or treat?"] + day_2: + name: "&6Jack o'Lanterns" + lore: ["&7Against the darkness."] + day_3: + name: "&fBone Stash" + lore: ["&7For your loyal wolf."] + day_4: + name: "&cSpider Eyes" + lore: ["&7An ingredient for dark potions."] + day_5: + name: "&fCobwebs" + lore: ["&7Spooky decoration for your home."] + day_6: + name: "&8Black Candles" + lore: ["&7For the perfect ritual."] + day_7: + name: "&bSoul Torches" + lore: ["&7Blue light in the night."] + day_8: + name: "&5Fermented Spider Eyes" + lore: ["&7Only for brave brewers."] + day_9: + name: "&fGhost Tears" + lore: ["&7A breath from the beyond."] + day_10: + name: "&bSoul Lanterns" + lore: ["&7They shine for lost souls."] + day_11: + name: "&7Phantom Membranes" + lore: ["&7Loot from the night hunt."] + day_12: + name: "&cWitch's Dust" + lore: ["&7Glows an eerie red."] + day_13: + name: "&eMagic Dust" + lore: ["&7A touch of magic."] + day_14: + name: "&6Blaze Rods" + lore: ["&7Hot and dangerous."] + day_15: + name: "&5Ender Pearls" + lore: ["&7Vanish like a ghost."] + day_16: + name: "&fSkeleton Skull" + lore: ["&7A skull as decoration."] + day_17: + name: "&2Zombie Head" + lore: ["&7It stares at you..."] + day_18: + name: "&aCreeper Head" + lore: ["&7Ssssss... just decoration."] + day_19: + name: "&6Golden Apples" + lore: ["&7A boost after the night."] + day_20: + name: "&bGhostbuster Sword" + lore: ["&7Strike down the undead."] + day_21: + name: "&fBone Bow" + lore: ["&7Shoot the ghosts away."] + day_22: + name: "&8Netherite Scrap" + lore: ["&7From the depths of the Nether."] + day_23: + name: "&8Wither Roses" + lore: ["&7Beautiful and deadly."] + day_24: + name: "&aBottles o' Enchanting" + lore: ["&7Gain levels fast."] + day_25: + name: "&dTotem of Undying" + lore: ["&7Give death no chance."] + day_26: + name: "&eSpooky Name Tags" + lore: ["&7Name your creatures."] + day_27: + name: "&fMusic Disc - 13" + lore: ["&7Eerie sounds."] + day_28: + name: "&bDiamonds" + lore: ["&7A shiny treasure."] + day_29: + name: "&8Wither Skeleton Skull" + lore: ["&7Summons nothing good."] + day_30: + name: "&eNether Star" + lore: ["&7The heart of the night."] + day_31: + name: "&c&lHalloween Finale!" + lore: ["&7Spooky netherite - Happy Halloween!"] + easter: + prefix: "&e[Easter] &r" + title: "&e✦ Easter Calendar ✦" + rewards: + day_1: + name: "&fEaster Eggs" + lore: ["&7Fresh from the bunny."] + day_2: + name: "&6Carrots" + lore: ["&7The bunny's favorite food."] + day_3: + name: "&eGolden Carrots" + lore: ["&7A noble snack."] + day_4: + name: "&fRabbit's Foot" + lore: ["&7Brings luck!"] + day_5: + name: "&6Chocolate Cookies" + lore: ["&7A sweet Easter treat."] + day_6: + name: "&eHoney Bottles" + lore: ["&7Sweet as spring."] + day_7: + name: "&dEaster Cake" + lore: ["&7Baked with lots of love."] + day_8: + name: "&dSpring Flowers" + lore: ["&7A colorful bouquet."] + day_9: + name: "&6Easter Bunny Saddle" + lore: ["&7Hop to it!"] + day_10: + name: "&6Golden Apples" + lore: ["&7A reward for egg hunters."] + day_11: + name: "&cSpring Fireworks" + lore: ["&7Let it bang!"] + day_12: + name: "&bDiamonds" + lore: ["&7Hidden in the Easter nest."] + day_13: + name: "&6Enchanted Golden Apple" + lore: ["&7The prize for egg hunting."] + day_14: + name: "&e&lThe Golden Easter Egg!" + lore: ["&7Happy Easter!"] + +# --- Birthdays --- +birthday: + prefix: "&6[Birthday] &r" + disabled: "&cBirthday gifts are disabled." + help: + - "&8&m---&r &6Birthday &8&m---" + - "&e/ak birthday set &7- Set your birthday (e.g. 15.03)" + - "&e/ak birthday claim &7- Claim your gift (on your birthday and %days% days after)" + invalid_date: "&cInvalid date. Please use the format &eDD.MM &c(e.g. 15.03)." + set_success: "&aYour birthday has been set to &e%date%&a. On the day and for %days% days after you can claim your gift with &e/ak birthday claim&a!" + already_set: "&cYou have already set your birthday (&e%date%&c). Contact an admin if it is wrong." + info: "&7Your birthday: &e%date%" + info_none: "&7You have not set a birthday yet: &e/ak birthday set " + removed: "&7Your birthday has been deleted." + change_not_allowed: "&cYou cannot change your birthday yourself. Please contact an admin." + not_set: "&cYou have not set a birthday yet. Use &e/ak birthday set &c." + not_today: "&cYour birthday (&e%date%&c) is not right now. The gift is available on the day and for %days% days after." + already_claimed: "&7You have already claimed your birthday gift this year." + reward_received: "&6&lHappy Birthday! &eEnjoy your gift!" + broadcast: "&6%player% &eis celebrating a birthday - happy birthday!" + join_notice: "&6&lHappy Birthday! &eClick here to claim your gift!" + join_hover: "&7Runs /ak birthday claim" + reward_error: "&cThe birthday gift is misconfigured. Please report this to an admin." + admin_set: "&a[Admin] &7Birthday of &e%player% &7set to &6%date%&7." + admin_removed: "&a[Admin] &7Birthday of &e%player% &7deleted." + admin_reset: "&a[Admin] &7Gift claim of &e%player% &7reset." + admin_none: "&cNo birthday is set for &e%player%&c." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index db3e22b..0a3a87d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,23 +1,26 @@ -name: Adventskalender -version: '1.0.2' -main: de.mviper.adventskalender.Adventskalender -api-version: 1.20 -author: M_Viper -description: Ein moderner, performanter Adventskalender (2026 Edition). - -softdepend: - - PlaceholderAPI - -commands: - adventskalender: - description: "Öffnet den Adventskalender oder führt Admin-Befehle aus." - aliases: [advent, kalender, ak] - usage: "/adventskalender" - -permissions: - adventskalender.use: - description: "Erlaubt dem Spieler, den Adventskalender zu benutzen." - default: true - adventskalender.admin: - description: "Erlaubt den Zugriff auf alle Admin-Befehle." +name: Adventskalender +version: '1.0.3' +main: de.mviper.adventskalender.Adventskalender +api-version: 1.20 +author: M_Viper +description: Ein moderner, performanter Adventskalender (2026 Edition). + +softdepend: + - PlaceholderAPI + +commands: + adventskalender: + description: "Öffnet den Adventskalender oder führt Admin-Befehle aus." + aliases: [advent, kalender, ak] + usage: "/adventskalender" + +permissions: + adventskalender.use: + description: "Erlaubt dem Spieler, den Adventskalender zu benutzen." + default: true + adventskalender.birthday: + description: "Erlaubt dem Spieler, seinen Geburtstag einzutragen und das Geburtstagsgeschenk abzuholen." + default: true + adventskalender.admin: + description: "Erlaubt den Zugriff auf alle Admin-Befehle." default: op \ No newline at end of file