From 179d46bb6fc66de17aa5860c99dc659b5d694b1f Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Sun, 19 Apr 2026 17:43:08 +0200 Subject: [PATCH] Upload folder via GUI - src --- .../java/de/ticketsystem/TicketPlugin.java | 26 +- .../ticketsystem/commands/TicketCommand.java | 304 ++++++++++++++---- .../ticketsystem/manager/LanguageManager.java | 222 ++++++++++--- src/main/resources/config.yml | 3 + src/main/resources/lang_de.yml | 3 - src/main/resources/lang_en.yml | 3 - src/main/resources/plugin.yml | 2 +- 7 files changed, 455 insertions(+), 108 deletions(-) diff --git a/src/main/java/de/ticketsystem/TicketPlugin.java b/src/main/java/de/ticketsystem/TicketPlugin.java index baf81db..7836f4e 100644 --- a/src/main/java/de/ticketsystem/TicketPlugin.java +++ b/src/main/java/de/ticketsystem/TicketPlugin.java @@ -115,11 +115,18 @@ public class TicketPlugin extends JavaPlugin { // Manager, GUI, FAQ & Discord-Webhook initialisieren categoryManager = new CategoryManager(this); ticketManager = new TicketManager(this); - faqManager = new FaqManager(this); ticketGUI = new TicketGUI(this); - faqGUI = new FaqGUI(this); discordWebhook = new DiscordWebhook(this); + // ── FAQ-System (nur initialisieren wenn aktiviert) ───────────────── + if (isFaqEnabled()) { + faqManager = new FaqManager(this); + faqGUI = new FaqGUI(this); + getLogger().info("[FAQ] FAQ-System aktiviert."); + } else { + getLogger().info("[FAQ] FAQ-System deaktiviert (faq-enabled: false in config.yml)."); + } + if (getConfig().getBoolean("discord.enabled", false)) { String url = getConfig().getString("discord.webhook-url", ""); if (url.isEmpty()) { @@ -134,7 +141,9 @@ public class TicketPlugin extends JavaPlugin { getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); getServer().getPluginManager().registerEvents(ticketGUI, this); - getServer().getPluginManager().registerEvents(faqGUI, this); + if (faqGUI != null) { + getServer().getPluginManager().registerEvents(faqGUI, this); + } // Automatische Archivierung int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24); @@ -166,6 +175,9 @@ public class TicketPlugin extends JavaPlugin { if (webServer != null) webServer.stop(); if (ticketCache != null) ticketCache.clear(); if (databaseManager != null) databaseManager.disconnect(); + // Adventure BukkitAudiences sauber schließen + if (languageManager != null && languageManager.getAudiences() != null) + languageManager.getAudiences().close(); getLogger().info("TicketSystem wurde deaktiviert."); } @@ -322,6 +334,14 @@ public class TicketPlugin extends JavaPlugin { TicketPriority.reloadLocalizedNames(this); } + /** + * Gibt zurück ob das FAQ-System aktiviert ist. + * Konfigurierbar in config.yml → faq-enabled (Standard: true) + */ + public boolean isFaqEnabled() { + return getConfig().getBoolean("faq-enabled", true); + } + public static TicketPlugin getInstance() { return instance; } public LanguageManager getLanguageManager() { return languageManager; } public DatabaseManager getDatabaseManager() { return databaseManager; } diff --git a/src/main/java/de/ticketsystem/commands/TicketCommand.java b/src/main/java/de/ticketsystem/commands/TicketCommand.java index e60111d..da31820 100644 --- a/src/main/java/de/ticketsystem/commands/TicketCommand.java +++ b/src/main/java/de/ticketsystem/commands/TicketCommand.java @@ -9,17 +9,22 @@ import de.ticketsystem.model.TicketComment; import de.ticketsystem.model.TicketPriority; import de.ticketsystem.manager.CategoryManager; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.OfflinePlayer; +import org.bukkit.World; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; import java.io.File; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.UUID; public class TicketCommand implements CommandExecutor, TabCompleter { @@ -31,15 +36,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter { // ── Subkommando-Auflösung ───────────────────────────────────────────── - /** - * Normalisiert ein Subkommando auf den internen englischen Schlüssel. - * Deutsche UND englische Varianten werden immer akzeptiert – unabhängig - * von der language-Einstellung. So kann ein Admin auf einem EN-Server - * trotzdem den deutschen Begriff tippen ohne einen Fehler zu bekommen. - */ private String normalize(String input) { return switch (input.toLowerCase()) { - // Deutsch → intern case "erstellen" -> "create"; case "liste" -> "list"; case "übernehmen", "uebernehmen", @@ -59,7 +57,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { case "priorität", "prioritaet" -> "setpriority"; case "hilfe" -> "help"; case "kategorie" -> "category"; - // Englisch + alles andere → unverändert default -> input.toLowerCase(); }; } @@ -68,17 +65,36 @@ public class TicketCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player player)) { - sender.sendMessage(plugin.lang().get("general.console-only")); - return true; - } if (args.length == 0) { - plugin.getTicketManager().sendHelpMessage(player); + if (sender instanceof Player player) { + plugin.getTicketManager().sendHelpMessage(player); + } else { + sendConsoleHelp(sender); + } return true; } - switch (normalize(args[0])) { + String sub = normalize(args[0]); + + // ── Konsolen-only: create mit Spielername ──────────────────────── + if (sender instanceof ConsoleCommandSender) { + switch (sub) { + case "create" -> { handleCreateConsole(sender, args); return true; } + case "reload" -> { handleReloadConsole(sender); return true; } + case "archive" -> { handleArchiveConsole(sender); return true; } + case "stats" -> { handleStatsConsole(sender); return true; } + default -> { + sender.sendMessage("[TicketSystem] Konsolen-Befehle: create [Kategorie] | reload | archive | stats"); + return true; + } + } + } + + // ── Spieler-Befehle ────────────────────────────────────────────── + Player player = (Player) sender; + + switch (sub) { case "create" -> handleCreate(player, args); case "list" -> handleList(player); case "claim" -> handleClaim(player, args); @@ -103,6 +119,179 @@ public class TicketCommand implements CommandExecutor, TabCompleter { return true; } + // ── /ticket create (Konsole) ────────────────────────────────────────── + // + // Syntax: /ticket create [Kategorie] [Priorität] + // + // Das Ticket wird im Namen des angegebenen Spielers erstellt. + // Als Erstell-Position wird die aktuelle Spieler-Location verwendet + // (oder eine Default-Location wenn der Spieler offline ist). + + private void handleCreateConsole(CommandSender console, String[] args) { + // args[0] = "create", args[1] = Spielername, args[2..] = [Kategorie] [Priorität] Text + if (args.length < 3) { + console.sendMessage("[TicketSystem] Verwendung: /ticket create [Kategorie] [Priorität] "); + console.sendMessage("[TicketSystem] Beispiel: /ticket create Notch bug high Spieler fällt durch den Boden"); + return; + } + + String targetName = args[1]; + + // Spieler suchen (online bevorzugt, sonst OfflinePlayer) + @SuppressWarnings("deprecation") + OfflinePlayer offlineTarget = Bukkit.getOfflinePlayer(targetName); + UUID targetUUID = offlineTarget.getUniqueId(); + String resolvedName = offlineTarget.getName() != null ? offlineTarget.getName() : targetName; + + CategoryManager cm = plugin.getCategoryManager(); + ConfigCategory category = cm.getDefault(); + TicketPriority priority = TicketPriority.NORMAL; + int msgStart = 2; // Index ab dem die Beschreibung beginnt (relativ zu args[2..]) + + boolean categoriesOn = plugin.getConfig().getBoolean("categories-enabled", true); + boolean prioritiesOn = plugin.getConfig().getBoolean("priorities-enabled", true); + + // args[2] = optional Kategorie, args[3] = optional Priorität, Rest = Text + if (args.length >= 4 && categoriesOn) { + ConfigCategory parsedCat = cm.resolve(args[2]); + if (parsedCat != null) { + category = parsedCat; + msgStart = 3; + if (prioritiesOn && args.length >= 5) { + TicketPriority parsedPrio = parsePriority(args[3]); + if (parsedPrio != null) { priority = parsedPrio; msgStart = 4; } + } + } else if (prioritiesOn) { + TicketPriority parsedPrio = parsePriority(args[2]); + if (parsedPrio != null) { priority = parsedPrio; msgStart = 3; } + } + } else if (args.length >= 4 && prioritiesOn) { + TicketPriority parsedPrio = parsePriority(args[2]); + if (parsedPrio != null) { priority = parsedPrio; msgStart = 3; } + } + + // Beschreibung zusammensetzen (args[msgStart+1] weil args[1]=Spielername) + int absoluteStart = msgStart + 1; // +1 für den Spielernamen in args[1] + if (absoluteStart >= args.length) { + console.sendMessage("[TicketSystem] Fehler: Keine Beschreibung angegeben."); + console.sendMessage("[TicketSystem] Verwendung: /ticket create [Kategorie] [Priorität] "); + return; + } + + String message = String.join(" ", Arrays.copyOfRange(args, absoluteStart, args.length)); + int maxLen = plugin.getConfig().getInt("max-description-length", 100); + + if (message.isEmpty()) { + console.sendMessage("[TicketSystem] Fehler: Beschreibung darf nicht leer sein."); + return; + } + if (message.length() > maxLen) { + console.sendMessage("[TicketSystem] Fehler: Beschreibung zu lang (max. " + maxLen + " Zeichen)."); + return; + } + + // Location: online-Spieler → aktuelle Pos; offline → Spawn der Default-Welt + Location location; + Player onlineTarget = Bukkit.getPlayer(targetUUID); + if (onlineTarget != null) { + location = onlineTarget.getLocation(); + } else { + World defaultWorld = Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0); + location = defaultWorld != null + ? defaultWorld.getSpawnLocation() + : new Location(null, 0, 64, 0); + } + + final ConfigCategory fCat = category; + final TicketPriority fPrio = priority; + final UUID fUUID = targetUUID; + final String fName = resolvedName; + final Location fLocation = location; + final String fMessage = message; + + Ticket ticket = new Ticket(fUUID, fName, fMessage, fLocation); + ticket.setCategoryKey(fCat.getKey()); + ticket.setPriority(fPrio); + ticket.setServerName(plugin.getServerName()); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + int id = plugin.getDatabaseManager().createTicket(ticket); + if (id == -1) { + console.sendMessage("[TicketSystem] Fehler: Ticket konnte nicht in der Datenbank gespeichert werden."); + return; + } + ticket.setId(id); + plugin.getTicketCache().put(ticket); + + Bukkit.getScheduler().runTask(plugin, () -> { + boolean catOn = plugin.getConfig().getBoolean("categories-enabled", true); + boolean prioOn = plugin.getConfig().getBoolean("priorities-enabled", true); + String catInfo = catOn ? " [" + fCat.getName() + "]" : ""; + String prioInfo = prioOn ? " [" + fPrio.getDisplayName() + "]" : ""; + + console.sendMessage("[TicketSystem] Ticket #" + id + " für Spieler '" + fName + + "' erstellt." + catInfo + prioInfo); + console.sendMessage("[TicketSystem] Nachricht: " + fMessage); + + // Team benachrichtigen (gleich wie bei normalem /ticket create) + plugin.getTicketManager().notifyTeam(ticket); + }); + }); + } + + // ── /ticket reload (Konsole) ────────────────────────────────────────── + + private void handleReloadConsole(CommandSender console) { + plugin.reloadConfig(); + plugin.lang().reload(); + plugin.reloadSettings(); + plugin.getTicketGUI().reloadConfig(); + plugin.getTicketGUI().reloadTitles(); + plugin.getFaqGUI().reloadConfig(); + plugin.getCategoryManager().reload(); + plugin.getFaqManager().reload(); + plugin.getTicketCache().clear(); + console.sendMessage("[TicketSystem] Konfiguration erfolgreich neu geladen."); + } + + // ── /ticket archive (Konsole) ───────────────────────────────────────── + + private void handleArchiveConsole(CommandSender console) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + int count = plugin.getDatabaseManager().archiveClosedTickets(); + Bukkit.getScheduler().runTask(plugin, () -> + console.sendMessage("[TicketSystem] " + count + " Ticket(s) archiviert.")); + }); + } + + // ── /ticket stats (Konsole) ─────────────────────────────────────────── + + private void handleStatsConsole(CommandSender console) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + var stats = plugin.getDatabaseManager().getTicketStats(); + Bukkit.getScheduler().runTask(plugin, () -> { + console.sendMessage("[TicketSystem] === Statistiken ==="); + console.sendMessage("[TicketSystem] Gesamt: " + stats.total); + console.sendMessage("[TicketSystem] Offen: " + stats.open); + console.sendMessage("[TicketSystem] Geschlossen: " + stats.closed); + console.sendMessage("[TicketSystem] Weitergeleitet: " + stats.forwarded); + if (plugin.getConfig().getBoolean("rating-enabled", true)) { + console.sendMessage("[TicketSystem] Bewertungen: 👍 " + stats.thumbsUp + " 👎 " + stats.thumbsDown); + } + }); + }); + } + + // ── Konsolen-Hilfe ──────────────────────────────────────────────────── + + private void sendConsoleHelp(CommandSender console) { + console.sendMessage("[TicketSystem] Verfügbare Konsolen-Befehle:"); + console.sendMessage("[TicketSystem] /ticket create [Kategorie] [Priorität] "); + console.sendMessage("[TicketSystem] /ticket reload"); + console.sendMessage("[TicketSystem] /ticket archive"); + console.sendMessage("[TicketSystem] /ticket stats"); + } + // ── /ticket create ──────────────────────────────────────────────────── private void handleCreate(Player player, String[] args) { @@ -136,8 +325,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter { TicketPriority priority = TicketPriority.NORMAL; int msgStart = 1; - boolean categoriesOn = plugin.getConfig().getBoolean("categories-enabled", true); - boolean prioritiesOn = plugin.getConfig().getBoolean("priorities-enabled", true); + boolean categoriesOn = plugin.getConfig().getBoolean("categories-enabled", true); + boolean prioritiesOn = plugin.getConfig().getBoolean("priorities-enabled", true); boolean allowPlayersPrio = plugin.getConfig().getBoolean("allow-players-to-set-priority", false); boolean isTeam = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"); @@ -163,8 +352,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter { String message = String.join(" ", Arrays.copyOfRange(args, msgStart, args.length)); int maxLen = plugin.getConfig().getInt("max-description-length", 100); - if (message.isEmpty()) { plugin.lang().send(player, "create.no-description"); return; } - if (message.length() > maxLen) { plugin.lang().send(player, "create.too-long", "{max}", String.valueOf(maxLen)); return; } + if (message.isEmpty()) { plugin.lang().send(player, "create.no-description"); return; } + if (message.length() > maxLen) { plugin.lang().send(player, "create.too-long", "{max}", String.valueOf(maxLen)); return; } final ConfigCategory fCat = category; final TicketPriority fPrio = priority; @@ -535,17 +724,12 @@ public class TicketCommand implements CommandExecutor, TabCompleter { if (!player.hasPermission("ticket.admin")) { plugin.lang().send(player, "general.no-permission"); return; } - // Reihenfolge zwingend: - // 1. Config neu laden (liest language neu ein) - // 2. lang().reload() (liest language aus der frischen Config, baut cmdNames neu) - // 3. GUI reloadConfig() (liest Rows/Slots aus der frischen Config) - // 4. weitere Manager (nutzen ggf. frische lang-Texte) plugin.reloadConfig(); plugin.lang().reload(); - plugin.reloadSettings(); // serverName & debug-Flag aktualisieren - plugin.getTicketGUI().reloadConfig(); // Slots, Rows & Materialien neu laden - plugin.getTicketGUI().reloadTitles(); // Inventar-Titel aktualisieren - plugin.getFaqGUI().reloadConfig(); // FAQ-Slots, Rows & Materialien neu laden + plugin.reloadSettings(); + plugin.getTicketGUI().reloadConfig(); + plugin.getTicketGUI().reloadTitles(); + plugin.getFaqGUI().reloadConfig(); plugin.getCategoryManager().reload(); plugin.getFaqManager().reload(); plugin.getTicketCache().clear(); @@ -565,8 +749,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter { java.io.File backup = plugin.getDatabaseManager().createBackup(); Bukkit.getScheduler().runTask(plugin, () -> { if (backup != null) { - player.sendMessage(plugin.lang().format("backup.success", - "{file}", backup.getName())); + player.sendMessage(plugin.lang().format("backup.success", "{file}", backup.getName())); } else { player.sendMessage(plugin.lang().get("backup.fail")); } @@ -761,6 +944,12 @@ public class TicketCommand implements CommandExecutor, TabCompleter { // ── /ticket faq ─────────────────────────────────────────────────────── private void handleFaq(Player player, String[] args) { + // FAQ-System global deaktiviert? + if (!plugin.getConfig().getBoolean("faq-enabled", true)) { + plugin.lang().send(player, "faq.disabled"); + return; + } + if (args.length == 1) { plugin.getFaqGUI().openFaqGUI(player); return; @@ -777,7 +966,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { player.sendMessage(plugin.lang().get("faq.usage-add-example")); return; } - // Prüfen ob args[2] ein bekannter FAQ-Kategorie-Schlüssel ist String faqCatKey = null; int textStart = 2; if (plugin.getFaqManager().hasCategoriesEnabled()) { @@ -861,11 +1049,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter { if (!player.hasPermission("ticket.admin")) { plugin.lang().send(player, "general.no-permission"); return; } - // Richtung bestimmen: tofile oder tomysql (Standard) String direction = args.length >= 3 ? args[2].toLowerCase() : "tomysql"; if (direction.equals("tofile") || direction.equals("zudatei")) { - // MySQL → faqs.yml if (!plugin.getFaqManager().isUsingMySQL()) { player.sendMessage(plugin.lang().get("faq.migrate-no-mysql")); return; } @@ -883,7 +1069,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { }); }); } else { - // faqs.yml → MySQL if (!plugin.getFaqManager().isUsingMySQL()) { player.sendMessage(plugin.lang().get("faq.migrate-no-mysql")); return; } @@ -931,19 +1116,16 @@ public class TicketCommand implements CommandExecutor, TabCompleter { } } - // ── /ticket kategorie (category) ───────────────────────────────────── - // - // /ticket kategorie add [Farbe] [Beschreibung] - // Farbe optional, z.B. &b (Standard: &7) - // Beschreibung optional, alles nach der Farbe - // - // /ticket kategorie delete - // /ticket kategorie list + // ── /ticket kategorie ───────────────────────────────────────────────── private void handleFaqCategory(Player player, String[] args) { if (!player.hasPermission("ticket.admin")) { plugin.lang().send(player, "general.no-permission"); return; } + // FAQ-System deaktiviert? + if (!plugin.getConfig().getBoolean("faq-enabled", true)) { + plugin.lang().send(player, "faq.disabled"); return; + } if (args.length < 2) { player.sendMessage(plugin.lang().get("faqcat.usage")); return; } @@ -951,7 +1133,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { switch (args[1].toLowerCase()) { case "add", "hinzufügen", "hinzufuegen" -> { - // /ticket kategorie add [&Farbe] [Beschreibung...] if (args.length < 3) { player.sendMessage(plugin.lang().get("faqcat.usage-add")); return; } @@ -1069,9 +1250,25 @@ public class TicketCommand implements CommandExecutor, TabCompleter { @Override public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { List completions = new ArrayList<>(); + + // Konsolen-Tab-Completion + if (sender instanceof ConsoleCommandSender) { + if (args.length == 1) { + for (String s : List.of("create", "reload", "archive", "stats")) + if (s.startsWith(args[0].toLowerCase())) completions.add(s); + } else if (args.length == 2 && normalize(args[0]).equals("create")) { + // Spielernamen vorschlagen + for (Player p : Bukkit.getOnlinePlayers()) + if (p.getName().toLowerCase().startsWith(args[1].toLowerCase())) completions.add(p.getName()); + } else if (args.length == 3 && normalize(args[0]).equals("create")) { + // Kategorien vorschlagen + completions.addAll(plugin.getCategoryManager().getDisplayNamesForTabComplete(args[2])); + } + return completions; + } + if (!(sender instanceof Player player)) return completions; - // Nur die in der Config eingestellte Sprache verwenden final boolean useDe = plugin.lang().acceptsGerman(); final boolean useEn = plugin.lang().acceptsEnglish(); @@ -1091,11 +1288,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { if (useDe) subs.add("priorität"); } - if (player.hasPermission("ticket.admin")) { - if (useEn) subs.addAll(List.of("faq")); - if (useDe) subs.addAll(List.of("faq")); - } - if (player.hasPermission("ticket.admin")) { subs.add("kategorie"); if (useEn) subs.add("category"); @@ -1123,7 +1315,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { && (args[1].equalsIgnoreCase("add") || args[1].equalsIgnoreCase("hinzufügen") || args[1].equalsIgnoreCase("hinzufuegen")) && player.hasPermission("ticket.admin")) { - // /ticket faq add → FAQ-Kategorie-Schlüssel vorschlagen completions.addAll(getFaqCategoryKeysForTab(args[2])); } else if (args.length == 3 && normalize(args[0]).equals("faq") @@ -1136,7 +1327,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { } else if (args.length == 2 && (normalize(args[0]).equals("category") || args[0].equalsIgnoreCase("kategorie")) && player.hasPermission("ticket.admin")) { - // /ticket kategorie if (useEn) completions.addAll(List.of("add", "delete", "list")); if (useDe) completions.addAll(List.of("hinzufügen", "löschen", "liste")); completions.removeIf(s -> !s.startsWith(args[1].toLowerCase())); @@ -1146,21 +1336,17 @@ public class TicketCommand implements CommandExecutor, TabCompleter { && (args[1].equalsIgnoreCase("delete") || args[1].equalsIgnoreCase("remove") || args[1].equalsIgnoreCase("löschen") || args[1].equalsIgnoreCase("loeschen")) && player.hasPermission("ticket.admin")) { - // /ticket kategorie delete → vorhandene Schlüssel completions.addAll(getFaqCategoryKeysForTab(args[2])); } else if (args.length == 2 && normalize(args[0]).equals("create")) { boolean categoriesOn = plugin.getConfig().getBoolean("categories-enabled", true); boolean prioritiesOn = plugin.getConfig().getBoolean("priorities-enabled", true); - - // Wenn Kategorien aktiviert: zeige nur Kategorien-Anzeigenamen bei args[1] + if (categoriesOn) { completions.addAll(plugin.getCategoryManager().getDisplayNamesForTabComplete(args[1])); } else if (prioritiesOn) { - // Wenn nur Prioritäten aktiviert (keine Kategorien): zeige Prioritäten bei args[1] boolean allowPlayersPrio = plugin.getConfig().getBoolean("allow-players-to-set-priority", false); - boolean isTeam = sender instanceof Player p && - (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")); + boolean isTeam = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"); if (allowPlayersPrio || isTeam) { completions.addAll(getPriorityInputsForTab(args[1])); } @@ -1169,12 +1355,10 @@ public class TicketCommand implements CommandExecutor, TabCompleter { } else if (args.length == 3 && normalize(args[0]).equals("create")) { boolean categoriesOn = plugin.getConfig().getBoolean("categories-enabled", true); boolean prioritiesOn = plugin.getConfig().getBoolean("priorities-enabled", true); - - // Wenn beide aktiviert: args[2] = Priorität + if (categoriesOn && prioritiesOn) { boolean allowPlayersPrio = plugin.getConfig().getBoolean("allow-players-to-set-priority", false); - boolean isTeam = sender instanceof Player p && - (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")); + boolean isTeam = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"); if (allowPlayersPrio || isTeam) { completions.addAll(getPriorityInputsForTab(args[2])); } diff --git a/src/main/java/de/ticketsystem/manager/LanguageManager.java b/src/main/java/de/ticketsystem/manager/LanguageManager.java index a5b8e50..0417cce 100644 --- a/src/main/java/de/ticketsystem/manager/LanguageManager.java +++ b/src/main/java/de/ticketsystem/manager/LanguageManager.java @@ -1,8 +1,14 @@ package de.ticketsystem.manager; import de.ticketsystem.TicketPlugin; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; import java.io.File; import java.io.IOException; @@ -20,8 +26,13 @@ import java.util.regex.Pattern; * Lädt alle Plugin-Texte aus der aktiven Sprachdatei (lang_de.yml / lang_en.yml) * und ersetzt {cmd_X}-Platzhalter durch die passenden Befehlsnamen. * - * Unterstützt Hex-Farbcodes (z.B. &#FF0055 oder <#FF0055>). - * Funktioniert ab Spigot 1.16+. + * Unterstützt: + * - &-Farbcodes und Hex-Farbcodes (&#RRGGBB oder <#RRGGBB>) + * - MiniMessage-Tags wie , , etc. + * → Texte mit MiniMessage-Tags werden als Adventure-Component gesendet, + * damit click/hover-Events tatsächlich funktionieren. + * + * Funktioniert ab Paper/Spigot 1.16+ (Adventure-API vorausgesetzt, ab 1.18 eingebaut). * * ┌─────────────────────────────────────────────────────────┐ * │ Einziger Konfigurations-Schlüssel: language │ @@ -29,9 +40,6 @@ import java.util.regex.Pattern; * │ language: de → deutsche Texte + deutsche Befehle │ * │ language: en → englische Texte + englische Befehle │ * │ language: both → deutsche Texte + beide Befehlsnamen │ - * │ │ - * │ "command-language" existiert nicht mehr und wird │ - * │ vollständig ignoriert. │ * └─────────────────────────────────────────────────────────┘ * * Verfügbare {cmd_X}-Platzhalter in lang.yml: @@ -53,6 +61,17 @@ public class LanguageManager { "blacklist.list-", "gui.", "join.pending-header", "update." }; + /** + * Erkennt MiniMessage-Tags, die interaktive Inhalte enthalten: + * click, hover, insertion. Nur wenn solche Tags vorhanden sind, + * wird die Nachricht als Component (statt als Legacy-String) gesendet. + */ + private static final Pattern MINIMESSAGE_INTERACTIVE = + Pattern.compile("<(?:click|hover|insertion)[^>]*>", Pattern.CASE_INSENSITIVE); + + /** MiniMessage-Instanz (singleton, thread-safe). */ + private static final MiniMessage MINI = MiniMessage.miniMessage(); + // ── Befehlsnamen-Tabellen (statisch, ändern sich nie) ─────────────────── private static final LinkedHashMap DE = new LinkedHashMap<>(); @@ -104,7 +123,6 @@ public class LanguageManager { /** * Aktiver Sprachmodus – wird bei jedem load() DIREKT aus der Config gelesen. - * Kein Cache, kein Zwischenwert. Immer frisch nach reloadConfig(). */ private String activeLang; @@ -120,23 +138,26 @@ public class LanguageManager { */ private Map cmdNames = new LinkedHashMap<>(); + /** + * BukkitAudiences – Brücke zwischen Adventure-Components und Spigot. + * Muss beim Plugin-onDisable via audiences.close() geschlossen werden. + * Siehe TicketPlugin.onDisable(). + */ + private final BukkitAudiences audiences; + // ── Konstruktor ────────────────────────────────────────────────────────── public LanguageManager(TicketPlugin plugin) { - this.plugin = plugin; + this.plugin = plugin; + this.audiences = BukkitAudiences.create(plugin); load(); } // ── Laden ──────────────────────────────────────────────────────────────── - /** - * Lädt (oder relädt) die Sprachdatei und baut alle Befehlsnamen neu. - * Muss nach plugin.reloadConfig() aufgerufen werden, damit die frische - * language-Einstellung übernommen wird. - */ public void load() { - // 1. language aus der (bereits neu geladenen) Config lesen + // 1. language aus der Config lesen String raw = plugin.getConfig().getString("language", FALLBACK) .toLowerCase().trim(); @@ -204,20 +225,13 @@ public class LanguageManager { // ── Befehlsnamen ───────────────────────────────────────────────────────── - /** - * Baut {cmd_X} → Anzeigename anhand von activeLang. - * - * de → /ticket erstellen - * en → /ticket create - * both → /ticket create §8(§7erstellen§8) - */ private Map buildCmdNames() { Map map = new LinkedHashMap<>(); for (String key : EN.keySet()) { String display = switch (activeLang) { case "en" -> "/ticket " + EN.get(key); case "both" -> "/ticket " + EN.get(key) + " §8(§7" + DE.get(key) + "§8)"; - default -> "/ticket " + DE.get(key); // "de" + alle unbekannten + default -> "/ticket " + DE.get(key); }; map.put("{cmd_" + key + "}", display); } @@ -232,12 +246,9 @@ public class LanguageManager { }; } - // ── Befehlssprache-Abfragen (für TicketCommand) ────────────────────────── + // ── Befehlssprache-Abfragen ────────────────────────────────────────────── - /** true wenn deutsche Subkommandos akzeptiert werden sollen (Tab-Complete & Eingabe). */ public boolean acceptsGerman() { return "de".equals(activeLang) || "both".equals(activeLang); } - - /** true wenn englische Subkommandos akzeptiert werden sollen (Tab-Complete & Eingabe). */ public boolean acceptsEnglish() { return "en".equals(activeLang) || "both".equals(activeLang); } // ── Interne Platzhalter-Ersetzung ─────────────────────────────────────── @@ -281,11 +292,48 @@ public class LanguageManager { return prefix + format(key, replacements); } - /** Sendet eine Nachricht (mit Prefix wenn nötig) an einen CommandSender. */ + /** + * Sendet eine Nachricht an einen CommandSender. + * + * Enthält der Rohtext MiniMessage-Tags mit interaktiven Elementen + * (click, hover, insertion), wird die Nachricht als Adventure-Component + * gesendet – damit run_command, suggest_command und hover-Texte + * tatsächlich funktionieren. + * + * Enthält er keine solchen Tags, wird der klassische Legacy-Pfad + * (§-Codes) verwendet – volle Abwärtskompatibilität. + */ public void send(CommandSender sender, String key, String... replacements) { - sender.sendMessage(needsPrefix(key) - ? prefix + format(key, replacements) - : format(key, replacements)); + String raw = applyCmdNames(getRaw(key)); + + // {placeholder}-Ersetzungen anwenden (vor MiniMessage-Parsing!) + if (replacements.length % 2 != 0) + plugin.getLogger().warning("[LanguageManager] send() benötigt eine gerade Anzahl an Argumenten für: " + key); + for (int i = 0; i + 1 < replacements.length; i += 2) + raw = raw.replace(replacements[i], replacements[i + 1]); + + if (hasMiniMessageInteractiveTags(raw)) { + // MiniMessage-Pfad: interaktive Komponenten (click/hover) + // Legacy-&-Farbcodes in MiniMessage-Äquivalente umwandeln damit + // beide Syntax-Varianten in derselben Nachricht funktionieren. + String mm = legacyToMiniMessage(raw); + Component component = MINI.deserialize(mm); + + // Prefix als Component voranstellen wenn nötig + if (needsPrefix(key)) { + Component prefixComp = MINI.deserialize(legacyToMiniMessage( + color("&8[&6Ticket&8] &r"))); + component = prefixComp.append(component); + } + + // BukkitAudiences sendet korrekt auf Spigot UND Paper + Audience audience = audiences.sender(sender); + audience.sendMessage(component); + } else { + // Legacy-Pfad: §-Codes, kein Adventure nötig + String text = color(raw); + sender.sendMessage(needsPrefix(key) ? prefix + text : text); + } } /** Sendet die Trennlinie. */ @@ -304,11 +352,12 @@ public class LanguageManager { /** * Übersetzt &-Farbcodes und Hex-Farbcodes (&#RRGGBB oder <#RRGGBB>) in §-Codes. + * Wird für alle Texte verwendet die NICHT als Component gesendet werden. */ public String color(String text) { if (text == null || text.isEmpty()) return ""; - // Regex für Hex Codes: &#RRGGBB oder <#RRGGBB> + // Hex-Codes: &#RRGGBB oder <#RRGGBB> Pattern hexPattern = Pattern.compile("&#([A-Fa-f0-9]{6})|<#([A-Fa-f0-9]{6})>"); Matcher matcher = hexPattern.matcher(text); StringBuffer buffer = new StringBuffer(); @@ -316,29 +365,126 @@ public class LanguageManager { while (matcher.find()) { String group = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); try { - // Fix: Explizite Nutzung von net.md_5.bungee.api.ChatColor für Hex-Support (Spigot 1.16+) - // Dies verhindert den "Symbol nicht gefunden"-Fehler beim Kompilieren mit der reinen Bukkit-API. net.md_5.bungee.api.ChatColor hexColor = net.md_5.bungee.api.ChatColor.of("#" + group); matcher.appendReplacement(buffer, hexColor.toString()); } catch (IllegalArgumentException e) { - // Falls der Farbcode ungültig ist, Tag entfernen matcher.appendReplacement(buffer, ""); } } String parsed = matcher.appendTail(buffer).toString(); - - // Übersetzung der klassischen &-Farbcodes (Bukkit Standard) return org.bukkit.ChatColor.translateAlternateColorCodes('&', parsed); } - public String getPrefix() { return prefix; } - public String getActiveLang() { return activeLang; } - public String getFileLang() { return fileLang; } + public String getPrefix() { return prefix; } + public String getActiveLang() { return activeLang; } + public String getFileLang() { return fileLang; } + public BukkitAudiences getAudiences() { return audiences; } /** Relädt die Sprachdatei. Muss NACH plugin.reloadConfig() aufgerufen werden. */ public void reload() { load(); } + // ── MiniMessage-Hilfsmethoden ──────────────────────────────────────────── + + /** + * Prüft ob ein Text MiniMessage-Tags mit interaktiven Inhalten enthält + * (click, hover, insertion). Nur dann lohnt sich der Component-Pfad. + */ + private boolean hasMiniMessageInteractiveTags(String text) { + if (text == null) return false; + return MINIMESSAGE_INTERACTIVE.matcher(text).find(); + } + + /** + * Konvertiert &-Farbcodes und &#RRGGBB/<#RRGGBB>-Hex-Codes in MiniMessage-Syntax, + * damit ein Text der beide Formate mischt korrekt von MiniMessage geparst werden kann. + * + * Beispiele: + * &6Hallo → Hallo + * &#FF5500Text → <#FF5500>Text + * &lFett → Fett + * &rReset → Reset + */ + private String legacyToMiniMessage(String text) { + if (text == null) return ""; + + // 1. &#RRGGBB → <#RRGGBB> + text = text.replaceAll("&#([A-Fa-f0-9]{6})", "<#$1>"); + + // 2. §-Codes (bereits konvertierte Hex-Codes §x§R§G§B... überspringen wir hier, + // da sie nicht in MiniMessage passen – nur &-Shorthand-Codes mappen) + // Wir ersetzen &X-Codes durch ihre MiniMessage-Äquivalente. + StringBuilder sb = new StringBuilder(); + char[] chars = text.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if ((chars[i] == '&' || chars[i] == '§') && i + 1 < chars.length) { + char code = Character.toLowerCase(chars[i + 1]); + String mm = switch (code) { + case '0' -> ""; + case '1' -> ""; + case '2' -> ""; + case '3' -> ""; + case '4' -> ""; + case '5' -> ""; + case '6' -> ""; + case '7' -> ""; + case '8' -> ""; + case '9' -> ""; + case 'a' -> ""; + case 'b' -> ""; + case 'c' -> ""; + case 'd' -> ""; + case 'e' -> ""; + case 'f' -> ""; + case 'l' -> ""; + case 'm' -> ""; + case 'n' -> ""; + case 'o' -> ""; + case 'r' -> ""; + default -> null; + }; + if (mm != null) { + sb.append(mm); + i++; // Code-Zeichen überspringen + continue; + } + } + sb.append(chars[i]); + } + return sb.toString(); + } + + /** + * Sendet eine Adventure-Component an einen CommandSender. + * + * Der Cast auf net.kyori.adventure.audience.Audience umgeht das compile-time + * Problem, dass Spigots CommandSender-Interface sendMessage(Component) nicht + * direkt exponiert – zur Laufzeit implementiert jeder CommandSender auf + * Paper/Spigot 1.18+ das Audience-Interface. + */ + private void sendComponent(CommandSender sender, Component component) { + if (sender instanceof net.kyori.adventure.audience.Audience audience) { + try { + audience.sendMessage(component); + } catch (NoSuchMethodError | AbstractMethodError e) { + // Fallback für reines Spigot ohne Adventure-Patch: + // Component in Legacy-String serialisieren (click/hover gehen verloren, + // aber der Text bleibt lesbar) + String legacy = LegacyComponentSerializer.legacySection().serialize(component); + sender.sendMessage(legacy); + if (plugin.isDebug()) { + plugin.getLogger().info("[LanguageManager] Adventure-API nicht verfügbar – " + + "interaktive Tags werden als Plain-Text gesendet. " + + "Wechsel auf Paper für volle MiniMessage-Unterstützung."); + } + } + } else { + // Kein Audience (z. B. alte Konsole) → Legacy-Fallback + String legacy = LegacyComponentSerializer.legacySection().serialize(component); + sender.sendMessage(legacy); + } + } + // ── Intern ─────────────────────────────────────────────────────────────── private boolean needsPrefix(String key) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ba07e3d..fb78d34 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -209,6 +209,9 @@ discord: # ============================================================ # GUI KONFIGURATION (Layouts, Slots, Items) # ============================================================ +# FAQ-System global deaktivieren (Standard: true) +faq-enabled: true + # Hier kannst du das Aussehen und die Anordnung der GUIs anpassen. # WICHTIG: gui-settings muss ganz links stehen (keine Raute davor!). gui-settings: diff --git a/src/main/resources/lang_de.yml b/src/main/resources/lang_de.yml index c585083..296aa2d 100644 --- a/src/main/resources/lang_de.yml +++ b/src/main/resources/lang_de.yml @@ -576,7 +576,6 @@ web: login-blocked: "Zu viele Fehlversuche. Bitte warte {seconds} Sekunden." archive-btn-restore: "Wiederherstellen" archive-btn-delete: "Permanent löschen" - detail-btn-archive: "Ins Archiv verschieben" login-label-user: "Benutzername" login-label-pass: "Passwort" login-btn: "Anmelden" @@ -615,13 +614,11 @@ web: tickets-col-prio: "Priorität" tickets-col-status: "Status" tickets-col-created: "Erstellt" - nav-archive: "Archiv" filter-all-status: "Alle Status" filter-open: "Offen" filter-claimed: "Angenommen" filter-forwarded: "Weitergeleitet" filter-closed: "Geschlossen" - filter-archived: "Archiviert" filter-all-cat: "Alle Kategorien" filter-all-prio: "Alle Prioritäten" filter-low: "Niedrig" diff --git a/src/main/resources/lang_en.yml b/src/main/resources/lang_en.yml index e433caf..b171cd9 100644 --- a/src/main/resources/lang_en.yml +++ b/src/main/resources/lang_en.yml @@ -576,7 +576,6 @@ web: login-error: "Username or password incorrect." archive-btn-restore: "Restore" archive-btn-delete: "Delete permanently" - detail-btn-archive: "Move to archive" login-label-user: "Username" login-label-pass: "Password" login-btn: "Sign In" @@ -615,13 +614,11 @@ web: tickets-col-prio: "Priority" tickets-col-status: "Status" tickets-col-created: "Created" - nav-archive: "Archive" filter-all-status: "All Statuses" filter-open: "Open" filter-claimed: "Claimed" filter-forwarded: "Forwarded" filter-closed: "Closed" - filter-archived: "Archived" filter-all-cat: "All Categories" filter-all-prio: "All Priorities" filter-low: "Low" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index fc7189c..38c356a 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: TicketSystem -version: 1.1.4 +version: 1.1.5 main: de.ticketsystem.TicketPlugin api-version: 1.20 author: M_Viper