23 Commits
1.0.2 ... 1.0.7

Author SHA1 Message Date
5259184e09 Upload pom.xml via GUI 2026-02-23 20:56:22 +00:00
a45e0f4731 Update from Git Manager GUI 2026-02-23 14:39:37 +01:00
33acd04c3b Upload pom.xml via GUI 2026-02-23 13:39:35 +00:00
0a547f90bf README.md aktualisiert 2026-02-23 12:07:52 +00:00
02811bafbd Update from Git Manager GUI 2026-02-23 13:06:59 +01:00
c8d4578fa6 Upload pom.xml via GUI 2026-02-23 12:06:58 +00:00
57a426a9c9 Update from Git Manager GUI 2026-02-21 22:26:13 +01:00
135d8b0fb3 Upload pom.xml via GUI 2026-02-21 21:26:12 +00:00
31c7a33cbb Upload pom.xml via GUI 2026-02-21 17:41:16 +00:00
301c0f1ce9 Update from Git Manager GUI 2026-02-21 18:41:15 +01:00
7ede377c07 Update from Git Manager GUI 2026-02-21 16:00:03 +01:00
834bd0e5e4 Upload pom.xml via GUI 2026-02-21 15:00:01 +00:00
f4bedaa288 README.md aktualisiert 2026-02-21 13:25:26 +00:00
8e7533a214 Upload pom.xml via GUI 2026-02-20 23:55:29 +00:00
b7de357e81 Update from Git Manager GUI 2026-02-21 00:55:27 +01:00
a91e17a097 README.md aktualisiert 2026-02-20 23:49:35 +00:00
12c9379797 Upload pom.xml via GUI 2026-02-20 17:38:44 +00:00
535b0aa2f3 Update from Git Manager GUI 2026-02-20 18:38:42 +01:00
d14646c5ae README.md aktualisiert 2026-02-20 16:49:53 +00:00
df6878db2f Update from Git Manager GUI 2026-02-20 12:31:38 +01:00
526cb8b442 Upload pom.xml via GUI 2026-02-20 11:31:37 +00:00
b930793c50 README.md aktualisiert 2026-02-20 11:29:55 +00:00
566941d687 README.md aktualisiert 2026-02-20 11:29:46 +00:00
23 changed files with 5703 additions and 1206 deletions

368
README.md
View File

