6 Commits
1.0.5 ... 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
12 changed files with 1204 additions and 274 deletions

225
README.md
View File

@@ -2,7 +2,7 @@
![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) ![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)
**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, Discord-Webhook und volle BungeeCord-Unterstützung. **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.
--- ---
@@ -23,7 +23,9 @@
- Offline-Benachrichtigungen - Offline-Benachrichtigungen
- Discord-Webhook mit Embeds & Rollen-Ping - Discord-Webhook mit Embeds & Rollen-Ping
- Blacklist für Spieler - Blacklist für Spieler
- Performance: asynchron, ressourcenschonend - **FAQ-System** mit eigenem GUI, eigener Datei (`faqs.yml`) und vollständiger In-Game-Verwaltung durch Admins
- **Performance-Caching** TTL-basierter In-Memory-Cache reduziert Datenbankabfragen spürbar
- **Saubere Konsole** minimale Start-Logs, kein unnötiger Spam
- Erweiterbarkeit: viele Hooks - Erweiterbarkeit: viele Hooks
- **BungeeCord-Unterstützung**: serverübergreifende Tickets, Teleports, Weiterleitungen, Benachrichtigungen - **BungeeCord-Unterstützung**: serverübergreifende Tickets, Teleports, Weiterleitungen, Benachrichtigungen
@@ -32,8 +34,8 @@
## Installation & Setup ## Installation & Setup
1. TicketSystem.jar in den plugins-Ordner legen und Server starten 1. TicketSystem.jar in den plugins-Ordner legen und Server starten
2. config.yml anpassen (Speicherorte, Nachrichten, Limits, Farben, MySQL-Daten etc.) 2. `config.yml` anpassen (Speicherorte, Nachrichten, Limits, Farben, MySQL-Daten etc.)
3. /ticket-Befehle nutzen 3. `/ticket`-Befehle nutzen
--- ---
@@ -41,60 +43,113 @@
### Übersicht der Befehle ### Übersicht der Befehle
| Befehl | Beschreibung | Nutzergruppe | | Befehl | Beschreibung | Nutzergruppe |
|-------------------------------------|---------------------------------------------------|----------------------| |-----------------------------------------------------------|--------------------------------------------------------|------------------|
| /ticket | Hilfe & Übersicht | Spieler, Support | | `/ticket` | Hilfe & Übersicht | Spieler, Support |
| /ticket create [Kategorie] [Priorität] <Text> | Ticket erstellen | Spieler | | `/ticket create [Kategorie] [Priorität] <Text>` | Ticket erstellen | Spieler |
| /ticket list | Eigene Tickets in der GUI anzeigen | Spieler | | `/ticket list` | Eigene Tickets in der GUI anzeigen | Spieler |
| /ticket comment <ID> <Nachricht> | Kommentar hinzufügen | Spieler, Support | | `/ticket comment <ID> <Nachricht>` | Kommentar hinzufügen | Spieler, Support |
| /ticket rate <ID> <good|bad> | Support bewerten | Spieler | | `/ticket rate <ID> <good\|bad>` | Support bewerten | Spieler |
| /ticket claim <ID> | Ticket annehmen | Support/Admin | | `/ticket faq` | FAQ-GUI öffnen (häufige Fragen) | Spieler |
| /ticket close <ID> [Kommentar] | Ticket schließen | Support/Admin | | `/ticket faq list` | FAQs im Chat auflisten | Spieler |
| /ticket forward <ID> <Spieler> | Ticket weiterleiten | Support/Admin | | `/ticket faq add <Frage> \| <Antwort>` | Neues FAQ hinzufügen | Admin |
| /ticket setpriority <ID> <low|normal|high|urgent> | Priorität ändern | Support/Admin | | `/ticket faq edit <ID> <Frage> \| <Antwort>` | Bestehendes FAQ bearbeiten | Admin |
| /ticket reload | Konfiguration neu laden | Support/Admin | | `/ticket faq delete <ID>` | FAQ löschen | Admin |
| /ticket stats | Statistiken anzeigen | Support/Admin | | `/ticket faq reload` | FAQs aus faqs.yml neu laden | Admin |
| /ticket archive | Tickets archivieren | Support/Admin | | `/ticket claim <ID>` | Ticket annehmen | Support/Admin |
| /ticket blacklist <add|remove|list> [Spieler] [Grund] | Blacklist verwalten | Support/Admin | | `/ticket close <ID> [Kommentar]` | Ticket schließen | Support/Admin |
| /ticket migrate <tomysql|tofile> | Speicherart wechseln | Support/Admin | | `/ticket forward <ID> <Spieler>` | Ticket weiterleiten | Support/Admin |
| /ticket export <Dateiname> | Tickets exportieren | Support/Admin | | `/ticket setpriority <ID> <low\|normal\|high\|urgent>` | Priorität ändern | Support/Admin |
| /ticket import <Dateiname> | Tickets importieren | Support/Admin | | `/ticket reload` | Konfiguration neu laden | Admin |
| /ticket teleport <ID> | Teleport zu Ticket (BungeeCord) | Support/Admin | | `/ticket stats` | Statistiken anzeigen | Admin |
| `/ticket archive` | Tickets archivieren | Admin |
| `/ticket blacklist <add\|remove\|list> [Spieler] [Grund]` | Blacklist verwalten | Admin |
| `/ticket migrate <tomysql\|tofile>` | Speicherart wechseln | Admin |
| `/ticket export <Dateiname>` | Tickets exportieren | Admin |
| `/ticket import <Dateiname>` | Tickets importieren | Admin |
| `/ticket teleport <ID>` | Teleport zu Ticket (BungeeCord) | Support/Admin |
### Rechte ### Rechte
| Permission | Beschreibung | Standard | | Permission | Beschreibung | Standard |
|-------------------|---------------------------------------------------|------------------| |-----------------|-----------------------------------------------------------------|------------------|
| ticket.create | Ticket erstellen | alle Spieler | | `ticket.create` | Ticket erstellen | alle Spieler |
| ticket.support | Tickets einsehen, claimen, schließen, Priorität | manuell vergeben | | `ticket.support`| Tickets einsehen, claimen, schließen, Priorität ändern | manuell vergeben |
| ticket.archive | Archiv öffnen, Tickets löschen | manuell vergeben | | `ticket.archive`| Archiv öffnen, Tickets permanent löschen | manuell vergeben |
| ticket.admin | Voller Zugriff (inkl. Weiterleitung, Reload, Blacklist) | OP | | `ticket.admin` | Voller Zugriff inkl. Weiterleitung, Reload, Blacklist, FAQ-Verwaltung | OP |
> ticket.archive ist nicht in ticket.admin enthalten und muss explizit vergeben werden. > `ticket.archive` ist nicht in `ticket.admin` enthalten und muss explizit vergeben werden.
---
## FAQ-System
Das FAQ-System ermöglicht es Admins, häufige Fragen und Antworten direkt im Spiel zu verwalten. Spieler können die FAQs per GUI oder Befehl einsehen.
### Für Spieler
```
/ticket faq öffnet die FAQ-GUI mit Custom-Skull-Items
/ticket faq list listet alle FAQs im Chat auf
```
In der GUI wird für jeden FAQ-Eintrag ein **Custom-Skull-Item** mit individueller Textur angezeigt. Ein Klick auf einen Eintrag zeigt die vollständige Antwort im Chat.
### Für Admins
```
/ticket faq add Wie melde ich einen Bug? | Nutze /ticket create bug <Beschreibung>.
/ticket faq edit 1 Neue Frage? | Neue Antwort.
/ticket faq delete 1
/ticket faq reload
```
Alternativ können FAQs auch direkt über die **Admin-FAQ-GUI** verwaltet werden (öffnet sich automatisch wenn `ticket.admin`-Berechtigung vorhanden). Ein Klick auf einen Eintrag öffnet eine Aktions-GUI mit den Optionen **Bearbeiten** und **Löschen**. Neue FAQs lassen sich ebenfalls per Schaltfläche in der GUI hinzufügen das Plugin führt den Admin Schritt für Schritt durch eine Chat-Eingabe.
### faqs.yml
Alle FAQs werden in einer eigenen Datei `plugins/TicketSystem/faqs.yml` gespeichert:
```yaml
faqs:
1:
question: "Wie erstelle ich ein Ticket?"
answer: "Nutze den Befehl /ticket create [Kategorie] [Beschreibung]."
2:
question: "Wie lange dauert die Bearbeitung?"
answer: "Unser Support-Team bearbeitet Tickets so schnell wie möglich."
```
Beim ersten Start werden automatisch vier Beispiel-FAQs erstellt.
---
## Performance-Caching
Ab dieser Version hält TicketSystem häufig abgerufene Tickets in einem **TTL-basierten In-Memory-Cache** vor. Das reduziert die Datenbankbelastung bei wiederholten Zugriffen (GUI, Kommentare, Bewertungen) deutlich.
- Standard-Lebenszeit: **60 Sekunden** (konfigurierbar: `cache-ttl-seconds` in `config.yml`)
- Der Cache wird bei Ticket-Änderungen (Claim, Close, Priorität usw.) automatisch invalidiert
- Regelmäßige Bereinigung abgelaufener Einträge alle 5 Minuten
- Der aktuelle Cache-Status ist in `/ticket stats` einsehbar
--- ---
## Kategorie & Priorität ## Kategorie & Priorität
Beim Erstellen eines Tickets können Kategorie und Priorität optional angegeben werden. Die folgende Tabelle zeigt die Möglichkeiten und Beispiele: Beim Erstellen eines Tickets können Kategorie und Priorität optional angegeben werden:
| Befehl | Kategorie | Priorität | | Befehl | Kategorie | Priorität |
|----------------------------------------|-------------|-----------| |-----------------------------------------|-----------|-----------|
| /ticket create <Text> | Standard | NORMAL | | `/ticket create <Text>` | Standard | NORMAL |
| /ticket create bug <Text> | Bug | NORMAL | | `/ticket create bug <Text>` | Bug | NORMAL |
| /ticket create high <Text> | Standard | HIGH | | `/ticket create high <Text>` | Standard | HIGH |
| /ticket create bug high <Text> | Bug | HIGH | | `/ticket create bug high <Text>` | Bug | HIGH |
| /ticket create question urgent <Text> | Frage | URGENT | | `/ticket create question urgent <Text>` | Frage | URGENT |
**Verfügbare Prioritäten:** **Verfügbare Prioritäten:** `low`, `normal`, `high`, `urgent` (auch deutsch: `niedrig`, `hoch`, `dringend`)
- low
- normal
- high
- urgent
(auch deutsch: niedrig, hoch, dringend) Kategorien und Aliases sind frei in der `config.yml` konfigurierbar.
**Kategorien und Aliases** sind frei in der config.yml konfigurierbar.
--- ---
@@ -104,8 +159,9 @@ Beim Erstellen eines Tickets können Kategorie und Priorität optional angegeben
- Rollen-Ping pro Nachrichtentyp - Rollen-Ping pro Nachrichtentyp
- Drei Ereignisse: neues Ticket, Ticket geschlossen, Ticket weitergeleitet - Drei Ereignisse: neues Ticket, Ticket geschlossen, Ticket weitergeleitet
Konfiguration in config.yml: Konfiguration in `config.yml`:
```yaml
discord: discord:
enabled: true enabled: true
webhook-url: "https://discord.com/api/webhooks/..." webhook-url: "https://discord.com/api/webhooks/..."
@@ -118,6 +174,7 @@ discord:
ticket-closed: ticket-closed:
enabled: true enabled: true
role-ping: false role-ping: false
```
--- ---
@@ -126,81 +183,71 @@ discord:
TicketSystem bietet volle Unterstützung für BungeeCord-Netzwerke: TicketSystem bietet volle Unterstützung für BungeeCord-Netzwerke:
- Tickets von jedem Server im Netzwerk - Tickets von jedem Server im Netzwerk
- Teleport zu Tickets auf anderen Servern (/ticket teleport <ID>) - Teleport zu Tickets auf anderen Servern (`/ticket teleport <ID>`)
- Tickets im Archiv und GUI serverübergreifend - Tickets im Archiv und GUI serverübergreifend
- Benachrichtigungen an alle Server - Benachrichtigungen an alle Server
- Discord-Webhooks zeigen Server-Namen - Discord-Webhooks zeigen Server-Namen
- Tickets an Supporter auf anderen Servern weiterleiten - Tickets an Supporter auf anderen Servern weiterleiten
- Teleport funktioniert auch zwischen Servern
**Voraussetzungen:** **Voraussetzungen:**
- spigot.yml: bungeecord: true - `spigot.yml`: `bungeecord: true`
- config.yml: bungeecord: true, server-name pro Server - `config.yml`: `bungeecord: true`, `server-name` pro Server
- TicketSystem.jar auf allen Spigot-Servern - TicketSystem.jar auf allen Spigot-Servern
- Alle Server nutzen dieselbe MySQL-Datenbank - Alle Server nutzen dieselbe MySQL-Datenbank
**Cross-Server-Befehle:**
- /ticket teleport <ID>
- /ticket forward <ID> <Spieler>
- /ticket archive
- /ticket list
**Tipps:**
- Server-Name erscheint in GUI & Discord
- Zielspieler muss online sein
- Funktionen auch im Einzelserver-Modus
- Bei Problemen: gleiche MySQL, Kanäle in plugin.yml prüfen
--- ---
## Vergleich mit anderen Plugins ## Vergleich mit anderen Plugins
TicketSystem hebt sich durch viele Alleinstellungsmerkmale von anderen Ticket-Plugins ab. Die folgende Tabelle zeigt die wichtigsten Unterschiede: | Feature | TicketSystem | SimpleTickets | AdvancedTickets |
|-----------------------------|:------------:|:-------------:|:---------------:|
| Speicher-Migration | ✔️ | ⚠️ | ✖️ |
| Automatische Backups | ✔️ | ⚠️ | ✖️ |
| GUI mit Kategorien | ✔️ | ⚠️ | ✖️ |
| Archivierung | ✔️ | ⚠️ | ✖️ |
| Rollenbasierter Archiv-Zugriff| ✔️ | ✖️ | ✖️ |
| Kategorie-System | ✔️ | ✖️ | ✖️ |
| Prioritäten-System | ✔️ | ✖️ | ✖️ |
| FAQ-System | ✔️ | ✖️ | ✖️ |
| Performance-Caching | ✔️ | ✖️ | ✖️ |
| Offline-Benachrichtigungen | ✔️ | ✖️ | ✖️ |
| Discord-Webhook | ✔️ | ✖️ | ✖️ |
| Bewertungs-System | ✔️ | ✖️ | ✖️ |
| Update-Checker | ✔️ | ✖️ | ✖️ |
| BungeeCord-Unterstützung | ✔️ | ✖️ | ✖️ |
| Feature | TicketSystem | SimpleTickets | AdvancedTickets | Legende: ✔️ Vollständige Unterstützung · ⚠️ Eingeschränkt · ✖️ Nicht vorhanden
|-------------------------|:------------:|:-------------:|:---------------:|
| Speicher-Migration | ✔️ | ⚠️ | ✖️ |
| Automatische Backups | ✔️ | ⚠️ | ✖️ |
| GUI mit Kategorien | ✔️ | ⚠️ | ✖️ |
| Archivierung | ✔️ | ⚠️ | ✖️ |
| Rollenbasierter Archiv | ✔️ | ✖️ | ✖️ |
| Kategorie-System | ✔️ | ✖️ | ✖️ |
| Prioritäten-System | ✔️ | ✖️ | ✖️ |
| Offline-Benachrichtigungen| ✔️ | ✖️ | ✖️ |
| Discord-Webhook | ✔️ | ✖️ | ✖️ |
| Bewertungs-System | ✔️ | ✖️ | ✖️ |
| Update-Checker | ✔️ | ✖️ | ✖️ |
| BungeeCord-Unterstützung | ✔️ | ✖️ | ✖️ |
Legende:
- ✔️ = Vollständige Unterstützung
- ⚠️ = Eingeschränkte oder fehleranfällige Unterstützung
- ✖️ = Nicht vorhanden
--- ---
## FAQ ## FAQ
**Kann ich zwischen MySQL und Datei-Speicherung wechseln?** **Kann ich zwischen MySQL und Datei-Speicherung wechseln?**
> Ja! Mit /ticket migrate tomysql oder /ticket migrate tofile werden alle Daten automatisch migriert. > Ja! Mit `/ticket migrate tomysql` oder `/ticket migrate tofile` werden alle Daten automatisch migriert.
**Wie konfiguriere ich eigene Kategorien?** **Wie konfiguriere ich eigene Kategorien?**
> In der config.yml unter categories: Name, Farbe, Material und Aliases frei wählbar. Änderungen mit /ticket reload übernehmen. > In der `config.yml` unter `categories:` Name, Farbe, Material und Aliases frei wählbar. Änderungen mit `/ticket reload` übernehmen.
**Wie verwalte ich FAQs?**
> Mit `/ticket faq` öffnest du die GUI. Als Admin kannst du über die GUI oder per `/ticket faq add|edit|delete` FAQs verwalten. Alle Daten liegen in `faqs.yml`.
**Was passiert mit Benachrichtigungen wenn ein Spieler offline ist?** **Was passiert mit Benachrichtigungen wenn ein Spieler offline ist?**
> Alle Benachrichtigungen werden gespeichert und beim nächsten Login angezeigt. > Alle Benachrichtigungen werden gespeichert und beim nächsten Login angezeigt.
**Wie ändere ich die Priorität eines Tickets?** **Wie ändere ich die Priorität eines Tickets?**
> Als Support/Admin per Befehl /ticket setpriority <ID> <Priorität> oder direkt in der GUI. > Als Support/Admin per `/ticket setpriority <ID> <Priorität>` oder direkt in der GUI.
**Wie aktiviere ich den Debug-Modus?** **Wie aktiviere ich den Debug-Modus?**
> debug: true in der config.yml setzen. > `debug: true` in der `config.yml` setzen.
**Wer darf das Ticket-Archiv sehen?** **Wer darf das Ticket-Archiv sehen?**
> Nur Spieler mit ticket.archive. Muss explizit vergeben werden. > Nur Spieler mit `ticket.archive`. Muss explizit vergeben werden.
**Wie funktioniert Teleport bei BungeeCord?** **Wie funktioniert Teleport bei BungeeCord?**
> Mit /ticket teleport <ID> wirst du automatisch auf den richtigen Server und zur Ticket-Position teleportiert. > Mit `/ticket teleport <ID>` wirst du automatisch auf den richtigen Server und zur Ticket-Position teleportiert.
**Wie lange werden Tickets gecacht?**
> Standardmäßig 60 Sekunden. Über `cache-ttl-seconds` in der `config.yml` anpassbar. Der Cache wird bei Änderungen sofort invalidiert.
--- ---

