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 |
-
-
-
-
-
-
+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.
-
-
-
-*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) |
-
+**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).
-
-
-[](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