Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 839932873b | |||
| 1a8121236f | |||
| 46cd01c9bc | |||
| e5505edf2c | |||
| bc3c2399a6 | |||
| 5259184e09 | |||
| a45e0f4731 | |||
| 33acd04c3b | |||
| 0a547f90bf | |||
| 02811bafbd | |||
| c8d4578fa6 |
273
README.md
273
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
  
|
||||
|
||||
**TicketSystem** ist das flexible, moderne Support- und Feedback-Plugin für Minecraft-Server (Spigot/Paper 1.18.x–1.21.x, Java 17+). Es bietet flexible Speicherung, automatische Backups & Migration, Export/Import, Statistiken, dynamische GUI, Kategorie- und Prioritäten-System, Bewertungs- und Kommentar-System, Discord-Webhook und volle BungeeCord-Unterstützung.
|
||||
**TicketSystem** ist das flexible, moderne Support- und Feedback-Plugin für Minecraft-Server (Spigot/Paper 1.18.x–1.21.x, Java 17+). Es bietet flexible Speicherung, automatische Backups & Migration, Export/Import, Statistiken, dynamische GUI, Kategorie- und Prioritäten-System, Bewertungs- und Kommentar-System, **FAQ-System**, Discord-Webhook und volle BungeeCord-Unterstützung.
|
||||
|
||||
---
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
- Offline-Benachrichtigungen
|
||||
- Discord-Webhook mit Embeds & Rollen-Ping
|
||||
- Blacklist für Spieler
|
||||
- Performance: asynchron, ressourcenschonend
|
||||
- **FAQ-System** mit eigenem GUI, eigener Datei (`faqs.yml`) und vollständiger In-Game-Verwaltung durch Admins
|
||||
- **Performance-Caching** – TTL-basierter In-Memory-Cache reduziert Datenbankabfragen spürbar
|
||||
- **Saubere Konsole** – minimale Start-Logs, kein unnötiger Spam
|
||||
- Erweiterbarkeit: viele Hooks
|
||||
- **BungeeCord-Unterstützung**: serverübergreifende Tickets, Teleports, Weiterleitungen, Benachrichtigungen
|
||||
|
||||
@@ -32,8 +34,8 @@
|
||||
## Installation & Setup
|
||||
|
||||
1. TicketSystem.jar in den plugins-Ordner legen und Server starten
|
||||
2. config.yml anpassen (Speicherorte, Nachrichten, Limits, Farben, MySQL-Daten etc.)
|
||||
3. /ticket-Befehle nutzen
|
||||
2. `config.yml` anpassen (Speicherorte, Nachrichten, Limits, Farben, MySQL-Daten etc.)
|
||||
3. `/ticket`-Befehle nutzen
|
||||
|
||||
---
|
||||
|
||||
@@ -42,59 +44,184 @@
|
||||
### Übersicht der Befehle
|
||||
|
||||
| Befehl | Beschreibung | Nutzergruppe |
|
||||
|-------------------------------------|---------------------------------------------------|----------------------|
|
||||
| /ticket | Hilfe & Übersicht | Spieler, Support |
|
||||
| /ticket create [Kategorie] [Priorität] <Text> | Ticket erstellen | Spieler |
|
||||
| /ticket list | Eigene Tickets in der GUI anzeigen | Spieler |
|
||||
| /ticket comment <ID> <Nachricht> | Kommentar hinzufügen | Spieler, Support |
|
||||
| /ticket rate <ID> <good|bad> | Support bewerten | Spieler |
|
||||
| /ticket claim <ID> | Ticket annehmen | Support/Admin |
|
||||
| /ticket close <ID> [Kommentar] | Ticket schließen | Support/Admin |
|
||||
| /ticket forward <ID> <Spieler> | Ticket weiterleiten | Support/Admin |
|
||||
| /ticket setpriority <ID> <low|normal|high|urgent> | Priorität ändern | Support/Admin |
|
||||
| /ticket reload | Konfiguration neu laden | Support/Admin |
|
||||
| /ticket stats | Statistiken anzeigen | Support/Admin |
|
||||
| /ticket archive | Tickets archivieren | Support/Admin |
|
||||
| /ticket blacklist <add|remove|list> [Spieler] [Grund] | Blacklist verwalten | Support/Admin |
|
||||
| /ticket migrate <tomysql|tofile> | Speicherart wechseln | Support/Admin |
|
||||
| /ticket export <Dateiname> | Tickets exportieren | Support/Admin |
|
||||
| /ticket import <Dateiname> | Tickets importieren | Support/Admin |
|
||||
| /ticket teleport <ID> | Teleport zu Ticket (BungeeCord) | Support/Admin |
|
||||
|-----------------------------------------------------------|--------------------------------------------------------|------------------|
|
||||
| `/ticket` | Hilfe & Übersicht | Spieler, Support |
|
||||
| `/ticket create [Kategorie] [Priorität] <Text>` | Ticket erstellen | Spieler |
|
||||
| `/ticket list` | Eigene Tickets in der GUI anzeigen | Spieler |
|
||||
| `/ticket comment <ID> <Nachricht>` | Kommentar hinzufügen | Spieler, Support |
|
||||
| `/ticket rate <ID> <good\|bad>` | Support bewerten | Spieler |
|
||||
| `/ticket faq` | FAQ-GUI öffnen (häufige Fragen) | Spieler |
|
||||
| `/ticket faq list` | FAQs im Chat auflisten | Spieler |
|
||||
| `/ticket faq add <Frage> \| <Antwort>` | Neues FAQ hinzufügen | Admin |
|
||||
| `/ticket faq edit <ID> <Frage> \| <Antwort>` | Bestehendes FAQ bearbeiten | Admin |
|
||||
| `/ticket faq delete <ID>` | FAQ löschen | Admin |
|
||||
| `/ticket faq reload` | FAQs aus faqs.yml neu laden | Admin |
|
||||
| `/ticket claim <ID>` | Ticket annehmen | Support/Admin |
|
||||
| `/ticket close <ID> [Kommentar]` | Ticket schließen | Support/Admin |
|
||||
| `/ticket forward <ID> <Spieler>` | Ticket weiterleiten | Support/Admin |
|
||||
| `/ticket setpriority <ID> <low\|normal\|high\|urgent>` | Priorität ändern | Support/Admin |
|
||||
| `/ticket reload` | Konfiguration neu laden | Admin |
|
||||
| `/ticket stats` | Statistiken anzeigen | Admin |
|
||||
| `/ticket archive` | Tickets archivieren | Admin |
|
||||
| `/ticket blacklist <add\|remove\|list> [Spieler] [Grund]` | Blacklist verwalten | Admin |
|
||||
| `/ticket migrate <tomysql\|tofile>` | Speicherart wechseln | Admin |
|
||||
| `/ticket export <Dateiname>` | Tickets exportieren | Admin |
|
||||
| `/ticket import <Dateiname>` | Tickets importieren | Admin |
|
||||
| `/ticket teleport <ID>` | Teleport zu Ticket (BungeeCord) | Support/Admin |
|
||||
|
||||
### Rechte
|
||||
|
||||
| Permission | Beschreibung | Standard |
|
||||
|-------------------|---------------------------------------------------|------------------|
|
||||
| ticket.create | Ticket erstellen | alle Spieler |
|
||||
| ticket.support | Tickets einsehen, claimen, schließen, Priorität | manuell vergeben |
|
||||
| ticket.archive | Archiv öffnen, Tickets löschen | manuell vergeben |
|
||||
| ticket.admin | Voller Zugriff (inkl. Weiterleitung, Reload, Blacklist) | OP |
|
||||
|-----------------|-----------------------------------------------------------------|------------------|
|
||||
| `ticket.create` | Ticket erstellen | alle Spieler |
|
||||
| `ticket.support`| Tickets einsehen, claimen, schließen, Priorität ändern | manuell vergeben |
|
||||
| `ticket.archive`| Archiv öffnen, Tickets permanent löschen | manuell vergeben |
|
||||
| `ticket.admin` | Voller Zugriff inkl. Weiterleitung, Reload, Blacklist, FAQ-Verwaltung | OP |
|
||||
|
||||
> ticket.archive ist nicht in ticket.admin enthalten und muss explizit vergeben werden.
|
||||
> `ticket.archive` ist nicht in `ticket.admin` enthalten und muss explizit vergeben werden.
|
||||
|
||||
---
|
||||
|
||||
## Mehrsprachigkeit
|
||||
|
||||
TicketSystem unterstützt drei Sprachmodi, die über einen einzigen Schlüssel in der `config.yml` gesteuert werden:
|
||||
|
||||
```yaml
|
||||
language: de # Deutsch – deutsche Texte & deutsche Befehlsnamen
|
||||
language: en # Englisch – englische Texte & englische Befehlsnamen
|
||||
language: both # Beides – deutsche Texte & beide Befehlsnamen gleichzeitig
|
||||
```
|
||||
|
||||
> `command-language` existiert nicht mehr. Alle Einstellungen laufen über `language`.
|
||||
|
||||
### Befehlsnamen je Sprachmodus
|
||||
|
||||
| Interner Schlüssel | `language: de` | `language: en` | `language: both` |
|
||||
|--------------------|---------------------------|-------------------------|-------------------------------------------|
|
||||
| `create` | `/ticket erstellen` | `/ticket create` | `/ticket create §8(§7erstellen§8)` |
|
||||
| `list` | `/ticket liste` | `/ticket list` | `/ticket list §8(§7liste§8)` |
|
||||
| `comment` | `/ticket kommentar` | `/ticket comment` | `/ticket comment §8(§7kommentar§8)` |
|
||||
| `rate` | `/ticket bewerten` | `/ticket rate` | `/ticket rate §8(§7bewerten§8)` |
|
||||
| `claim` | `/ticket übernehmen` | `/ticket claim` | `/ticket claim §8(§7übernehmen§8)` |
|
||||
| `close` | `/ticket schließen` | `/ticket close` | `/ticket close §8(§7schließen§8)` |
|
||||
| `forward` | `/ticket weiterleiten` | `/ticket forward` | `/ticket forward §8(§7weiterleiten§8)` |
|
||||
| `reload` | `/ticket neuladen` | `/ticket reload` | `/ticket reload §8(§7neuladen§8)` |
|
||||
| `stats` | `/ticket statistik` | `/ticket stats` | `/ticket stats §8(§7statistik§8)` |
|
||||
| `archive` | `/ticket archivieren` | `/ticket archive` | `/ticket archive §8(§7archivieren§8)` |
|
||||
| `migrate` | `/ticket migrieren` | `/ticket migrate` | `/ticket migrate §8(§7migrieren§8)` |
|
||||
| `export` | `/ticket exportieren` | `/ticket export` | `/ticket export §8(§7exportieren§8)` |
|
||||
| `import` | `/ticket importieren` | `/ticket import` | `/ticket import §8(§7importieren§8)` |
|
||||
| `blacklist` | `/ticket sperrliste` | `/ticket blacklist` | `/ticket blacklist §8(§7sperrliste§8)` |
|
||||
| `setpriority` | `/ticket priorität` | `/ticket setpriority` | `/ticket setpriority §8(§7priorität§8)` |
|
||||
| `faq` | `/ticket faq` | `/ticket faq` | `/ticket faq` |
|
||||
| `top` | `/ticket top` | `/ticket top` | `/ticket top` |
|
||||
|
||||
### `{cmd_X}`-Platzhalter in den Sprachdateien
|
||||
|
||||
Alle Hilfetexte, Fehlermeldungen und GUI-Hinweise in `lang_de.yml` / `lang_en.yml` verwenden `{cmd_X}`-Platzhalter statt fester Befehlsnamen. Beim Anzeigen ersetzt der `LanguageManager` diese automatisch anhand des aktiven Sprachmodus.
|
||||
|
||||
| Platzhalter | Beispiel-Ausgabe (`language: en`) |
|
||||
|----------------------|-----------------------------------|
|
||||
| `{cmd_create}` | `/ticket create` |
|
||||
| `{cmd_list}` | `/ticket list` |
|
||||
| `{cmd_comment}` | `/ticket comment` |
|
||||
| `{cmd_rate}` | `/ticket rate` |
|
||||
| `{cmd_claim}` | `/ticket claim` |
|
||||
| `{cmd_close}` | `/ticket close` |
|
||||
| `{cmd_forward}` | `/ticket forward` |
|
||||
| `{cmd_reload}` | `/ticket reload` |
|
||||
| `{cmd_stats}` | `/ticket stats` |
|
||||
| `{cmd_archive}` | `/ticket archive` |
|
||||
| `{cmd_migrate}` | `/ticket migrate` |
|
||||
| `{cmd_export}` | `/ticket export` |
|
||||
| `{cmd_import}` | `/ticket import` |
|
||||
| `{cmd_blacklist}` | `/ticket blacklist` |
|
||||
| `{cmd_setpriority}` | `/ticket setpriority` |
|
||||
| `{cmd_faq}` | `/ticket faq` |
|
||||
| `{cmd_top}` | `/ticket top` |
|
||||
|
||||
**Verwendungsbeispiel in der Sprachdatei:**
|
||||
```yaml
|
||||
help:
|
||||
create: "&e{cmd_create} [Kategorie] <Text> &7– Neues Ticket erstellen"
|
||||
# → language: de gibt aus: /ticket erstellen [Kategorie] <Text>
|
||||
# → language: en gibt aus: /ticket create [Kategorie] <Text>
|
||||
```
|
||||
|
||||
### GUI-Sprachanpassung
|
||||
|
||||
Seit dieser Version sind auch alle **GUI-Texte** vollständig sprachabhängig – Inventar-Titel, Item-Lore-Labels und Action-Buttons werden aus `gui.item.*` in der aktiven Sprachdatei geladen. Nach einem `/ticket reload` mit geändertem `language`-Wert wechselt die gesamte Oberfläche sofort.
|
||||
|
||||
---
|
||||
|
||||
## FAQ-System
|
||||
|
||||
Das FAQ-System ermöglicht es Admins, häufige Fragen und Antworten direkt im Spiel zu verwalten. Spieler können die FAQs per GUI oder Befehl einsehen.
|
||||
|
||||
### Für Spieler
|
||||
|
||||
```
|
||||
/ticket faq – öffnet die FAQ-GUI mit Custom-Skull-Items
|
||||
/ticket faq list – listet alle FAQs im Chat auf
|
||||
```
|
||||
|
||||
In der GUI wird für jeden FAQ-Eintrag ein **Custom-Skull-Item** mit individueller Textur angezeigt. Ein Klick auf einen Eintrag zeigt die vollständige Antwort im Chat.
|
||||
|
||||
### Für Admins
|
||||
|
||||
```
|
||||
/ticket faq add Wie melde ich einen Bug? | Nutze /ticket create bug <Beschreibung>.
|
||||
/ticket faq edit 1 Neue Frage? | Neue Antwort.
|
||||
/ticket faq delete 1
|
||||
/ticket faq reload
|
||||
```
|
||||
|
||||
Alternativ können FAQs auch direkt über die **Admin-FAQ-GUI** verwaltet werden (öffnet sich automatisch wenn `ticket.admin`-Berechtigung vorhanden). Ein Klick auf einen Eintrag öffnet eine Aktions-GUI mit den Optionen **Bearbeiten** und **Löschen**. Neue FAQs lassen sich ebenfalls per Schaltfläche in der GUI hinzufügen – das Plugin führt den Admin Schritt für Schritt durch eine Chat-Eingabe.
|
||||
|
||||
### faqs.yml
|
||||
|
||||
Alle FAQs werden in einer eigenen Datei `plugins/TicketSystem/faqs.yml` gespeichert:
|
||||
|
||||
```yaml
|
||||
faqs:
|
||||
1:
|
||||
question: "Wie erstelle ich ein Ticket?"
|
||||
answer: "Nutze den Befehl /ticket create [Kategorie] [Beschreibung]."
|
||||
2:
|
||||
question: "Wie lange dauert die Bearbeitung?"
|
||||
answer: "Unser Support-Team bearbeitet Tickets so schnell wie möglich."
|
||||
```
|
||||
|
||||
Beim ersten Start werden automatisch vier Beispiel-FAQs erstellt.
|
||||
|
||||
---
|
||||
|
||||
## Performance-Caching
|
||||
|
||||
Ab dieser Version hält TicketSystem häufig abgerufene Tickets in einem **TTL-basierten In-Memory-Cache** vor. Das reduziert die Datenbankbelastung bei wiederholten Zugriffen (GUI, Kommentare, Bewertungen) deutlich.
|
||||
|
||||
- Standard-Lebenszeit: **60 Sekunden** (konfigurierbar: `cache-ttl-seconds` in `config.yml`)
|
||||
- Der Cache wird bei Ticket-Änderungen (Claim, Close, Priorität usw.) automatisch invalidiert
|
||||
- Regelmäßige Bereinigung abgelaufener Einträge alle 5 Minuten
|
||||
- Der aktuelle Cache-Status ist in `/ticket stats` einsehbar
|
||||
|
||||
---
|
||||
|
||||
## Kategorie & Priorität
|
||||
|
||||
Beim Erstellen eines Tickets können Kategorie und Priorität optional angegeben werden. Die folgende Tabelle zeigt die Möglichkeiten und Beispiele:
|
||||
Beim Erstellen eines Tickets können Kategorie und Priorität optional angegeben werden:
|
||||
|
||||
| Befehl | Kategorie | Priorität |
|
||||
|----------------------------------------|-------------|-----------|
|
||||
| /ticket create <Text> | Standard | NORMAL |
|
||||
| /ticket create bug <Text> | Bug | NORMAL |
|
||||
| /ticket create high <Text> | Standard | HIGH |
|
||||
| /ticket create bug high <Text> | Bug | HIGH |
|
||||
| /ticket create question urgent <Text> | Frage | URGENT |
|
||||
|-----------------------------------------|-----------|-----------|
|
||||
| `/ticket create <Text>` | Standard | NORMAL |
|
||||
| `/ticket create bug <Text>` | Bug | NORMAL |
|
||||
| `/ticket create high <Text>` | Standard | HIGH |
|
||||
| `/ticket create bug high <Text>` | Bug | HIGH |
|
||||
| `/ticket create question urgent <Text>` | Frage | URGENT |
|
||||
|
||||
**Verfügbare Prioritäten:**
|
||||
- low
|
||||
- normal
|
||||
- high
|
||||
- urgent
|
||||
**Verfügbare Prioritäten:** `low`, `normal`, `high`, `urgent` (auch deutsch: `niedrig`, `hoch`, `dringend`)
|
||||
|
||||
(auch deutsch: niedrig, hoch, dringend)
|
||||
|
||||
**Kategorien und Aliases** sind frei in der config.yml konfigurierbar.
|
||||
Kategorien und Aliases sind frei in der `config.yml` konfigurierbar.
|
||||
|
||||
---
|
||||
|
||||
@@ -104,8 +231,9 @@ Beim Erstellen eines Tickets können Kategorie und Priorität optional angegeben
|
||||
- Rollen-Ping pro Nachrichtentyp
|
||||
- Drei Ereignisse: neues Ticket, Ticket geschlossen, Ticket weitergeleitet
|
||||
|
||||
Konfiguration in config.yml:
|
||||
Konfiguration in `config.yml`:
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
enabled: true
|
||||
webhook-url: "https://discord.com/api/webhooks/..."
|
||||
@@ -118,6 +246,7 @@ discord:
|
||||
ticket-closed:
|
||||
enabled: true
|
||||
role-ping: false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -126,81 +255,71 @@ discord:
|
||||
TicketSystem bietet volle Unterstützung für BungeeCord-Netzwerke:
|
||||
|
||||
- Tickets von jedem Server im Netzwerk
|
||||
- Teleport zu Tickets auf anderen Servern (/ticket teleport <ID>)
|
||||
- Teleport zu Tickets auf anderen Servern (`/ticket teleport <ID>`)
|
||||
- Tickets im Archiv und GUI serverübergreifend
|
||||
- Benachrichtigungen an alle Server
|
||||
- Discord-Webhooks zeigen Server-Namen
|
||||
- Tickets an Supporter auf anderen Servern weiterleiten
|
||||
- Teleport funktioniert auch zwischen Servern
|
||||
|
||||
**Voraussetzungen:**
|
||||
- spigot.yml: bungeecord: true
|
||||
- config.yml: bungeecord: true, server-name pro Server
|
||||
- `spigot.yml`: `bungeecord: true`
|
||||
- `config.yml`: `bungeecord: true`, `server-name` pro Server
|
||||
- TicketSystem.jar auf allen Spigot-Servern
|
||||
- Alle Server nutzen dieselbe MySQL-Datenbank
|
||||
|
||||
**Cross-Server-Befehle:**
|
||||
- /ticket teleport <ID>
|
||||
- /ticket forward <ID> <Spieler>
|
||||
- /ticket archive
|
||||
- /ticket list
|
||||
|
||||
**Tipps:**
|
||||
- Server-Name erscheint in GUI & Discord
|
||||
- Zielspieler muss online sein
|
||||
- Funktionen auch im Einzelserver-Modus
|
||||
- Bei Problemen: gleiche MySQL, Kanäle in plugin.yml prüfen
|
||||
|
||||
---
|
||||
|
||||
## Vergleich mit anderen Plugins
|
||||
|
||||
TicketSystem hebt sich durch viele Alleinstellungsmerkmale von anderen Ticket-Plugins ab. Die folgende Tabelle zeigt die wichtigsten Unterschiede:
|
||||
|
||||
| Feature | TicketSystem | SimpleTickets | AdvancedTickets |
|
||||
|-------------------------|:------------:|:-------------:|:---------------:|
|
||||
|-----------------------------|:------------:|:-------------:|:---------------:|
|
||||
| Speicher-Migration | ✔️ | ⚠️ | ✖️ |
|
||||
| Automatische Backups | ✔️ | ⚠️ | ✖️ |
|
||||
| GUI mit Kategorien | ✔️ | ⚠️ | ✖️ |
|
||||
| Archivierung | ✔️ | ⚠️ | ✖️ |
|
||||
| Rollenbasierter Archiv | ✔️ | ✖️ | ✖️ |
|
||||
| Rollenbasierter Archiv-Zugriff| ✔️ | ✖️ | ✖️ |
|
||||
| Kategorie-System | ✔️ | ✖️ | ✖️ |
|
||||
| Prioritäten-System | ✔️ | ✖️ | ✖️ |
|
||||
| FAQ-System | ✔️ | ✖️ | ✖️ |
|
||||
| Performance-Caching | ✔️ | ✖️ | ✖️ |
|
||||
| Offline-Benachrichtigungen | ✔️ | ✖️ | ✖️ |
|
||||
| Discord-Webhook | ✔️ | ✖️ | ✖️ |
|
||||
| Bewertungs-System | ✔️ | ✖️ | ✖️ |
|
||||
| Update-Checker | ✔️ | ✖️ | ✖️ |
|
||||
| BungeeCord-Unterstützung | ✔️ | ✖️ | ✖️ |
|
||||
|
||||
Legende:
|
||||
- ✔️ = Vollständige Unterstützung
|
||||
- ⚠️ = Eingeschränkte oder fehleranfällige Unterstützung
|
||||
- ✖️ = Nicht vorhanden
|
||||
Legende: ✔️ Vollständige Unterstützung · ⚠️ Eingeschränkt · ✖️ Nicht vorhanden
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Kann ich zwischen MySQL und Datei-Speicherung wechseln?**
|
||||
> Ja! Mit /ticket migrate tomysql oder /ticket migrate tofile werden alle Daten automatisch migriert.
|
||||
> Ja! Mit `/ticket migrate tomysql` oder `/ticket migrate tofile` werden alle Daten automatisch migriert.
|
||||
|
||||
**Wie konfiguriere ich eigene Kategorien?**
|
||||
> In der config.yml unter categories: Name, Farbe, Material und Aliases frei wählbar. Änderungen mit /ticket reload übernehmen.
|
||||
> In der `config.yml` unter `categories:` – Name, Farbe, Material und Aliases frei wählbar. Änderungen mit `/ticket reload` übernehmen.
|
||||
|
||||
**Wie verwalte ich FAQs?**
|
||||
> Mit `/ticket faq` öffnest du die GUI. Als Admin kannst du über die GUI oder per `/ticket faq add|edit|delete` FAQs verwalten. Alle Daten liegen in `faqs.yml`.
|
||||
|
||||
**Was passiert mit Benachrichtigungen wenn ein Spieler offline ist?**
|
||||
> Alle Benachrichtigungen werden gespeichert und beim nächsten Login angezeigt.
|
||||
|
||||
**Wie ändere ich die Priorität eines Tickets?**
|
||||
> Als Support/Admin per Befehl /ticket setpriority <ID> <Priorität> oder direkt in der GUI.
|
||||
> Als Support/Admin per `/ticket setpriority <ID> <Priorität>` oder direkt in der GUI.
|
||||
|
||||
**Wie aktiviere ich den Debug-Modus?**
|
||||
> debug: true in der config.yml setzen.
|
||||
> `debug: true` in der `config.yml` setzen.
|
||||
|
||||
**Wer darf das Ticket-Archiv sehen?**
|
||||
> Nur Spieler mit ticket.archive. Muss explizit vergeben werden.
|
||||
> Nur Spieler mit `ticket.archive`. Muss explizit vergeben werden.
|
||||
|
||||
**Wie funktioniert Teleport bei BungeeCord?**
|
||||
> Mit /ticket teleport <ID> wirst du automatisch auf den richtigen Server und zur Ticket-Position teleportiert.
|
||||
> Mit `/ticket teleport <ID>` wirst du automatisch auf den richtigen Server und zur Ticket-Position teleportiert.
|
||||
|
||||
**Wie lange werden Tickets gecacht?**
|
||||
> Standardmäßig 60 Sekunden. Über `cache-ttl-seconds` in der `config.yml` anpassbar. Der Cache wird bei Änderungen sofort invalidiert.
|
||||
|
||||
---
|
||||
|
||||
@@ -216,3 +335,9 @@ Wir antworten in der Regel innerhalb von 24 Stunden!
|
||||
**Dein Feedback zählt:**
|
||||
Wenn TicketSystem deinen Server bereichert hat, freuen wir uns über eine 5-Sterne Bewertung auf SpigotMC!
|
||||
Jede Rückmeldung hilft, das Plugin weiter zu verbessern und die Community zu stärken.
|
||||
|
||||
---
|
||||
|
||||
**Copyright © 2026 - M_Viper - Alle Rechte vorbehalten**
|
||||
|
||||
Die unbefugte Vervielfältigung, Verbreitung oder Weitergabe dieses Plugins ist strafbar und wird rechtlich verfolgt.
|
||||
7
pom.xml
7
pom.xml
@@ -6,15 +6,16 @@
|
||||
|
||||
<groupId>de.ticketsystem</groupId>
|
||||
<artifactId>TicketSystem</artifactId>
|
||||
<version>1.0.5</version>
|
||||
<version>1.0.8</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>TicketSystem</name>
|
||||
<description>Ingame Support Ticket System with MySQL</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package de.ticketsystem;
|
||||
|
||||
import de.ticketsystem.bungee.BungeeMessenger;
|
||||
import de.ticketsystem.cache.TicketCache;
|
||||
import de.ticketsystem.commands.TicketCommand;
|
||||
import de.ticketsystem.database.DatabaseManager;
|
||||
import de.ticketsystem.discord.DiscordWebhook;
|
||||
import de.ticketsystem.gui.FaqGUI;
|
||||
import de.ticketsystem.gui.TicketGUI;
|
||||
import de.ticketsystem.listeners.PlayerJoinListener;
|
||||
import de.ticketsystem.manager.CategoryManager;
|
||||
import de.ticketsystem.manager.FaqManager;
|
||||
import de.ticketsystem.manager.LanguageManager;
|
||||
import de.ticketsystem.manager.TicketManager;
|
||||
import de.ticketsystem.model.Ticket;
|
||||
import org.bukkit.ChatColor;
|
||||
@@ -23,16 +27,19 @@ public class TicketPlugin extends JavaPlugin {
|
||||
/**
|
||||
* Name dieses Servers im BungeeCord-Netzwerk.
|
||||
* Konfigurierbar in config.yml → server-name
|
||||
* Wird in Tickets gespeichert und in Benachrichtigungen angezeigt.
|
||||
*/
|
||||
private String serverName;
|
||||
|
||||
private LanguageManager languageManager;
|
||||
private DatabaseManager databaseManager;
|
||||
private TicketManager ticketManager;
|
||||
private CategoryManager categoryManager;
|
||||
private FaqManager faqManager;
|
||||
private TicketGUI ticketGUI;
|
||||
private FaqGUI faqGUI;
|
||||
private DiscordWebhook discordWebhook;
|
||||
private BungeeMessenger bungeeMessenger;
|
||||
private TicketCache ticketCache;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -43,10 +50,12 @@ public class TicketPlugin extends JavaPlugin {
|
||||
// Ticket-Klasse für YAML-Serialisierung registrieren
|
||||
Ticket.register();
|
||||
|
||||
// ── Sprachdatei laden (lang.yml) ──────────────────────────────────
|
||||
// Muss VOR allen anderen Managern geschehen, da diese lang() nutzen.
|
||||
languageManager = new LanguageManager(this);
|
||||
|
||||
// ── BungeeCord Plugin-Messaging-Kanäle registrieren ───────────────
|
||||
// Ausgehend: BungeeCord-Standardkanal (für Forward / Message)
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.BUNGEE_CHANNEL);
|
||||
// Eingehend & Ausgehend: Eigener Kanal für Team- und Spielerbenachrichtigungen
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.CUSTOM_CHANNEL);
|
||||
|
||||
bungeeMessenger = new BungeeMessenger(this);
|
||||
@@ -55,19 +64,12 @@ public class TicketPlugin extends JavaPlugin {
|
||||
// Server-Name aus Config lesen
|
||||
serverName = getConfig().getString("server-name", "unknown");
|
||||
if ("unknown".equals(serverName)) {
|
||||
getLogger().warning("[BungeeCord] Kein 'server-name' in der config.yml definiert! " +
|
||||
"Setze 'server-name: dein-server' für korrekte Cross-Server-Anzeige.");
|
||||
} else {
|
||||
getLogger().info("[BungeeCord] Server-Name: §e" + serverName);
|
||||
getLogger().warning("[BungeeCord] Kein 'server-name' in der config.yml definiert!");
|
||||
}
|
||||
|
||||
// BungeeCord-Hinweis prüfen
|
||||
// BungeeCord-Hinweis nur bei deaktiviertem Feature ausgeben
|
||||
if (!getConfig().getBoolean("bungeecord", false)) {
|
||||
getLogger().info("[BungeeCord] Hinweis: Cross-Server-Features sind deaktiviert. " +
|
||||
"Setze 'bungeecord: true' in der config.yml und stelle sicher, " +
|
||||
"dass 'bungeecord: true' auch in spigot.yml gesetzt ist.");
|
||||
} else {
|
||||
getLogger().info("[BungeeCord] Cross-Server-Benachrichtigungen aktiviert.");
|
||||
getLogger().info("[BungeeCord] Cross-Server-Features deaktiviert. Setze 'bungeecord: true' um sie zu aktivieren.");
|
||||
}
|
||||
|
||||
// Update-Checker
|
||||
@@ -75,47 +77,52 @@ public class TicketPlugin extends JavaPlugin {
|
||||
new UpdateChecker(this, resourceId).getVersion(version -> {
|
||||
String current = getDescription().getVersion();
|
||||
if (!current.equals(version)) {
|
||||
String msg = ChatColor.translateAlternateColorCodes('&',
|
||||
"&6[TicketSystem] &eEs ist eine neue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")");
|
||||
getLogger().info("Es ist eine neue Version verfügbar: " + version + " (aktuell: " + current + ")");
|
||||
getServer().getScheduler().runTaskLater(this, () -> {
|
||||
String msg = lang().format("update.available-line1", "{version}", version);
|
||||
getLogger().warning(lang().format("update.available-console",
|
||||
"{new}", version, "{current}", current));
|
||||
getServer().getScheduler().runTaskLater(this, () ->
|
||||
getServer().getOnlinePlayers().stream()
|
||||
.filter(p -> p.hasPermission("ticket.admin"))
|
||||
.forEach(p -> p.sendMessage(msg));
|
||||
}, 20L);
|
||||
} else {
|
||||
getLogger().info("TicketSystem ist aktuell (Version " + current + ")");
|
||||
.forEach(p -> p.sendMessage(msg)), 20L);
|
||||
}
|
||||
});
|
||||
|
||||
// Versionsprüfung
|
||||
// Versionsprüfung der config.yml
|
||||
String configVersion = getConfig().getString("version", "");
|
||||
String expectedVersion = "2.0";
|
||||
String expectedVersion = "2.2";
|
||||
if (!expectedVersion.equals(configVersion)) {
|
||||
getLogger().warning("[WARNUNG] Die Version deiner config.yml (" + configVersion
|
||||
getLogger().warning("[WARNUNG] config.yml-Version (" + configVersion
|
||||
+ ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!");
|
||||
}
|
||||
|
||||
debug = getConfig().getBoolean("debug", false);
|
||||
|
||||
// ── Performance: Ticket-Cache ──────────────────────────────────────
|
||||
long cacheTtl = getConfig().getLong("cache-ttl-seconds", 60) * 1000L;
|
||||
ticketCache = new TicketCache(cacheTtl);
|
||||
|
||||
// Regelmäßige Cache-Bereinigung alle 5 Minuten
|
||||
getServer().getScheduler().runTaskTimerAsynchronously(this,
|
||||
() -> ticketCache.evictExpired(), 6000L, 6000L);
|
||||
|
||||
// Datenbankverbindung
|
||||
databaseManager = new DatabaseManager(this);
|
||||
if (!databaseManager.connect()) {
|
||||
getLogger().severe("Konnte keine Datenbankverbindung herstellen! Plugin läuft im Datei-Modus weiter.");
|
||||
}
|
||||
|
||||
// Manager, GUI & Discord-Webhook initialisieren
|
||||
// Manager, GUI, FAQ & Discord-Webhook initialisieren
|
||||
categoryManager = new CategoryManager(this);
|
||||
ticketManager = new TicketManager(this);
|
||||
faqManager = new FaqManager(this);
|
||||
ticketGUI = new TicketGUI(this);
|
||||
faqGUI = new FaqGUI(this);
|
||||
discordWebhook = new DiscordWebhook(this);
|
||||
|
||||
if (getConfig().getBoolean("discord.enabled", false)) {
|
||||
String url = getConfig().getString("discord.webhook-url", "");
|
||||
if (url.isEmpty()) {
|
||||
getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in der config.yml eingetragen!");
|
||||
} else {
|
||||
getLogger().info("[DiscordWebhook] Integration aktiv.");
|
||||
getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in config.yml eingetragen!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +133,7 @@ public class TicketPlugin extends JavaPlugin {
|
||||
|
||||
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
|
||||
getServer().getPluginManager().registerEvents(ticketGUI, this);
|
||||
getServer().getPluginManager().registerEvents(faqGUI, this);
|
||||
|
||||
// Automatische Archivierung
|
||||
int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24);
|
||||
@@ -137,54 +145,142 @@ public class TicketPlugin extends JavaPlugin {
|
||||
getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert.");
|
||||
}
|
||||
}, ticks, ticks);
|
||||
getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert.");
|
||||
}
|
||||
|
||||
getLogger().info("TicketSystem erfolgreich gestartet!");
|
||||
getLogger().info("TicketSystem v" + getDescription().getVersion() + " erfolgreich gestartet!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Plugin-Messaging-Kanäle abmelden
|
||||
getServer().getMessenger().unregisterOutgoingPluginChannel(this);
|
||||
getServer().getMessenger().unregisterIncomingPluginChannel(this);
|
||||
|
||||
if (ticketCache != null) ticketCache.clear();
|
||||
if (databaseManager != null) databaseManager.disconnect();
|
||||
getLogger().info("TicketSystem wurde deaktiviert.");
|
||||
}
|
||||
|
||||
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
||||
|
||||
public String formatMessage(String path) {
|
||||
String prefix = color(getConfig().getString("prefix", "&8[&6Ticket&8] &r"));
|
||||
String message = getConfig().getString(path, "&cNachricht nicht gefunden: " + path);
|
||||
return prefix + color(message);
|
||||
/**
|
||||
* Gibt den LanguageManager zurück – bevorzugte Methode für alle Texte.
|
||||
*
|
||||
* Verwendung:
|
||||
* plugin.lang().get("ticket.created")
|
||||
* plugin.lang().format("ticket.created", "{id}", String.valueOf(id))
|
||||
* plugin.lang().send(player, "ticket.created", "{id}", String.valueOf(id))
|
||||
*/
|
||||
public LanguageManager lang() {
|
||||
return languageManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kompatibilitätsmethode für bestehenden Code.
|
||||
* Liest Pfade der Form "messages.xxx" aus lang.yml (ohne "messages."-Prefix).
|
||||
*
|
||||
* Beispiel: formatMessage("messages.ticket-created") → lang "ticket.created"
|
||||
*
|
||||
* @deprecated Direkt {@link #lang()} verwenden.
|
||||
*/
|
||||
@Deprecated
|
||||
public String formatMessage(String path) {
|
||||
// "messages.ticket-created" → "ticket.created" (legacy-Mapping)
|
||||
String langKey = mapLegacyPath(path);
|
||||
if (langKey != null) {
|
||||
return lang().formatWithPrefix(langKey);
|
||||
}
|
||||
// Fallback: Direkt in lang.yml nachschlagen
|
||||
String value = lang().getRaw(path);
|
||||
return lang().getPrefix() + lang().color(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Übersetzt &-Farbcodes in §-Farbcodes.
|
||||
* Kompatibilitätsmethode – bevorzugt lang().color() verwenden.
|
||||
*/
|
||||
public String color(String text) {
|
||||
return ChatColor.translateAlternateColorCodes('&', text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappt alte "messages.xxx"-Pfade auf neue lang.yml-Pfade.
|
||||
* Muss ergänzt werden wenn neue Schlüssel im alten Stil genutzt wurden.
|
||||
*/
|
||||
private String mapLegacyPath(String path) {
|
||||
if (path == null) return null;
|
||||
return switch (path) {
|
||||
case "messages.export-success" -> "system.export-success";
|
||||
case "messages.export-fail" -> "system.export-fail";
|
||||
case "messages.import-success" -> "system.import-success";
|
||||
case "messages.import-fail" -> "system.import-fail";
|
||||
case "messages.migration-success" -> "system.migration-success";
|
||||
case "messages.migration-fail" -> "system.migration-fail";
|
||||
case "messages.archive-success" -> "system.archive-success";
|
||||
case "messages.archive-fail" -> "system.archive-fail";
|
||||
case "messages.file-not-found" -> "system.file-not-found";
|
||||
case "messages.unknown-mode" -> "system.unknown-mode";
|
||||
case "messages.validation-warning" -> "system.validation-warning";
|
||||
case "messages.ticket-created" -> "ticket.created";
|
||||
case "messages.ticket-created-category" -> "ticket.created-category";
|
||||
case "messages.ticket-claimed" -> "ticket.claimed";
|
||||
case "messages.ticket-claimed-notify" -> "ticket.claimed-notify";
|
||||
case "messages.ticket-closed" -> "ticket.closed";
|
||||
case "messages.ticket-closed-notify" -> "ticket.closed-notify";
|
||||
case "messages.ticket-forwarded" -> "ticket.forwarded";
|
||||
case "messages.ticket-forwarded-notify" -> "ticket.forwarded-notify";
|
||||
case "messages.ticket-forwarded-creator-notify" -> "ticket.forwarded-creator";
|
||||
case "messages.new-ticket-notify" -> "ticket.new-notify";
|
||||
case "messages.comment-saved" -> "comment.saved";
|
||||
case "messages.comment-notify" -> "comment.notify-online";
|
||||
case "messages.comment-no-permission" -> "comment.no-permission";
|
||||
case "messages.rating-saved-good" -> "rating.saved-good";
|
||||
case "messages.rating-saved-bad" -> "rating.saved-bad";
|
||||
case "messages.rating-already-rated" -> "rating.already-rated";
|
||||
case "messages.rating-not-yours" -> "rating.not-yours";
|
||||
case "messages.rating-disabled" -> "rating.disabled";
|
||||
case "messages.rating-prompt" -> "rating.prompt-title";
|
||||
case "messages.blacklist-added" -> "blacklist.added";
|
||||
case "messages.blacklist-removed" -> "blacklist.removed";
|
||||
case "messages.blacklist-already" -> "blacklist.already";
|
||||
case "messages.blacklist-not-found" -> "blacklist.not-found";
|
||||
case "messages.blacklist-blocked" -> "create.blacklist-blocked";
|
||||
case "messages.no-permission" -> "general.no-permission";
|
||||
case "messages.no-open-tickets" -> "general.no-open-tickets";
|
||||
case "messages.join-open-tickets" -> "join.open-tickets";
|
||||
case "messages.already-claimed" -> "general.already-claimed";
|
||||
case "messages.ticket-not-found" -> "general.ticket-not-found";
|
||||
case "messages.cooldown" -> "general.cooldown";
|
||||
case "messages.category-invalid" -> "create.category-invalid";
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
// ─────────────────────────── Getter ────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Aktualisiert serverName und debug-Flag aus der (bereits neu geladenen) Config.
|
||||
* Muss nach plugin.reloadConfig() aufgerufen werden.
|
||||
*/
|
||||
public void reloadSettings() {
|
||||
serverName = getConfig().getString("server-name", "unknown");
|
||||
if ("unknown".equals(serverName)) {
|
||||
getLogger().warning("[BungeeCord] Kein 'server-name' in der config.yml definiert!");
|
||||
}
|
||||
debug = getConfig().getBoolean("debug", false);
|
||||
}
|
||||
|
||||
public static TicketPlugin getInstance() { return instance; }
|
||||
public LanguageManager getLanguageManager() { return languageManager; }
|
||||
public DatabaseManager getDatabaseManager() { return databaseManager; }
|
||||
public TicketManager getTicketManager() { return ticketManager; }
|
||||
public CategoryManager getCategoryManager() { return categoryManager; }
|
||||
public FaqManager getFaqManager() { return faqManager; }
|
||||
public TicketGUI getTicketGUI() { return ticketGUI; }
|
||||
public FaqGUI getFaqGUI() { return faqGUI; }
|
||||
public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
|
||||
public BungeeMessenger getBungeeMessenger() { return bungeeMessenger; }
|
||||
public TicketCache getTicketCache() { return ticketCache; }
|
||||
public boolean isDebug() { return debug; }
|
||||
|
||||
/**
|
||||
* BungeeCord: Gibt den konfigurierten Server-Namen zurück.
|
||||
* Entspricht dem Wert aus config.yml → server-name.
|
||||
*/
|
||||
public String getServerName() { return serverName; }
|
||||
|
||||
/**
|
||||
* BungeeCord: Gibt zurück ob Cross-Server-Features aktiviert sind.
|
||||
* Entspricht config.yml → bungeecord: true
|
||||
*/
|
||||
public boolean isBungeeCordEnabled() { return getConfig().getBoolean("bungeecord", false); }
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* UpdateChecker für SpigotMC-Plugins.
|
||||
* Prüft asynchron, ob eine neue Version verfügbar ist.
|
||||
* Quelle: https://www.spigotmc.org/wiki/creating-an-update-checker-that-checks-for-updates
|
||||
* Prüft asynchron ob eine neue Version verfügbar ist.
|
||||
* Gibt den Consumer nur aus, wenn die Spigot-Version NEUER ist als die lokale.
|
||||
*/
|
||||
public class UpdateChecker {
|
||||
private final JavaPlugin plugin;
|
||||
@@ -24,17 +24,51 @@ 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)) {
|
||||
try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource="
|
||||
+ this.resourceId).openStream();
|
||||
Scanner scann = new Scanner(is)) {
|
||||
if (scann.hasNext()) {
|
||||
String latest = scann.next();
|
||||
plugin.getLogger().info("[UpdateChecker] Spigot-API Rückgabe: '" + latest + "'");
|
||||
consumer.accept(latest);
|
||||
} else {
|
||||
plugin.getLogger().warning("[UpdateChecker] Keine Version von Spigot erhalten!");
|
||||
String spigotVersion = scann.next().trim();
|
||||
String localVersion = plugin.getDescription().getVersion().trim();
|
||||
|
||||
// Nur melden wenn Spigot-Version wirklich neuer ist
|
||||
if (isNewerVersion(spigotVersion, localVersion)) {
|
||||
consumer.accept(spigotVersion);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().info("Unable to check for updates: " + e.getMessage());
|
||||
// Netzwerkfehler schweigen – kein Spam in der Konsole
|
||||
if (((TicketPlugin) plugin).isDebug()) {
|
||||
plugin.getLogger().info("[UpdateChecker] Konnte nicht prüfen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vergleicht zwei semantische Versionen (z.B. "1.0.5" und "1.0.6").
|
||||
*
|
||||
* @param spigot Version von SpigotMC
|
||||
* @param local Lokale Plugin-Version
|
||||
* @return true wenn spigot NEUER ist als local
|
||||
*/
|
||||
private boolean isNewerVersion(String spigot, String local) {
|
||||
try {
|
||||
String[] spigotParts = spigot.split("\\.");
|
||||
String[] localParts = local.split("\\.");
|
||||
|
||||
int length = Math.max(spigotParts.length, localParts.length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
int s = i < spigotParts.length ? Integer.parseInt(spigotParts[i]) : 0;
|
||||
int l = i < localParts.length ? Integer.parseInt(localParts[i]) : 0;
|
||||
|
||||
if (s > l) return true; // Spigot ist neuer
|
||||
if (s < l) return false; // Lokal ist neuer (z.B. noch nicht veröffentlicht)
|
||||
}
|
||||
return false; // Versionen identisch
|
||||
} catch (NumberFormatException e) {
|
||||
// Fallback: einfacher String-Vergleich falls Format ungewöhnlich
|
||||
return !spigot.equals(local);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,9 +149,9 @@ public class BungeeMessenger implements PluginMessageListener {
|
||||
|
||||
ByteArrayDataOutput inner = ByteStreams.newDataOutput();
|
||||
inner.writeByte(TYPE_PLAYER_MSG);
|
||||
inner.writeShort(uuidBytes.length);
|
||||
inner.writeInt(uuidBytes.length);
|
||||
inner.write(uuidBytes);
|
||||
inner.writeShort(msgBytes.length);
|
||||
inner.writeInt(msgBytes.length);
|
||||
inner.write(msgBytes);
|
||||
byte[] innerBytes = inner.toByteArray();
|
||||
|
||||
@@ -159,7 +159,7 @@ public class BungeeMessenger implements PluginMessageListener {
|
||||
out.writeUTF("Forward");
|
||||
out.writeUTF("ALL");
|
||||
out.writeUTF(CUSTOM_CHANNEL);
|
||||
out.writeShort(innerBytes.length);
|
||||
out.writeInt(innerBytes.length);
|
||||
out.write(innerBytes);
|
||||
|
||||
messenger.sendPluginMessage(plugin, BUNGEE_CHANNEL, out.toByteArray());
|
||||
@@ -198,12 +198,12 @@ public class BungeeMessenger implements PluginMessageListener {
|
||||
);
|
||||
|
||||
} else if (type == TYPE_PLAYER_MSG) {
|
||||
int uuidLen = in.readShort();
|
||||
int uuidLen = in.readInt();
|
||||
byte[] uuidBytes = new byte[uuidLen];
|
||||
in.readFully(uuidBytes);
|
||||
UUID targetUUID = UUID.fromString(new String(uuidBytes, StandardCharsets.UTF_8));
|
||||
|
||||
int msgLen = in.readShort();
|
||||
int msgLen = in.readInt();
|
||||
byte[] msgBytes = new byte[msgLen];
|
||||
in.readFully(msgBytes);
|
||||
String message = new String(msgBytes, StandardCharsets.UTF_8);
|
||||
@@ -246,7 +246,7 @@ public class BungeeMessenger implements PluginMessageListener {
|
||||
out.writeUTF("Forward");
|
||||
out.writeUTF("ALL");
|
||||
out.writeUTF(CUSTOM_CHANNEL);
|
||||
out.writeShort(innerBytes.length);
|
||||
out.writeInt(innerBytes.length);
|
||||
out.write(innerBytes);
|
||||
|
||||
messenger.sendPluginMessage(plugin, BUNGEE_CHANNEL, out.toByteArray());
|
||||
|
||||
93
src/main/java/de/ticketsystem/cache/TicketCache.java
vendored
Normal file
93
src/main/java/de/ticketsystem/cache/TicketCache.java
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
package de.ticketsystem.cache;
|
||||
|
||||
import de.ticketsystem.model.Ticket;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Einfacher TTL-basierter In-Memory-Cache für Ticket-Objekte.
|
||||
*
|
||||
* Reduziert wiederholte Datenbankabfragen beim häufigen Lesen desselben
|
||||
* Tickets (z.B. beim Öffnen der GUI, Kommentarbenachrichtigungen usw.).
|
||||
*
|
||||
* Standard-TTL: 60 Sekunden (konfigurierbar per Konstruktor).
|
||||
*
|
||||
* Thread-sicher (ConcurrentHashMap) – kann aus asynchronen Tasks heraus
|
||||
* lese- und schreibend aufgerufen werden.
|
||||
*/
|
||||
public class TicketCache {
|
||||
|
||||
private static final long DEFAULT_TTL_MS = 60_000L; // 60 Sekunden
|
||||
|
||||
private final long ttlMs;
|
||||
|
||||
/** Cache-Eintrag: Ticket + Verfallszeitstempel */
|
||||
private record CacheEntry(Ticket ticket, long expiresAt) {}
|
||||
|
||||
private final Map<Integer, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||
|
||||
// ─────────────────────────── Konstruktor ───────────────────────────────
|
||||
|
||||
public TicketCache() {
|
||||
this(DEFAULT_TTL_MS);
|
||||
}
|
||||
|
||||
public TicketCache(long ttlMs) {
|
||||
this.ttlMs = ttlMs;
|
||||
}
|
||||
|
||||
// ─────────────────────────── Public API ────────────────────────────────
|
||||
|
||||
/**
|
||||
* Gibt ein gecachtes Ticket zurück oder {@code null} wenn nicht vorhanden
|
||||
* oder der Eintrag abgelaufen ist.
|
||||
*/
|
||||
public Ticket get(int ticketId) {
|
||||
CacheEntry entry = cache.get(ticketId);
|
||||
if (entry == null) return null;
|
||||
if (System.currentTimeMillis() > entry.expiresAt()) {
|
||||
cache.remove(ticketId);
|
||||
return null;
|
||||
}
|
||||
return entry.ticket();
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert ein Ticket im Cache.
|
||||
* Überschreibt vorhandene Einträge.
|
||||
*/
|
||||
public void put(Ticket ticket) {
|
||||
if (ticket == null) return;
|
||||
cache.put(ticket.getId(), new CacheEntry(ticket, System.currentTimeMillis() + ttlMs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt ein Ticket aus dem Cache (z.B. nach einem Update).
|
||||
*/
|
||||
public void invalidate(int ticketId) {
|
||||
cache.remove(ticketId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leert den gesamten Cache (z.B. nach einem Plugin-Reload).
|
||||
*/
|
||||
public void clear() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt alle abgelaufenen Einträge.
|
||||
* Sollte periodisch aufgerufen werden um Speicher freizugeben.
|
||||
*/
|
||||
public void evictExpired() {
|
||||
long now = System.currentTimeMillis();
|
||||
cache.entrySet().removeIf(e -> now > e.getValue().expiresAt());
|
||||
}
|
||||
|
||||
/** Gibt die aktuelle Anzahl der (möglicherweise teils abgelaufenen) Einträge zurück. */
|
||||
public int size() {
|
||||
return cache.size();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -128,7 +128,7 @@ public class DatabaseManager {
|
||||
dataSource = new HikariDataSource(config);
|
||||
createTables();
|
||||
ensureColumns();
|
||||
plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt.");
|
||||
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] MySQL-Verbindung erfolgreich hergestellt.");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e);
|
||||
@@ -143,7 +143,7 @@ public class DatabaseManager {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
plugin.getLogger().info("MySQL deaktiviert. Verwende Datei-Speicherung (data.yml).");
|
||||
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] MySQL deaktiviert. Verwende Datei-Speicherung (data.yml).");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@ public class DatabaseManager {
|
||||
public void disconnect() {
|
||||
if (useMySQL && dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
plugin.getLogger().info("MySQL-Verbindung getrennt.");
|
||||
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] MySQL-Verbindung getrennt.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1102,6 +1102,38 @@ public class DatabaseManager {
|
||||
|
||||
// ─────────────────────────── Abfragen ──────────────────────────────────
|
||||
|
||||
/**
|
||||
* Gibt alle geschlossenen Tickets eines bestimmten Spielers zurück,
|
||||
* die noch nicht als close_notified markiert wurden.
|
||||
* Deutlich effizienter als alle CLOSED-Tickets zu laden und per UUID zu filtern.
|
||||
*/
|
||||
public List<Ticket> getUnnotifiedClosedTicketsByPlayer(UUID uuid) {
|
||||
List<Ticket> list = new ArrayList<>();
|
||||
if (useMySQL) {
|
||||
String sql = "SELECT * FROM tickets WHERE creator_uuid = ? AND status = 'CLOSED' AND close_notified = 0";
|
||||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setString(1, uuid.toString());
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) list.add(mapRow(rs));
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Abrufen der Tickets: " + e.getMessage(), e);
|
||||
}
|
||||
return list;
|
||||
} else {
|
||||
if (!dataConfig.contains("tickets")) return list;
|
||||
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
|
||||
Ticket t = (Ticket) dataConfig.get("tickets." + key);
|
||||
if (t != null
|
||||
&& uuid.equals(t.getCreatorUUID())
|
||||
&& t.getStatus() == TicketStatus.CLOSED
|
||||
&& !t.isCloseNotified()) {
|
||||
list.add(t);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Ticket> getTicketsByStatus(TicketStatus... statuses) {
|
||||
List<Ticket> list = new ArrayList<>();
|
||||
if (statuses.length == 0) return list;
|
||||
|
||||
578
src/main/java/de/ticketsystem/gui/FaqGUI.java
Normal file
578
src/main/java/de/ticketsystem/gui/FaqGUI.java
Normal file
@@ -0,0 +1,578 @@
|
||||
package de.ticketsystem.gui;
|
||||
|
||||
import de.ticketsystem.TicketPlugin;
|
||||
import de.ticketsystem.model.FaqEntry;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
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.URL;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* FAQ GUI für Spieler (Lesemodus) und Admins (Verwaltungsmodus).
|
||||
* Layout, Größe und Slots sind über config.yml steuerbar.
|
||||
*/
|
||||
public class FaqGUI implements Listener {
|
||||
|
||||
// ── Konfigurierbare Felder ────────────────────────────────────────────────
|
||||
private int faqRows;
|
||||
private int faqNavPrev, faqNavNext, faqNavAdd, faqNavPage;
|
||||
|
||||
private Material headMaterial = Material.PLAYER_HEAD;
|
||||
private String headTexture = "http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e";
|
||||
private List<Integer> contentSlots = new ArrayList<>();
|
||||
|
||||
// ── System Felder ────────────────────────────────────────────────────────
|
||||
private static final String DEFAULT_SKIN_URL =
|
||||
"http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e";
|
||||
|
||||
private final TicketPlugin plugin;
|
||||
|
||||
private final Map<UUID, Map<Integer, FaqEntry>> slotMap = new HashMap<>();
|
||||
private final Map<UUID, Integer> faqPage = new HashMap<>();
|
||||
private final Set<UUID> adminView = new HashSet<>();
|
||||
private final Map<UUID, FaqEntry> actionEntry = new HashMap<>();
|
||||
|
||||
private final Map<UUID, String> awaitingQuestion = new HashMap<>();
|
||||
private final Map<UUID, String> awaitingAnswer = new HashMap<>();
|
||||
|
||||
// Materialien für Navigations-Buttons
|
||||
private Material matNavPrev, matNavNext, matNavPage, matNavAdd;
|
||||
|
||||
public FaqGUI(TicketPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
reloadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Item-Slots:
|
||||
* Schachbrett-Muster ohne Glas – passt sich automatisch an jede Größe an:
|
||||
*
|
||||
* Reihe 0 : Glas in geraden Spalten (0,2,4,6,8), Items in ungeraden (1,3,5,7) → 4 Items
|
||||
* Reihe 1 : Items in geraden Spalten (0,2,4,6,8), leer in ungeraden → 5 Items
|
||||
* Reihe 2 : Items in ungeraden Spalten (1,3,5,7), leer in geraden → 4 Items
|
||||
* Reihe 3 : Items in geraden Spalten (0,2,4,6,8), leer in ungeraden → 5 Items
|
||||
* ... (Reihe 1+ kein Glas – nur Items und leere Slots wechselnd)
|
||||
* Letzte Reihe: Navigation (Footer)
|
||||
*
|
||||
* Kapazität:
|
||||
* 4 Reihen → 13 Items | 5 Reihen → 18 Items | 6 Reihen → 22 Items
|
||||
*/
|
||||
private List<Integer> buildPatternSlots(int rows) {
|
||||
List<Integer> slots = new ArrayList<>();
|
||||
int contentRows = rows - 1; // Letzte Reihe = Navigation
|
||||
for (int row = 0; row < contentRows; row++) {
|
||||
int base = row * 9;
|
||||
if (row % 2 == 0) {
|
||||
// Gerade Reihen (0,2,4,...): Items in geraden Spalten
|
||||
slots.add(base + 0);
|
||||
slots.add(base + 2);
|
||||
slots.add(base + 4);
|
||||
slots.add(base + 6);
|
||||
slots.add(base + 8);
|
||||
} else {
|
||||
// Ungerade Reihen (1,3,5,...): Items in ungeraden Spalten
|
||||
slots.add(base + 1);
|
||||
slots.add(base + 3);
|
||||
slots.add(base + 5);
|
||||
slots.add(base + 7);
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Navigations-Slot-Defaults passend zur aktuellen Zeilenanzahl.
|
||||
* Nav-Leiste liegt immer in der letzten Reihe.
|
||||
*/
|
||||
/**
|
||||
* Nav-Leiste liegt in der letzten Reihe.
|
||||
* Gerade Spalten werden durch fillNavBar bereits mit Glas gefüllt.
|
||||
* Buttons liegen auf ungerade Spalten – passend zum Content-Muster:
|
||||
* Slot +1 = Prev | Slot +3 = Add | Slot +5 = Page | Slot +7 = Next
|
||||
*/
|
||||
private void applyNavDefaults(int rows) {
|
||||
int navBase = (rows - 1) * 9; // Erster Slot der letzten Reihe
|
||||
faqNavPrev = navBase + 1; // 2. Slot (ungerade) – Blättern zurück
|
||||
faqNavAdd = navBase + 3; // 4. Slot (ungerade) – Hinzufügen
|
||||
faqNavPage = navBase + 5; // 6. Slot (ungerade) – Seitenanzeige
|
||||
faqNavNext = navBase + 7; // 8. Slot (ungerade) – Blättern vor
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Konfiguration sicher mit Fallback-Werten.
|
||||
*/
|
||||
public void reloadConfig() {
|
||||
// ── 1. STANDARDWERTE SETZEN (Sicherheit gegen leere Config) ─────────
|
||||
faqRows = 6;
|
||||
|
||||
headMaterial = Material.PLAYER_HEAD;
|
||||
headTexture = DEFAULT_SKIN_URL;
|
||||
|
||||
matNavPrev = Material.ARROW;
|
||||
matNavNext = Material.ARROW;
|
||||
matNavPage = Material.PAPER;
|
||||
matNavAdd = Material.LIME_WOOL;
|
||||
|
||||
// Standard Content-Slots nach Muster: Glasscheiben in Spalte 0, 4, 8
|
||||
contentSlots = buildPatternSlots(faqRows);
|
||||
applyNavDefaults(faqRows);
|
||||
|
||||
// ── 2. VERSUCHEN AUS CONFIG ZU LADEN ─────────────────────────────────
|
||||
ConfigurationSection guiSettings = plugin.getConfig().getConfigurationSection("gui-settings");
|
||||
|
||||
if (guiSettings != null) {
|
||||
ConfigurationSection faqConf = guiSettings.getConfigurationSection("faq");
|
||||
if (faqConf != null) {
|
||||
// Rows laden
|
||||
faqRows = faqConf.getInt("rows", 6);
|
||||
if (faqRows < 4) faqRows = 4; // Minimum 4 Reihen
|
||||
if (faqRows > 6) faqRows = 6;
|
||||
|
||||
// Nav-Defaults für die gewählte Zeilenanzahl setzen
|
||||
applyNavDefaults(faqRows);
|
||||
|
||||
// Content Slots laden – nur überschreiben wenn explizit in config gesetzt
|
||||
if (faqConf.contains("content-slots") && !faqConf.getIntegerList("content-slots").isEmpty()) {
|
||||
contentSlots = faqConf.getIntegerList("content-slots");
|
||||
} else {
|
||||
// Muster für die konfigurierte Zeilenanzahl neu berechnen
|
||||
contentSlots = buildPatternSlots(faqRows);
|
||||
}
|
||||
|
||||
// Navigation Slots laden (Config überschreibt dynamische Defaults)
|
||||
faqNavPrev = getSlot(faqConf, "nav.prev", faqNavPrev, faqRows);
|
||||
faqNavNext = getSlot(faqConf, "nav.next", faqNavNext, faqRows);
|
||||
faqNavPage = getSlot(faqConf, "nav.page", faqNavPage, faqRows);
|
||||
faqNavAdd = getSlot(faqConf, "nav.add", faqNavAdd, faqRows);
|
||||
|
||||
// Head Config laden
|
||||
ConfigurationSection headConf = faqConf.getConfigurationSection("head-item");
|
||||
if (headConf != null) {
|
||||
headMaterial = getMaterial(headConf, "material", Material.PLAYER_HEAD);
|
||||
headTexture = headConf.getString("texture", DEFAULT_SKIN_URL);
|
||||
}
|
||||
}
|
||||
|
||||
// Materialien laden (Global)
|
||||
ConfigurationSection itemsSettings = guiSettings.getConfigurationSection("items");
|
||||
if (itemsSettings != null) {
|
||||
matNavPrev = getMaterial(itemsSettings, "nav-prev", Material.ARROW);
|
||||
matNavNext = getMaterial(itemsSettings, "nav-next", Material.ARROW);
|
||||
matNavPage = getMaterial(itemsSettings, "nav-page", Material.PAPER);
|
||||
matNavAdd = getMaterial(itemsSettings, "nav-add", Material.LIME_WOOL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getSlot(ConfigurationSection section, String path, int def, int rows) {
|
||||
int val = section.getInt(path, def);
|
||||
int max = rows * 9;
|
||||
if (val >= 0 && val < max) return val;
|
||||
// Slot liegt außerhalb des Inventars → Spalte beibehalten, letzte Reihe verwenden
|
||||
int col = (val >= 0 ? val : def) % 9;
|
||||
return (rows - 1) * 9 + col;
|
||||
}
|
||||
|
||||
private Material getMaterial(ConfigurationSection section, String path, Material def) {
|
||||
try {
|
||||
return Material.valueOf(section.getString(path, def.name()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfsmethode für Sprachzugriff ─────────────────────────────────────────
|
||||
private String f(String key) {
|
||||
return plugin.lang().get("gui.faq." + key);
|
||||
}
|
||||
|
||||
private String f(String key, String... replacements) {
|
||||
return plugin.lang().format("gui.faq." + key, replacements);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// PUBLIC OPEN-METHODEN
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
public void openFaqGUI(Player player) {
|
||||
openFaqGUI(player, faqPage.getOrDefault(player.getUniqueId(), 0));
|
||||
}
|
||||
|
||||
public void openFaqGUI(Player player, int page) {
|
||||
boolean isAdmin = player.hasPermission("ticket.admin");
|
||||
String title = isAdmin ? f("admin-title") : f("title");
|
||||
|
||||
List<FaqEntry> all = plugin.getFaqManager().getAll();
|
||||
|
||||
// Sicherheit gegen leere Content-Slots Liste (Division by Zero)
|
||||
int pageSize = contentSlots.isEmpty() ? 45 : contentSlots.size();
|
||||
|
||||
int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / pageSize));
|
||||
page = Math.max(0, Math.min(page, totalPages - 1));
|
||||
faqPage.put(player.getUniqueId(), page);
|
||||
|
||||
int invSize = faqRows * 9;
|
||||
Inventory inv = Bukkit.createInventory(null, invSize, title);
|
||||
Map<Integer, FaqEntry> sm = new HashMap<>();
|
||||
|
||||
int start = page * pageSize;
|
||||
int itemsOnCurrentPage = 0; // Zähler für Items auf dieser Seite
|
||||
|
||||
// Wir nutzen entweder die konfigurierten Slots oder sequentiell von 0, falls Liste leer
|
||||
for (int i = 0; i < pageSize && (start + i) < all.size(); i++) {
|
||||
FaqEntry entry = all.get(start + i);
|
||||
int slot = contentSlots.isEmpty() ? i : contentSlots.get(i);
|
||||
|
||||
if (slot < invSize) {
|
||||
inv.setItem(slot, buildFaqItem(entry, isAdmin));
|
||||
sm.put(slot, entry);
|
||||
itemsOnCurrentPage++;
|
||||
}
|
||||
}
|
||||
|
||||
slotMap.put(player.getUniqueId(), sm);
|
||||
|
||||
if (isAdmin) adminView.add(player.getUniqueId());
|
||||
else adminView.remove(player.getUniqueId());
|
||||
|
||||
// Kein Glas im Content-Bereich – nur Footer (fillNavBar) hat Glasscheiben
|
||||
|
||||
// Übergeben der korrekten Anzahl an die Navigationsleiste
|
||||
fillNavBar(inv, page, totalPages, isAdmin, all.isEmpty(), itemsOnCurrentPage);
|
||||
player.openInventory(inv);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// CLICK-EVENTS
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||
String title = event.getView().getTitle();
|
||||
|
||||
String playerTitle = f("title");
|
||||
String adminTitle = f("admin-title");
|
||||
String actionTitle = f("action-title");
|
||||
|
||||
if (!title.equals(playerTitle) && !title.equals(adminTitle)
|
||||
&& !title.equals(actionTitle)) return;
|
||||
|
||||
event.setCancelled(true);
|
||||
int slot = event.getRawSlot();
|
||||
if (slot < 0) return;
|
||||
|
||||
if (title.equals(actionTitle)) {
|
||||
FaqEntry entry = actionEntry.get(player.getUniqueId());
|
||||
if (entry == null) return;
|
||||
switch (slot) {
|
||||
case 10 -> startEditFlow(player, entry);
|
||||
case 12 -> deleteFaq(player, entry);
|
||||
case 16 -> openFaqGUI(player);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isAdmin = adminView.contains(player.getUniqueId());
|
||||
int curPage = faqPage.getOrDefault(player.getUniqueId(), 0);
|
||||
|
||||
if (slot == faqNavPrev) { openFaqGUI(player, curPage - 1); return; }
|
||||
if (slot == faqNavNext) { openFaqGUI(player, curPage + 1); return; }
|
||||
if (slot == faqNavAdd && isAdmin) { startAddFlow(player); return; }
|
||||
|
||||
// Check ob Slot in contentSlots ist
|
||||
if (contentSlots.contains(slot)) {
|
||||
Map<Integer, FaqEntry> sm = slotMap.get(player.getUniqueId());
|
||||
if (sm == null) return;
|
||||
FaqEntry entry = sm.get(slot);
|
||||
if (entry == null) return;
|
||||
|
||||
if (isAdmin) {
|
||||
openActionGUI(player, entry);
|
||||
} else {
|
||||
player.closeInventory();
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
player.sendMessage(plugin.lang().format("faq.list-entry",
|
||||
"{id}", String.valueOf(entry.getId()), "{question}", entry.getQuestion()));
|
||||
player.sendMessage(plugin.lang().color("§f" + entry.getAnswer()));
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── Admin-Aktions-GUI ─────────────────────────
|
||||
|
||||
private void openActionGUI(Player player, FaqEntry entry) {
|
||||
actionEntry.put(player.getUniqueId(), entry);
|
||||
Inventory inv = Bukkit.createInventory(null, 27, f("action-title"));
|
||||
|
||||
inv.setItem(4, buildFaqItem(entry, false));
|
||||
inv.setItem(10, buildItem(Material.WRITABLE_BOOK, f("edit-button"),
|
||||
List.of(f("edit-lore-1"), f("edit-lore-2"))));
|
||||
inv.setItem(12, buildItem(Material.BARRIER, f("delete-button"),
|
||||
List.of(f("delete-lore-1"), f("delete-lore-2"))));
|
||||
inv.setItem(16, buildItem(Material.ARROW, f("back-button"),
|
||||
List.of(f("back-lore"))));
|
||||
|
||||
fillGlass(inv);
|
||||
player.openInventory(inv);
|
||||
}
|
||||
|
||||
// ─────────────────────────── Chat-Flow: Hinzufügen ─────────────────────
|
||||
|
||||
private void startAddFlow(Player player) {
|
||||
player.closeInventory();
|
||||
awaitingQuestion.put(player.getUniqueId(), "new");
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
player.sendMessage(f("chat-create-title"));
|
||||
player.sendMessage(f("chat-question-prompt"));
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
}
|
||||
|
||||
// ─────────────────────────── Chat-Flow: Bearbeiten ─────────────────────
|
||||
|
||||
private void startEditFlow(Player player, FaqEntry entry) {
|
||||
player.closeInventory();
|
||||
awaitingQuestion.put(player.getUniqueId(), "edit:" + entry.getId());
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
player.sendMessage(f("chat-edit-title", "{id}", String.valueOf(entry.getId())));
|
||||
player.sendMessage(f("chat-current-question", "{question}", entry.getQuestion()));
|
||||
player.sendMessage(f("chat-question-prompt"));
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
}
|
||||
|
||||
// ─────────────────────────── Löschen ───────────────────────────────────
|
||||
|
||||
private void deleteFaq(Player player, FaqEntry entry) {
|
||||
player.closeInventory();
|
||||
boolean success = plugin.getFaqManager().delete(entry.getId());
|
||||
if (success) {
|
||||
player.sendMessage(plugin.lang().format("faq.deleted", "{id}", String.valueOf(entry.getId())));
|
||||
} else {
|
||||
player.sendMessage(f("delete-error", "{id}", String.valueOf(entry.getId())));
|
||||
}
|
||||
openFaqGUI(player);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// CHAT-EVENTS
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerChat(AsyncPlayerChatEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
UUID uuid = player.getUniqueId();
|
||||
|
||||
// ── Schritt 1: Warte auf Frage ─────────────────────────────────────
|
||||
if (awaitingQuestion.containsKey(uuid)) {
|
||||
event.setCancelled(true);
|
||||
String state = awaitingQuestion.remove(uuid);
|
||||
String input = event.getMessage().trim();
|
||||
|
||||
if (input.equalsIgnoreCase("cancel")) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
player.sendMessage(plugin.lang().get("gui.close-cancelled"));
|
||||
openFaqGUI(player);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
awaitingAnswer.put(uuid, state + "\u0000" + input);
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
player.sendMessage(f("question-set", "{question}", input));
|
||||
player.sendMessage(f("chat-answer-prompt"));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Schritt 2: Warte auf Antwort ───────────────────────────────────
|
||||
if (awaitingAnswer.containsKey(uuid)) {
|
||||
event.setCancelled(true);
|
||||
String stateAndQuestion = awaitingAnswer.remove(uuid);
|
||||
String input = event.getMessage().trim();
|
||||
|
||||
if (input.equalsIgnoreCase("cancel")) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
player.sendMessage(plugin.lang().get("gui.close-cancelled"));
|
||||
openFaqGUI(player);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
int sep = stateAndQuestion.indexOf("\u0000");
|
||||
String state = stateAndQuestion.substring(0, sep);
|
||||
String question = stateAndQuestion.substring(sep + 1);
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (state.equals("new")) {
|
||||
FaqEntry created = plugin.getFaqManager().add(question, input);
|
||||
player.sendMessage(plugin.lang().format("faq.created", "{id}", String.valueOf(created.getId())));
|
||||
} else {
|
||||
int id;
|
||||
try {
|
||||
id = Integer.parseInt(state.substring(5));
|
||||
} catch (NumberFormatException e) {
|
||||
player.sendMessage(f("internal-error"));
|
||||
openFaqGUI(player);
|
||||
return;
|
||||
}
|
||||
boolean ok = plugin.getFaqManager().edit(id, question, input);
|
||||
if (ok) player.sendMessage(plugin.lang().format("faq.updated", "{id}", String.valueOf(id)));
|
||||
else player.sendMessage(plugin.lang().format("faq.not-found", "{id}", String.valueOf(id)));
|
||||
}
|
||||
openFaqGUI(player);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// ITEM-BUILDER
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
private ItemStack buildFaqItem(FaqEntry entry, boolean adminHint) {
|
||||
ItemStack item;
|
||||
|
||||
if (headMaterial == Material.PLAYER_HEAD) {
|
||||
try {
|
||||
item = new ItemStack(Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) item.getItemMeta();
|
||||
if (meta != null) {
|
||||
PlayerProfile profile = Bukkit.createPlayerProfile(
|
||||
UUID.nameUUIDFromBytes(("FAQ_" + entry.getId()).getBytes()), "FAQ_" + entry.getId());
|
||||
PlayerTextures textures = profile.getTextures();
|
||||
textures.setSkin(new URL(headTexture));
|
||||
profile.setTextures(textures);
|
||||
meta.setOwnerProfile(profile);
|
||||
meta.setDisplayName("§e§l" + entry.getQuestion());
|
||||
meta.setLore(buildFaqLore(entry, adminHint));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
item = buildFallbackItem(entry, adminHint);
|
||||
}
|
||||
} else {
|
||||
item = new ItemStack(headMaterial);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName("§e§l" + entry.getQuestion());
|
||||
meta.setLore(buildFaqLore(entry, adminHint));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack buildFallbackItem(FaqEntry entry, boolean adminHint) {
|
||||
ItemStack item = new ItemStack(Material.BOOK);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName("§e§l" + entry.getQuestion());
|
||||
meta.setLore(buildFaqLore(entry, adminHint));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private List<String> buildFaqLore(FaqEntry entry, boolean adminHint) {
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add(f("lore-separator"));
|
||||
lore.add(f("lore-id", "{id}", String.valueOf(entry.getId())));
|
||||
lore.add(f("lore-separator"));
|
||||
|
||||
String answer = entry.getAnswer();
|
||||
int chunkSize = 40;
|
||||
for (int i = 0; i < answer.length(); i += chunkSize) {
|
||||
int end = Math.min(i + chunkSize, answer.length());
|
||||
if (end < answer.length() && answer.charAt(end) != ' ') {
|
||||
int lastSpace = answer.lastIndexOf(' ', end);
|
||||
if (lastSpace > i) end = lastSpace;
|
||||
}
|
||||
lore.add("§f" + answer.substring(i, end).trim());
|
||||
i = end - chunkSize;
|
||||
}
|
||||
|
||||
lore.add(f("lore-separator"));
|
||||
if (adminHint) lore.add(f("click-edit"));
|
||||
else lore.add(f("click-detail"));
|
||||
return lore;
|
||||
}
|
||||
|
||||
private ItemStack buildItem(Material material, String displayName, List<String> lore) {
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return item;
|
||||
meta.setDisplayName(displayName);
|
||||
meta.setLore(lore);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
// ─────────────────────────── Navigationsleiste ─────────────────────────
|
||||
|
||||
private void fillNavBar(Inventory inv, int page, int totalPages, boolean isAdmin, boolean isEmpty, int itemCount) {
|
||||
ItemStack glass = makeGlass();
|
||||
// Untere Reihe füllen (Nav-Bar)
|
||||
for (int i = inv.getSize() - 9; i < inv.getSize(); i++) inv.setItem(i, glass);
|
||||
|
||||
if (page > 0) {
|
||||
inv.setItem(faqNavPrev, buildActionItem(matNavPrev,
|
||||
f("nav-prev"),
|
||||
List.of(f("nav-prev-lore", "{page}", String.valueOf(page), "{total}", String.valueOf(totalPages)))));
|
||||
}
|
||||
if (page < totalPages - 1) {
|
||||
inv.setItem(faqNavNext, buildActionItem(matNavNext,
|
||||
f("nav-next"),
|
||||
List.of(f("nav-next-lore", "{page}", String.valueOf(page + 2), "{total}", String.valueOf(totalPages)))));
|
||||
}
|
||||
|
||||
// itemCount ist jetzt die tatsächliche Anzahl der Items auf der Seite
|
||||
int displayCount = isEmpty ? 0 : itemCount;
|
||||
|
||||
inv.setItem(faqNavPage, buildActionItem(matNavPage, f("nav-page", "{page}", String.valueOf(page + 1), "{total}", String.valueOf(totalPages)),
|
||||
List.of(f("nav-page-lore", "{count}", String.valueOf(displayCount)))));
|
||||
|
||||
if (isAdmin) {
|
||||
inv.setItem(faqNavAdd, buildActionItem(matNavAdd, f("add-button"),
|
||||
List.of(f("add-lore-1"), f("add-lore-2"))));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillGlass(Inventory inv) {
|
||||
ItemStack glass = makeGlass();
|
||||
for (int i = 0; i < inv.getSize(); i++) {
|
||||
if (inv.getItem(i) == null) inv.setItem(i, glass);
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack makeGlass() {
|
||||
ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
|
||||
ItemMeta meta = glass.getItemMeta();
|
||||
if (meta != null) { meta.setDisplayName(" "); glass.setItemMeta(meta); }
|
||||
return glass;
|
||||
}
|
||||
|
||||
private ItemStack buildActionItem(Material material, String displayName, List<String> lore) {
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return item;
|
||||
meta.setDisplayName(displayName);
|
||||
meta.setLore(lore);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import java.util.List;
|
||||
import de.ticketsystem.TicketPlugin;
|
||||
import de.ticketsystem.database.DatabaseManager;
|
||||
import de.ticketsystem.model.Ticket;
|
||||
import de.ticketsystem.model.TicketStatus;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Location;
|
||||
@@ -33,18 +32,15 @@ public class PlayerJoinListener implements Listener {
|
||||
int count = plugin.getDatabaseManager().countOpenTickets();
|
||||
if (count > 0) {
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
String msg = plugin.formatMessage("messages.join-open-tickets")
|
||||
.replace("{count}", String.valueOf(count));
|
||||
player.sendMessage(msg);
|
||||
player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht."));
|
||||
player.sendMessage(plugin.lang().format("join.open-tickets",
|
||||
"{count}", String.valueOf(count)));
|
||||
player.sendMessage(plugin.lang().get("join.open-tickets-hint"));
|
||||
}, 40L);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── BungeeCord: ausstehenden Teleport-Auftrag prüfen ─────────────
|
||||
// Wenn ein Admin via GUI auf einen anderen Server geschickt wurde,
|
||||
// liegt hier die Zielposition. Wir teleportieren ihn nach dem Spawn.
|
||||
if (plugin.isBungeeCordEnabled()
|
||||
&& plugin.getConfig().getBoolean("bungee-teleport-enabled", true)) {
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
@@ -58,23 +54,20 @@ public class PlayerJoinListener implements Listener {
|
||||
if (!player.isOnline()) return;
|
||||
World world = Bukkit.getWorld(pt.world());
|
||||
if (world == null) {
|
||||
player.sendMessage(plugin.color(
|
||||
"&cTeleport-Zielwelt &e" + pt.world() + " &cnicht gefunden!"));
|
||||
player.sendMessage(plugin.lang().format("join.teleport-world-missing",
|
||||
"{world}", pt.world()));
|
||||
return;
|
||||
}
|
||||
Location loc = new Location(world, pt.x(), pt.y(), pt.z(), pt.yaw(), pt.pitch());
|
||||
player.teleport(loc);
|
||||
player.sendMessage(plugin.color(
|
||||
"&7Du wurdest zur Ticket-Position teleportiert. &8("
|
||||
+ String.format("%.0f, %.0f, %.0f", pt.x(), pt.y(), pt.z()) + ")"));
|
||||
String coords = String.format("%.0f, %.0f, %.0f", pt.x(), pt.y(), pt.z());
|
||||
player.sendMessage(plugin.lang().format("join.teleport-success", "{coords}", coords));
|
||||
});
|
||||
});
|
||||
// 40 Ticks (2 Sek) Verzögerung damit der Spieler vollständig gespawnt ist
|
||||
}, 40L);
|
||||
}
|
||||
|
||||
// ── Ausstehende Kommentar-/Schließ-Benachrichtigungen anzeigen ────
|
||||
// (Nachrichten die ankamen während der Spieler offline war)
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
if (!player.isOnline()) return;
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
@@ -82,36 +75,30 @@ public class PlayerJoinListener implements Listener {
|
||||
if (pending.isEmpty()) return;
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (!player.isOnline()) return;
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
player.sendMessage(plugin.color("&6Ticket-Benachrichtigungen &7(während du offline warst):"));
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
player.sendMessage(plugin.lang().get("join.pending-header"));
|
||||
for (String msg : pending) {
|
||||
player.sendMessage(plugin.color(msg));
|
||||
player.sendMessage(plugin.lang().color(msg));
|
||||
}
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
});
|
||||
plugin.getDatabaseManager().clearPendingNotifications(player.getUniqueId());
|
||||
});
|
||||
}, 60L);
|
||||
|
||||
// ── [NEU] Spieler: Ticket-claimed-Benachrichtigung für Offline-Zeit ──
|
||||
// Läuft mit 60 Ticks Verzögerung (3 Sek) damit der Spieler zuerst normal spawnt
|
||||
// ── Spieler: Ticket-claimed-Benachrichtigung für Offline-Zeit ──────
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
if (!player.isOnline()) return;
|
||||
plugin.getTicketManager().notifyClaimedWhileOffline(player);
|
||||
}, 60L);
|
||||
|
||||
// ── Spieler: über geschlossene Tickets informieren (nur wenn noch nicht geschehen) ──
|
||||
// Bug-Fix: Nutzt close_notified aus der DB statt in-memory Set.
|
||||
// Verhindert Duplikate bei Server-Wechseln in BungeeCord-Netzwerken.
|
||||
// ── Spieler: über geschlossene Tickets informieren ────────────────
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
// Nur Tickets dieses Spielers laden (nicht ALLE closed Tickets)
|
||||
List<Ticket> closed = plugin.getDatabaseManager()
|
||||
.getTicketsByStatus(TicketStatus.CLOSED);
|
||||
.getUnnotifiedClosedTicketsByPlayer(player.getUniqueId());
|
||||
|
||||
for (Ticket t : closed) {
|
||||
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
|
||||
// DB-Feld prüfen – funktioniert serverübergreifend
|
||||
if (t.isCloseNotified()) continue;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
plugin.getTicketManager().notifyCreatorClosed(t));
|
||||
}
|
||||
@@ -124,13 +111,13 @@ public class PlayerJoinListener implements Listener {
|
||||
new de.ticketsystem.UpdateChecker(plugin, resourceId).getVersion(version -> {
|
||||
String current = plugin.getDescription().getVersion();
|
||||
if (!current.equals(version)) {
|
||||
String bar = ChatColor.GOLD + "====================================================";
|
||||
player.sendMessage(bar);
|
||||
player.sendMessage(ChatColor.GOLD + "[TicketSystem] "
|
||||
+ ChatColor.YELLOW + "NEUES UPDATE VERFÜGBAR: v" + version);
|
||||
player.sendMessage(ChatColor.GOLD + "[TicketSystem] "
|
||||
+ ChatColor.YELLOW + "Download: https://www.spigotmc.org/resources/132757");
|
||||
player.sendMessage(bar);
|
||||
String bar = plugin.lang().get("update.available-bar");
|
||||
String line1 = plugin.lang().format("update.available-line1", "{version}", version);
|
||||
String line2 = plugin.lang().get("update.available-line2");
|
||||
player.sendMessage(ChatColor.GOLD + bar);
|
||||
player.sendMessage(line1);
|
||||
player.sendMessage(line2);
|
||||
player.sendMessage(ChatColor.GOLD + bar);
|
||||
}
|
||||
});
|
||||
}, 20L);
|
||||
|
||||
181
src/main/java/de/ticketsystem/manager/FaqManager.java
Normal file
181
src/main/java/de/ticketsystem/manager/FaqManager.java
Normal file
@@ -0,0 +1,181 @@
|
||||
package de.ticketsystem.manager;
|
||||
|
||||
import de.ticketsystem.TicketPlugin;
|
||||
import de.ticketsystem.model.FaqEntry;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Manages FAQ entries stored in faqs.yml.
|
||||
*
|
||||
* Admins can add, edit and delete FAQs in-game.
|
||||
* All changes are saved immediately to faqs.yml.
|
||||
*
|
||||
* faqs.yml layout:
|
||||
*
|
||||
* faqs:
|
||||
* 1:
|
||||
* question: "Wie erstelle ich ein Ticket?"
|
||||
* answer: "Nutze /ticket create [Kategorie] [Beschreibung]."
|
||||
* 2:
|
||||
* question: "..."
|
||||
* answer: "..."
|
||||
*/
|
||||
public class FaqManager {
|
||||
|
||||
private final TicketPlugin plugin;
|
||||
private final File faqFile;
|
||||
private YamlConfiguration faqConfig;
|
||||
private final List<FaqEntry> entries = new ArrayList<>();
|
||||
private int nextId = 1;
|
||||
|
||||
public FaqManager(TicketPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.faqFile = new File(plugin.getDataFolder(), "faqs.yml");
|
||||
load();
|
||||
}
|
||||
|
||||
// ─────────────────────────── Loading & Saving ───────────────────────────
|
||||
|
||||
private void load() {
|
||||
entries.clear();
|
||||
nextId = 1;
|
||||
|
||||
if (!faqFile.exists()) {
|
||||
try {
|
||||
faqFile.getParentFile().mkdirs();
|
||||
faqFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().severe("[FaqManager] Konnte faqs.yml nicht erstellen: " + e.getMessage());
|
||||
}
|
||||
faqConfig = new YamlConfiguration();
|
||||
loadDefaults();
|
||||
save();
|
||||
return;
|
||||
}
|
||||
|
||||
faqConfig = YamlConfiguration.loadConfiguration(faqFile);
|
||||
ConfigurationSection section = faqConfig.getConfigurationSection("faqs");
|
||||
|
||||
if (section != null) {
|
||||
for (String key : section.getKeys(false)) {
|
||||
try {
|
||||
int id = Integer.parseInt(key);
|
||||
String question = faqConfig.getString("faqs." + key + ".question", "");
|
||||
String answer = faqConfig.getString("faqs." + key + ".answer", "");
|
||||
if (!question.isBlank() && !answer.isBlank()) {
|
||||
entries.add(new FaqEntry(id, question, answer));
|
||||
if (id >= nextId) nextId = id + 1;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort(Comparator.comparingInt(FaqEntry::getId));
|
||||
|
||||
if (plugin.isDebug()) {
|
||||
plugin.getLogger().info("[FaqManager] " + entries.size() + " FAQ(s) geladen.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes the example FAQs into a freshly created faqs.yml. */
|
||||
private void loadDefaults() {
|
||||
writeEntry(1, "Wie erstelle ich ein Ticket?",
|
||||
"Nutze den Befehl /ticket create [Kategorie] [Prio][Beschreibung] um ein neues Ticket zu erstellen.");
|
||||
writeEntry(2, "Wie lange dauert die Bearbeitung?",
|
||||
"Unser Support-Team bearbeitet Tickets so schnell wie möglich. Bitte habe etwas Geduld.");
|
||||
writeEntry(3, "Kann ich mein Ticket löschen?",
|
||||
"Ja! Öffne /ticket list und klicke auf dein Ticket, um es aus der Übersicht zu entfernen.");
|
||||
writeEntry(4, "Wie kann ich meinen Support bewerten?",
|
||||
"Nach dem Schließen eines Tickets kannst du mit /ticket rate <ID> good/bad eine Bewertung abgeben.");
|
||||
nextId = 5;
|
||||
// Sync entries list with what we just wrote – Text muss identisch sein!
|
||||
entries.add(new FaqEntry(1, "Wie erstelle ich ein Ticket?",
|
||||
"Nutze den Befehl /ticket create [Kategorie] [Prio][Beschreibung] um ein neues Ticket zu erstellen."));
|
||||
entries.add(new FaqEntry(2, "Wie lange dauert die Bearbeitung?",
|
||||
"Unser Support-Team bearbeitet Tickets so schnell wie möglich. Bitte habe etwas Geduld."));
|
||||
entries.add(new FaqEntry(3, "Kann ich mein Ticket löschen?",
|
||||
"Ja! Öffne /ticket list und klicke auf dein Ticket, um es aus der Übersicht zu entfernen."));
|
||||
entries.add(new FaqEntry(4, "Wie kann ich meinen Support bewerten?",
|
||||
"Nach dem Schließen eines Tickets kannst du mit /ticket rate <ID> good/bad eine Bewertung abgeben."));
|
||||
}
|
||||
|
||||
private void writeEntry(int id, String question, String answer) {
|
||||
faqConfig.set("faqs." + id + ".question", question);
|
||||
faqConfig.set("faqs." + id + ".answer", answer);
|
||||
}
|
||||
|
||||
private void save() {
|
||||
try {
|
||||
faqConfig.save(faqFile);
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().severe("[FaqManager] Konnte faqs.yml nicht speichern: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── Public API ────────────────────────────────
|
||||
|
||||
/** Returns an unmodifiable view of all FAQ entries in ID order. */
|
||||
public List<FaqEntry> getAll() {
|
||||
return Collections.unmodifiableList(entries);
|
||||
}
|
||||
|
||||
/** Looks up an entry by its numeric ID. Returns null if not found. */
|
||||
public FaqEntry getById(int id) {
|
||||
return entries.stream().filter(e -> e.getId() == id).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new FAQ entry and saves immediately.
|
||||
*
|
||||
* @param question The question text.
|
||||
* @param answer The answer text.
|
||||
* @return The newly created {@link FaqEntry}.
|
||||
*/
|
||||
public FaqEntry add(String question, String answer) {
|
||||
int id = nextId++;
|
||||
FaqEntry entry = new FaqEntry(id, question, answer);
|
||||
entries.add(entry);
|
||||
writeEntry(id, question, answer);
|
||||
save();
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits an existing FAQ entry and saves immediately.
|
||||
*
|
||||
* @return true if the entry was found and updated, false otherwise.
|
||||
*/
|
||||
public boolean edit(int id, String question, String answer) {
|
||||
FaqEntry entry = getById(id);
|
||||
if (entry == null) return false;
|
||||
entry.setQuestion(question);
|
||||
entry.setAnswer(answer);
|
||||
writeEntry(id, question, answer);
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a FAQ entry and saves immediately.
|
||||
*
|
||||
* @return true if the entry was found and deleted, false otherwise.
|
||||
*/
|
||||
public boolean delete(int id) {
|
||||
FaqEntry entry = getById(id);
|
||||
if (entry == null) return false;
|
||||
entries.remove(entry);
|
||||
faqConfig.set("faqs." + id, null);
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Reloads FAQs from faqs.yml without restarting the server. */
|
||||
public void reload() {
|
||||
load();
|
||||
}
|
||||
}
|
||||
346
src/main/java/de/ticketsystem/manager/LanguageManager.java
Normal file
346
src/main/java/de/ticketsystem/manager/LanguageManager.java
Normal file
@@ -0,0 +1,346 @@
|
||||
package de.ticketsystem.manager;
|
||||
|
||||
import de.ticketsystem.TicketPlugin;
|
||||
import org.bukkit.command.CommandSender;
|
||||
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.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Lädt alle Plugin-Texte aus der aktiven Sprachdatei (lang_de.yml / lang_en.yml)
|
||||
* und ersetzt {cmd_X}-Platzhalter durch die passenden Befehlsnamen.
|
||||
*
|
||||
* Unterstützt Hex-Farbcodes (z.B. &#FF0055 oder <#FF0055>).
|
||||
* Funktioniert ab Spigot 1.16+.
|
||||
*
|
||||
* ┌─────────────────────────────────────────────────────────┐
|
||||
* │ Einziger Konfigurations-Schlüssel: language │
|
||||
* │ │
|
||||
* │ language: de → deutsche Texte + deutsche Befehle │
|
||||
* │ language: en → englische Texte + englische Befehle │
|
||||
* │ language: both → deutsche Texte + beide Befehlsnamen │
|
||||
* │ │
|
||||
* │ "command-language" existiert nicht mehr und wird │
|
||||
* │ vollständig ignoriert. │
|
||||
* └─────────────────────────────────────────────────────────┘
|
||||
*
|
||||
* Verfügbare {cmd_X}-Platzhalter in lang.yml:
|
||||
* {cmd_create} {cmd_list} {cmd_comment} {cmd_rate}
|
||||
* {cmd_claim} {cmd_close} {cmd_forward} {cmd_reload}
|
||||
* {cmd_stats} {cmd_archive} {cmd_migrate} {cmd_export}
|
||||
* {cmd_import} {cmd_blacklist} {cmd_setpriority} {cmd_faq} {cmd_top}
|
||||
*/
|
||||
public class LanguageManager {
|
||||
|
||||
// ── Konstanten ──────────────────────────────────────────────────────────
|
||||
|
||||
private static final Set<String> SUPPORTED = Set.of("de", "en", "both");
|
||||
private static final String FALLBACK = "de";
|
||||
|
||||
/** Nachrichten-Pfade die KEINEN Plugin-Prefix erhalten. */
|
||||
private static final String[] NO_PREFIX_PATHS = {
|
||||
"general.separator", "help.", "stats.", "top.", "faq.list-",
|
||||
"blacklist.list-", "gui.", "join.pending-header", "update."
|
||||
};
|
||||
|
||||
// ── Befehlsnamen-Tabellen (statisch, ändern sich nie) ───────────────────
|
||||
|
||||
private static final LinkedHashMap<String, String> DE = new LinkedHashMap<>();
|
||||
private static final LinkedHashMap<String, String> EN = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
DE.put("create", "erstellen");
|
||||
DE.put("list", "liste");
|
||||
DE.put("comment", "kommentar");
|
||||
DE.put("rate", "bewerten");
|
||||
DE.put("claim", "übernehmen");
|
||||
DE.put("close", "schließen");
|
||||
DE.put("forward", "weiterleiten");
|
||||
DE.put("reload", "neuladen");
|
||||
DE.put("stats", "statistik");
|
||||
DE.put("archive", "archivieren");
|
||||
DE.put("migrate", "migrieren");
|
||||
DE.put("export", "exportieren");
|
||||
DE.put("import", "importieren");
|
||||
DE.put("blacklist", "sperrliste");
|
||||
DE.put("setpriority", "priorität");
|
||||
DE.put("faq", "faq");
|
||||
DE.put("top", "top");
|
||||
|
||||
EN.put("create", "create");
|
||||
EN.put("list", "list");
|
||||
EN.put("comment", "comment");
|
||||
EN.put("rate", "rate");
|
||||
EN.put("claim", "claim");
|
||||
EN.put("close", "close");
|
||||
EN.put("forward", "forward");
|
||||
EN.put("reload", "reload");
|
||||
EN.put("stats", "stats");
|
||||
EN.put("archive", "archive");
|
||||
EN.put("migrate", "migrate");
|
||||
EN.put("export", "export");
|
||||
EN.put("import", "import");
|
||||
EN.put("blacklist", "blacklist");
|
||||
EN.put("setpriority", "setpriority");
|
||||
EN.put("faq", "faq");
|
||||
EN.put("top", "top");
|
||||
}
|
||||
|
||||
// ── Felder ───────────────────────────────────────────────────────────────
|
||||
|
||||
private final TicketPlugin plugin;
|
||||
private YamlConfiguration lang;
|
||||
private String prefix;
|
||||
|
||||
/**
|
||||
* Aktiver Sprachmodus – wird bei jedem load() DIREKT aus der Config gelesen.
|
||||
* Kein Cache, kein Zwischenwert. Immer frisch nach reloadConfig().
|
||||
*/
|
||||
private String activeLang;
|
||||
|
||||
/**
|
||||
* Dateiname-Kürzel: "de" oder "en".
|
||||
* "both" verwendet die DE-Datei für die Texte.
|
||||
*/
|
||||
private String fileLang;
|
||||
|
||||
/**
|
||||
* Ersetzungsmap {cmd_X} → Anzeigename.
|
||||
* Wird bei jedem load() komplett neu gebaut.
|
||||
*/
|
||||
private Map<String, String> cmdNames = new LinkedHashMap<>();
|
||||
|
||||
// ── Konstruktor ──────────────────────────────────────────────────────────
|
||||
|
||||
public LanguageManager(TicketPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
load();
|
||||
}
|
||||
|
||||
// ── Laden ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Lädt (oder relädt) die Sprachdatei und baut alle Befehlsnamen neu.
|
||||
* Muss nach plugin.reloadConfig() aufgerufen werden, damit die frische
|
||||
* language-Einstellung übernommen wird.
|
||||
*/
|
||||
public void load() {
|
||||
|
||||
// 1. language aus der (bereits neu geladenen) Config lesen
|
||||
String raw = plugin.getConfig().getString("language", FALLBACK)
|
||||
.toLowerCase().trim();
|
||||
|
||||
if (!SUPPORTED.contains(raw)) {
|
||||
plugin.getLogger().warning("[LanguageManager] Unbekannter Wert language='"
|
||||
+ raw + "' in config.yml – verwende '" + FALLBACK + "'.");
|
||||
raw = FALLBACK;
|
||||
}
|
||||
|
||||
activeLang = raw;
|
||||
fileLang = "en".equals(activeLang) ? "en" : "de";
|
||||
|
||||
// 2. Sprachdatei einlesen (ggf. aus JAR extrahieren)
|
||||
String fileName = "lang_" + fileLang + ".yml";
|
||||
File file = new File(plugin.getDataFolder(), fileName);
|
||||
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
plugin.saveResource(fileName, false);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
plugin.getLogger().severe("[LanguageManager] '" + fileName
|
||||
+ "' nicht im Plugin-JAR – Plugin neu installieren!");
|
||||
lang = new YamlConfiguration();
|
||||
prefix = color("&8[&6Ticket&8] &r");
|
||||
cmdNames = buildCmdNames();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
lang = YamlConfiguration.loadConfiguration(file);
|
||||
|
||||
// 3. Fehlende Schlüssel aus JAR-Defaults ergänzen & ggf. speichern
|
||||
InputStream defaultStream = plugin.getResource(fileName);
|
||||
if (defaultStream != null) {
|
||||
YamlConfiguration defaults = YamlConfiguration.loadConfiguration(
|
||||
new InputStreamReader(defaultStream, StandardCharsets.UTF_8));
|
||||
lang.setDefaults(defaults);
|
||||
|
||||
boolean changed = false;
|
||||
for (String key : defaults.getKeys(true)) {
|
||||
if (!lang.isSet(key)) {
|
||||
lang.set(key, defaults.get(key));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
try { lang.save(file); }
|
||||
catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"[LanguageManager] Konnte " + fileName + " nicht speichern.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Prefix & Befehlsnamen aufbauen
|
||||
prefix = color(lang.getString("prefix", "&8[&6Ticket&8] &r"));
|
||||
cmdNames = buildCmdNames();
|
||||
|
||||
plugin.getLogger().info("[LanguageManager] Geladen: " + fileName
|
||||
+ " | language=" + activeLang
|
||||
+ " | Befehle: " + describeMode());
|
||||
}
|
||||
|
||||
// ── Befehlsnamen ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Baut {cmd_X} → Anzeigename anhand von activeLang.
|
||||
*
|
||||
* de → /ticket erstellen
|
||||
* en → /ticket create
|
||||
* both → /ticket create §8(§7erstellen§8)
|
||||
*/
|
||||
private Map<String, String> buildCmdNames() {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
for (String key : EN.keySet()) {
|
||||
String display = switch (activeLang) {
|
||||
case "en" -> "/ticket " + EN.get(key);
|
||||
case "both" -> "/ticket " + EN.get(key) + " §8(§7" + DE.get(key) + "§8)";
|
||||
default -> "/ticket " + DE.get(key); // "de" + alle unbekannten
|
||||
};
|
||||
map.put("{cmd_" + key + "}", display);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private String describeMode() {
|
||||
return switch (activeLang) {
|
||||
case "en" -> "Englisch (/ticket create ...)";
|
||||
case "both" -> "Beides (/ticket create (erstellen) ...)";
|
||||
default -> "Deutsch (/ticket erstellen ...)";
|
||||
};
|
||||
}
|
||||
|
||||
// ── Befehlssprache-Abfragen (für TicketCommand) ──────────────────────────
|
||||
|
||||
/** true wenn deutsche Subkommandos akzeptiert werden sollen (Tab-Complete & Eingabe). */
|
||||
public boolean acceptsGerman() { return "de".equals(activeLang) || "both".equals(activeLang); }
|
||||
|
||||
/** true wenn englische Subkommandos akzeptiert werden sollen (Tab-Complete & Eingabe). */
|
||||
public boolean acceptsEnglish() { return "en".equals(activeLang) || "both".equals(activeLang); }
|
||||
|
||||
// ── Interne Platzhalter-Ersetzung ───────────────────────────────────────
|
||||
|
||||
private String applyCmdNames(String text) {
|
||||
if (text == null) return "";
|
||||
for (Map.Entry<String, String> e : cmdNames.entrySet())
|
||||
text = text.replace(e.getKey(), e.getValue());
|
||||
return text;
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
/** Roher Wert aus der Sprachdatei – ohne Farbe oder Platzhalter-Ersetzung. */
|
||||
public String getRaw(String key) {
|
||||
String value = lang.getString(key);
|
||||
if (value == null) {
|
||||
plugin.getLogger().warning("[LanguageManager] Fehlender Schlüssel: " + key);
|
||||
return key;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Übersetzter, eingefärbter Text. {cmd_X}-Platzhalter werden ersetzt. */
|
||||
public String get(String key) {
|
||||
return color(applyCmdNames(getRaw(key)));
|
||||
}
|
||||
|
||||
/** Übersetzter Text mit zusätzlichen {placeholder} → Wert Ersetzungen. */
|
||||
public String format(String key, String... replacements) {
|
||||
String text = applyCmdNames(getRaw(key));
|
||||
if (replacements.length % 2 != 0)
|
||||
plugin.getLogger().warning("[LanguageManager] format() benötigt eine gerade Anzahl an Argumenten für: " + key);
|
||||
for (int i = 0; i + 1 < replacements.length; i += 2)
|
||||
text = text.replace(replacements[i], replacements[i + 1]);
|
||||
return color(text);
|
||||
}
|
||||
|
||||
/** Gibt prefix + format(...) zurück. */
|
||||
public String formatWithPrefix(String key, String... replacements) {
|
||||
return prefix + format(key, replacements);
|
||||
}
|
||||
|
||||
/** Sendet eine Nachricht (mit Prefix wenn nötig) an einen CommandSender. */
|
||||
public void send(CommandSender sender, String key, String... replacements) {
|
||||
sender.sendMessage(needsPrefix(key)
|
||||
? prefix + format(key, replacements)
|
||||
: format(key, replacements));
|
||||
}
|
||||
|
||||
/** Sendet die Trennlinie. */
|
||||
public void sendSeparator(CommandSender sender) {
|
||||
sender.sendMessage(get("general.separator"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den vollständigen Befehlsstring zurück.
|
||||
* Beispiel: getCmdName("create") → "/ticket create" bei language=en
|
||||
*/
|
||||
public String getCmdName(String internalKey) {
|
||||
String full = cmdNames.get("{cmd_" + internalKey + "}");
|
||||
return full != null ? full : "/ticket " + internalKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Übersetzt &-Farbcodes und Hex-Farbcodes (&#RRGGBB oder <#RRGGBB>) in §-Codes.
|
||||
*/
|
||||
public String color(String text) {
|
||||
if (text == null || text.isEmpty()) return "";
|
||||
|
||||
// Regex für Hex Codes: &#RRGGBB oder <#RRGGBB>
|
||||
Pattern hexPattern = Pattern.compile("&#([A-Fa-f0-9]{6})|<#([A-Fa-f0-9]{6})>");
|
||||
Matcher matcher = hexPattern.matcher(text);
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String group = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
|
||||
try {
|
||||
// Fix: Explizite Nutzung von net.md_5.bungee.api.ChatColor für Hex-Support (Spigot 1.16+)
|
||||
// Dies verhindert den "Symbol nicht gefunden"-Fehler beim Kompilieren mit der reinen Bukkit-API.
|
||||
net.md_5.bungee.api.ChatColor hexColor = net.md_5.bungee.api.ChatColor.of("#" + group);
|
||||
matcher.appendReplacement(buffer, hexColor.toString());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Falls der Farbcode ungültig ist, Tag entfernen
|
||||
matcher.appendReplacement(buffer, "");
|
||||
}
|
||||
}
|
||||
|
||||
String parsed = matcher.appendTail(buffer).toString();
|
||||
|
||||
// Übersetzung der klassischen &-Farbcodes (Bukkit Standard)
|
||||
return org.bukkit.ChatColor.translateAlternateColorCodes('&', parsed);
|
||||
}
|
||||
|
||||
public String getPrefix() { return prefix; }
|
||||
public String getActiveLang() { return activeLang; }
|
||||
public String getFileLang() { return fileLang; }
|
||||
|
||||
/** Relädt die Sprachdatei. Muss NACH plugin.reloadConfig() aufgerufen werden. */
|
||||
public void reload() { load(); }
|
||||
|
||||
// ── Intern ───────────────────────────────────────────────────────────────
|
||||
|
||||
private boolean needsPrefix(String key) {
|
||||
for (String p : NO_PREFIX_PATHS) if (key.startsWith(p)) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -15,76 +15,68 @@ public class TicketManager {
|
||||
|
||||
private final TicketPlugin plugin;
|
||||
|
||||
/** Cooldown Map: UUID → Zeitstempel letztes Ticket */
|
||||
/** UUID → Zeitstempel der letzten Ticket-Erstellung */
|
||||
private final Map<UUID, Long> cooldowns = new HashMap<>();
|
||||
|
||||
public TicketManager(TicketPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// ─────────────────────────── Cooldown ──────────────────────────────────
|
||||
// ── Cooldown ──────────────────────────────────────────────────────────
|
||||
|
||||
public boolean hasCooldown(UUID uuid) {
|
||||
if (!cooldowns.containsKey(uuid)) return false;
|
||||
long cooldownSeconds = plugin.getConfig().getLong("ticket-cooldown", 60) * 1000L;
|
||||
return (System.currentTimeMillis() - cooldowns.get(uuid)) < cooldownSeconds;
|
||||
long cdMillis = plugin.getConfig().getLong("ticket-cooldown", 60) * 1000L;
|
||||
return (System.currentTimeMillis() - cooldowns.get(uuid)) < cdMillis;
|
||||
}
|
||||
|
||||
public long getRemainingCooldown(UUID uuid) {
|
||||
long cooldownMillis = plugin.getConfig().getLong("ticket-cooldown", 60) * 1000L;
|
||||
long cdMillis = plugin.getConfig().getLong("ticket-cooldown", 60) * 1000L;
|
||||
long elapsed = System.currentTimeMillis() - cooldowns.getOrDefault(uuid, 0L);
|
||||
return Math.max(0, (cooldownMillis - elapsed) / 1000);
|
||||
return Math.max(0, (cdMillis - elapsed) / 1000);
|
||||
}
|
||||
|
||||
public void setCooldown(UUID uuid) { cooldowns.put(uuid, System.currentTimeMillis()); }
|
||||
public void setCooldown(UUID uuid) {
|
||||
cooldowns.put(uuid, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// ─────────────────────────── Benachrichtigungen ────────────────────────
|
||||
// ── Team-Benachrichtigungen ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Benachrichtigt alle Supporter/Admins über ein neues Ticket – auch auf anderen Servern.
|
||||
*
|
||||
* Lokal online Spieler werden direkt angesprochen.
|
||||
* Über BungeeCord werden alle anderen Server im Netzwerk ebenfalls benachrichtigt.
|
||||
* Optional sendet der Discord-Webhook eine Nachricht.
|
||||
* Benachrichtigt alle Supporter/Admins über ein neues Ticket.
|
||||
* Bei BungeeCord wird die Nachricht an alle Server weitergeleitet.
|
||||
*/
|
||||
public void notifyTeam(Ticket ticket) {
|
||||
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
||||
String message = ticket.getMessage() != null ? ticket.getMessage() : "";
|
||||
|
||||
// Kategorie & Priorität optional anzeigen
|
||||
String categoryInfo = "";
|
||||
String priorityInfo = "";
|
||||
|
||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
||||
categoryInfo = " §7[§r" + cat.getColored() + "§7]";
|
||||
categoryInfo = plugin.lang().format("notify.team-category", "{category}", cat.getColored());
|
||||
}
|
||||
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||
priorityInfo = " §7Priorität: §r" + ticket.getPriority().getColored();
|
||||
priorityInfo = plugin.lang().format("notify.team-priority", "{priority}", ticket.getPriority().getColored());
|
||||
}
|
||||
|
||||
// BungeeCord: Server-Herkunft anzeigen wenn BungeeCord aktiviert
|
||||
String serverInfo = "";
|
||||
if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||
serverInfo = " §7Server: §b" + ticket.getServerName();
|
||||
serverInfo = plugin.lang().format("notify.team-server", "{server}", ticket.getServerName());
|
||||
}
|
||||
|
||||
String msg = plugin.formatMessage("messages.new-ticket-notify")
|
||||
.replace("{player}", creatorName)
|
||||
.replace("{message}", message)
|
||||
.replace("{id}", String.valueOf(ticket.getId()))
|
||||
String msg = plugin.lang().format("ticket.new-notify",
|
||||
"{player}", creatorName,
|
||||
"{message}", message,
|
||||
"{id}", String.valueOf(ticket.getId()))
|
||||
+ categoryInfo + priorityInfo + serverInfo;
|
||||
|
||||
String guiHint = plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen.");
|
||||
String guiHint = plugin.lang().get("notify.gui-hint");
|
||||
|
||||
if (plugin.isBungeeCordEnabled()) {
|
||||
// ─ BungeeCord-Modus: Team-Broadcast über alle Server ─────────────────
|
||||
// BungeeMessenger sendet lokal direkt, dann per Forward an alle anderen Server.
|
||||
// Beide Nachrichten werden zu einer zusammengefasst um ein einzelnes
|
||||
// Forward-Paket zu erzeugen statt zwei (reduziert Netzwerklast und
|
||||
// verhindert mögliche Reihenfolge-Probleme).
|
||||
plugin.getBungeeMessenger().broadcastTeamNotification(msg + "\n" + guiHint);
|
||||
} else {
|
||||
// ─ Standalone-Modus: Nur lokal ───────────────────────────────
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
|
||||
p.sendMessage(msg);
|
||||
@@ -96,239 +88,227 @@ public class TicketManager {
|
||||
plugin.getDiscordWebhook().sendNewTicket(ticket);
|
||||
}
|
||||
|
||||
// ── Ersteller-Benachrichtigungen ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde.
|
||||
* Setzt claimer_notified = true und persistiert es.
|
||||
*
|
||||
* BungeeCord: Zustellung auch wenn der Spieler auf einem anderen Server ist.
|
||||
* Benachrichtigt den Ersteller, dass sein Ticket angenommen wurde.
|
||||
*/
|
||||
public void notifyCreatorClaimed(Ticket ticket) {
|
||||
String claimerName = resolveClaimerName(ticket);
|
||||
|
||||
String msg = plugin.formatMessage("messages.ticket-claimed-notify")
|
||||
.replace("{id}", String.valueOf(ticket.getId()))
|
||||
.replace("{claimer}", claimerName);
|
||||
|
||||
String msg = plugin.lang().format("ticket.claimed-notify",
|
||||
"{id}", String.valueOf(ticket.getId()),
|
||||
"{claimer}", claimerName);
|
||||
deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
|
||||
|
||||
// Persistiert setzen, damit Join-Listener weiß, dass Spieler bereits informiert ist
|
||||
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wird beim Server-Join aufgerufen – informiert den Spieler über Tickets,
|
||||
* die geclaimt oder weitergeleitet wurden während er offline war.
|
||||
* Prüft beim Server-Join ob Tickets während der Offline-Zeit
|
||||
* geclaimt oder weitergeleitet wurden, und informiert den Spieler.
|
||||
*/
|
||||
public void notifyClaimedWhileOffline(Player player) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
var tickets = plugin.getDatabaseManager().getTicketsByStatus(
|
||||
TicketStatus.CLAIMED, TicketStatus.FORWARDED);
|
||||
var tickets = plugin.getDatabaseManager()
|
||||
.getTicketsByStatus(TicketStatus.CLAIMED, TicketStatus.FORWARDED);
|
||||
|
||||
for (Ticket t : tickets) {
|
||||
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
|
||||
if (t.isClaimerNotified()) continue;
|
||||
|
||||
String claimerName = t.getClaimerName() != null ? t.getClaimerName() : "Support";
|
||||
final String name = claimerName;
|
||||
final String name = t.getClaimerName() != null ? t.getClaimerName() : "Support";
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (!player.isOnline()) return;
|
||||
if (t.getStatus() == TicketStatus.CLAIMED) {
|
||||
String msg = plugin.formatMessage("messages.ticket-claimed-notify")
|
||||
.replace("{id}", String.valueOf(t.getId()))
|
||||
.replace("{claimer}", name);
|
||||
player.sendMessage(msg);
|
||||
player.sendMessage(plugin.lang().format("ticket.claimed-notify",
|
||||
"{id}", String.valueOf(t.getId()), "{claimer}", name));
|
||||
} else {
|
||||
String forwardedTo = t.getForwardedToName() != null ? t.getForwardedToName() : "einen Supporter";
|
||||
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
|
||||
.replace("{id}", String.valueOf(t.getId()))
|
||||
.replace("{supporter}", forwardedTo);
|
||||
player.sendMessage(msg);
|
||||
player.sendMessage(plugin.lang().format("ticket.forwarded-creator",
|
||||
"{id}", String.valueOf(t.getId()), "{supporter}", forwardedTo));
|
||||
}
|
||||
// Flag NACH der Nachricht setzen – sicher im Hauptthread
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin,
|
||||
() -> plugin.getDatabaseManager().markClaimerNotified(t.getId()));
|
||||
});
|
||||
|
||||
plugin.getDatabaseManager().markClaimerNotified(t.getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde.
|
||||
* BungeeCord: Cross-Server-Zustellung.
|
||||
* Benachrichtigt den Ersteller, dass sein Ticket weitergeleitet wurde.
|
||||
*/
|
||||
public void notifyCreatorForwarded(Ticket ticket) {
|
||||
String forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter";
|
||||
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
|
||||
.replace("{id}", String.valueOf(ticket.getId()))
|
||||
.replace("{supporter}", forwardedTo);
|
||||
|
||||
String msg = plugin.lang().format("ticket.forwarded-creator",
|
||||
"{id}", String.valueOf(ticket.getId()), "{supporter}", forwardedTo);
|
||||
deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
|
||||
|
||||
// Auch bei Weiterleitung notified setzen
|
||||
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet dem weitergeleiteten Supporter eine Benachrichtigung.
|
||||
* BungeeCord: Zustellung auch wenn der Supporter auf einem anderen Server ist.
|
||||
* Benachrichtigt den Supporter, an den ein Ticket weitergeleitet wurde.
|
||||
*/
|
||||
public void notifyForwardedTo(Ticket ticket, String fromName) {
|
||||
if (ticket.getForwardedToUUID() == null) return;
|
||||
|
||||
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
||||
String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
|
||||
.replace("{player}", creatorName)
|
||||
.replace("{id}", String.valueOf(ticket.getId()));
|
||||
|
||||
String msg = plugin.lang().format("ticket.forwarded-notify",
|
||||
"{player}", creatorName, "{id}", String.valueOf(ticket.getId()));
|
||||
deliverToPlayer(ticket.getForwardedToUUID(), ticket.getForwardedToName(), msg);
|
||||
|
||||
plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde.
|
||||
* BungeeCord: Cross-Server-Zustellung + Fallback in Pending-DB.
|
||||
*/
|
||||
public void notifyCreatorClosed(Ticket ticket) { notifyCreatorClosed(ticket, null); }
|
||||
/** Benachrichtigt den Ersteller über die Schließung seines Tickets. */
|
||||
public void notifyCreatorClosed(Ticket ticket) {
|
||||
notifyCreatorClosed(ticket, null);
|
||||
}
|
||||
|
||||
public void notifyCreatorClosed(Ticket ticket, String closerName) {
|
||||
// Bug-Fix: close_notified wird in der DB gespeichert – kein In-Memory-Set mehr.
|
||||
// Dadurch funktioniert der Check auch nach einem Server-Wechsel korrekt.
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||
plugin.getDatabaseManager().markCloseNotified(ticket.getId()));
|
||||
|
||||
String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
|
||||
? ticket.getCloseComment() : "";
|
||||
|
||||
// Hauptnachricht
|
||||
String msg = plugin.formatMessage("messages.ticket-closed-notify")
|
||||
.replace("{id}", String.valueOf(ticket.getId()))
|
||||
.replace("{comment}", comment);
|
||||
String msg = plugin.lang().format("ticket.closed-notify", "{id}", String.valueOf(ticket.getId()));
|
||||
|
||||
// Bewertungsaufforderung
|
||||
// Bewertungsaufforderung aufbauen
|
||||
String ratingMsg = null;
|
||||
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
|
||||
ratingMsg = plugin.color(
|
||||
"&8&m &r\n" +
|
||||
"&6Wie zufrieden bist du mit dem Support?\n" +
|
||||
"&a/ticket rate " + ticket.getId() + " good &7– 👍 Gut\n" +
|
||||
"&c/ticket rate " + ticket.getId() + " bad &7– 👎 Schlecht\n" +
|
||||
"&8&m ");
|
||||
String id = String.valueOf(ticket.getId());
|
||||
ratingMsg = plugin.lang().get("rating.prompt-header") + "\n"
|
||||
+ plugin.lang().get("rating.prompt-title") + "\n"
|
||||
+ plugin.lang().format("rating.prompt-good", "{id}", id) + "\n"
|
||||
+ plugin.lang().format("rating.prompt-bad", "{id}", id) + "\n"
|
||||
+ plugin.lang().get("rating.prompt-footer");
|
||||
}
|
||||
|
||||
// Prüfen ob Ersteller lokal online ist
|
||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
||||
if (creator != null && creator.isOnline()) {
|
||||
// ─ Lokal online: direkt zustellen ────────────────────────────
|
||||
creator.sendMessage(msg);
|
||||
if (!comment.isEmpty())
|
||||
creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment));
|
||||
if (ratingMsg != null) creator.sendMessage(ratingMsg);
|
||||
creator.sendMessage(plugin.lang().format("ticket.close-comment-label", "{comment}", comment));
|
||||
if (ratingMsg != null)
|
||||
creator.sendMessage(ratingMsg);
|
||||
|
||||
} else if (plugin.isBungeeCordEnabled()) {
|
||||
// ─ BungeeCord: via Plugin-Messaging auf anderen Servern zustellen ─
|
||||
// KEIN savePendingClosedNotification hier! Das würde bei Server-Wechsel
|
||||
// als "Offline-Nachricht" doppelt angezeigt werden.
|
||||
// BungeeCord's "Message"-Kanal erreicht den Spieler netzwerkweit sofern er online ist.
|
||||
// Ist er wirklich offline, sieht er beim nächsten Login via PlayerJoinListener
|
||||
// eine frische Benachrichtigung (close_notified=true verhindert Duplikate).
|
||||
plugin.getBungeeMessenger().sendMessageToPlayer(
|
||||
ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
|
||||
plugin.getBungeeMessenger().sendMessageToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
|
||||
if (!comment.isEmpty())
|
||||
plugin.getBungeeMessenger().sendMessageToPlayer(
|
||||
ticket.getCreatorUUID(), ticket.getCreatorName(),
|
||||
plugin.color("&7Kommentar des Supports: &f" + comment));
|
||||
plugin.lang().format("ticket.close-comment-label", "{comment}", comment));
|
||||
if (ratingMsg != null)
|
||||
plugin.getBungeeMessenger().sendMessageToPlayer(
|
||||
ticket.getCreatorUUID(), ticket.getCreatorName(), ratingMsg);
|
||||
|
||||
} else {
|
||||
// ─ Standalone, Spieler offline: in Pending-DB speichern ──────
|
||||
savePendingClosedNotification(ticket, comment);
|
||||
}
|
||||
|
||||
String closer = closerName != null ? closerName : "Unbekannt";
|
||||
plugin.getDiscordWebhook().sendTicketClosed(ticket, closer);
|
||||
plugin.getDiscordWebhook().sendTicketClosed(ticket,
|
||||
closerName != null ? closerName : "Unbekannt");
|
||||
}
|
||||
|
||||
/**
|
||||
* Bug-Fix: Nutzt jetzt close_notified aus der DB statt ein In-Memory-Set.
|
||||
* Funktioniert damit auch nach Server-Wechseln in BungeeCord-Netzwerken korrekt.
|
||||
*
|
||||
* @deprecated Bitte stattdessen ticket.isCloseNotified() direkt prüfen,
|
||||
* da das Ticket-Objekt aus der DB bereits den korrekten Wert hat.
|
||||
* @deprecated Bitte ticket.isCloseNotified() direkt verwenden.
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean wasClosedNotificationSent(int ticketId) {
|
||||
// Direkt in der DB nachschlagen – kein In-Memory-Set, kein Server-gebundener State
|
||||
Ticket t = plugin.getDatabaseManager().getTicketById(ticketId);
|
||||
return t != null && t.isCloseNotified();
|
||||
}
|
||||
|
||||
// ─────────────────────────── BungeeCord Hilfsmethoden ──────────────────
|
||||
// ── Ticket-Limit ──────────────────────────────────────────────────────
|
||||
|
||||
// ── BUG FIX #2 ──────────────────────────────────────────────────────────
|
||||
// Vorher: addPendingNotification() wurde IMMER asynchron ausgeführt –
|
||||
// auch wenn der Spieler lokal online war oder BungeeCord die
|
||||
// Nachricht bereits zugestellt hat. Das führte dazu, dass Spieler
|
||||
// beim nächsten Login immer noch eine "verpasste Nachricht" sahen,
|
||||
// obwohl sie die Nachricht bereits erhalten hatten.
|
||||
//
|
||||
// Fix: addPendingNotification() wird nur noch aufgerufen wenn:
|
||||
// 1. Der Spieler NICHT lokal online ist, UND
|
||||
// 2. BungeeCord NICHT aktiviert ist (Standalone-Fallback).
|
||||
// Im BungeeCord-Modus ist der BungeeCord-"Message"-Kanal für die
|
||||
// Zustellung zuständig. Offline-Spieler werden über close_notified
|
||||
// und den PlayerJoinListener beim nächsten Login benachrichtigt.
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
public boolean hasReachedTicketLimit(UUID uuid) {
|
||||
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
|
||||
if (max <= 0) return false;
|
||||
return plugin.getDatabaseManager().countOpenTicketsByPlayer(uuid) >= max;
|
||||
}
|
||||
|
||||
// ── Hilfe-Nachricht ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Zustellung einer Nachricht an einen Spieler.
|
||||
* Sendet die Hilfe-Nachricht an den Spieler.
|
||||
*
|
||||
* Ablauf:
|
||||
* 1. Spieler lokal online → direkt
|
||||
* 2. BungeeCord aktiv → via Plugin-Messaging (kein Pending-Eintrag)
|
||||
* 3. Offline + Standalone → Pending-DB (Zustellung beim nächsten Login)
|
||||
* Die Befehlsnamen in den lang.yml-Schlüsseln (z.B. help.create) enthalten
|
||||
* {cmd_X}-Platzhalter. Der LanguageManager ersetzt diese automatisch
|
||||
* anhand von language in config.yml:
|
||||
*
|
||||
* @param uuid UUID des Empfängers
|
||||
* @param name Spielername (für BungeeCord-Lookup)
|
||||
* @param message Bereits color-übersetzter Text
|
||||
* language: de → /ticket erstellen
|
||||
* language: en → /ticket create
|
||||
* language: both → /ticket create (erstellen)
|
||||
*
|
||||
* Hier muss kein manueller Sprachcode gelesen werden.
|
||||
*/
|
||||
public void sendHelpMessage(Player player) {
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
player.sendMessage(plugin.lang().get("help.header"));
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
player.sendMessage(plugin.lang().get("help.create"));
|
||||
player.sendMessage(plugin.lang().get("help.list"));
|
||||
player.sendMessage(plugin.lang().get("help.comment"));
|
||||
|
||||
if (plugin.getConfig().getBoolean("rating-enabled", true))
|
||||
player.sendMessage(plugin.lang().get("help.rate"));
|
||||
|
||||
if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) {
|
||||
player.sendMessage(plugin.lang().get("help.claim"));
|
||||
player.sendMessage(plugin.lang().get("help.close"));
|
||||
}
|
||||
if (player.hasPermission("ticket.admin")) {
|
||||
player.sendMessage(plugin.lang().get("help.forward"));
|
||||
player.sendMessage(plugin.lang().get("help.blacklist"));
|
||||
player.sendMessage(plugin.lang().get("help.reload"));
|
||||
player.sendMessage(plugin.lang().get("help.stats"));
|
||||
}
|
||||
player.sendMessage(plugin.lang().get("general.separator"));
|
||||
|
||||
if (player.hasPermission("ticket.admin") && plugin.isBungeeCordEnabled())
|
||||
player.sendMessage(plugin.lang().format("help.bungee-status", "{server}", plugin.getServerName()));
|
||||
}
|
||||
|
||||
// ── Interne Hilfsmethoden ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Zustellung einer Nachricht an einen Spieler:
|
||||
* 1. Lokal online → direkt senden
|
||||
* 2. BungeeCord → via Plugin-Messaging
|
||||
* 3. Offline → in Pending-DB speichern
|
||||
*/
|
||||
private void deliverToPlayer(UUID uuid, String name, String message) {
|
||||
Player local = Bukkit.getPlayer(uuid);
|
||||
if (local != null && local.isOnline()) {
|
||||
// Lokal online → direkt zustellen, fertig
|
||||
local.sendMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (plugin.isBungeeCordEnabled()) {
|
||||
// BungeeCord-Modus: Nachricht über Plugin-Messaging weiterleiten.
|
||||
// KEIN Pending-Eintrag! BungeeCord übernimmt die Zustellung.
|
||||
// Ist der Spieler wirklich offline, kümmert sich der PlayerJoinListener
|
||||
// beim nächsten Login um die Benachrichtigung.
|
||||
plugin.getBungeeMessenger().sendMessageToPlayer(uuid, name, message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Standalone-Modus, Spieler offline → in Pending-DB speichern
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||
plugin.getDatabaseManager().addPendingNotification(uuid, message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert eine ausstehende Schließ-Benachrichtigung in der DB.
|
||||
* Speichert eine Schließ-Benachrichtigung für einen Offline-Spieler
|
||||
* in der Pending-DB, damit sie beim nächsten Login zugestellt wird.
|
||||
*/
|
||||
private void savePendingClosedNotification(Ticket ticket, String comment) {
|
||||
String pendingMsg = "&e[Ticket #" + ticket.getId() + "] &7Dein Ticket wurde geschlossen."
|
||||
+ (comment.isEmpty() ? "" : " &7Kommentar: &f" + comment)
|
||||
+ (plugin.getConfig().getBoolean("rating-enabled", true)
|
||||
? " &7Bewertung: &e/ticket rate " + ticket.getId() + " good/bad" : "");
|
||||
String commentPart = comment.isEmpty()
|
||||
? ""
|
||||
: plugin.lang().format("ticket.pending-closed-comment", "{comment}", comment);
|
||||
String ratingPart = plugin.getConfig().getBoolean("rating-enabled", true)
|
||||
? plugin.lang().format("ticket.pending-closed-rating", "{id}", String.valueOf(ticket.getId()))
|
||||
: "";
|
||||
String pendingMsg = plugin.lang().format("ticket.pending-closed",
|
||||
"{id}", String.valueOf(ticket.getId()),
|
||||
"{comment}", commentPart,
|
||||
"{rating}", ratingPart);
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||
plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), pendingMsg));
|
||||
}
|
||||
|
||||
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
||||
|
||||
private String resolveClaimerName(Ticket ticket) {
|
||||
if (ticket.getClaimerName() != null) return ticket.getClaimerName();
|
||||
if (ticket.getClaimerUUID() != null) {
|
||||
@@ -337,40 +317,4 @@ public class TicketManager {
|
||||
}
|
||||
return "Support";
|
||||
}
|
||||
|
||||
public boolean hasReachedTicketLimit(UUID uuid) {
|
||||
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
|
||||
if (max <= 0) return false;
|
||||
return plugin.getDatabaseManager().countOpenTicketsByPlayer(uuid) >= max;
|
||||
}
|
||||
|
||||
public void sendHelpMessage(Player player) {
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
player.sendMessage(plugin.color("&6TicketSystem &7– Befehle"));
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
player.sendMessage(plugin.color("&e/ticket create [Kategorie] <Text> &7– Neues Ticket erstellen"));
|
||||
player.sendMessage(plugin.color("&e/ticket list &7– Deine Tickets ansehen (GUI)"));
|
||||
player.sendMessage(plugin.color("&e/ticket comment <ID> <Text> &7– Nachricht zu einem Ticket"));
|
||||
|
||||
if (plugin.getConfig().getBoolean("rating-enabled", true))
|
||||
player.sendMessage(plugin.color("&e/ticket rate <ID> <good|bad> &7– Support bewerten"));
|
||||
|
||||
if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) {
|
||||
player.sendMessage(plugin.color("&e/ticket claim <ID> &7– Ticket annehmen"));
|
||||
player.sendMessage(plugin.color("&e/ticket close <ID> [Kommentar] &7– Ticket schließen"));
|
||||
}
|
||||
if (player.hasPermission("ticket.admin")) {
|
||||
player.sendMessage(plugin.color("&e/ticket forward <ID> <Spieler> &7– Ticket weiterleiten"));
|
||||
player.sendMessage(plugin.color("&e/ticket blacklist <add|remove|list> [Spieler] [Grund] &7– Blacklist verwalten"));
|
||||
player.sendMessage(plugin.color("&e/ticket reload &7– Konfiguration neu laden"));
|
||||
player.sendMessage(plugin.color("&e/ticket stats &7– Statistiken anzeigen"));
|
||||
}
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
|
||||
// BungeeCord-Status anzeigen
|
||||
if (player.hasPermission("ticket.admin") && plugin.isBungeeCordEnabled()) {
|
||||
player.sendMessage(plugin.color("&8[BungeeCord] &7Server: &b" + plugin.getServerName()
|
||||
+ " &8| Cross-Server-Benachrichtigungen &aaktiv"));
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/main/java/de/ticketsystem/model/FaqEntry.java
Normal file
29
src/main/java/de/ticketsystem/model/FaqEntry.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package de.ticketsystem.model;
|
||||
|
||||
/**
|
||||
* Represents a single FAQ entry stored in faqs.yml.
|
||||
*/
|
||||
public class FaqEntry {
|
||||
|
||||
private int id;
|
||||
private String question;
|
||||
private String answer;
|
||||
|
||||
public FaqEntry(int id, String question, String answer) {
|
||||
this.id = id;
|
||||
this.question = question;
|
||||
this.answer = answer;
|
||||
}
|
||||
|
||||
public int getId() { return id; }
|
||||
public void setId(int id) { this.id = id; }
|
||||
public String getQuestion() { return question; }
|
||||
public void setQuestion(String q) { this.question = q; }
|
||||
public String getAnswer() { return answer; }
|
||||
public void setAnswer(String a) { this.answer = a; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FaqEntry{id=" + id + ", question='" + question + "'}";
|
||||
}
|
||||
}
|
||||
@@ -110,26 +110,33 @@ public class Ticket implements ConfigurationSerializable {
|
||||
public Map<String, Object> serialize() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("id", id);
|
||||
map.put("creatorUUID", creatorUUID.toString());
|
||||
map.put("creatorName", creatorName);
|
||||
map.put("message", message);
|
||||
map.put("world", worldName);
|
||||
// Null-Schutz: SnakeYAML wirft NPE wenn Key oder Value null sind
|
||||
map.put("creatorUUID", creatorUUID != null ? creatorUUID.toString() : "");
|
||||
map.put("creatorName", creatorName != null ? creatorName : "");
|
||||
map.put("message", message != null ? message : "");
|
||||
map.put("world", worldName != null ? worldName : "world");
|
||||
map.put("x", x); map.put("y", y); map.put("z", z);
|
||||
map.put("yaw", yaw); map.put("pitch", pitch);
|
||||
map.put("status", status.name());
|
||||
map.put("status", status != null ? status.name() : TicketStatus.OPEN.name());
|
||||
if (createdAt != null) map.put("createdAt", createdAt.getTime());
|
||||
if (claimedAt != null) map.put("claimedAt", claimedAt.getTime());
|
||||
if (closedAt != null) map.put("closedAt", closedAt.getTime());
|
||||
if (closeComment != null) map.put("closeComment", closeComment);
|
||||
if (claimerUUID != null) { map.put("claimerUUID", claimerUUID.toString()); map.put("claimerName", claimerName); }
|
||||
if (forwardedToUUID != null) { map.put("forwardedToUUID", forwardedToUUID.toString()); map.put("forwardedToName", forwardedToName); }
|
||||
if (claimerUUID != null) {
|
||||
map.put("claimerUUID", claimerUUID.toString());
|
||||
map.put("claimerName", claimerName != null ? claimerName : "");
|
||||
}
|
||||
if (forwardedToUUID != null) {
|
||||
map.put("forwardedToUUID", forwardedToUUID.toString());
|
||||
map.put("forwardedToName", forwardedToName != null ? forwardedToName : "");
|
||||
}
|
||||
map.put("playerDeleted", playerDeleted);
|
||||
map.put("category", categoryKey);
|
||||
map.put("priority", priority.name());
|
||||
map.put("category", categoryKey != null ? categoryKey : "general");
|
||||
map.put("priority", priority != null ? priority.name() : TicketPriority.NORMAL.name());
|
||||
if (playerRating != null) map.put("playerRating", playerRating);
|
||||
map.put("claimerNotified", claimerNotified);
|
||||
// BungeeCord: Server-Name speichern
|
||||
map.put("serverName", serverName);
|
||||
map.put("serverName", serverName != null ? serverName : "unknown");
|
||||
map.put("closeNotified", closeNotified);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -8,13 +8,29 @@
|
||||
#
|
||||
# TicketSystem - Ein einfaches und effizientes Ticketsystem für Minecraft-Server
|
||||
# Entwickelt von M_Viper
|
||||
#
|
||||
# HINWEIS: Alle Texte und Nachrichten befinden sich in lang_de.yml / lang_en.yml!
|
||||
# ============================================================
|
||||
|
||||
# --- GRUNDLEGEND ---
|
||||
# Version der Konfigurationsdatei. Nicht ändern!
|
||||
version: "2.0"
|
||||
version: "2.2"
|
||||
|
||||
# Debug-Modus (true = Logs in der Konsole)
|
||||
# ----------------------------------------------------
|
||||
# SPRACHE / LANGUAGE
|
||||
# ----------------------------------------------------
|
||||
# Steuert sowohl die Texte als auch die Befehlsnamen.
|
||||
#
|
||||
# de → deutsche Texte + /ticket erstellen, /ticket schließen ...
|
||||
# en → englische Texte + /ticket create, /ticket close ...
|
||||
# both → deutsche Texte + /ticket create (erstellen) ...
|
||||
#
|
||||
# Die passende Datei (lang_de.yml / lang_en.yml) wird automatisch
|
||||
# im Plugin-Ordner erstellt und kann frei bearbeitet werden.
|
||||
# ----------------------------------------------------
|
||||
language: de
|
||||
|
||||
# Debug-Modus (true = zusätzliche Logs in der Konsole)
|
||||
debug: false
|
||||
|
||||
# ----------------------------------------------------
|
||||
@@ -62,11 +78,6 @@ mysql:
|
||||
pool-size: 10 # HikariCP Poolgröße
|
||||
connection-timeout: 30000 # Timeout in ms
|
||||
|
||||
# ----------------------------------------------------
|
||||
# PLUGIN-PRÄFIX (Chat)
|
||||
# ----------------------------------------------------
|
||||
prefix: "&8[&6Ticket&8] &r" # Präfix für Chat-Ausgaben
|
||||
|
||||
# ----------------------------------------------------
|
||||
# LIMITS & OPTIONEN
|
||||
# ----------------------------------------------------
|
||||
@@ -79,6 +90,11 @@ max-open-tickets-per-player: 2 # Maximale offene Tickets pro Spieler (0 = unbeg
|
||||
# ----------------------------------------------------
|
||||
auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# PERFORMANCE
|
||||
# ----------------------------------------------------
|
||||
cache-ttl-seconds: 60 # Wie lange Tickets im In-Memory-Cache gehalten werden
|
||||
|
||||
# ----------------------------------------------------
|
||||
# OPTIONALE FEATURES
|
||||
# ----------------------------------------------------
|
||||
@@ -185,10 +201,10 @@ discord:
|
||||
title: "🔒 Ticket geschlossen"
|
||||
color: "15158332" # Rot
|
||||
footer: "TicketSystem"
|
||||
show-category: true # Kategorie im Embed anzeigen
|
||||
show-priority: true # Priorität im Embed anzeigen
|
||||
show-server: true # BungeeCord: Server-Name im Embed anzeigen
|
||||
role-ping: false # Rollen-Ping beim Schließen senden
|
||||
show-category: true
|
||||
show-priority: true
|
||||
show-server: true
|
||||
role-ping: false
|
||||
|
||||
# ── Ticket weitergeleitet ───────────────────────────────────────────────
|
||||
ticket-forwarded:
|
||||
@@ -196,70 +212,78 @@ discord:
|
||||
title: "🔀 Ticket weitergeleitet"
|
||||
color: "15105570" # Orange
|
||||
footer: "TicketSystem"
|
||||
show-category: true # Kategorie im Embed anzeigen
|
||||
show-priority: true # Priorität im Embed anzeigen
|
||||
show-server: true # BungeeCord: Server-Name im Embed anzeigen
|
||||
role-ping: false # Rollen-Ping beim Weiterleiten senden
|
||||
show-category: true
|
||||
show-priority: true
|
||||
show-server: true
|
||||
role-ping: false
|
||||
|
||||
# ----------------------------------------------------
|
||||
# SYSTEM-NACHRICHTEN (mit &-Farbcodes)
|
||||
# ----------------------------------------------------
|
||||
messages:
|
||||
# --- SYSTEM ---
|
||||
export-success: "&aExport erfolgreich: &e{count} &aTickets nach &e{file} &aexportiert."
|
||||
export-fail: "&cExport fehlgeschlagen oder keine Tickets gefunden."
|
||||
import-success: "&aImport erfolgreich: &e{count} &aTickets importiert."
|
||||
import-fail: "&cImport fehlgeschlagen oder keine Tickets gefunden."
|
||||
migration-success: "&aMigration abgeschlossen: &e{count} &aTickets migriert."
|
||||
migration-fail: "&cKeine Tickets migriert oder Fehler aufgetreten."
|
||||
archive-success: "&aArchivierung abgeschlossen: &e{count} &aTickets archiviert."
|
||||
archive-fail: "&cKeine geschlossenen Tickets zum Archivieren gefunden."
|
||||
file-not-found: "&cDatei nicht gefunden: &e{file}"
|
||||
unknown-mode: "&cUnbekannter Modus! Benutze: tomysql oder tofile"
|
||||
validation-warning: "&cEs wurden &e{count} &cungültige Tickets beim Laden gefunden."
|
||||
# ============================================================
|
||||
# GUI KONFIGURATION (Layouts, Slots, Items)
|
||||
# ============================================================
|
||||
# Hier kannst du das Aussehen und die Anordnung der GUIs anpassen.
|
||||
# WICHTIG: gui-settings muss ganz links stehen (keine Raute davor!).
|
||||
gui-settings:
|
||||
|
||||
# --- TICKET-AKTIONEN ---
|
||||
ticket-created: "&aTicket &e#{id} &awurde erfolgreich erstellt!"
|
||||
ticket-claimed: "&aDu hast Ticket &e#{id} &avon &e{player} &ageclaimt."
|
||||
ticket-claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen."
|
||||
ticket-closed: "&aTicket &e#{id} &awurde geschlossen."
|
||||
ticket-forwarded: "&aTicket &e#{id} &awurde an &e{player} &aweitergeleitet."
|
||||
ticket-forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen. &7(ID: {id})"
|
||||
# --- FAQ SYSTEM SETTINGS ---
|
||||
faq:
|
||||
# Größe des Inventars (4-6 Reihen, Minimum 4)
|
||||
rows: 6
|
||||
# Content-Slots für FAQ-Items.
|
||||
# Wenn leer: Automatisches Schachbrett-Muster (Items und leere Slots wechselnd,
|
||||
# letzte Reihe = Navigation/Footer).
|
||||
# Wenn gefüllt: Nur diese Slots werden für FAQs genutzt (Liste von Zahlen).
|
||||
# Beispiel: content-slots: [1, 3, 5, 7, 10, 12, 14, 16] -> Nur ungerade Slots
|
||||
content-slots: []
|
||||
|
||||
# --- BENACHRICHTIGUNGEN FÜR DEN TICKET-ERSTELLER ---
|
||||
ticket-closed-notify: "&aDein Ticket &e#{id} &awurde geschlossen."
|
||||
ticket-forwarded-creator-notify: "&eDein Ticket &6#{id} &ewurde an &b{supporter} &eweitergeleitet."
|
||||
# Kopfeinstellungen
|
||||
head-item:
|
||||
# Material des FAQ-Items (z.B. PLAYER_HEAD, BOOK, PAPER)
|
||||
material: PLAYER_HEAD
|
||||
# Optional: Texture-URL für den Kopf (wenn Material PLAYER_HEAD)
|
||||
texture: "http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e"
|
||||
|
||||
# --- KATEGORIEN ---
|
||||
# {category} wird durch den Anzeigenamen der gewählten Kategorie ersetzt
|
||||
ticket-created-category: "&aTicket &e#{id} &aerstellt! &7Kategorie: {category}"
|
||||
category-invalid: "&cUnbekannte Kategorie: &e{input}&c. Verfügbare Kategorien: &e{categories}"
|
||||
# Navigations-Slots (Prev, Next, Add, Page)
|
||||
nav:
|
||||
prev: 45
|
||||
next: 53
|
||||
add: 50
|
||||
page: 49
|
||||
|
||||
# --- KOMMENTARE ---
|
||||
comment-saved: "&aDein Kommentar zu Ticket &e#{id} &awurde gespeichert."
|
||||
comment-notify: "&e[Ticket #{id}] &f{author} &7kommentiert: &f{message}"
|
||||
comment-no-permission: "&cDu kannst nur deine eigenen Tickets kommentieren."
|
||||
# --- TICKET GUI SETTINGS ---
|
||||
ticket:
|
||||
|
||||
# --- BEWERTUNGEN ---
|
||||
rating-saved-good: "&aDanke für deine Bewertung! &a👍 Positiv"
|
||||
rating-saved-bad: "&aDanke für deine Bewertung! &c👎 Negativ"
|
||||
rating-already-rated: "&cDu hast dieses Ticket bereits bewertet."
|
||||
rating-not-yours: "&cDu kannst nur deine eigenen Tickets bewerten."
|
||||
rating-disabled: "&cBewertungen sind aktuell deaktiviert."
|
||||
rating-prompt: "&6Wie zufrieden bist du mit dem Support?\n&a/ticket rate {id} good &7– 👍 Gut\n&c/ticket rate {id} bad &7– 👎 Schlecht"
|
||||
# Spieler GUI
|
||||
player:
|
||||
rows: 6
|
||||
nav:
|
||||
prev: 45
|
||||
next: 53
|
||||
page: 49
|
||||
|
||||
# --- BLACKLIST ---
|
||||
blacklist-added: "&a{player} &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e{reason}"
|
||||
blacklist-removed: "&a{player} &awurde von der Blacklist entfernt."
|
||||
blacklist-already: "&cSpieler ist bereits auf der Blacklist."
|
||||
blacklist-not-found: "&cSpieler war nicht auf der Blacklist."
|
||||
blacklist-blocked: "&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen."
|
||||
# Admin / Team GUI
|
||||
admin:
|
||||
nav:
|
||||
prev: 45
|
||||
next: 53
|
||||
page: 48
|
||||
archive: 49
|
||||
filter: 47
|
||||
|
||||
# --- FEHLER & HINWEISE ---
|
||||
no-permission: "&cDu hast keine Berechtigung!"
|
||||
no-open-tickets: "&aAktuell gibt es keine offenen Tickets."
|
||||
join-open-tickets: "&eEs gibt noch &6{count} &eoffene Ticket(s)!"
|
||||
new-ticket-notify: "&e{player} &ahat ein neues Ticket erstellt: &7{message} &7(ID: &e{id}&7)"
|
||||
already-claimed: "&cDieses Ticket wurde bereits geclaimt!"
|
||||
ticket-not-found: "&cTicket nicht gefunden!"
|
||||
cooldown: "&cBitte warte &e{seconds} Sekunden &cbevor du ein neues Ticket erstellst."
|
||||
# Archiv GUI
|
||||
archive:
|
||||
nav:
|
||||
prev: 45
|
||||
next: 53
|
||||
back: 49
|
||||
|
||||
# --- GUI ITEM MATERIALS (Optional) ---
|
||||
# Hier kannst du das Material der Navigations-Buttons ändern.
|
||||
# Wenn nicht gesetzt, werden Standard-Werte genutzt.
|
||||
items:
|
||||
nav-prev: ARROW
|
||||
nav-next: ARROW
|
||||
nav-page: PAPER
|
||||
nav-archive: CHEST
|
||||
nav-back: ARROW
|
||||
nav-filter: HOPPER
|
||||
nav-add: LIME_WOOL
|
||||
460
src/main/resources/lang_de.yml
Normal file
460
src/main/resources/lang_de.yml
Normal file
@@ -0,0 +1,460 @@
|
||||
# ============================================================
|
||||
# TicketSystem – Sprachdatei Deutsch (de)
|
||||
#
|
||||
# Alle Texte des Plugins können hier angepasst werden.
|
||||
# Farbcodes: & (z. B. &a = Grün, &c = Rot, &e = Gelb, &7 = Grau)
|
||||
# HEX-Codes: &#RRGGBB (z. B. &#FFD700 = Gold)
|
||||
# Platzhalter werden in geschweiften Klammern angegeben: {id}, {player}, ...
|
||||
#
|
||||
# Sprache in config.yml wechseln: language: de | en | both
|
||||
#
|
||||
# {cmd_X} wird automatisch je nach language ersetzt, z.B.:
|
||||
# language: de → /ticket erstellen
|
||||
# language: en → /ticket create
|
||||
# language: both → /ticket create (erstellen)
|
||||
# ============================================================
|
||||
|
||||
prefix: "&#FFAA00[&fTicket&#FFAA00] &r"
|
||||
|
||||
# ============================================================
|
||||
# ALLGEMEINE FEHLER & HINWEISE
|
||||
# ============================================================
|
||||
general:
|
||||
no-permission: "&cDu hast keine Berechtigung!"
|
||||
console-only: "&cDieser Befehl kann nur von Spielern ausgeführt werden."
|
||||
invalid-id: "&cUngültige ID!"
|
||||
invalid-player-id: "&cUngültige Ticket-ID: &e{id}"
|
||||
player-not-found: "&cSpieler nicht gefunden!"
|
||||
ticket-not-found: "&cTicket nicht gefunden!"
|
||||
already-claimed: "&cDieses Ticket wurde bereits angenommen!"
|
||||
no-open-tickets: "&aAktuell gibt es keine offenen Tickets."
|
||||
cooldown: "&cBitte warte &e{seconds} Sekunden &cbevor du ein neues Ticket erstellst."
|
||||
separator: "򇨣&m "
|
||||
|
||||
# ============================================================
|
||||
# SYSTEM (Export, Import, Migration, Archiv, Validierung)
|
||||
# ============================================================
|
||||
system:
|
||||
export-success: "&aExport erfolgreich: &e{count} &aTickets nach &e{file} &aexportiert."
|
||||
export-fail: "&cExport fehlgeschlagen oder keine Tickets gefunden."
|
||||
import-success: "&aImport erfolgreich: &e{count} &aTickets importiert."
|
||||
import-fail: "&cImport fehlgeschlagen oder keine Tickets gefunden."
|
||||
migration-success: "&aMigration abgeschlossen: &e{count} &aTickets migriert."
|
||||
migration-fail: "&cKeine Tickets migriert oder Fehler aufgetreten."
|
||||
archive-success: "&aArchivierung abgeschlossen: &e{count} &aTickets archiviert."
|
||||
archive-fail: "&cKeine geschlossenen Tickets zum Archivieren gefunden."
|
||||
file-not-found: "&cDatei nicht gefunden: &e{file}"
|
||||
unknown-mode: "&cUnbekannter Modus! Benutze: tomysql oder tofile"
|
||||
validation-warning: "&cEs wurden &e{count} &cungültige Tickets beim Laden gefunden."
|
||||
db-create-error: "&cFehler beim Erstellen des Tickets!"
|
||||
|
||||
# ============================================================
|
||||
# TICKET-AKTIONEN
|
||||
# ============================================================
|
||||
ticket:
|
||||
created: "&aTicket &e#{id} &awurde erfolgreich erstellt!"
|
||||
created-category: "&aTicket &e#{id} &aerstellt! &7Kategorie: {category}"
|
||||
claimed: "&aDu hast Ticket &e#{id} &avon &e{player} &aangenommen."
|
||||
claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen."
|
||||
closed: "&aTicket &e#{id} &awurde geschlossen."
|
||||
closed-notify: "&aDein Ticket &e#{id} &awurde geschlossen."
|
||||
forwarded: "&aTicket &e#{id} &awurde an &e{player} &aweitergeleitet."
|
||||
forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen. &7(ID: {id})"
|
||||
forwarded-creator: "&eDein Ticket &6#{id} &ewurde an &b{supporter} &eweitergeleitet."
|
||||
new-notify: "&e{player} &ahat ein neues Ticket erstellt: &7{message} &7(ID: &e{id}&7)"
|
||||
close-comment-label: "&7Kommentar des Supports: &f{comment}"
|
||||
close-comment-short: "&7Kommentar: &f{comment}"
|
||||
pending-closed: "&e[Ticket #{id}] &7Dein Ticket wurde geschlossen.{comment}{rating}"
|
||||
pending-closed-comment: " &7Kommentar: &f{comment}"
|
||||
pending-closed-rating: " &7Bewertung: &e{cmd_rate} {id} good/bad"
|
||||
|
||||
# ============================================================
|
||||
# BENACHRICHTIGUNGEN (Team / GUI-Hinweis)
|
||||
# ============================================================
|
||||
notify:
|
||||
gui-hint: "&7» Klicke &e{cmd_list} &7um die Übersicht zu öffnen."
|
||||
team-category: " §7[§r{category}§7]"
|
||||
team-priority: " §7Priorität: §r{priority}"
|
||||
team-server: " §7Server: §b{server}"
|
||||
|
||||
# ============================================================
|
||||
# TICKET ERSTELLEN
|
||||
# ============================================================
|
||||
create:
|
||||
usage: "&cBenutzung: {cmd_create} [Kategorie] [Priorität] <Beschreibung>"
|
||||
categories-hint: "&7Kategorien: &ebug&7, &efrage&7, &ebeschwerde&7, &esonstiges&7, &eallgemein"
|
||||
priorities-hint: "&7Prioritäten: &alow&7, &enormal&7, &6high&7, &curgent"
|
||||
max-tickets: "&cDu hast bereits &e{max} &coffene Ticket(s). Bitte warte, bis dein Ticket bearbeitet wurde."
|
||||
no-description: "&cBitte gib eine Beschreibung für dein Ticket an."
|
||||
too-long: "&cDeine Beschreibung ist zu lang! Maximal {max} Zeichen."
|
||||
blacklist-blocked: "&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen."
|
||||
category-invalid: "&cUnbekannte Kategorie: &e{input}&c. Verfügbare Kategorien: &e{categories}"
|
||||
|
||||
# ============================================================
|
||||
# CLAIM / CLOSE / FORWARD
|
||||
# ============================================================
|
||||
claim:
|
||||
usage: "&cBenutzung: {cmd_claim} <ID>"
|
||||
|
||||
close:
|
||||
usage: "&cBenutzung: {cmd_close} <ID> [Kommentar]"
|
||||
|
||||
forward:
|
||||
usage: "&cBenutzung: {cmd_forward} <ID> <Spieler>"
|
||||
bungee-offline: "&7[BungeeCord] Spieler &e{player} &7ist auf diesem Server nicht online."
|
||||
local-not-found: "&cSpieler nicht gefunden!"
|
||||
|
||||
# ============================================================
|
||||
# KOMMENTARE
|
||||
# ============================================================
|
||||
comment:
|
||||
saved: "&aDein Kommentar zu Ticket &e#{id} &awurde gespeichert."
|
||||
usage: "&cBenutzung: {cmd_comment} <ID> <Nachricht>"
|
||||
too-long: "&cNachricht zu lang! Maximal 500 Zeichen."
|
||||
no-permission: "&cDu kannst nur deine eigenen Tickets kommentieren."
|
||||
error: "&cFehler beim Speichern des Kommentars."
|
||||
notify-online: "&e[Ticket #{id}] &f{author} &7hat kommentiert: &f{message}"
|
||||
notify-offline: "&e[Ticket #{id}] &f{author} &7hat kommentiert (während du offline warst): &f{message}"
|
||||
claimer-offline: "&e[Ticket #{id}] &f{author} &7hat auf dein bearbeitetes Ticket kommentiert (offline): &f{message}"
|
||||
|
||||
# ============================================================
|
||||
# BEWERTUNGEN
|
||||
# ============================================================
|
||||
rating:
|
||||
saved-good: "&aDanke für deine Bewertung! &a👍 Positiv"
|
||||
saved-bad: "&aDanke für deine Bewertung! &c👎 Negativ"
|
||||
already-rated: "&cDu hast dieses Ticket bereits bewertet."
|
||||
not-yours: "&cDu kannst nur deine eigenen Tickets bewerten."
|
||||
disabled: "&cBewertungen sind aktuell deaktiviert."
|
||||
not-closeable: "&cBewertung konnte nicht gespeichert werden. Ist das Ticket noch offen?"
|
||||
usage: "&cBenutzung: {cmd_rate} <ID> <good|bad>"
|
||||
invalid: "&cUngültige Bewertung! Benutze &egood &coder &ebad&c."
|
||||
prompt-header: "򇨣&m "
|
||||
prompt-title: "&6Wie zufrieden bist du mit dem Support?"
|
||||
prompt-good: "&a{cmd_rate} {id} good &7– 👍 Gut"
|
||||
prompt-bad: "&c{cmd_rate} {id} bad &7– 👎 Schlecht"
|
||||
prompt-footer: "򇨣&m "
|
||||
|
||||
# ============================================================
|
||||
# PRIORITÄT SETZEN
|
||||
# ============================================================
|
||||
setpriority:
|
||||
usage: "&cBenutzung: {cmd_setpriority} <ID> <low|normal|high|urgent>"
|
||||
disabled: "&cDas Prioritäten-System ist deaktiviert."
|
||||
invalid: "&cUngültige Priorität! Gültig: &alow&7, &enormal&7, &6high&7, &curgent"
|
||||
success: "&aPriorität von Ticket &e#{id} &awurde auf {priority} &agesetzt."
|
||||
not-found: "&cTicket &e#{id} &cwurde nicht gefunden."
|
||||
|
||||
# ============================================================
|
||||
# BLACKLIST
|
||||
# ============================================================
|
||||
blacklist:
|
||||
added: "&a{player} &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e{reason}"
|
||||
removed: "&a{player} &awurde von der Blacklist entfernt."
|
||||
already: "&cSpieler ist bereits auf der Blacklist."
|
||||
not-found: "&cSpieler war nicht auf der Blacklist."
|
||||
usage: "&cBenutzung: {cmd_blacklist} <add|remove|list> [Spieler] [Grund]"
|
||||
usage-add: "&cBenutzung: {cmd_blacklist} add <Spieler> [Grund]"
|
||||
usage-remove: "&cBenutzung: {cmd_blacklist} remove <Spieler>"
|
||||
list-header: "&6Ticket-Blacklist &7({count} Einträge)"
|
||||
list-empty: "&7Keine gesperrten Spieler."
|
||||
list-entry: "&e{player} &7– &f{reason} &7(gesperrt von &e{by}&7)"
|
||||
|
||||
# ============================================================
|
||||
# STATISTIKEN
|
||||
# ============================================================
|
||||
stats:
|
||||
header: "&6Ticket Statistik"
|
||||
total: "&eGesamt: &a{count}"
|
||||
open: "&eOffen: &a{count}"
|
||||
closed: "&eGeschlossen: &a{count} &7(historisch)"
|
||||
forwarded: "&eWeitergeleitet: &a{count}"
|
||||
ratings-header: "&6Support-Bewertungen &7(gesamt, historisch)"
|
||||
ratings-summary: "&a👍 Positiv: &f{up} &c👎 Negativ: &f{down}"
|
||||
ratings-percent: "&7Zufriedenheit: &e{percent}%"
|
||||
staff-header: "&6Bewertungen nach Support-Mitarbeiter:"
|
||||
staff-table-header: "&7 Name 👍 👎 Tickets Zufrieden"
|
||||
staff-entry: "&e {name} &a{up} &c{down} &7{total} &e{percent}"
|
||||
servers-header: "&6Tickets nach Server:"
|
||||
server-entry: "&b {server}: &a{count}"
|
||||
top-header: "&6Top-5 Ticket-Ersteller &7(historisch, persistent)"
|
||||
top-empty: "&7Noch keine Daten vorhanden."
|
||||
top-entry: " {medal} &f{name} &e{count} &7{label}"
|
||||
top-ticket-label: "Ticket"
|
||||
top-tickets-label: "Tickets"
|
||||
cache-info: "&7Cache: &e{count} &7gecachte Ticket(s)"
|
||||
|
||||
# ============================================================
|
||||
# TOP-ERSTELLER
|
||||
# ============================================================
|
||||
top:
|
||||
header: "&6&lTop-5 Ticket-Ersteller"
|
||||
empty: "&7Noch keine Daten vorhanden."
|
||||
entry: "{medal} &f{name} &e{count} &7{label}"
|
||||
footer: "&7(Zähler bleiben auch nach dem Löschen von Tickets erhalten)"
|
||||
|
||||
# ============================================================
|
||||
# RELOAD
|
||||
# ============================================================
|
||||
reload:
|
||||
success: "&aKonfiguration wurde neu geladen. &7(Kategorien, FAQs, Cache geleert)"
|
||||
bungee-info: "&8[BungeeCord] &7Server: &b{server}"
|
||||
|
||||
# ============================================================
|
||||
# MIGRATE / EXPORT / IMPORT
|
||||
# ============================================================
|
||||
migrate:
|
||||
usage: "&cBenutzung: {cmd_migrate} <tomysql|tofile>"
|
||||
|
||||
export:
|
||||
usage: "&cBenutzung: {cmd_export} <Dateiname>"
|
||||
|
||||
import:
|
||||
usage: "&cBenutzung: {cmd_import} <Dateiname>"
|
||||
|
||||
# ============================================================
|
||||
# FAQ-SYSTEM
|
||||
# ============================================================
|
||||
faq:
|
||||
usage-add: "&cBenutzung: {cmd_faq} add <Frage> | <Antwort>"
|
||||
usage-add-example: "&7Beispiel: &e{cmd_faq} add Wie erstelle ich ein Ticket? | Nutze {cmd_create}."
|
||||
usage-edit: "&cBenutzung: {cmd_faq} edit <ID> <Frage> | <Antwort>"
|
||||
usage-delete: "&cBenutzung: {cmd_faq} delete <ID>"
|
||||
separator-missing: "&cTrenne Frage und Antwort mit &e|&c, z.B.:"
|
||||
separator-example: "&e{cmd_faq} add Wie erstelle ich ein Ticket? | Nutze {cmd_create}."
|
||||
separator-short: "&cTrenne Frage und Antwort mit &e|&c."
|
||||
invalid-id: "&cUngültige FAQ-ID: &e{id}"
|
||||
created: "&aFAQ &e#{id} &awurde erfolgreich erstellt!"
|
||||
created-question: "&7Frage: &e{question}"
|
||||
created-answer: "&7Antwort: &f{answer}"
|
||||
updated: "&aFAQ &e#{id} &awurde erfolgreich aktualisiert!"
|
||||
deleted: "&aFAQ &e#{id} &awurde gelöscht."
|
||||
not-found: "&cFAQ &e#{id} &cwurde nicht gefunden."
|
||||
reloaded: "&aFAQs wurden neu geladen. ({count} Einträge)"
|
||||
list-header: "&6Häufige Fragen (FAQ) &7— {count} Einträge"
|
||||
list-empty: "&7Noch keine FAQs vorhanden."
|
||||
list-entry: "&e#{id} &f{question}"
|
||||
list-answer: " &7→ &f{answer}"
|
||||
list-admin-hint: "&7Befehle: &e{cmd_faq} add &8| &e{cmd_faq} edit <ID> &8| &e{cmd_faq} delete <ID>"
|
||||
unknown-sub: "&cUnbekannter FAQ-Befehl."
|
||||
hint-open: "&7Benutze &e{cmd_faq} &7zum Öffnen der GUI."
|
||||
admin-commands: "&7Admin-Befehle: &e{cmd_faq} add | edit | delete | reload | list"
|
||||
|
||||
# ============================================================
|
||||
# HILFE-MENÜ (/ticket ohne Argumente)
|
||||
# ============================================================
|
||||
help:
|
||||
header: "�FFFF&lTicketSystem &7– Befehle"
|
||||
create: "&e{cmd_create} [Kategorie] <Text> &7– Neues Ticket erstellen"
|
||||
list: "&e{cmd_list} &7– Deine Tickets ansehen (GUI)"
|
||||
comment: "&e{cmd_comment} <ID> <Text> &7– Nachricht zu einem Ticket"
|
||||
rate: "&e{cmd_rate} <ID> <good|bad> &7– Support bewerten"
|
||||
claim: "&e{cmd_claim} <ID> &7– Ticket annehmen"
|
||||
close: "&e{cmd_close} <ID> [Kommentar] &7– Ticket schließen"
|
||||
forward: "&e{cmd_forward} <ID> <Spieler> &7– Ticket weiterleiten"
|
||||
blacklist: "&e{cmd_blacklist} <add|remove|list> [Spieler] [Grund] &7– Blacklist verwalten"
|
||||
reload: "&e{cmd_reload} &7– Konfiguration neu laden"
|
||||
stats: "&e{cmd_stats} &7– Statistiken anzeigen"
|
||||
bungee-status: "&8[BungeeCord] &7Server: &b{server} &8| Cross-Server-Benachrichtigungen &aaktiv"
|
||||
|
||||
# ============================================================
|
||||
# GUI-TEXTE (TicketGUI)
|
||||
# ============================================================
|
||||
gui:
|
||||
# ── Chat-Nachrichten ────────────────────────────────────
|
||||
no-archive-permission: "&cDu hast keine Berechtigung, das Archiv zu öffnen."
|
||||
no-tickets: "&aDu hast aktuell keine Tickets."
|
||||
filter-label: "&7Filter: {filter}"
|
||||
ticket-removed: "&aDein Ticket &e#{id} &awurde aus deiner Übersicht entfernt."
|
||||
ticket-remove-error: "&cFehler beim Entfernen des Tickets."
|
||||
ticket-remove-claimed: "&cDu kannst dieses Ticket nicht löschen, da es bereits von einem Supporter bearbeitet wird."
|
||||
teleport-success: "&7Du wurdest zu Ticket &e#{id} &7teleportiert."
|
||||
world-not-loaded: "&cDie Welt des Tickets ist nicht geladen!"
|
||||
teleport-disabled: "&cServerübergreifender Teleport ist in der Config deaktiviert.{hint}"
|
||||
teleport-unknown: "&cServer des Tickets unbekannt – Teleport nicht möglich."
|
||||
bungee-connect: "&7Verbinde dich mit Server &b{server} &7für Ticket &e#{id}&7..."
|
||||
bungee-connect-fail: "&cServer-Wechsel fehlgeschlagen. Bitte manuell verbinden."
|
||||
no-delete-permission: "&cDu hast keine Berechtigung, Tickets permanent zu löschen."
|
||||
only-closed-deletable: "&cNur geschlossene Tickets können permanent gelöscht werden."
|
||||
ticket-deleted: "&aTicket &e#{id} &awurde permanent gelöscht."
|
||||
ticket-delete-error: "&cFehler beim Löschen des Tickets."
|
||||
already-closed: "&cDieses Ticket ist bereits geschlossen."
|
||||
close-prompt-header: "&6Ticket #{id} schließen"
|
||||
close-prompt-hint: "&7Gib einen Kommentar ein (&e- &7für keinen)."
|
||||
close-prompt-cancel: "&7Abbrechen mit &ccancel"
|
||||
close-cancelled: "&cAbgebrochen."
|
||||
close-comment-echo: "&7Kommentar: &f{comment}"
|
||||
no-priority-permission: "&cDu hast keine Berechtigung, die Priorität zu ändern."
|
||||
priority-closed: "&cDie Priorität geschlossener Tickets kann nicht geändert werden."
|
||||
priority-set: "&aPriorität auf {priority} &agesetzt."
|
||||
priority-error: "&cFehler beim Ändern der Priorität."
|
||||
comments-header: "&6Kommentare zu Ticket #{id}"
|
||||
comments-empty: "&7Noch keine Kommentare vorhanden."
|
||||
comments-entry: "&e{author} &7({time})&8: &f{message}"
|
||||
|
||||
# ── Inventar-Titel ──────────────────────────────────────
|
||||
item:
|
||||
title-admin: "§8§lTicket-Übersicht"
|
||||
title-archive: "§8§lTicket-Archiv"
|
||||
title-player: "§8§lMeine Tickets"
|
||||
title-detail: "§8§lTicket-Details"
|
||||
|
||||
# ── Lore-Labels in Ticket-Items ─────────────────────
|
||||
lore-creator: "§7Ersteller: §e{value}"
|
||||
lore-message: "§7Anliegen: §f{value}"
|
||||
lore-created: "§7Erstellt: §e{value}"
|
||||
lore-server: "§7Server: §b{value}"
|
||||
lore-world: "§7Welt: §e{value}"
|
||||
lore-position: "§7Position: §e{value}"
|
||||
lore-category: "§7Kategorie: {value}"
|
||||
lore-priority: "§7Priorität: {value}"
|
||||
lore-claimed-by: "§7Angenommen von: §a{value}"
|
||||
lore-claimed-at: "§7Angenommen am: §a{value}"
|
||||
lore-closed-at: "§7Geschlossen am: §c{value}"
|
||||
lore-comment: "§7Kommentar: §f{value}"
|
||||
lore-rating-none: "§7Keine Bewertung"
|
||||
lore-rating-good: "§a👍 Positiv"
|
||||
lore-rating-bad: "§c👎 Negativ"
|
||||
lore-rating-label: "§7Bewertung: {value}"
|
||||
lore-player-deleted: "§cSpieler hat Ticket gelöscht."
|
||||
|
||||
# ── Admin-Listen-Item ───────────────────────────────
|
||||
list-click: "§e§l» KLICKEN für Details"
|
||||
|
||||
# ── Spieler-Listen-Item ─────────────────────────────
|
||||
player-delete-hint: "§c§l» KLICKEN zum Löschen"
|
||||
player-delete-desc: "§7Entferne dieses Ticket aus deiner Übersicht."
|
||||
player-in-progress: "§e» Ticket wird bearbeitet..."
|
||||
player-no-delete: "§7Kann nicht mehr gelöscht werden."
|
||||
player-rate-hint: "§e» /ticket rate {id} good/bad"
|
||||
player-rated-good: "§7Bewertet: §a👍"
|
||||
player-rated-bad: "§7Bewertet: §c👎"
|
||||
player-comment-label: "§7Kommentar des Supports:"
|
||||
|
||||
# ── Detail-Aktions-Buttons ──────────────────────────
|
||||
btn-teleport: "§b§lTeleportieren"
|
||||
btn-teleport-lore1: "§7Teleportiert dich zur"
|
||||
btn-teleport-lore2: "§7Position des Tickets."
|
||||
btn-teleport-bungee1: "§7Teleportiert dich zur Ticket-Position."
|
||||
btn-teleport-same: "§7Dieser Server §a(direkt)"
|
||||
btn-teleport-other: "§7Ziel-Server: §b{server}"
|
||||
btn-teleport-local: "§8Lokaler Teleport"
|
||||
btn-teleport-switch: "§8Server-Wechsel erforderlich"
|
||||
btn-teleport-unknown: "§cServer unbekannt"
|
||||
btn-teleport-disabled: "§8Teleport deaktiviert"
|
||||
btn-teleport-dis1: "§7Im BungeeCord-Modus ist"
|
||||
btn-teleport-dis2: "§7Teleportation deaktiviert."
|
||||
btn-teleport-dis3: "§8(bungee-teleport-enabled: false)"
|
||||
btn-teleport-server: "§7Ticket-Server: §b{server}"
|
||||
btn-teleport-noserver: "§7Server unbekannt"
|
||||
|
||||
btn-claim: "§a§lTicket annehmen"
|
||||
btn-claim-lore1: "§7Nimmt dieses Ticket an"
|
||||
btn-claim-lore2: "§7und markiert es als bearbeitet."
|
||||
btn-claimed: "§8Bereits angenommen"
|
||||
btn-claimed-lore1: "§7Dieses Ticket wurde bereits"
|
||||
btn-claimed-lore2: "§7angenommen."
|
||||
|
||||
btn-delete: "§4§lTicket permanent löschen"
|
||||
btn-delete-lore1: "§7Löscht dieses Ticket"
|
||||
btn-delete-lore2: "§7unwiderruflich aus der Datenbank."
|
||||
btn-delete-warn: "§c§lACHTUNG: §cNicht rückgängig zu machen!"
|
||||
|
||||
btn-close: "§c§lTicket schließen"
|
||||
btn-close-lore1: "§7Schließt das Ticket."
|
||||
btn-close-lore2: "§eKlick für Kommentar-Eingabe."
|
||||
btn-closed: "§8Bereits geschlossen"
|
||||
btn-closed-lore1: "§7Dieses Ticket ist bereits"
|
||||
btn-closed-lore2: "§7geschlossen."
|
||||
|
||||
btn-comments: "§e§lKommentare anzeigen"
|
||||
btn-comments-lore1: "§7Zeigt alle Nachrichten/Antworten"
|
||||
btn-comments-lore2: "§7zu diesem Ticket im Chat."
|
||||
|
||||
btn-prio: "§6§lPriorität ändern"
|
||||
btn-prio-current: "§7Aktuell: {value}"
|
||||
btn-prio-click: "§8Klicken zum Wechseln"
|
||||
|
||||
btn-back: "§7§lZurück"
|
||||
btn-back-lore: "§7Zurück zur Ticket-Übersicht."
|
||||
|
||||
# ── Navigation ──────────────────────────────────────
|
||||
nav-prev: "§7§l◄ Zurück"
|
||||
nav-prev-lore: "§7Seite {page} von {total}"
|
||||
nav-next: "§7§lWeiter ►"
|
||||
nav-next-lore: "§7Seite {page} von {total}"
|
||||
nav-page: "§8Seite {page}/{total}"
|
||||
nav-page-lore: "§7Gesamt: {count} Tickets auf dieser Seite"
|
||||
|
||||
nav-archive: "§7§lGeschlossene Tickets"
|
||||
nav-archive-lore1: "§7Zeigt alle abgeschlossenen"
|
||||
nav-archive-lore2: "§7Tickets im Archiv an."
|
||||
nav-back-overview: "§7§lZurück zur Übersicht"
|
||||
nav-back-ov-lore: "§7Zeigt alle offenen Tickets."
|
||||
|
||||
nav-filter: "§e§lKategorie-Filter"
|
||||
nav-filter-current: "§7Aktuell: {value}"
|
||||
nav-filter-click: "§8Klicken zum Wechseln"
|
||||
nav-filter-all: "§7Alle (kein Filter)"
|
||||
|
||||
# ── FAQ GUI Texte (Neu) ─────────────────────────────────
|
||||
faq:
|
||||
title: "&#FFD700&lHäufige Fragen (FAQ)"
|
||||
admin-title: "§8§lFAQ verwalten"
|
||||
action-title: "§8§lFAQ Aktionen"
|
||||
|
||||
add-button: "§a§lNeues FAQ hinzufügen"
|
||||
add-lore-1: "§7Fügt einen neuen FAQ-Eintrag hinzu."
|
||||
add-lore-2: "§7Du wirst nach Frage und Antwort gefragt."
|
||||
|
||||
edit-button: "§a§lFAQ bearbeiten"
|
||||
edit-lore-1: "§7Ändere Frage und Antwort"
|
||||
edit-lore-2: "§7dieses FAQ-Eintrags."
|
||||
|
||||
delete-button: "§c§lFAQ löschen"
|
||||
delete-lore-1: "§7Löscht diesen FAQ-Eintrag."
|
||||
delete-lore-2: "§c§lACHTUNG: §cNicht rückgängig zu machen!"
|
||||
delete-error: "§cFehler: FAQ #{id} konnte nicht gelöscht werden."
|
||||
|
||||
back-button: "§7§lZurück"
|
||||
back-lore: "§7Zurück zur FAQ-Übersicht."
|
||||
|
||||
click-detail: "§e» Klicken für mehr Details im Chat"
|
||||
click-edit: "§e» Klicken zum Bearbeiten / Löschen"
|
||||
|
||||
nav-prev: "§7§l◄ Zurück"
|
||||
nav-prev-lore: "§7Seite {page} von {total}"
|
||||
nav-next: "§7§lWeiter ►"
|
||||
nav-next-lore: "§7Seite {page} von {total}"
|
||||
nav-page: "§8Seite {page}/{total}"
|
||||
nav-page-lore: "§7Gesamt: {count} FAQ(s)"
|
||||
|
||||
chat-create-title: "§6§lNeues FAQ erstellen"
|
||||
chat-question-prompt: "§7Gib die §eFrage §7ein (oder §ccancel§7):"
|
||||
chat-answer-prompt: "§7Gib jetzt die §eAntwort §7ein (oder §ccancel§7):"
|
||||
chat-edit-title: "§6§lFAQ #{id} bearbeiten"
|
||||
chat-current-question: "§7Aktuelle Frage: §e{question}"
|
||||
|
||||
lore-id: "§7FAQ #{id}"
|
||||
lore-separator: "§8§m "
|
||||
question-set: "§7Frage gesetzt: §e{question}"
|
||||
internal-error: "§cInterner Fehler beim Bearbeiten des FAQs."
|
||||
|
||||
# ============================================================
|
||||
# JOIN-LISTENER
|
||||
# ============================================================
|
||||
join:
|
||||
open-tickets: "&eEs gibt noch &6{count} &eoffene Ticket(s)!"
|
||||
open-tickets-hint: "&7» Tippe &e{cmd_list} &7für die Übersicht."
|
||||
teleport-world-missing: "&cTeleport-Zielwelt &e{world} &cnicht gefunden!"
|
||||
teleport-success: "&7Du wurdest zur Ticket-Position teleportiert. &8({coords})"
|
||||
pending-header: "&6Ticket-Benachrichtigungen &7(während du offline warst):"
|
||||
|
||||
# ============================================================
|
||||
# UPDATE-CHECKER
|
||||
# ============================================================
|
||||
update:
|
||||
available-console: "Neue Version verfügbar: {new} (aktuell: {current})"
|
||||
available-bar: "===================================================="
|
||||
available-line1: "&6[TicketSystem] &eNEUES UPDATE VERFÜGBAR: v{version}"
|
||||
available-line2: "&6[TicketSystem] &eDownload: https://www.spigotmc.org/resources/132757"
|
||||
461
src/main/resources/lang_en.yml
Normal file
461
src/main/resources/lang_en.yml
Normal file
@@ -0,0 +1,461 @@
|
||||
# ============================================================
|
||||
# TicketSystem – Language File English (en)
|
||||
#
|
||||
# All plugin messages can be customized here.
|
||||
# Color codes: & (e.g. &a = green, &c = red, &e = yellow, &7 = grey)
|
||||
# HEX-Codes: &#RRGGBB (e.g. �AA00 = Green)
|
||||
# Placeholders are written in curly braces: {id}, {player}, ...
|
||||
#
|
||||
# Switch language in config.yml: language: en
|
||||
# Switch command language in config.yml: command-language: de | en | both
|
||||
#
|
||||
# {cmd_X} is automatically replaced based on command-language, e.g.:
|
||||
# command-language: de → /ticket erstellen
|
||||
# command-language: en → /ticket create
|
||||
# command-language: both → /ticket create (erstellen)
|
||||
# ============================================================
|
||||
|
||||
prefix: "ᖳFF[&fTicketᖳFF] &r"
|
||||
|
||||
# ============================================================
|
||||
# GENERAL ERRORS & HINTS
|
||||
# ============================================================
|
||||
general:
|
||||
no-permission: "&cYou don't have permission to do this!"
|
||||
console-only: "&cThis command can only be used by players."
|
||||
invalid-id: "&cInvalid ID!"
|
||||
invalid-player-id: "&cInvalid ticket ID: &e{id}"
|
||||
player-not-found: "&cPlayer not found!"
|
||||
ticket-not-found: "&cTicket not found!"
|
||||
already-claimed: "&cThis ticket has already been claimed!"
|
||||
no-open-tickets: "&aThere are no open tickets right now."
|
||||
cooldown: "&cPlease wait &e{seconds} seconds &cbefore creating a new ticket."
|
||||
separator: "򽸱&m "
|
||||
|
||||
# ============================================================
|
||||
# SYSTEM (Export, Import, Migration, Archive, Validation)
|
||||
# ============================================================
|
||||
system:
|
||||
export-success: "&aExport successful: &e{count} &atickets exported to &e{file}&a."
|
||||
export-fail: "&cExport failed or no tickets found."
|
||||
import-success: "&aImport successful: &e{count} &atickets imported."
|
||||
import-fail: "&cImport failed or no tickets found."
|
||||
migration-success: "&aMigration complete: &e{count} &atickets migrated."
|
||||
migration-fail: "&cNo tickets migrated or an error occurred."
|
||||
archive-success: "&aArchiving complete: &e{count} &atickets archived."
|
||||
archive-fail: "&cNo closed tickets found to archive."
|
||||
file-not-found: "&cFile not found: &e{file}"
|
||||
unknown-mode: "&cUnknown mode! Use: tomysql or tofile"
|
||||
validation-warning: "&c&e{count} &cinvalid tickets were found during loading."
|
||||
db-create-error: "&cFailed to create the ticket!"
|
||||
|
||||
# ============================================================
|
||||
# TICKET ACTIONS
|
||||
# ============================================================
|
||||
ticket:
|
||||
created: "&aTicket &e#{id} &ahas been created successfully!"
|
||||
created-category: "&aTicket &e#{id} &acreated! &7Category: {category}"
|
||||
claimed: "&aYou have claimed ticket &e#{id} &afrom &e{player}&a."
|
||||
claimed-notify: "&aYour ticket &e#{id} &ahas been claimed by &e{claimer}&a."
|
||||
closed: "&aTicket &e#{id} &ahas been closed."
|
||||
closed-notify: "&aYour ticket &e#{id} &ahas been closed."
|
||||
forwarded: "&aTicket &e#{id} &ahas been forwarded to &e{player}&a."
|
||||
forwarded-notify: "&eYou have received a forwarded ticket from &6{player}&e. &7(ID: {id})"
|
||||
forwarded-creator: "&eYour ticket &6#{id} &ehas been forwarded to &b{supporter}&e."
|
||||
new-notify: "&e{player} &acreated a new ticket: &7{message} &7(ID: &e{id}&7)"
|
||||
close-comment-label: "&7Support comment: &f{comment}"
|
||||
close-comment-short: "&7Comment: &f{comment}"
|
||||
pending-closed: "&e[Ticket #{id}] &7Your ticket has been closed.{comment}{rating}"
|
||||
pending-closed-comment: " &7Comment: &f{comment}"
|
||||
pending-closed-rating: " &7Rating: &e{cmd_rate} {id} good/bad"
|
||||
|
||||
# ============================================================
|
||||
# NOTIFICATIONS (Team / GUI hint)
|
||||
# ============================================================
|
||||
notify:
|
||||
gui-hint: "&7» Click &e{cmd_list} &7to open the overview."
|
||||
team-category: " §7[§r{category}§7]"
|
||||
team-priority: " §7Priority: §r{priority}"
|
||||
team-server: " §7Server: §b{server}"
|
||||
|
||||
# ============================================================
|
||||
# TICKET CREATE
|
||||
# ============================================================
|
||||
create:
|
||||
usage: "&cUsage: {cmd_create} [category] [priority] <description>"
|
||||
categories-hint: "&7Categories: &ebug&7, &equestion&7, &ecomplaint&7, &eother&7, &egeneral"
|
||||
priorities-hint: "&7Priorities: &alow&7, &enormal&7, &6high&7, &curgent"
|
||||
max-tickets: "&cYou already have &e{max} &copen ticket(s). Please wait until your ticket is processed."
|
||||
no-description: "&cPlease provide a description for your ticket."
|
||||
too-long: "&cYour description is too long! Maximum {max} characters."
|
||||
blacklist-blocked: "&cYou have been banned from the ticket system and cannot create tickets."
|
||||
category-invalid: "&cUnknown category: &e{input}&c. Available categories: &e{categories}"
|
||||
|
||||
# ============================================================
|
||||
# CLAIM / CLOSE / FORWARD
|
||||
# ============================================================
|
||||
claim:
|
||||
usage: "&cUsage: {cmd_claim} <ID>"
|
||||
|
||||
close:
|
||||
usage: "&cUsage: {cmd_close} <ID> [comment]"
|
||||
|
||||
forward:
|
||||
usage: "&cUsage: {cmd_forward} <ID> <player>"
|
||||
bungee-offline: "&7[BungeeCord] Player &e{player} &7is not online on this server."
|
||||
local-not-found: "&cPlayer not found!"
|
||||
|
||||
# ============================================================
|
||||
# COMMENTS
|
||||
# ============================================================
|
||||
comment:
|
||||
saved: "&aYour comment on ticket &e#{id} &ahas been saved."
|
||||
usage: "&cUsage: {cmd_comment} <ID> <message>"
|
||||
too-long: "&cMessage too long! Maximum 500 characters."
|
||||
no-permission: "&cYou can only comment on your own tickets."
|
||||
error: "&cFailed to save the comment."
|
||||
notify-online: "&e[Ticket #{id}] &f{author} &7commented: &f{message}"
|
||||
notify-offline: "&e[Ticket #{id}] &f{author} &7commented while you were offline: &f{message}"
|
||||
claimer-offline: "&e[Ticket #{id}] &f{author} &7commented on your claimed ticket (offline): &f{message}"
|
||||
|
||||
# ============================================================
|
||||
# RATINGS
|
||||
# ============================================================
|
||||
rating:
|
||||
saved-good: "&aThank you for your rating! &a👍 Positive"
|
||||
saved-bad: "&aThank you for your rating! &c👎 Negative"
|
||||
already-rated: "&cYou have already rated this ticket."
|
||||
not-yours: "&cYou can only rate your own tickets."
|
||||
disabled: "&cRatings are currently disabled."
|
||||
not-closeable: "&cRating could not be saved. Is the ticket still open?"
|
||||
usage: "&cUsage: {cmd_rate} <ID> <good|bad>"
|
||||
invalid: "&cInvalid rating! Use &egood &cor &ebad&c."
|
||||
prompt-header: "򽸱&m "
|
||||
prompt-title: "&6How satisfied are you with the support?"
|
||||
prompt-good: "&a{cmd_rate} {id} good &7– 👍 Good"
|
||||
prompt-bad: "&c{cmd_rate} {id} bad &7– 👎 Bad"
|
||||
prompt-footer: "򽸱&m "
|
||||
|
||||
# ============================================================
|
||||
# SET PRIORITY
|
||||
# ============================================================
|
||||
setpriority:
|
||||
usage: "&cUsage: {cmd_setpriority} <ID> <low|normal|high|urgent>"
|
||||
disabled: "&cThe priority system is disabled."
|
||||
invalid: "&cInvalid priority! Valid: &alow&7, &enormal&7, &6high&7, &curgent"
|
||||
success: "&aPriority of ticket &e#{id} &ahas been set to {priority}&a."
|
||||
not-found: "&cTicket &e#{id} &cwas not found."
|
||||
|
||||
# ============================================================
|
||||
# BLACKLIST
|
||||
# ============================================================
|
||||
blacklist:
|
||||
added: "&a{player} &ahas been added to the ticket blacklist. &7Reason: &e{reason}"
|
||||
removed: "&a{player} &ahas been removed from the blacklist."
|
||||
already: "&cPlayer is already on the blacklist."
|
||||
not-found: "&cPlayer was not on the blacklist."
|
||||
usage: "&cUsage: {cmd_blacklist} <add|remove|list> [player] [reason]"
|
||||
usage-add: "&cUsage: {cmd_blacklist} add <player> [reason]"
|
||||
usage-remove: "&cUsage: {cmd_blacklist} remove <player>"
|
||||
list-header: "&6Ticket Blacklist &7({count} entries)"
|
||||
list-empty: "&7No banned players."
|
||||
list-entry: "&e{player} &7– &f{reason} &7(banned by &e{by}&7)"
|
||||
|
||||
# ============================================================
|
||||
# STATISTICS
|
||||
# ============================================================
|
||||
stats:
|
||||
header: "&6Ticket Statistics"
|
||||
total: "&eTotal: &a{count}"
|
||||
open: "&eOpen: &a{count}"
|
||||
closed: "&eClosed: &a{count} &7(historical)"
|
||||
forwarded: "&eForwarded: &a{count}"
|
||||
ratings-header: "&6Support Ratings &7(total, historical)"
|
||||
ratings-summary: "&a👍 Positive: &f{up} &c👎 Negative: &f{down}"
|
||||
ratings-percent: "&7Satisfaction: &e{percent}%"
|
||||
staff-header: "&6Ratings by support staff:"
|
||||
staff-table-header: "&7 Name 👍 👎 Tickets Satisfied"
|
||||
staff-entry: "&e {name} &a{up} &c{down} &7{total} &e{percent}"
|
||||
servers-header: "&6Tickets by server:"
|
||||
server-entry: "&b {server}: &a{count}"
|
||||
top-header: "&6Top-5 Ticket Creators &7(historical, persistent)"
|
||||
top-empty: "&7No data available yet."
|
||||
top-entry: " {medal} &f{name} &e{count} &7{label}"
|
||||
top-ticket-label: "Ticket"
|
||||
top-tickets-label: "Tickets"
|
||||
cache-info: "&7Cache: &e{count} &7cached ticket(s)"
|
||||
|
||||
# ============================================================
|
||||
# TOP CREATORS
|
||||
# ============================================================
|
||||
top:
|
||||
header: "&6&lTop-5 Ticket Creators"
|
||||
empty: "&7No data available yet."
|
||||
entry: "{medal} &f{name} &e{count} &7{label}"
|
||||
footer: "&7(Counts are kept even after tickets are deleted)"
|
||||
|
||||
# ============================================================
|
||||
# RELOAD
|
||||
# ============================================================
|
||||
reload:
|
||||
success: "&aConfiguration reloaded. &7(Categories, FAQs, cache cleared)"
|
||||
bungee-info: "&8[BungeeCord] &7Server: &b{server}"
|
||||
|
||||
# ============================================================
|
||||
# MIGRATE / EXPORT / IMPORT
|
||||
# ============================================================
|
||||
migrate:
|
||||
usage: "&cUsage: {cmd_migrate} <tomysql|tofile>"
|
||||
|
||||
export:
|
||||
usage: "&cUsage: {cmd_export} <filename>"
|
||||
|
||||
import:
|
||||
usage: "&cUsage: {cmd_import} <filename>"
|
||||
|
||||
# ============================================================
|
||||
# FAQ SYSTEM
|
||||
# ============================================================
|
||||
faq:
|
||||
usage-add: "&cUsage: {cmd_faq} add <question> | <answer>"
|
||||
usage-add-example: "&7Example: &e{cmd_faq} add How do I create a ticket? | Use {cmd_create}."
|
||||
usage-edit: "&cUsage: {cmd_faq} edit <ID> <question> | <answer>"
|
||||
usage-delete: "&cUsage: {cmd_faq} delete <ID>"
|
||||
separator-missing: "&cSeparate question and answer with &e|&c, e.g.:"
|
||||
separator-example: "&e{cmd_faq} add How do I create a ticket? | Use {cmd_create}."
|
||||
separator-short: "&cSeparate question and answer with &e|&c."
|
||||
invalid-id: "&cInvalid FAQ ID: &e{id}"
|
||||
created: "&aFAQ &e#{id} &ahas been created successfully!"
|
||||
created-question: "&7Question: &e{question}"
|
||||
created-answer: "&7Answer: &f{answer}"
|
||||
updated: "&aFAQ &e#{id} &ahas been updated successfully!"
|
||||
deleted: "&aFAQ &e#{id} &ahas been deleted."
|
||||
not-found: "&cFAQ &e#{id} &cwas not found."
|
||||
reloaded: "&aFAQs reloaded. ({count} entries)"
|
||||
list-header: "&6Frequently Asked Questions &7— {count} entries"
|
||||
list-empty: "&7No FAQs available yet."
|
||||
list-entry: "&e#{id} &f{question}"
|
||||
list-answer: " &7→ &f{answer}"
|
||||
list-admin-hint: "&7Commands: &e{cmd_faq} add &8| &e{cmd_faq} edit <ID> &8| &e{cmd_faq} delete <ID>"
|
||||
unknown-sub: "&cUnknown FAQ command."
|
||||
hint-open: "&7Use &e{cmd_faq} &7to open the GUI."
|
||||
admin-commands: "&7Admin commands: &e{cmd_faq} add | edit | delete | reload | list"
|
||||
|
||||
# ============================================================
|
||||
# HELP MENU (/ticket without arguments)
|
||||
# ============================================================
|
||||
help:
|
||||
header: "�FF00&lTicketSystem &7– Commands"
|
||||
create: "&e{cmd_create} [category] <text> &7– Create a new ticket"
|
||||
list: "&e{cmd_list} &7– View your tickets (GUI)"
|
||||
comment: "&e{cmd_comment} <ID> <text> &7– Add a message to a ticket"
|
||||
rate: "&e{cmd_rate} <ID> <good|bad> &7– Rate the support"
|
||||
claim: "&e{cmd_claim} <ID> &7– Claim a ticket"
|
||||
close: "&e{cmd_close} <ID> [comment] &7– Close a ticket"
|
||||
forward: "&e{cmd_forward} <ID> <player> &7– Forward a ticket"
|
||||
blacklist: "&e{cmd_blacklist} <add|remove|list> [player] [reason] &7– Manage blacklist"
|
||||
reload: "&e{cmd_reload} &7– Reload configuration"
|
||||
stats: "&e{cmd_stats} &7– Show statistics"
|
||||
bungee-status: "&8[BungeeCord] &7Server: &b{server} &8| Cross-server notifications &aactive"
|
||||
|
||||
# ============================================================
|
||||
# GUI TEXTS (TicketGUI)
|
||||
# ============================================================
|
||||
gui:
|
||||
# ── Chat messages ────────────────────────────────────────
|
||||
no-archive-permission: "&cYou don't have permission to open the archive."
|
||||
no-tickets: "&aYou don't have any tickets right now."
|
||||
filter-label: "&7Filter: {filter}"
|
||||
ticket-removed: "&aYour ticket &e#{id} &ahas been removed from your overview."
|
||||
ticket-remove-error: "&cFailed to remove the ticket."
|
||||
ticket-remove-claimed: "&cYou cannot delete this ticket because it is already being handled by a supporter."
|
||||
teleport-success: "&7You have been teleported to ticket &e#{id}&7."
|
||||
world-not-loaded: "&cThe world of this ticket is not loaded!"
|
||||
teleport-disabled: "&cCross-server teleport is disabled in the config.{hint}"
|
||||
teleport-unknown: "&cTicket server unknown – teleport not possible."
|
||||
bungee-connect: "&7Connecting to server &b{server} &7for ticket &e#{id}&7..."
|
||||
bungee-connect-fail: "&cServer switch failed. Please connect manually."
|
||||
no-delete-permission: "&cYou don't have permission to permanently delete tickets."
|
||||
only-closed-deletable: "&cOnly closed tickets can be permanently deleted."
|
||||
ticket-deleted: "&aTicket &e#{id} &ahas been permanently deleted."
|
||||
ticket-delete-error: "&cFailed to delete the ticket."
|
||||
already-closed: "&cThis ticket is already closed."
|
||||
close-prompt-header: "&6Close ticket #{id}"
|
||||
close-prompt-hint: "&7Enter a comment (&e- &7for none)."
|
||||
close-prompt-cancel: "&7Type &ccancel &7to abort."
|
||||
close-cancelled: "&cCancelled."
|
||||
close-comment-echo: "&7Comment: &f{comment}"
|
||||
no-priority-permission: "&cYou don't have permission to change the priority."
|
||||
priority-closed: "&cThe priority of closed tickets cannot be changed."
|
||||
priority-set: "&aPriority set to {priority}&a."
|
||||
priority-error: "&cFailed to change the priority."
|
||||
comments-header: "&6Comments on ticket #{id}"
|
||||
comments-empty: "&7No comments yet."
|
||||
comments-entry: "&e{author} &7({time})&8: &f{message}"
|
||||
|
||||
# ── Inventory titles ─────────────────────────────────────
|
||||
item:
|
||||
title-admin: "§8§lTicket Overview"
|
||||
title-archive: "§8§lTicket Archive"
|
||||
title-player: "§8§lMy Tickets"
|
||||
title-detail: "§8§lTicket Details"
|
||||
|
||||
# ── Lore labels in ticket items ─────────────────────
|
||||
lore-creator: "§7Creator: §e{value}"
|
||||
lore-message: "§7Message: §f{value}"
|
||||
lore-created: "§7Created: §e{value}"
|
||||
lore-server: "§7Server: §b{value}"
|
||||
lore-world: "§7World: §e{value}"
|
||||
lore-position: "§7Position: §e{value}"
|
||||
lore-category: "§7Category: {value}"
|
||||
lore-priority: "§7Priority: {value}"
|
||||
lore-claimed-by: "§7Claimed by: §a{value}"
|
||||
lore-claimed-at: "§7Claimed at: §a{value}"
|
||||
lore-closed-at: "§7Closed at: §c{value}"
|
||||
lore-comment: "§7Comment: §f{value}"
|
||||
lore-rating-none: "§7No rating"
|
||||
lore-rating-good: "§a👍 Positive"
|
||||
lore-rating-bad: "§c👎 Negative"
|
||||
lore-rating-label: "§7Rating: {value}"
|
||||
lore-player-deleted: "§cPlayer deleted this ticket."
|
||||
|
||||
# ── Admin list item ─────────────────────────────────
|
||||
list-click: "§e§l» CLICK for details"
|
||||
|
||||
# ── Player list item ────────────────────────────────
|
||||
player-delete-hint: "§c§l» CLICK to remove"
|
||||
player-delete-desc: "§7Remove this ticket from your overview."
|
||||
player-in-progress: "§e» Ticket is being processed..."
|
||||
player-no-delete: "§7Cannot be deleted anymore."
|
||||
player-rate-hint: "§e» /ticket rate {id} good/bad"
|
||||
player-rated-good: "§7Rated: §a👍"
|
||||
player-rated-bad: "§7Rated: §c👎"
|
||||
player-comment-label: "§7Support comment:"
|
||||
|
||||
# ── Detail action buttons ───────────────────────────
|
||||
btn-teleport: "§b§lTeleport"
|
||||
btn-teleport-lore1: "§7Teleports you to the"
|
||||
btn-teleport-lore2: "§7location of this ticket."
|
||||
btn-teleport-bungee1: "§7Teleports you to the ticket location."
|
||||
btn-teleport-same: "§7This server §a(direct)"
|
||||
btn-teleport-other: "§7Target server: §b{server}"
|
||||
btn-teleport-local: "§8Local teleport"
|
||||
btn-teleport-switch: "§8Server switch required"
|
||||
btn-teleport-unknown: "§cServer unknown"
|
||||
btn-teleport-disabled: "§8Teleport disabled"
|
||||
btn-teleport-dis1: "§7In BungeeCord mode"
|
||||
btn-teleport-dis2: "§7teleportation is disabled."
|
||||
btn-teleport-dis3: "§8(bungee-teleport-enabled: false)"
|
||||
btn-teleport-server: "§7Ticket server: §b{server}"
|
||||
btn-teleport-noserver: "§7Server unknown"
|
||||
|
||||
btn-claim: "§a§lClaim ticket"
|
||||
btn-claim-lore1: "§7Claims this ticket and"
|
||||
btn-claim-lore2: "§7marks it as being processed."
|
||||
btn-claimed: "§8Already claimed"
|
||||
btn-claimed-lore1: "§7This ticket has already"
|
||||
btn-claimed-lore2: "§7been claimed."
|
||||
|
||||
btn-delete: "§4§lPermanently delete ticket"
|
||||
btn-delete-lore1: "§7Deletes this ticket"
|
||||
btn-delete-lore2: "§7irreversibly from the database."
|
||||
btn-delete-warn: "§c§lWARNING: §cThis cannot be undone!"
|
||||
|
||||
btn-close: "§c§lClose ticket"
|
||||
btn-close-lore1: "§7Closes the ticket."
|
||||
btn-close-lore2: "§eClick to enter a comment."
|
||||
btn-closed: "§8Already closed"
|
||||
btn-closed-lore1: "§7This ticket is already"
|
||||
btn-closed-lore2: "§7closed."
|
||||
|
||||
btn-comments: "§e§lShow comments"
|
||||
btn-comments-lore1: "§7Shows all messages/replies"
|
||||
btn-comments-lore2: "§7for this ticket in chat."
|
||||
|
||||
btn-prio: "§6§lChange priority"
|
||||
btn-prio-current: "§7Current: {value}"
|
||||
btn-prio-click: "§8Click to cycle"
|
||||
|
||||
btn-back: "§7§lBack"
|
||||
btn-back-lore: "§7Back to ticket overview."
|
||||
|
||||
# ── Navigation ──────────────────────────────────────
|
||||
nav-prev: "§7§l◄ Previous"
|
||||
nav-prev-lore: "§7Page {page} of {total}"
|
||||
nav-next: "§7§lNext ►"
|
||||
nav-next-lore: "§7Page {page} of {total}"
|
||||
nav-page: "§8Page {page}/{total}"
|
||||
nav-page-lore: "§7Total: {count} tickets on this page"
|
||||
|
||||
nav-archive: "§7§lClosed Tickets"
|
||||
nav-archive-lore1: "§7Shows all completed"
|
||||
nav-archive-lore2: "§7tickets in the archive."
|
||||
nav-back-overview: "§7§lBack to Overview"
|
||||
nav-back-ov-lore: "§7Shows all open tickets."
|
||||
|
||||
nav-filter: "§e§lCategory Filter"
|
||||
nav-filter-current: "§7Current: {value}"
|
||||
nav-filter-click: "§8Click to cycle"
|
||||
nav-filter-all: "§7All (no filter)"
|
||||
|
||||
# ── FAQ GUI Texts (New) ─────────────────────────────────
|
||||
faq:
|
||||
title: "�FF00&lFrequently Asked Questions (FAQ)"
|
||||
admin-title: "§8§lManage FAQ"
|
||||
action-title: "§8§lFAQ Actions"
|
||||
|
||||
add-button: "§a§lAdd new FAQ"
|
||||
add-lore-1: "§7Adds a new FAQ entry."
|
||||
add-lore-2: "§7You will be asked for question and answer."
|
||||
|
||||
edit-button: "§a§lEdit FAQ"
|
||||
edit-lore-1: "§7Change question and answer"
|
||||
edit-lore-2: "§7of this FAQ entry."
|
||||
|
||||
delete-button: "§c§lDelete FAQ"
|
||||
delete-lore-1: "§7Deletes this FAQ entry."
|
||||
delete-lore-2: "§c§lWARNING: §cCannot be undone!"
|
||||
delete-error: "§cError: FAQ #{id} could not be deleted."
|
||||
|
||||
back-button: "§7§lBack"
|
||||
back-lore: "§7Back to FAQ overview."
|
||||
|
||||
click-detail: "§e» Click for more details in chat"
|
||||
click-edit: "§e» Click to edit / delete"
|
||||
|
||||
nav-prev: "§7§l◄ Previous"
|
||||
nav-prev-lore: "§7Page {page} of {total}"
|
||||
nav-next: "§7§lNext ►"
|
||||
nav-next-lore: "§7Page {page} of {total}"
|
||||
nav-page: "§8Page {page}/{total}"
|
||||
nav-page-lore: "§7Total: {count} FAQ(s)"
|
||||
|
||||
chat-create-title: "§6§lCreate new FAQ"
|
||||
chat-question-prompt: "§7Enter the §eQuestion §7(or §ccancel§7):"
|
||||
chat-answer-prompt: "§7Now enter the §eAnswer §7(or §ccancel§7):"
|
||||
chat-edit-title: "§6§lEdit FAQ #{id}"
|
||||
chat-current-question: "§7Current Question: §e{question}"
|
||||
|
||||
lore-id: "§7FAQ #{id}"
|
||||
lore-separator: "§8§m "
|
||||
question-set: "§7Question set: §e{question}"
|
||||
internal-error: "§cInternal error while editing the FAQ."
|
||||
|
||||
# ============================================================
|
||||
# JOIN LISTENER
|
||||
# ============================================================
|
||||
join:
|
||||
open-tickets: "&eThere are still &6{count} &eopen ticket(s)!"
|
||||
open-tickets-hint: "&7» Type &e{cmd_list} &7for the overview."
|
||||
teleport-world-missing: "&cTeleport target world &e{world} &cnot found!"
|
||||
teleport-success: "&7You have been teleported to the ticket location. &8({coords})"
|
||||
pending-header: "&6Ticket notifications &7(while you were offline):"
|
||||
|
||||
# ============================================================
|
||||
# UPDATE CHECKER
|
||||
# ============================================================
|
||||
update:
|
||||
available-console: "New version available: {new} (current: {current})"
|
||||
available-bar: "===================================================="
|
||||
available-line1: "&6[TicketSystem] &eNEW UPDATE AVAILABLE: v{version}"
|
||||
available-line2: "&6[TicketSystem] &eDownload: https://www.spigotmc.org/resources/132757"
|
||||
@@ -1,5 +1,5 @@
|
||||
name: TicketSystem
|
||||
version: 1.0.5
|
||||
version: 1.0.8
|
||||
main: de.ticketsystem.TicketPlugin
|
||||
api-version: 1.20
|
||||
author: M_Viper
|
||||
|
||||
Reference in New Issue
Block a user