View File

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

View File

@@ -1,12 +1,15 @@
package de.ticketsystem; package de.ticketsystem;
import de.ticketsystem.bungee.BungeeMessenger; import de.ticketsystem.bungee.BungeeMessenger;
import de.ticketsystem.cache.TicketCache;
import de.ticketsystem.commands.TicketCommand; import de.ticketsystem.commands.TicketCommand;
import de.ticketsystem.database.DatabaseManager; import de.ticketsystem.database.DatabaseManager;
import de.ticketsystem.discord.DiscordWebhook; import de.ticketsystem.discord.DiscordWebhook;
import de.ticketsystem.gui.FaqGUI;
import de.ticketsystem.gui.TicketGUI; import de.ticketsystem.gui.TicketGUI;
import de.ticketsystem.listeners.PlayerJoinListener; import de.ticketsystem.listeners.PlayerJoinListener;
import de.ticketsystem.manager.CategoryManager; import de.ticketsystem.manager.CategoryManager;
import de.ticketsystem.manager.FaqManager;
import de.ticketsystem.manager.TicketManager; import de.ticketsystem.manager.TicketManager;
import de.ticketsystem.model.Ticket; import de.ticketsystem.model.Ticket;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
@@ -23,16 +26,18 @@ public class TicketPlugin extends JavaPlugin {
/** /**
* Name dieses Servers im BungeeCord-Netzwerk. * Name dieses Servers im BungeeCord-Netzwerk.
* Konfigurierbar in config.yml → server-name * Konfigurierbar in config.yml → server-name
* Wird in Tickets gespeichert und in Benachrichtigungen angezeigt.
*/ */
private String serverName; private String serverName;
private DatabaseManager databaseManager; private DatabaseManager databaseManager;
private TicketManager ticketManager; private TicketManager ticketManager;
private CategoryManager categoryManager; private CategoryManager categoryManager;
private FaqManager faqManager;
private TicketGUI ticketGUI; private TicketGUI ticketGUI;
private FaqGUI faqGUI;
private DiscordWebhook discordWebhook; private DiscordWebhook discordWebhook;
private BungeeMessenger bungeeMessenger; private BungeeMessenger bungeeMessenger;
private TicketCache ticketCache;
@Override @Override
public void onEnable() { public void onEnable() {
@@ -44,9 +49,7 @@ public class TicketPlugin extends JavaPlugin {
Ticket.register(); Ticket.register();
// ── BungeeCord Plugin-Messaging-Kanäle registrieren ─────────────── // ── BungeeCord Plugin-Messaging-Kanäle registrieren ───────────────
// Ausgehend: BungeeCord-Standardkanal (für Forward / Message)
getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.BUNGEE_CHANNEL); getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.BUNGEE_CHANNEL);
// Eingehend & Ausgehend: Eigener Kanal für Team- und Spielerbenachrichtigungen
getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.CUSTOM_CHANNEL); getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.CUSTOM_CHANNEL);
bungeeMessenger = new BungeeMessenger(this); bungeeMessenger = new BungeeMessenger(this);
@@ -55,67 +58,65 @@ public class TicketPlugin extends JavaPlugin {
// Server-Name aus Config lesen // Server-Name aus Config lesen
serverName = getConfig().getString("server-name", "unknown"); serverName = getConfig().getString("server-name", "unknown");
if ("unknown".equals(serverName)) { if ("unknown".equals(serverName)) {
getLogger().warning("[BungeeCord] Kein 'server-name' in der config.yml definiert! " + getLogger().warning("[BungeeCord] Kein 'server-name' in der config.yml definiert!");
"Setze 'server-name: dein-server' für korrekte Cross-Server-Anzeige.");
} else {
getLogger().info("[BungeeCord] Server-Name: §e" + serverName);
} }
// BungeeCord-Hinweis prüfen // BungeeCord-Hinweis nur bei deaktiviertem Feature ausgeben
if (!getConfig().getBoolean("bungeecord", false)) { if (!getConfig().getBoolean("bungeecord", false)) {
getLogger().info("[BungeeCord] Hinweis: Cross-Server-Features sind deaktiviert. " + getLogger().info("[BungeeCord] Cross-Server-Features deaktiviert. Setze 'bungeecord: true' um sie zu aktivieren.");
"Setze 'bungeecord: true' in der config.yml und stelle sicher, " +
"dass 'bungeecord: true' auch in spigot.yml gesetzt ist.");
} else {
getLogger().info("[BungeeCord] Cross-Server-Benachrichtigungen aktiviert.");
} }
// Update-Checker // Update-Checker (nur Warnung wenn Update verfügbar kein API-Raw-Log)
int resourceId = 132757; int resourceId = 132757;
new UpdateChecker(this, resourceId).getVersion(version -> { new UpdateChecker(this, resourceId).getVersion(version -> {
String current = getDescription().getVersion(); String current = getDescription().getVersion();
if (!current.equals(version)) { if (!current.equals(version)) {
String msg = ChatColor.translateAlternateColorCodes('&', String msg = ChatColor.translateAlternateColorCodes('&',
"&6[TicketSystem] &eEs ist eine neue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")"); "&6[TicketSystem] &eNeue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")");
getLogger().info("Es ist eine neue Version verfügbar: " + version + " (aktuell: " + current + ")"); getLogger().warning("Neue Version verfügbar: " + version + " (aktuell: " + current + ")");
getServer().getScheduler().runTaskLater(this, () -> { getServer().getScheduler().runTaskLater(this, () ->
getServer().getOnlinePlayers().stream() getServer().getOnlinePlayers().stream()
.filter(p -> p.hasPermission("ticket.admin")) .filter(p -> p.hasPermission("ticket.admin"))
.forEach(p -> p.sendMessage(msg)); .forEach(p -> p.sendMessage(msg)), 20L);
}, 20L);
} else {
getLogger().info("TicketSystem ist aktuell (Version " + current + ")");
} }
}); });
// Versionsprüfung // Versionsprüfung der config.yml
String configVersion = getConfig().getString("version", ""); String configVersion = getConfig().getString("version", "");
String expectedVersion = "2.0"; String expectedVersion = "2.0";
if (!expectedVersion.equals(configVersion)) { if (!expectedVersion.equals(configVersion)) {
getLogger().warning("[WARNUNG] Die Version deiner config.yml (" + configVersion getLogger().warning("[WARNUNG] config.yml-Version (" + configVersion
+ ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!"); + ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!");
} }
debug = getConfig().getBoolean("debug", false); debug = getConfig().getBoolean("debug", false);
// ── Performance: Ticket-Cache ──────────────────────────────────────
long cacheTtl = getConfig().getLong("cache-ttl-seconds", 60) * 1000L;
ticketCache = new TicketCache(cacheTtl);
// Regelmäßige Cache-Bereinigung alle 5 Minuten
getServer().getScheduler().runTaskTimerAsynchronously(this,
() -> ticketCache.evictExpired(), 6000L, 6000L);
// Datenbankverbindung // Datenbankverbindung
databaseManager = new DatabaseManager(this); databaseManager = new DatabaseManager(this);
if (!databaseManager.connect()) { if (!databaseManager.connect()) {
getLogger().severe("Konnte keine Datenbankverbindung herstellen! Plugin läuft im Datei-Modus weiter."); getLogger().severe("Konnte keine Datenbankverbindung herstellen! Plugin läuft im Datei-Modus weiter.");
} }
// Manager, GUI & Discord-Webhook initialisieren // Manager, GUI, FAQ & Discord-Webhook initialisieren
categoryManager = new CategoryManager(this); categoryManager = new CategoryManager(this);
ticketManager = new TicketManager(this); ticketManager = new TicketManager(this);
faqManager = new FaqManager(this);
ticketGUI = new TicketGUI(this); ticketGUI = new TicketGUI(this);
faqGUI = new FaqGUI(this);
discordWebhook = new DiscordWebhook(this); discordWebhook = new DiscordWebhook(this);
if (getConfig().getBoolean("discord.enabled", false)) { if (getConfig().getBoolean("discord.enabled", false)) {
String url = getConfig().getString("discord.webhook-url", ""); String url = getConfig().getString("discord.webhook-url", "");
if (url.isEmpty()) { if (url.isEmpty()) {
getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in der config.yml eingetragen!"); getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in config.yml eingetragen!");
} else {
getLogger().info("[DiscordWebhook] Integration aktiv.");
} }
} }
@@ -126,6 +127,7 @@ public class TicketPlugin extends JavaPlugin {
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
getServer().getPluginManager().registerEvents(ticketGUI, this); getServer().getPluginManager().registerEvents(ticketGUI, this);
getServer().getPluginManager().registerEvents(faqGUI, this);
// Automatische Archivierung // Automatische Archivierung
int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24); int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24);
@@ -137,18 +139,17 @@ public class TicketPlugin extends JavaPlugin {
getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert."); getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert.");
} }
}, ticks, ticks); }, ticks, ticks);
getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert.");
} }
getLogger().info("TicketSystem erfolgreich gestartet!"); getLogger().info("TicketSystem v" + getDescription().getVersion() + " erfolgreich gestartet!");
} }
@Override @Override
public void onDisable() { public void onDisable() {
// Plugin-Messaging-Kanäle abmelden
getServer().getMessenger().unregisterOutgoingPluginChannel(this); getServer().getMessenger().unregisterOutgoingPluginChannel(this);
getServer().getMessenger().unregisterIncomingPluginChannel(this); getServer().getMessenger().unregisterIncomingPluginChannel(this);
if (ticketCache != null) ticketCache.clear();
if (databaseManager != null) databaseManager.disconnect(); if (databaseManager != null) databaseManager.disconnect();
getLogger().info("TicketSystem wurde deaktiviert."); getLogger().info("TicketSystem wurde deaktiviert.");
} }
@@ -171,20 +172,13 @@ public class TicketPlugin extends JavaPlugin {
public DatabaseManager getDatabaseManager() { return databaseManager; } public DatabaseManager getDatabaseManager() { return databaseManager; }
public TicketManager getTicketManager() { return ticketManager; } public TicketManager getTicketManager() { return ticketManager; }
public CategoryManager getCategoryManager() { return categoryManager; } public CategoryManager getCategoryManager() { return categoryManager; }
public FaqManager getFaqManager() { return faqManager; }
public TicketGUI getTicketGUI() { return ticketGUI; } public TicketGUI getTicketGUI() { return ticketGUI; }
public FaqGUI getFaqGUI() { return faqGUI; }
public DiscordWebhook getDiscordWebhook() { return discordWebhook; } public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
public BungeeMessenger getBungeeMessenger() { return bungeeMessenger; } public BungeeMessenger getBungeeMessenger() { return bungeeMessenger; }
public TicketCache getTicketCache() { return ticketCache; }
public boolean isDebug() { return debug; } public boolean isDebug() { return debug; }
/**
* BungeeCord: Gibt den konfigurierten Server-Namen zurück.
* Entspricht dem Wert aus config.yml → server-name.
*/
public String getServerName() { return serverName; } public String getServerName() { return serverName; }
/**
* BungeeCord: Gibt zurück ob Cross-Server-Features aktiviert sind.
* Entspricht config.yml → bungeecord: true
*/
public boolean isBungeeCordEnabled() { return getConfig().getBoolean("bungeecord", false); } public boolean isBungeeCordEnabled() { return getConfig().getBoolean("bungeecord", false); }
} }

