Update from Git Manager GUI
This commit is contained in:
@@ -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); }
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/main/java/de/ticketsystem/cache/TicketCache.java
vendored
Normal file
93
src/main/java/de/ticketsystem/cache/TicketCache.java
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package de.ticketsystem.cache;
|
||||||
|
|
||||||
|
import de.ticketsystem.model.Ticket;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einfacher TTL-basierter In-Memory-Cache für Ticket-Objekte.
|
||||||
|
*
|
||||||
|
* Reduziert wiederholte Datenbankabfragen beim häufigen Lesen desselben
|
||||||
|
* Tickets (z.B. beim Öffnen der GUI, Kommentarbenachrichtigungen usw.).
|
||||||
|
*
|
||||||
|
* Standard-TTL: 60 Sekunden (konfigurierbar per Konstruktor).
|
||||||
|
*
|
||||||
|
* Thread-sicher (ConcurrentHashMap) – kann aus asynchronen Tasks heraus
|
||||||
|
* lese- und schreibend aufgerufen werden.
|
||||||
|
*/
|
||||||
|
public class TicketCache {
|
||||||
|
|
||||||
|
private static final long DEFAULT_TTL_MS = 60_000L; // 60 Sekunden
|
||||||
|
|
||||||
|
private final long ttlMs;
|
||||||
|
|
||||||
|
/** Cache-Eintrag: Ticket + Verfallszeitstempel */
|
||||||
|
private record CacheEntry(Ticket ticket, long expiresAt) {}
|
||||||
|
|
||||||
|
private final Map<Integer, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// ─────────────────────────── Konstruktor ───────────────────────────────
|
||||||
|
|
||||||
|
public TicketCache() {
|
||||||
|
this(DEFAULT_TTL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TicketCache(long ttlMs) {
|
||||||
|
this.ttlMs = ttlMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Public API ────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt ein gecachtes Ticket zurück oder {@code null} wenn nicht vorhanden
|
||||||
|
* oder der Eintrag abgelaufen ist.
|
||||||
|
*/
|
||||||
|
public Ticket get(int ticketId) {
|
||||||
|
CacheEntry entry = cache.get(ticketId);
|
||||||
|
if (entry == null) return null;
|
||||||
|
if (System.currentTimeMillis() > entry.expiresAt()) {
|
||||||
|
cache.remove(ticketId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return entry.ticket();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speichert ein Ticket im Cache.
|
||||||
|
* Überschreibt vorhandene Einträge.
|
||||||
|
*/
|
||||||
|
public void put(Ticket ticket) {
|
||||||
|
if (ticket == null) return;
|
||||||
|
cache.put(ticket.getId(), new CacheEntry(ticket, System.currentTimeMillis() + ttlMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt ein Ticket aus dem Cache (z.B. nach einem Update).
|
||||||
|
*/
|
||||||
|
public void invalidate(int ticketId) {
|
||||||
|
cache.remove(ticketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leert den gesamten Cache (z.B. nach einem Plugin-Reload).
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt alle abgelaufenen Einträge.
|
||||||
|
* Sollte periodisch aufgerufen werden um Speicher freizugeben.
|
||||||
|
*/
|
||||||
|
public void evictExpired() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
cache.entrySet().removeIf(e -> now > e.getValue().expiresAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt die aktuelle Anzahl der (möglicherweise teils abgelaufenen) Einträge zurück. */
|
||||||
|
public int size() {
|
||||||
|
return cache.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
449
src/main/java/de/ticketsystem/gui/FaqGUI.java
Normal file
449
src/main/java/de/ticketsystem/gui/FaqGUI.java
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
package de.ticketsystem.gui;
|
||||||
|
|
||||||
|
import de.ticketsystem.TicketPlugin;
|
||||||
|
import de.ticketsystem.model.FaqEntry;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.inventory.meta.SkullMeta;
|
||||||
|
import org.bukkit.profile.PlayerProfile;
|
||||||
|
import org.bukkit.profile.PlayerTextures;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ GUI für Spieler (Lesemodus) und Admins (Verwaltungsmodus).
|
||||||
|
*
|
||||||
|
* ──── Spieler-GUI (/ticket faq) ─────────────────────────────────────────
|
||||||
|
* Slots 0-44 : FAQ-Einträge als Custom-Skull-Items
|
||||||
|
* › Name = Frage
|
||||||
|
* › Lore = Antwort (aufgeteilt auf 40-Zeichen-Zeilen)
|
||||||
|
* Slot 49 : Seitenanzeige
|
||||||
|
* Slot 45/53 : Vorherige / Nächste Seite
|
||||||
|
*
|
||||||
|
* ──── Admin-GUI (ticket.admin-Berechtigung) ─────────────────────────────
|
||||||
|
* Wie Spieler-GUI, zusätzlich:
|
||||||
|
* Slot 50 : "Neues FAQ hinzufügen" (Lime Wool)
|
||||||
|
* Klick auf FAQ-Item → Aktions-GUI
|
||||||
|
*
|
||||||
|
* ──── Aktions-GUI (27 Slots) ────────────────────────────────────────────
|
||||||
|
* Slot 4 : FAQ-Info (Skull)
|
||||||
|
* Slot 10 : Bearbeiten (Book & Quill)
|
||||||
|
* Slot 12 : Löschen (Barrier)
|
||||||
|
* Slot 16 : Zurück (Arrow)
|
||||||
|
*
|
||||||
|
* ──── Chat-Eingabe (Hinzufügen / Bearbeiten) ────────────────────────────
|
||||||
|
* Step 1: Admin gibt Frage ein
|
||||||
|
* Step 2: Admin gibt Antwort ein → FAQ wird gespeichert
|
||||||
|
*/
|
||||||
|
public class FaqGUI implements Listener {
|
||||||
|
|
||||||
|
// ─────────────────────────── Titel-Konstanten ──────────────────────────
|
||||||
|
|
||||||
|
private static final String FAQ_GUI_TITLE = "§8§lHäufige Fragen (FAQ)";
|
||||||
|
private static final String FAQ_ADMIN_TITLE = "§8§lFAQ verwalten";
|
||||||
|
private static final String FAQ_ACTION_TITLE = "§8§lFAQ Aktionen";
|
||||||
|
|
||||||
|
/** FAQ-Einträge pro Seite (Zeilen 0-4, Slots 0-44). */
|
||||||
|
private static final int PAGE_SIZE = 45;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Texture-URL für die Custom-Skull-Items.
|
||||||
|
* http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e
|
||||||
|
*/
|
||||||
|
private static final String FAQ_SKIN_URL =
|
||||||
|
"http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e";
|
||||||
|
|
||||||
|
private final TicketPlugin plugin;
|
||||||
|
|
||||||
|
// ─────────────────────────── State ─────────────────────────────────────
|
||||||
|
|
||||||
|
/** Slot-Map für Spieler-FAQ-GUI: UUID → (Slot → FaqEntry) */
|
||||||
|
private final Map<UUID, Map<Integer, FaqEntry>> slotMap = new HashMap<>();
|
||||||
|
/** Aktuelle Seite pro Spieler in der FAQ-GUI */
|
||||||
|
private final Map<UUID, Integer> faqPage = new HashMap<>();
|
||||||
|
/** Ob der Spieler sich in der Admin-Ansicht befindet */
|
||||||
|
private final Set<UUID> adminView = new HashSet<>();
|
||||||
|
/** Aktuell ausgewähltes FAQ für die Aktions-GUI */
|
||||||
|
private final Map<UUID, FaqEntry> actionEntry = new HashMap<>();
|
||||||
|
|
||||||
|
// ─── Chat-Input-States ──────────────────────────────────────────────
|
||||||
|
/** Wartet auf Frage-Eingabe: null = neu, "edit:<id>" = bearbeiten */
|
||||||
|
private final Map<UUID, String> awaitingQuestion = new HashMap<>();
|
||||||
|
/** Wartet auf Antwort-Eingabe: key = Frage-Text (|id) bei Edit */
|
||||||
|
private final Map<UUID, String> awaitingAnswer = new HashMap<>();
|
||||||
|
|
||||||
|
// ─────────────────────────── Konstruktor ───────────────────────────────
|
||||||
|
|
||||||
|
public FaqGUI(TicketPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// PUBLIC OPEN-METHODEN
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Öffnet die Spieler-FAQ-GUI (Seite 0). */
|
||||||
|
public void openFaqGUI(Player player) {
|
||||||
|
openFaqGUI(player, faqPage.getOrDefault(player.getUniqueId(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Öffnet die Spieler-FAQ-GUI auf der angegebenen Seite. */
|
||||||
|
public void openFaqGUI(Player player, int page) {
|
||||||
|
boolean isAdmin = player.hasPermission("ticket.admin");
|
||||||
|
String title = isAdmin ? FAQ_ADMIN_TITLE : FAQ_GUI_TITLE;
|
||||||
|
|
||||||
|
List<FaqEntry> all = plugin.getFaqManager().getAll();
|
||||||
|
int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / PAGE_SIZE));
|
||||||
|
page = Math.max(0, Math.min(page, totalPages - 1));
|
||||||
|
faqPage.put(player.getUniqueId(), page);
|
||||||
|
|
||||||
|
Inventory inv = Bukkit.createInventory(null, 54, title);
|
||||||
|
Map<Integer, FaqEntry> sm = new HashMap<>();
|
||||||
|
|
||||||
|
int start = page * PAGE_SIZE;
|
||||||
|
for (int i = 0; i < PAGE_SIZE && (start + i) < all.size(); i++) {
|
||||||
|
FaqEntry entry = all.get(start + i);
|
||||||
|
inv.setItem(i, buildFaqSkull(entry, isAdmin));
|
||||||
|
sm.put(i, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
slotMap.put(player.getUniqueId(), sm);
|
||||||
|
|
||||||
|
if (isAdmin) adminView.add(player.getUniqueId());
|
||||||
|
else adminView.remove(player.getUniqueId());
|
||||||
|
|
||||||
|
// ── Navigationsleiste ──────────────────────────────────────────────
|
||||||
|
fillNavBar(inv, page, totalPages, isAdmin, all.isEmpty());
|
||||||
|
player.openInventory(inv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// CLICK-EVENTS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onInventoryClick(InventoryClickEvent event) {
|
||||||
|
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||||
|
String title = event.getView().getTitle();
|
||||||
|
|
||||||
|
if (!title.equals(FAQ_GUI_TITLE) && !title.equals(FAQ_ADMIN_TITLE)
|
||||||
|
&& !title.equals(FAQ_ACTION_TITLE)) return;
|
||||||
|
|
||||||
|
event.setCancelled(true);
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
if (slot < 0) return;
|
||||||
|
|
||||||
|
// ── Aktions-GUI ────────────────────────────────────────────────────
|
||||||
|
if (title.equals(FAQ_ACTION_TITLE)) {
|
||||||
|
FaqEntry entry = actionEntry.get(player.getUniqueId());
|
||||||
|
if (entry == null) return;
|
||||||
|
switch (slot) {
|
||||||
|
case 10 -> startEditFlow(player, entry);
|
||||||
|
case 12 -> deleteFaq(player, entry);
|
||||||
|
case 16 -> openFaqGUI(player);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FAQ-Listen-GUI ─────────────────────────────────────────────────
|
||||||
|
boolean isAdmin = adminView.contains(player.getUniqueId());
|
||||||
|
int curPage = faqPage.getOrDefault(player.getUniqueId(), 0);
|
||||||
|
|
||||||
|
// Navigationsslots
|
||||||
|
if (slot == 45) { openFaqGUI(player, curPage - 1); return; }
|
||||||
|
if (slot == 53) { openFaqGUI(player, curPage + 1); return; }
|
||||||
|
|
||||||
|
// Admin-spezifisch: Neues FAQ hinzufügen
|
||||||
|
if (slot == 50 && isAdmin) {
|
||||||
|
startAddFlow(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAQ-Item angeklickt
|
||||||
|
if (slot < PAGE_SIZE) {
|
||||||
|
Map<Integer, FaqEntry> sm = slotMap.get(player.getUniqueId());
|
||||||
|
if (sm == null) return;
|
||||||
|
FaqEntry entry = sm.get(slot);
|
||||||
|
if (entry == null) return;
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
openActionGUI(player, entry);
|
||||||
|
} else {
|
||||||
|
// Spieler: Antwort im Chat ausgeben
|
||||||
|
player.closeInventory();
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
player.sendMessage(plugin.color("&6&lFAQ #" + entry.getId() + ": &e" + entry.getQuestion()));
|
||||||
|
player.sendMessage(plugin.color("&f" + entry.getAnswer()));
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Admin-Aktions-GUI ─────────────────────────
|
||||||
|
|
||||||
|
private void openActionGUI(Player player, FaqEntry entry) {
|
||||||
|
actionEntry.put(player.getUniqueId(), entry);
|
||||||
|
Inventory inv = Bukkit.createInventory(null, 27, FAQ_ACTION_TITLE);
|
||||||
|
|
||||||
|
// Slot 4: FAQ-Info
|
||||||
|
inv.setItem(4, buildFaqSkull(entry, false));
|
||||||
|
|
||||||
|
// Slot 10: Bearbeiten
|
||||||
|
inv.setItem(10, buildItem(Material.WRITABLE_BOOK, "§a§lFAQ bearbeiten",
|
||||||
|
List.of("§7Ändere Frage und Antwort", "§7dieses FAQ-Eintrags.")));
|
||||||
|
|
||||||
|
// Slot 12: Löschen
|
||||||
|
inv.setItem(12, buildItem(Material.BARRIER, "§c§lFAQ löschen",
|
||||||
|
List.of("§7Löscht diesen FAQ-Eintrag.", "§c§lACHTUNG: §cNicht rückgängig zu machen!")));
|
||||||
|
|
||||||
|
// Slot 16: Zurück
|
||||||
|
inv.setItem(16, buildItem(Material.ARROW, "§7§lZurück",
|
||||||
|
List.of("§7Zurück zur FAQ-Übersicht.")));
|
||||||
|
|
||||||
|
fillGlass(inv);
|
||||||
|
player.openInventory(inv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Chat-Flow: Hinzufügen ─────────────────────
|
||||||
|
|
||||||
|
private void startAddFlow(Player player) {
|
||||||
|
player.closeInventory();
|
||||||
|
awaitingQuestion.put(player.getUniqueId(), "new");
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
player.sendMessage(plugin.color("&6&lNeues FAQ erstellen"));
|
||||||
|
player.sendMessage(plugin.color("&7Gib die &eFrage &7ein (oder &ccancel&7):"));
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Chat-Flow: Bearbeiten ─────────────────────
|
||||||
|
|
||||||
|
private void startEditFlow(Player player, FaqEntry entry) {
|
||||||
|
player.closeInventory();
|
||||||
|
awaitingQuestion.put(player.getUniqueId(), "edit:" + entry.getId());
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
player.sendMessage(plugin.color("&6&lFAQ #" + entry.getId() + " bearbeiten"));
|
||||||
|
player.sendMessage(plugin.color("&7Aktuelle Frage: &e" + entry.getQuestion()));
|
||||||
|
player.sendMessage(plugin.color("&7Gib die neue &eFrage &7ein (oder &ccancel&7):"));
|
||||||
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Löschen ───────────────────────────────────
|
||||||
|
|
||||||
|
private void deleteFaq(Player player, FaqEntry entry) {
|
||||||
|
player.closeInventory();
|
||||||
|
boolean success = plugin.getFaqManager().delete(entry.getId());
|
||||||
|
if (success) {
|
||||||
|
player.sendMessage(plugin.color("&aFAQ #" + entry.getId() + " &a(§e" + entry.getQuestion() + "§a) wurde gelöscht."));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.color("&cFehler: FAQ #" + entry.getId() + " konnte nicht gelöscht werden."));
|
||||||
|
}
|
||||||
|
openFaqGUI(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// CHAT-EVENTS (Frage & Antwort Eingabe)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOWEST)
|
||||||
|
public void onPlayerChat(AsyncPlayerChatEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
|
||||||
|
// ── Schritt 1: Warte auf Frage ─────────────────────────────────────
|
||||||
|
if (awaitingQuestion.containsKey(uuid)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
String state = awaitingQuestion.remove(uuid);
|
||||||
|
String input = event.getMessage().trim();
|
||||||
|
|
||||||
|
if (input.equalsIgnoreCase("cancel")) {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
player.sendMessage(plugin.color("&cAbgebrochen."));
|
||||||
|
openFaqGUI(player);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frage gespeichert → jetzt auf Antwort warten
|
||||||
|
// Encoded state: "new" oder "edit:<id>"
|
||||||
|
awaitingAnswer.put(uuid, state + "§§" + input); // "§§" as internal separator
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
player.sendMessage(plugin.color("&7Frage gesetzt: &e" + input));
|
||||||
|
player.sendMessage(plugin.color("&7Gib jetzt die &eAntwort &7ein (oder &ccancel&7):"));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Schritt 2: Warte auf Antwort ───────────────────────────────────
|
||||||
|
if (awaitingAnswer.containsKey(uuid)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
String stateAndQuestion = awaitingAnswer.remove(uuid);
|
||||||
|
String input = event.getMessage().trim();
|
||||||
|
|
||||||
|
if (input.equalsIgnoreCase("cancel")) {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
player.sendMessage(plugin.color("&cAbgebrochen."));
|
||||||
|
openFaqGUI(player);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateAndQuestion = "<state>§§<question>"
|
||||||
|
int sep = stateAndQuestion.indexOf("§§");
|
||||||
|
String state = stateAndQuestion.substring(0, sep);
|
||||||
|
String question = stateAndQuestion.substring(sep + 2);
|
||||||
|
String answer = input;
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (state.equals("new")) {
|
||||||
|
// Hinzufügen
|
||||||
|
FaqEntry created = plugin.getFaqManager().add(question, answer);
|
||||||
|
player.sendMessage(plugin.color("&aFAQ #" + created.getId() + " wurde erfolgreich erstellt!"));
|
||||||
|
} else {
|
||||||
|
// Bearbeiten: state = "edit:<id>"
|
||||||
|
int id;
|
||||||
|
try {
|
||||||
|
id = Integer.parseInt(state.substring(5)); // "edit:".length() = 5
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
player.sendMessage(plugin.color("&cInterner Fehler beim Bearbeiten des FAQs."));
|
||||||
|
openFaqGUI(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean ok = plugin.getFaqManager().edit(id, question, answer);
|
||||||
|
if (ok) player.sendMessage(plugin.color("&aFAQ #" + id + " wurde erfolgreich aktualisiert!"));
|
||||||
|
else player.sendMessage(plugin.color("&cFAQ #" + id + " wurde nicht gefunden."));
|
||||||
|
}
|
||||||
|
openFaqGUI(player);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// ITEM-BUILDER
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut ein Custom-Skull-Item für einen FAQ-Eintrag.
|
||||||
|
* Nutzt die Textur: da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e
|
||||||
|
*
|
||||||
|
* Bei einem Fehler mit der Textur wird auf BOOK zurückgefallen.
|
||||||
|
*/
|
||||||
|
private ItemStack buildFaqSkull(FaqEntry entry, boolean adminHint) {
|
||||||
|
ItemStack skull;
|
||||||
|
|
||||||
|
try {
|
||||||
|
skull = new ItemStack(Material.PLAYER_HEAD);
|
||||||
|
SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
PlayerProfile profile = Bukkit.createPlayerProfile(
|
||||||
|
UUID.nameUUIDFromBytes(("FAQ_" + entry.getId()).getBytes()), "FAQ_" + entry.getId());
|
||||||
|
PlayerTextures textures = profile.getTextures();
|
||||||
|
textures.setSkin(new URL(FAQ_SKIN_URL));
|
||||||
|
profile.setTextures(textures);
|
||||||
|
meta.setOwnerProfile(profile);
|
||||||
|
meta.setDisplayName("§e§l" + entry.getQuestion());
|
||||||
|
meta.setLore(buildFaqLore(entry, adminHint));
|
||||||
|
skull.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Fallback auf BOOK wenn Textur nicht gesetzt werden kann
|
||||||
|
skull = new ItemStack(Material.BOOK);
|
||||||
|
ItemMeta meta = skull.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setDisplayName("§e§l" + entry.getQuestion());
|
||||||
|
meta.setLore(buildFaqLore(entry, adminHint));
|
||||||
|
skull.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return skull;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Erstellt die Lore-Zeilen für ein FAQ-Item (Antwort aufgeteilt in 40er-Zeilen). */
|
||||||
|
private List<String> buildFaqLore(FaqEntry entry, boolean adminHint) {
|
||||||
|
List<String> lore = new ArrayList<>();
|
||||||
|
lore.add("§8§m ");
|
||||||
|
lore.add("§7FAQ #" + entry.getId());
|
||||||
|
lore.add("§8§m ");
|
||||||
|
|
||||||
|
// Antwort in 40-Zeichen-Abschnitte aufteilen
|
||||||
|
String answer = entry.getAnswer();
|
||||||
|
int chunkSize = 40;
|
||||||
|
for (int i = 0; i < answer.length(); i += chunkSize) {
|
||||||
|
int end = Math.min(i + chunkSize, answer.length());
|
||||||
|
// Wortgrenzen bevorzugen
|
||||||
|
if (end < answer.length() && answer.charAt(end) != ' ') {
|
||||||
|
int lastSpace = answer.lastIndexOf(' ', end);
|
||||||
|
if (lastSpace > i) end = lastSpace;
|
||||||
|
}
|
||||||
|
lore.add("§f" + answer.substring(i, end).trim());
|
||||||
|
i = end - chunkSize; // Schleifeninkrement korrigieren
|
||||||
|
}
|
||||||
|
|
||||||
|
lore.add("§8§m ");
|
||||||
|
if (adminHint) {
|
||||||
|
lore.add("§e» Klicken zum Bearbeiten / Löschen");
|
||||||
|
} else {
|
||||||
|
lore.add("§e» Klicken für mehr Details im Chat");
|
||||||
|
}
|
||||||
|
return lore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack buildItem(Material material, String displayName, List<String> lore) {
|
||||||
|
ItemStack item = new ItemStack(material);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta == null) return item;
|
||||||
|
meta.setDisplayName(displayName);
|
||||||
|
meta.setLore(lore);
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Navigationsleiste ─────────────────────────
|
||||||
|
|
||||||
|
private void fillNavBar(Inventory inv, int page, int totalPages, boolean isAdmin, boolean isEmpty) {
|
||||||
|
ItemStack glass = makeGlass();
|
||||||
|
for (int i = 45; i < 54; i++) inv.setItem(i, glass);
|
||||||
|
|
||||||
|
if (page > 0) {
|
||||||
|
inv.setItem(45, buildItem(Material.ARROW, "§7§l◄ Zurück",
|
||||||
|
List.of("§7Seite " + page + " von " + totalPages)));
|
||||||
|
}
|
||||||
|
if (page < totalPages - 1) {
|
||||||
|
inv.setItem(53, buildItem(Material.ARROW, "§7§lWeiter ►",
|
||||||
|
List.of("§7Seite " + (page + 2) + " von " + totalPages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
inv.setItem(49, buildItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages,
|
||||||
|
List.of("§7Gesamt: " + (isEmpty ? 0 : Math.min(PAGE_SIZE, totalPages * PAGE_SIZE)) + " FAQ(s)")));
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
inv.setItem(50, buildItem(Material.LIME_WOOL, "§a§lNeues FAQ hinzufügen",
|
||||||
|
List.of("§7Fügt einen neuen FAQ-Eintrag hinzu.", "§7Du wirst nach Frage und Antwort gefragt.")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillGlass(Inventory inv) {
|
||||||
|
ItemStack glass = makeGlass();
|
||||||
|
for (int i = 0; i < inv.getSize(); i++) {
|
||||||
|
if (inv.getItem(i) == null) inv.setItem(i, glass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack makeGlass() {
|
||||||
|
ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
|
||||||
|
ItemMeta meta = glass.getItemMeta();
|
||||||
|
if (meta != null) { meta.setDisplayName(" "); glass.setItemMeta(meta); }
|
||||||
|
return glass;
|
||||||
|
}
|
||||||
|
}
|
||||||
181
src/main/java/de/ticketsystem/manager/FaqManager.java
Normal file
181
src/main/java/de/ticketsystem/manager/FaqManager.java
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package de.ticketsystem.manager;
|
||||||
|
|
||||||
|
import de.ticketsystem.TicketPlugin;
|
||||||
|
import de.ticketsystem.model.FaqEntry;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages FAQ entries stored in faqs.yml.
|
||||||
|
*
|
||||||
|
* Admins can add, edit and delete FAQs in-game.
|
||||||
|
* All changes are saved immediately to faqs.yml.
|
||||||
|
*
|
||||||
|
* faqs.yml layout:
|
||||||
|
*
|
||||||
|
* faqs:
|
||||||
|
* 1:
|
||||||
|
* question: "Wie erstelle ich ein Ticket?"
|
||||||
|
* answer: "Nutze /ticket create [Kategorie] [Beschreibung]."
|
||||||
|
* 2:
|
||||||
|
* question: "..."
|
||||||
|
* answer: "..."
|
||||||
|
*/
|
||||||
|
public class FaqManager {
|
||||||
|
|
||||||
|
private final TicketPlugin plugin;
|
||||||
|
private final File faqFile;
|
||||||
|
private YamlConfiguration faqConfig;
|
||||||
|
private final List<FaqEntry> entries = new ArrayList<>();
|
||||||
|
private int nextId = 1;
|
||||||
|
|
||||||
|
public FaqManager(TicketPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.faqFile = new File(plugin.getDataFolder(), "faqs.yml");
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Loading & Saving ───────────────────────────
|
||||||
|
|
||||||
|
private void load() {
|
||||||
|
entries.clear();
|
||||||
|
nextId = 1;
|
||||||
|
|
||||||
|
if (!faqFile.exists()) {
|
||||||
|
try {
|
||||||
|
faqFile.getParentFile().mkdirs();
|
||||||
|
faqFile.createNewFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().severe("[FaqManager] Konnte faqs.yml nicht erstellen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
faqConfig = new YamlConfiguration();
|
||||||
|
loadDefaults();
|
||||||
|
save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
faqConfig = YamlConfiguration.loadConfiguration(faqFile);
|
||||||
|
ConfigurationSection section = faqConfig.getConfigurationSection("faqs");
|
||||||
|
|
||||||
|
if (section != null) {
|
||||||
|
for (String key : section.getKeys(false)) {
|
||||||
|
try {
|
||||||
|
int id = Integer.parseInt(key);
|
||||||
|
String question = faqConfig.getString("faqs." + key + ".question", "");
|
||||||
|
String answer = faqConfig.getString("faqs." + key + ".answer", "");
|
||||||
|
if (!question.isBlank() && !answer.isBlank()) {
|
||||||
|
entries.add(new FaqEntry(id, question, answer));
|
||||||
|
if (id >= nextId) nextId = id + 1;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.sort(Comparator.comparingInt(FaqEntry::getId));
|
||||||
|
|
||||||
|
if (plugin.isDebug()) {
|
||||||
|
plugin.getLogger().info("[FaqManager] " + entries.size() + " FAQ(s) geladen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes the example FAQs into a freshly created faqs.yml. */
|
||||||
|
private void loadDefaults() {
|
||||||
|
writeEntry(1, "Wie erstelle ich ein Ticket?",
|
||||||
|
"Nutze den Befehl /ticket create [Kategorie] [Prio][Beschreibung] um ein neues Ticket zu erstellen.");
|
||||||
|
writeEntry(2, "Wie lange dauert die Bearbeitung?",
|
||||||
|
"Unser Support-Team bearbeitet Tickets so schnell wie möglich. Bitte habe etwas Geduld.");
|
||||||
|
writeEntry(3, "Kann ich mein Ticket löschen?",
|
||||||
|
"Ja! Öffne /ticket list und klicke auf dein Ticket, um es aus der Übersicht zu entfernen.");
|
||||||
|
writeEntry(4, "Wie kann ich meinen Support bewerten?",
|
||||||
|
"Nach dem Schließen eines Tickets kannst du mit /ticket rate <ID> good/bad eine Bewertung abgeben.");
|
||||||
|
nextId = 5;
|
||||||
|
// Sync entries list with what we just wrote
|
||||||
|
entries.add(new FaqEntry(1, "Wie erstelle ich ein Ticket?",
|
||||||
|
"Nutze den Befehl /ticket create [Kategorie] [Beschreibung] um ein neues Ticket zu erstellen."));
|
||||||
|
entries.add(new FaqEntry(2, "Wie lange dauert die Bearbeitung?",
|
||||||
|
"Unser Support-Team bearbeitet Tickets so schnell wie möglich. Bitte habe etwas Geduld."));
|
||||||
|
entries.add(new FaqEntry(3, "Kann ich mein Ticket löschen?",
|
||||||
|
"Ja! Öffne /ticket list und klicke auf dein Ticket, um es aus der Übersicht zu entfernen."));
|
||||||
|
entries.add(new FaqEntry(4, "Wie kann ich meinen Support bewerten?",
|
||||||
|
"Nach dem Schließen eines Tickets kannst du mit /ticket rate <ID> good/bad eine Bewertung abgeben."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeEntry(int id, String question, String answer) {
|
||||||
|
faqConfig.set("faqs." + id + ".question", question);
|
||||||
|
faqConfig.set("faqs." + id + ".answer", answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save() {
|
||||||
|
try {
|
||||||
|
faqConfig.save(faqFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().severe("[FaqManager] Konnte faqs.yml nicht speichern: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Public API ────────────────────────────────
|
||||||
|
|
||||||
|
/** Returns an unmodifiable view of all FAQ entries in ID order. */
|
||||||
|
public List<FaqEntry> getAll() {
|
||||||
|
return Collections.unmodifiableList(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Looks up an entry by its numeric ID. Returns null if not found. */
|
||||||
|
public FaqEntry getById(int id) {
|
||||||
|
return entries.stream().filter(e -> e.getId() == id).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new FAQ entry and saves immediately.
|
||||||
|
*
|
||||||
|
* @param question The question text.
|
||||||
|
* @param answer The answer text.
|
||||||
|
* @return The newly created {@link FaqEntry}.
|
||||||
|
*/
|
||||||
|
public FaqEntry add(String question, String answer) {
|
||||||
|
int id = nextId++;
|
||||||
|
FaqEntry entry = new FaqEntry(id, question, answer);
|
||||||
|
entries.add(entry);
|
||||||
|
writeEntry(id, question, answer);
|
||||||
|
save();
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits an existing FAQ entry and saves immediately.
|
||||||
|
*
|
||||||
|
* @return true if the entry was found and updated, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean edit(int id, String question, String answer) {
|
||||||
|
FaqEntry entry = getById(id);
|
||||||
|
if (entry == null) return false;
|
||||||
|
entry.setQuestion(question);
|
||||||
|
entry.setAnswer(answer);
|
||||||
|
writeEntry(id, question, answer);
|
||||||
|
save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a FAQ entry and saves immediately.
|
||||||
|
*
|
||||||
|
* @return true if the entry was found and deleted, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean delete(int id) {
|
||||||
|
FaqEntry entry = getById(id);
|
||||||
|
if (entry == null) return false;
|
||||||
|
entries.remove(entry);
|
||||||
|
faqConfig.set("faqs." + id, null);
|
||||||
|
save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reloads FAQs from faqs.yml without restarting the server. */
|
||||||
|
public void reload() {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/main/java/de/ticketsystem/model/FaqEntry.java
Normal file
29
src/main/java/de/ticketsystem/model/FaqEntry.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package de.ticketsystem.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single FAQ entry stored in faqs.yml.
|
||||||
|
*/
|
||||||
|
public class FaqEntry {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
private String question;
|
||||||
|
private String answer;
|
||||||
|
|
||||||
|
public FaqEntry(int id, String question, String answer) {
|
||||||
|
this.id = id;
|
||||||
|
this.question = question;
|
||||||
|
this.answer = answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() { return id; }
|
||||||
|
public void setId(int id) { this.id = id; }
|
||||||
|
public String getQuestion() { return question; }
|
||||||
|
public void setQuestion(String q) { this.question = q; }
|
||||||
|
public String getAnswer() { return answer; }
|
||||||
|
public void setAnswer(String a) { this.answer = a; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "FaqEntry{id=" + id + ", question='" + question + "'}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
name: TicketSystem
|
name: TicketSystem
|
||||||
version: 1.0.5
|
version: 1.0.6
|
||||||
main: de.ticketsystem.TicketPlugin
|
main: de.ticketsystem.TicketPlugin
|
||||||
api-version: 1.20
|
api-version: 1.20
|
||||||
author: M_Viper
|
author: M_Viper
|
||||||
|
|||||||
Reference in New Issue
Block a user