@@ -1,219 +1,265 @@
<div align="center"> # TicketSystem
# 🎫 TicketSystem ![Version](https://img.shields.io/badge/Minecraft-1.18.x--1.21.x-green?style=for-the-badge) ![Java](https://img.shields.io/badge/Java-17+-orange?style=for-the-badge) ![Type](https://img.shields.io/badge/Type-Support-blue?style=for-the-badge)
### Das flexible, moderne Support- und Feedback-System für Minecraft-Server **TicketSystem** ist das flexible, moderne Support- und Feedback-Plugin für Minecraft-Server (Spigot/Paper 1.18.x1.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.
[![Minecraft](https://img.shields.io/badge/Minecraft-1.18.x--1.21.x-brightgreen?style=for-the-badge&logo=minecraft&logoColor=white)](https://www.spigotmc.org/)
[![Java](https://img.shields.io/badge/Java-17+-orange?style=for-the-badge&logo=openjdk&logoColor=white)](https://adoptium.net/)
[![Type](https://img.shields.io/badge/Type-Support%20Plugin-blue?style=for-the-badge)](https://github.com/)
[![License](https://img.shields.io/badge/License-All%20Rights%20Reserved-red?style=for-the-badge)](LICENSE)
**⭐ Zero-Lag · Production Ready · Fully Customizable ⭐**
[Features](#-features) · [Installation](#-installation) · [Konfiguration](#-konfiguration) · [Befehle](#-befehle--permissions) · [FAQ](#-faq) · [Support](#-support)
</div>
--- ---
## 📋 Über TicketSystem ## Features
**TicketSystem** ist die Komplettlösung für Support, Bug-Reports und Feedback auf deinem Minecraft-Server. Spieler erstellen Tickets direkt im Spiel Admins verwalten alles komfortabel per GUI oder Befehl. Optimiert für kleine und große Server, vollständig konfigurierbar und vollgepackt mit Profi-Features. - MySQL oder Datei-Speicherung (YAML/JSON oder MySQL/MariaDB, jederzeit umschaltbar)
- Automatische Backups & Migration
- Export/Import von Tickets
- Statistiken & Archivierung
- Rollenbasierter Archiv-Zugriff
- Konfigurierbare Speicherpfade
- Vollständige Validierung & Fehlerausgaben
- Debug-Modus & Versionsprüfung
- Anpassbare Nachrichten, Farben, Limits, Speicherpfade, Archiv-Intervall, Cooldowns, Rechte
- Dynamische GUI mit Seiten-System
- Kategorie- und Prioritäten-System
- Bewertungs- und Kommentar-System
- Offline-Benachrichtigungen
- Discord-Webhook mit Embeds & Rollen-Ping
- Blacklist für Spieler
- **FAQ-System** mit eigenem GUI, eigener Datei (`faqs.yml`) und vollständiger In-Game-Verwaltung durch Admins
- **Performance-Caching** TTL-basierter In-Memory-Cache reduziert Datenbankabfragen spürbar
- **Saubere Konsole** minimale Start-Logs, kein unnötiger Spam
- Erweiterbarkeit: viele Hooks
- **BungeeCord-Unterstützung**: serverübergreifende Tickets, Teleports, Weiterleitungen, Benachrichtigungen
--- ---
## ✨ Features ## Installation & Setup
| Feature | Beschreibung | 1. TicketSystem.jar in den plugins-Ordner legen und Server starten
|---|---| 2. `config.yml` anpassen (Speicherorte, Nachrichten, Limits, Farben, MySQL-Daten etc.)
| 🗄️ **MySQL & Datei-Speicherung** | YAML/JSON oder MySQL/MariaDB jederzeit umschaltbar, Migration & Backup inklusive | 3. `/ticket`-Befehle nutzen
| 🔄 **Automatische Migration** | Sicheres Wechseln zwischen Speicherarten, Datenverlust ausgeschlossen |
| 📤 **Export / Import** | Tickets einfach zwischen Servern oder Instanzen übertragen |
| 📊 **Statistiken & Archivierung** | Übersichtliche Auswertung, automatische & manuelle Archivierung nach Zeitplan |
| ✅ **Vollständige Validierung** | Fehlerhafte Tickets werden erkannt, gemeldet und übersprungen |
| 🐛 **Debug-Modus** | Ausführliche Logs für Entwickler und Admins, erkennt veraltete `config.yml` |
| 🖥️ **Dynamische GUI** | Passt sich automatisch der Ticketanzahl an bis zu 54 Tickets pro Seite mit Blättern |
| ⚡ **Performance** | Alle Operationen laufen asynchron optimiert für große Server |
| 🔧 **Komplett anpassbar** | Nachrichten, Farben, Limits, Cooldowns, Rechte alles in der `config.yml` |
| 🧪 **Unit-Tests** | Getestete Speicher-Logik für maximale Zuverlässigkeit |
--- ---
## 📦 Installation ## Befehle & Rechte
> **Voraussetzungen:** Paper / Spigot / Purpur `1.18.x 1.21.x` · Java `17+` · optional MySQL/MariaDB ### Übersicht der Befehle
**Schritt 1 Plugin installieren** | Befehl | Beschreibung | Nutzergruppe |
``` |-----------------------------------------------------------|--------------------------------------------------------|------------------|
1. Lade die neueste TicketSystem.jar von den Releases herunter | `/ticket` | Hilfe & Übersicht | Spieler, Support |
2. Verschiebe die .jar in den /plugins Ordner deines Servers | `/ticket create [Kategorie] [Priorität] <Text>` | Ticket erstellen | Spieler |
3. Starte den Server neu (kein /reload verwenden!) | `/ticket list` | Eigene Tickets in der GUI anzeigen | Spieler |
4. Die Konfigurationsdateien werden automatisch generiert | `/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 |
**Schritt 2 Konfiguration anpassen** ### Rechte
```
1. Öffne plugins/TicketSystem/config.yml
2. Passe Speicherpfade, Nachrichten, Limits und Farben an
3. Aktiviere MySQL falls gewünscht und trage Zugangsdaten ein
4. Nutze /ticket reload um Änderungen zu übernehmen
```
**Fertig!** Dein Support-System ist einsatzbereit. 🎉 | Permission | Beschreibung | Standard |
|-----------------|-----------------------------------------------------------------|------------------|
| `ticket.create` | Ticket erstellen | alle Spieler |
| `ticket.support`| Tickets einsehen, claimen, schließen, Priorität ändern | manuell vergeben |
| `ticket.archive`| Archiv öffnen, Tickets permanent löschen | manuell vergeben |
| `ticket.admin` | Voller Zugriff inkl. Weiterleitung, Reload, Blacklist, FAQ-Verwaltung | OP |
> `ticket.archive` ist nicht in `ticket.admin` enthalten und muss explizit vergeben werden.
--- ---
## ⚙️ Konfiguration ## FAQ-System
<details> 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.
<summary><b>📄 Beispiel: config.yml (klicken zum Ausklappen)</b></summary>
### 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 ```yaml
# TicketSystem - Hauptkonfiguration faqs:
# © 2026 Viper Plugins 1:
question: "Wie erstelle ich ein Ticket?"
version: "2.0" answer: "Nutze den Befehl /ticket create [Kategorie] [Beschreibung]."
debug: false 2:
question: "Wie lange dauert die Bearbeitung?"
# Speicherung answer: "Unser Support-Team bearbeitet Tickets so schnell wie möglich."
data-file: "data.yml"
archive-file: "archive.yml"
use-mysql: false
use-json: false
# MySQL (nur wenn use-mysql: true)
mysql:
host: "localhost"
port: 3306
database: "tickets"
user: "root"
password: "password"
useSSL: false
# Archivierung
auto-archive-interval-hours: 24 # 0 = deaktiviert
# Allgemein
prefix: "&8[&6Ticket&8] &r"
ticket-cooldown: 60 # Sekunden zwischen Tickets
max-description-length: 100
max-open-tickets-per-player: 2
``` ```
</details> Beim ersten Start werden automatisch vier Beispiel-FAQs erstellt.
--- ---
## 💬 Befehle & Permissions ## Performance-Caching
### Spieler-Befehle 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.
| Befehl | Beschreibung | Permission |
|---|---|---|
| `/ticket` | GUI mit allen offenen Tickets öffnen | `ticket.use` |
| `/ticket create <Nachricht>` | Neues Ticket erstellen | `ticket.use` |
| `/ticket close <ID>` | Eigenes Ticket schließen | `ticket.use` |
### Admin-Befehle - Standard-Lebenszeit: **60 Sekunden** (konfigurierbar: `cache-ttl-seconds` in `config.yml`)
| Befehl | Beschreibung | Permission | - Der Cache wird bei Ticket-Änderungen (Claim, Close, Priorität usw.) automatisch invalidiert
|---|---|---| - Regelmäßige Bereinigung abgelaufener Einträge alle 5 Minuten
| `/ticket claim <ID>` | Ticket übernehmen | `ticket.admin` | - Der aktuelle Cache-Status ist in `/ticket stats` einsehbar
| `/ticket forward <ID> <Spieler>` | Ticket weiterleiten | `ticket.admin` |
| `/ticket archive` | Tickets manuell archivieren | `ticket.admin` |
| `/ticket export <Datei>` | Tickets exportieren | `ticket.admin` |
| `/ticket import <Datei>` | Tickets importieren | `ticket.admin` |
| `/ticket migrate <tomysql\|tofile>` | Speicherart migrieren | `ticket.admin` |
| `/ticket stats` | Statistiken anzeigen | `ticket.admin` |
| `/ticket reload` | Konfiguration neu laden | `ticket.admin` |
### Permissions-Übersicht ---
```
ticket.use → Ticket erstellen und eigene Tickets verwalten (Standard für alle Spieler) ## Kategorie & Priorität
ticket.admin → Zugriff auf alle Admin- und Management-Funktionen
Beim Erstellen eines Tickets können Kategorie und Priorität optional angegeben werden:
| Befehl | Kategorie | Priorität |
|-----------------------------------------|-----------|-----------|
| `/ticket create <Text>` | Standard | NORMAL |
| `/ticket create bug <Text>` | Bug | NORMAL |
| `/ticket create high <Text>` | Standard | HIGH |
| `/ticket create bug high <Text>` | Bug | HIGH |
| `/ticket create question urgent <Text>` | Frage | URGENT |
**Verfügbare Prioritäten:** `low`, `normal`, `high`, `urgent` (auch deutsch: `niedrig`, `hoch`, `dringend`)
Kategorien und Aliases sind frei in der `config.yml` konfigurierbar.
---
## Discord-Webhook
- Embeds mit Kategorie & Priorität
- Rollen-Ping pro Nachrichtentyp
- Drei Ereignisse: neues Ticket, Ticket geschlossen, Ticket weitergeleitet
Konfiguration in `config.yml`:
```yaml
discord:
enabled: true
webhook-url: "https://discord.com/api/webhooks/..."
role-ping-id: "123456789012345678"
messages:
new-ticket:
role-ping: true
show-category: true
show-priority: true
ticket-closed:
enabled: true
role-ping: false
``` ```
--- ---
## ❓ FAQ ## BungeeCord-Unterstützung
<details> TicketSystem bietet volle Unterstützung für BungeeCord-Netzwerke:
<summary><b>Kann ich zwischen MySQL und Datei-Speicherung wechseln?</b></summary>
Ja! Einfach per `/ticket migrate tomysql` oder `/ticket migrate tofile`. Das Plugin migriert alle Daten automatisch und sicher kein Datenverlust. - Tickets von jedem Server im Netzwerk
</details> - 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
<details> **Voraussetzungen:**
<summary><b>Wie viele Tickets passen in die GUI?</b></summary> - `spigot.yml`: `bungeecord: true`
- `config.yml`: `bungeecord: true`, `server-name` pro Server
Bis zu 54 Tickets pro Seite. Bei mehr Tickets wird automatisch geblättert. - TicketSystem.jar auf allen Spigot-Servern
</details> - Alle Server nutzen dieselbe MySQL-Datenbank
<details>
<summary><b>Werden automatisch Backups erstellt?</b></summary>
Ja, bei jedem Speicherwechsel und regelmäßig nach dem konfigurierten Archiv-Intervall.
</details>
<details>
<summary><b>Wie aktiviere ich den Debug-Modus?</b></summary>
Setze `debug: true` in der `config.yml` und nutze anschließend `/ticket reload`.
</details>
<details>
<summary><b>Wie exportiere/importiere ich Tickets?</b></summary>
Mit `/ticket export <Dateiname>` und `/ticket import <Dateiname>` ideal für Server-Umzüge oder Testumgebungen.
</details>
--- ---
## 📊 Vergleich ## Vergleich mit anderen Plugins
| | **TicketSystem** | SimpleTickets | AdvancedTickets | | Feature | TicketSystem | SimpleTickets | AdvancedTickets |
|---|:---:|:---:|:---:| |-----------------------------|:------------:|:-------------:|:---------------:|
| Speicher-Migration | ✅ Vollständig | ⚠️ Nur manuell | ❌ | | Speicher-Migration | ✔️ | ⚠️ | ✖️ |
| Automatische Backups | ✅ | ⚠️ Teilweise | ❌ | | Automatische Backups | ✔️ | ⚠️ | ✖️ |
| Dynamische GUI | ✅ Modern | ⚠️ Basic | ❌ | | GUI mit Kategorien | ✔️ | ⚠️ | ✖️ |
| Archivierung | ✅ Auto & manuell | ⚠️ Nur manuell | ❌ | | Archivierung | ✔️ | ⚠️ | ✖️ |
| Export / Import | ✅ | ❌ | ❌ | | Rollenbasierter Archiv-Zugriff| ✔️ | ✖️ | ✖️ |
| Debug-Modus | ✅ | ❌ | ❌ | | Kategorie-System | ✔️ | ✖️ | ✖️ |
| Update-Checker | ✅ | ❌ | ❌ | | Prioritäten-System | ✔️ | ✖️ | ✖️ |
| Unit-Tests | ✅ | ❌ | ❌ | | FAQ-System | ✔️ | ✖️ | ✖️ |
| Performance-Caching | ✔️ | ✖️ | ✖️ |
| Offline-Benachrichtigungen | ✔️ | ✖️ | ✖️ |
| Discord-Webhook | ✔️ | ✖️ | ✖️ |
| Bewertungs-System | ✔️ | ✖️ | ✖️ |
| Update-Checker | ✔️ | ✖️ | ✖️ |
| BungeeCord-Unterstützung | ✔️ | ✖️ | ✖️ |
Legende: ✔️ Vollständige Unterstützung · ⚠️ Eingeschränkt · ✖️ Nicht vorhanden
--- ---
## 🆘 Support ## FAQ
<div align="center"> **Kann ich zwischen MySQL und Datei-Speicherung wechseln?**
> Ja! Mit `/ticket migrate tomysql` oder `/ticket migrate tofile` werden alle Daten automatisch migriert.
Hast du Fragen, einen Bug gefunden oder eine Feature-Idee? **Wie konfiguriere ich eigene Kategorien?**
> In der `config.yml` unter `categories:` Name, Farbe, Material und Aliases frei wählbar. Änderungen mit `/ticket reload` übernehmen.
[![Discord](https://img.shields.io/badge/Discord-Support%20beitreten-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/FdRs4BRd8D) **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`.
**Wir antworten in der Regel innerhalb von 24 Stunden!** **Was passiert mit Benachrichtigungen wenn ein Spieler offline ist?**
> Alle Benachrichtigungen werden gespeichert und beim nächsten Login angezeigt.
Bitte öffne für Bug-Reports ein Issue. **Wie ändere ich die Priorität eines Tickets?**
> Als Support/Admin per `/ticket setpriority <ID> <Priorität>` oder direkt in der GUI.
</div> **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.
--- ---
## 📜 Kompatibilität ## Support, Community & Motivation
| Plattform | Version | Du hast Fragen, brauchst Hilfe oder möchtest Feedback geben?
|---|---|
| Paper | ✅ 1.18.x 1.21.x |
| Spigot | ✅ 1.18.x 1.21.x |
| Purpur | ✅ 1.18.x 1.21.x |
| Folia | ❌ Nicht unterstützt |
--- - [Discord Support](https://discord.com/invite/FdRs4BRd8D)
- [Git Issues](https://git.viper.ipv64.net/M_Viper/TicketSystem/issues)
<div align="center"> Wir antworten in der Regel innerhalb von 24 Stunden!
**© 2026 Viper Plugins · TicketSystem · Alle Rechte vorbehalten** **Dein Feedback zählt:**
Wenn TicketSystem deinen Server bereichert hat, freuen wir uns über eine 5-Sterne Bewertung auf SpigotMC!
Wenn TicketSystem deinen Server bereichert hat, freuen wir uns über eine Bewertung auf spigotmc! Jede Rückmeldung hilft, das Plugin weiter zu verbessern und die Community zu stärken.
</div>

View File

@@ -6,7 +6,7 @@
<groupId>de.ticketsystem</groupId> <groupId>de.ticketsystem</groupId>
<artifactId>TicketSystem</artifactId> <artifactId>TicketSystem</artifactId>
<version>1.0.1</version> <version>1.0.7</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>TicketSystem</name> <name>TicketSystem</name>

View File

@@ -1,11 +1,17 @@
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.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;
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;
@@ -16,120 +22,163 @@ public class TicketPlugin extends JavaPlugin {
private static TicketPlugin instance; private static TicketPlugin instance;
private boolean debug; private boolean debug;
private DatabaseManager databaseManager;
private TicketManager ticketManager; /**
private TicketGUI ticketGUI; * Name dieses Servers im BungeeCord-Netzwerk.
* Konfigurierbar in config.yml → server-name
*/
private String serverName;
private DatabaseManager databaseManager;
private TicketManager ticketManager;
private CategoryManager categoryManager;
private FaqManager faqManager;
private TicketGUI ticketGUI;
private FaqGUI faqGUI;
private DiscordWebhook discordWebhook;
private BungeeMessenger bungeeMessenger;
private TicketCache ticketCache;
@Override @Override
public void onEnable() { public void onEnable() {
instance = this; instance = this;
// Config speichern falls nicht vorhanden
saveDefaultConfig(); saveDefaultConfig();
// Update-Checker (Spigot Resource-ID anpassen!) // Ticket-Klasse für YAML-Serialisierung registrieren
Ticket.register();
// ── 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 + ")");
// Sende Nachricht an alle Admins (online) mit 1 Sekunde Verzögerung 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); // 20 Ticks = 1 Sekunde
} 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 + ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein! Bitte prüfe, ob deine Konfiguration aktuell ist."); getLogger().warning("[WARNUNG] config.yml-Version (" + configVersion
+ ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!");
} }
// Debug-Status aus Config lesen
debug = getConfig().getBoolean("debug", false); debug = getConfig().getBoolean("debug", false);
// Datenbankverbindung aufbauen // ── Performance: Ticket-Cache ──────────────────────────────────────
long cacheTtl = getConfig().getLong("cache-ttl-seconds", 60) * 1000L;
ticketCache = new TicketCache(cacheTtl);
// Regelmäßige Cache-Bereinigung alle 5 Minuten
getServer().getScheduler().runTaskTimerAsynchronously(this,
() -> ticketCache.evictExpired(), 6000L, 6000L);
// Datenbankverbindung
databaseManager = new DatabaseManager(this); 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.");
if (isDebug()) getLogger().warning("[DEBUG] DatabaseManager.connect() fehlgeschlagen, Datei-Modus aktiviert.");
// Plugin bleibt aktiv, DatabaseManager wechselt auf Datei-Storage
} }
// Manager und GUI initialisieren // Manager, GUI, FAQ & Discord-Webhook initialisieren
ticketManager = new TicketManager(this); categoryManager = new CategoryManager(this);
ticketGUI = new TicketGUI(this); ticketManager = new TicketManager(this);
faqManager = new FaqManager(this);
ticketGUI = new TicketGUI(this);
faqGUI = new FaqGUI(this);
discordWebhook = new DiscordWebhook(this);
// Commands registrieren if (getConfig().getBoolean("discord.enabled", false)) {
String url = getConfig().getString("discord.webhook-url", "");
if (url.isEmpty()) {
getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in config.yml eingetragen!");
}
}
// Commands & Events
TicketCommand ticketCommand = new TicketCommand(this); TicketCommand ticketCommand = new TicketCommand(this);
Objects.requireNonNull(getCommand("ticket")).setExecutor(ticketCommand); Objects.requireNonNull(getCommand("ticket")).setExecutor(ticketCommand);
Objects.requireNonNull(getCommand("ticket")).setTabCompleter(ticketCommand); Objects.requireNonNull(getCommand("ticket")).setTabCompleter(ticketCommand);
// Events registrieren
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 nach Zeitplan (Intervall in Stunden, Standard: 24h) // Automatische Archivierung
int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24); int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24);
if (archiveIntervalH > 0) { if (archiveIntervalH > 0) {
long ticks = archiveIntervalH * 60L * 60L * 20L; // Stunden → Ticks long ticks = archiveIntervalH * 60L * 60L * 20L;
getServer().getScheduler().runTaskTimer(this, () -> { getServer().getScheduler().runTaskTimer(this, () -> {
int archived = databaseManager.archiveClosedTickets(); int archived = databaseManager.archiveClosedTickets();
if (archived > 0) { if (archived > 0) {
getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert."); getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert.");
if (isDebug()) getLogger().info("[DEBUG] Archivierung ausgeführt, " + archived + " Tickets verschoben.");
} }
}, ticks, ticks); }, ticks, ticks);
getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert.");
if (isDebug()) getLogger().info("[DEBUG] Archivierungs-Timer gesetzt: alle " + archiveIntervalH + " Stunden.");
} }
getLogger().info("TicketSystem erfolgreich gestartet!"); getLogger().info("TicketSystem v" + getDescription().getVersion() + " erfolgreich gestartet!");
} }
@Override @Override
public void onDisable() { public void onDisable() {
if (databaseManager != null) { getServer().getMessenger().unregisterOutgoingPluginChannel(this);
databaseManager.disconnect(); getServer().getMessenger().unregisterIncomingPluginChannel(this);
}
if (ticketCache != null) ticketCache.clear();
if (databaseManager != null) databaseManager.disconnect();
getLogger().info("TicketSystem wurde deaktiviert."); getLogger().info("TicketSystem wurde deaktiviert.");
} }
// ─────────────────────────── Hilfsmethoden ───────────────────────────── // ─────────────────────────── Hilfsmethoden ─────────────────────────────
/**
* Formatiert eine Nachricht aus der Config mit Prefix und Farben.
*/
public String formatMessage(String path) { public String formatMessage(String path) {
String prefix = color(getConfig().getString("prefix", "&8[&6Ticket&8] &r")); String prefix = color(getConfig().getString("prefix", "&8[&6Ticket&8] &r"));
String message = getConfig().getString(path, "&cNachricht nicht gefunden: " + path); String message = getConfig().getString(path, "&cNachricht nicht gefunden: " + path);
return prefix + color(message); return prefix + color(message);
} }
/**
* Konvertiert Farbcodes (&x → §x).
*/
public String color(String text) { public String color(String text) {
return ChatColor.translateAlternateColorCodes('&', text); return ChatColor.translateAlternateColorCodes('&', text);
} }
// ─────────────────────────── Getter ──────────────────────────────────── // ─────────────────────────── Getter ────────────────────────────────────
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 TicketGUI getTicketGUI() { return ticketGUI; } public CategoryManager getCategoryManager() { return categoryManager; }
public FaqManager getFaqManager() { return faqManager; }
/** public TicketGUI getTicketGUI() { return ticketGUI; }
* Gibt zurück, ob der Debug-Modus aktiv ist (aus config.yml) public FaqGUI getFaqGUI() { return faqGUI; }
*/ public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
public boolean isDebug() { return debug; } public BungeeMessenger getBungeeMessenger() { return bungeeMessenger; }
public TicketCache getTicketCache() { return ticketCache; }
public boolean isDebug() { return debug; }
public String getServerName() { return serverName; }
public boolean isBungeeCordEnabled() { return getConfig().getBoolean("bungeecord", false); }
} }

View File

@@ -10,27 +10,65 @@ 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;
private final int resourceId; private final int resourceId;
public UpdateChecker(JavaPlugin plugin, int resourceId) { public UpdateChecker(JavaPlugin plugin, int resourceId) {
this.plugin = plugin; this.plugin = plugin;
this.resourceId = resourceId; this.resourceId = resourceId;
} }
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()) {
consumer.accept(scann.next()); String spigotVersion = scann.next().trim();
String localVersion = plugin.getDescription().getVersion().trim();
// Nur melden wenn Spigot-Version wirklich neuer ist
if (isNewerVersion(spigotVersion, localVersion)) {
consumer.accept(spigotVersion);
}
} }
} catch (IOException e) { } 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);
}
}
} }