View File

@@ -10,31 +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()) {
String latest = scann.next(); String spigotVersion = scann.next().trim();
plugin.getLogger().info("[UpdateChecker] Spigot-API Rückgabe: '" + latest + "'"); String localVersion = plugin.getDescription().getVersion().trim();
consumer.accept(latest);
} else { // Nur melden wenn Spigot-Version wirklich neuer ist
plugin.getLogger().warning("[UpdateChecker] Keine Version von Spigot erhalten!"); if (isNewerVersion(spigotVersion, localVersion)) {
consumer.accept(spigotVersion);
}
} }
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().info("Unable to check for updates: " + e.getMessage()); // Netzwerkfehler schweigen kein Spam in der Konsole
if (((TicketPlugin) plugin).isDebug()) {
plugin.getLogger().info("[UpdateChecker] Konnte nicht prüfen: " + e.getMessage());
}
} }
}); });
} }
}
/**
* Vergleicht zwei semantische Versionen (z.B. "1.0.5" und "1.0.6").
*
* @param spigot Version von SpigotMC
* @param local Lokale Plugin-Version
* @return true wenn spigot NEUER ist als local
*/
private boolean isNewerVersion(String spigot, String local) {
try {
String[] spigotParts = spigot.split("\\.");
String[] localParts = local.split("\\.");
int length = Math.max(spigotParts.length, localParts.length);
for (int i = 0; i < length; i++) {
int s = i < spigotParts.length ? Integer.parseInt(spigotParts[i]) : 0;
int l = i < localParts.length ? Integer.parseInt(localParts[i]) : 0;
if (s > l) return true; // Spigot ist neuer
if (s < l) return false; // Lokal ist neuer (z.B. noch nicht veröffentlicht)
}
return false; // Versionen identisch
} catch (NumberFormatException e) {
// Fallback: einfacher String-Vergleich falls Format ungewöhnlich
return !spigot.equals(local);
}
}
}

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

