Upload via GUI (19 Dateien)

This commit is contained in:
2026-11-03 19:10:19 +00:00
parent 420b6fcc8e
commit 208ec12e0c
19 changed files with 3587 additions and 1109 deletions
+504 -71
View File
@@ -1,124 +1,557 @@
<div align="center">
# 🎄 Adventskalender – Event-Kalender & Geburtstage für Minecraft
# ❄️ Minecraft Adventskalender ❄️
Ein Plugin für deinen Minecraft-Server, das Spielern jeden Tag ein Geschenk gibt.
Es hat **drei Kalender** und ein **Geburtstags-Geschenk**:
*Bringe festliche Stimmung auf deinen Server mit einem hochgradig anpassbaren System*
| Was | Wann | Türchen |
|---|---|---|
| 🎄 **Adventskalender** | ab 1. Dezember | 24 |
| 🎃 **Halloween-Kalender** | ab 1. Oktober | 31 |
| 🐰 **Oster-Kalender** | 13 Tage vor Ostersonntag bis Ostersonntag | 14 |
| 🎂 **Geburtstags-Geschenk** | am Geburtstag des Spielers + 2 Tage danach | 1 pro Jahr |
![Version](https://img.shields.io/badge/Version-1.18.x--1.21.x-green.svg)
![Java](https://img.shields.io/badge/Java-17+-orange.svg)
![Update Checker](https://img.shields.io/badge/Update--Checker-Klickbar-blue.svg)
![Type](https://img.shields.io/badge/Type-Event-red.svg)
</div>
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
<div align="center">
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.
<img src="https://git.viper.ipv64.net/M_Viper/Advenskalender/raw/branch/main/img/03.png" width="400" alt="Kalender GUI 1">
<img src="https://git.viper.ipv64.net/M_Viper/Advenskalender/raw/branch/main/img/04.png" width="400" alt="Kalender GUI 2">
*Die intuitive Kalender-Oberfläche mit allen 24 Türchen*
</div>
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 <global | Spieler> <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 <global | Spieler>`**
- **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`.
<div align="center">
| Befehl | Was er macht |
|---|---|
| `/ak admin reload` | Lädt Config und Sprachdateien neu, ohne Neustart |
| `/ak admin test [event] <Tag>` | 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] <Spieler>` | 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] <Spieler> <Tag>` | Macht **ein** Türchen für einen Spieler wieder zu |
| `/ak admin resetday [event] global <Tag>` | Macht ein Türchen im Server-weiten Modus wieder frei |
| `/ak admin birthday set <Spieler> <TT.MM>` | Trägt den Geburtstag eines Spielers ein oder ändert ihn |
| `/ak admin birthday remove <Spieler>` | Löscht den Geburtstag eines Spielers |
| `/ak admin birthday reset <Spieler>` | Erlaubt dem Spieler, das Geburtstagsgeschenk **nochmal** abzuholen (zum Testen oder bei Problemen) |
<img src="https://git.viper.ipv64.net/M_Viper/Advenskalender/raw/branch/main/img/02.png" alt="Placeholder Beispiel">
**Beispiele:**
*Ein Schild, das die Placeholder des Plugins anzeigt.*
```
/ak admin test halloween 15 (Halloween-Kalender: so tun, als wäre der 15. Tag)
/ak admin test reset (alle Tests beenden)
/ak admin resetday advent Steve 3 (Steve darf Türchen 3 im Advent nochmal öffnen)
/ak admin reset halloween Steve (Steves Halloween-Daten löschen)
/ak admin birthday set Steve 24.12 (Steves Geburtstag auf den 24.12. setzen)
```
</div>
> 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).
<div align="center">
[![Discord Support](https://img.shields.io/badge/Discord-Support-7289DA?style=for-the-badge&logo=discord)](https://discord.com/invite/FdRs4BRd8D)
*Klicke auf den Button für direkten Support durch die Community!*
</div>
| 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).
---
<div align="center">
## 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:
</div>
```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.
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> <TT.MM>` |
| **Spieler hat sein Geburtstagsgeschenk verloren / Fehler beim Abholen** | `/ak admin birthday reset <Spieler>`, 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! 🎄🎃🐰🎂*
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>de.mviper</groupId>
<artifactId>adventskalender</artifactId>
<version>1.0.2</version>
<version>1.0.3</version>
<packaging>jar</packaging>
<properties>
@@ -4,11 +4,26 @@ import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.time.LocalDate;
import java.time.Month;
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<Integer> days, long time) {}
// Kurzer Cache, damit Scoreboards/Hologramme die Datenbank nicht jede Sekunde abfragen
private final ConcurrentHashMap<String, Cached> cache = new ConcurrentHashMap<>();
@Override
public @NotNull String getIdentifier() {
return "ak";
@@ -21,7 +36,7 @@ public class AdventCalendarExpansion extends PlaceholderExpansion {
@Override
public @NotNull String getVersion() {
return "2.0.0";
return Adventskalender.getInstance().getDescription().getVersion();
}
@Override
@@ -29,39 +44,93 @@ public class AdventCalendarExpansion extends PlaceholderExpansion {
return true; // Expansion bleibt nach PAPI-Reload registriert
}
private Set<Integer> 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<Integer> days = DataHandler.getViewClaimedDays(player, ev);
if (days == null) days = new HashSet<>();
cache.put(key, new Cached(days, now));
return days;
}
private record CachedBirthday(Optional<DataHandler.Birthday> birthday, long time) {}
private final ConcurrentHashMap<UUID, CachedBirthday> 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 "";
// 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);
String id = identifier.toLowerCase();
switch (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(DataHandler.getClaimedDays(player).size());
return String.valueOf(claimedDays(player, ev).size());
// %ak_day% - Der aktuelle Tag des Adventskalenders (1-24)
// %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 DataHandler.hasClaimed(player, currentDay) ? "Ja" : "Nein";
return claimedDays(player, ev).contains(currentDay) ? "Ja" : "Nein";
// %ak_remaining% - Wie viele Türchen sind noch offen (für diesen Spieler)?
// %ak_remaining% - Wie viele Türchen sind noch offen?
case "remaining":
int claimedCount = DataHandler.getClaimedDays(player).size();
return String.valueOf(24 - claimedCount);
return String.valueOf(ev.getDays() - claimedDays(player, ev).size());
// %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 ev.isGlobal() ? "Global" : "Spieler-basiert";
}
return null; // Placeholder unbekannt
@@ -2,14 +2,20 @@ 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"));
@@ -18,143 +24,353 @@ public class AdventCommand implements CommandExecutor, TabCompleter {
// /ak admin reload
if (args.length == 2 && args[1].equalsIgnoreCase("reload")) {
Adventskalender.getInstance().reloadConfig();
plugin.reloadConfig();
plugin.reloadSettings();
LanguageManager.setup();
DataHandler.setup();
AdventInventory.initCache();
sender.sendMessage(LanguageManager.getMessage("messages.admin.reload_success"));
return true;
}
// /ak admin test <Tag/reset>
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.");
// /ak admin test [Event] <Tag/reset>
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<CalendarEvent> 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;
}
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').");
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] <global/Spieler> <Tag>
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;
}
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.");
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;
}
return true;
}
// /ak admin resetday <global/Spieler> <Tag>
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();
final CalendarEvent event = ev;
runAsync(() -> {
if (target.equalsIgnoreCase("global")) {
DataHandler.GlobalDataManager.resetGlobalDay(day);
sender.sendMessage("§a[Admin] §7Tag §6" + day + " §7global zurückgesetzt.");
DataHandler.resetGlobalDay(event, day);
reply(sender, LanguageManager.getString("messages.admin.reset_day_global")
.replace("%day%", String.valueOf(day)).replace("%event%", label(event)));
} else {
DataHandler.resetPlayerDay(target, day);
sender.sendMessage("§a[Admin] §7Tag §6" + day + " §7für §e" + target + " §7zurückgesetzt.");
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));
}
} catch (NumberFormatException e) {
sender.sendMessage("§cTag muss zwischen 1 und 24 liegen.");
}
});
return true;
}
// /ak admin reset <global/Spieler>
// /ak admin reset [Event] <global/Spieler>
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();
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;
}
sender.sendMessage("§a[Admin] §7Kompletter Reset durchgeführt.");
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 {
String targetName = args[2];
Player target = Bukkit.getPlayer(targetName);
Player target = Bukkit.getPlayerExact(name);
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.");
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 {
DataHandler.resetOfflinePlayer(targetName);
sender.sendMessage("§a[Admin] §7Offline-Daten für §e" + targetName + " §7gelöscht.");
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;
}
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 <global/Spieler> <Tag> §7- Tag löschen");
sender.sendMessage("§e/ak admin reset <global/Spieler> §7- Alles löschen");
// /ak admin birthday <set|remove|reset> <Spieler> [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)) 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;
}
AdventInventory.open(player);
// /ak birthday [set <TT.MM> | 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<String> filter(Collection<String> options, String prefix) {
return options.stream()
.filter(opt -> opt.toLowerCase().startsWith(prefix.toLowerCase()))
.collect(Collectors.toList());
}
private static List<String> eventIds() {
return EventManager.allConfigured().stream().map(CalendarEvent::getId).collect(Collectors.toList());
}
private static List<String> targets() {
List<String> options = new ArrayList<>();
options.add("global");
Bukkit.getOnlinePlayers().forEach(p -> options.add(p.getName()));
return options;
}
private static List<String> dayOptions(int max) {
List<String> days = new ArrayList<>();
for (int i = 1; i <= max; i++) days.add(String.valueOf(i));
return days;
}
@Override
public List<String> onTabComplete(CommandSender s, Command c, String a, String[] args) {
if (args.length == 1) return Collections.singletonList("admin");
boolean admin = s.hasPermission("adventskalender.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<String> 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());
if (args.length == 1) {
List<String> 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]);
}
// Tab-Completion für die TAGE (Numerisch sortiert)
if ((args.length == 3 && args[1].equalsIgnoreCase("test")) || (args.length == 4 && args[1].equalsIgnoreCase("resetday"))) {
List<String> 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());
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<String> options = new ArrayList<>(eventIds());
options.add(0, "reset");
options.addAll(dayOptions(MAX_DAYS));
return filter(options, last);
}
if (args.length == 4 && ev != null) {
List<String> options = new ArrayList<>(dayOptions(ev.getDays()));
options.add(0, "reset");
return filter(options, last);
}
break;
}
case "reset": {
if (args.length == 3) {
List<String> 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<String> 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();
}
}
@@ -1,21 +1,22 @@
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.MalformedURLException;
import java.net.URL;
import java.time.LocalDate;
import java.time.Month;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class AdventInventory {
@@ -57,28 +58,83 @@ public class AdventInventory {
// 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));
// Spieler, die gerade einen Kalender geöffnet haben
private static final Set<UUID> VIEWERS = ConcurrentHashMap.newKeySet();
// Cache alle nummerierten geschlossenen Köpfe
/** 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 open(Player player) {
String title = " " + LanguageManager.getString("gui.title");
Inventory inv = Bukkit.createInventory(null, 54, title);
public static void markClosed(Player player) { VIEWERS.remove(player.getUniqueId()); }
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);
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<Integer> 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<Integer> 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++) {
if (i < 9 || i >= 45 || i % 9 == 0 || (i + 1) % 9 == 0) {
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 {
@@ -86,45 +142,58 @@ public class AdventInventory {
}
}
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));
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 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();
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<String> 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(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.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);
@@ -136,12 +205,16 @@ public class AdventInventory {
ItemStack item = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) item.getItemMeta();
try {
UUID uuid = UUID.nameUUIDFromBytes(url.getBytes());
UUID uuid = UUID.nameUUIDFromBytes(url.getBytes(StandardCharsets.UTF_8));
PlayerProfile profile = Bukkit.createPlayerProfile(uuid, "AdventHead");
profile.getTextures().setSkin(new URL(url));
PlayerTextures textures = profile.getTextures();
textures.setSkin(URI.create(url).toURL());
profile.setTextures(textures);
meta.setOwnerProfile(profile);
item.setItemMeta(meta);
} catch (MalformedURLException e) { e.printStackTrace(); }
} catch (Exception e) {
Adventskalender.getInstance().getLogger().warning("Kopf-Textur konnte nicht geladen werden (" + url + "): " + e.getMessage());
}
return item;
}
@@ -5,32 +5,39 @@ 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.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
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;
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<UUID> 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();
@@ -58,78 +65,99 @@ public class AdventListener implements Listener {
}
}
@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) {
String baseTitle = LanguageManager.getString("gui.title");
if (!event.getView().getTitle().contains(baseTitle)) return;
if (!(event.getInventory().getHolder() instanceof AdventInventory.AdventHolder)) return;
event.setCancelled(true);
if (!(event.getWhoClicked() instanceof Player player)) return;
if (event.getClickedInventory() != event.getInventory()) return;
ItemStack clicked = event.getCurrentItem();
if (clicked == null || clicked.getType() != Material.PLAYER_HEAD) return;
if (!clicked.hasItemMeta() || !clicked.getItemMeta().hasDisplayName()) return;
CalendarEvent calendar = ((AdventInventory.AdventHolder) event.getInventory().getHolder()).getEvent();
int day = calendar.dayForSlot(event.getRawSlot());
if (day == -1) return;
String displayName = ChatColor.stripColor(clicked.getItemMeta().getDisplayName());
int day;
try {
day = Integer.parseInt(displayName.replaceAll("[^0-9]", ""));
} catch (NumberFormatException e) {
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;
}
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);
// 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));
}
});
});
}
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);
/** 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() + ")");
}
ItemStack reward = new ItemStack(Material.valueOf(matName.toUpperCase()), amount);
ItemMeta meta = reward.getItemMeta();
if (meta != null) {
List<Map<?, ?>> 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);
private void giveReward(Player player, CalendarEvent calendar, int day, ItemStack reward) {
if (RewardUtil.give(player, reward)) {
player.sendMessage(LanguageManager.getMessage("messages.inventory_full", calendar));
}
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)));
player.sendMessage(LanguageManager.getMessage("messages.reward_received", calendar).replace("%day%", String.valueOf(day)));
}
}
@@ -4,13 +4,16 @@ 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 boolean updateAvailable = false;
private String latestVersion = "";
private volatile boolean updateAvailable = false;
private volatile String latestVersion = "";
@Override
public void onEnable() {
@@ -18,6 +21,7 @@ public final class Adventskalender extends JavaPlugin {
this.dataKey = new NamespacedKey(this, "claimed_days_v2");
saveDefaultConfig();
reloadSettings();
LanguageManager.setup();
// Initialisierung der Daten (MySQL wird hier bevorzugt behandelt)
@@ -31,7 +35,8 @@ public final class Adventskalender extends JavaPlugin {
// Update Checker Integration (ID: 130974)
new UpdateChecker(this, 130974).getVersion(version -> {
if (!this.getDescription().getVersion().equals(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!");
@@ -53,6 +58,42 @@ public final class Adventskalender extends JavaPlugin {
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; }
@@ -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<DataHandler.Birthday> 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<DataHandler.Birthday> 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<ItemStack> 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<String> 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<DataHandler.Birthday> 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<ItemStack> items, List<String> 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<DataHandler.Birthday> 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
}
});
}
}
@@ -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.<id> 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; }
}
@@ -2,6 +2,7 @@ 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;
@@ -11,17 +12,39 @@ 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;
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 void setup() {
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()) {
@@ -43,199 +66,413 @@ public class DataHandler {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
Adventskalender.getInstance().getLogger().severe("data.yml konnte nicht erstellt werden: " + e.getMessage());
}
}
dataConfig = YamlConfiguration.loadConfiguration(file);
}
public static boolean hasClaimed(Player player, int day) {
if (isGlobal()) {
return GlobalDataManager.isDayClaimedGlobally(day);
/** 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<CalendarEvent> 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<Integer> queryDays(String sql, Object... params) throws SQLException {
Set<Integer> 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<Integer> 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<Integer> 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<Integer> loadGlobalDays(CalendarEvent ev) throws SQLException {
if (mysqlActive) {
return queryDays("SELECT day FROM advent_global WHERE event = ?", ev.getId());
}
Set<Integer> 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());
}
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();
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) {
e.printStackTrace();
logSql(e);
return ClaimResult.ERROR;
}
}
String base = playersPath(ev) + "." + uuid;
List<Integer> 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) {
List<Integer> claimed = dataConfig.getIntegerList("players." + player.getUniqueId() + ".claimed");
return claimed.contains(day);
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<Integer> 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<Birthday> 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;
}
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<Integer> 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();
}
}
@SuppressWarnings("deprecation")
private static String lookupUuid(String name) {
OfflinePlayer target = Bukkit.getOfflinePlayer(name);
return target.getUniqueId().toString();
}
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<Integer> claimed = dataConfig.getIntegerList("players." + uuid + ".claimed");
claimed.remove(Integer.valueOf(day));
dataConfig.set("players." + uuid + ".claimed", claimed);
save();
break;
}
}
}
}
public static Set<Integer> getClaimedDays(Player player) {
if (mysqlActive && MySQLManager.isConnected()) {
Set<Integer> 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;
private static boolean save() {
if (dataConfig == null || file == null) return false;
try {
dataConfig.save(file);
return true;
} 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();
}
Adventskalender.getInstance().getLogger().severe("data.yml konnte nicht gespeichert werden: " + e.getMessage());
return false;
}
}
}
@@ -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<String, CalendarEvent> 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<String> 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<String, CalendarEvent> 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<CalendarEvent> all() {
List<CalendarEvent> 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<CalendarEvent> 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;
}
}
@@ -3,6 +3,10 @@ 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;
@@ -11,10 +15,28 @@ public class LanguageManager {
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);
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] "));
}
@@ -23,10 +45,29 @@ public class LanguageManager {
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<String> getStringList(String path) {
return cfg.getStringList(path).stream()
.map(s -> ChatColor.translateAlternateColorCodes('&', s))
@@ -2,15 +2,13 @@ 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() {
/** 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");
@@ -18,33 +16,62 @@ public class MySQLManager {
String user = config.getString("mysql.user");
String password = config.getString("mysql.password");
close();
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);
}
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), day INT, PRIMARY KEY(uuid, day))");
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 (day INT PRIMARY KEY, player_name VARCHAR(16))");
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) {
e.printStackTrace();
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 boolean isConnected() {
public static synchronized boolean isConnected() {
try {
return connection != null && !connection.isClosed();
} catch (SQLException e) {
@@ -52,15 +79,26 @@ public class MySQLManager {
}
}
public static void close() {
/** Prüft die Verbindung und baut sie bei Bedarf neu auf. */
public static synchronized boolean ensureConnected() {
try {
if (isConnected()) connection.close();
} catch (SQLException e) {
e.printStackTrace();
if (connection != null && connection.isValid(2)) return true;
} catch (SQLException ignored) {
}
connect();
return connection != null;
}
public static Connection getConnection() {
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;
}
}
@@ -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<Map<?, ?>> 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<Integer, ItemStack> leftover = player.getInventory().addItem(reward);
for (ItemStack item : leftover.values()) {
player.getWorld().dropItemNaturally(player.getLocation(), item);
}
return !leftover.isEmpty();
}
}
@@ -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,12 +20,16 @@ public class UpdateChecker {
public void getVersion(final Consumer<String> 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());
}
});
+276 -1
View File
@@ -8,7 +8,11 @@ general:
# Verfügbare Optionen: 'de', 'en'
language: "de"
# Nur für Testzwecke: Simuliert einen Tag (0 = echtes Datum verwenden)
# 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 <Tag> kann der Tag zur Laufzeit geändert werden (wird nicht gespeichert).
test_day: 0
# --- Kalender-Einstellungen ---
@@ -209,3 +213,274 @@ rewards:
amount: 64
key: "rewards.day_24"
enchantments: []
# =============================================================================
# Events (Kalender)
# =============================================================================
# Jedes Event hat einen eigenen Kalender, eigene Belohnungen und eigene Daten.
# Öffnen mit /ak (laufendes Event) oder /ak <event>.
#
# 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.<id>.
# 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 <TT.MM> 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 <Spieler>), 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: []
+202 -3
View File
@@ -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] <Tag/reset> &7- Datum simulieren"
- "&e/ak admin resetday [event] <global/Spieler> <Tag> &7- Tag löschen"
- "&e/ak admin reset [event] <global/Spieler> &7- Alles löschen"
- "&7Ohne [event] gilt der Befehl für alle Events."
- "&e/ak admin birthday <set|remove|reset> <Spieler> [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:
@@ -106,3 +131,177 @@ rewards:
day_24:
name: "&cTag 24: Das große Finale!"
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 <TT.MM> &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 <TT.MM>"
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 <TT.MM>&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."
+201 -2
View File
@@ -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] <day/reset> &7- Simulate date"
- "&e/ak admin resetday [event] <global/player> <day> &7- Reset a day"
- "&e/ak admin reset [event] <global/player> &7- Reset everything"
- "&7Without [event] the command applies to all events."
- "&e/ak admin birthday <set|remove|reset> <player> [DD.MM] &7- Manage birthdays"
# --- Reward Texts in GUI ---
rewards:
@@ -105,3 +130,177 @@ rewards:
day_24:
name: "&cDay 24: The Grand Finale!"
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 <DD.MM> &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 <DD.MM>"
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 <DD.MM>&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."
+4 -1
View File
@@ -1,5 +1,5 @@
name: Adventskalender
version: '1.0.2'
version: '1.0.3'
main: de.mviper.adventskalender.Adventskalender
api-version: 1.20
author: M_Viper
@@ -18,6 +18,9 @@ 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