Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5259184e09 | |||
| a45e0f4731 | |||
| 33acd04c3b | |||
| 0a547f90bf | |||
| 02811bafbd | |||
| c8d4578fa6 | |||
| 57a426a9c9 | |||
| 135d8b0fb3 | |||
| 31c7a33cbb | |||
| 301c0f1ce9 | |||
| 7ede377c07 | |||
| 834bd0e5e4 | |||
| f4bedaa288 | |||
| 8e7533a214 | |||
| b7de357e81 |
289
README.md
289
README.md
@@ -2,119 +2,170 @@
|
|||||||
|
|
||||||
  
|
  
|
||||||
|
|
||||||
**TicketSystem** ist das flexible, moderne Support- und Feedback-Plugin für Minecraft-Server. Es bietet flexible Speicherung (MySQL oder Datei), automatische Archivierung, Migration, Export/Import, Statistiken, vollständige Validierung, Debug-Modus, eine übersichtliche config.yml mit Versionsprüfung und eine dynamische GUI.
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **MySQL oder Datei-Speicherung** – YAML/JSON oder MySQL/MariaDB, jederzeit umschaltbar, Migration & Backup inklusive
|
- MySQL oder Datei-Speicherung (YAML/JSON oder MySQL/MariaDB, jederzeit umschaltbar)
|
||||||
- **Automatische Backups & Migration** – Sicheres Wechseln zwischen Speicherarten, Datenverlust ausgeschlossen
|
- Automatische Backups & Migration
|
||||||
- **Export/Import** – Tickets einfach zwischen Servern oder Instanzen übertragen
|
- Export/Import von Tickets
|
||||||
- **Statistiken & Archivierung** – Übersichtliche Auswertung, automatische Archivierung nach Zeitplan, manuelles Archivieren möglich
|
- Statistiken & Archivierung
|
||||||
- **Rollenbasierter Archiv-Zugriff** – Nur Spieler mit `ticket.archive` können das Archiv sehen, öffnen und Tickets permanent löschen – unabhängig von `ticket.admin` oder OP-Status
|
- Rollenbasierter Archiv-Zugriff
|
||||||
- **Konfigurierbare Speicherpfade** – Daten- und Archivdateien frei wählbar, auch absolute Pfade
|
- Konfigurierbare Speicherpfade
|
||||||
- **Vollständige Validierung** – Fehlerhafte Tickets werden beim Laden erkannt, gemeldet und übersprungen
|
- Vollständige Validierung & Fehlerausgaben
|
||||||
- **Bessere Fehlerausgaben** – Alle Fehler erscheinen im Log und für Admins im Chat, inkl. Validierungs- und Speicherfehler
|
- Debug-Modus & Versionsprüfung
|
||||||
- **Debug-Modus & Versionsprüfung** – Für Entwickler und Admins, erkennt veraltete config.yml automatisch
|
- Anpassbare Nachrichten, Farben, Limits, Speicherpfade, Archiv-Intervall, Cooldowns, Rechte
|
||||||
- **Komplett anpassbar** – Nachrichten, Farben, Limits, Speicherpfade, Archiv-Intervall, Cooldowns, Rechte
|
- Dynamische GUI mit Seiten-System
|
||||||
- **Dynamische GUI** – Die Ticket-GUI passt sich automatisch der Ticketanzahl an (bis zu 45 Tickets pro Seite), Item-Material richtet sich nach der konfigurierten Kategorie
|
- Kategorie- und Prioritäten-System
|
||||||
- **Seiten-System** – Bei sehr vielen Tickets wird automatisch geblättert
|
- Bewertungs- und Kommentar-System
|
||||||
- **Kategorie-System** – Frei konfigurierbare Kategorien (Name, Farbe, Material, Aliases) in der config.yml
|
- Offline-Benachrichtigungen
|
||||||
- **Prioritäten-System** – Vier Stufen (LOW / NORMAL / HIGH / URGENT), beim Erstellen wählbar und nachträglich via GUI oder Befehl änderbar
|
- Discord-Webhook mit Embeds & Rollen-Ping
|
||||||
- **Bewertungs-System** – Spieler können nach Ticket-Schließung den Support bewerten (`good` / `bad`), Ergebnisse in `/ticket stats`
|
- Blacklist für Spieler
|
||||||
- **Kommentar-System** – Spieler und Support können Nachrichten direkt am Ticket hinterlassen
|
- **FAQ-System** mit eigenem GUI, eigener Datei (`faqs.yml`) und vollständiger In-Game-Verwaltung durch Admins
|
||||||
- **Offline-Benachrichtigungen** – Verpasste Kommentar-, Schließ- und Status-Benachrichtigungen werden gespeichert und beim nächsten Login angezeigt
|
- **Performance-Caching** – TTL-basierter In-Memory-Cache reduziert Datenbankabfragen spürbar
|
||||||
- **Discord-Webhook** – Benachrichtigungen mit Embeds, konfigurierbarem Rollen-Ping und Kategorie/Priorität-Anzeige
|
- **Saubere Konsole** – minimale Start-Logs, kein unnötiger Spam
|
||||||
- **Blacklist** – Spieler vom Ticket-System ausschließen
|
- Erweiterbarkeit: viele Hooks
|
||||||
- **Performance** – Optimiert für große Server, alle Operationen laufen asynchron und ressourcenschonend
|
- **BungeeCord-Unterstützung**: serverübergreifende Tickets, Teleports, Weiterleitungen, Benachrichtigungen
|
||||||
- **Support & Erweiterbarkeit** – Sauberer Code, viele Hooks für eigene Erweiterungen
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installation & Setup
|
## Installation & Setup
|
||||||
|
|
||||||
1. **TicketSystem.jar** in den `plugins`-Ordner legen und Server starten
|
1. TicketSystem.jar in den plugins-Ordner legen und Server starten
|
||||||
2. **config.yml** anpassen (Speicherorte, Nachrichten, Limits, Farben, MySQL-Daten etc.)
|
2. `config.yml` anpassen (Speicherorte, Nachrichten, Limits, Farben, MySQL-Daten etc.)
|
||||||
3. **/ticket**-Befehle nutzen (siehe unten)
|
3. `/ticket`-Befehle nutzen
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Befehle & Rechte
|
## Befehle & Rechte
|
||||||
|
|
||||||
### Spieler-Befehle
|
### Übersicht der Befehle
|
||||||
|
|
||||||
```
|
| Befehl | Beschreibung | Nutzergruppe |
|
||||||
/ticket - Hilfe & Befehlsübersicht
|
|-----------------------------------------------------------|--------------------------------------------------------|------------------|
|
||||||
/ticket create [Kategorie] [Priorität] <Text> - Neues Ticket erstellen
|
| `/ticket` | Hilfe & Übersicht | Spieler, Support |
|
||||||
/ticket list - Eigene Tickets in der GUI anzeigen
|
| `/ticket create [Kategorie] [Priorität] <Text>` | Ticket erstellen | Spieler |
|
||||||
/ticket comment <ID> <Nachricht> - Kommentar zu einem Ticket hinzufügen
|
| `/ticket list` | Eigene Tickets in der GUI anzeigen | Spieler |
|
||||||
/ticket rate <ID> <good|bad> - Abgeschlossenes Ticket bewerten
|
| `/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 |
|
||||||
|
|
||||||
### Support/Admin-Befehle
|
### Rechte
|
||||||
|
|
||||||
```
|
|
||||||
/ticket claim <ID> - Ticket annehmen
|
|
||||||
/ticket close <ID> [Kommentar] - Ticket schließen
|
|
||||||
/ticket forward <ID> <Spieler> - Ticket weiterleiten
|
|
||||||
/ticket setpriority <ID> <low|normal|high|urgent> - Priorität eines Tickets ändern
|
|
||||||
/ticket reload - Konfiguration neu laden (inkl. Kategorien)
|
|
||||||
/ticket stats - Statistiken anzeigen
|
|
||||||
/ticket archive - Geschlossene Tickets archivieren
|
|
||||||
/ticket blacklist <add|remove|list> [Spieler] [Grund] - Blacklist verwalten
|
|
||||||
/ticket migrate <tomysql|tofile> - Speicherart wechseln
|
|
||||||
/ticket export <Dateiname> - Tickets exportieren
|
|
||||||
/ticket import <Dateiname> - Tickets importieren
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permissions
|
|
||||||
|
|
||||||
| Permission | Beschreibung | Standard |
|
| Permission | Beschreibung | Standard |
|
||||||
|---|---|---|
|
|-----------------|-----------------------------------------------------------------|------------------|
|
||||||
| `ticket.create` | Ticket erstellen | ✅ alle Spieler |
|
| `ticket.create` | Ticket erstellen | alle Spieler |
|
||||||
| `ticket.support` | Tickets einsehen, claimen, schließen & Priorität ändern | ❌ manuell vergeben |
|
| `ticket.support`| Tickets einsehen, claimen, schließen, Priorität ändern | manuell vergeben |
|
||||||
| `ticket.archive` | Archiv öffnen, einsehen & Tickets permanent löschen | ❌ manuell vergeben |
|
| `ticket.archive`| Archiv öffnen, Tickets permanent löschen | manuell vergeben |
|
||||||
| `ticket.admin` | Voller Zugriff inkl. Weiterleitung, Reload & Blacklist (beinhaltet `ticket.support`) | OP |
|
| `ticket.admin` | Voller Zugriff inkl. Weiterleitung, Reload, Blacklist, FAQ-Verwaltung | OP |
|
||||||
|
|
||||||
> ⚠️ **Wichtig:** `ticket.archive` ist bewusst **nicht** in `ticket.admin` enthalten und wird auch **nicht automatisch an OPs vergeben**. Das Archiv-Recht muss explizit zugewiesen werden:
|
> `ticket.archive` ist nicht in `ticket.admin` enthalten und muss explizit vergeben werden.
|
||||||
> ```
|
|
||||||
> /lp user <Spielername> permission set ticket.archive true
|
|
||||||
> ```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Kategorie & Priorität beim Erstellen
|
## 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:
|
Beim Erstellen eines Tickets können Kategorie und Priorität optional angegeben werden:
|
||||||
|
|
||||||
```
|
| Befehl | Kategorie | Priorität |
|
||||||
/ticket create <Text> → Kategorie: Standard, Priorität: NORMAL
|
|-----------------------------------------|-----------|-----------|
|
||||||
/ticket create bug <Text> → Kategorie: Bug, Priorität: NORMAL
|
| `/ticket create <Text>` | Standard | NORMAL |
|
||||||
/ticket create high <Text> → Kategorie: Standard, Priorität: HIGH
|
| `/ticket create bug <Text>` | Bug | NORMAL |
|
||||||
/ticket create bug high <Text> → Kategorie: Bug, Priorität: HIGH
|
| `/ticket create high <Text>` | Standard | HIGH |
|
||||||
/ticket create question urgent <Text> → Kategorie: Frage, Priorität: URGENT
|
| `/ticket create bug high <Text>` | Bug | HIGH |
|
||||||
```
|
| `/ticket create question urgent <Text>` | Frage | URGENT |
|
||||||
|
|
||||||
Verfügbare Prioritäten: `low`, `normal`, `high`, `urgent` (auch auf Deutsch: `niedrig`, `hoch`, `dringend`)
|
**Verfügbare Prioritäten:** `low`, `normal`, `high`, `urgent` (auch deutsch: `niedrig`, `hoch`, `dringend`)
|
||||||
|
|
||||||
Kategorien und ihre Aliases sind frei in der `config.yml` konfigurierbar.
|
Kategorien und Aliases sind frei in der `config.yml` konfigurierbar.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Discord-Webhook
|
## Discord-Webhook
|
||||||
|
|
||||||
Der integrierte Discord-Webhook unterstützt:
|
- Embeds mit Kategorie & Priorität
|
||||||
|
- Rollen-Ping pro Nachrichtentyp
|
||||||
|
- Drei Ereignisse: neues Ticket, Ticket geschlossen, Ticket weitergeleitet
|
||||||
|
|
||||||
- **Embeds** mit Kategorie und Priorität als eigene Felder
|
Konfiguration in `config.yml`:
|
||||||
- **Rollen-Ping** (`@role`) pro Nachrichtentyp einzeln konfigurierbar
|
|
||||||
- **Drei Ereignisse:** Neues Ticket, Ticket geschlossen, Ticket weitergeleitet
|
|
||||||
|
|
||||||
Konfiguration in der `config.yml`:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
discord:
|
discord:
|
||||||
enabled: true
|
enabled: true
|
||||||
webhook-url: "https://discord.com/api/webhooks/..."
|
webhook-url: "https://discord.com/api/webhooks/..."
|
||||||
role-ping-id: "123456789012345678" # Discord-Rollen-ID (leer = kein Ping)
|
role-ping-id: "123456789012345678"
|
||||||
messages:
|
messages:
|
||||||
new-ticket:
|
new-ticket:
|
||||||
role-ping: true
|
role-ping: true
|
||||||
@@ -127,56 +178,88 @@ discord:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## FAQ
|
## BungeeCord-Unterstützung
|
||||||
|
|
||||||
**Kann ich zwischen MySQL und Datei-Speicherung wechseln?**
|
TicketSystem bietet volle Unterstützung für BungeeCord-Netzwerke:
|
||||||
> Ja! Mit `/ticket migrate tomysql` oder `/ticket migrate tofile` werden alle Daten automatisch migriert.
|
|
||||||
|
|
||||||
**Wie konfiguriere ich eigene Kategorien?**
|
- Tickets von jedem Server im Netzwerk
|
||||||
> In der `config.yml` unter `categories:` — Name, Farbe, Material (für die GUI) und Aliases frei wählbar. Änderungen werden mit `/ticket reload` übernommen.
|
- 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
|
||||||
|
|
||||||
**Was passiert mit Benachrichtigungen wenn ein Spieler offline ist?**
|
**Voraussetzungen:**
|
||||||
> Alle Kommentar-, Schließ- und Status-Benachrichtigungen werden gespeichert und beim nächsten Login gebündelt angezeigt.
|
- `spigot.yml`: `bungeecord: true`
|
||||||
|
- `config.yml`: `bungeecord: true`, `server-name` pro Server
|
||||||
**Wie ändere ich die Priorität eines bestehenden Tickets?**
|
- TicketSystem.jar auf allen Spigot-Servern
|
||||||
> Als Support/Admin entweder per Befehl `/ticket setpriority <ID> <Priorität>` oder direkt in der Detail-GUI per Klick.
|
- Alle Server nutzen dieselbe MySQL-Datenbank
|
||||||
|
|
||||||
**Wie aktiviere ich den Debug-Modus?**
|
|
||||||
> Setze `debug: true` in der `config.yml`.
|
|
||||||
|
|
||||||
**Wer darf das Ticket-Archiv sehen?**
|
|
||||||
> Nur Spieler mit der Permission `ticket.archive`. Diese wird weder automatisch an OPs noch an Admins vergeben und muss explizit zugewiesen werden.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Vergleich mit anderen Plugins
|
## Vergleich mit anderen Plugins
|
||||||
|
|
||||||
| Feature | TicketSystem | SimpleTickets | AdvancedTickets |
|
| Feature | TicketSystem | SimpleTickets | AdvancedTickets |
|
||||||
|----------------------------------|:------------:|:-------------:|:---------------:|
|
|-----------------------------|:------------:|:-------------:|:---------------:|
|
||||||
| Speicher-Migration | ✔️ | ⚠️ | ✖️ |
|
| Speicher-Migration | ✔️ | ⚠️ | ✖️ |
|
||||||
| Automatische Backups | ✔️ | ⚠️ | ✖️ |
|
| Automatische Backups | ✔️ | ⚠️ | ✖️ |
|
||||||
| GUI mit Kategorie-Materialien | ✔️ | ⚠️ | ✖️ |
|
| GUI mit Kategorien | ✔️ | ⚠️ | ✖️ |
|
||||||
| Archivierung | ✔️ | ⚠️ | ✖️ |
|
| Archivierung | ✔️ | ⚠️ | ✖️ |
|
||||||
| Rollenbasierter Archiv-Zugriff| ✔️ | ✖️ | ✖️ |
|
| Rollenbasierter Archiv-Zugriff| ✔️ | ✖️ | ✖️ |
|
||||||
| Kategorie-System (konfigurierbar)| ✔️ | ✖️ | ✖️ |
|
| Kategorie-System | ✔️ | ✖️ | ✖️ |
|
||||||
| Prioritäten-System | ✔️ | ✖️ | ✖️ |
|
| Prioritäten-System | ✔️ | ✖️ | ✖️ |
|
||||||
|
| FAQ-System | ✔️ | ✖️ | ✖️ |
|
||||||
|
| Performance-Caching | ✔️ | ✖️ | ✖️ |
|
||||||
| Offline-Benachrichtigungen | ✔️ | ✖️ | ✖️ |
|
| Offline-Benachrichtigungen | ✔️ | ✖️ | ✖️ |
|
||||||
| Discord-Webhook mit Rollen-Ping | ✔️ | ✖️ | ✖️ |
|
| Discord-Webhook | ✔️ | ✖️ | ✖️ |
|
||||||
| Bewertungs-System | ✔️ | ✖️ | ✖️ |
|
| Bewertungs-System | ✔️ | ✖️ | ✖️ |
|
||||||
| Update-Checker | ✔️ | ✖️ | ✖️ |
|
| Update-Checker | ✔️ | ✖️ | ✖️ |
|
||||||
|
| BungeeCord-Unterstützung | ✔️ | ✖️ | ✖️ |
|
||||||
|
|
||||||
|
Legende: ✔️ Vollständige Unterstützung · ⚠️ Eingeschränkt · ✖️ Nicht vorhanden
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Support & Community
|
## FAQ
|
||||||
|
|
||||||
|
**Kann ich zwischen MySQL und Datei-Speicherung wechseln?**
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
**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 `/ticket setpriority <ID> <Priorität>` oder direkt in der GUI.
|
||||||
|
|
||||||
|
**Wie aktiviere ich den Debug-Modus?**
|
||||||
|
> `debug: true` in der `config.yml` setzen.
|
||||||
|
|
||||||
|
**Wer darf das Ticket-Archiv sehen?**
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support, Community & Motivation
|
||||||
|
|
||||||
|
Du hast Fragen, brauchst Hilfe oder möchtest Feedback geben?
|
||||||
|
|
||||||
- [Discord Support](https://discord.com/invite/FdRs4BRd8D)
|
- [Discord Support](https://discord.com/invite/FdRs4BRd8D)
|
||||||
- [Git Issues](https://git.viper.ipv64.net/M_Viper/TicketSystem/issues)
|
- [Git Issues](https://git.viper.ipv64.net/M_Viper/TicketSystem/issues)
|
||||||
|
|
||||||
Wir antworten in der Regel innerhalb von 24 Stunden!
|
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!
|
||||||
## ⭐ Unterstütze das Projekt
|
Jede Rückmeldung hilft, das Plugin weiter zu verbessern und die Community zu stärken.
|
||||||
|
|
||||||
Wenn TicketSystem deinen Server bereichert hat, freuen wir uns über eine **5-Sterne Bewertung auf SpigotMC**!
|
|
||||||
Dein Feedback hilft uns, das Plugin weiter zu verbessern.
|
|
||||||
2
pom.xml
2
pom.xml
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>de.ticketsystem</groupId>
|
<groupId>de.ticketsystem</groupId>
|
||||||
<artifactId>TicketSystem</artifactId>
|
<artifactId>TicketSystem</artifactId>
|
||||||
<version>1.0.3</version>
|
<version>1.0.7</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>TicketSystem</name>
|
<name>TicketSystem</name>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package de.ticketsystem;
|
package de.ticketsystem;
|
||||||
|
|
||||||
|
import de.ticketsystem.bungee.BungeeMessenger;
|
||||||
|
import de.ticketsystem.cache.TicketCache;
|
||||||
import de.ticketsystem.commands.TicketCommand;
|
import de.ticketsystem.commands.TicketCommand;
|
||||||
import de.ticketsystem.database.DatabaseManager;
|
import de.ticketsystem.database.DatabaseManager;
|
||||||
import de.ticketsystem.discord.DiscordWebhook;
|
import de.ticketsystem.discord.DiscordWebhook;
|
||||||
|
import de.ticketsystem.gui.FaqGUI;
|
||||||
import de.ticketsystem.gui.TicketGUI;
|
import de.ticketsystem.gui.TicketGUI;
|
||||||
import de.ticketsystem.listeners.PlayerJoinListener;
|
import de.ticketsystem.listeners.PlayerJoinListener;
|
||||||
|
import de.ticketsystem.manager.CategoryManager;
|
||||||
|
import de.ticketsystem.manager.FaqManager;
|
||||||
import de.ticketsystem.manager.TicketManager;
|
import de.ticketsystem.manager.TicketManager;
|
||||||
// WICHTIG: Import hinzugefügt
|
|
||||||
import de.ticketsystem.model.Ticket;
|
import de.ticketsystem.model.Ticket;
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
@@ -18,10 +22,22 @@ public class TicketPlugin extends JavaPlugin {
|
|||||||
private static TicketPlugin instance;
|
private static TicketPlugin instance;
|
||||||
|
|
||||||
private boolean debug;
|
private boolean debug;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name dieses Servers im BungeeCord-Netzwerk.
|
||||||
|
* Konfigurierbar in config.yml → server-name
|
||||||
|
*/
|
||||||
|
private String serverName;
|
||||||
|
|
||||||
private DatabaseManager databaseManager;
|
private DatabaseManager databaseManager;
|
||||||
private TicketManager ticketManager;
|
private TicketManager ticketManager;
|
||||||
|
private CategoryManager categoryManager;
|
||||||
|
private FaqManager faqManager;
|
||||||
private TicketGUI ticketGUI;
|
private TicketGUI ticketGUI;
|
||||||
|
private FaqGUI faqGUI;
|
||||||
private DiscordWebhook discordWebhook;
|
private DiscordWebhook discordWebhook;
|
||||||
|
private BungeeMessenger bungeeMessenger;
|
||||||
|
private TicketCache ticketCache;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -29,55 +45,78 @@ public class TicketPlugin extends JavaPlugin {
|
|||||||
|
|
||||||
saveDefaultConfig();
|
saveDefaultConfig();
|
||||||
|
|
||||||
// --- WICHTIG: Ticket-Klasse registrieren ---
|
// Ticket-Klasse für YAML-Serialisierung registrieren
|
||||||
Ticket.register();
|
Ticket.register();
|
||||||
// -------------------------------------------
|
|
||||||
|
|
||||||
// Update-Checker
|
// ── BungeeCord Plugin-Messaging-Kanäle registrieren ───────────────
|
||||||
|
getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.BUNGEE_CHANNEL);
|
||||||
|
getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.CUSTOM_CHANNEL);
|
||||||
|
|
||||||
|
bungeeMessenger = new BungeeMessenger(this);
|
||||||
|
getServer().getMessenger().registerIncomingPluginChannel(this, BungeeMessenger.CUSTOM_CHANNEL, bungeeMessenger);
|
||||||
|
|
||||||
|
// 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!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// BungeeCord-Hinweis nur bei deaktiviertem Feature ausgeben
|
||||||
|
if (!getConfig().getBoolean("bungeecord", false)) {
|
||||||
|
getLogger().info("[BungeeCord] Cross-Server-Features deaktiviert. Setze 'bungeecord: true' um sie zu aktivieren.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update-Checker (nur Warnung wenn Update verfügbar – kein API-Raw-Log)
|
||||||
int resourceId = 132757;
|
int resourceId = 132757;
|
||||||
new UpdateChecker(this, resourceId).getVersion(version -> {
|
new UpdateChecker(this, resourceId).getVersion(version -> {
|
||||||
String current = getDescription().getVersion();
|
String current = getDescription().getVersion();
|
||||||
if (!current.equals(version)) {
|
if (!current.equals(version)) {
|
||||||
String msg = ChatColor.translateAlternateColorCodes('&',
|
String msg = ChatColor.translateAlternateColorCodes('&',
|
||||||
"&6[TicketSystem] &eEs ist eine neue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")");
|
"&6[TicketSystem] &eNeue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")");
|
||||||
getLogger().info("Es ist eine neue Version verfügbar: " + version + " (aktuell: " + current + ")");
|
getLogger().warning("Neue Version verfügbar: " + version + " (aktuell: " + current + ")");
|
||||||
getServer().getScheduler().runTaskLater(this, () -> {
|
getServer().getScheduler().runTaskLater(this, () ->
|
||||||
getServer().getOnlinePlayers().stream()
|
getServer().getOnlinePlayers().stream()
|
||||||
.filter(p -> p.hasPermission("ticket.admin"))
|
.filter(p -> p.hasPermission("ticket.admin"))
|
||||||
.forEach(p -> p.sendMessage(msg));
|
.forEach(p -> p.sendMessage(msg)), 20L);
|
||||||
}, 20L);
|
|
||||||
} else {
|
|
||||||
getLogger().info("TicketSystem ist aktuell (Version " + current + ")");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Versionsprüfung
|
// Versionsprüfung der config.yml
|
||||||
String configVersion = getConfig().getString("version", "");
|
String configVersion = getConfig().getString("version", "");
|
||||||
String expectedVersion = "2.0";
|
String expectedVersion = "2.0";
|
||||||
if (!expectedVersion.equals(configVersion)) {
|
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!");
|
+ ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!");
|
||||||
}
|
}
|
||||||
|
|
||||||
debug = getConfig().getBoolean("debug", false);
|
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
|
// Datenbankverbindung
|
||||||
databaseManager = new DatabaseManager(this);
|
databaseManager = new DatabaseManager(this);
|
||||||
if (!databaseManager.connect()) {
|
if (!databaseManager.connect()) {
|
||||||
getLogger().severe("Konnte keine Datenbankverbindung herstellen! Plugin läuft im Datei-Modus weiter.");
|
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);
|
ticketManager = new TicketManager(this);
|
||||||
|
faqManager = new FaqManager(this);
|
||||||
ticketGUI = new TicketGUI(this);
|
ticketGUI = new TicketGUI(this);
|
||||||
|
faqGUI = new FaqGUI(this);
|
||||||
discordWebhook = new DiscordWebhook(this);
|
discordWebhook = new DiscordWebhook(this);
|
||||||
|
|
||||||
if (getConfig().getBoolean("discord.enabled", false)) {
|
if (getConfig().getBoolean("discord.enabled", false)) {
|
||||||
String url = getConfig().getString("discord.webhook-url", "");
|
String url = getConfig().getString("discord.webhook-url", "");
|
||||||
if (url.isEmpty()) {
|
if (url.isEmpty()) {
|
||||||
getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in der config.yml eingetragen!");
|
getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in config.yml eingetragen!");
|
||||||
} else {
|
|
||||||
getLogger().info("[DiscordWebhook] Integration aktiv.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +127,7 @@ public class TicketPlugin extends JavaPlugin {
|
|||||||
|
|
||||||
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
|
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
|
||||||
getServer().getPluginManager().registerEvents(ticketGUI, this);
|
getServer().getPluginManager().registerEvents(ticketGUI, this);
|
||||||
|
getServer().getPluginManager().registerEvents(faqGUI, this);
|
||||||
|
|
||||||
// Automatische Archivierung
|
// Automatische Archivierung
|
||||||
int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24);
|
int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24);
|
||||||
@@ -99,14 +139,17 @@ public class TicketPlugin extends JavaPlugin {
|
|||||||
getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert.");
|
getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert.");
|
||||||
}
|
}
|
||||||
}, ticks, ticks);
|
}, ticks, ticks);
|
||||||
getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogger().info("TicketSystem erfolgreich gestartet!");
|
getLogger().info("TicketSystem v" + getDescription().getVersion() + " erfolgreich gestartet!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
getServer().getMessenger().unregisterOutgoingPluginChannel(this);
|
||||||
|
getServer().getMessenger().unregisterIncomingPluginChannel(this);
|
||||||
|
|
||||||
|
if (ticketCache != null) ticketCache.clear();
|
||||||
if (databaseManager != null) databaseManager.disconnect();
|
if (databaseManager != null) databaseManager.disconnect();
|
||||||
getLogger().info("TicketSystem wurde deaktiviert.");
|
getLogger().info("TicketSystem wurde deaktiviert.");
|
||||||
}
|
}
|
||||||
@@ -128,7 +171,14 @@ public class TicketPlugin extends JavaPlugin {
|
|||||||
public static TicketPlugin getInstance() { return instance; }
|
public static TicketPlugin getInstance() { return instance; }
|
||||||
public DatabaseManager getDatabaseManager() { return databaseManager; }
|
public DatabaseManager getDatabaseManager() { return databaseManager; }
|
||||||
public TicketManager getTicketManager() { return ticketManager; }
|
public TicketManager getTicketManager() { return ticketManager; }
|
||||||
|
public CategoryManager getCategoryManager() { return categoryManager; }
|
||||||
|
public FaqManager getFaqManager() { return faqManager; }
|
||||||
public TicketGUI getTicketGUI() { return ticketGUI; }
|
public TicketGUI getTicketGUI() { return ticketGUI; }
|
||||||
|
public FaqGUI getFaqGUI() { return faqGUI; }
|
||||||
public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
|
public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
|
||||||
|
public BungeeMessenger getBungeeMessenger() { return bungeeMessenger; }
|
||||||
|
public TicketCache getTicketCache() { return ticketCache; }
|
||||||
public boolean isDebug() { return debug; }
|
public boolean isDebug() { return debug; }
|
||||||
|
public String getServerName() { return serverName; }
|
||||||
|
public boolean isBungeeCordEnabled() { return getConfig().getBoolean("bungeecord", false); }
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,8 @@ import java.util.function.Consumer;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateChecker für SpigotMC-Plugins.
|
* UpdateChecker für SpigotMC-Plugins.
|
||||||
* Prüft asynchron, ob eine neue Version verfügbar ist.
|
* Prüft asynchron ob eine neue Version verfügbar ist.
|
||||||
* Quelle: https://www.spigotmc.org/wiki/creating-an-update-checker-that-checks-for-updates
|
* Gibt den Consumer nur aus, wenn die Spigot-Version NEUER ist als die lokale.
|
||||||
*/
|
*/
|
||||||
public class UpdateChecker {
|
public class UpdateChecker {
|
||||||
private final JavaPlugin plugin;
|
private final JavaPlugin plugin;
|
||||||
@@ -24,17 +24,51 @@ public class UpdateChecker {
|
|||||||
|
|
||||||
public void getVersion(final Consumer<String> consumer) {
|
public void getVersion(final Consumer<String> consumer) {
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> {
|
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()) {
|
if (scann.hasNext()) {
|
||||||
String latest = scann.next();
|
String spigotVersion = scann.next().trim();
|
||||||
plugin.getLogger().info("[UpdateChecker] Spigot-API Rückgabe: '" + latest + "'");
|
String localVersion = plugin.getDescription().getVersion().trim();
|
||||||
consumer.accept(latest);
|
|
||||||
} else {
|
// Nur melden wenn Spigot-Version wirklich neuer ist
|
||||||
plugin.getLogger().warning("[UpdateChecker] Keine Version von Spigot erhalten!");
|
if (isNewerVersion(spigotVersion, localVersion)) {
|
||||||
|
consumer.accept(spigotVersion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
263
src/main/java/de/ticketsystem/bungee/BungeeMessenger.java
Normal file
263
src/main/java/de/ticketsystem/bungee/BungeeMessenger.java
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
package de.ticketsystem.bungee;
|
||||||
|
|
||||||
|
import com.google.common.io.ByteArrayDataInput;
|
||||||
|
import com.google.common.io.ByteArrayDataOutput;
|
||||||
|
import com.google.common.io.ByteStreams;
|
||||||
|
import de.ticketsystem.TicketPlugin;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet die BungeeCord Plugin-Messaging-Kanäle für Cross-Server-Kommunikation.
|
||||||
|
*
|
||||||
|
* Kanalübersicht:
|
||||||
|
* Ausgehend: "BungeeCord" – Standard-BungeeCord-Kanal (Forward, Message)
|
||||||
|
* Eingehend: "ticketsystem:notify" – Eigener Kanal für weitergeleitete Nachrichten
|
||||||
|
*
|
||||||
|
* Voraussetzung:
|
||||||
|
* - In spigot.yml muss "bungeecord: true" gesetzt sein
|
||||||
|
* - In plugin.yml müssen beide Kanäle unter "channels:" deklariert sein
|
||||||
|
*
|
||||||
|
* Pakettypen (erstes Byte bei ticketsystem:notify):
|
||||||
|
* 0x01 = TEAM_NOTIFY – Nachricht an alle Online-Supporter/Admins auf diesem Server
|
||||||
|
* 0x02 = PLAYER_MSG – Nachricht an einen bestimmten Spieler (UUID + Text)
|
||||||
|
*
|
||||||
|
* ── BUG FIX ──────────────────────────────────────────────────────────────────
|
||||||
|
* Problem: BungeeCord's "Forward ALL" liefert auf dem Zielserver den inneren
|
||||||
|
* Payload BEREITS ENTPACKT via onPluginMessageReceived auf dem
|
||||||
|
* CUSTOM_CHANNEL. Das war korrekt implementiert.
|
||||||
|
*
|
||||||
|
* Der eigentliche Fehler lag in broadcastTeamNotification():
|
||||||
|
* - Nachrichten mit "\n" wurden als ein einzelner String gesendet.
|
||||||
|
* Minecraft verarbeitet "\n" in sendMessage() nicht → beide Zeilen kamen
|
||||||
|
* als eine zusammen an (unleserlich, aber nicht die Ursache für "gar nichts").
|
||||||
|
* - Die Methode wird jetzt mit einer Liste von Strings aufgerufen (broadcastLines)
|
||||||
|
* damit jede Zeile als separates Paket gesendet wird – klar und lesbar.
|
||||||
|
*
|
||||||
|
* Hauptursache für "gar nichts auf Lobby":
|
||||||
|
* Die plugin.yml hatte keinen "channels:"-Block. Ohne diesen Eintrag
|
||||||
|
* registriert BungeeCord den Kanal "ticketsystem:notify" nicht und
|
||||||
|
* verwirft alle eingehenden Forward-Pakete lautlos auf den Ziel-Servern.
|
||||||
|
* → plugin.yml Fix ist die primäre Lösung.
|
||||||
|
*
|
||||||
|
* Diese Datei enthält zusätzlich Debug-Logging (wenn debug: true in config.yml)
|
||||||
|
* damit zukünftige Probleme schneller gefunden werden können.
|
||||||
|
* ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
public class BungeeMessenger implements PluginMessageListener {
|
||||||
|
|
||||||
|
/** BungeeCord-Standardkanal für Forward/Message-Subkanäle */
|
||||||
|
public static final String BUNGEE_CHANNEL = "BungeeCord";
|
||||||
|
|
||||||
|
/** Eigener Weiterleitungskanal – muss in plugin.yml unter channels stehen */
|
||||||
|
public static final String CUSTOM_CHANNEL = "ticketsystem:notify";
|
||||||
|
|
||||||
|
private static final byte TYPE_TEAM_NOTIFY = 0x01;
|
||||||
|
private static final byte TYPE_PLAYER_MSG = 0x02;
|
||||||
|
|
||||||
|
private final TicketPlugin plugin;
|
||||||
|
|
||||||
|
public BungeeMessenger(TicketPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Ausgehende Nachrichten ────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet eine Chat-Nachricht an einen bestimmten Spieler – egal auf welchem
|
||||||
|
* Server im Netzwerk er sich befindet.
|
||||||
|
*
|
||||||
|
* Reihenfolge:
|
||||||
|
* 1. Spieler ist lokal online → direkte Zustellung
|
||||||
|
* 2. Spieler ist woanders → BungeeCord "Message"-Subkanal (nach Name)
|
||||||
|
* 3. Spieler ist offline → Pendende DB-Benachrichtigung (vorher speichern!)
|
||||||
|
*/
|
||||||
|
public void sendMessageToPlayer(UUID targetUUID, String targetName, String message) {
|
||||||
|
// 1. Lokal online?
|
||||||
|
Player local = Bukkit.getPlayer(targetUUID);
|
||||||
|
if (local != null && local.isOnline()) {
|
||||||
|
local.sendMessage(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Cross-Server via BungeeCord "Message"-Subkanal
|
||||||
|
Player messenger = getAnyOnlinePlayer();
|
||||||
|
if (messenger == null || targetName == null) return;
|
||||||
|
|
||||||
|
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||||
|
out.writeUTF("Message");
|
||||||
|
out.writeUTF(targetName);
|
||||||
|
out.writeUTF(message);
|
||||||
|
messenger.sendPluginMessage(plugin, BUNGEE_CHANNEL, out.toByteArray());
|
||||||
|
|
||||||
|
if (plugin.isDebug()) {
|
||||||
|
plugin.getLogger().info("[DEBUG][BungeeMessenger] sendMessageToPlayer → " + targetName + ": " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcastet eine Team-Benachrichtigung an alle Supporter/Admins im gesamten Netzwerk.
|
||||||
|
*
|
||||||
|
* Lokal online Spieler werden sofort benachrichtigt.
|
||||||
|
* Alle anderen Server erhalten das Paket über den "Forward ALL"-Mechanismus.
|
||||||
|
*
|
||||||
|
* WICHTIG: Jede Zeile wird als separates Paket gesendet damit Minecraft
|
||||||
|
* die Nachrichten korrekt zeilenweise anzeigt.
|
||||||
|
*/
|
||||||
|
public void broadcastTeamNotification(String message) {
|
||||||
|
// Lokale Supporter direkt benachrichtigen
|
||||||
|
Bukkit.getOnlinePlayers().stream()
|
||||||
|
.filter(p -> p.hasPermission("ticket.support") || p.hasPermission("ticket.admin"))
|
||||||
|
.forEach(p -> p.sendMessage(message));
|
||||||
|
|
||||||
|
// An alle anderen Server forwarden
|
||||||
|
Player messenger = getAnyOnlinePlayer();
|
||||||
|
if (messenger == null) {
|
||||||
|
if (plugin.isDebug()) {
|
||||||
|
plugin.getLogger().warning("[DEBUG][BungeeMessenger] broadcastTeamNotification: kein Bote online – Forward nicht möglich!");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendForwardPacket(messenger, message);
|
||||||
|
|
||||||
|
if (plugin.isDebug()) {
|
||||||
|
plugin.getLogger().info("[DEBUG][BungeeMessenger] broadcastTeamNotification gesendet via " + messenger.getName() + ": " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet eine Nachricht an einen bestimmten Spieler via eigenem Forward-Paket.
|
||||||
|
*/
|
||||||
|
public void forwardPlayerMessage(UUID targetUUID, String targetName, String message) {
|
||||||
|
Player local = Bukkit.getPlayer(targetUUID);
|
||||||
|
if (local != null && local.isOnline()) {
|
||||||
|
local.sendMessage(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player messenger = getAnyOnlinePlayer();
|
||||||
|
if (messenger == null) return;
|
||||||
|
|
||||||
|
byte[] uuidBytes = targetUUID.toString().getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
ByteArrayDataOutput inner = ByteStreams.newDataOutput();
|
||||||
|
inner.writeByte(TYPE_PLAYER_MSG);
|
||||||
|
inner.writeShort(uuidBytes.length);
|
||||||
|
inner.write(uuidBytes);
|
||||||
|
inner.writeShort(msgBytes.length);
|
||||||
|
inner.write(msgBytes);
|
||||||
|
byte[] innerBytes = inner.toByteArray();
|
||||||
|
|
||||||
|
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||||
|
out.writeUTF("Forward");
|
||||||
|
out.writeUTF("ALL");
|
||||||
|
out.writeUTF(CUSTOM_CHANNEL);
|
||||||
|
out.writeShort(innerBytes.length);
|
||||||
|
out.write(innerBytes);
|
||||||
|
|
||||||
|
messenger.sendPluginMessage(plugin, BUNGEE_CHANNEL, out.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Eingehende Nachrichten ────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPluginMessageReceived(String channel, Player player, byte[] data) {
|
||||||
|
if (!CUSTOM_CHANNEL.equals(channel)) return;
|
||||||
|
|
||||||
|
if (plugin.isDebug()) {
|
||||||
|
plugin.getLogger().info("[DEBUG][BungeeMessenger] Paket empfangen auf " + channel + ", " + data.length + " Bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ByteArrayDataInput in = ByteStreams.newDataInput(data);
|
||||||
|
byte type = in.readByte();
|
||||||
|
|
||||||
|
if (type == TYPE_TEAM_NOTIFY) {
|
||||||
|
// Rest der Bytes = UTF-8-kodierte Nachricht
|
||||||
|
int len = data.length - 1;
|
||||||
|
byte[] msgBytes = new byte[len];
|
||||||
|
in.readFully(msgBytes);
|
||||||
|
String message = new String(msgBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
if (plugin.isDebug()) {
|
||||||
|
plugin.getLogger().info("[DEBUG][BungeeMessenger] TEAM_NOTIFY empfangen: " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Im Hauptthread an lokale Supporter zustellen
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () ->
|
||||||
|
Bukkit.getOnlinePlayers().stream()
|
||||||
|
.filter(p -> p.hasPermission("ticket.support") || p.hasPermission("ticket.admin"))
|
||||||
|
.forEach(p -> p.sendMessage(message))
|
||||||
|
);
|
||||||
|
|
||||||
|
} else if (type == TYPE_PLAYER_MSG) {
|
||||||
|
int uuidLen = in.readShort();
|
||||||
|
byte[] uuidBytes = new byte[uuidLen];
|
||||||
|
in.readFully(uuidBytes);
|
||||||
|
UUID targetUUID = UUID.fromString(new String(uuidBytes, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
int msgLen = in.readShort();
|
||||||
|
byte[] msgBytes = new byte[msgLen];
|
||||||
|
in.readFully(msgBytes);
|
||||||
|
String message = new String(msgBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
if (plugin.isDebug()) {
|
||||||
|
plugin.getLogger().info("[DEBUG][BungeeMessenger] PLAYER_MSG empfangen für: " + targetUUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
Player target = Bukkit.getPlayer(targetUUID);
|
||||||
|
if (target != null && target.isOnline()) {
|
||||||
|
target.sendMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().warning("[BungeeMessenger] Unbekannter Pakettyp: " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[BungeeMessenger] Fehler beim Verarbeiten einer Plugin-Message: " + e.getMessage());
|
||||||
|
if (plugin.isDebug()) e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut und sendet ein Forward-ALL-Paket mit TYPE_TEAM_NOTIFY.
|
||||||
|
*/
|
||||||
|
private void sendForwardPacket(Player messenger, String message) {
|
||||||
|
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
ByteArrayDataOutput inner = ByteStreams.newDataOutput();
|
||||||
|
inner.writeByte(TYPE_TEAM_NOTIFY);
|
||||||
|
inner.write(msgBytes);
|
||||||
|
byte[] innerBytes = inner.toByteArray();
|
||||||
|
|
||||||
|
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||||
|
out.writeUTF("Forward");
|
||||||
|
out.writeUTF("ALL");
|
||||||
|
out.writeUTF(CUSTOM_CHANNEL);
|
||||||
|
out.writeShort(innerBytes.length);
|
||||||
|
out.write(innerBytes);
|
||||||
|
|
||||||
|
messenger.sendPluginMessage(plugin, BUNGEE_CHANNEL, out.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt einen beliebigen online Spieler zurück der als "Bote" für Plugin-Messages
|
||||||
|
* verwendet werden kann. BungeeCord verlangt einen Spieler als Absender.
|
||||||
|
*/
|
||||||
|
private Player getAnyOnlinePlayer() {
|
||||||
|
Collection<? extends Player> online = Bukkit.getOnlinePlayers();
|
||||||
|
return online.isEmpty() ? null : online.iterator().next();
|
||||||
|
}
|
||||||
|
}
|
||||||
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
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
|||||||
package de.ticketsystem.discord;
|
package de.ticketsystem.discord;
|
||||||
|
|
||||||
import de.ticketsystem.TicketPlugin;
|
import de.ticketsystem.TicketPlugin;
|
||||||
|
import de.ticketsystem.model.ConfigCategory;
|
||||||
import de.ticketsystem.model.Ticket;
|
import de.ticketsystem.model.Ticket;
|
||||||
|
import de.ticketsystem.model.TicketPriority;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@@ -9,156 +11,344 @@ import java.net.HttpURLConnection;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
|
||||||
* Sendet Benachrichtigungen an einen Discord-Webhook.
|
|
||||||
* Unterstützt Embeds mit Farbe, Feldern und Timestamp.
|
|
||||||
*/
|
|
||||||
public class DiscordWebhook {
|
public class DiscordWebhook {
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Konstanten & Felder
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static final String AVATAR_URL = "https://mc-heads.net/avatar/%s/64";
|
||||||
|
|
||||||
private final TicketPlugin plugin;
|
private final TicketPlugin plugin;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Konstruktor
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public DiscordWebhook(TicketPlugin plugin) {
|
public DiscordWebhook(TicketPlugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Öffentliche Methoden ──────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Öffentliche Methoden – Webhook-Events
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Sendet eine Benachrichtigung wenn ein neues Ticket erstellt wurde.
|
|
||||||
*/
|
|
||||||
public void sendNewTicket(Ticket ticket) {
|
public void sendNewTicket(Ticket ticket) {
|
||||||
if (!isEnabled()) return;
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
String webhookUrl = plugin.getConfig().getString("discord.webhook-url", "");
|
String webhookUrl = getWebhookUrl();
|
||||||
if (webhookUrl.isEmpty()) return;
|
if (webhookUrl == null) return;
|
||||||
|
|
||||||
// Felder aus Config lesen
|
// Konfiguration lesen
|
||||||
String title = plugin.getConfig().getString("discord.messages.new-ticket.title", "🎫 Neues Ticket erstellt");
|
String title = plugin.getConfig().getString ("discord.messages.new-ticket.title", "Neues Ticket");
|
||||||
String color = plugin.getConfig().getString("discord.messages.new-ticket.color", "3066993"); // Grün
|
String color = plugin.getConfig().getString ("discord.messages.new-ticket.color", "5793266");
|
||||||
String footer = plugin.getConfig().getString ("discord.messages.new-ticket.footer", "TicketSystem");
|
String footer = plugin.getConfig().getString ("discord.messages.new-ticket.footer", "TicketSystem");
|
||||||
boolean showPos = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-position", true);
|
boolean showPos = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-position", true);
|
||||||
|
boolean showCat = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-category", true);
|
||||||
|
boolean showPri = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-priority", true);
|
||||||
|
boolean showSrv = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-server", true);
|
||||||
|
boolean ping = plugin.getConfig().getBoolean("discord.messages.new-ticket.role-ping", true);
|
||||||
|
|
||||||
// JSON-Embed aufbauen
|
// Hilfs-Werte berechnen
|
||||||
StringBuilder fields = new StringBuilder();
|
String prioEmoji = getPriorityEmoji(ticket.getPriority());
|
||||||
fields.append(field("Spieler", ticket.getCreatorName(), true));
|
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
|
||||||
fields.append(",");
|
|
||||||
fields.append(field("Ticket ID", "#" + ticket.getId(), true));
|
// Felder aufbauen
|
||||||
fields.append(",");
|
List<Field> fields = new ArrayList<>();
|
||||||
fields.append(field("Anliegen", ticket.getMessage(), false));
|
fields.add(new Field("👤 Spieler", ticket.getCreatorName(), true));
|
||||||
|
fields.add(new Field("🎫 Ticket", "#" + ticket.getId(), true));
|
||||||
|
|
||||||
|
if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
|
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
||||||
|
fields.add(new Field("🏷️ Kategorie", cat.getName(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||||
|
fields.add(new Field("⚡ Priorität", prioEmoji + " " + ticket.getPriority().getDisplayName(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSrv && plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||||
|
fields.add(new Field("🖥️ Server", ticket.getServerName(), true));
|
||||||
|
}
|
||||||
|
|
||||||
if (showPos) {
|
if (showPos) {
|
||||||
fields.append(",");
|
fields.add(new Field("🌍 Welt", ticket.getWorldName(), true));
|
||||||
fields.append(field("Welt", ticket.getWorldName(), true));
|
fields.add(new Field("📍 Position",
|
||||||
fields.append(",");
|
|
||||||
fields.append(field("Position",
|
|
||||||
String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()), true));
|
String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer);
|
// JSON zusammenbauen & senden
|
||||||
|
String description = "**Anliegen**\n> " + j(ticket.getMessage());
|
||||||
|
|
||||||
|
String json = buildJson(
|
||||||
|
ping ? buildRolePing() : "",
|
||||||
|
prioEmoji + " " + j(title) + " #" + ticket.getId(),
|
||||||
|
description,
|
||||||
|
Integer.parseInt(color),
|
||||||
|
j(ticket.getCreatorName()), avatarUrl,
|
||||||
|
avatarUrl,
|
||||||
|
fields,
|
||||||
|
j(footer) + " • Neues Ticket"
|
||||||
|
);
|
||||||
|
|
||||||
sendAsync(webhookUrl, json);
|
sendAsync(webhookUrl, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
* Sendet eine Benachrichtigung wenn ein Ticket geschlossen wurde.
|
|
||||||
*/
|
|
||||||
public void sendTicketClosed(Ticket ticket, String closerName) {
|
public void sendTicketClosed(Ticket ticket, String closerName) {
|
||||||
if (!isEnabled()) return;
|
if (!isEnabled()) return;
|
||||||
if (!plugin.getConfig().getBoolean("discord.messages.ticket-closed.enabled", false)) return;
|
if (!plugin.getConfig().getBoolean("discord.messages.ticket-closed.enabled", false)) return;
|
||||||
|
|
||||||
String webhookUrl = plugin.getConfig().getString("discord.webhook-url", "");
|
String webhookUrl = getWebhookUrl();
|
||||||
if (webhookUrl.isEmpty()) return;
|
if (webhookUrl == null) return;
|
||||||
|
|
||||||
String title = plugin.getConfig().getString("discord.messages.ticket-closed.title", "🔒 Ticket geschlossen");
|
// Konfiguration lesen
|
||||||
String color = plugin.getConfig().getString("discord.messages.ticket-closed.color", "15158332"); // Rot
|
String title = plugin.getConfig().getString ("discord.messages.ticket-closed.title", "Ticket geschlossen");
|
||||||
|
String color = plugin.getConfig().getString ("discord.messages.ticket-closed.color", "15548997");
|
||||||
String footer = plugin.getConfig().getString ("discord.messages.ticket-closed.footer", "TicketSystem");
|
String footer = plugin.getConfig().getString ("discord.messages.ticket-closed.footer", "TicketSystem");
|
||||||
|
boolean showCat = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-category", true);
|
||||||
|
boolean showPri = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-priority", true);
|
||||||
|
boolean showSrv = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-server", true);
|
||||||
|
boolean ping = plugin.getConfig().getBoolean("discord.messages.ticket-closed.role-ping", false);
|
||||||
|
|
||||||
|
// Hilfs-Werte berechnen
|
||||||
|
String prioEmoji = getPriorityEmoji(ticket.getPriority());
|
||||||
|
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
|
||||||
|
|
||||||
|
// Beschreibung aufbauen
|
||||||
|
StringBuilder desc = new StringBuilder();
|
||||||
|
desc.append("**Anliegen**\n> ").append(j(ticket.getMessage()));
|
||||||
|
|
||||||
StringBuilder fields = new StringBuilder();
|
|
||||||
fields.append(field("Ticket ID", "#" + ticket.getId(), true));
|
|
||||||
fields.append(",");
|
|
||||||
fields.append(field("Ersteller", ticket.getCreatorName(), true));
|
|
||||||
fields.append(",");
|
|
||||||
fields.append(field("Geschlossen von", closerName, true));
|
|
||||||
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
|
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
|
||||||
fields.append(",");
|
desc.append("\n\n**Kommentar des Supports**\n> ").append(j(ticket.getCloseComment()));
|
||||||
fields.append(field("Kommentar", ticket.getCloseComment(), false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer);
|
// Felder aufbauen
|
||||||
|
List<Field> fields = new ArrayList<>();
|
||||||
|
fields.add(new Field("👤 Ersteller", ticket.getCreatorName(), true));
|
||||||
|
fields.add(new Field("🔒 Geschlossen von", j(closerName), true));
|
||||||
|
fields.add(new Field("🎫 Ticket ID", "#" + ticket.getId(), true));
|
||||||
|
|
||||||
|
if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
|
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
||||||
|
fields.add(new Field("🏷️ Kategorie", cat.getName(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||||
|
fields.add(new Field("⚡ Priorität", prioEmoji + " " + ticket.getPriority().getDisplayName(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSrv && plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||||
|
fields.add(new Field("🖥️ Server", ticket.getServerName(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.getConfig().getBoolean("rating-enabled", true) && ticket.getPlayerRating() != null) {
|
||||||
|
String rating = "THUMBS_UP".equals(ticket.getPlayerRating()) ? "👍 Positiv" : "👎 Negativ";
|
||||||
|
fields.add(new Field("⭐ Bewertung", rating, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON zusammenbauen & senden
|
||||||
|
String json = buildJson(
|
||||||
|
ping ? buildRolePing() : "",
|
||||||
|
"🔒 " + j(title) + " #" + ticket.getId(),
|
||||||
|
desc.toString(),
|
||||||
|
Integer.parseInt(color),
|
||||||
|
j(ticket.getCreatorName()), avatarUrl,
|
||||||
|
avatarUrl,
|
||||||
|
fields,
|
||||||
|
j(footer) + " • Ticket geschlossen"
|
||||||
|
);
|
||||||
|
|
||||||
sendAsync(webhookUrl, json);
|
sendAsync(webhookUrl, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
* Sendet eine Benachrichtigung wenn ein Ticket weitergeleitet wurde.
|
|
||||||
*/
|
|
||||||
public void sendTicketForwarded(Ticket ticket, String fromName) {
|
public void sendTicketForwarded(Ticket ticket, String fromName) {
|
||||||
if (!isEnabled()) return;
|
if (!isEnabled()) return;
|
||||||
if (!plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.enabled", false)) return;
|
if (!plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.enabled", false)) return;
|
||||||
|
|
||||||
String webhookUrl = plugin.getConfig().getString("discord.webhook-url", "");
|
String webhookUrl = getWebhookUrl();
|
||||||
if (webhookUrl.isEmpty()) return;
|
if (webhookUrl == null) return;
|
||||||
|
|
||||||
String title = plugin.getConfig().getString("discord.messages.ticket-forwarded.title", "🔀 Ticket weitergeleitet");
|
// Konfiguration lesen
|
||||||
String color = plugin.getConfig().getString("discord.messages.ticket-forwarded.color", "15105570"); // Orange
|
String title = plugin.getConfig().getString ("discord.messages.ticket-forwarded.title", "Ticket weitergeleitet");
|
||||||
|
String color = plugin.getConfig().getString ("discord.messages.ticket-forwarded.color", "15105570");
|
||||||
String footer = plugin.getConfig().getString ("discord.messages.ticket-forwarded.footer", "TicketSystem");
|
String footer = plugin.getConfig().getString ("discord.messages.ticket-forwarded.footer", "TicketSystem");
|
||||||
|
boolean showCat = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-category", true);
|
||||||
|
boolean showPri = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-priority", true);
|
||||||
|
boolean showSrv = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-server", true);
|
||||||
|
boolean ping = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.role-ping", false);
|
||||||
|
|
||||||
StringBuilder fields = new StringBuilder();
|
// Hilfs-Werte berechnen
|
||||||
fields.append(field("Ticket ID", "#" + ticket.getId(), true));
|
String prioEmoji = getPriorityEmoji(ticket.getPriority());
|
||||||
fields.append(",");
|
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
|
||||||
fields.append(field("Ersteller", ticket.getCreatorName(), true));
|
|
||||||
fields.append(",");
|
// Felder aufbauen
|
||||||
fields.append(field("Weitergeleitet von", fromName, true));
|
String forwardedTo = ticket.getForwardedToName() != null ? j(ticket.getForwardedToName()) : "–";
|
||||||
fields.append(",");
|
|
||||||
fields.append(field("Weitergeleitet an", ticket.getForwardedToName(), true));
|
List<Field> fields = new ArrayList<>();
|
||||||
|
fields.add(new Field("👤 Ersteller", ticket.getCreatorName(), true));
|
||||||
|
fields.add(new Field("📤 Weitergeleitet von", j(fromName), true));
|
||||||
|
fields.add(new Field("📥 Weitergeleitet an", forwardedTo, true));
|
||||||
|
fields.add(new Field("🎫 Ticket ID", "#" + ticket.getId(), true));
|
||||||
|
|
||||||
|
if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
|
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
||||||
|
fields.add(new Field("🏷️ Kategorie", cat.getName(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||||
|
fields.add(new Field("⚡ Priorität", prioEmoji + " " + ticket.getPriority().getDisplayName(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSrv && plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||||
|
fields.add(new Field("🖥️ Server", ticket.getServerName(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON zusammenbauen & senden
|
||||||
|
String json = buildJson(
|
||||||
|
ping ? buildRolePing() : "",
|
||||||
|
"🔀 " + j(title) + " #" + ticket.getId(),
|
||||||
|
"**Anliegen**\n> " + j(ticket.getMessage()),
|
||||||
|
Integer.parseInt(color),
|
||||||
|
j(ticket.getCreatorName()), avatarUrl,
|
||||||
|
avatarUrl,
|
||||||
|
fields,
|
||||||
|
j(footer) + " • Ticket weitergeleitet"
|
||||||
|
);
|
||||||
|
|
||||||
String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer);
|
|
||||||
sendAsync(webhookUrl, json);
|
sendAsync(webhookUrl, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Private Hilfsmethoden ─────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// JSON-Bau
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private record Field(String name, String value, boolean inline) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut den kompletten JSON-Payload ohne String.format()-Chaos.
|
||||||
|
* Kein verschachteltes Escaping, kein ungültiges JSON.
|
||||||
|
*/
|
||||||
|
private String buildJson(
|
||||||
|
String content,
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
int color,
|
||||||
|
String authorName,
|
||||||
|
String authorIcon,
|
||||||
|
String thumbnailUrl,
|
||||||
|
List<Field> fields,
|
||||||
|
String footer
|
||||||
|
) {
|
||||||
|
// Fields-Array aufbauen
|
||||||
|
StringBuilder fieldsJson = new StringBuilder("[");
|
||||||
|
for (int i = 0; i < fields.size(); i++) {
|
||||||
|
Field f = fields.get(i);
|
||||||
|
if (i > 0) fieldsJson.append(",");
|
||||||
|
fieldsJson
|
||||||
|
.append("{")
|
||||||
|
.append("\"name\":") .append(jsonString(f.name())) .append(",")
|
||||||
|
.append("\"value\":") .append(jsonString(f.value())) .append(",")
|
||||||
|
.append("\"inline\":") .append(f.inline())
|
||||||
|
.append("}");
|
||||||
|
}
|
||||||
|
fieldsJson.append("]");
|
||||||
|
|
||||||
|
// Embed-Objekt aufbauen
|
||||||
|
StringBuilder embed = new StringBuilder("{");
|
||||||
|
embed.append("\"title\":") .append(jsonString(title)) .append(",");
|
||||||
|
embed.append("\"description\":") .append(jsonString(description)) .append(",");
|
||||||
|
embed.append("\"color\":") .append(color) .append(",");
|
||||||
|
embed.append("\"author\":{")
|
||||||
|
.append("\"name\":") .append(jsonString(authorName)) .append(",")
|
||||||
|
.append("\"icon_url\":") .append(jsonString(authorIcon))
|
||||||
|
.append("},");
|
||||||
|
embed.append("\"thumbnail\":{\"url\":").append(jsonString(thumbnailUrl)).append("},");
|
||||||
|
embed.append("\"fields\":") .append(fieldsJson) .append(",");
|
||||||
|
embed.append("\"footer\":{\"text\":").append(jsonString(footer)) .append("},");
|
||||||
|
embed.append("\"timestamp\":") .append(jsonString(Instant.now().toString()));
|
||||||
|
embed.append("}");
|
||||||
|
|
||||||
|
// Root-Objekt
|
||||||
|
return "{" +
|
||||||
|
"\"content\":" + jsonString(content) + "," +
|
||||||
|
"\"embeds\":[" + embed + "]" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt einen JSON-String-Wert zurück inkl. Anführungszeichen.
|
||||||
|
* Alle Sonderzeichen werden korrekt escaped.
|
||||||
|
*/
|
||||||
|
private String jsonString(String value) {
|
||||||
|
if (value == null) value = "";
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder("\"");
|
||||||
|
for (char c : value.toCharArray()) {
|
||||||
|
switch (c) {
|
||||||
|
case '"' -> sb.append("\\\"");
|
||||||
|
case '\\' -> sb.append("\\\\");
|
||||||
|
case '\n' -> sb.append("\\n");
|
||||||
|
case '\r' -> { /* ignorieren */ }
|
||||||
|
case '\t' -> sb.append("\\t");
|
||||||
|
default -> {
|
||||||
|
if (c < 0x20) {
|
||||||
|
sb.append(String.format("\\u%04x", (int) c));
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("\"");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kurz-Alias: Escaped einen Wert für die Verwendung innerhalb von
|
||||||
|
* description-Strings (die bereits durch jsonString() laufen).
|
||||||
|
*/
|
||||||
|
private String j(String s) {
|
||||||
|
if (s == null) return "–";
|
||||||
|
return s.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", " ")
|
||||||
|
.replace("\r", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Hilfsmethoden
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private boolean isEnabled() {
|
private boolean isEnabled() {
|
||||||
return plugin.getConfig().getBoolean("discord.enabled", false);
|
return plugin.getConfig().getBoolean("discord.enabled", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private String getWebhookUrl() {
|
||||||
* Baut einen einzelnen Embed-Field als JSON-String.
|
String url = plugin.getConfig().getString("discord.webhook-url", "");
|
||||||
*/
|
return url.isEmpty() ? null : url;
|
||||||
private String field(String name, String value, boolean inline) {
|
|
||||||
// Anführungszeichen und Backslashes im Wert escapen
|
|
||||||
String safeValue = value != null
|
|
||||||
? value.replace("\\", "\\\\").replace("\"", "\\\"")
|
|
||||||
: "–";
|
|
||||||
String safeName = name.replace("\\", "\\\\").replace("\"", "\\\"");
|
|
||||||
return String.format("{\"name\":\"%s\",\"value\":\"%s\",\"inline\":%b}",
|
|
||||||
safeName, safeValue, inline);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private String buildRolePing() {
|
||||||
* Baut den kompletten Webhook-Payload als JSON.
|
String roleId = plugin.getConfig().getString("discord.role-ping-id", "").trim();
|
||||||
*/
|
return roleId.isEmpty() ? "" : "<@&" + roleId + ">";
|
||||||
private String buildPayload(String title, int color, String fieldsJson, String footer) {
|
}
|
||||||
String timestamp = Instant.now().toString(); // ISO-8601
|
|
||||||
return String.format("""
|
private String getPriorityEmoji(TicketPriority priority) {
|
||||||
{
|
return switch (priority) {
|
||||||
"embeds": [{
|
case LOW -> "🟢";
|
||||||
"title": "%s",
|
case NORMAL -> "🟡";
|
||||||
"color": %d,
|
case HIGH -> "🟠";
|
||||||
"fields": [%s],
|
case URGENT -> "🔴";
|
||||||
"footer": { "text": "%s" },
|
};
|
||||||
"timestamp": "%s"
|
|
||||||
}]
|
|
||||||
}""",
|
|
||||||
title.replace("\"", "\\\""),
|
|
||||||
color,
|
|
||||||
fieldsJson,
|
|
||||||
footer.replace("\"", "\\\""),
|
|
||||||
timestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sendet den JSON-Payload asynchron an den Webhook.
|
|
||||||
*/
|
|
||||||
private void sendAsync(String webhookUrl, String json) {
|
private void sendAsync(String webhookUrl, String json) {
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
try {
|
try {
|
||||||
@@ -176,16 +366,20 @@ public class DiscordWebhook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
int responseCode = conn.getResponseCode();
|
||||||
|
|
||||||
if (plugin.isDebug()) {
|
if (plugin.isDebug()) {
|
||||||
plugin.getLogger().info("[DEBUG] Discord Webhook Response: " + responseCode);
|
plugin.getLogger().info("[DEBUG][DiscordWebhook] Response: " + responseCode);
|
||||||
|
if (responseCode != 200 && responseCode != 204) {
|
||||||
|
plugin.getLogger().info("[DEBUG][DiscordWebhook] Payload: " + json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 204 = No Content → Erfolg bei Discord
|
|
||||||
if (responseCode != 200 && responseCode != 204) {
|
if (responseCode != 200 && responseCode != 204) {
|
||||||
plugin.getLogger().warning("[DiscordWebhook] Unerwarteter Response-Code: " + responseCode);
|
plugin.getLogger().warning("[DiscordWebhook] Unerwarteter Response-Code: " + responseCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.disconnect();
|
conn.disconnect();
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().warning("[DiscordWebhook] Fehler beim Senden: " + e.getMessage());
|
plugin.getLogger().warning("[DiscordWebhook] Fehler beim Senden: " + e.getMessage());
|
||||||
if (plugin.isDebug()) e.printStackTrace();
|
if (plugin.isDebug()) e.printStackTrace();
|
||||||
|
|||||||
449
src/main/java/de/ticketsystem/gui/FaqGUI.java
Normal file
449
src/main/java/de/ticketsystem/gui/FaqGUI.java
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
package de.ticketsystem.gui;
|
||||||
|
|
||||||
|
import de.ticketsystem.TicketPlugin;
|
||||||
|
import de.ticketsystem.model.FaqEntry;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
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).
|
||||||
|
*
|
||||||
|
* ──── Spieler-GUI (/ticket faq) ─────────────────────────────────────────
|
||||||
|
* Slots 0-44 : FAQ-Einträge als Custom-Skull-Items
|
||||||
|
* › Name = Frage
|
||||||
|
* › Lore = Antwort (aufgeteilt auf 40-Zeichen-Zeilen)
|
||||||
|
* Slot 49 : Seitenanzeige
|
||||||
|
* Slot 45/53 : Vorherige / Nächste Seite
|
||||||
|
*
|
||||||
|
* ──── Admin-GUI (ticket.admin-Berechtigung) ─────────────────────────────
|
||||||
|
* Wie Spieler-GUI, zusätzlich:
|
||||||
|
* Slot 50 : "Neues FAQ hinzufügen" (Lime Wool)
|
||||||
|
* Klick auf FAQ-Item → Aktions-GUI
|
||||||
|
*
|
||||||
|
* ──── Aktions-GUI (27 Slots) ────────────────────────────────────────────
|
||||||
|
* Slot 4 : FAQ-Info (Skull)
|
||||||
|
* Slot 10 : Bearbeiten (Book & Quill)
|
||||||
|
* Slot 12 : Löschen (Barrier)
|
||||||
|
* Slot 16 : Zurück (Arrow)
|
||||||
|
*
|
||||||
|
* ──── Chat-Eingabe (Hinzufügen / Bearbeiten) ────────────────────────────
|
||||||
|
* Step 1: Admin gibt Frage ein
|
||||||
|
* Step 2: Admin gibt Antwort ein → FAQ wird gespeichert
|
||||||
|
*/
|
||||||
|
public class FaqGUI implements Listener {
|
||||||
|
|
||||||
|
// ─────────────────────────── Titel-Konstanten ──────────────────────────
|
||||||
|
|
||||||
|
private static final String FAQ_GUI_TITLE = "§8§lHäufige Fragen (FAQ)";
|
||||||
|
private static final String FAQ_ADMIN_TITLE = "§8§lFAQ verwalten";
|
||||||
|
private static final String FAQ_ACTION_TITLE = "§8§lFAQ Aktionen";
|
||||||
|
|
||||||
|
/** FAQ-Einträge pro Seite (Zeilen 0-4, Slots 0-44). */
|
||||||
|
private static final int PAGE_SIZE = 45;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Texture-URL für die Custom-Skull-Items.
|
||||||
|
* http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e
|
||||||
|
*/
|
||||||
|
private static final String FAQ_SKIN_URL =
|
||||||
|
"http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e";
|
||||||
|
|
||||||
|
private final TicketPlugin plugin;
|
||||||
|
|
||||||
|
// ─────────────────────────── State ─────────────────────────────────────
|
||||||
|
|
||||||
|
/** Slot-Map für Spieler-FAQ-GUI: UUID → (Slot → FaqEntry) */
|
||||||
|
private final Map<UUID, Map<Integer, FaqEntry>> slotMap = new HashMap<>();
|
||||||
|
/** Aktuelle Seite pro Spieler in der FAQ-GUI */
|
||||||
|
private final Map<UUID, Integer> faqPage = new HashMap<>();
|
||||||
|
/** Ob der Spieler sich in der Admin-Ansicht befindet */
|
||||||
|
private final Set<UUID> adminView = new HashSet<>();
|
||||||
|
/** Aktuell ausgewähltes FAQ für die Aktions-GUI */
|
||||||
|
private final Map<UUID, FaqEntry> actionEntry = new HashMap<>();
|
||||||
|
|
||||||
|
// ─── Chat-Input-States ──────────────────────────────────────────────
|
||||||
|
/** Wartet auf Frage-Eingabe: null = neu, "edit:<id>" = bearbeiten */
|
||||||
|
private final Map<UUID, String> awaitingQuestion = new HashMap<>();
|
||||||
|
/** Wartet auf Antwort-Eingabe: key = Frage-Text (|id) bei Edit */
|
||||||
|
private final Map<UUID, String> awaitingAnswer = new HashMap<>();
|
||||||
|
|
||||||
|
// ─────────────────────────── Konstruktor ───────────────────────────────
|
||||||
|
|
||||||
|
public FaqGUI(TicketPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// PUBLIC OPEN-METHODEN
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Öffnet die Spieler-FAQ-GUI (Seite 0). */
|
||||||
|
public void openFaqGUI(Player player) {
|
||||||
|
openFaqGUI(player, faqPage.getOrDefault(player.getUniqueId(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Öffnet die Spieler-FAQ-GUI auf der angegebenen Seite. */
|
||||||
|
public void openFaqGUI(Player player, int page) {
|
||||||
|
boolean isAdmin = player.hasPermission("ticket.admin");
|
||||||
|
String title = isAdmin ? FAQ_ADMIN_TITLE : FAQ_GUI_TITLE;
|
||||||
|
|
||||||
|
List<FaqEntry> all = plugin.getFaqManager().getAll();
|
||||||
|
int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / PAGE_SIZE));
|
||||||
|
page = Math.max(0, Math.min(page, totalPages - 1));
|
||||||
|
faqPage.put(player.getUniqueId(), page);
|
||||||
|
|
||||||
|
Inventory inv = Bukkit.createInventory(null, 54, title);
|
||||||
|
Map<Integer, FaqEntry> sm = new HashMap<>();
|
||||||
|
|
||||||
|
int start = page * PAGE_SIZE;
|
||||||
|
for (int i = 0; i < PAGE_SIZE && (start + i) < all.size(); i++) {
|
||||||
|
FaqEntry entry = all.get(start + i);
|
||||||
|
inv.setItem(i, buildFaqSkull(entry, isAdmin));
|
||||||
|
sm.put(i, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
slotMap.put(player.getUniqueId(), sm);
|
||||||
|
|
||||||
|
if (isAdmin) adminView.add(player.getUniqueId());
|
||||||
|
else adminView.remove(player.getUniqueId());
|
||||||
|
|
||||||
|
// ── Navigationsleiste ──────────────────────────────────────────────
|
||||||
|
fillNavBar(inv, page, totalPages, isAdmin, all.isEmpty());
|
||||||
|
player.openInventory(inv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// CLICK-EVENTS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onInventoryClick(InventoryClickEvent event) {
|
||||||
|
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||||
|
String title = event.getView().getTitle();
|
||||||
|
|
||||||
|
if (!title.equals(FAQ_GUI_TITLE) && !title.equals(FAQ_ADMIN_TITLE)
|
||||||
|
&& !title.equals(FAQ_ACTION_TITLE)) return;
|
||||||
|
|
||||||
|
event.setCancelled(true);
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
if (slot < 0) return;
|
||||||
|
|
||||||
|
// ── Aktions-GUI ────────────────────────────────────────────────────
|
||||||
|
if (title.equals(FAQ_ACTION_TITLE)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FAQ-Listen-GUI ─────────────────────────────────────────────────
|
||||||
|
boolean isAdmin = adminView.contains(player.getUniqueId());
|
||||||
|
int curPage = faqPage.getOrDefault(player.getUniqueId(), 0);
|
||||||
|
|
||||||
|
// Navigationsslots
|
||||||
|
if (slot == 45) { openFaqGUI(player, curPage - 1); return; }
|
||||||
|
if (slot == 53) { openFaqGUI(player, curPage + 1); return; }
|
||||||
|
|
||||||
|
// Admin-spezifisch: Neues FAQ hinzufügen
|
||||||
|
if (slot == 50 && isAdmin) {
|
||||||
|
startAddFlow(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAQ-Item angeklickt
|
||||||
|
if (slot < PAGE_SIZE) {
|
||||||
|
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 {
|
||||||
|
// Spieler: Antwort im Chat ausgeben
|
||||||
|
player.closeInventory();
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
player.sendMessage(plugin.color("&6&lFAQ #" + entry.getId() + ": &e" + entry.getQuestion()));
|
||||||
|
player.sendMessage(plugin.color("&f" + entry.getAnswer()));
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Admin-Aktions-GUI ─────────────────────────
|
||||||
|
|
||||||
|
private void openActionGUI(Player player, FaqEntry entry) {
|
||||||
|
actionEntry.put(player.getUniqueId(), entry);
|
||||||
|
Inventory inv = Bukkit.createInventory(null, 27, FAQ_ACTION_TITLE);
|
||||||
|
|
||||||
|
// Slot 4: FAQ-Info
|
||||||
|
inv.setItem(4, buildFaqSkull(entry, false));
|
||||||
|
|
||||||
|
// Slot 10: Bearbeiten
|
||||||
|
inv.setItem(10, buildItem(Material.WRITABLE_BOOK, "§a§lFAQ bearbeiten",
|
||||||
|
List.of("§7Ändere Frage und Antwort", "§7dieses FAQ-Eintrags.")));
|
||||||
|
|
||||||
|
// Slot 12: Löschen
|
||||||
|
inv.setItem(12, buildItem(Material.BARRIER, "§c§lFAQ löschen",
|
||||||
|
List.of("§7Löscht diesen FAQ-Eintrag.", "§c§lACHTUNG: §cNicht rückgängig zu machen!")));
|
||||||
|
|
||||||
|
// Slot 16: Zurück
|
||||||
|
inv.setItem(16, buildItem(Material.ARROW, "§7§lZurück",
|
||||||
|
List.of("§7Zurück zur FAQ-Übersicht.")));
|
||||||
|
|
||||||
|
fillGlass(inv);
|
||||||
|
player.openInventory(inv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Chat-Flow: Hinzufügen ─────────────────────
|
||||||
|
|
||||||
|
private void startAddFlow(Player player) {
|
||||||
|
player.closeInventory();
|
||||||
|
awaitingQuestion.put(player.getUniqueId(), "new");
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
player.sendMessage(plugin.color("&6&lNeues FAQ erstellen"));
|
||||||
|
player.sendMessage(plugin.color("&7Gib die &eFrage &7ein (oder &ccancel&7):"));
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Chat-Flow: Bearbeiten ─────────────────────
|
||||||
|
|
||||||
|
private void startEditFlow(Player player, FaqEntry entry) {
|
||||||
|
player.closeInventory();
|
||||||
|
awaitingQuestion.put(player.getUniqueId(), "edit:" + entry.getId());
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
player.sendMessage(plugin.color("&6&lFAQ #" + entry.getId() + " bearbeiten"));
|
||||||
|
player.sendMessage(plugin.color("&7Aktuelle Frage: &e" + entry.getQuestion()));
|
||||||
|
player.sendMessage(plugin.color("&7Gib die neue &eFrage &7ein (oder &ccancel&7):"));
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Löschen ───────────────────────────────────
|
||||||
|
|
||||||
|
private void deleteFaq(Player player, FaqEntry entry) {
|
||||||
|
player.closeInventory();
|
||||||
|
boolean success = plugin.getFaqManager().delete(entry.getId());
|
||||||
|
if (success) {
|
||||||
|
player.sendMessage(plugin.color("&aFAQ #" + entry.getId() + " &a(§e" + entry.getQuestion() + "§a) wurde gelöscht."));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.color("&cFehler: FAQ #" + entry.getId() + " konnte nicht gelöscht werden."));
|
||||||
|
}
|
||||||
|
openFaqGUI(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// CHAT-EVENTS (Frage & Antwort Eingabe)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@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.color("&cAbgebrochen."));
|
||||||
|
openFaqGUI(player);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frage gespeichert → jetzt auf Antwort warten
|
||||||
|
// Encoded state: "new" oder "edit:<id>"
|
||||||
|
awaitingAnswer.put(uuid, state + "§§" + input); // "§§" as internal separator
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
player.sendMessage(plugin.color("&7Frage gesetzt: &e" + input));
|
||||||
|
player.sendMessage(plugin.color("&7Gib jetzt die &eAntwort &7ein (oder &ccancel&7):"));
|
||||||
|
});
|
||||||
|
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.color("&cAbgebrochen."));
|
||||||
|
openFaqGUI(player);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateAndQuestion = "<state>§§<question>"
|
||||||
|
int sep = stateAndQuestion.indexOf("§§");
|
||||||
|
String state = stateAndQuestion.substring(0, sep);
|
||||||
|
String question = stateAndQuestion.substring(sep + 2);
|
||||||
|
String answer = input;
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (state.equals("new")) {
|
||||||
|
// Hinzufügen
|
||||||
|
FaqEntry created = plugin.getFaqManager().add(question, answer);
|
||||||
|
player.sendMessage(plugin.color("&aFAQ #" + created.getId() + " wurde erfolgreich erstellt!"));
|
||||||
|
} else {
|
||||||
|
// Bearbeiten: state = "edit:<id>"
|
||||||
|
int id;
|
||||||
|
try {
|
||||||
|
id = Integer.parseInt(state.substring(5)); // "edit:".length() = 5
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
player.sendMessage(plugin.color("&cInterner Fehler beim Bearbeiten des FAQs."));
|
||||||
|
openFaqGUI(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean ok = plugin.getFaqManager().edit(id, question, answer);
|
||||||
|
if (ok) player.sendMessage(plugin.color("&aFAQ #" + id + " wurde erfolgreich aktualisiert!"));
|
||||||
|
else player.sendMessage(plugin.color("&cFAQ #" + id + " wurde nicht gefunden."));
|
||||||
|
}
|
||||||
|
openFaqGUI(player);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// ITEM-BUILDER
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut ein Custom-Skull-Item für einen FAQ-Eintrag.
|
||||||
|
* Nutzt die Textur: da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e
|
||||||
|
*
|
||||||
|
* Bei einem Fehler mit der Textur wird auf BOOK zurückgefallen.
|
||||||
|
*/
|
||||||
|
private ItemStack buildFaqSkull(FaqEntry entry, boolean adminHint) {
|
||||||
|
ItemStack skull;
|
||||||
|
|
||||||
|
try {
|
||||||
|
skull = new ItemStack(Material.PLAYER_HEAD);
|
||||||
|
SkullMeta meta = (SkullMeta) skull.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(FAQ_SKIN_URL));
|
||||||
|
profile.setTextures(textures);
|
||||||
|
meta.setOwnerProfile(profile);
|
||||||
|
meta.setDisplayName("§e§l" + entry.getQuestion());
|
||||||
|
meta.setLore(buildFaqLore(entry, adminHint));
|
||||||
|
skull.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Fallback auf BOOK wenn Textur nicht gesetzt werden kann
|
||||||
|
skull = new ItemStack(Material.BOOK);
|
||||||
|
ItemMeta meta = skull.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setDisplayName("§e§l" + entry.getQuestion());
|
||||||
|
meta.setLore(buildFaqLore(entry, adminHint));
|
||||||
|
skull.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return skull;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Erstellt die Lore-Zeilen für ein FAQ-Item (Antwort aufgeteilt in 40er-Zeilen). */
|
||||||
|
private List<String> buildFaqLore(FaqEntry entry, boolean adminHint) {
|
||||||
|
List<String> lore = new ArrayList<>();
|
||||||
|
lore.add("§8§m ");
|
||||||
|
lore.add("§7FAQ #" + entry.getId());
|
||||||
|
lore.add("§8§m ");
|
||||||
|
|
||||||
|
// Antwort in 40-Zeichen-Abschnitte aufteilen
|
||||||
|
String answer = entry.getAnswer();
|
||||||
|
int chunkSize = 40;
|
||||||
|
for (int i = 0; i < answer.length(); i += chunkSize) {
|
||||||
|
int end = Math.min(i + chunkSize, answer.length());
|
||||||
|
// Wortgrenzen bevorzugen
|
||||||
|
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; // Schleifeninkrement korrigieren
|
||||||
|
}
|
||||||
|
|
||||||
|
lore.add("§8§m ");
|
||||||
|
if (adminHint) {
|
||||||
|
lore.add("§e» Klicken zum Bearbeiten / Löschen");
|
||||||
|
} else {
|
||||||
|
lore.add("§e» Klicken für mehr Details im Chat");
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
ItemStack glass = makeGlass();
|
||||||
|
for (int i = 45; i < 54; i++) inv.setItem(i, glass);
|
||||||
|
|
||||||
|
if (page > 0) {
|
||||||
|
inv.setItem(45, buildItem(Material.ARROW, "§7§l◄ Zurück",
|
||||||
|
List.of("§7Seite " + page + " von " + totalPages)));
|
||||||
|
}
|
||||||
|
if (page < totalPages - 1) {
|
||||||
|
inv.setItem(53, buildItem(Material.ARROW, "§7§lWeiter ►",
|
||||||
|
List.of("§7Seite " + (page + 2) + " von " + totalPages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
inv.setItem(49, buildItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages,
|
||||||
|
List.of("§7Gesamt: " + (isEmpty ? 0 : Math.min(PAGE_SIZE, totalPages * PAGE_SIZE)) + " FAQ(s)")));
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
inv.setItem(50, buildItem(Material.LIME_WOOL, "§a§lNeues FAQ hinzufügen",
|
||||||
|
List.of("§7Fügt einen neuen FAQ-Eintrag hinzu.", "§7Du wirst nach Frage und Antwort gefragt.")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,13 @@ package de.ticketsystem.listeners;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import de.ticketsystem.TicketPlugin;
|
import de.ticketsystem.TicketPlugin;
|
||||||
|
import de.ticketsystem.database.DatabaseManager;
|
||||||
import de.ticketsystem.model.Ticket;
|
import de.ticketsystem.model.Ticket;
|
||||||
import de.ticketsystem.model.TicketStatus;
|
import de.ticketsystem.model.TicketStatus;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
@@ -34,23 +37,80 @@ public class PlayerJoinListener implements Listener {
|
|||||||
.replace("{count}", String.valueOf(count));
|
.replace("{count}", String.valueOf(count));
|
||||||
player.sendMessage(msg);
|
player.sendMessage(msg);
|
||||||
player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht."));
|
player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht."));
|
||||||
}, 40L); // 2 Sekunden Verzögerung
|
}, 40L);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Spieler: über geschlossene Tickets mit Kommentar informieren ──
|
// ── BungeeCord: ausstehenden Teleport-Auftrag prüfen ─────────────
|
||||||
// Nur wenn der Ersteller noch nicht live benachrichtigt wurde
|
// 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, () -> {
|
||||||
|
if (!player.isOnline()) return;
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
DatabaseManager.PendingTeleport pt =
|
||||||
|
plugin.getDatabaseManager().consumePendingTeleport(player.getUniqueId());
|
||||||
|
if (pt == null) return;
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (!player.isOnline()) return;
|
||||||
|
World world = Bukkit.getWorld(pt.world());
|
||||||
|
if (world == null) {
|
||||||
|
player.sendMessage(plugin.color(
|
||||||
|
"&cTeleport-Zielwelt &e" + pt.world() + " &cnicht gefunden!"));
|
||||||
|
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()) + ")"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 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, () -> {
|
||||||
|
List<String> pending = plugin.getDatabaseManager().getPendingNotifications(player.getUniqueId());
|
||||||
|
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):"));
|
||||||
|
for (String msg : pending) {
|
||||||
|
player.sendMessage(plugin.color(msg));
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
});
|
||||||
|
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
|
||||||
|
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.
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
List<Ticket> closed = plugin.getDatabaseManager()
|
List<Ticket> closed = plugin.getDatabaseManager()
|
||||||
.getTicketsByStatus(TicketStatus.CLOSED);
|
.getTicketsByStatus(TicketStatus.CLOSED);
|
||||||
|
|
||||||
for (Ticket t : closed) {
|
for (Ticket t : closed) {
|
||||||
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
|
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
|
||||||
if (t.getCloseComment() == null || t.getCloseComment().isEmpty()) continue;
|
// DB-Feld prüfen – funktioniert serverübergreifend
|
||||||
|
if (t.isCloseNotified()) continue;
|
||||||
// Nicht erneut senden, wenn bereits live benachrichtigt (In-Memory-Set)
|
|
||||||
if (plugin.getTicketManager().wasClosedNotificationSent(t.getId())) continue;
|
|
||||||
|
|
||||||
Bukkit.getScheduler().runTask(plugin, () ->
|
Bukkit.getScheduler().runTask(plugin, () ->
|
||||||
plugin.getTicketManager().notifyCreatorClosed(t));
|
plugin.getTicketManager().notifyCreatorClosed(t));
|
||||||
@@ -73,7 +133,7 @@ public class PlayerJoinListener implements Listener {
|
|||||||
player.sendMessage(bar);
|
player.sendMessage(bar);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 20L); // 1 Sekunde
|
}, 20L);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
177
src/main/java/de/ticketsystem/manager/CategoryManager.java
Normal file
177
src/main/java/de/ticketsystem/manager/CategoryManager.java
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package de.ticketsystem.manager;
|
||||||
|
|
||||||
|
import de.ticketsystem.TicketPlugin;
|
||||||
|
import de.ticketsystem.model.ConfigCategory;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and manages ticket categories defined in config.yml under the "categories" section.
|
||||||
|
*
|
||||||
|
* Example config.yml layout:
|
||||||
|
*
|
||||||
|
* categories:
|
||||||
|
* general:
|
||||||
|
* name: "Allgemein"
|
||||||
|
* color: "&7"
|
||||||
|
* material: "PAPER"
|
||||||
|
* aliases:
|
||||||
|
* - "allgemein"
|
||||||
|
* - "general"
|
||||||
|
* bug:
|
||||||
|
* name: "Bug"
|
||||||
|
* color: "&c"
|
||||||
|
* material: "REDSTONE"
|
||||||
|
* aliases:
|
||||||
|
* - "bug"
|
||||||
|
* - "fehler"
|
||||||
|
*/
|
||||||
|
public class CategoryManager {
|
||||||
|
|
||||||
|
private final TicketPlugin plugin;
|
||||||
|
|
||||||
|
/** Ordered map: key → ConfigCategory */
|
||||||
|
private final Map<String, ConfigCategory> categories = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/** Alias → key mapping for fast resolve() lookups */
|
||||||
|
private final Map<String, String> aliasMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
public CategoryManager(TicketPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Loading ───────────────────────────────────
|
||||||
|
|
||||||
|
private void load() {
|
||||||
|
categories.clear();
|
||||||
|
aliasMap.clear();
|
||||||
|
|
||||||
|
ConfigurationSection section = plugin.getConfig().getConfigurationSection("categories");
|
||||||
|
|
||||||
|
if (section == null || section.getKeys(false).isEmpty()) {
|
||||||
|
// Fallback: create built-in defaults so the plugin always works
|
||||||
|
loadDefaults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String key : section.getKeys(false)) {
|
||||||
|
ConfigurationSection cat = section.getConfigurationSection(key);
|
||||||
|
if (cat == null) continue;
|
||||||
|
|
||||||
|
String name = cat.getString("name", capitalize(key));
|
||||||
|
String color = cat.getString("color", "&7");
|
||||||
|
String matStr = cat.getString("material", "PAPER").toUpperCase();
|
||||||
|
Material material;
|
||||||
|
try {
|
||||||
|
material = Material.valueOf(matStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
plugin.getLogger().warning("[CategoryManager] Unbekanntes Material '" + matStr
|
||||||
|
+ "' für Kategorie '" + key + "'. Fallback: PAPER");
|
||||||
|
material = Material.PAPER;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigCategory category = new ConfigCategory(key, name, color, material);
|
||||||
|
categories.put(key.toLowerCase(), category);
|
||||||
|
|
||||||
|
// Register key itself as alias
|
||||||
|
aliasMap.put(key.toLowerCase(), key.toLowerCase());
|
||||||
|
|
||||||
|
// Register additional aliases
|
||||||
|
List<String> aliases = cat.getStringList("aliases");
|
||||||
|
for (String alias : aliases) {
|
||||||
|
aliasMap.put(alias.toLowerCase(), key.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categories.isEmpty()) {
|
||||||
|
plugin.getLogger().warning("[CategoryManager] Keine gültigen Kategorien in der config.yml gefunden. Lade Standardkategorien.");
|
||||||
|
loadDefaults();
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().info("[CategoryManager] " + categories.size() + " Kategorie(n) geladen: " + String.join(", ", categories.keySet()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Built-in fallback categories — mirrors the old TicketCategory enum */
|
||||||
|
private void loadDefaults() {
|
||||||
|
addDefault("general", "Allgemein", "&7", Material.PAPER, "allgemein", "general");
|
||||||
|
addDefault("bug", "Bug", "&c", Material.REDSTONE, "bug", "fehler");
|
||||||
|
addDefault("question", "Frage", "&e", Material.BOOK, "frage", "question");
|
||||||
|
addDefault("complaint", "Beschwerde", "&6", Material.WRITABLE_BOOK, "beschwerde", "complaint");
|
||||||
|
addDefault("other", "Sonstiges", "&8", Material.FEATHER, "sonstiges", "other");
|
||||||
|
plugin.getLogger().info("[CategoryManager] Standard-Kategorien geladen (5).");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDefault(String key, String name, String color, Material mat, String... aliases) {
|
||||||
|
ConfigCategory cat = new ConfigCategory(key, name, color, mat);
|
||||||
|
categories.put(key, cat);
|
||||||
|
aliasMap.put(key, key);
|
||||||
|
for (String alias : aliases) aliasMap.put(alias.toLowerCase(), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Public API ────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all loaded categories in config order.
|
||||||
|
*/
|
||||||
|
public List<ConfigCategory> getAll() {
|
||||||
|
return Collections.unmodifiableList(new ArrayList<>(categories.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first category (default), or a hard-coded fallback if empty.
|
||||||
|
*/
|
||||||
|
public ConfigCategory getDefault() {
|
||||||
|
if (categories.isEmpty()) return new ConfigCategory("general", "Allgemein", "&7", Material.PAPER);
|
||||||
|
return categories.values().iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up a category by exact key (case-insensitive).
|
||||||
|
* Returns null if not found.
|
||||||
|
*/
|
||||||
|
public ConfigCategory fromKey(String key) {
|
||||||
|
if (key == null) return getDefault();
|
||||||
|
ConfigCategory cat = categories.get(key.toLowerCase());
|
||||||
|
return cat != null ? cat : getDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a user-supplied string (key or alias) to a ConfigCategory.
|
||||||
|
* Returns null if no match is found (so callers can show an error).
|
||||||
|
*/
|
||||||
|
public ConfigCategory resolve(String input) {
|
||||||
|
if (input == null) return null;
|
||||||
|
String key = aliasMap.get(input.toLowerCase());
|
||||||
|
return key != null ? categories.get(key) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a human-readable comma-separated list of all category keys,
|
||||||
|
* e.g. "general, bug, question, complaint, other"
|
||||||
|
*/
|
||||||
|
public String getAvailableNames() {
|
||||||
|
return String.join(", ", categories.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads categories from the (already reloaded) config.
|
||||||
|
* Call this after plugin.reloadConfig().
|
||||||
|
*/
|
||||||
|
public void reload() {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Helpers ───────────────────────────────────
|
||||||
|
|
||||||
|
private static String capitalize(String s) {
|
||||||
|
if (s == null || s.isEmpty()) return s;
|
||||||
|
return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
entries.add(new FaqEntry(1, "Wie erstelle ich ein Ticket?",
|
||||||
|
"Nutze den Befehl /ticket create [Kategorie] [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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
package de.ticketsystem.manager;
|
package de.ticketsystem.manager;
|
||||||
|
|
||||||
import de.ticketsystem.TicketPlugin;
|
import de.ticketsystem.TicketPlugin;
|
||||||
|
import de.ticketsystem.model.ConfigCategory;
|
||||||
import de.ticketsystem.model.Ticket;
|
import de.ticketsystem.model.Ticket;
|
||||||
import de.ticketsystem.model.TicketStatus;
|
import de.ticketsystem.model.TicketStatus;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class TicketManager {
|
public class TicketManager {
|
||||||
@@ -19,9 +18,6 @@ public class TicketManager {
|
|||||||
/** Cooldown Map: UUID → Zeitstempel letztes Ticket */
|
/** Cooldown Map: UUID → Zeitstempel letztes Ticket */
|
||||||
private final Map<UUID, Long> cooldowns = new HashMap<>();
|
private final Map<UUID, Long> cooldowns = new HashMap<>();
|
||||||
|
|
||||||
/** Ticket-IDs für die der Ersteller bereits über Schließung informiert wurde */
|
|
||||||
private final Set<Integer> notifiedClosedTickets = new HashSet<>();
|
|
||||||
|
|
||||||
public TicketManager(TicketPlugin plugin) {
|
public TicketManager(TicketPlugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
@@ -40,139 +36,308 @@ public class TicketManager {
|
|||||||
return Math.max(0, (cooldownMillis - elapsed) / 1000);
|
return Math.max(0, (cooldownMillis - elapsed) / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCooldown(UUID uuid) {
|
public void setCooldown(UUID uuid) { cooldowns.put(uuid, System.currentTimeMillis()); }
|
||||||
cooldowns.put(uuid, System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────── Benachrichtigungen ────────────────────────
|
// ─────────────────────────── Benachrichtigungen ────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benachrichtigt alle Online-Supporter/Admins über ein neues Ticket
|
* Benachrichtigt alle Supporter/Admins über ein neues Ticket – auch auf anderen Servern.
|
||||||
* und sendet optional eine Discord-Webhook-Nachricht.
|
*
|
||||||
|
* Lokal online Spieler werden direkt angesprochen.
|
||||||
|
* Über BungeeCord werden alle anderen Server im Netzwerk ebenfalls benachrichtigt.
|
||||||
|
* Optional sendet der Discord-Webhook eine Nachricht.
|
||||||
*/
|
*/
|
||||||
public void notifyTeam(Ticket ticket) {
|
public void notifyTeam(Ticket ticket) {
|
||||||
// Sicherheitschecks für null-Werte
|
|
||||||
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
||||||
String message = ticket.getMessage() != null ? ticket.getMessage() : "";
|
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]";
|
||||||
|
}
|
||||||
|
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||||
|
priorityInfo = " §7Priorität: §r" + ticket.getPriority().getColored();
|
||||||
|
}
|
||||||
|
|
||||||
|
// BungeeCord: Server-Herkunft anzeigen wenn BungeeCord aktiviert
|
||||||
|
String serverInfo = "";
|
||||||
|
if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||||
|
serverInfo = " §7Server: §b" + ticket.getServerName();
|
||||||
|
}
|
||||||
|
|
||||||
String msg = plugin.formatMessage("messages.new-ticket-notify")
|
String msg = plugin.formatMessage("messages.new-ticket-notify")
|
||||||
.replace("{player}", creatorName)
|
.replace("{player}", creatorName)
|
||||||
.replace("{message}", message)
|
.replace("{message}", message)
|
||||||
.replace("{id}", String.valueOf(ticket.getId()));
|
.replace("{id}", String.valueOf(ticket.getId()))
|
||||||
|
+ categoryInfo + priorityInfo + serverInfo;
|
||||||
|
|
||||||
|
String guiHint = plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen.");
|
||||||
|
|
||||||
|
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()) {
|
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||||
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
|
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
|
||||||
p.sendMessage(msg);
|
p.sendMessage(msg);
|
||||||
p.sendMessage(plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen."));
|
p.sendMessage(guiHint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discord-Webhook (asynchron)
|
|
||||||
plugin.getDiscordWebhook().sendNewTicket(ticket);
|
plugin.getDiscordWebhook().sendNewTicket(ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde.
|
* Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde.
|
||||||
* --- FIX PROBLEMK 1: NIE "UNBEKANNT" ---
|
* Setzt claimer_notified = true und persistiert es.
|
||||||
|
*
|
||||||
|
* BungeeCord: Zustellung auch wenn der Spieler auf einem anderen Server ist.
|
||||||
*/
|
*/
|
||||||
public void notifyCreatorClaimed(Ticket ticket) {
|
public void notifyCreatorClaimed(Ticket ticket) {
|
||||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
String claimerName = resolveClaimerName(ticket);
|
||||||
if (creator != null && creator.isOnline()) {
|
|
||||||
|
|
||||||
// 1. Versuch: Name aus dem Ticket-Objekt
|
|
||||||
String claimerName = ticket.getClaimerName();
|
|
||||||
|
|
||||||
// 2. Versuch: Wenn Name fehlt, aber UUID vorhanden -> Namen über Bukkit holen
|
|
||||||
if (claimerName == null && ticket.getClaimerUUID() != null) {
|
|
||||||
claimerName = Bukkit.getOfflinePlayer(ticket.getClaimerUUID()).getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Fallback: Falls immer noch kein Name da ist, nimm "Support" (nie "Unbekannt")
|
|
||||||
if (claimerName == null) claimerName = "Support";
|
|
||||||
|
|
||||||
String msg = plugin.formatMessage("messages.ticket-claimed-notify")
|
String msg = plugin.formatMessage("messages.ticket-claimed-notify")
|
||||||
.replace("{id}", String.valueOf(ticket.getId()))
|
.replace("{id}", String.valueOf(ticket.getId()))
|
||||||
.replace("{claimer}", claimerName);
|
.replace("{claimer}", claimerName);
|
||||||
creator.sendMessage(msg);
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public void notifyClaimedWhileOffline(Player player) {
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.getDatabaseManager().markClaimerNotified(t.getId());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde.
|
* Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde.
|
||||||
|
* BungeeCord: Cross-Server-Zustellung.
|
||||||
*/
|
*/
|
||||||
public void notifyCreatorForwarded(Ticket ticket) {
|
public void notifyCreatorForwarded(Ticket ticket) {
|
||||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
|
||||||
if (creator != null && creator.isOnline()) {
|
|
||||||
String forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter";
|
String forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter";
|
||||||
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
|
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
|
||||||
.replace("{id}", String.valueOf(ticket.getId()))
|
.replace("{id}", String.valueOf(ticket.getId()))
|
||||||
.replace("{supporter}", forwardedTo);
|
.replace("{supporter}", forwardedTo);
|
||||||
creator.sendMessage(msg);
|
|
||||||
}
|
deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
|
||||||
|
|
||||||
|
// Auch bei Weiterleitung notified setzen
|
||||||
|
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sendet dem weitergeleiteten Supporter eine Benachrichtigung
|
* Sendet dem weitergeleiteten Supporter eine Benachrichtigung.
|
||||||
* und informiert optional Discord.
|
* BungeeCord: Zustellung auch wenn der Supporter auf einem anderen Server ist.
|
||||||
*/
|
*/
|
||||||
public void notifyForwardedTo(Ticket ticket, String fromName) {
|
public void notifyForwardedTo(Ticket ticket, String fromName) {
|
||||||
Player target = Bukkit.getPlayer(ticket.getForwardedToUUID());
|
if (ticket.getForwardedToUUID() == null) return;
|
||||||
if (target != null && target.isOnline()) {
|
|
||||||
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
|
||||||
|
|
||||||
|
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
||||||
String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
|
String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
|
||||||
.replace("{player}", creatorName)
|
.replace("{player}", creatorName)
|
||||||
.replace("{id}", String.valueOf(ticket.getId()));
|
.replace("{id}", String.valueOf(ticket.getId()));
|
||||||
target.sendMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discord
|
deliverToPlayer(ticket.getForwardedToUUID(), ticket.getForwardedToName(), msg);
|
||||||
|
|
||||||
plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName);
|
plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde,
|
|
||||||
* und informiert optional Discord.
|
|
||||||
*/
|
|
||||||
public void notifyCreatorClosed(Ticket ticket) {
|
|
||||||
notifyCreatorClosed(ticket, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde.
|
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde.
|
||||||
|
* BungeeCord: Cross-Server-Zustellung + Fallback in Pending-DB.
|
||||||
*/
|
*/
|
||||||
public void notifyCreatorClosed(Ticket ticket, String closerName) {
|
public void notifyCreatorClosed(Ticket ticket) { notifyCreatorClosed(ticket, null); }
|
||||||
notifiedClosedTickets.add(ticket.getId());
|
|
||||||
|
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()));
|
||||||
|
|
||||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
|
||||||
if (creator != null && creator.isOnline()) {
|
|
||||||
String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
|
String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
|
||||||
? ticket.getCloseComment() : "";
|
? ticket.getCloseComment() : "";
|
||||||
|
|
||||||
|
// Hauptnachricht
|
||||||
String msg = plugin.formatMessage("messages.ticket-closed-notify")
|
String msg = plugin.formatMessage("messages.ticket-closed-notify")
|
||||||
.replace("{id}", String.valueOf(ticket.getId()))
|
.replace("{id}", String.valueOf(ticket.getId()))
|
||||||
.replace("{comment}", comment);
|
.replace("{comment}", comment);
|
||||||
|
|
||||||
|
// Bewertungsaufforderung
|
||||||
|
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 ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
creator.sendMessage(msg);
|
||||||
|
if (!comment.isEmpty())
|
||||||
if (!comment.isEmpty()) {
|
|
||||||
creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment));
|
creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + 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);
|
||||||
|
if (!comment.isEmpty())
|
||||||
|
plugin.getBungeeMessenger().sendMessageToPlayer(
|
||||||
|
ticket.getCreatorUUID(), ticket.getCreatorName(),
|
||||||
|
plugin.color("&7Kommentar des Supports: &f" + comment));
|
||||||
|
if (ratingMsg != null)
|
||||||
|
plugin.getBungeeMessenger().sendMessageToPlayer(
|
||||||
|
ticket.getCreatorUUID(), ticket.getCreatorName(), ratingMsg);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// ─ Standalone, Spieler offline: in Pending-DB speichern ──────
|
||||||
|
savePendingClosedNotification(ticket, comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discord
|
|
||||||
String closer = closerName != null ? closerName : "Unbekannt";
|
String closer = closerName != null ? closerName : "Unbekannt";
|
||||||
plugin.getDiscordWebhook().sendTicketClosed(ticket, closer);
|
plugin.getDiscordWebhook().sendTicketClosed(ticket, closer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft ob der Ersteller für dieses Ticket bereits über die Schließung informiert wurde.
|
* 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.
|
||||||
*/
|
*/
|
||||||
public boolean wasClosedNotificationSent(int ticketId) {
|
public boolean wasClosedNotificationSent(int ticketId) {
|
||||||
return notifiedClosedTickets.contains(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 ──────────────────
|
||||||
|
|
||||||
|
// ── 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.
|
||||||
|
// ────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zustellung einer Nachricht an einen 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)
|
||||||
|
*
|
||||||
|
* @param uuid UUID des Empfängers
|
||||||
|
* @param name Spielername (für BungeeCord-Lookup)
|
||||||
|
* @param message Bereits color-übersetzter Text
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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" : "");
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||||
|
plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), pendingMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
||||||
|
|
||||||
|
private String resolveClaimerName(Ticket ticket) {
|
||||||
|
if (ticket.getClaimerName() != null) return ticket.getClaimerName();
|
||||||
|
if (ticket.getClaimerUUID() != null) {
|
||||||
|
String name = Bukkit.getOfflinePlayer(ticket.getClaimerUUID()).getName();
|
||||||
|
if (name != null) return name;
|
||||||
|
}
|
||||||
|
return "Support";
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasReachedTicketLimit(UUID uuid) {
|
public boolean hasReachedTicketLimit(UUID uuid) {
|
||||||
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
|
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
|
||||||
if (max <= 0) return false;
|
if (max <= 0) return false;
|
||||||
@@ -183,16 +348,29 @@ public class TicketManager {
|
|||||||
player.sendMessage(plugin.color("&8&m "));
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
player.sendMessage(plugin.color("&6TicketSystem &7– Befehle"));
|
player.sendMessage(plugin.color("&6TicketSystem &7– Befehle"));
|
||||||
player.sendMessage(plugin.color("&8&m "));
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
player.sendMessage(plugin.color("&e/ticket create <Text> &7– Neues Ticket erstellen"));
|
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 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")) {
|
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 claim <ID> &7– Ticket annehmen"));
|
||||||
player.sendMessage(plugin.color("&e/ticket close <ID> [Kommentar] &7– Ticket schließen"));
|
player.sendMessage(plugin.color("&e/ticket close <ID> [Kommentar] &7– Ticket schließen"));
|
||||||
}
|
}
|
||||||
if (player.hasPermission("ticket.admin")) {
|
if (player.hasPermission("ticket.admin")) {
|
||||||
player.sendMessage(plugin.color("&e/ticket forward <ID> <Spieler> &7– Ticket weiterleiten"));
|
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 reload &7– Konfiguration neu laden"));
|
||||||
|
player.sendMessage(plugin.color("&e/ticket stats &7– Statistiken anzeigen"));
|
||||||
}
|
}
|
||||||
player.sendMessage(plugin.color("&8&m "));
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
53
src/main/java/de/ticketsystem/model/ConfigCategory.java
Normal file
53
src/main/java/de/ticketsystem/model/ConfigCategory.java
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package de.ticketsystem.model;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine aus der config.yml geladene Ticket-Kategorie.
|
||||||
|
* Ersetzt das hardcodierte TicketCategory-Enum vollständig.
|
||||||
|
*
|
||||||
|
* Konfigurationsbeispiel (config.yml):
|
||||||
|
*
|
||||||
|
* categories:
|
||||||
|
* bug:
|
||||||
|
* name: "Bug"
|
||||||
|
* color: "&c"
|
||||||
|
* material: "REDSTONE"
|
||||||
|
* aliases:
|
||||||
|
* - "bug"
|
||||||
|
* - "fehler"
|
||||||
|
*/
|
||||||
|
public class ConfigCategory {
|
||||||
|
|
||||||
|
/** Interner Schlüssel aus der Config (z.B. "bug", "general") – immer Kleinbuchstaben */
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
/** Anzeigename (z.B. "Bug", "Allgemein") */
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
/** Minecraft-Farbcode (z.B. "&c") */
|
||||||
|
private final String color;
|
||||||
|
|
||||||
|
/** GUI-Item-Material */
|
||||||
|
private final Material material;
|
||||||
|
|
||||||
|
public ConfigCategory(String key, String name, String color, Material material) {
|
||||||
|
this.key = key.toLowerCase();
|
||||||
|
this.name = name;
|
||||||
|
this.color = color;
|
||||||
|
this.material = material;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() { return key; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public String getColor() { return color; }
|
||||||
|
public Material getMaterial() { return material; }
|
||||||
|
|
||||||
|
/** Gibt den farbigen Anzeigenamen zurück, z.B. "§cBug" */
|
||||||
|
public String getColored() {
|
||||||
|
return org.bukkit.ChatColor.translateAlternateColorCodes('&', color + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() { return key; }
|
||||||
|
}
|
||||||
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 + "'}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package de.ticketsystem.model;
|
|||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
|
||||||
import org.bukkit.configuration.serialization.ConfigurationSerializable;
|
import org.bukkit.configuration.serialization.ConfigurationSerializable;
|
||||||
import org.bukkit.configuration.serialization.SerializableAs;
|
import org.bukkit.configuration.serialization.SerializableAs;
|
||||||
import org.bukkit.configuration.serialization.ConfigurationSerialization;
|
import org.bukkit.configuration.serialization.ConfigurationSerialization;
|
||||||
@@ -26,6 +25,13 @@ public class Ticket implements ConfigurationSerializable {
|
|||||||
private double x, y, z;
|
private double x, y, z;
|
||||||
private float yaw, pitch;
|
private float yaw, pitch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name des Servers auf dem das Ticket erstellt wurde (BungeeCord-Netzwerk).
|
||||||
|
* Entspricht dem Wert aus config.yml → server-name.
|
||||||
|
* Standardwert: "unknown"
|
||||||
|
*/
|
||||||
|
private String serverName = "unknown";
|
||||||
|
|
||||||
private TicketStatus status;
|
private TicketStatus status;
|
||||||
private UUID claimerUUID;
|
private UUID claimerUUID;
|
||||||
private String claimerName;
|
private String claimerName;
|
||||||
@@ -36,196 +42,175 @@ public class Ticket implements ConfigurationSerializable {
|
|||||||
private Timestamp closedAt;
|
private Timestamp closedAt;
|
||||||
private String closeComment;
|
private String closeComment;
|
||||||
|
|
||||||
// ─── NEU: Soft Delete Flag ───
|
|
||||||
private boolean playerDeleted = false;
|
private boolean playerDeleted = false;
|
||||||
|
|
||||||
|
/** Kategorie-Key aus config.yml, z.B. "bug", "general" */
|
||||||
|
private String categoryKey = "general";
|
||||||
|
|
||||||
|
private TicketPriority priority = TicketPriority.NORMAL;
|
||||||
|
|
||||||
|
/** null = nicht bewertet | "THUMBS_UP" | "THUMBS_DOWN" */
|
||||||
|
private String playerRating = null;
|
||||||
|
private boolean claimerNotified = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt an ob der Ersteller bereits über die Schließung informiert wurde.
|
||||||
|
* Wird in der DB gespeichert damit Server-Wechsel keine Duplikate erzeugen.
|
||||||
|
*/
|
||||||
|
private boolean closeNotified = false;
|
||||||
|
|
||||||
public Ticket() {}
|
public Ticket() {}
|
||||||
|
|
||||||
|
|
||||||
public Ticket(UUID creatorUUID, String creatorName, String message, Location location) {
|
public Ticket(UUID creatorUUID, String creatorName, String message, Location location) {
|
||||||
this.creatorUUID = creatorUUID;
|
this.creatorUUID = creatorUUID;
|
||||||
this.creatorName = creatorName;
|
this.creatorName = creatorName;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.worldName = location.getWorld().getName();
|
this.worldName = location.getWorld().getName();
|
||||||
this.x = location.getX();
|
this.x = location.getX(); this.y = location.getY(); this.z = location.getZ();
|
||||||
this.y = location.getY();
|
this.yaw = location.getYaw(); this.pitch = location.getPitch();
|
||||||
this.z = location.getZ();
|
|
||||||
this.yaw = location.getYaw();
|
|
||||||
this.pitch = location.getPitch();
|
|
||||||
this.status = TicketStatus.OPEN;
|
this.status = TicketStatus.OPEN;
|
||||||
this.createdAt = new Timestamp(System.currentTimeMillis());
|
this.createdAt = new Timestamp(System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NEU: Konstruktor zum Laden aus der YAML (Deserialisierung) ---
|
|
||||||
public Ticket(Map<String, Object> map) {
|
public Ticket(Map<String, Object> map) {
|
||||||
this.id = (int) map.get("id");
|
this.id = (int) map.get("id");
|
||||||
|
Object cObj = map.get("creatorUUID");
|
||||||
// UUIDs sicher aus String konvertieren
|
this.creatorUUID = cObj instanceof UUID ? (UUID) cObj : UUID.fromString((String) cObj);
|
||||||
Object creatorObj = map.get("creatorUUID");
|
|
||||||
this.creatorUUID = creatorObj instanceof UUID ? (UUID) creatorObj : UUID.fromString((String) creatorObj);
|
|
||||||
|
|
||||||
this.creatorName = (String) map.get("creatorName");
|
this.creatorName = (String) map.get("creatorName");
|
||||||
this.message = (String) map.get("message");
|
this.message = (String) map.get("message");
|
||||||
this.worldName = (String) map.get("world");
|
this.worldName = (String) map.get("world");
|
||||||
|
this.x = toDouble(map.get("x")); this.y = toDouble(map.get("y")); this.z = toDouble(map.get("z"));
|
||||||
// Koordinaten sicher parsen
|
this.yaw = toFloat(map.get("yaw")); this.pitch = toFloat(map.get("pitch"));
|
||||||
this.x = map.get("x") instanceof Double ? (Double) map.get("x") : ((Number) map.get("x")).doubleValue();
|
|
||||||
this.y = map.get("y") instanceof Double ? (Double) map.get("y") : ((Number) map.get("y")).doubleValue();
|
|
||||||
this.z = map.get("z") instanceof Double ? (Double) map.get("z") : ((Number) map.get("z")).doubleValue();
|
|
||||||
|
|
||||||
this.yaw = map.get("yaw") instanceof Float ? (Float) map.get("yaw") : ((Number) map.get("yaw")).floatValue();
|
|
||||||
this.pitch = map.get("pitch") instanceof Float ? (Float) map.get("pitch") : ((Number) map.get("pitch")).floatValue();
|
|
||||||
|
|
||||||
this.status = TicketStatus.valueOf((String) map.get("status"));
|
this.status = TicketStatus.valueOf((String) map.get("status"));
|
||||||
|
if (map.get("createdAt") != null) this.createdAt = new Timestamp(toLong(map.get("createdAt")));
|
||||||
// Timestamps aus Long (Millis) wieder zu Timestamp machen
|
if (map.get("claimedAt") != null) this.claimedAt = new Timestamp(toLong(map.get("claimedAt")));
|
||||||
if (map.get("createdAt") != null) {
|
if (map.get("closedAt") != null) this.closedAt = new Timestamp(toLong(map.get("closedAt")));
|
||||||
this.createdAt = new Timestamp(((Number) map.get("createdAt")).longValue());
|
|
||||||
}
|
|
||||||
if (map.get("claimedAt") != null) {
|
|
||||||
this.claimedAt = new Timestamp(((Number) map.get("claimedAt")).longValue());
|
|
||||||
}
|
|
||||||
if (map.get("closedAt") != null) {
|
|
||||||
this.closedAt = new Timestamp(((Number) map.get("closedAt")).longValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.closeComment = (String) map.get("closeComment");
|
this.closeComment = (String) map.get("closeComment");
|
||||||
|
|
||||||
// Optionale Felder
|
|
||||||
if (map.containsKey("claimerUUID") && map.get("claimerUUID") != null) {
|
if (map.containsKey("claimerUUID") && map.get("claimerUUID") != null) {
|
||||||
Object claimerObj = map.get("claimerUUID");
|
Object o = map.get("claimerUUID");
|
||||||
this.claimerUUID = claimerObj instanceof UUID ? (UUID) claimerObj : UUID.fromString((String) claimerObj);
|
this.claimerUUID = o instanceof UUID ? (UUID) o : UUID.fromString((String) o);
|
||||||
this.claimerName = (String) map.get("claimerName");
|
this.claimerName = (String) map.get("claimerName");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (map.containsKey("forwardedToUUID") && map.get("forwardedToUUID") != null) {
|
if (map.containsKey("forwardedToUUID") && map.get("forwardedToUUID") != null) {
|
||||||
Object fwdObj = map.get("forwardedToUUID");
|
Object o = map.get("forwardedToUUID");
|
||||||
this.forwardedToUUID = fwdObj instanceof UUID ? (UUID) fwdObj : UUID.fromString((String) fwdObj);
|
this.forwardedToUUID = o instanceof UUID ? (UUID) o : UUID.fromString((String) o);
|
||||||
this.forwardedToName = (String) map.get("forwardedToName");
|
this.forwardedToName = (String) map.get("forwardedToName");
|
||||||
}
|
}
|
||||||
|
if (map.containsKey("playerDeleted")) this.playerDeleted = (boolean) map.get("playerDeleted");
|
||||||
// ─── NEU: Laden des Soft Delete Flags ───
|
if (map.containsKey("category")) this.categoryKey = (String) map.get("category");
|
||||||
if (map.containsKey("playerDeleted")) {
|
if (map.containsKey("priority")) this.priority = TicketPriority.fromString((String) map.get("priority"));
|
||||||
this.playerDeleted = (boolean) map.get("playerDeleted");
|
if (map.containsKey("playerRating")) this.playerRating = (String) map.get("playerRating");
|
||||||
}
|
if (map.containsKey("claimerNotified")) this.claimerNotified = (boolean) map.get("claimerNotified");
|
||||||
|
// BungeeCord: Server-Name laden (Fallback: "unknown")
|
||||||
|
if (map.containsKey("serverName")) this.serverName = (String) map.get("serverName");
|
||||||
|
if (map.containsKey("closeNotified")) this.closeNotified = (boolean) map.get("closeNotified");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NEU: Methode zum Speichern in die YAML (Serialisierung) ---
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> serialize() {
|
public Map<String, Object> serialize() {
|
||||||
Map<String, Object> map = new HashMap<>();
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
|
||||||
map.put("id", id);
|
map.put("id", id);
|
||||||
// WICHTIG: UUID als String speichern, um !!java.util.UUID Tag zu vermeiden
|
// Null-Schutz: SnakeYAML wirft NPE wenn Key oder Value null sind
|
||||||
map.put("creatorUUID", creatorUUID.toString());
|
map.put("creatorUUID", creatorUUID != null ? creatorUUID.toString() : "");
|
||||||
map.put("creatorName", creatorName);
|
map.put("creatorName", creatorName != null ? creatorName : "");
|
||||||
map.put("message", message);
|
map.put("message", message != null ? message : "");
|
||||||
map.put("world", worldName);
|
map.put("world", worldName != null ? worldName : "world");
|
||||||
|
map.put("x", x); map.put("y", y); map.put("z", z);
|
||||||
map.put("x", x);
|
map.put("yaw", yaw); map.put("pitch", pitch);
|
||||||
map.put("y", y);
|
map.put("status", status != null ? status.name() : TicketStatus.OPEN.name());
|
||||||
map.put("z", z);
|
|
||||||
map.put("yaw", yaw);
|
|
||||||
map.put("pitch", pitch);
|
|
||||||
|
|
||||||
map.put("status", status.name());
|
|
||||||
|
|
||||||
// Timestamps als Long speichern
|
|
||||||
if (createdAt != null) map.put("createdAt", createdAt.getTime());
|
if (createdAt != null) map.put("createdAt", createdAt.getTime());
|
||||||
if (claimedAt != null) map.put("claimedAt", claimedAt.getTime());
|
if (claimedAt != null) map.put("claimedAt", claimedAt.getTime());
|
||||||
if (closedAt != null) map.put("closedAt", closedAt.getTime());
|
if (closedAt != null) map.put("closedAt", closedAt.getTime());
|
||||||
|
|
||||||
if (closeComment != null) map.put("closeComment", closeComment);
|
if (closeComment != null) map.put("closeComment", closeComment);
|
||||||
|
|
||||||
if (claimerUUID != null) {
|
if (claimerUUID != null) {
|
||||||
map.put("claimerUUID", claimerUUID.toString());
|
map.put("claimerUUID", claimerUUID.toString());
|
||||||
map.put("claimerName", claimerName);
|
map.put("claimerName", claimerName != null ? claimerName : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forwardedToUUID != null) {
|
if (forwardedToUUID != null) {
|
||||||
map.put("forwardedToUUID", forwardedToUUID.toString());
|
map.put("forwardedToUUID", forwardedToUUID.toString());
|
||||||
map.put("forwardedToName", forwardedToName);
|
map.put("forwardedToName", forwardedToName != null ? forwardedToName : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── NEU: Speichern des Soft Delete Flags ───
|
|
||||||
map.put("playerDeleted", playerDeleted);
|
map.put("playerDeleted", playerDeleted);
|
||||||
|
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 != null ? serverName : "unknown");
|
||||||
|
map.put("closeNotified", closeNotified);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NEU: Registrierung ---
|
public static void register() { ConfigurationSerialization.registerClass(Ticket.class, "Ticket"); }
|
||||||
public static void register() {
|
|
||||||
ConfigurationSerialization.registerClass(Ticket.class, "Ticket");
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Deine ursprüngliche getLocation Methode (beibehalten) ---
|
|
||||||
public Location getLocation() {
|
public Location getLocation() {
|
||||||
World world = Bukkit.getWorld(worldName);
|
World world = Bukkit.getWorld(worldName);
|
||||||
if (world == null) return null;
|
return world == null ? null : new Location(world, x, y, z, yaw, pitch);
|
||||||
return new Location(world, x, y, z, yaw, pitch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Getter & Setter ────────────────────────────
|
private static double toDouble(Object o) { return o instanceof Double d ? d : ((Number) o).doubleValue(); }
|
||||||
|
private static float toFloat(Object o) { return o instanceof Float f ? f : ((Number) o).floatValue(); }
|
||||||
|
private static long toLong(Object o) { return ((Number) o).longValue(); }
|
||||||
|
|
||||||
|
// ─────────────────────────── Getter & Setter ───────────────────────────
|
||||||
|
|
||||||
public int getId() { return id; }
|
public int getId() { return id; }
|
||||||
public void setId(int id) { this.id = id; }
|
public void setId(int id) { this.id = id; }
|
||||||
|
|
||||||
public UUID getCreatorUUID() { return creatorUUID; }
|
public UUID getCreatorUUID() { return creatorUUID; }
|
||||||
public void setCreatorUUID(UUID creatorUUID) { this.creatorUUID = creatorUUID; }
|
public void setCreatorUUID(UUID v) { this.creatorUUID = v; }
|
||||||
|
|
||||||
public String getCreatorName() { return creatorName; }
|
public String getCreatorName() { return creatorName; }
|
||||||
public void setCreatorName(String creatorName) { this.creatorName = creatorName; }
|
public void setCreatorName(String v) { this.creatorName = v; }
|
||||||
|
|
||||||
public String getMessage() { return message; }
|
public String getMessage() { return message; }
|
||||||
public void setMessage(String message) { this.message = message; }
|
public void setMessage(String v) { this.message = v; }
|
||||||
|
|
||||||
public String getWorldName() { return worldName; }
|
public String getWorldName() { return worldName; }
|
||||||
public void setWorldName(String worldName) { this.worldName = worldName; }
|
public void setWorldName(String v) { this.worldName = v; }
|
||||||
|
|
||||||
public double getX() { return x; }
|
public double getX() { return x; }
|
||||||
public void setX(double x) { this.x = x; }
|
public void setX(double v) { this.x = v; }
|
||||||
|
|
||||||
public double getY() { return y; }
|
public double getY() { return y; }
|
||||||
public void setY(double y) { this.y = y; }
|
public void setY(double v) { this.y = v; }
|
||||||
|
|
||||||
public double getZ() { return z; }
|
public double getZ() { return z; }
|
||||||
public void setZ(double z) { this.z = z; }
|
public void setZ(double v) { this.z = v; }
|
||||||
|
|
||||||
public float getYaw() { return yaw; }
|
public float getYaw() { return yaw; }
|
||||||
public void setYaw(float yaw) { this.yaw = yaw; }
|
public void setYaw(float v) { this.yaw = v; }
|
||||||
|
|
||||||
public float getPitch() { return pitch; }
|
public float getPitch() { return pitch; }
|
||||||
public void setPitch(float pitch) { this.pitch = pitch; }
|
public void setPitch(float v) { this.pitch = v; }
|
||||||
|
|
||||||
public TicketStatus getStatus() { return status; }
|
public TicketStatus getStatus() { return status; }
|
||||||
public void setStatus(TicketStatus status) { this.status = status; }
|
public void setStatus(TicketStatus v) { this.status = v; }
|
||||||
|
|
||||||
public UUID getClaimerUUID() { return claimerUUID; }
|
public UUID getClaimerUUID() { return claimerUUID; }
|
||||||
public void setClaimerUUID(UUID claimerUUID) { this.claimerUUID = claimerUUID; }
|
public void setClaimerUUID(UUID v) { this.claimerUUID = v; }
|
||||||
|
|
||||||
public String getClaimerName() { return claimerName; }
|
public String getClaimerName() { return claimerName; }
|
||||||
public void setClaimerName(String claimerName) { this.claimerName = claimerName; }
|
public void setClaimerName(String v) { this.claimerName = v; }
|
||||||
|
|
||||||
public UUID getForwardedToUUID() { return forwardedToUUID; }
|
public UUID getForwardedToUUID() { return forwardedToUUID; }
|
||||||
public void setForwardedToUUID(UUID forwardedToUUID) { this.forwardedToUUID = forwardedToUUID; }
|
public void setForwardedToUUID(UUID v) { this.forwardedToUUID = v; }
|
||||||
|
|
||||||
public String getForwardedToName() { return forwardedToName; }
|
public String getForwardedToName() { return forwardedToName; }
|
||||||
public void setForwardedToName(String forwardedToName) { this.forwardedToName = forwardedToName; }
|
public void setForwardedToName(String v) { this.forwardedToName = v; }
|
||||||
|
|
||||||
public Timestamp getCreatedAt() { return createdAt; }
|
public Timestamp getCreatedAt() { return createdAt; }
|
||||||
public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; }
|
public void setCreatedAt(Timestamp v) { this.createdAt = v; }
|
||||||
|
|
||||||
public Timestamp getClaimedAt() { return claimedAt; }
|
public Timestamp getClaimedAt() { return claimedAt; }
|
||||||
public void setClaimedAt(Timestamp claimedAt) { this.claimedAt = claimedAt; }
|
public void setClaimedAt(Timestamp v) { this.claimedAt = v; }
|
||||||
|
|
||||||
public Timestamp getClosedAt() { return closedAt; }
|
public Timestamp getClosedAt() { return closedAt; }
|
||||||
public void setClosedAt(Timestamp closedAt) { this.closedAt = closedAt; }
|
public void setClosedAt(Timestamp v) { this.closedAt = v; }
|
||||||
|
|
||||||
public String getCloseComment() { return closeComment; }
|
public String getCloseComment() { return closeComment; }
|
||||||
public void setCloseComment(String closeComment) { this.closeComment = closeComment; }
|
public void setCloseComment(String v) { this.closeComment = v; }
|
||||||
|
|
||||||
// ─── NEU: Getter/Setter für Soft Delete ───
|
|
||||||
public boolean isPlayerDeleted() { return playerDeleted; }
|
public boolean isPlayerDeleted() { return playerDeleted; }
|
||||||
public void setPlayerDeleted(boolean playerDeleted) { this.playerDeleted = playerDeleted; }
|
public void setPlayerDeleted(boolean v) { this.playerDeleted = v; }
|
||||||
|
public String getCategoryKey() { return categoryKey; }
|
||||||
|
public void setCategoryKey(String v) { this.categoryKey = v != null ? v.toLowerCase() : "general"; }
|
||||||
|
public TicketPriority getPriority() { return priority; }
|
||||||
|
public void setPriority(TicketPriority v) { this.priority = v; }
|
||||||
|
public String getPlayerRating() { return playerRating; }
|
||||||
|
public void setPlayerRating(String v) { this.playerRating = v; }
|
||||||
|
public boolean hasRating() { return playerRating != null; }
|
||||||
|
public boolean isClaimerNotified() { return claimerNotified; }
|
||||||
|
public void setClaimerNotified(boolean v) { this.claimerNotified = v; }
|
||||||
|
|
||||||
|
/** BungeeCord: Gibt den Server-Namen zurück, auf dem das Ticket erstellt wurde. */
|
||||||
|
public String getServerName() { return serverName != null ? serverName : "unknown"; }
|
||||||
|
/** BungeeCord: Setzt den Server-Namen (aus config.yml → server-name). */
|
||||||
|
public void setServerName(String v) { this.serverName = v != null ? v : "unknown"; }
|
||||||
|
|
||||||
|
/** Gibt an ob der Ersteller bereits über die Schließung informiert wurde (DB-persistent). */
|
||||||
|
public boolean isCloseNotified() { return closeNotified; }
|
||||||
|
/** Setzt den close_notified-Flag (wird in DB gespeichert). */
|
||||||
|
public void setCloseNotified(boolean v) { this.closeNotified = v; }
|
||||||
}
|
}
|
||||||
33
src/main/java/de/ticketsystem/model/TicketCategory.java
Normal file
33
src/main/java/de/ticketsystem/model/TicketCategory.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package de.ticketsystem.model;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
|
||||||
|
public enum TicketCategory {
|
||||||
|
GENERAL ("Allgemein", "§7", Material.PAPER),
|
||||||
|
BUG ("Bug", "§c", Material.REDSTONE),
|
||||||
|
QUESTION ("Frage", "§e", Material.BOOK),
|
||||||
|
COMPLAINT ("Beschwerde", "§6", Material.WRITABLE_BOOK),
|
||||||
|
OTHER ("Sonstiges", "§8", Material.FEATHER);
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final String color;
|
||||||
|
private final Material guiMaterial;
|
||||||
|
|
||||||
|
TicketCategory(String displayName, String color, Material guiMaterial) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.color = color;
|
||||||
|
this.guiMaterial = guiMaterial;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() { return displayName; }
|
||||||
|
public String getColor() { return color; }
|
||||||
|
public String getColored() { return color + displayName; }
|
||||||
|
public Material getGuiMaterial() { return guiMaterial; }
|
||||||
|
|
||||||
|
/** Safely parse from stored string, fall back to GENERAL. */
|
||||||
|
public static TicketCategory fromString(String s) {
|
||||||
|
if (s == null) return GENERAL;
|
||||||
|
try { return valueOf(s.toUpperCase()); }
|
||||||
|
catch (IllegalArgumentException e) { return GENERAL; }
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/main/java/de/ticketsystem/model/TicketComment.java
Normal file
47
src/main/java/de/ticketsystem/model/TicketComment.java
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package de.ticketsystem.model;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a player comment/reply on a ticket.
|
||||||
|
*/
|
||||||
|
public class TicketComment {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
private int ticketId;
|
||||||
|
private UUID authorUUID;
|
||||||
|
private String authorName;
|
||||||
|
private String message;
|
||||||
|
private Timestamp createdAt;
|
||||||
|
|
||||||
|
public TicketComment() {}
|
||||||
|
|
||||||
|
public TicketComment(int ticketId, UUID authorUUID, String authorName, String message) {
|
||||||
|
this.ticketId = ticketId;
|
||||||
|
this.authorUUID = authorUUID;
|
||||||
|
this.authorName = authorName;
|
||||||
|
this.message = message;
|
||||||
|
this.createdAt = new Timestamp(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── Getters / Setters ────────────────────────────────────
|
||||||
|
|
||||||
|
public int getId() { return id; }
|
||||||
|
public void setId(int id) { this.id = id; }
|
||||||
|
|
||||||
|
public int getTicketId() { return ticketId; }
|
||||||
|
public void setTicketId(int ticketId) { this.ticketId = ticketId; }
|
||||||
|
|
||||||
|
public UUID getAuthorUUID() { return authorUUID; }
|
||||||
|
public void setAuthorUUID(UUID authorUUID) { this.authorUUID = authorUUID; }
|
||||||
|
|
||||||
|
public String getAuthorName() { return authorName; }
|
||||||
|
public void setAuthorName(String authorName){ this.authorName = authorName; }
|
||||||
|
|
||||||
|
public String getMessage() { return message; }
|
||||||
|
public void setMessage(String message) { this.message = message; }
|
||||||
|
|
||||||
|
public Timestamp getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(Timestamp ts) { this.createdAt = ts; }
|
||||||
|
}
|
||||||
31
src/main/java/de/ticketsystem/model/TicketPriority.java
Normal file
31
src/main/java/de/ticketsystem/model/TicketPriority.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package de.ticketsystem.model;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
|
||||||
|
public enum TicketPriority {
|
||||||
|
LOW ("Niedrig", "§a", Material.GREEN_WOOL),
|
||||||
|
NORMAL ("Normal", "§e", Material.YELLOW_WOOL),
|
||||||
|
HIGH ("Hoch", "§6", Material.ORANGE_WOOL),
|
||||||
|
URGENT ("Dringend","§c", Material.RED_WOOL);
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final String color;
|
||||||
|
private final Material guiMaterial;
|
||||||
|
|
||||||
|
TicketPriority(String displayName, String color, Material guiMaterial) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.color = color;
|
||||||
|
this.guiMaterial = guiMaterial;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() { return displayName; }
|
||||||
|
public String getColor() { return color; }
|
||||||
|
public String getColored() { return color + displayName; }
|
||||||
|
public Material getGuiMaterial() { return guiMaterial; }
|
||||||
|
|
||||||
|
public static TicketPriority fromString(String s) {
|
||||||
|
if (s == null) return NORMAL;
|
||||||
|
try { return valueOf(s.toUpperCase()); }
|
||||||
|
catch (IllegalArgumentException e) { return NORMAL; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,26 @@ version: "2.0"
|
|||||||
# Debug-Modus (true = Logs in der Konsole)
|
# Debug-Modus (true = Logs in der Konsole)
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# BUNGEECORD (Cross-Server-Unterstützung)
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# VORAUSSETZUNGEN:
|
||||||
|
# 1. In spigot.yml auf JEDEM Server: bungeecord: true
|
||||||
|
# 2. MySQL muss aktiviert sein (use-mysql: true)
|
||||||
|
# 3. Plugin auf JEDEM Spigot-Server installieren
|
||||||
|
# 4. Alle Server müssen dieselbe MySQL-Datenbank verwenden
|
||||||
|
#
|
||||||
|
# false = Normaler Single-Server-Modus (Standard)
|
||||||
|
# true = Cross-Server Benachrichtigungen aktiv
|
||||||
|
# ----------------------------------------------------
|
||||||
|
bungeecord: false
|
||||||
|
bungee-teleport-enabled: true
|
||||||
|
|
||||||
|
# Name dieses Servers im BungeeCord-Netzwerk.
|
||||||
|
# Wird in Tickets, GUI und Discord-Embeds angezeigt.
|
||||||
|
# Auf jedem Server ANDERS einstellen! (z.B. "survival", "creative", "skyblock")
|
||||||
|
server-name: "survival"
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# SPEICHERPFAD & ARCHIV
|
# SPEICHERPFAD & ARCHIV
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
@@ -59,6 +79,80 @@ max-open-tickets-per-player: 2 # Maximale offene Tickets pro Spieler (0 = unbeg
|
|||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus)
|
auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus)
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# OPTIONALE FEATURES
|
||||||
|
# ----------------------------------------------------
|
||||||
|
|
||||||
|
# Kategorie-System (true = aktiviert)
|
||||||
|
# Spieler können beim Erstellen eine Kategorie wählen: /ticket create [kategorie] [priorität] <text>
|
||||||
|
categories-enabled: true
|
||||||
|
|
||||||
|
# Prioritäten-System (true = aktiviert)
|
||||||
|
# Spieler können beim Erstellen eine Priorität wählen: /ticket create [kategorie] [priorität] <text>
|
||||||
|
# Admins/Supporter können die Priorität nachträglich ändern: /ticket setpriority <id> <low|normal|high|urgent>
|
||||||
|
priorities-enabled: true
|
||||||
|
|
||||||
|
# Bewertungs-System (true = aktiviert)
|
||||||
|
# Spieler können nach dem Schließen den Support bewerten: /ticket rate <id> good|bad
|
||||||
|
# Ergebnisse sind in /ticket stats sichtbar
|
||||||
|
rating-enabled: true
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# KATEGORIEN (nur aktiv wenn categories-enabled: true)
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# Jede Kategorie hat:
|
||||||
|
# name: Anzeigename im Chat und in der GUI
|
||||||
|
# color: Farbcode mit & (Minecraft Farbcodes)
|
||||||
|
# material: Minecraft-Material für das GUI-Item (Großbuchstaben, z.B. PAPER, REDSTONE, BOOK)
|
||||||
|
# aliases: Alternative Eingaben beim /ticket create Befehl (Kleinbuchstaben!)
|
||||||
|
#
|
||||||
|
# Das erste eingetragene Item ist die Standard-Kategorie für Tickets ohne Angabe.
|
||||||
|
# Du kannst beliebig viele Kategorien hinzufügen oder entfernen.
|
||||||
|
# ----------------------------------------------------
|
||||||
|
categories:
|
||||||
|
general:
|
||||||
|
name: "Allgemein"
|
||||||
|
color: "&7"
|
||||||
|
material: "PAPER"
|
||||||
|
aliases:
|
||||||
|
- "allgemein"
|
||||||
|
- "general"
|
||||||
|
- "default"
|
||||||
|
bug:
|
||||||
|
name: "Bug"
|
||||||
|
color: "&c"
|
||||||
|
material: "REDSTONE"
|
||||||
|
aliases:
|
||||||
|
- "bug"
|
||||||
|
- "fehler"
|
||||||
|
- "error"
|
||||||
|
question:
|
||||||
|
name: "Frage"
|
||||||
|
color: "&e"
|
||||||
|
material: "BOOK"
|
||||||
|
aliases:
|
||||||
|
- "frage"
|
||||||
|
- "question"
|
||||||
|
- "help"
|
||||||
|
- "hilfe"
|
||||||
|
complaint:
|
||||||
|
name: "Beschwerde"
|
||||||
|
color: "&6"
|
||||||
|
material: "WRITABLE_BOOK"
|
||||||
|
aliases:
|
||||||
|
- "beschwerde"
|
||||||
|
- "complaint"
|
||||||
|
- "report"
|
||||||
|
- "melden"
|
||||||
|
other:
|
||||||
|
name: "Sonstiges"
|
||||||
|
color: "&8"
|
||||||
|
material: "FEATHER"
|
||||||
|
aliases:
|
||||||
|
- "sonstiges"
|
||||||
|
- "other"
|
||||||
|
- "misc"
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# DISCORD WEBHOOK (Optional)
|
# DISCORD WEBHOOK (Optional)
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
@@ -69,6 +163,44 @@ discord:
|
|||||||
# Webhook-URL aus Discord (Kanaleinstellungen → Integrationen → Webhook erstellen)
|
# Webhook-URL aus Discord (Kanaleinstellungen → Integrationen → Webhook erstellen)
|
||||||
webhook-url: ""
|
webhook-url: ""
|
||||||
|
|
||||||
|
# Rollen-Ping: Discord-Rollen-ID (Rechtsklick auf Rolle → ID kopieren)
|
||||||
|
# Leer lassen ("") = kein Ping
|
||||||
|
role-ping-id: ""
|
||||||
|
|
||||||
|
messages:
|
||||||
|
# ── Neues Ticket ────────────────────────────────────────────────────────
|
||||||
|
new-ticket:
|
||||||
|
title: "🎫 Neues Ticket erstellt"
|
||||||
|
color: "3066993" # Grün
|
||||||
|
footer: "TicketSystem"
|
||||||
|
show-position: true # Welt & Koordinaten im Embed anzeigen
|
||||||
|
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 bei neuem Ticket senden
|
||||||
|
|
||||||
|
# ── Ticket geschlossen ──────────────────────────────────────────────────
|
||||||
|
ticket-closed:
|
||||||
|
enabled: false # Webhook-Nachricht beim Schließen senden
|
||||||
|
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
|
||||||
|
|
||||||
|
# ── Ticket weitergeleitet ───────────────────────────────────────────────
|
||||||
|
ticket-forwarded:
|
||||||
|
enabled: false # Webhook-Nachricht beim Weiterleiten senden
|
||||||
|
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
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# SYSTEM-NACHRICHTEN (mit &-Farbcodes)
|
# SYSTEM-NACHRICHTEN (mit &-Farbcodes)
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
@@ -94,12 +226,35 @@ messages:
|
|||||||
ticket-forwarded: "&aTicket &e#{id} &awurde an &e{player} &aweitergeleitet."
|
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})"
|
ticket-forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen. &7(ID: {id})"
|
||||||
|
|
||||||
# --- NEU: Benachrichtigungen für den Ticket-Ersteller ---
|
# --- BENACHRICHTIGUNGEN FÜR DEN TICKET-ERSTELLER ---
|
||||||
# Wird gesendet, wenn das eigene Ticket geschlossen wurde
|
|
||||||
ticket-closed-notify: "&aDein Ticket &e#{id} &awurde geschlossen."
|
ticket-closed-notify: "&aDein Ticket &e#{id} &awurde geschlossen."
|
||||||
# Wird gesendet, wenn das eigene Ticket an einen anderen Supporter weitergeleitet wurde
|
|
||||||
ticket-forwarded-creator-notify: "&eDein Ticket &6#{id} &ewurde an &b{supporter} &eweitergeleitet."
|
ticket-forwarded-creator-notify: "&eDein Ticket &6#{id} &ewurde an &b{supporter} &eweitergeleitet."
|
||||||
|
|
||||||
|
# --- 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}"
|
||||||
|
|
||||||
|
# --- 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."
|
||||||
|
|
||||||
|
# --- 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"
|
||||||
|
|
||||||
|
# --- 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."
|
||||||
|
|
||||||
# --- FEHLER & HINWEISE ---
|
# --- FEHLER & HINWEISE ---
|
||||||
no-permission: "&cDu hast keine Berechtigung!"
|
no-permission: "&cDu hast keine Berechtigung!"
|
||||||
no-open-tickets: "&aAktuell gibt es keine offenen Tickets."
|
no-open-tickets: "&aAktuell gibt es keine offenen Tickets."
|
||||||
|
|||||||
@@ -1,31 +1,62 @@
|
|||||||
name: TicketSystem
|
name: TicketSystem
|
||||||
version: 1.0.3
|
version: 1.0.7
|
||||||
main: de.ticketsystem.TicketPlugin
|
main: de.ticketsystem.TicketPlugin
|
||||||
api-version: 1.20
|
api-version: 1.20
|
||||||
author: M_Viper
|
author: M_Viper
|
||||||
description: Ingame Support Ticket System with MySQL
|
description: Ingame Support Ticket System with MySQL
|
||||||
|
|
||||||
|
# ── BungeeCord Plugin-Messaging-Kanäle ───────────────────────────────────────
|
||||||
|
# PFLICHTFELD für Cross-Server-Benachrichtigungen!
|
||||||
|
channels:
|
||||||
|
- BungeeCord
|
||||||
|
- ticketsystem:notify
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
ticket:
|
ticket:
|
||||||
description: TicketSystem Hauptbefehl
|
description: TicketSystem Hauptbefehl
|
||||||
usage: /ticket <create|list|claim|close|forward|reload>
|
usage: |
|
||||||
|
/ticket create [Kategorie] <Text>
|
||||||
|
/ticket list
|
||||||
|
/ticket comment <ID> <Nachricht>
|
||||||
|
/ticket rate <ID> <good|bad>
|
||||||
|
/ticket claim <ID>
|
||||||
|
/ticket close <ID> [Kommentar]
|
||||||
|
/ticket forward <ID> <Spieler>
|
||||||
|
/ticket blacklist <add|remove|list> [Spieler] [Grund]
|
||||||
|
/ticket stats
|
||||||
|
/ticket archive
|
||||||
|
/ticket reload
|
||||||
aliases: [t, support]
|
aliases: [t, support]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
||||||
|
# ── Spieler-Permissions ───────────────────────────────────────────────────
|
||||||
|
|
||||||
ticket.create:
|
ticket.create:
|
||||||
description: Spieler kann Tickets erstellen
|
description: Spieler kann Tickets erstellen und kommentieren
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
|
# ── Supporter-Permissions ─────────────────────────────────────────────────
|
||||||
|
|
||||||
ticket.support:
|
ticket.support:
|
||||||
description: Supporter kann Tickets einsehen und claimen
|
description: Supporter kann Tickets einsehen, claimen und schließen
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
ticket.archive:
|
ticket.archive:
|
||||||
description: Zugriff auf das Ticket-Archiv (öffnen, einsehen, permanent löschen)
|
description: Zugriff auf das Ticket-Archiv (öffnen, einsehen, permanent löschen)
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
|
# ── Admin-Permissions ────────────────────────────────────────────────────
|
||||||
|
|
||||||
ticket.admin:
|
ticket.admin:
|
||||||
description: Admin hat vollen Zugriff inkl. Weiterleitung und Reload
|
description: >
|
||||||
|
Admin hat vollen Zugriff: Weiterleiten, Blacklist verwalten,
|
||||||
|
Statistiken, Reload, Archiv, Export/Import, Migration
|
||||||
default: op
|
default: op
|
||||||
children:
|
children:
|
||||||
ticket.support: true
|
ticket.support: true
|
||||||
|
ticket.blacklist: true
|
||||||
|
|
||||||
|
ticket.blacklist:
|
||||||
|
description: Kann Spieler zur Ticket-Blacklist hinzufügen und entfernen
|
||||||
|
default: false
|
||||||
Reference in New Issue
Block a user