View File

@@ -1,6 +1,7 @@
package de.ticketsystem.commands; package de.ticketsystem.commands;
import de.ticketsystem.TicketPlugin; import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.FaqEntry;
import de.ticketsystem.model.Ticket; import de.ticketsystem.model.Ticket;
import de.ticketsystem.manager.CategoryManager; import de.ticketsystem.manager.CategoryManager;
@@ -35,27 +36,152 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
if (args.length == 0) { plugin.getTicketManager().sendHelpMessage(player); return true; } if (args.length == 0) { plugin.getTicketManager().sendHelpMessage(player); return true; }
switch (args[0].toLowerCase()) { switch (args[0].toLowerCase()) {
case "create" -> handleCreate(player, args); case "create" -> handleCreate(player, args);
case "list" -> handleList(player); case "list" -> handleList(player);
case "claim" -> handleClaim(player, args); case "claim" -> handleClaim(player, args);
case "close" -> handleClose(player, args); case "close" -> handleClose(player, args);
case "forward" -> handleForward(player, args); case "forward" -> handleForward(player, args);
case "reload" -> handleReload(player); case "reload" -> handleReload(player);
case "migrate" -> handleMigrate(player, args); case "migrate" -> handleMigrate(player, args);
case "export" -> handleExport(player, args); case "export" -> handleExport(player, args);
case "import" -> handleImport(player, args); case "import" -> handleImport(player, args);
case "stats" -> handleStats(player); case "stats" -> handleStats(player);
case "top" -> handleTop(player); case "top" -> handleTop(player);
case "archive" -> handleArchive(player); case "archive" -> handleArchive(player);
case "comment" -> handleComment(player, args); case "comment" -> handleComment(player, args);
case "blacklist" -> handleBlacklist(player, args); case "blacklist" -> handleBlacklist(player, args);
case "rate" -> handleRate(player, args); case "rate" -> handleRate(player, args);
case "setpriority" -> handleSetPriority(player, args); case "setpriority" -> handleSetPriority(player, args);
default -> plugin.getTicketManager().sendHelpMessage(player); case "faq" -> handleFaq(player, args);
default -> plugin.getTicketManager().sendHelpMessage(player);
} }
return true; return true;
} }
// ─────────────────────────── /ticket faq ───────────────────────────────
/**
* /ticket faq öffnet die FAQ-GUI (alle Spieler)
* /ticket faq add <Frage> | <Antwort> fügt ein FAQ hinzu (ticket.admin)
* /ticket faq edit <ID> <Frage> | <Antwort> bearbeitet ein FAQ (ticket.admin)
* /ticket faq delete <ID> löscht ein FAQ (ticket.admin)
* /ticket faq reload lädt FAQs neu (ticket.admin)
*/
private void handleFaq(Player player, String[] args) {
// Kein Subbefehl → GUI öffnen
if (args.length == 1) {
plugin.getFaqGUI().openFaqGUI(player);
return;
}
switch (args[1].toLowerCase()) {
// ── /ticket faq add <Frage> | <Antwort> ────────────────────────
case "add" -> {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
}
if (args.length < 3) {
player.sendMessage(plugin.color("&cBenutzung: /ticket faq add <Frage> | <Antwort>"));
player.sendMessage(plugin.color("&7Beispiel: &e/ticket faq add Wie erstelle ich ein Ticket? | Nutze /ticket create."));
return;
}
String full = String.join(" ", Arrays.copyOfRange(args, 2, args.length));
String[] parts = full.split("\\s*\\|\\s*", 2);
if (parts.length < 2 || parts[0].isBlank() || parts[1].isBlank()) {
player.sendMessage(plugin.color("&cTrenne Frage und Antwort mit &e|&c, z.B.:"));
player.sendMessage(plugin.color("&e/ticket faq add Wie erstelle ich ein Ticket? | Nutze /ticket create."));
return;
}
FaqEntry created = plugin.getFaqManager().add(parts[0].trim(), parts[1].trim());
player.sendMessage(plugin.color("&aFAQ &e#" + created.getId() + " &awurde erfolgreich erstellt!"));
player.sendMessage(plugin.color("&7Frage: &e" + created.getQuestion()));
player.sendMessage(plugin.color("&7Antwort: &f" + created.getAnswer()));
}
// ── /ticket faq edit <ID> <Frage> | <Antwort> ──────────────────
case "edit" -> {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
}
if (args.length < 4) {
player.sendMessage(plugin.color("&cBenutzung: /ticket faq edit <ID> <Frage> | <Antwort>"));
return;
}
int id;
try { id = Integer.parseInt(args[2]); }
catch (NumberFormatException e) {
player.sendMessage(plugin.color("&cUngültige FAQ-ID: &e" + args[2])); return;
}
String full = String.join(" ", Arrays.copyOfRange(args, 3, args.length));
String[] parts = full.split("\\s*\\|\\s*", 2);
if (parts.length < 2 || parts[0].isBlank() || parts[1].isBlank()) {
player.sendMessage(plugin.color("&cTrenne Frage und Antwort mit &e|&c."));
return;
}
boolean ok = plugin.getFaqManager().edit(id, parts[0].trim(), parts[1].trim());
if (ok) player.sendMessage(plugin.color("&aFAQ &e#" + id + " &awurde erfolgreich aktualisiert!"));
else player.sendMessage(plugin.color("&cFAQ &e#" + id + " &cwurde nicht gefunden."));
}
// ── /ticket faq delete <ID> ─────────────────────────────────────
case "delete", "remove" -> {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
}
if (args.length < 3) {
player.sendMessage(plugin.color("&cBenutzung: /ticket faq delete <ID>")); return;
}
int id;
try { id = Integer.parseInt(args[2]); }
catch (NumberFormatException e) {
player.sendMessage(plugin.color("&cUngültige FAQ-ID: &e" + args[2])); return;
}
boolean ok = plugin.getFaqManager().delete(id);
if (ok) player.sendMessage(plugin.color("&aFAQ &e#" + id + " &awurde gelöscht."));
else player.sendMessage(plugin.color("&cFAQ &e#" + id + " &cwurde nicht gefunden."));
}
// ── /ticket faq reload ──────────────────────────────────────────
case "reload" -> {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
}
plugin.getFaqManager().reload();
player.sendMessage(plugin.color("&aFAQs wurden neu geladen. ("
+ plugin.getFaqManager().getAll().size() + " Einträge)"));
}
// ── /ticket faq list ────────────────────────────────────────────
case "list" -> {
List<FaqEntry> all = plugin.getFaqManager().getAll();
player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6Häufige Fragen (FAQ) &7— " + all.size() + " Einträge"));
player.sendMessage(plugin.color("&8&m "));
if (all.isEmpty()) {
player.sendMessage(plugin.color("&7Noch keine FAQs vorhanden."));
} else {
for (FaqEntry e : all) {
player.sendMessage(plugin.color("&e#" + e.getId() + " &f" + e.getQuestion()));
player.sendMessage(plugin.color(" &7→ &f" + e.getAnswer()));
}
}
player.sendMessage(plugin.color("&8&m "));
if (player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.color("&7Befehle: &e/ticket faq add &8| &e/ticket faq edit <ID> &8| &e/ticket faq delete <ID>"));
}
}
default -> {
player.sendMessage(plugin.color("&cUnbekannter FAQ-Befehl."));
player.sendMessage(plugin.color("&7Benutze &e/ticket faq &7zum Öffnen der GUI."));
if (player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.color("&7Admin-Befehle: &e/ticket faq add | edit | delete | reload | list"));
}
}
}
}
// ─────────────────────────── /ticket create ──────────────────────────── // ─────────────────────────── /ticket create ────────────────────────────
private void handleCreate(Player player, String[] args) { private void handleCreate(Player player, String[] args) {
@@ -63,7 +189,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
player.sendMessage(plugin.formatMessage("messages.no-permission")); return; player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
} }
// Blacklist-Check
if (plugin.getDatabaseManager().isBlacklisted(player.getUniqueId())) { if (plugin.getDatabaseManager().isBlacklisted(player.getUniqueId())) {
player.sendMessage(plugin.color("&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen.")); player.sendMessage(plugin.color("&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen."));
return; return;
@@ -91,7 +216,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
return; return;
} }
// Kategorie und Priorität optional parsen
CategoryManager cm = plugin.getCategoryManager(); CategoryManager cm = plugin.getCategoryManager();
ConfigCategory category = cm.getDefault(); ConfigCategory category = cm.getDefault();
TicketPriority priority = TicketPriority.NORMAL; TicketPriority priority = TicketPriority.NORMAL;
@@ -147,13 +271,14 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation()); Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation());
ticket.setCategoryKey(finalCategory.getKey()); ticket.setCategoryKey(finalCategory.getKey());
ticket.setPriority(finalPriority); ticket.setPriority(finalPriority);
// BungeeCord: Server-Name des erstellenden Servers speichern
ticket.setServerName(plugin.getServerName()); ticket.setServerName(plugin.getServerName());
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int id = plugin.getDatabaseManager().createTicket(ticket); int id = plugin.getDatabaseManager().createTicket(ticket);
if (id == -1) { player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets!")); return; } if (id == -1) { player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets!")); return; }
ticket.setId(id); ticket.setId(id);
// Cache befüllen
plugin.getTicketCache().put(ticket);
plugin.getTicketManager().setCooldown(player.getUniqueId()); plugin.getTicketManager().setCooldown(player.getUniqueId());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
String catInfo = plugin.getConfig().getBoolean("categories-enabled", true) String catInfo = plugin.getConfig().getBoolean("categories-enabled", true)
@@ -194,14 +319,14 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
boolean success = plugin.getDatabaseManager().claimTicket(ticketId, player.getUniqueId(), player.getName()); boolean success = plugin.getDatabaseManager().claimTicket(ticketId, player.getUniqueId(), player.getName());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; } if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; }
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); Ticket ticket = getCachedOrFetch(ticketId);
if (ticket == null) return; if (ticket == null) return;
player.sendMessage(plugin.formatMessage("messages.ticket-claimed") player.sendMessage(plugin.formatMessage("messages.ticket-claimed")
.replace("{id}", String.valueOf(ticketId)) .replace("{id}", String.valueOf(ticketId))
.replace("{player}", ticket.getCreatorName())); .replace("{player}", ticket.getCreatorName()));
plugin.getTicketManager().notifyCreatorClaimed(ticket); plugin.getTicketManager().notifyCreatorClaimed(ticket);
// Teleport beim Annehmen entfernt Teleport nur noch über das GUI-Item möglich. plugin.getTicketCache().invalidate(ticketId); // Stale cache löschen
}); });
}); });
} }
@@ -224,10 +349,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment); boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment);
if (success) { if (success) {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); Ticket ticket = getCachedOrFetch(ticketId);
// Ticket in persistente Stats-Tabelle eintragen (bleibt auch nach Löschung erhalten).
// player.getName() = der Admin der /ticket close ausgeführt hat nicht zwingend der Claimer.
if (ticket != null) plugin.getDatabaseManager().recordClosedTicket(ticket, player.getName()); if (ticket != null) plugin.getDatabaseManager().recordClosedTicket(ticket, player.getName());
plugin.getTicketCache().invalidate(ticketId);
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId))); player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId)));
if (ticket != null) { if (ticket != null) {
@@ -252,29 +376,26 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
try { id = Integer.parseInt(args[1]); } try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
// BungeeCord: Ziel-Spieler lokal suchen
Player localTarget = Bukkit.getPlayer(args[2]); Player localTarget = Bukkit.getPlayer(args[2]);
if (localTarget == null) { if (localTarget == null) {
if (plugin.isBungeeCordEnabled()) { if (plugin.isBungeeCordEnabled()) {
player.sendMessage(plugin.color("&7[BungeeCord] Spieler &e" + args[2] player.sendMessage(plugin.color("&7[BungeeCord] Spieler &e" + args[2] + " &7ist auf diesem Server nicht online."));
+ " &7ist auf diesem Server nicht online."));
player.sendMessage(plugin.color("&7Tipp: Forwarden geht nur zu Spielern auf &bdemselben Server&7."));
} else { } else {
player.sendMessage(plugin.color("&cSpieler nicht gefunden!")); player.sendMessage(plugin.color("&cSpieler nicht gefunden!"));
} }
return; return;
} }
final int ticketId = id; final int ticketId = id;
final String fromName = player.getName(); final String fromName = player.getName();
final Player t = localTarget; final Player t = localTarget;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().forwardTicket(ticketId, t.getUniqueId(), t.getName()); boolean success = plugin.getDatabaseManager().forwardTicket(ticketId, t.getUniqueId(), t.getName());
plugin.getTicketCache().invalidate(ticketId);
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (!success) { player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); return; } if (!success) { player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); return; }
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); Ticket ticket = getCachedOrFetch(ticketId);
if (ticket == null) return; if (ticket == null) return;
player.sendMessage(plugin.color("&aTicket &e#" + ticketId + " &awurde an &e" + t.getName() + " &aweitergeleitet.")); player.sendMessage(plugin.color("&aTicket &e#" + ticketId + " &awurde an &e" + t.getName() + " &aweitergeleitet."));
plugin.getTicketManager().notifyForwardedTo(ticket, fromName); plugin.getTicketManager().notifyForwardedTo(ticket, fromName);
@@ -297,17 +418,17 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
String msg = String.join(" ", Arrays.copyOfRange(args, 2, args.length)); String msg = String.join(" ", Arrays.copyOfRange(args, 2, args.length));
if (msg.length() > 500) { player.sendMessage(plugin.color("&cNachricht zu lang! Maximal 500 Zeichen.")); return; } if (msg.length() > 500) { player.sendMessage(plugin.color("&cNachricht zu lang! Maximal 500 Zeichen.")); return; }
final int ticketId = id; final int ticketId = id;
final String message = msg; final String message = msg;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); Ticket ticket = getCachedOrFetch(ticketId);
if (ticket == null) { if (ticket == null) {
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
return; return;
} }
boolean isOwner = ticket.getCreatorUUID().equals(player.getUniqueId()); boolean isOwner = ticket.getCreatorUUID().equals(player.getUniqueId());
boolean isStaff = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"); boolean isStaff = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin");
if (!isOwner && !isStaff) { if (!isOwner && !isStaff) {
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu kannst nur deine eigenen Tickets kommentieren."))); Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu kannst nur deine eigenen Tickets kommentieren.")));
return; return;
@@ -327,59 +448,27 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
}); });
} }
/**
* Benachrichtigt alle relevanten Empfänger über einen neuen Kommentar.
*
* ── BUG FIX #2 ──────────────────────────────────────────────────────────
* Vorher: broadcastTeamNotification() wurde am Ende ZUSÄTZLICH aufgerufen
* obwohl alle lokalen Supporter bereits einzeln per Schleife
* benachrichtigt wurden. Das führte zu:
* a) Doppelter Nachricht für lokale Supporter
* b) broadcastTeamNotification() sendet intern ebenfalls lokal →
* lokale Supporter sahen die Nachricht dreifach
* c) Das Forward-Paket an andere Server war korrekt, aber die
* Empfänger auf anderen Servern sahen auch Duplikate da
* broadcastTeamNotification() wiederum lokal sendet
*
* Fix: broadcastTeamNotification() ERSETZT die lokale Supporter-Schleife
* komplett. Die Methode sendet bereits lokal direkt und forwardet
* gleichzeitig an alle anderen BungeeCord-Server.
* Im Standalone-Modus bleibt die lokale Schleife erhalten.
* ────────────────────────────────────────────────────────────────────────
*/
private void notifyCommentReceivers(Player author, Ticket ticket, String message) { private void notifyCommentReceivers(Player author, Ticket ticket, String message) {
String onlineMsg = plugin.color("&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert: &f" + message); String onlineMsg = plugin.color("&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert: &f" + message);
String offlineMsg = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert (während du offline warst): &f" + message; String offlineMsg = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert (während du offline warst): &f" + message;
// ── 1. Ticket-Ersteller benachrichtigen (wenn nicht der Autor selbst) ──
if (!ticket.getCreatorUUID().equals(author.getUniqueId())) { if (!ticket.getCreatorUUID().equals(author.getUniqueId())) {
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
if (creator != null && creator.isOnline()) { if (creator != null && creator.isOnline()) {
creator.sendMessage(onlineMsg); creator.sendMessage(onlineMsg);
} else if (plugin.isBungeeCordEnabled()) { } else if (plugin.isBungeeCordEnabled()) {
// BungeeCord: Zustellung via Plugin-Messaging, kein Pending-Eintrag plugin.getBungeeMessenger().sendMessageToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), onlineMsg);
// (PlayerJoinListener übernimmt Offline-Fallback via close_notified-Logik)
plugin.getBungeeMessenger().sendMessageToPlayer(
ticket.getCreatorUUID(), ticket.getCreatorName(), onlineMsg);
} else { } else {
// Standalone: Offline → für nächsten Login speichern
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), offlineMsg)); plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), offlineMsg));
} }
} }
// ── 2. Supporter/Admin benachrichtigen (wenn Kommentar vom Spieler kommt) ──
if (!author.hasPermission("ticket.support") && !author.hasPermission("ticket.admin")) { if (!author.hasPermission("ticket.support") && !author.hasPermission("ticket.admin")) {
if (plugin.isBungeeCordEnabled()) { if (plugin.isBungeeCordEnabled()) {
// BungeeCord-Modus: broadcastTeamNotification() übernimmt ALLES
// lokal direkt + Forward an alle anderen Server in einem Paket.
// KEINE zusätzliche lokale Schleife, da das zu Duplikaten führt.
plugin.getBungeeMessenger().broadcastTeamNotification(onlineMsg); plugin.getBungeeMessenger().broadcastTeamNotification(onlineMsg);
} else { } else {
// Standalone-Modus: Claimer gezielt benachrichtigen var claimerUUID = ticket.getClaimerUUID();
UUID claimerUUID = ticket.getClaimerUUID();
if (claimerUUID != null && !claimerUUID.equals(author.getUniqueId())) { if (claimerUUID != null && !claimerUUID.equals(author.getUniqueId())) {
Player claimer = Bukkit.getPlayer(claimerUUID); Player claimer = Bukkit.getPlayer(claimerUUID);
if (claimer != null && claimer.isOnline()) { if (claimer != null && claimer.isOnline()) {
@@ -391,8 +480,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
plugin.getDatabaseManager().addPendingNotification(claimerUUID, claimerOffline)); plugin.getDatabaseManager().addPendingNotification(claimerUUID, claimerOffline));
} }
} }
// Alle anderen Online-Supporter auf diesem Server informieren
for (Player p : Bukkit.getOnlinePlayers()) { for (Player p : Bukkit.getOnlinePlayers()) {
if (p.getUniqueId().equals(author.getUniqueId())) continue; if (p.getUniqueId().equals(author.getUniqueId())) continue;
if (claimerUUID != null && p.getUniqueId().equals(claimerUUID)) continue; if (claimerUUID != null && p.getUniqueId().equals(claimerUUID)) continue;
@@ -426,11 +513,11 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
return; return;
} }
final int ticketId = id; final int ticketId = id;
final String finalRating = rating; final String finalRating = rating;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); Ticket ticket = getCachedOrFetch(ticketId);
if (ticket == null) { if (ticket == null) {
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
return; return;
@@ -445,6 +532,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
} }
boolean success = plugin.getDatabaseManager().rateTicket(ticketId, finalRating); boolean success = plugin.getDatabaseManager().rateTicket(ticketId, finalRating);
plugin.getTicketCache().invalidate(ticketId);
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (success) { if (success) {
String emoji = "THUMBS_UP".equals(finalRating) ? "§a👍 Positiv" : "§c👎 Negativ"; String emoji = "THUMBS_UP".equals(finalRating) ? "§a👍 Positiv" : "§c👎 Negativ";
@@ -478,27 +566,22 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
if (target.getUniqueId() == null) { player.sendMessage(plugin.color("&cSpieler nicht gefunden.")); return; } if (target.getUniqueId() == null) { player.sendMessage(plugin.color("&cSpieler nicht gefunden.")); return; }
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().addBlacklist( boolean success = plugin.getDatabaseManager().addBlacklist(target.getUniqueId(), targetName, reason, player.getName());
target.getUniqueId(), targetName, reason, player.getName());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (success) if (success) player.sendMessage(plugin.color("&a" + targetName + " &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e" + reason));
player.sendMessage(plugin.color("&a" + targetName + " &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e" + reason)); else player.sendMessage(plugin.color("&cSpieler ist bereits auf der Blacklist."));
else
player.sendMessage(plugin.color("&cSpieler ist bereits auf der Blacklist."));
}); });
}); });
} }
case "remove" -> { case "remove" -> {
if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist remove <Spieler>")); return; } if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist remove <Spieler>")); return; }
String targetName = args[2];
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); OfflinePlayer target = Bukkit.getOfflinePlayer(args[2]);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().removeBlacklist(target.getUniqueId()); boolean success = plugin.getDatabaseManager().removeBlacklist(target.getUniqueId());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (success) player.sendMessage(plugin.color("&a" + targetName + " &awurde von der Blacklist entfernt.")); if (success) player.sendMessage(plugin.color("&a" + args[2] + " &awurde von der Blacklist entfernt."));
else player.sendMessage(plugin.color("&cSpieler war nicht auf der Blacklist.")); else player.sendMessage(plugin.color("&cSpieler war nicht auf der Blacklist."));
}); });
}); });
} }
@@ -527,16 +610,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
// ─────────────────────────── /ticket top ────────────────────────────── // ─────────────────────────── /ticket top ──────────────────────────────
/**
* Zeigt das Leaderboard der Top-5 Ticket-Ersteller.
* Basiert auf der ticket_creator_stats-Tabelle, die Werte auch nach
* dem Löschen oder Archivieren von Tickets beibehält.
* Berechtigung: ticket.create (alle Spieler)
*/
private void handleTop(Player player) { private void handleTop(Player player) {
if (!player.hasPermission("ticket.create") && !player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.create") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
return;
} }
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
List<String[]> top = plugin.getDatabaseManager().getTopCreators(5); List<String[]> top = plugin.getDatabaseManager().getTopCreators(5);
@@ -553,8 +629,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
String medal = rankIdx < medals.length ? medals[rankIdx] : "&7#" + entry[0]; String medal = rankIdx < medals.length ? medals[rankIdx] : "&7#" + entry[0];
String name = entry[1]; String name = entry[1];
String count = entry[2]; String count = entry[2];
player.sendMessage(plugin.color( player.sendMessage(plugin.color(medal + " &f" + String.format("%-16s", name)
medal + " &f" + String.format("%-16s", name)
+ " &e" + count + " &7Ticket" + (Integer.parseInt(count) == 1 ? "" : "s"))); + " &e" + count + " &7Ticket" + (Integer.parseInt(count) == 1 ? "" : "s")));
} }
} }
@@ -570,7 +645,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; }
plugin.reloadConfig(); plugin.reloadConfig();
plugin.getCategoryManager().reload(); plugin.getCategoryManager().reload();
player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen. &7(inkl. Kategorien)")); plugin.getFaqManager().reload();
plugin.getTicketCache().clear();
player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen. &7(Kategorien, FAQs, Cache geleert)"));
if (plugin.isBungeeCordEnabled()) { if (plugin.isBungeeCordEnabled()) {
player.sendMessage(plugin.color("&8[BungeeCord] &7Server: &b" + plugin.getServerName())); player.sendMessage(plugin.color("&8[BungeeCord] &7Server: &b" + plugin.getServerName()));
} }
@@ -584,7 +661,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
int count = plugin.getDatabaseManager().archiveClosedTickets(); int count = plugin.getDatabaseManager().archiveClosedTickets();
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) player.sendMessage(plugin.formatMessage("messages.archive-success").replace("{count}", String.valueOf(count))); if (count > 0) player.sendMessage(plugin.formatMessage("messages.archive-success").replace("{count}", String.valueOf(count)));
else player.sendMessage(plugin.formatMessage("messages.archive-fail")); else player.sendMessage(plugin.formatMessage("messages.archive-fail"));
}); });
}); });
} }
@@ -596,7 +673,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
var stats = plugin.getDatabaseManager().getTicketStats(); var stats = plugin.getDatabaseManager().getTicketStats();
var staffRatings = plugin.getDatabaseManager().getStaffRatings(); var staffRatings = plugin.getDatabaseManager().getStaffRatings();
// Persistente Ersteller-Statistik überlebt Löschen/Archivieren
var topCreators = plugin.getDatabaseManager().getTopCreators(5); var topCreators = plugin.getDatabaseManager().getTopCreators(5);
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
@@ -617,26 +693,21 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
int percent = (int) Math.round(stats.thumbsUp * 100.0 / totalRated); int percent = (int) Math.round(stats.thumbsUp * 100.0 / totalRated);
player.sendMessage(plugin.color("&7Zufriedenheit: &e" + percent + "%")); player.sendMessage(plugin.color("&7Zufriedenheit: &e" + percent + "%"));
} }
// Bewertungen pro Support-Mitarbeiter
if (!staffRatings.isEmpty()) { if (!staffRatings.isEmpty()) {
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6Bewertungen nach Support-Mitarbeiter:")); player.sendMessage(plugin.color("&6Bewertungen nach Support-Mitarbeiter:"));
player.sendMessage(plugin.color("&7 Name 👍 👎 Tickets Zufrieden")); player.sendMessage(plugin.color("&7 Name 👍 👎 Tickets Zufrieden"));
for (String[] row : staffRatings) { for (String[] row : staffRatings) {
// row: [name, up, down, totalClosed, percent]
String name = String.format("%-16s", row[0]); String name = String.format("%-16s", row[0]);
String up = String.format("%-5s", row[1]); String up = String.format("%-5s", row[1]);
String down = String.format("%-5s", row[2]); String down = String.format("%-5s", row[2]);
String total = String.format("%-8s", row[3]); String total = String.format("%-8s", row[3]);
String percent = row[4]; String percent = row[4];
player.sendMessage(plugin.color( player.sendMessage(plugin.color("&e " + name + " &a" + up + " &c" + down + " &7" + total + " &e" + percent));
"&e " + name + " &a" + up + " &c" + down + " &7" + total + " &e" + percent));
} }
} }
} }
// BungeeCord: Tickets pro Server anzeigen
if (plugin.isBungeeCordEnabled() && !stats.byServer.isEmpty()) { if (plugin.isBungeeCordEnabled() && !stats.byServer.isEmpty()) {
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6Tickets nach Server:")); player.sendMessage(plugin.color("&6Tickets nach Server:"));
@@ -662,6 +733,10 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
} }
} }
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
// Cache-Status anzeigen
player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&7Cache: &e" + plugin.getTicketCache().size() + " &7gecachte Ticket(s)"));
}); });
}); });
} }
@@ -680,7 +755,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
int f = migrated; int f = migrated;
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (f > 0) player.sendMessage(plugin.formatMessage("messages.migration-success").replace("{count}", String.valueOf(f))); if (f > 0) player.sendMessage(plugin.formatMessage("messages.migration-success").replace("{count}", String.valueOf(f)));
else player.sendMessage(plugin.formatMessage("messages.migration-fail")); else player.sendMessage(plugin.formatMessage("messages.migration-fail"));
}); });
}); });
} }
@@ -696,7 +771,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
int count = plugin.getDatabaseManager().exportTickets(exportFile); int count = plugin.getDatabaseManager().exportTickets(exportFile);
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) player.sendMessage(plugin.formatMessage("messages.export-success").replace("{count}", String.valueOf(count)).replace("{file}", filename)); if (count > 0) player.sendMessage(plugin.formatMessage("messages.export-success").replace("{count}", String.valueOf(count)).replace("{file}", filename));
else player.sendMessage(plugin.formatMessage("messages.export-fail")); else player.sendMessage(plugin.formatMessage("messages.export-fail"));
}); });
}); });
} }
@@ -713,7 +788,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
int count = plugin.getDatabaseManager().importTickets(importFile); int count = plugin.getDatabaseManager().importTickets(importFile);
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) player.sendMessage(plugin.formatMessage("messages.import-success").replace("{count}", String.valueOf(count))); if (count > 0) player.sendMessage(plugin.formatMessage("messages.import-success").replace("{count}", String.valueOf(count)));
else player.sendMessage(plugin.formatMessage("messages.import-fail")); else player.sendMessage(plugin.formatMessage("messages.import-fail"));
}); });
}); });
} }
@@ -739,20 +814,31 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
if (priority == null) { if (priority == null) {
player.sendMessage(plugin.color("&cUngültige Priorität! Gültig: &alow&7, &enormal&7, &6high&7, &curgent")); return; player.sendMessage(plugin.color("&cUngültige Priorität! Gültig: &alow&7, &enormal&7, &6high&7, &curgent")); return;
} }
final int finalId = ticketId;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().setTicketPriority(ticketId, priority); boolean success = plugin.getDatabaseManager().setTicketPriority(finalId, priority);
plugin.getTicketCache().invalidate(finalId);
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (success) { if (success) player.sendMessage(plugin.color("&aPriorität von Ticket &e#" + finalId + " &awurde auf " + priority.getColored() + " &agesetzt."));
player.sendMessage(plugin.color("&aPriorität von Ticket &e#" + ticketId else player.sendMessage(plugin.color("&cTicket &e#" + finalId + " &cwurde nicht gefunden."));
+ " &awurde auf " + priority.getColored() + " &agesetzt."));
} else {
player.sendMessage(plugin.color("&cTicket &e#" + ticketId + " &cwurde nicht gefunden."));
}
}); });
}); });
} }
/** Parst Benutzer-Eingaben zu TicketPriority. Gibt null zurück wenn keine Übereinstimmung. */ // ─────────────────────────── Hilfsmethoden ─────────────────────────────
/**
* Gibt ein Ticket aus dem Cache zurück, oder lädt es aus der Datenbank
* und legt es anschließend in den Cache. Gibt null zurück wenn nicht gefunden.
*/
private Ticket getCachedOrFetch(int ticketId) {
Ticket cached = plugin.getTicketCache().get(ticketId);
if (cached != null) return cached;
Ticket fresh = plugin.getDatabaseManager().getTicketById(ticketId);
if (fresh != null) plugin.getTicketCache().put(fresh);
return fresh;
}
private TicketPriority parsePriority(String input) { private TicketPriority parsePriority(String input) {
if (input == null) return null; if (input == null) return null;
return switch (input.toLowerCase()) { return switch (input.toLowerCase()) {
@@ -772,7 +858,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
if (!(sender instanceof Player player)) return completions; if (!(sender instanceof Player player)) return completions;
if (args.length == 1) { if (args.length == 1) {
List<String> subs = new ArrayList<>(List.of("create", "list", "comment", "top")); List<String> subs = new ArrayList<>(List.of("create", "list", "comment", "top", "faq"));
if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"))
subs.addAll(List.of("claim", "close")); subs.addAll(List.of("claim", "close"));
if (plugin.getConfig().getBoolean("rating-enabled", true)) subs.add("rate"); if (plugin.getConfig().getBoolean("rating-enabled", true)) subs.add("rate");
@@ -783,6 +869,17 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
subs.add("setpriority"); subs.add("setpriority");
for (String s : subs) if (s.startsWith(args[0].toLowerCase())) completions.add(s); for (String s : subs) if (s.startsWith(args[0].toLowerCase())) completions.add(s);
} else if (args.length == 2 && args[0].equalsIgnoreCase("faq")) {
List<String> faqSubs = new ArrayList<>(List.of("list"));
if (player.hasPermission("ticket.admin")) faqSubs.addAll(List.of("add", "edit", "delete", "reload"));
for (String s : faqSubs) if (s.startsWith(args[1].toLowerCase())) completions.add(s);
} else if (args.length == 3 && args[0].equalsIgnoreCase("faq")
&& (args[1].equalsIgnoreCase("edit") || args[1].equalsIgnoreCase("delete"))
&& player.hasPermission("ticket.admin")) {
for (FaqEntry e : plugin.getFaqManager().getAll())
completions.add(String.valueOf(e.getId()));
} else if (args.length == 2 && args[0].equalsIgnoreCase("create") } else if (args.length == 2 && args[0].equalsIgnoreCase("create")
&& plugin.getConfig().getBoolean("categories-enabled", true)) { && plugin.getConfig().getBoolean("categories-enabled", true)) {
for (ConfigCategory c : plugin.getCategoryManager().getAll()) for (ConfigCategory c : plugin.getCategoryManager().getAll())
@@ -801,7 +898,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
if (p.startsWith(args[2].toLowerCase())) completions.add(p); if (p.startsWith(args[2].toLowerCase())) completions.add(p);
} else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) { } else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) {
// BungeeCord: Nur lokal online Spieler als Tab-Completion
for (Player p : Bukkit.getOnlinePlayers()) for (Player p : Bukkit.getOnlinePlayers())
if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName()); if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName());

View File

@@ -128,7 +128,7 @@ public class DatabaseManager {
dataSource = new HikariDataSource(config); dataSource = new HikariDataSource(config);
createTables(); createTables();
ensureColumns(); ensureColumns();
plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt."); if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] MySQL-Verbindung erfolgreich hergestellt.");
return true; return true;
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e); plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e);
@@ -143,7 +143,7 @@ public class DatabaseManager {
return true; return true;
} }
} else { } else {
plugin.getLogger().info("MySQL deaktiviert. Verwende Datei-Speicherung (data.yml)."); if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] MySQL deaktiviert. Verwende Datei-Speicherung (data.yml).");
return true; return true;
} }
} }
@@ -151,7 +151,7 @@ public class DatabaseManager {
public void disconnect() { public void disconnect() {
if (useMySQL && dataSource != null && !dataSource.isClosed()) { if (useMySQL && dataSource != null && !dataSource.isClosed()) {
dataSource.close(); dataSource.close();
plugin.getLogger().info("MySQL-Verbindung getrennt."); if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] MySQL-Verbindung getrennt.");
} }
} }

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

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