View 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();
}
}

View 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

View File

@@ -0,0 +1,389 @@
package de.ticketsystem.discord;
import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.ConfigCategory;
import de.ticketsystem.model.Ticket;
import de.ticketsystem.model.TicketPriority;
import org.bukkit.Bukkit;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public class DiscordWebhook {
// ─────────────────────────────────────────────────────────────────────────
// Konstanten & Felder
// ─────────────────────────────────────────────────────────────────────────
private static final String AVATAR_URL = "https://mc-heads.net/avatar/%s/64";
private final TicketPlugin plugin;
// ─────────────────────────────────────────────────────────────────────────
// Konstruktor
// ─────────────────────────────────────────────────────────────────────────
public DiscordWebhook(TicketPlugin plugin) {
this.plugin = plugin;
}
// ─────────────────────────────────────────────────────────────────────────
// Öffentliche Methoden Webhook-Events
// ─────────────────────────────────────────────────────────────────────────
public void sendNewTicket(Ticket ticket) {
if (!isEnabled()) return;
String webhookUrl = getWebhookUrl();
if (webhookUrl == null) return;
// Konfiguration lesen
String title = plugin.getConfig().getString ("discord.messages.new-ticket.title", "Neues Ticket");
String color = plugin.getConfig().getString ("discord.messages.new-ticket.color", "5793266");
String footer = plugin.getConfig().getString ("discord.messages.new-ticket.footer", "TicketSystem");
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);
// Hilfs-Werte berechnen
String prioEmoji = getPriorityEmoji(ticket.getPriority());
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
// Felder aufbauen
List<Field> fields = new ArrayList<>();
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) {
fields.add(new Field("🌍 Welt", ticket.getWorldName(), true));
fields.add(new Field("📍 Position",
String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()), true));
}
// 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);
}
// ─────────────────────────────────────────────────────────────────────────
public void sendTicketClosed(Ticket ticket, String closerName) {
if (!isEnabled()) return;
if (!plugin.getConfig().getBoolean("discord.messages.ticket-closed.enabled", false)) return;
String webhookUrl = getWebhookUrl();
if (webhookUrl == null) return;
// Konfiguration lesen
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");
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()));
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
desc.append("\n\n**Kommentar des Supports**\n> ").append(j(ticket.getCloseComment()));
}
// 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);
}
// ─────────────────────────────────────────────────────────────────────────
public void sendTicketForwarded(Ticket ticket, String fromName) {
if (!isEnabled()) return;
if (!plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.enabled", false)) return;
String webhookUrl = getWebhookUrl();
if (webhookUrl == null) return;
// Konfiguration lesen
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");
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);
// Hilfs-Werte berechnen
String prioEmoji = getPriorityEmoji(ticket.getPriority());
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
// Felder aufbauen
String forwardedTo = ticket.getForwardedToName() != null ? j(ticket.getForwardedToName()) : "";
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"
);
sendAsync(webhookUrl, json);
}
// ─────────────────────────────────────────────────────────────────────────
// 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() {
return plugin.getConfig().getBoolean("discord.enabled", false);
}
private String getWebhookUrl() {
String url = plugin.getConfig().getString("discord.webhook-url", "");
return url.isEmpty() ? null : url;
}
private String buildRolePing() {
String roleId = plugin.getConfig().getString("discord.role-ping-id", "").trim();
return roleId.isEmpty() ? "" : "<@&" + roleId + ">";
}
private String getPriorityEmoji(TicketPriority priority) {
return switch (priority) {
case LOW -> "🟢";
case NORMAL -> "🟡";
case HIGH -> "🟠";
case URGENT -> "🔴";
};
}
private void sendAsync(String webhookUrl, String json) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
URL url = new URL(webhookUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("User-Agent", "TicketSystem-Plugin");
conn.setDoOutput(true);
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
try (OutputStream os = conn.getOutputStream()) {
os.write(json.getBytes(StandardCharsets.UTF_8));
}
int responseCode = conn.getResponseCode();
if (plugin.isDebug()) {
plugin.getLogger().info("[DEBUG][DiscordWebhook] Response: " + responseCode);
if (responseCode != 200 && responseCode != 204) {
plugin.getLogger().info("[DEBUG][DiscordWebhook] Payload: " + json);
}
}
if (responseCode != 200 && responseCode != 204) {
plugin.getLogger().warning("[DiscordWebhook] Unerwarteter Response-Code: " + responseCode);
}
conn.disconnect();
} catch (Exception e) {
plugin.getLogger().warning("[DiscordWebhook] Fehler beim Senden: " + e.getMessage());
if (plugin.isDebug()) e.printStackTrace();
}
});
}
}