@@ -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

@@ -110,26 +110,33 @@ public class Ticket implements ConfigurationSerializable {
public Map<String, Object> serialize() { public Map<String, Object> serialize() {
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();
map.put("id", id); map.put("id", id);
map.put("creatorUUID", creatorUUID.toString()); // Null-Schutz: SnakeYAML wirft NPE wenn Key oder Value null sind
map.put("creatorName", creatorName); map.put("creatorUUID", creatorUUID != null ? creatorUUID.toString() : "");
map.put("message", message); map.put("creatorName", creatorName != null ? creatorName : "");
map.put("world", worldName); 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("x", x); map.put("y", y); map.put("z", z);
map.put("yaw", yaw); map.put("pitch", pitch); map.put("yaw", yaw); map.put("pitch", pitch);
map.put("status", status.name()); map.put("status", status != null ? status.name() : TicketStatus.OPEN.name());
if (createdAt != null) map.put("createdAt", createdAt.getTime()); if (createdAt != null) map.put("createdAt", createdAt.getTime());
if (claimedAt != null) map.put("claimedAt", claimedAt.getTime()); if (claimedAt != null) map.put("claimedAt", claimedAt.getTime());
if (closedAt != null) map.put("closedAt", closedAt.getTime()); if (closedAt != null) map.put("closedAt", closedAt.getTime());
if (closeComment != null) map.put("closeComment", closeComment); if (closeComment != null) map.put("closeComment", closeComment);
if (claimerUUID != null) { map.put("claimerUUID", claimerUUID.toString()); map.put("claimerName", claimerName); } if (claimerUUID != null) {
if (forwardedToUUID != null) { map.put("forwardedToUUID", forwardedToUUID.toString()); map.put("forwardedToName", forwardedToName); } 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("playerDeleted", playerDeleted);
map.put("category", categoryKey); map.put("category", categoryKey != null ? categoryKey : "general");
map.put("priority", priority.name()); map.put("priority", priority != null ? priority.name() : TicketPriority.NORMAL.name());
if (playerRating != null) map.put("playerRating", playerRating); if (playerRating != null) map.put("playerRating", playerRating);
map.put("claimerNotified", claimerNotified); map.put("claimerNotified", claimerNotified);
// BungeeCord: Server-Name speichern // BungeeCord: Server-Name speichern
map.put("serverName", serverName); map.put("serverName", serverName != null ? serverName : "unknown");
map.put("closeNotified", closeNotified); map.put("closeNotified", closeNotified);
return map; return map;
} }

View File

@@ -1,5 +1,5 @@
name: TicketSystem name: TicketSystem
version: 1.0.5 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