View 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

View File

@@ -1,7 +1,15 @@
package de.ticketsystem.listeners; package de.ticketsystem.listeners;
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.TicketStatus;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
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;
@@ -19,21 +27,113 @@ public class PlayerJoinListener implements Listener {
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();
// Nur Supporter und Admins erhalten die Join-Benachrichtigung // ── Supporter/Admin: offene Tickets anzeigen ──────────────────────
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) return; if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().countOpenTickets();
if (count > 0) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
String msg = plugin.formatMessage("messages.join-open-tickets")
.replace("{count}", String.valueOf(count));
player.sendMessage(msg);
player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht."));
}, 40L);
}
});
}
// Verzögerung von 2 Sekunden damit die Join-Sequenz abgeschlossen ist // ── BungeeCord: ausstehenden Teleport-Auftrag prüfen ─────────────
// Wenn ein Admin via GUI auf einen anderen Server geschickt wurde,
// liegt hier die Zielposition. Wir teleportieren ihn nach dem Spawn.
if (plugin.isBungeeCordEnabled()
&& plugin.getConfig().getBoolean("bungee-teleport-enabled", true)) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
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, () -> {
int count = plugin.getDatabaseManager().countOpenTickets(); List<Ticket> closed = plugin.getDatabaseManager()
.getTicketsByStatus(TicketStatus.CLOSED);
if (count > 0) { for (Ticket t : closed) {
Bukkit.getScheduler().runTaskLater(plugin, () -> { if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
String msg = plugin.formatMessage("messages.join-open-tickets") // DB-Feld prüfen funktioniert serverübergreifend
.replace("{count}", String.valueOf(count)); if (t.isCloseNotified()) continue;
player.sendMessage(msg);
player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht.")); Bukkit.getScheduler().runTask(plugin, () ->
}, 40L); // 40 Ticks = 2 Sekunden plugin.getTicketManager().notifyCreatorClosed(t));
} }
}); });
// ── Update-Hinweis für OPs/Admins ────────────────────────────────
if (player.isOp() || player.hasPermission("ticket.admin")) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
int resourceId = 132757;
new de.ticketsystem.UpdateChecker(plugin, resourceId).getVersion(version -> {
String current = plugin.getDescription().getVersion();
if (!current.equals(version)) {
String bar = ChatColor.GOLD + "====================================================";
player.sendMessage(bar);
player.sendMessage(ChatColor.GOLD + "[TicketSystem] "
+ ChatColor.YELLOW + "NEUES UPDATE VERFÜGBAR: v" + version);
player.sendMessage(ChatColor.GOLD + "[TicketSystem] "
+ ChatColor.YELLOW + "Download: https://www.spigotmc.org/resources/132757");
player.sendMessage(bar);
}
});
}, 20L);
}
} }
} }

View 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();
}
}

View 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();
}
}

View File

@@ -1,6 +1,7 @@
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;
@@ -14,7 +15,7 @@ public class TicketManager {
private final TicketPlugin plugin; private final TicketPlugin plugin;
// Cooldown Map: UUID → Zeit in Millis, wann das letzte Ticket erstellt wurde /** Cooldown Map: UUID → Zeitstempel letztes Ticket */
private final Map<UUID, Long> cooldowns = new HashMap<>(); private final Map<UUID, Long> cooldowns = new HashMap<>();
public TicketManager(TicketPlugin plugin) { public TicketManager(TicketPlugin plugin) {
@@ -35,62 +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 und Admins über ein neues Ticket. * Benachrichtigt alle Supporter/Admins über ein neues Ticket auch auf anderen Servern.
*
* Lokal online Spieler werden direkt angesprochen.
* Über BungeeCord werden alle anderen Server im Netzwerk ebenfalls benachrichtigt.
* Optional sendet der Discord-Webhook eine Nachricht.
*/ */
public void notifyTeam(Ticket ticket) { public void notifyTeam(Ticket ticket) {
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
String message = ticket.getMessage() != null ? ticket.getMessage() : "";
// Kategorie & Priorität optional anzeigen
String categoryInfo = "";
String priorityInfo = "";
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
categoryInfo = " §7[§r" + cat.getColored() + "§7]";
}
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}", ticket.getCreatorName()) .replace("{player}", creatorName)
.replace("{message}", ticket.getMessage()) .replace("{message}", message)
.replace("{id}", String.valueOf(ticket.getId())); .replace("{id}", String.valueOf(ticket.getId()))
+ categoryInfo + priorityInfo + serverInfo;
for (Player p : Bukkit.getOnlinePlayers()) { String guiHint = plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen.");
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
p.sendMessage(msg);
// Klickbaren Hinweis senden (Bukkit Chat-Component) if (plugin.isBungeeCordEnabled()) {
p.sendMessage(plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen.")); // ─ BungeeCord-Modus: Team-Broadcast über alle Server ─────────────────
// BungeeMessenger sendet lokal direkt, dann per Forward an alle anderen Server.
// Beide Nachrichten werden zu einer zusammengefasst um ein einzelnes
// Forward-Paket zu erzeugen statt zwei (reduziert Netzwerklast und
// verhindert mögliche Reihenfolge-Probleme).
plugin.getBungeeMessenger().broadcastTeamNotification(msg + "\n" + guiHint);
} else {
// ─ Standalone-Modus: Nur lokal ───────────────────────────────
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
p.sendMessage(msg);
p.sendMessage(guiHint);
}
} }
} }
plugin.getDiscordWebhook().sendNewTicket(ticket);
} }
/** /**
* Benachrichtigt den Ersteller des Tickets, wenn es geclaimt wurde. * Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde.
* Setzt claimer_notified = true und persistiert es.
*
* BungeeCord: Zustellung auch wenn der Spieler auf einem anderen Server ist.
*/ */
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()) {
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}", ticket.getClaimerName()); .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.
* BungeeCord: Cross-Server-Zustellung.
*/
public void notifyCreatorForwarded(Ticket ticket) {
String forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter";
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
.replace("{id}", String.valueOf(ticket.getId()))
.replace("{supporter}", forwardedTo);
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.
* BungeeCord: Zustellung auch wenn der Supporter auf einem anderen Server ist.
*/ */
public void notifyForwardedTo(Ticket ticket) { public void notifyForwardedTo(Ticket ticket, String fromName) {
Player target = Bukkit.getPlayer(ticket.getForwardedToUUID()); if (ticket.getForwardedToUUID() == null) return;
if (target != null && target.isOnline()) {
String msg = plugin.formatMessage("messages.ticket-forwarded-notify") String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
.replace("{player}", ticket.getCreatorName()) String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
.replace("{id}", String.valueOf(ticket.getId())); .replace("{player}", creatorName)
target.sendMessage(msg); .replace("{id}", String.valueOf(ticket.getId()));
deliverToPlayer(ticket.getForwardedToUUID(), ticket.getForwardedToName(), msg);
plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName);
}
/**
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde.
* BungeeCord: Cross-Server-Zustellung + Fallback in Pending-DB.
*/
public void notifyCreatorClosed(Ticket ticket) { notifyCreatorClosed(ticket, null); }
public void notifyCreatorClosed(Ticket ticket, String closerName) {
// Bug-Fix: close_notified wird in der DB gespeichert kein In-Memory-Set mehr.
// Dadurch funktioniert der Check auch nach einem Server-Wechsel korrekt.
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
plugin.getDatabaseManager().markCloseNotified(ticket.getId()));
String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
? ticket.getCloseComment() : "";
// Hauptnachricht
String msg = plugin.formatMessage("messages.ticket-closed-notify")
.replace("{id}", String.valueOf(ticket.getId()))
.replace("{comment}", comment);
// 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);
if (!comment.isEmpty())
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);
}
String closer = closerName != null ? closerName : "Unbekannt";
plugin.getDiscordWebhook().sendTicketClosed(ticket, closer);
}
/**
* 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) {
// 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) {
* Prüft, ob ein Spieler zu viele offene Tickets hat. 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;
@@ -101,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 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 list &7 Ticket-Übersicht (GUI)"));
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> &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"));
}
} }
} }

View 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; }
}

View 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 + "'}";
}
}

View File

@@ -3,106 +3,214 @@ 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.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.SerializableAs;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class Ticket {
private int id; @SerializableAs("Ticket")
private UUID creatorUUID; public class Ticket implements ConfigurationSerializable {
private int id;
private UUID creatorUUID;
private String creatorName; private String creatorName;
private String message; private String message;
// Location-Felder (werden separat gespeichert)
private String worldName; private String worldName;
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;
private UUID forwardedToUUID; private UUID forwardedToUUID;
private String forwardedToName; private String forwardedToName;
private Timestamp createdAt; private Timestamp createdAt;
private Timestamp claimedAt; private Timestamp claimedAt;
private Timestamp closedAt; private Timestamp closedAt;
private String closeComment;
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.status = TicketStatus.OPEN;
this.yaw = location.getYaw();
this.pitch = location.getPitch();
this.status = TicketStatus.OPEN;
this.createdAt = new Timestamp(System.currentTimeMillis()); this.createdAt = new Timestamp(System.currentTimeMillis());
} }
public Location getLocation() { public Ticket(Map<String, Object> map) {
World world = Bukkit.getWorld(worldName); this.id = (int) map.get("id");
if (world == null) return null; Object cObj = map.get("creatorUUID");
return new Location(world, x, y, z, yaw, pitch); this.creatorUUID = cObj instanceof UUID ? (UUID) cObj : UUID.fromString((String) cObj);
this.creatorName = (String) map.get("creatorName");
this.message = (String) map.get("message");
this.worldName = (String) map.get("world");
this.x = toDouble(map.get("x")); this.y = toDouble(map.get("y")); this.z = toDouble(map.get("z"));
this.yaw = toFloat(map.get("yaw")); this.pitch = toFloat(map.get("pitch"));
this.status = TicketStatus.valueOf((String) map.get("status"));
if (map.get("createdAt") != null) this.createdAt = new Timestamp(toLong(map.get("createdAt")));
if (map.get("claimedAt") != null) this.claimedAt = new Timestamp(toLong(map.get("claimedAt")));
if (map.get("closedAt") != null) this.closedAt = new Timestamp(toLong(map.get("closedAt")));
this.closeComment = (String) map.get("closeComment");
if (map.containsKey("claimerUUID") && map.get("claimerUUID") != null) {
Object o = map.get("claimerUUID");
this.claimerUUID = o instanceof UUID ? (UUID) o : UUID.fromString((String) o);
this.claimerName = (String) map.get("claimerName");
}
if (map.containsKey("forwardedToUUID") && map.get("forwardedToUUID") != null) {
Object o = map.get("forwardedToUUID");
this.forwardedToUUID = o instanceof UUID ? (UUID) o : UUID.fromString((String) o);
this.forwardedToName = (String) map.get("forwardedToName");
}
if (map.containsKey("playerDeleted")) this.playerDeleted = (boolean) map.get("playerDeleted");
if (map.containsKey("category")) this.categoryKey = (String) map.get("category");
if (map.containsKey("priority")) this.priority = TicketPriority.fromString((String) map.get("priority"));
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");
} }
// ─────────────────────────── Getter & Setter ──────────────────────────── @Override
public Map<String, Object> serialize() {
Map<String, Object> map = new HashMap<>();
map.put("id", id);
// Null-Schutz: SnakeYAML wirft NPE wenn Key oder Value null sind
map.put("creatorUUID", creatorUUID != null ? creatorUUID.toString() : "");
map.put("creatorName", creatorName != null ? creatorName : "");
map.put("message", message != null ? message : "");
map.put("world", worldName != null ? worldName : "world");
map.put("x", x); map.put("y", y); map.put("z", z);
map.put("yaw", yaw); map.put("pitch", pitch);
map.put("status", status != null ? status.name() : TicketStatus.OPEN.name());
if (createdAt != null) map.put("createdAt", createdAt.getTime());
if (claimedAt != null) map.put("claimedAt", claimedAt.getTime());
if (closedAt != null) map.put("closedAt", closedAt.getTime());
if (closeComment != null) map.put("closeComment", closeComment);
if (claimerUUID != null) {
map.put("claimerUUID", claimerUUID.toString());
map.put("claimerName", claimerName != null ? claimerName : "");
}
if (forwardedToUUID != null) {
map.put("forwardedToUUID", forwardedToUUID.toString());
map.put("forwardedToName", forwardedToName != null ? forwardedToName : "");
}
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;
}
public int getId() { return id; } public static void register() { ConfigurationSerialization.registerClass(Ticket.class, "Ticket"); }
public void setId(int id) { this.id = id; }
public UUID getCreatorUUID() { return creatorUUID; } public Location getLocation() {
public void setCreatorUUID(UUID creatorUUID) { this.creatorUUID = creatorUUID; } World world = Bukkit.getWorld(worldName);
return world == null ? null : new Location(world, x, y, z, yaw, pitch);
}
public String getCreatorName() { return creatorName; } private static double toDouble(Object o) { return o instanceof Double d ? d : ((Number) o).doubleValue(); }
public void setCreatorName(String creatorName) { this.creatorName = creatorName; } 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(); }
public String getMessage() { return message; } // ─────────────────────────── Getter & Setter ───────────────────────────
public void setMessage(String message) { this.message = message; }
public String getWorldName() { return worldName; } public int getId() { return id; }
public void setWorldName(String worldName) { this.worldName = worldName; } public void setId(int id) { this.id = id; }
public UUID getCreatorUUID() { return creatorUUID; }
public void setCreatorUUID(UUID v) { this.creatorUUID = v; }
public String getCreatorName() { return creatorName; }
public void setCreatorName(String v) { this.creatorName = v; }
public String getMessage() { return message; }
public void setMessage(String v) { this.message = v; }
public String getWorldName() { return worldName; }
public void setWorldName(String v) { this.worldName = v; }
public double getX() { return x; }
public void setX(double v) { this.x = v; }
public double getY() { return y; }
public void setY(double v) { this.y = v; }
public double getZ() { return z; }
public void setZ(double v) { this.z = v; }
public float getYaw() { return yaw; }
public void setYaw(float v) { this.yaw = v; }
public float getPitch() { return pitch; }
public void setPitch(float v) { this.pitch = v; }
public TicketStatus getStatus() { return status; }
public void setStatus(TicketStatus v) { this.status = v; }
public UUID getClaimerUUID() { return claimerUUID; }
public void setClaimerUUID(UUID v) { this.claimerUUID = v; }
public String getClaimerName() { return claimerName; }
public void setClaimerName(String v) { this.claimerName = v; }
public UUID getForwardedToUUID() { return forwardedToUUID; }
public void setForwardedToUUID(UUID v) { this.forwardedToUUID = v; }
public String getForwardedToName() { return forwardedToName; }
public void setForwardedToName(String v) { this.forwardedToName = v; }
public Timestamp getCreatedAt() { return createdAt; }
public void setCreatedAt(Timestamp v) { this.createdAt = v; }
public Timestamp getClaimedAt() { return claimedAt; }
public void setClaimedAt(Timestamp v) { this.claimedAt = v; }
public Timestamp getClosedAt() { return closedAt; }
public void setClosedAt(Timestamp v) { this.closedAt = v; }
public String getCloseComment() { return closeComment; }
public void setCloseComment(String v) { this.closeComment = v; }
public boolean isPlayerDeleted() { return 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; }
public double getX() { return x; } /** BungeeCord: Gibt den Server-Namen zurück, auf dem das Ticket erstellt wurde. */
public void setX(double x) { this.x = x; } 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"; }
public double getY() { return y; } /** Gibt an ob der Ersteller bereits über die Schließung informiert wurde (DB-persistent). */
public void setY(double y) { this.y = y; } public boolean isCloseNotified() { return closeNotified; }
/** Setzt den close_notified-Flag (wird in DB gespeichert). */
public double getZ() { return z; } public void setCloseNotified(boolean v) { this.closeNotified = v; }
public void setZ(double z) { this.z = z; }
public float getYaw() { return yaw; }
public void setYaw(float yaw) { this.yaw = yaw; }
public float getPitch() { return pitch; }
public void setPitch(float pitch) { this.pitch = pitch; }
public TicketStatus getStatus() { return status; }
public void setStatus(TicketStatus status) { this.status = status; }
public UUID getClaimerUUID() { return claimerUUID; }
public void setClaimerUUID(UUID claimerUUID) { this.claimerUUID = claimerUUID; }
public String getClaimerName() { return claimerName; }
public void setClaimerName(String claimerName) { this.claimerName = claimerName; }
public UUID getForwardedToUUID() { return forwardedToUUID; }
public void setForwardedToUUID(UUID forwardedToUUID) { this.forwardedToUUID = forwardedToUUID; }
public String getForwardedToName() { return forwardedToName; }
public void setForwardedToName(String forwardedToName) { this.forwardedToName = forwardedToName; }
public Timestamp getCreatedAt() { return createdAt; }
public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; }
public Timestamp getClaimedAt() { return claimedAt; }
public void setClaimedAt(Timestamp claimedAt) { this.claimedAt = claimedAt; }
public Timestamp getClosedAt() { return closedAt; }
public void setClosedAt(Timestamp closedAt) { this.closedAt = closedAt; }
} }

View 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; }
}
}

View 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; }
}

View 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; }
}
}

View File

@@ -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,128 @@ 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:
# Auf true setzen um Discord-Benachrichtigungen zu aktivieren
enabled: false
# Webhook-URL aus Discord (Kanaleinstellungen → Integrationen → Webhook erstellen)
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)
# ---------------------------------------------------- # ----------------------------------------------------
@@ -82,13 +224,42 @@ messages:
ticket-claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen." ticket-claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen."
ticket-closed: "&aTicket &e#{id} &awurde geschlossen." ticket-closed: "&aTicket &e#{id} &awurde geschlossen."
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." ticket-forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen. &7(ID: {id})"
# --- BENACHRICHTIGUNGEN FÜR DEN TICKET-ERSTELLER ---
ticket-closed-notify: "&aDein Ticket &e#{id} &awurde geschlossen."
ticket-forwarded-creator-notify: "&eDein Ticket &6#{id} &ewurde an &b{supporter} &eweitergeleitet."
# --- 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."
join-open-tickets: "&eEs gibt noch &6{count} &eoffene Ticket(s)!" join-open-tickets: "&eEs gibt noch &6{count} &eoffene Ticket(s)!"
new-ticket-notify: "&e{player} &ahat ein neues Ticket erstellt: &7{message}" new-ticket-notify: "&e{player} &ahat ein neues Ticket erstellt: &7{message} &7(ID: &e{id}&7)"
already-claimed: "&cDieses Ticket wurde bereits geclaimt!" already-claimed: "&cDieses Ticket wurde bereits geclaimt!"
ticket-not-found: "&cTicket nicht gefunden!" ticket-not-found: "&cTicket nicht gefunden!"
cooldown: "&cBitte warte &e{seconds} Sekunden &cbevor du ein neues Ticket erstellst." cooldown: "&cBitte warte &e{seconds} Sekunden &cbevor du ein neues Ticket erstellst."

View File

@@ -1,25 +1,62 @@
name: TicketSystem name: TicketSystem
version: 1.0.1 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: Ticket System 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:
description: Zugriff auf das Ticket-Archiv (öffnen, einsehen, permanent löschen)
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:
ticket.support: true
ticket.blacklist: true
ticket.blacklist:
description: Kann Spieler zur Ticket-Blacklist hinzufügen und entfernen
default: false