diff --git a/src/main/java/de/ticketsystem/TicketPlugin.java b/src/main/java/de/ticketsystem/TicketPlugin.java index f115350..8e6419e 100644 --- a/src/main/java/de/ticketsystem/TicketPlugin.java +++ b/src/main/java/de/ticketsystem/TicketPlugin.java @@ -10,6 +10,7 @@ import de.ticketsystem.gui.TicketGUI; import de.ticketsystem.listeners.PlayerJoinListener; import de.ticketsystem.manager.CategoryManager; import de.ticketsystem.manager.FaqManager; +import de.ticketsystem.manager.LanguageManager; import de.ticketsystem.manager.TicketManager; import de.ticketsystem.model.Ticket; import org.bukkit.ChatColor; @@ -29,6 +30,7 @@ public class TicketPlugin extends JavaPlugin { */ private String serverName; + private LanguageManager languageManager; private DatabaseManager databaseManager; private TicketManager ticketManager; private CategoryManager categoryManager; @@ -48,6 +50,10 @@ public class TicketPlugin extends JavaPlugin { // Ticket-Klasse für YAML-Serialisierung registrieren Ticket.register(); + // ── Sprachdatei laden (lang.yml) ────────────────────────────────── + // Muss VOR allen anderen Managern geschehen, da diese lang() nutzen. + languageManager = new LanguageManager(this); + // ── BungeeCord Plugin-Messaging-Kanäle registrieren ─────────────── getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.BUNGEE_CHANNEL); getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.CUSTOM_CHANNEL); @@ -66,14 +72,14 @@ public class TicketPlugin extends JavaPlugin { getLogger().info("[BungeeCord] Cross-Server-Features deaktiviert. Setze 'bungeecord: true' um sie zu aktivieren."); } - // Update-Checker (nur Warnung wenn Update verfügbar – kein API-Raw-Log) + // Update-Checker int resourceId = 132757; new UpdateChecker(this, resourceId).getVersion(version -> { String current = getDescription().getVersion(); if (!current.equals(version)) { - String msg = ChatColor.translateAlternateColorCodes('&', - "&6[TicketSystem] &eNeue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")"); - getLogger().warning("Neue Version verfügbar: " + version + " (aktuell: " + current + ")"); + String msg = lang().format("update.available-line1", "{version}", version); + getLogger().warning(lang().format("update.available-console", + "{new}", version, "{current}", current)); getServer().getScheduler().runTaskLater(this, () -> getServer().getOnlinePlayers().stream() .filter(p -> p.hasPermission("ticket.admin")) @@ -83,7 +89,7 @@ public class TicketPlugin extends JavaPlugin { // Versionsprüfung der config.yml String configVersion = getConfig().getString("version", ""); - String expectedVersion = "2.0"; + String expectedVersion = "2.2"; if (!expectedVersion.equals(configVersion)) { getLogger().warning("[WARNUNG] config.yml-Version (" + configVersion + ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!"); @@ -156,29 +162,125 @@ public class TicketPlugin extends JavaPlugin { // ─────────────────────────── Hilfsmethoden ───────────────────────────── - public String formatMessage(String path) { - String prefix = color(getConfig().getString("prefix", "&8[&6Ticket&8] &r")); - String message = getConfig().getString(path, "&cNachricht nicht gefunden: " + path); - return prefix + color(message); + /** + * Gibt den LanguageManager zurück – bevorzugte Methode für alle Texte. + * + * Verwendung: + * plugin.lang().get("ticket.created") + * plugin.lang().format("ticket.created", "{id}", String.valueOf(id)) + * plugin.lang().send(player, "ticket.created", "{id}", String.valueOf(id)) + */ + public LanguageManager lang() { + return languageManager; } + /** + * Kompatibilitätsmethode für bestehenden Code. + * Liest Pfade der Form "messages.xxx" aus lang.yml (ohne "messages."-Prefix). + * + * Beispiel: formatMessage("messages.ticket-created") → lang "ticket.created" + * + * @deprecated Direkt {@link #lang()} verwenden. + */ + @Deprecated + public String formatMessage(String path) { + // "messages.ticket-created" → "ticket.created" (legacy-Mapping) + String langKey = mapLegacyPath(path); + if (langKey != null) { + return lang().formatWithPrefix(langKey); + } + // Fallback: Direkt in lang.yml nachschlagen + String value = lang().getRaw(path); + return lang().getPrefix() + lang().color(value); + } + + /** + * Übersetzt &-Farbcodes in §-Farbcodes. + * Kompatibilitätsmethode – bevorzugt lang().color() verwenden. + */ public String color(String text) { return ChatColor.translateAlternateColorCodes('&', text); } + /** + * Mappt alte "messages.xxx"-Pfade auf neue lang.yml-Pfade. + * Muss ergänzt werden wenn neue Schlüssel im alten Stil genutzt wurden. + */ + private String mapLegacyPath(String path) { + if (path == null) return null; + return switch (path) { + case "messages.export-success" -> "system.export-success"; + case "messages.export-fail" -> "system.export-fail"; + case "messages.import-success" -> "system.import-success"; + case "messages.import-fail" -> "system.import-fail"; + case "messages.migration-success" -> "system.migration-success"; + case "messages.migration-fail" -> "system.migration-fail"; + case "messages.archive-success" -> "system.archive-success"; + case "messages.archive-fail" -> "system.archive-fail"; + case "messages.file-not-found" -> "system.file-not-found"; + case "messages.unknown-mode" -> "system.unknown-mode"; + case "messages.validation-warning" -> "system.validation-warning"; + case "messages.ticket-created" -> "ticket.created"; + case "messages.ticket-created-category" -> "ticket.created-category"; + case "messages.ticket-claimed" -> "ticket.claimed"; + case "messages.ticket-claimed-notify" -> "ticket.claimed-notify"; + case "messages.ticket-closed" -> "ticket.closed"; + case "messages.ticket-closed-notify" -> "ticket.closed-notify"; + case "messages.ticket-forwarded" -> "ticket.forwarded"; + case "messages.ticket-forwarded-notify" -> "ticket.forwarded-notify"; + case "messages.ticket-forwarded-creator-notify" -> "ticket.forwarded-creator"; + case "messages.new-ticket-notify" -> "ticket.new-notify"; + case "messages.comment-saved" -> "comment.saved"; + case "messages.comment-notify" -> "comment.notify-online"; + case "messages.comment-no-permission" -> "comment.no-permission"; + case "messages.rating-saved-good" -> "rating.saved-good"; + case "messages.rating-saved-bad" -> "rating.saved-bad"; + case "messages.rating-already-rated" -> "rating.already-rated"; + case "messages.rating-not-yours" -> "rating.not-yours"; + case "messages.rating-disabled" -> "rating.disabled"; + case "messages.rating-prompt" -> "rating.prompt-title"; + case "messages.blacklist-added" -> "blacklist.added"; + case "messages.blacklist-removed" -> "blacklist.removed"; + case "messages.blacklist-already" -> "blacklist.already"; + case "messages.blacklist-not-found" -> "blacklist.not-found"; + case "messages.blacklist-blocked" -> "create.blacklist-blocked"; + case "messages.no-permission" -> "general.no-permission"; + case "messages.no-open-tickets" -> "general.no-open-tickets"; + case "messages.join-open-tickets" -> "join.open-tickets"; + case "messages.already-claimed" -> "general.already-claimed"; + case "messages.ticket-not-found" -> "general.ticket-not-found"; + case "messages.cooldown" -> "general.cooldown"; + case "messages.category-invalid" -> "create.category-invalid"; + default -> null; + }; + } + // ─────────────────────────── Getter ──────────────────────────────────── + /** + * Aktualisiert serverName und debug-Flag aus der (bereits neu geladenen) Config. + * Muss nach plugin.reloadConfig() aufgerufen werden. + */ + public void reloadSettings() { + serverName = getConfig().getString("server-name", "unknown"); + if ("unknown".equals(serverName)) { + getLogger().warning("[BungeeCord] Kein 'server-name' in der config.yml definiert!"); + } + debug = getConfig().getBoolean("debug", false); + } + public static TicketPlugin getInstance() { return instance; } - public DatabaseManager getDatabaseManager() { return databaseManager; } - public TicketManager getTicketManager() { return ticketManager; } - public CategoryManager getCategoryManager() { return categoryManager; } - public FaqManager getFaqManager() { return faqManager; } - public TicketGUI getTicketGUI() { return ticketGUI; } - public FaqGUI getFaqGUI() { return faqGUI; } - public DiscordWebhook getDiscordWebhook() { return discordWebhook; } - public BungeeMessenger getBungeeMessenger() { return bungeeMessenger; } - public TicketCache getTicketCache() { return ticketCache; } - public boolean isDebug() { return debug; } - public String getServerName() { return serverName; } - public boolean isBungeeCordEnabled() { return getConfig().getBoolean("bungeecord", false); } + public LanguageManager getLanguageManager() { return languageManager; } + public DatabaseManager getDatabaseManager() { return databaseManager; } + public TicketManager getTicketManager() { return ticketManager; } + public CategoryManager getCategoryManager() { return categoryManager; } + public FaqManager getFaqManager() { return faqManager; } + public TicketGUI getTicketGUI() { return ticketGUI; } + public FaqGUI getFaqGUI() { return faqGUI; } + public DiscordWebhook getDiscordWebhook() { return discordWebhook; } + public BungeeMessenger getBungeeMessenger() { return bungeeMessenger; } + public TicketCache getTicketCache() { return ticketCache; } + public boolean isDebug() { return debug; } + public String getServerName() { return serverName; } + public boolean isBungeeCordEnabled() { return getConfig().getBoolean("bungeecord", false); } } \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/bungee/BungeeMessenger.java b/src/main/java/de/ticketsystem/bungee/BungeeMessenger.java index be406df..717aa32 100644 --- a/src/main/java/de/ticketsystem/bungee/BungeeMessenger.java +++ b/src/main/java/de/ticketsystem/bungee/BungeeMessenger.java @@ -149,9 +149,9 @@ public class BungeeMessenger implements PluginMessageListener { ByteArrayDataOutput inner = ByteStreams.newDataOutput(); inner.writeByte(TYPE_PLAYER_MSG); - inner.writeShort(uuidBytes.length); + inner.writeInt(uuidBytes.length); inner.write(uuidBytes); - inner.writeShort(msgBytes.length); + inner.writeInt(msgBytes.length); inner.write(msgBytes); byte[] innerBytes = inner.toByteArray(); @@ -159,7 +159,7 @@ public class BungeeMessenger implements PluginMessageListener { out.writeUTF("Forward"); out.writeUTF("ALL"); out.writeUTF(CUSTOM_CHANNEL); - out.writeShort(innerBytes.length); + out.writeInt(innerBytes.length); out.write(innerBytes); messenger.sendPluginMessage(plugin, BUNGEE_CHANNEL, out.toByteArray()); @@ -198,12 +198,12 @@ public class BungeeMessenger implements PluginMessageListener { ); } else if (type == TYPE_PLAYER_MSG) { - int uuidLen = in.readShort(); + int uuidLen = in.readInt(); byte[] uuidBytes = new byte[uuidLen]; in.readFully(uuidBytes); UUID targetUUID = UUID.fromString(new String(uuidBytes, StandardCharsets.UTF_8)); - int msgLen = in.readShort(); + int msgLen = in.readInt(); byte[] msgBytes = new byte[msgLen]; in.readFully(msgBytes); String message = new String(msgBytes, StandardCharsets.UTF_8); @@ -246,7 +246,7 @@ public class BungeeMessenger implements PluginMessageListener { out.writeUTF("Forward"); out.writeUTF("ALL"); out.writeUTF(CUSTOM_CHANNEL); - out.writeShort(innerBytes.length); + out.writeInt(innerBytes.length); out.write(innerBytes); messenger.sendPluginMessage(plugin, BUNGEE_CHANNEL, out.toByteArray()); diff --git a/src/main/java/de/ticketsystem/commands/TicketCommand.java b/src/main/java/de/ticketsystem/commands/TicketCommand.java index d436cf1..7905bdd 100644 --- a/src/main/java/de/ticketsystem/commands/TicketCommand.java +++ b/src/main/java/de/ticketsystem/commands/TicketCommand.java @@ -1,13 +1,12 @@ package de.ticketsystem.commands; import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.ConfigCategory; import de.ticketsystem.model.FaqEntry; import de.ticketsystem.model.Ticket; - -import de.ticketsystem.manager.CategoryManager; -import de.ticketsystem.model.ConfigCategory; import de.ticketsystem.model.TicketComment; import de.ticketsystem.model.TicketPriority; +import de.ticketsystem.manager.CategoryManager; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.command.Command; @@ -15,27 +14,68 @@ import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; + import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.UUID; public class TicketCommand implements CommandExecutor, TabCompleter { private final TicketPlugin plugin; - public TicketCommand(TicketPlugin plugin) { this.plugin = plugin; } + public TicketCommand(TicketPlugin plugin) { + this.plugin = plugin; + } + + // ── 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", + "beanspruchen" -> "claim"; + case "schließen", "schliessen" -> "close"; + case "weiterleiten" -> "forward"; + case "neuladen" -> "reload"; + case "migrieren" -> "migrate"; + case "exportieren" -> "export"; + case "importieren" -> "import"; + case "statistik" -> "stats"; + case "archivieren" -> "archive"; + case "kommentar" -> "comment"; + case "sperrliste" -> "blacklist"; + case "bewerten" -> "rate"; + case "priorität", "prioritaet" -> "setpriority"; + case "hilfe" -> "help"; + // Englisch + alles andere → unverändert + default -> input.toLowerCase(); + }; + } + + // ── onCommand ───────────────────────────────────────────────────────── @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!(sender instanceof Player player)) { - sender.sendMessage("Dieser Befehl kann nur von Spielern ausgeführt werden."); + sender.sendMessage(plugin.lang().get("general.console-only")); return true; } - if (args.length == 0) { plugin.getTicketManager().sendHelpMessage(player); return true; } - switch (args[0].toLowerCase()) { + if (args.length == 0) { + plugin.getTicketManager().sendHelpMessage(player); + return true; + } + + switch (normalize(args[0])) { case "create" -> handleCreate(player, args); case "list" -> handleList(player); case "claim" -> handleClaim(player, args); @@ -58,240 +98,94 @@ public class TicketCommand implements CommandExecutor, TabCompleter { return true; } - // ─────────────────────────── /ticket faq ─────────────────────────────── - - /** - * /ticket faq – öffnet die FAQ-GUI (alle Spieler) - * /ticket faq add | – fügt ein FAQ hinzu (ticket.admin) - * /ticket faq edit | – bearbeitet ein FAQ (ticket.admin) - * /ticket faq delete – 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 | ──────────────────────── - 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 | ")); - 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 | ────────────────── - 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 | ")); - 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 ───────────────────────────────────── - 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 ")); 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 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 &8| &e/ticket faq delete ")); - } - } - - 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) { if (!player.hasPermission("ticket.create")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + plugin.lang().send(player, "general.no-permission"); return; } - if (plugin.getDatabaseManager().isBlacklisted(player.getUniqueId())) { - player.sendMessage(plugin.color("&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen.")); - return; + plugin.lang().send(player, "create.blacklist-blocked"); return; } - if (args.length < 2) { - player.sendMessage(plugin.color("&cBenutzung: /ticket create [Kategorie] [Priorität] ")); - if (plugin.getConfig().getBoolean("categories-enabled", true)) { - player.sendMessage(plugin.color("&7Kategorien: &ebug&7, &efrage&7, &ebeschwerde&7, &esonstiges&7, &eallgemein")); - } - if (plugin.getConfig().getBoolean("priorities-enabled", true)) { - player.sendMessage(plugin.color("&7Prioritäten: &alow&7, &enormal&7, &6high&7, &curgent")); - } + player.sendMessage(plugin.lang().get("create.usage")); + if (plugin.getConfig().getBoolean("categories-enabled", true)) + player.sendMessage(plugin.lang().get("create.categories-hint")); + if (plugin.getConfig().getBoolean("priorities-enabled", true)) + player.sendMessage(plugin.lang().get("create.priorities-hint")); return; } - if (plugin.getTicketManager().hasCooldown(player.getUniqueId())) { - long remaining = plugin.getTicketManager().getRemainingCooldown(player.getUniqueId()); - player.sendMessage(plugin.formatMessage("messages.cooldown").replace("{seconds}", String.valueOf(remaining))); + long rem = plugin.getTicketManager().getRemainingCooldown(player.getUniqueId()); + plugin.lang().send(player, "general.cooldown", "{seconds}", String.valueOf(rem)); return; } if (plugin.getTicketManager().hasReachedTicketLimit(player.getUniqueId())) { int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2); - player.sendMessage(plugin.color("&cDu hast bereits &e" + max + " &coffene Ticket(s). Bitte warte, bis dein Ticket bearbeitet wurde.")); + plugin.lang().send(player, "create.max-tickets", "{max}", String.valueOf(max)); return; } - CategoryManager cm = plugin.getCategoryManager(); + CategoryManager cm = plugin.getCategoryManager(); ConfigCategory category = cm.getDefault(); TicketPriority priority = TicketPriority.NORMAL; - int messageStartIndex = 1; + 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); if (args.length >= 3) { if (categoriesOn) { ConfigCategory parsedCat = cm.resolve(args[1]); if (parsedCat != null) { category = parsedCat; - messageStartIndex = 2; + msgStart = 2; if (prioritiesOn && args.length >= 4) { TicketPriority parsedPrio = parsePriority(args[2]); - if (parsedPrio != null) { - priority = parsedPrio; - messageStartIndex = 3; - } - } - } else { - if (prioritiesOn) { - TicketPriority parsedPrio = parsePriority(args[1]); - if (parsedPrio != null) { - priority = parsedPrio; - messageStartIndex = 2; - } + if (parsedPrio != null) { priority = parsedPrio; msgStart = 3; } } + } else if (prioritiesOn) { + TicketPriority parsedPrio = parsePriority(args[1]); + if (parsedPrio != null) { priority = parsedPrio; msgStart = 2; } } } else if (prioritiesOn) { TicketPriority parsedPrio = parsePriority(args[1]); - if (parsedPrio != null) { - priority = parsedPrio; - messageStartIndex = 2; - } + if (parsedPrio != null) { priority = parsedPrio; msgStart = 2; } } } - String message = String.join(" ", Arrays.copyOfRange(args, messageStartIndex, args.length)); - int maxLen = plugin.getConfig().getInt("max-description-length", 100); - if (message.isEmpty()) { - player.sendMessage(plugin.color("&cBitte gib eine Beschreibung für dein Ticket an.")); - return; - } - if (message.length() > maxLen) { - player.sendMessage(plugin.color("&cDeine Beschreibung ist zu lang! Maximal " + maxLen + " Zeichen.")); - return; - } + 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; } + + final ConfigCategory fCat = category; + final TicketPriority fPrio = priority; - final ConfigCategory finalCategory = category; - final TicketPriority finalPriority = priority; Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation()); - ticket.setCategoryKey(finalCategory.getKey()); - ticket.setPriority(finalPriority); + ticket.setCategoryKey(fCat.getKey()); + ticket.setPriority(fPrio); ticket.setServerName(plugin.getServerName()); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { int id = plugin.getDatabaseManager().createTicket(ticket); - if (id == -1) { player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets!")); return; } + if (id == -1) { + Bukkit.getScheduler().runTask(plugin, () -> plugin.lang().send(player, "system.db-create-error")); + return; + } ticket.setId(id); - // Cache befüllen plugin.getTicketCache().put(ticket); plugin.getTicketManager().setCooldown(player.getUniqueId()); Bukkit.getScheduler().runTask(plugin, () -> { - String catInfo = plugin.getConfig().getBoolean("categories-enabled", true) - ? " §7[" + finalCategory.getColored() + "§7]" : ""; - String prioInfo = plugin.getConfig().getBoolean("priorities-enabled", true) - ? " §7[" + finalPriority.getColored() + "§7]" : ""; - player.sendMessage(plugin.formatMessage("messages.ticket-created").replace("{id}", String.valueOf(id)) + catInfo + prioInfo); + String catInfo = categoriesOn ? " §7[" + fCat.getColored() + "§7]" : ""; + String prioInfo = prioritiesOn ? " §7[" + fPrio.getColored() + "§7]" : ""; + player.sendMessage(plugin.lang().format("ticket.created", "{id}", String.valueOf(id)) + catInfo + prioInfo); plugin.getTicketManager().notifyTeam(ticket); }); }); } - // ─────────────────────────── /ticket list ────────────────────────────── + // ── /ticket list ────────────────────────────────────────────────────── private void handleList(Player player) { if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { @@ -303,120 +197,118 @@ public class TicketCommand implements CommandExecutor, TabCompleter { } } - // ─────────────────────────── /ticket claim ───────────────────────────── + // ── /ticket claim ───────────────────────────────────────────────────── private void handleClaim(Player player, String[] args) { if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + plugin.lang().send(player, "general.no-permission"); return; } - if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket claim ")); return; } + if (args.length < 2) { player.sendMessage(plugin.lang().get("claim.usage")); return; } int id; try { id = Integer.parseInt(args[1]); } - catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } + catch (NumberFormatException e) { plugin.lang().send(player, "general.invalid-id"); return; } final int ticketId = id; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().claimTicket(ticketId, player.getUniqueId(), player.getName()); + boolean ok = plugin.getDatabaseManager().claimTicket(ticketId, player.getUniqueId(), player.getName()); Bukkit.getScheduler().runTask(plugin, () -> { - if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; } - Ticket ticket = getCachedOrFetch(ticketId); - if (ticket == null) return; - - player.sendMessage(plugin.formatMessage("messages.ticket-claimed") - .replace("{id}", String.valueOf(ticketId)) - .replace("{player}", ticket.getCreatorName())); - plugin.getTicketManager().notifyCreatorClaimed(ticket); - plugin.getTicketCache().invalidate(ticketId); // Stale cache löschen + if (!ok) { plugin.lang().send(player, "general.already-claimed"); return; } + Ticket t = getCachedOrFetch(ticketId); + if (t == null) return; + player.sendMessage(plugin.lang().format("ticket.claimed", + "{id}", String.valueOf(ticketId), "{player}", t.getCreatorName())); + plugin.getTicketManager().notifyCreatorClaimed(t); + plugin.getTicketCache().invalidate(ticketId); }); }); } - // ─────────────────────────── /ticket close ───────────────────────────── + // ── /ticket close ───────────────────────────────────────────────────── private void handleClose(Player player, String[] args) { if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + plugin.lang().send(player, "general.no-permission"); return; } - if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket close [Kommentar]")); return; } + if (args.length < 2) { player.sendMessage(plugin.lang().get("close.usage")); return; } int id; try { id = Integer.parseInt(args[1]); } - catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } + catch (NumberFormatException e) { plugin.lang().send(player, "general.invalid-id"); return; } - String closeComment = args.length > 2 ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) : ""; - final int ticketId = id; - final String comment = closeComment; + final int ticketId = id; + final String comment = args.length > 2 + ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) : ""; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment); - if (success) { - Ticket ticket = getCachedOrFetch(ticketId); - if (ticket != null) plugin.getDatabaseManager().recordClosedTicket(ticket, player.getName()); + boolean ok = plugin.getDatabaseManager().closeTicket(ticketId, comment); + if (ok) { + Ticket t = getCachedOrFetch(ticketId); + if (t != null) plugin.getDatabaseManager().recordClosedTicket(t, player.getName()); plugin.getTicketCache().invalidate(ticketId); Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId))); - if (ticket != null) { - ticket.setCloseComment(comment); - plugin.getTicketManager().notifyCreatorClosed(ticket, player.getName()); + player.sendMessage(plugin.lang().format("ticket.closed", "{id}", String.valueOf(ticketId))); + if (t != null) { + t.setCloseComment(comment); + plugin.getTicketManager().notifyCreatorClosed(t, player.getName()); } }); } else { - Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); + Bukkit.getScheduler().runTask(plugin, () -> plugin.lang().send(player, "general.ticket-not-found")); } }); } - // ─────────────────────────── /ticket forward ─────────────────────────── + // ── /ticket forward ─────────────────────────────────────────────────── private void handleForward(Player player, String[] args) { if (!player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + plugin.lang().send(player, "general.no-permission"); return; } - if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket forward ")); return; } + if (args.length < 3) { player.sendMessage(plugin.lang().get("forward.usage")); return; } int id; try { id = Integer.parseInt(args[1]); } - catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } + catch (NumberFormatException e) { plugin.lang().send(player, "general.invalid-id"); return; } - Player localTarget = Bukkit.getPlayer(args[2]); - if (localTarget == null) { - if (plugin.isBungeeCordEnabled()) { - player.sendMessage(plugin.color("&7[BungeeCord] Spieler &e" + args[2] + " &7ist auf diesem Server nicht online.")); - } else { - player.sendMessage(plugin.color("&cSpieler nicht gefunden!")); - } + Player target = Bukkit.getPlayer(args[2]); + if (target == null) { + player.sendMessage(plugin.isBungeeCordEnabled() + ? plugin.lang().format("forward.bungee-offline", "{player}", args[2]) + : plugin.lang().get("forward.local-not-found")); return; } final int ticketId = id; final String fromName = player.getName(); - final Player t = localTarget; + final Player t = target; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().forwardTicket(ticketId, t.getUniqueId(), t.getName()); + boolean ok = plugin.getDatabaseManager().forwardTicket(ticketId, t.getUniqueId(), t.getName()); plugin.getTicketCache().invalidate(ticketId); Bukkit.getScheduler().runTask(plugin, () -> { - if (!success) { player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); return; } + if (!ok) { plugin.lang().send(player, "general.ticket-not-found"); return; } Ticket ticket = getCachedOrFetch(ticketId); if (ticket == null) return; - player.sendMessage(plugin.color("&aTicket &e#" + ticketId + " &awurde an &e" + t.getName() + " &aweitergeleitet.")); + player.sendMessage(plugin.lang().format("ticket.forwarded", + "{id}", String.valueOf(ticketId), "{player}", t.getName())); plugin.getTicketManager().notifyForwardedTo(ticket, fromName); plugin.getTicketManager().notifyCreatorForwarded(ticket); }); }); } - // ─────────────────────────── /ticket comment ─────────────────────────── + // ── /ticket comment ─────────────────────────────────────────────────── private void handleComment(Player player, String[] args) { - if (!player.hasPermission("ticket.create") && !player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + if (!player.hasPermission("ticket.create") && !player.hasPermission("ticket.support") + && !player.hasPermission("ticket.admin")) { + plugin.lang().send(player, "general.no-permission"); return; } - if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket comment ")); return; } + if (args.length < 3) { player.sendMessage(plugin.lang().get("comment.usage")); return; } int id; try { id = Integer.parseInt(args[1]); } - catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } + catch (NumberFormatException e) { plugin.lang().send(player, "general.invalid-id"); return; } 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.lang().get("comment.too-long")); return; } final int ticketId = id; final String message = msg; @@ -424,40 +316,41 @@ public class TicketCommand implements CommandExecutor, TabCompleter { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Ticket ticket = getCachedOrFetch(ticketId); if (ticket == null) { - Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); + Bukkit.getScheduler().runTask(plugin, () -> plugin.lang().send(player, "general.ticket-not-found")); return; } boolean isOwner = ticket.getCreatorUUID().equals(player.getUniqueId()); boolean isStaff = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"); if (!isOwner && !isStaff) { - Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu kannst nur deine eigenen Tickets kommentieren."))); + Bukkit.getScheduler().runTask(plugin, () -> plugin.lang().send(player, "comment.no-permission")); return; } - TicketComment comment = new TicketComment(ticketId, player.getUniqueId(), player.getName(), message); - boolean success = plugin.getDatabaseManager().addComment(comment); - + boolean ok = plugin.getDatabaseManager().addComment(comment); Bukkit.getScheduler().runTask(plugin, () -> { - if (success) { - player.sendMessage(plugin.color("&aDein Kommentar zu Ticket &e#" + ticketId + " &awurde gespeichert.")); + if (ok) { + player.sendMessage(plugin.lang().format("comment.saved", "{id}", String.valueOf(ticketId))); notifyCommentReceivers(player, ticket, message); } else { - player.sendMessage(plugin.color("&cFehler beim Speichern des Kommentars.")); + player.sendMessage(plugin.lang().get("comment.error")); } }); }); } 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 offlineMsg = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert (während du offline warst): &f" + message; + String onlineMsg = plugin.lang().format("comment.notify-online", + "{id}", String.valueOf(ticket.getId()), "{author}", author.getName(), "{message}", message); + String offlineMsg = plugin.lang().format("comment.notify-offline", + "{id}", String.valueOf(ticket.getId()), "{author}", author.getName(), "{message}", message); if (!ticket.getCreatorUUID().equals(author.getUniqueId())) { Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); if (creator != null && creator.isOnline()) { creator.sendMessage(onlineMsg); } else if (plugin.isBungeeCordEnabled()) { - plugin.getBungeeMessenger().sendMessageToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), onlineMsg); + plugin.getBungeeMessenger().sendMessageToPlayer( + ticket.getCreatorUUID(), ticket.getCreatorName(), onlineMsg); } else { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), offlineMsg)); @@ -474,44 +367,37 @@ public class TicketCommand implements CommandExecutor, TabCompleter { if (claimer != null && claimer.isOnline()) { claimer.sendMessage(onlineMsg); } else { - String claimerOffline = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName() - + " &7hat auf dein bearbeitetes Ticket kommentiert (offline): &f" + message; + String claimerMsg = plugin.lang().format("comment.claimer-offline", + "{id}", String.valueOf(ticket.getId()), + "{author}", author.getName(), "{message}", message); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> - plugin.getDatabaseManager().addPendingNotification(claimerUUID, claimerOffline)); + plugin.getDatabaseManager().addPendingNotification(claimerUUID, claimerMsg)); } } for (Player p : Bukkit.getOnlinePlayers()) { - if (p.getUniqueId().equals(author.getUniqueId())) continue; - if (claimerUUID != null && p.getUniqueId().equals(claimerUUID)) continue; - if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) { + if (!p.equals(author) && (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin"))) p.sendMessage(onlineMsg); - } } } } } - // ─────────────────────────── /ticket rate ────────────────────────────── + // ── /ticket rate ────────────────────────────────────────────────────── private void handleRate(Player player, String[] args) { if (!plugin.getConfig().getBoolean("rating-enabled", true)) { - player.sendMessage(plugin.color("&cBewertungen sind deaktiviert.")); return; + plugin.lang().send(player, "rating.disabled"); return; } - if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket rate ")); return; } + if (args.length < 3) { player.sendMessage(plugin.lang().get("rating.usage")); return; } int id; try { id = Integer.parseInt(args[1]); } - catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } + catch (NumberFormatException e) { plugin.lang().send(player, "general.invalid-id"); return; } - String ratingArg = args[2].toLowerCase(); + String raw = args[2].toLowerCase(); String rating; - if (ratingArg.equals("good") || ratingArg.equals("gut") || ratingArg.equals("thumbsup")) { - rating = "THUMBS_UP"; - } else if (ratingArg.equals("bad") || ratingArg.equals("schlecht") || ratingArg.equals("thumbsdown")) { - rating = "THUMBS_DOWN"; - } else { - player.sendMessage(plugin.color("&cUngültige Bewertung! Benutze &egood &coder &ebad&c.")); - return; - } + if (raw.equals("good") || raw.equals("gut") || raw.equals("thumbsup")) rating = "THUMBS_UP"; + else if (raw.equals("bad") || raw.equals("schlecht") || raw.equals("thumbsdown")) rating = "THUMBS_DOWN"; + else { player.sendMessage(plugin.lang().get("rating.invalid")); return; } final int ticketId = id; final String finalRating = rating; @@ -519,318 +405,438 @@ public class TicketCommand implements CommandExecutor, TabCompleter { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Ticket ticket = getCachedOrFetch(ticketId); if (ticket == null) { - Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); - return; + Bukkit.getScheduler().runTask(plugin, () -> plugin.lang().send(player, "general.ticket-not-found")); return; } if (!ticket.getCreatorUUID().equals(player.getUniqueId())) { - Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu kannst nur deine eigenen Tickets bewerten."))); - return; + Bukkit.getScheduler().runTask(plugin, () -> plugin.lang().send(player, "rating.not-yours")); return; } if (ticket.hasRating()) { - Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu hast dieses Ticket bereits bewertet."))); - return; + Bukkit.getScheduler().runTask(plugin, () -> plugin.lang().send(player, "rating.already-rated")); return; } - - boolean success = plugin.getDatabaseManager().rateTicket(ticketId, finalRating); + boolean ok = plugin.getDatabaseManager().rateTicket(ticketId, finalRating); plugin.getTicketCache().invalidate(ticketId); Bukkit.getScheduler().runTask(plugin, () -> { - if (success) { - String emoji = "THUMBS_UP".equals(finalRating) ? "§a👍 Positiv" : "§c👎 Negativ"; - player.sendMessage(plugin.color("&aDanke für deine Bewertung! (" + emoji + "§a)")); - } else { - player.sendMessage(plugin.color("&cBewertung konnte nicht gespeichert werden. Ticket geschlossen?")); - } + if (ok) player.sendMessage("THUMBS_UP".equals(finalRating) + ? plugin.lang().get("rating.saved-good") + : plugin.lang().get("rating.saved-bad")); + else player.sendMessage(plugin.lang().get("rating.not-closeable")); }); }); } - // ─────────────────────────── /ticket blacklist ───────────────────────── + // ── /ticket blacklist ───────────────────────────────────────────────── private void handleBlacklist(Player player, String[] args) { if (!player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); return; - } - if (args.length < 2) { - player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist [Spieler] [Grund]")); - return; + plugin.lang().send(player, "general.no-permission"); return; } + if (args.length < 2) { player.sendMessage(plugin.lang().get("blacklist.usage")); return; } switch (args[1].toLowerCase()) { - case "add" -> { - if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist add [Grund]")); return; } + case "add", "hinzufügen", "hinzufuegen" -> { + if (args.length < 3) { player.sendMessage(plugin.lang().get("blacklist.usage-add")); return; } String targetName = args[2]; - String reason = args.length > 3 ? String.join(" ", Arrays.copyOfRange(args, 3, args.length)) : "Missbrauch"; - + String reason = args.length > 3 + ? String.join(" ", Arrays.copyOfRange(args, 3, args.length)) : "Missbrauch"; @SuppressWarnings("deprecation") OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); - if (target.getUniqueId() == null) { player.sendMessage(plugin.color("&cSpieler nicht gefunden.")); return; } - + if (target.getUniqueId() == null) { plugin.lang().send(player, "general.player-not-found"); return; } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().addBlacklist(target.getUniqueId(), targetName, reason, player.getName()); + boolean ok = plugin.getDatabaseManager().addBlacklist( + target.getUniqueId(), targetName, reason, player.getName()); Bukkit.getScheduler().runTask(plugin, () -> { - if (success) 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.")); + if (ok) player.sendMessage(plugin.lang().format("blacklist.added", + "{player}", targetName, "{reason}", reason)); + else player.sendMessage(plugin.lang().get("blacklist.already")); }); }); } - case "remove" -> { - if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist remove ")); return; } + case "remove", "entfernen" -> { + if (args.length < 3) { player.sendMessage(plugin.lang().get("blacklist.usage-remove")); return; } @SuppressWarnings("deprecation") OfflinePlayer target = Bukkit.getOfflinePlayer(args[2]); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().removeBlacklist(target.getUniqueId()); + boolean ok = plugin.getDatabaseManager().removeBlacklist(target.getUniqueId()); Bukkit.getScheduler().runTask(plugin, () -> { - 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.")); + if (ok) player.sendMessage(plugin.lang().format("blacklist.removed", "{player}", args[2])); + else player.sendMessage(plugin.lang().get("blacklist.not-found")); }); }); } - case "list" -> { + case "list", "liste" -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { var list = plugin.getDatabaseManager().getBlacklist(); Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6Ticket-Blacklist &7(" + list.size() + " Einträge)")); - player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().format("blacklist.list-header", + "{count}", String.valueOf(list.size()))); + player.sendMessage(plugin.lang().get("general.separator")); if (list.isEmpty()) { - player.sendMessage(plugin.color("&7Keine gesperrten Spieler.")); + player.sendMessage(plugin.lang().get("blacklist.list-empty")); } else { - for (String[] entry : list) { - player.sendMessage(plugin.color("&e" + entry[1] + " &7– &f" + entry[2] - + " &7(gesperrt von &e" + entry[3] + "&7)")); - } + for (String[] e : list) + player.sendMessage(plugin.lang().format("blacklist.list-entry", + "{player}", e[1], "{reason}", e[2], "{by}", e[3])); } - player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.lang().get("general.separator")); }); }); } - default -> player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist [Spieler] [Grund]")); + default -> player.sendMessage(plugin.lang().get("blacklist.usage")); } } - // ─────────────────────────── /ticket top ────────────────────────────── + // ── /ticket top ─────────────────────────────────────────────────────── private void handleTop(Player player) { if (!player.hasPermission("ticket.create") && !player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + plugin.lang().send(player, "general.no-permission"); return; } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { List top = plugin.getDatabaseManager().getTopCreators(5); Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6&lTop-5 Ticket-Ersteller")); - player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().get("top.header")); + player.sendMessage(plugin.lang().get("general.separator")); if (top.isEmpty()) { - player.sendMessage(plugin.color("&7Noch keine Daten vorhanden.")); + player.sendMessage(plugin.lang().get("top.empty")); } else { String[] medals = {"&e🥇", "&7🥈", "&6🥉", "&7#4", "&7#5"}; for (String[] entry : top) { - int rankIdx = Integer.parseInt(entry[0]) - 1; - String medal = rankIdx < medals.length ? medals[rankIdx] : "&7#" + entry[0]; - String name = entry[1]; - String count = entry[2]; - player.sendMessage(plugin.color(medal + " &f" + String.format("%-16s", name) - + " &e" + count + " &7Ticket" + (Integer.parseInt(count) == 1 ? "" : "s"))); + int rankIdx = Integer.parseInt(entry[0]) - 1; + String medal = plugin.lang().color(rankIdx < medals.length ? medals[rankIdx] : "&7#" + entry[0]); + int cnt = Integer.parseInt(entry[2]); + String label = cnt == 1 + ? plugin.lang().get("stats.top-ticket-label") + : plugin.lang().get("stats.top-tickets-label"); + player.sendMessage(plugin.lang().format("top.entry", + "{medal}", medal, + "{name}", String.format("%-16s", entry[1]), + "{count}", entry[2], + "{label}", label)); } } - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&7(Zähler bleiben auch nach dem Löschen von Tickets erhalten)")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().get("top.footer")); }); }); } - // ─────────────────────────── /ticket reload ──────────────────────────── + // ── /ticket reload ──────────────────────────────────────────────────── private void handleReload(Player player) { - if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } + 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.getCategoryManager().reload(); plugin.getFaqManager().reload(); plugin.getTicketCache().clear(); - player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen. &7(Kategorien, FAQs, Cache geleert)")); - if (plugin.isBungeeCordEnabled()) { - player.sendMessage(plugin.color("&8[BungeeCord] &7Server: &b" + plugin.getServerName())); - } + player.sendMessage(plugin.lang().get("reload.success")); + if (plugin.isBungeeCordEnabled()) + player.sendMessage(plugin.lang().format("reload.bungee-info", "{server}", plugin.getServerName())); } - // ─────────────────────────── /ticket archive ─────────────────────────── + // ── /ticket archive ─────────────────────────────────────────────────── private void handleArchive(Player player) { - if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } + if (!player.hasPermission("ticket.admin")) { + plugin.lang().send(player, "general.no-permission"); return; + } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { int count = plugin.getDatabaseManager().archiveClosedTickets(); Bukkit.getScheduler().runTask(plugin, () -> { - if (count > 0) player.sendMessage(plugin.formatMessage("messages.archive-success").replace("{count}", String.valueOf(count))); - else player.sendMessage(plugin.formatMessage("messages.archive-fail")); + if (count > 0) player.sendMessage(plugin.lang().format("system.archive-success", "{count}", String.valueOf(count))); + else player.sendMessage(plugin.lang().get("system.archive-fail")); }); }); } - // ─────────────────────────── /ticket stats ───────────────────────────── + // ── /ticket stats ───────────────────────────────────────────────────── private void handleStats(Player player) { - if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } + if (!player.hasPermission("ticket.admin")) { + plugin.lang().send(player, "general.no-permission"); return; + } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { var stats = plugin.getDatabaseManager().getTicketStats(); var staffRatings = plugin.getDatabaseManager().getStaffRatings(); var topCreators = plugin.getDatabaseManager().getTopCreators(5); Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6Ticket Statistik")); - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&eGesamt: &a" + stats.total)); - player.sendMessage(plugin.color("&eOffen: &a" + stats.open)); - player.sendMessage(plugin.color("&eGeschlossen: &a" + stats.closed + " &7(historisch)")); - player.sendMessage(plugin.color("&eWeitergeleitet: &a" + stats.forwarded)); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().get("stats.header")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().format("stats.total", "{count}", String.valueOf(stats.total))); + player.sendMessage(plugin.lang().format("stats.open", "{count}", String.valueOf(stats.open))); + player.sendMessage(plugin.lang().format("stats.closed", "{count}", String.valueOf(stats.closed))); + player.sendMessage(plugin.lang().format("stats.forwarded", "{count}", String.valueOf(stats.forwarded))); if (plugin.getConfig().getBoolean("rating-enabled", true)) { - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6Support-Bewertungen &7(gesamt, historisch)")); - player.sendMessage(plugin.color("&a👍 Positiv: &f" + stats.thumbsUp - + " &c👎 Negativ: &f" + stats.thumbsDown)); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().get("stats.ratings-header")); + player.sendMessage(plugin.lang().format("stats.ratings-summary", + "{up}", String.valueOf(stats.thumbsUp), "{down}", String.valueOf(stats.thumbsDown))); int totalRated = stats.thumbsUp + stats.thumbsDown; if (totalRated > 0) { - int percent = (int) Math.round(stats.thumbsUp * 100.0 / totalRated); - player.sendMessage(plugin.color("&7Zufriedenheit: &e" + percent + "%")); + int pct = (int) Math.round(stats.thumbsUp * 100.0 / totalRated); + player.sendMessage(plugin.lang().format("stats.ratings-percent", "{percent}", String.valueOf(pct))); } if (!staffRatings.isEmpty()) { - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6Bewertungen nach Support-Mitarbeiter:")); - player.sendMessage(plugin.color("&7 Name 👍 👎 Tickets Zufrieden")); - for (String[] row : staffRatings) { - String name = String.format("%-16s", row[0]); - String up = String.format("%-5s", row[1]); - String down = String.format("%-5s", row[2]); - String total = String.format("%-8s", row[3]); - String percent = row[4]; - player.sendMessage(plugin.color("&e " + name + " &a" + up + " &c" + down + " &7" + total + " &e" + percent)); - } + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().get("stats.staff-header")); + player.sendMessage(plugin.lang().get("stats.staff-table-header")); + for (String[] row : staffRatings) + player.sendMessage(plugin.lang().format("stats.staff-entry", + "{name}", String.format("%-16s", row[0]), + "{up}", String.format("%-5s", row[1]), + "{down}", String.format("%-5s", row[2]), + "{total}", String.format("%-8s", row[3]), + "{percent}", row[4])); } } - if (plugin.isBungeeCordEnabled() && !stats.byServer.isEmpty()) { - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6Tickets nach Server:")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().get("stats.servers-header")); stats.byServer.entrySet().stream() .sorted((a, b) -> b.getValue() - a.getValue()) - .forEach(e -> player.sendMessage(plugin.color("&b " + e.getKey() + ": &a" + e.getValue()))); + .forEach(e -> player.sendMessage(plugin.lang().format("stats.server-entry", + "{server}", e.getKey(), "{count}", String.valueOf(e.getValue())))); } - - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6Top-5 Ticket-Ersteller &7(historisch, persistent)")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().get("stats.top-header")); if (topCreators.isEmpty()) { - player.sendMessage(plugin.color("&7Noch keine Daten vorhanden.")); + player.sendMessage(plugin.lang().get("stats.top-empty")); } else { String[] medals = {"&e🥇", "&7🥈", "&6🥉", "&7#4", "&7#5"}; for (String[] entry : topCreators) { - int rankIdx = Integer.parseInt(entry[0]) - 1; - String medal = rankIdx < medals.length ? medals[rankIdx] : "&7#" + entry[0]; - String name = String.format("%-16s", entry[1]); - String count = entry[2]; - player.sendMessage(plugin.color( - " " + medal + " &f" + name + " &e" + count - + " &7Ticket" + (Integer.parseInt(count) == 1 ? "" : "s"))); + int ri = Integer.parseInt(entry[0]) - 1; + String medal = plugin.lang().color(ri < medals.length ? medals[ri] : "&7#" + entry[0]); + int cnt = Integer.parseInt(entry[2]); + String label = cnt == 1 ? plugin.lang().get("stats.top-ticket-label") + : plugin.lang().get("stats.top-tickets-label"); + player.sendMessage(plugin.lang().format("stats.top-entry", + "{medal}", medal, "{name}", String.format("%-16s", entry[1]), + "{count}", entry[2], "{label}", label)); } } - 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)")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().format("stats.cache-info", + "{count}", String.valueOf(plugin.getTicketCache().size()))); }); }); } - // ─────────────────────────── /ticket migrate ─────────────────────────── + // ── /ticket migrate ─────────────────────────────────────────────────── private void handleMigrate(Player player, String[] args) { - if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } - if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket migrate ")); return; } + if (!player.hasPermission("ticket.admin")) { + plugin.lang().send(player, "general.no-permission"); return; + } + if (args.length < 2) { player.sendMessage(plugin.lang().get("migrate.usage")); return; } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - int migrated = 0; - String mode = args[1].toLowerCase(); - if (mode.equals("tomysql")) migrated = plugin.getDatabaseManager().migrateToMySQL(); - else if (mode.equals("tofile")) migrated = plugin.getDatabaseManager().migrateToFile(); - else { player.sendMessage(plugin.formatMessage("messages.unknown-mode")); return; } + String mode = args[1].toLowerCase(); + int migrated = 0; + if (mode.equals("tomysql")) migrated = plugin.getDatabaseManager().migrateToMySQL(); + else if (mode.equals("tofile")) migrated = plugin.getDatabaseManager().migrateToFile(); + else { + Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.lang().get("system.unknown-mode"))); return; + } int f = migrated; Bukkit.getScheduler().runTask(plugin, () -> { - if (f > 0) player.sendMessage(plugin.formatMessage("messages.migration-success").replace("{count}", String.valueOf(f))); - else player.sendMessage(plugin.formatMessage("messages.migration-fail")); + if (f > 0) player.sendMessage(plugin.lang().format("system.migration-success", "{count}", String.valueOf(f))); + else player.sendMessage(plugin.lang().get("system.migration-fail")); }); }); } - // ─────────────────────────── /ticket export ──────────────────────────── + // ── /ticket export ──────────────────────────────────────────────────── private void handleExport(Player player, String[] args) { - if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } - if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket export ")); return; } - String filename = args[1]; - File exportFile = new File(plugin.getDataFolder(), filename); + if (!player.hasPermission("ticket.admin")) { + plugin.lang().send(player, "general.no-permission"); return; + } + if (args.length < 2) { player.sendMessage(plugin.lang().get("export.usage")); return; } + String filename = args[1]; + File exportFile = new File(plugin.getDataFolder(), filename); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { int count = plugin.getDatabaseManager().exportTickets(exportFile); Bukkit.getScheduler().runTask(plugin, () -> { - 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")); + if (count > 0) player.sendMessage(plugin.lang().format("system.export-success", + "{count}", String.valueOf(count), "{file}", filename)); + else player.sendMessage(plugin.lang().get("system.export-fail")); }); }); } - // ─────────────────────────── /ticket import ──────────────────────────── + // ── /ticket import ──────────────────────────────────────────────────── private void handleImport(Player player, String[] args) { - if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } - if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket import ")); return; } - String filename = args[1]; - File importFile = new File(plugin.getDataFolder(), filename); - if (!importFile.exists()) { player.sendMessage(plugin.formatMessage("messages.file-not-found").replace("{file}", filename)); return; } + if (!player.hasPermission("ticket.admin")) { + plugin.lang().send(player, "general.no-permission"); return; + } + if (args.length < 2) { player.sendMessage(plugin.lang().get("import.usage")); return; } + String filename = args[1]; + File importFile = new File(plugin.getDataFolder(), filename); + if (!importFile.exists()) { + player.sendMessage(plugin.lang().format("system.file-not-found", "{file}", filename)); return; + } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { int count = plugin.getDatabaseManager().importTickets(importFile); Bukkit.getScheduler().runTask(plugin, () -> { - if (count > 0) player.sendMessage(plugin.formatMessage("messages.import-success").replace("{count}", String.valueOf(count))); - else player.sendMessage(plugin.formatMessage("messages.import-fail")); + if (count > 0) player.sendMessage(plugin.lang().format("system.import-success", "{count}", String.valueOf(count))); + else player.sendMessage(plugin.lang().get("system.import-fail")); }); }); } - // ─────────────────────────── /ticket setpriority ────────────────────── + // ── /ticket setpriority ─────────────────────────────────────────────── private void handleSetPriority(Player player, String[] args) { if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + plugin.lang().send(player, "general.no-permission"); return; } if (!plugin.getConfig().getBoolean("priorities-enabled", true)) { - player.sendMessage(plugin.color("&cDas Prioritäten-System ist deaktiviert.")); return; - } - if (args.length < 3) { - player.sendMessage(plugin.color("&cBenutzung: /ticket setpriority ")); return; + player.sendMessage(plugin.lang().get("setpriority.disabled")); return; } + if (args.length < 3) { player.sendMessage(plugin.lang().get("setpriority.usage")); return; } int ticketId; try { ticketId = Integer.parseInt(args[1]); } catch (NumberFormatException e) { - player.sendMessage(plugin.color("&cUngültige Ticket-ID: &e" + args[1])); return; + player.sendMessage(plugin.lang().format("general.invalid-player-id", "{id}", args[1])); return; } - TicketPriority priority = parsePriority(args[2]); - if (priority == null) { - player.sendMessage(plugin.color("&cUngültige Priorität! Gültig: &alow&7, &enormal&7, &6high&7, &curgent")); return; - } - final int finalId = ticketId; + TicketPriority prio = parsePriority(args[2]); + if (prio == null) { player.sendMessage(plugin.lang().get("setpriority.invalid")); return; } + + final int finalId = ticketId; + final TicketPriority finalPrio = prio; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().setTicketPriority(finalId, priority); + boolean ok = plugin.getDatabaseManager().setTicketPriority(finalId, finalPrio); plugin.getTicketCache().invalidate(finalId); Bukkit.getScheduler().runTask(plugin, () -> { - if (success) player.sendMessage(plugin.color("&aPriorität von Ticket &e#" + finalId + " &awurde auf " + priority.getColored() + " &agesetzt.")); - else player.sendMessage(plugin.color("&cTicket &e#" + finalId + " &cwurde nicht gefunden.")); + if (ok) player.sendMessage(plugin.lang().format("setpriority.success", + "{id}", String.valueOf(finalId), "{priority}", finalPrio.getColored())); + else player.sendMessage(plugin.lang().format("setpriority.not-found", + "{id}", String.valueOf(finalId))); }); }); } - // ─────────────────────────── Hilfsmethoden ───────────────────────────── + // ── /ticket faq ─────────────────────────────────────────────────────── + + private void handleFaq(Player player, String[] args) { + if (args.length == 1) { + plugin.getFaqGUI().openFaqGUI(player); + return; + } + + switch (args[1].toLowerCase()) { + + case "add", "hinzufügen", "hinzufuegen" -> { + if (!player.hasPermission("ticket.admin")) { + plugin.lang().send(player, "general.no-permission"); return; + } + if (args.length < 3) { + player.sendMessage(plugin.lang().get("faq.usage-add")); + player.sendMessage(plugin.lang().get("faq.usage-add-example")); 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.lang().get("faq.separator-missing")); + player.sendMessage(plugin.lang().get("faq.separator-example")); return; + } + FaqEntry created = plugin.getFaqManager().add(parts[0].trim(), parts[1].trim()); + player.sendMessage(plugin.lang().format("faq.created", "{id}", String.valueOf(created.getId()))); + player.sendMessage(plugin.lang().format("faq.created-question", "{question}", created.getQuestion())); + player.sendMessage(plugin.lang().format("faq.created-answer", "{answer}", created.getAnswer())); + } + + case "edit", "bearbeiten" -> { + if (!player.hasPermission("ticket.admin")) { + plugin.lang().send(player, "general.no-permission"); return; + } + if (args.length < 4) { player.sendMessage(plugin.lang().get("faq.usage-edit")); return; } + int id; + try { id = Integer.parseInt(args[2]); } + catch (NumberFormatException e) { + player.sendMessage(plugin.lang().format("faq.invalid-id", "{id}", 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.lang().get("faq.separator-short")); return; + } + boolean ok = plugin.getFaqManager().edit(id, parts[0].trim(), parts[1].trim()); + player.sendMessage(ok + ? plugin.lang().format("faq.updated", "{id}", String.valueOf(id)) + : plugin.lang().format("faq.not-found", "{id}", String.valueOf(id))); + } + + case "delete", "remove", "löschen", "loeschen", "entfernen" -> { + if (!player.hasPermission("ticket.admin")) { + plugin.lang().send(player, "general.no-permission"); return; + } + if (args.length < 3) { player.sendMessage(plugin.lang().get("faq.usage-delete")); return; } + int id; + try { id = Integer.parseInt(args[2]); } + catch (NumberFormatException e) { + player.sendMessage(plugin.lang().format("faq.invalid-id", "{id}", args[2])); return; + } + boolean ok = plugin.getFaqManager().delete(id); + player.sendMessage(ok + ? plugin.lang().format("faq.deleted", "{id}", String.valueOf(id)) + : plugin.lang().format("faq.not-found", "{id}", String.valueOf(id))); + } + + case "reload", "neuladen" -> { + if (!player.hasPermission("ticket.admin")) { + plugin.lang().send(player, "general.no-permission"); return; + } + plugin.getFaqManager().reload(); + player.sendMessage(plugin.lang().format("faq.reloaded", + "{count}", String.valueOf(plugin.getFaqManager().getAll().size()))); + } + + case "list", "liste" -> { + List all = plugin.getFaqManager().getAll(); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().format("faq.list-header", "{count}", String.valueOf(all.size()))); + player.sendMessage(plugin.lang().get("general.separator")); + if (all.isEmpty()) { + player.sendMessage(plugin.lang().get("faq.list-empty")); + } else { + for (FaqEntry e : all) { + player.sendMessage(plugin.lang().format("faq.list-entry", + "{id}", String.valueOf(e.getId()), "{question}", e.getQuestion())); + player.sendMessage(plugin.lang().format("faq.list-answer", "{answer}", e.getAnswer())); + } + } + player.sendMessage(plugin.lang().get("general.separator")); + if (player.hasPermission("ticket.admin")) + player.sendMessage(plugin.lang().get("faq.list-admin-hint")); + } + + default -> { + player.sendMessage(plugin.lang().get("faq.unknown-sub")); + player.sendMessage(plugin.lang().get("faq.hint-open")); + if (player.hasPermission("ticket.admin")) + player.sendMessage(plugin.lang().get("faq.admin-commands")); + } + } + } + + // ── 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; @@ -846,41 +852,67 @@ public class TicketCommand implements CommandExecutor, TabCompleter { case "normal" -> TicketPriority.NORMAL; case "high", "hoch" -> TicketPriority.HIGH; case "urgent", "dringend" -> TicketPriority.URGENT; - default -> null; + default -> null; }; } - // ─────────────────────────── Tab-Completion ──────────────────────────── + // ── Tab-Completion ──────────────────────────────────────────────────── @Override public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { List completions = new ArrayList<>(); if (!(sender instanceof Player player)) return completions; + // Immer direkt vom LanguageManager lesen – kein Cache, immer aktuell nach reload + final boolean useDe = plugin.lang().acceptsGerman(); + final boolean useEn = plugin.lang().acceptsEnglish(); + if (args.length == 1) { - List subs = new ArrayList<>(List.of("create", "list", "comment", "top", "faq")); - if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) - subs.addAll(List.of("claim", "close")); - if (plugin.getConfig().getBoolean("rating-enabled", true)) subs.add("rate"); - if (player.hasPermission("ticket.admin")) - subs.addAll(List.of("forward", "reload", "stats", "archive", "migrate", "export", "import", "blacklist")); + List subs = new ArrayList<>(); + if (useEn) subs.addAll(List.of("create", "list", "comment", "top", "faq")); + if (useDe) subs.addAll(List.of("erstellen", "liste", "kommentar", "top", "faq")); + + if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { + if (useEn) subs.addAll(List.of("claim", "close")); + if (useDe) subs.addAll(List.of("übernehmen", "schließen")); + } + if (plugin.getConfig().getBoolean("rating-enabled", true)) { + if (useEn) subs.add("rate"); + if (useDe) subs.add("bewerten"); + } + if (player.hasPermission("ticket.admin")) { + if (useEn) subs.addAll(List.of("forward", "reload", "stats", "archive", + "migrate", "export", "import", "blacklist")); + if (useDe) subs.addAll(List.of("weiterleiten", "neuladen", "statistik", + "archivieren", "migrieren", "exportieren", "importieren", "sperrliste")); + } if ((player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) - && plugin.getConfig().getBoolean("priorities-enabled", true)) - subs.add("setpriority"); - for (String s : subs) if (s.startsWith(args[0].toLowerCase())) completions.add(s); + && plugin.getConfig().getBoolean("priorities-enabled", true)) { + if (useEn) subs.add("setpriority"); + if (useDe) subs.add("priorität"); + } + for (String s : subs) + if (s.startsWith(args[0].toLowerCase())) completions.add(s); - } else if (args.length == 2 && args[0].equalsIgnoreCase("faq")) { - List 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 == 2 && normalize(args[0]).equals("faq")) { + List faqSubs = new ArrayList<>(); + if (useEn) faqSubs.add("list"); + if (useDe) faqSubs.add("liste"); + if (player.hasPermission("ticket.admin")) { + if (useEn) faqSubs.addAll(List.of("add", "edit", "delete", "reload")); + if (useDe) faqSubs.addAll(List.of("hinzufügen", "bearbeiten", "löschen", "neuladen")); + } + 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")) + } else if (args.length == 3 && normalize(args[0]).equals("faq") + && (args[1].equalsIgnoreCase("edit") || args[1].equalsIgnoreCase("delete") + || args[1].equalsIgnoreCase("bearbeiten") || args[1].equalsIgnoreCase("löschen")) && 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 && normalize(args[0]).equals("create") && plugin.getConfig().getBoolean("categories-enabled", true)) { for (ConfigCategory c : plugin.getCategoryManager().getAll()) if (c.getKey().startsWith(args[1].toLowerCase())) completions.add(c.getKey()); @@ -888,30 +920,34 @@ public class TicketCommand implements CommandExecutor, TabCompleter { for (String p : List.of("low", "normal", "high", "urgent")) if (p.startsWith(args[1].toLowerCase())) completions.add(p); - } else if (args.length == 3 && args[0].equalsIgnoreCase("create") + } else if (args.length == 3 && normalize(args[0]).equals("create") && plugin.getConfig().getBoolean("priorities-enabled", true)) { for (String p : List.of("low", "normal", "high", "urgent")) if (p.startsWith(args[2].toLowerCase())) completions.add(p); - } else if (args.length == 3 && args[0].equalsIgnoreCase("setpriority")) { + } else if (args.length == 3 && normalize(args[0]).equals("setpriority")) { for (String p : List.of("low", "normal", "high", "urgent")) if (p.startsWith(args[2].toLowerCase())) completions.add(p); - } else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) { + } else if (args.length == 3 && normalize(args[0]).equals("forward")) { for (Player p : Bukkit.getOnlinePlayers()) if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName()); - } else if (args.length == 2 && args[0].equalsIgnoreCase("blacklist")) { - completions.addAll(List.of("add", "remove", "list")); + } else if (args.length == 2 && normalize(args[0]).equals("blacklist")) { + if (useEn) completions.addAll(List.of("add", "remove", "list")); + if (useDe) completions.addAll(List.of("hinzufügen", "entfernen", "liste")); - } else if (args.length == 3 && args[0].equalsIgnoreCase("blacklist") - && (args[1].equalsIgnoreCase("add") || args[1].equalsIgnoreCase("remove"))) { + } else if (args.length == 3 && normalize(args[0]).equals("blacklist") + && (args[1].equalsIgnoreCase("add") || args[1].equalsIgnoreCase("remove") + || args[1].equalsIgnoreCase("hinzufügen") || args[1].equalsIgnoreCase("entfernen"))) { for (Player p : Bukkit.getOnlinePlayers()) if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName()); - } else if (args.length == 3 && args[0].equalsIgnoreCase("rate")) { - completions.addAll(List.of("good", "bad")); + } else if (args.length == 3 && normalize(args[0]).equals("rate")) { + if (useEn) completions.addAll(List.of("good", "bad")); + if (useDe) completions.addAll(List.of("gut", "schlecht")); } + return completions; } } \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/database/DatabaseManager.java b/src/main/java/de/ticketsystem/database/DatabaseManager.java index f52cbea..06bb9ae 100644 --- a/src/main/java/de/ticketsystem/database/DatabaseManager.java +++ b/src/main/java/de/ticketsystem/database/DatabaseManager.java @@ -1102,6 +1102,38 @@ public class DatabaseManager { // ─────────────────────────── Abfragen ────────────────────────────────── + /** + * Gibt alle geschlossenen Tickets eines bestimmten Spielers zurück, + * die noch nicht als close_notified markiert wurden. + * Deutlich effizienter als alle CLOSED-Tickets zu laden und per UUID zu filtern. + */ + public List getUnnotifiedClosedTicketsByPlayer(UUID uuid) { + List list = new ArrayList<>(); + if (useMySQL) { + String sql = "SELECT * FROM tickets WHERE creator_uuid = ? AND status = 'CLOSED' AND close_notified = 0"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + while (rs.next()) list.add(mapRow(rs)); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Abrufen der Tickets: " + e.getMessage(), e); + } + return list; + } else { + if (!dataConfig.contains("tickets")) return list; + for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { + Ticket t = (Ticket) dataConfig.get("tickets." + key); + if (t != null + && uuid.equals(t.getCreatorUUID()) + && t.getStatus() == TicketStatus.CLOSED + && !t.isCloseNotified()) { + list.add(t); + } + } + return list; + } + } + public List getTicketsByStatus(TicketStatus... statuses) { List list = new ArrayList<>(); if (statuses.length == 0) return list; diff --git a/src/main/java/de/ticketsystem/gui/FaqGUI.java b/src/main/java/de/ticketsystem/gui/FaqGUI.java index de619ed..84afc83 100644 --- a/src/main/java/de/ticketsystem/gui/FaqGUI.java +++ b/src/main/java/de/ticketsystem/gui/FaqGUI.java @@ -4,6 +4,7 @@ import de.ticketsystem.TicketPlugin; import de.ticketsystem.model.FaqEntry; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -22,99 +23,225 @@ 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 + * Layout, Größe und Slots sind über config.yml steuerbar. */ public class FaqGUI implements Listener { - // ─────────────────────────── Titel-Konstanten ────────────────────────── + // ── Konfigurierbare Felder ──────────────────────────────────────────────── + private int faqRows; + private int faqNavPrev, faqNavNext, faqNavAdd, faqNavPage; + + private Material headMaterial = Material.PLAYER_HEAD; + private String headTexture = "http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e"; + private List contentSlots = new ArrayList<>(); - 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 = + // ── System Felder ──────────────────────────────────────────────────────── + private static final String DEFAULT_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> slotMap = new HashMap<>(); - /** Aktuelle Seite pro Spieler in der FAQ-GUI */ private final Map faqPage = new HashMap<>(); - /** Ob der Spieler sich in der Admin-Ansicht befindet */ private final Set adminView = new HashSet<>(); - /** Aktuell ausgewähltes FAQ für die Aktions-GUI */ private final Map actionEntry = new HashMap<>(); - // ─── Chat-Input-States ────────────────────────────────────────────── - /** Wartet auf Frage-Eingabe: null = neu, "edit:" = bearbeiten */ - private final Map awaitingQuestion = new HashMap<>(); - /** Wartet auf Antwort-Eingabe: key = Frage-Text (|id) bei Edit */ - private final Map awaitingAnswer = new HashMap<>(); + private final Map awaitingQuestion = new HashMap<>(); + private final Map awaitingAnswer = new HashMap<>(); - // ─────────────────────────── Konstruktor ─────────────────────────────── + // Materialien für Navigations-Buttons + private Material matNavPrev, matNavNext, matNavPage, matNavAdd; public FaqGUI(TicketPlugin plugin) { this.plugin = plugin; + reloadConfig(); + } + + /** + * Berechnet die Item-Slots: + * Schachbrett-Muster ohne Glas – passt sich automatisch an jede Größe an: + * + * Reihe 0 : Glas in geraden Spalten (0,2,4,6,8), Items in ungeraden (1,3,5,7) → 4 Items + * Reihe 1 : Items in geraden Spalten (0,2,4,6,8), leer in ungeraden → 5 Items + * Reihe 2 : Items in ungeraden Spalten (1,3,5,7), leer in geraden → 4 Items + * Reihe 3 : Items in geraden Spalten (0,2,4,6,8), leer in ungeraden → 5 Items + * ... (Reihe 1+ kein Glas – nur Items und leere Slots wechselnd) + * Letzte Reihe: Navigation (Footer) + * + * Kapazität: + * 4 Reihen → 13 Items | 5 Reihen → 18 Items | 6 Reihen → 22 Items + */ + private List buildPatternSlots(int rows) { + List slots = new ArrayList<>(); + int contentRows = rows - 1; // Letzte Reihe = Navigation + for (int row = 0; row < contentRows; row++) { + int base = row * 9; + if (row % 2 == 0) { + // Gerade Reihen (0,2,4,...): Items in geraden Spalten + slots.add(base + 0); + slots.add(base + 2); + slots.add(base + 4); + slots.add(base + 6); + slots.add(base + 8); + } else { + // Ungerade Reihen (1,3,5,...): Items in ungeraden Spalten + slots.add(base + 1); + slots.add(base + 3); + slots.add(base + 5); + slots.add(base + 7); + } + } + return slots; + } + + /** + * Setzt die Navigations-Slot-Defaults passend zur aktuellen Zeilenanzahl. + * Nav-Leiste liegt immer in der letzten Reihe. + */ + /** + * Nav-Leiste liegt in der letzten Reihe. + * Gerade Spalten werden durch fillNavBar bereits mit Glas gefüllt. + * Buttons liegen auf ungerade Spalten – passend zum Content-Muster: + * Slot +1 = Prev | Slot +3 = Add | Slot +5 = Page | Slot +7 = Next + */ + private void applyNavDefaults(int rows) { + int navBase = (rows - 1) * 9; // Erster Slot der letzten Reihe + faqNavPrev = navBase + 1; // 2. Slot (ungerade) – Blättern zurück + faqNavAdd = navBase + 3; // 4. Slot (ungerade) – Hinzufügen + faqNavPage = navBase + 5; // 6. Slot (ungerade) – Seitenanzeige + faqNavNext = navBase + 7; // 8. Slot (ungerade) – Blättern vor + } + + /** + * Lädt die Konfiguration sicher mit Fallback-Werten. + */ + public void reloadConfig() { + // ── 1. STANDARDWERTE SETZEN (Sicherheit gegen leere Config) ───────── + faqRows = 6; + + headMaterial = Material.PLAYER_HEAD; + headTexture = DEFAULT_SKIN_URL; + + matNavPrev = Material.ARROW; + matNavNext = Material.ARROW; + matNavPage = Material.PAPER; + matNavAdd = Material.LIME_WOOL; + + // Standard Content-Slots nach Muster: Glasscheiben in Spalte 0, 4, 8 + contentSlots = buildPatternSlots(faqRows); + applyNavDefaults(faqRows); + + // ── 2. VERSUCHEN AUS CONFIG ZU LADEN ───────────────────────────────── + ConfigurationSection guiSettings = plugin.getConfig().getConfigurationSection("gui-settings"); + + if (guiSettings != null) { + ConfigurationSection faqConf = guiSettings.getConfigurationSection("faq"); + if (faqConf != null) { + // Rows laden + faqRows = faqConf.getInt("rows", 6); + if (faqRows < 4) faqRows = 4; // Minimum 4 Reihen + if (faqRows > 6) faqRows = 6; + + // Nav-Defaults für die gewählte Zeilenanzahl setzen + applyNavDefaults(faqRows); + + // Content Slots laden – nur überschreiben wenn explizit in config gesetzt + if (faqConf.contains("content-slots") && !faqConf.getIntegerList("content-slots").isEmpty()) { + contentSlots = faqConf.getIntegerList("content-slots"); + } else { + // Muster für die konfigurierte Zeilenanzahl neu berechnen + contentSlots = buildPatternSlots(faqRows); + } + + // Navigation Slots laden (Config überschreibt dynamische Defaults) + faqNavPrev = getSlot(faqConf, "nav.prev", faqNavPrev, faqRows); + faqNavNext = getSlot(faqConf, "nav.next", faqNavNext, faqRows); + faqNavPage = getSlot(faqConf, "nav.page", faqNavPage, faqRows); + faqNavAdd = getSlot(faqConf, "nav.add", faqNavAdd, faqRows); + + // Head Config laden + ConfigurationSection headConf = faqConf.getConfigurationSection("head-item"); + if (headConf != null) { + headMaterial = getMaterial(headConf, "material", Material.PLAYER_HEAD); + headTexture = headConf.getString("texture", DEFAULT_SKIN_URL); + } + } + + // Materialien laden (Global) + ConfigurationSection itemsSettings = guiSettings.getConfigurationSection("items"); + if (itemsSettings != null) { + matNavPrev = getMaterial(itemsSettings, "nav-prev", Material.ARROW); + matNavNext = getMaterial(itemsSettings, "nav-next", Material.ARROW); + matNavPage = getMaterial(itemsSettings, "nav-page", Material.PAPER); + matNavAdd = getMaterial(itemsSettings, "nav-add", Material.LIME_WOOL); + } + } + } + + private int getSlot(ConfigurationSection section, String path, int def, int rows) { + int val = section.getInt(path, def); + int max = rows * 9; + if (val >= 0 && val < max) return val; + // Slot liegt außerhalb des Inventars → Spalte beibehalten, letzte Reihe verwenden + int col = (val >= 0 ? val : def) % 9; + return (rows - 1) * 9 + col; + } + + private Material getMaterial(ConfigurationSection section, String path, Material def) { + try { + return Material.valueOf(section.getString(path, def.name())); + } catch (IllegalArgumentException e) { + return def; + } + } + + // ── Hilfsmethode für Sprachzugriff ───────────────────────────────────────── + private String f(String key) { + return plugin.lang().get("gui.faq." + key); + } + + private String f(String key, String... replacements) { + return plugin.lang().format("gui.faq." + key, replacements); } // ═══════════════════════════════════════════════════════════════════════ // 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; + String title = isAdmin ? f("admin-title") : f("title"); - List all = plugin.getFaqManager().getAll(); - int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / PAGE_SIZE)); + List all = plugin.getFaqManager().getAll(); + + // Sicherheit gegen leere Content-Slots Liste (Division by Zero) + int pageSize = contentSlots.isEmpty() ? 45 : contentSlots.size(); + + int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / pageSize)); page = Math.max(0, Math.min(page, totalPages - 1)); faqPage.put(player.getUniqueId(), page); - Inventory inv = Bukkit.createInventory(null, 54, title); + int invSize = faqRows * 9; + Inventory inv = Bukkit.createInventory(null, invSize, title); Map sm = new HashMap<>(); - int start = page * PAGE_SIZE; - for (int i = 0; i < PAGE_SIZE && (start + i) < all.size(); i++) { + int start = page * pageSize; + int itemsOnCurrentPage = 0; // Zähler für Items auf dieser Seite + + // Wir nutzen entweder die konfigurierten Slots oder sequentiell von 0, falls Liste leer + for (int i = 0; i < pageSize && (start + i) < all.size(); i++) { FaqEntry entry = all.get(start + i); - inv.setItem(i, buildFaqSkull(entry, isAdmin)); - sm.put(i, entry); + int slot = contentSlots.isEmpty() ? i : contentSlots.get(i); + + if (slot < invSize) { + inv.setItem(slot, buildFaqItem(entry, isAdmin)); + sm.put(slot, entry); + itemsOnCurrentPage++; + } } slotMap.put(player.getUniqueId(), sm); @@ -122,8 +249,10 @@ public class FaqGUI implements Listener { if (isAdmin) adminView.add(player.getUniqueId()); else adminView.remove(player.getUniqueId()); - // ── Navigationsleiste ────────────────────────────────────────────── - fillNavBar(inv, page, totalPages, isAdmin, all.isEmpty()); + // Kein Glas im Content-Bereich – nur Footer (fillNavBar) hat Glasscheiben + + // Übergeben der korrekten Anzahl an die Navigationsleiste + fillNavBar(inv, page, totalPages, isAdmin, all.isEmpty(), itemsOnCurrentPage); player.openInventory(inv); } @@ -136,15 +265,18 @@ public class FaqGUI implements Listener { 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; + String playerTitle = f("title"); + String adminTitle = f("admin-title"); + String actionTitle = f("action-title"); + + if (!title.equals(playerTitle) && !title.equals(adminTitle) + && !title.equals(actionTitle)) return; event.setCancelled(true); int slot = event.getRawSlot(); if (slot < 0) return; - // ── Aktions-GUI ──────────────────────────────────────────────────── - if (title.equals(FAQ_ACTION_TITLE)) { + if (title.equals(actionTitle)) { FaqEntry entry = actionEntry.get(player.getUniqueId()); if (entry == null) return; switch (slot) { @@ -155,22 +287,15 @@ public class FaqGUI implements Listener { 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; } + if (slot == faqNavPrev) { openFaqGUI(player, curPage - 1); return; } + if (slot == faqNavNext) { openFaqGUI(player, curPage + 1); return; } + if (slot == faqNavAdd && isAdmin) { startAddFlow(player); return; } - // Admin-spezifisch: Neues FAQ hinzufügen - if (slot == 50 && isAdmin) { - startAddFlow(player); - return; - } - - // FAQ-Item angeklickt - if (slot < PAGE_SIZE) { + // Check ob Slot in contentSlots ist + if (contentSlots.contains(slot)) { Map sm = slotMap.get(player.getUniqueId()); if (sm == null) return; FaqEntry entry = sm.get(slot); @@ -179,12 +304,12 @@ public class FaqGUI implements Listener { 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 ")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().format("faq.list-entry", + "{id}", String.valueOf(entry.getId()), "{question}", entry.getQuestion())); + player.sendMessage(plugin.lang().color("§f" + entry.getAnswer())); + player.sendMessage(plugin.lang().get("general.separator")); } } } @@ -193,22 +318,15 @@ public class FaqGUI implements Listener { private void openActionGUI(Player player, FaqEntry entry) { actionEntry.put(player.getUniqueId(), entry); - Inventory inv = Bukkit.createInventory(null, 27, FAQ_ACTION_TITLE); + Inventory inv = Bukkit.createInventory(null, 27, f("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."))); + inv.setItem(4, buildFaqItem(entry, false)); + inv.setItem(10, buildItem(Material.WRITABLE_BOOK, f("edit-button"), + List.of(f("edit-lore-1"), f("edit-lore-2")))); + inv.setItem(12, buildItem(Material.BARRIER, f("delete-button"), + List.of(f("delete-lore-1"), f("delete-lore-2")))); + inv.setItem(16, buildItem(Material.ARROW, f("back-button"), + List.of(f("back-lore")))); fillGlass(inv); player.openInventory(inv); @@ -219,10 +337,10 @@ public class FaqGUI implements Listener { 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 ")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(f("chat-create-title")); + player.sendMessage(f("chat-question-prompt")); + player.sendMessage(plugin.lang().get("general.separator")); } // ─────────────────────────── Chat-Flow: Bearbeiten ───────────────────── @@ -230,11 +348,11 @@ public class FaqGUI implements Listener { 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 ")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(f("chat-edit-title", "{id}", String.valueOf(entry.getId()))); + player.sendMessage(f("chat-current-question", "{question}", entry.getQuestion())); + player.sendMessage(f("chat-question-prompt")); + player.sendMessage(plugin.lang().get("general.separator")); } // ─────────────────────────── Löschen ─────────────────────────────────── @@ -243,15 +361,15 @@ public class FaqGUI implements Listener { 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.")); + player.sendMessage(plugin.lang().format("faq.deleted", "{id}", String.valueOf(entry.getId()))); } else { - player.sendMessage(plugin.color("&cFehler: FAQ #" + entry.getId() + " konnte nicht gelöscht werden.")); + player.sendMessage(f("delete-error", "{id}", String.valueOf(entry.getId()))); } openFaqGUI(player); } // ═══════════════════════════════════════════════════════════════════════ - // CHAT-EVENTS (Frage & Antwort Eingabe) + // CHAT-EVENTS // ═══════════════════════════════════════════════════════════════════════ @EventHandler(priority = EventPriority.LOWEST) @@ -262,24 +380,22 @@ public class FaqGUI implements Listener { // ── Schritt 1: Warte auf Frage ───────────────────────────────────── if (awaitingQuestion.containsKey(uuid)) { event.setCancelled(true); - String state = awaitingQuestion.remove(uuid); - String input = event.getMessage().trim(); + String state = awaitingQuestion.remove(uuid); + String input = event.getMessage().trim(); if (input.equalsIgnoreCase("cancel")) { Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.color("&cAbgebrochen.")); + player.sendMessage(plugin.lang().get("gui.close-cancelled")); openFaqGUI(player); }); return; } - // Frage gespeichert → jetzt auf Antwort warten - // Encoded state: "new" oder "edit:" - awaitingAnswer.put(uuid, state + "§§" + input); // "§§" as internal separator + awaitingAnswer.put(uuid, state + "\u0000" + input); Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.color("&7Frage gesetzt: &e" + input)); - player.sendMessage(plugin.color("&7Gib jetzt die &eAntwort &7ein (oder &ccancel&7):")); + player.sendMessage(f("question-set", "{question}", input)); + player.sendMessage(f("chat-answer-prompt")); }); return; } @@ -292,36 +408,32 @@ public class FaqGUI implements Listener { if (input.equalsIgnoreCase("cancel")) { Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.color("&cAbgebrochen.")); + player.sendMessage(plugin.lang().get("gui.close-cancelled")); openFaqGUI(player); }); return; } - // stateAndQuestion = "§§" - int sep = stateAndQuestion.indexOf("§§"); + int sep = stateAndQuestion.indexOf("\u0000"); String state = stateAndQuestion.substring(0, sep); - String question = stateAndQuestion.substring(sep + 2); - String answer = input; + String question = stateAndQuestion.substring(sep + 1); 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!")); + FaqEntry created = plugin.getFaqManager().add(question, input); + player.sendMessage(plugin.lang().format("faq.created", "{id}", String.valueOf(created.getId()))); } else { - // Bearbeiten: state = "edit:" int id; try { - id = Integer.parseInt(state.substring(5)); // "edit:".length() = 5 + id = Integer.parseInt(state.substring(5)); } catch (NumberFormatException e) { - player.sendMessage(plugin.color("&cInterner Fehler beim Bearbeiten des FAQs.")); + player.sendMessage(f("internal-error")); 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.")); + boolean ok = plugin.getFaqManager().edit(id, question, input); + if (ok) player.sendMessage(plugin.lang().format("faq.updated", "{id}", String.valueOf(id))); + else player.sendMessage(plugin.lang().format("faq.not-found", "{id}", String.valueOf(id))); } openFaqGUI(player); }); @@ -332,70 +444,71 @@ public class FaqGUI implements Listener { // 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); + private ItemStack buildFaqItem(FaqEntry entry, boolean adminHint) { + ItemStack item; + + if (headMaterial == Material.PLAYER_HEAD) { + try { + item = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) item.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(headTexture)); + profile.setTextures(textures); + meta.setOwnerProfile(profile); + meta.setDisplayName("§e§l" + entry.getQuestion()); + meta.setLore(buildFaqLore(entry, adminHint)); + item.setItemMeta(meta); + } + } catch (Exception e) { + item = buildFallbackItem(entry, adminHint); } - } catch (Exception e) { - // Fallback auf BOOK wenn Textur nicht gesetzt werden kann - skull = new ItemStack(Material.BOOK); - ItemMeta meta = skull.getItemMeta(); + } else { + item = new ItemStack(headMaterial); + ItemMeta meta = item.getItemMeta(); if (meta != null) { meta.setDisplayName("§e§l" + entry.getQuestion()); meta.setLore(buildFaqLore(entry, adminHint)); - skull.setItemMeta(meta); + item.setItemMeta(meta); } } - - return skull; + return item; + } + + private ItemStack buildFallbackItem(FaqEntry entry, boolean adminHint) { + ItemStack item = new ItemStack(Material.BOOK); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName("§e§l" + entry.getQuestion()); + meta.setLore(buildFaqLore(entry, adminHint)); + item.setItemMeta(meta); + } + return item; } - /** Erstellt die Lore-Zeilen für ein FAQ-Item (Antwort aufgeteilt in 40er-Zeilen). */ private List buildFaqLore(FaqEntry entry, boolean adminHint) { List lore = new ArrayList<>(); - lore.add("§8§m "); - lore.add("§7FAQ #" + entry.getId()); - lore.add("§8§m "); + lore.add(f("lore-separator")); + lore.add(f("lore-id", "{id}", String.valueOf(entry.getId()))); + lore.add(f("lore-separator")); - // Antwort in 40-Zeichen-Abschnitte aufteilen - String answer = entry.getAnswer(); - int chunkSize = 40; + 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 + i = end - chunkSize; } - 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"); - } + lore.add(f("lore-separator")); + if (adminHint) lore.add(f("click-edit")); + else lore.add(f("click-detail")); return lore; } @@ -411,25 +524,31 @@ public class FaqGUI implements Listener { // ─────────────────────────── Navigationsleiste ───────────────────────── - private void fillNavBar(Inventory inv, int page, int totalPages, boolean isAdmin, boolean isEmpty) { + private void fillNavBar(Inventory inv, int page, int totalPages, boolean isAdmin, boolean isEmpty, int itemCount) { ItemStack glass = makeGlass(); - for (int i = 45; i < 54; i++) inv.setItem(i, glass); + // Untere Reihe füllen (Nav-Bar) + for (int i = inv.getSize() - 9; i < inv.getSize(); i++) inv.setItem(i, glass); if (page > 0) { - inv.setItem(45, buildItem(Material.ARROW, "§7§l◄ Zurück", - List.of("§7Seite " + page + " von " + totalPages))); + inv.setItem(faqNavPrev, buildActionItem(matNavPrev, + f("nav-prev"), + List.of(f("nav-prev-lore", "{page}", String.valueOf(page), "{total}", String.valueOf(totalPages))))); } if (page < totalPages - 1) { - inv.setItem(53, buildItem(Material.ARROW, "§7§lWeiter ►", - List.of("§7Seite " + (page + 2) + " von " + totalPages))); + inv.setItem(faqNavNext, buildActionItem(matNavNext, + f("nav-next"), + List.of(f("nav-next-lore", "{page}", String.valueOf(page + 2), "{total}", String.valueOf(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)"))); + // itemCount ist jetzt die tatsächliche Anzahl der Items auf der Seite + int displayCount = isEmpty ? 0 : itemCount; + + inv.setItem(faqNavPage, buildActionItem(matNavPage, f("nav-page", "{page}", String.valueOf(page + 1), "{total}", String.valueOf(totalPages)), + List.of(f("nav-page-lore", "{count}", String.valueOf(displayCount))))); 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."))); + inv.setItem(faqNavAdd, buildActionItem(matNavAdd, f("add-button"), + List.of(f("add-lore-1"), f("add-lore-2")))); } } @@ -446,4 +565,14 @@ public class FaqGUI implements Listener { if (meta != null) { meta.setDisplayName(" "); glass.setItemMeta(meta); } return glass; } + + private ItemStack buildActionItem(Material material, String displayName, List 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; + } } \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/gui/TicketGUI.java b/src/main/java/de/ticketsystem/gui/TicketGUI.java index 12a65fb..ec9e761 100644 --- a/src/main/java/de/ticketsystem/gui/TicketGUI.java +++ b/src/main/java/de/ticketsystem/gui/TicketGUI.java @@ -9,6 +9,7 @@ import de.ticketsystem.model.TicketPriority; import de.ticketsystem.model.TicketStatus; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -22,57 +23,193 @@ import org.bukkit.inventory.meta.ItemMeta; import java.text.SimpleDateFormat; import java.util.*; +/** + * GUI für das TicketSystem. + * Layouts und Slots sind nun in der config.yml unter gui-settings konfigurierbar. + * Java 17 Kompatibel. + */ public class TicketGUI implements Listener { - // ─────────────────────────── Titel-Konstanten ────────────────────────── + // ── Konfigurierbare Felder ──────────────────────────────────────────────── + + // Spieler GUI + private int playerRows; + private int playerNavPrev, playerNavNext, playerNavPage; + + // Admin GUI + private int adminRows; + private int adminNavPrev, adminNavNext, adminNavPage, adminNavArchive, adminNavFilter; + + // Archiv GUI + private static final int ARCHIVE_ROWS = 6; // Fest auf 6 – nicht konfigurierbar + private int archiveNavPrev, archiveNavNext, archiveNavBack; + + // Materialien (Standardwerte als Fallback) + private Material matNavPrev = Material.ARROW; + private Material matNavNext = Material.ARROW; + private Material matNavPage = Material.PAPER; + private Material matNavArchive = Material.CHEST; + private Material matNavBack = Material.ARROW; + private Material matNavFilter = Material.HOPPER; + private Material matNavAdd = Material.LIME_WOOL; + + // ── System Felder ──────────────────────────────────────────────────────── + + private String GUI_TITLE; + private String CLOSED_GUI_TITLE; + private String PLAYER_GUI_TITLE; + private String DETAIL_GUI_TITLE; - private static final String GUI_TITLE = "§8§lTicket-Übersicht"; - private static final String CLOSED_GUI_TITLE = "§8§lTicket-Archiv"; - private static final String PLAYER_GUI_TITLE = "§8§lMeine Tickets"; - private static final String DETAIL_GUI_TITLE = "§8§lTicket-Details"; private static final String ARCHIVE_PERMISSION = "ticket.archive"; - - /** Ticket-Slots pro Seite (Reihen 0–4, Slots 0–44) */ - private static final int PAGE_SIZE = 45; - + private static final int PAGE_SIZE_DEFAULT = 45; // Fallback private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm"); private final TicketPlugin plugin; - // ─────────────────────────── State-Maps ──────────────────────────────── - - /** Admin-Übersicht: Slot → Ticket */ + // ── State-Maps ──────────────────────────────────────────────────────── private final Map> playerSlotMap = new HashMap<>(); - /** Admin-Archiv: Slot → Ticket */ private final Map> playerClosedSlotMap = new HashMap<>(); - /** Spieler-GUI: Slot → Ticket */ private final Map> playerOwnSlotMap = new HashMap<>(); - /** Detail-Ansicht: UUID → Ticket */ private final Map detailTicketMap = new HashMap<>(); - /** Aktuelle Seite pro Spieler (Admin, Archiv, Spieler) */ - private final Map adminPage = new HashMap<>(); - private final Map archivePage= new HashMap<>(); - private final Map playerPage = new HashMap<>(); + private final Map adminPage = new HashMap<>(); + private final Map archivePage = new HashMap<>(); + private final Map playerPage = new HashMap<>(); - /** Kategorie-Filter für die Admin-GUI: null = alle */ - private final Map categoryFilter = new HashMap<>(); + private final Map categoryFilter = new HashMap<>(); + private final Map awaitingComment = new HashMap<>(); + private final Set viewingFromArchive = new HashSet<>(); - /** Wartet auf Chat-Eingabe für Close-Kommentar */ - private final Map awaitingComment = new HashMap<>(); + // ── Konstruktor ─────────────────────────────────────────────────────── - /** Aus Archiv heraus in Detail gegangen */ - private final Set viewingFromArchive = new HashSet<>(); + public TicketGUI(TicketPlugin plugin) { + this.plugin = plugin; + reloadConfig(); + reloadTitles(); + } - // ─────────────────────────── Konstruktor ─────────────────────────────── + /** + * Lädt die GUI-Konfiguration (Slots, Reihen, Materialien) aus der config.yml. + */ + public void reloadConfig() { + ConfigurationSection guiSettings = plugin.getConfig().getConfigurationSection("gui-settings"); + ConfigurationSection ticketSettings = guiSettings != null ? guiSettings.getConfigurationSection("ticket") : null; + ConfigurationSection itemsSettings = guiSettings != null ? guiSettings.getConfigurationSection("items") : null; - public TicketGUI(TicketPlugin plugin) { this.plugin = plugin; } + if (ticketSettings != null) { + // Spieler Config + ConfigurationSection playerConf = ticketSettings.getConfigurationSection("player"); + if (playerConf != null) { + playerRows = playerConf.getInt("rows", 6); + if (playerRows < 4) playerRows = 4; // Minimum 4 Reihen + if (playerRows > 6) playerRows = 6; + playerNavPrev = playerConf.getInt("nav.prev", 45); + playerNavNext = playerConf.getInt("nav.next", 53); + playerNavPage = playerConf.getInt("nav.page", 49); + } else { + setDefaultsPlayer(); + } + + // Admin Config + ConfigurationSection adminConf = ticketSettings.getConfigurationSection("admin"); + if (adminConf != null) { + adminRows = adminConf.getInt("rows", 6); + adminNavPrev = adminConf.getInt("nav.prev", 45); + adminNavNext = adminConf.getInt("nav.next", 53); + adminNavPage = adminConf.getInt("nav.page", 48); + adminNavArchive = adminConf.getInt("nav.archive", 49); + adminNavFilter = adminConf.getInt("nav.filter", 47); + } else { + setDefaultsAdmin(); + } + + // Archiv Config + ConfigurationSection archiveConf = ticketSettings.getConfigurationSection("archive"); + if (archiveConf != null) { + archiveNavPrev = archiveConf.getInt("nav.prev", 45); + archiveNavNext = archiveConf.getInt("nav.next", 53); + archiveNavBack = archiveConf.getInt("nav.back", 49); + } else { + setDefaultsArchive(); + } + } else { + setDefaultsPlayer(); + setDefaultsAdmin(); + setDefaultsArchive(); + } + + // Materialien laden + if (itemsSettings != null) { + matNavPrev = getMaterial(itemsSettings, "nav-prev", Material.ARROW); + matNavNext = getMaterial(itemsSettings, "nav-next", Material.ARROW); + matNavPage = getMaterial(itemsSettings, "nav-page", Material.PAPER); + matNavArchive = getMaterial(itemsSettings, "nav-archive", Material.CHEST); + matNavBack = getMaterial(itemsSettings, "nav-back", Material.ARROW); + matNavFilter = getMaterial(itemsSettings, "nav-filter", Material.HOPPER); + matNavAdd = getMaterial(itemsSettings, "nav-add", Material.LIME_WOOL); + } + } + + private void setDefaultsPlayer() { + playerRows = 6; + playerNavPrev = 45; playerNavNext = 53; playerNavPage = 49; + } + private void setDefaultsAdmin() { + adminRows = 6; + adminNavPrev = 45; adminNavNext = 53; adminNavPage = 48; adminNavArchive = 49; adminNavFilter = 47; + } + private void setDefaultsArchive() { + archiveNavPrev = 45; archiveNavNext = 53; archiveNavBack = 49; + } + + private Material getMaterial(ConfigurationSection section, String path, Material def) { + try { + return Material.valueOf(section.getString(path, def.name())); + } catch (IllegalArgumentException e) { + return def; + } + } + + /** + * Hilfsmethode, um einen Slot in die letzte Reihe zu schieben, falls er zu groß ist. + * Dadurch bleibt der Footer immer visuell gleich, egal ob 4 oder 6 Reihen. + */ + private int getSafeSlot(int configuredSlot, int inventorySize) { + if (configuredSlot < inventorySize) { + return configuredSlot; + } + // Der Slot liegt außerhalb. Wir berechnen die Spalte und schieben ihn in die letzte Reihe. + int col = configuredSlot % 9; + return (inventorySize - 9) + col; + } + + /** + * Liest die Inventar-Titel neu aus der Sprachdatei. + */ + public void reloadTitles() { + GUI_TITLE = gi("title-admin"); + CLOSED_GUI_TITLE = gi("title-archive"); + PLAYER_GUI_TITLE = gi("title-player"); + DETAIL_GUI_TITLE = gi("title-detail"); + } + + // ── Kurzformen für lang-Zugriff ────────────────────────────────────── + + private String gi(String key) { + return plugin.lang().get("gui.item." + key); + } + + private String gi(String key, String... replacements) { + return plugin.lang().format("gui.item." + key, replacements); + } // ═══════════════════════════════════════════════════════════════════════ - // ADMIN / SUPPORTER GUI (paginiert, mit Kategorie-Filter) + // ADMIN / SUPPORTER GUI // ═══════════════════════════════════════════════════════════════════════ - public void openGUI(Player player) { openGUI(player, adminPage.getOrDefault(player.getUniqueId(), 0)); } + public void openGUI(Player player) { + openGUI(player, adminPage.getOrDefault(player.getUniqueId(), 0)); + } public void openGUI(Player player, int page) { adminPage.put(player.getUniqueId(), page); @@ -80,32 +217,30 @@ public class TicketGUI implements Listener { List all = plugin.getDatabaseManager().getTicketsByStatus( TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED); - // Kategorie-Filter anwenden ConfigCategory filter = categoryFilter.getOrDefault(player.getUniqueId(), null); - if (filter != null && plugin.getConfig().getBoolean("categories-enabled", true)) { + if (filter != null && plugin.getConfig().getBoolean("categories-enabled", true)) all.removeIf(t -> !t.getCategoryKey().equals(filter.getKey())); - } - // Priorität-Sortierung (URGENT → HIGH → NORMAL → LOW) - if (plugin.getConfig().getBoolean("priorities-enabled", true)) { + if (plugin.getConfig().getBoolean("priorities-enabled", true)) all.sort((a, b) -> b.getPriority().ordinal() - a.getPriority().ordinal()); - } - int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / PAGE_SIZE)); + int size = adminRows * 9; + int pageSize = size - 9; + int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / pageSize)); page = Math.max(0, Math.min(page, totalPages - 1)); adminPage.put(player.getUniqueId(), page); - Inventory inv = Bukkit.createInventory(null, 54, GUI_TITLE); + Inventory inv = Bukkit.createInventory(null, size, GUI_TITLE); Map slotMap = new HashMap<>(); - int start = page * PAGE_SIZE; - for (int i = 0; i < PAGE_SIZE && (start + i) < all.size(); i++) { + int start = page * pageSize; + for (int i = 0; i < pageSize && (start + i) < all.size(); i++) { Ticket ticket = all.get(start + i); inv.setItem(i, buildAdminListItem(ticket)); slotMap.put(i, ticket); } - fillAdminNavigation(inv, false, player, page, totalPages); + fillAdminNavigation(inv, false, player, page, totalPages, size); playerSlotMap.put(player.getUniqueId(), slotMap); player.openInventory(inv); } @@ -114,40 +249,45 @@ public class TicketGUI implements Listener { // ADMIN ARCHIV GUI // ═══════════════════════════════════════════════════════════════════════ - public void openClosedGUI(Player player) { openClosedGUI(player, archivePage.getOrDefault(player.getUniqueId(), 0)); } + public void openClosedGUI(Player player) { + openClosedGUI(player, archivePage.getOrDefault(player.getUniqueId(), 0)); + } public void openClosedGUI(Player player, int page) { if (!player.hasPermission(ARCHIVE_PERMISSION)) { - player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen.")); - return; + player.sendMessage(plugin.lang().get("gui.no-archive-permission")); return; } archivePage.put(player.getUniqueId(), page); List tickets = plugin.getDatabaseManager().getTicketsByStatus(TicketStatus.CLOSED); - int totalPages = Math.max(1, (int) Math.ceil((double) tickets.size() / PAGE_SIZE)); + int size = ARCHIVE_ROWS * 9; + int pageSize = size - 9; + int totalPages = Math.max(1, (int) Math.ceil((double) tickets.size() / pageSize)); page = Math.max(0, Math.min(page, totalPages - 1)); archivePage.put(player.getUniqueId(), page); - Inventory inv = Bukkit.createInventory(null, 54, CLOSED_GUI_TITLE); + Inventory inv = Bukkit.createInventory(null, size, CLOSED_GUI_TITLE); Map slotMap = new HashMap<>(); - int start = page * PAGE_SIZE; - for (int i = 0; i < PAGE_SIZE && (start + i) < tickets.size(); i++) { + int start = page * pageSize; + for (int i = 0; i < pageSize && (start + i) < tickets.size(); i++) { Ticket ticket = tickets.get(start + i); inv.setItem(i, buildAdminListItem(ticket)); slotMap.put(i, ticket); } - fillAdminNavigation(inv, true, player, page, totalPages); + fillAdminNavigation(inv, true, player, page, totalPages, size); playerClosedSlotMap.put(player.getUniqueId(), slotMap); player.openInventory(inv); } // ═══════════════════════════════════════════════════════════════════════ - // SPIELER-GUI (paginiert) + // SPIELER-GUI // ═══════════════════════════════════════════════════════════════════════ - public void openPlayerGUI(Player player) { openPlayerGUI(player, playerPage.getOrDefault(player.getUniqueId(), 0)); } + public void openPlayerGUI(Player player) { + openPlayerGUI(player, playerPage.getOrDefault(player.getUniqueId(), 0)); + } public void openPlayerGUI(Player player, int page) { playerPage.put(player.getUniqueId(), page); @@ -156,32 +296,30 @@ public class TicketGUI implements Listener { TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED, TicketStatus.CLOSED); List tickets = new ArrayList<>(); - for (Ticket t : all) { + for (Ticket t : all) if (t.getCreatorUUID().equals(player.getUniqueId()) && !t.isPlayerDeleted()) tickets.add(t); - } if (tickets.isEmpty()) { - player.sendMessage(plugin.color("&aDu hast aktuell keine Tickets.")); - return; + player.sendMessage(plugin.lang().get("gui.no-tickets")); return; } - int totalPages = Math.max(1, (int) Math.ceil((double) tickets.size() / PAGE_SIZE)); + int size = playerRows * 9; + int pageSize = size - 9; + int totalPages = Math.max(1, (int) Math.ceil((double) tickets.size() / pageSize)); page = Math.max(0, Math.min(page, totalPages - 1)); playerPage.put(player.getUniqueId(), page); - Inventory inv = Bukkit.createInventory(null, 54, PLAYER_GUI_TITLE); + Inventory inv = Bukkit.createInventory(null, size, PLAYER_GUI_TITLE); Map slotMap = new HashMap<>(); - int start = page * PAGE_SIZE; - for (int i = 0; i < PAGE_SIZE && (start + i) < tickets.size(); i++) { + int start = page * pageSize; + for (int i = 0; i < pageSize && (start + i) < tickets.size(); i++) { Ticket ticket = tickets.get(start + i); inv.setItem(i, buildPlayerTicketItem(ticket)); slotMap.put(i, ticket); } - // Nav-Leiste für Spieler-GUI (nur Prev/Next, kein Filter/Archiv) - fillPlayerNavigation(inv, page, totalPages); - + fillPlayerNavigation(inv, page, totalPages, size); playerOwnSlotMap.put(player.getUniqueId(), slotMap); player.openInventory(inv); } @@ -193,79 +331,89 @@ public class TicketGUI implements Listener { public void openDetailGUI(Player player, Ticket ticket) { Inventory inv = Bukkit.createInventory(null, 27, DETAIL_GUI_TITLE); - // Slot 4: Ticket-Info inv.setItem(4, buildDetailInfoItem(ticket)); - // ── Teleport-Button ─────────────────────────────────────────────── - // Standalone: → normaler Teleport-Button - // BungeeCord + bungee-teleport-enabled: → serverübergreifender Teleport-Button - // BungeeCord + bungee-teleport deaktiviert → gesperrter Button + // ── Teleport-Button ────────────────────────────────────────────── if (!plugin.isBungeeCordEnabled()) { - inv.setItem(10, buildActionItem(Material.ENDER_PEARL, "§b§lTeleportieren", - List.of("§7Teleportiert dich zur", "§7Position des Tickets."))); + inv.setItem(10, buildActionItem(Material.ENDER_PEARL, + gi("btn-teleport"), + List.of(gi("btn-teleport-lore1"), gi("btn-teleport-lore2")))); + } else if (plugin.getConfig().getBoolean("bungee-teleport-enabled", true)) { String targetServer = ticket.getServerName(); boolean sameServer = plugin.getServerName().equals(targetServer); String serverLine = "unknown".equals(targetServer) - ? "§cServer unbekannt" + ? gi("btn-teleport-unknown") : sameServer - ? "§7Dieser Server §a(direkt)" - : "§7Ziel-Server: §b" + targetServer; - inv.setItem(10, buildActionItem(Material.ENDER_PEARL, "§b§lTeleportieren", - List.of("§7Teleportiert dich zur Ticket-Position.", serverLine, - "§8" + (sameServer ? "Lokaler Teleport" : "Server-Wechsel erforderlich")))); + ? gi("btn-teleport-same") + : gi("btn-teleport-other", "{server}", targetServer); + String localLine = sameServer ? gi("btn-teleport-local") : gi("btn-teleport-switch"); + inv.setItem(10, buildActionItem(Material.ENDER_PEARL, + gi("btn-teleport"), + List.of(gi("btn-teleport-bungee1"), serverLine, localLine))); + } else { String serverInfo = !"unknown".equals(ticket.getServerName()) - ? "§7Ticket-Server: §b" + ticket.getServerName() - : "§7Server unbekannt"; - inv.setItem(10, buildActionItem(Material.GRAY_STAINED_GLASS_PANE, "§8Teleport deaktiviert", - List.of("§7Im BungeeCord-Modus ist", "§7Teleportation deaktiviert.", serverInfo, - "§8(bungee-teleport-enabled: false)"))); + ? gi("btn-teleport-server", "{server}", ticket.getServerName()) + : gi("btn-teleport-noserver"); + inv.setItem(10, buildActionItem(Material.GRAY_STAINED_GLASS_PANE, + gi("btn-teleport-disabled"), + List.of(gi("btn-teleport-dis1"), gi("btn-teleport-dis2"), + serverInfo, gi("btn-teleport-dis3")))); } - // Slot 12: Claimen / Löschen / Grau + // ── Claim / Permanent-Löschen-Button ───────────────────────────── if (ticket.getStatus() == TicketStatus.OPEN) { - inv.setItem(12, buildActionItem(Material.LIME_WOOL, "§a§lTicket annehmen", - List.of("§7Nimmt dieses Ticket an", "§7und markiert es als bearbeitet."))); + inv.setItem(12, buildActionItem(Material.LIME_WOOL, + gi("btn-claim"), + List.of(gi("btn-claim-lore1"), gi("btn-claim-lore2")))); + } else if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) { - inv.setItem(12, buildActionItem(Material.BARRIER, "§4§lTicket permanent löschen", - List.of("§7Löscht dieses Ticket", "§7unwiderruflich aus der Datenbank.", - "§8§m ", "§c§lACHTUNG: §cNicht rückgängig zu machen!"))); + inv.setItem(12, buildActionItem(Material.BARRIER, + gi("btn-delete"), + List.of(gi("btn-delete-lore1"), gi("btn-delete-lore2"), + "§8§m ", gi("btn-delete-warn")))); + } else { - inv.setItem(12, buildActionItem(Material.GRAY_WOOL, "§8Bereits angenommen", - List.of("§7Dieses Ticket wurde bereits", "§7angenommen."))); + inv.setItem(12, buildActionItem(Material.GRAY_WOOL, + gi("btn-claimed"), + List.of(gi("btn-claimed-lore1"), gi("btn-claimed-lore2")))); } - // Slot 14: Schließen + // ── Schließen-Button ───────────────────────────────────────────── if (ticket.getStatus() != TicketStatus.CLOSED) { - inv.setItem(14, buildActionItem(Material.RED_WOOL, "§c§lTicket schließen", - List.of("§7Schließt das Ticket.", "§8§m ", "§eKlick für Kommentar-Eingabe."))); + inv.setItem(14, buildActionItem(Material.RED_WOOL, + gi("btn-close"), + List.of(gi("btn-close-lore1"), "§8§m ", gi("btn-close-lore2")))); } else { - inv.setItem(14, buildActionItem(Material.GRAY_WOOL, "§8Bereits geschlossen", - List.of("§7Dieses Ticket ist bereits", "§7geschlossen."))); + inv.setItem(14, buildActionItem(Material.GRAY_WOOL, + gi("btn-closed"), + List.of(gi("btn-closed-lore1"), gi("btn-closed-lore2")))); } - // Slot 22: Kommentare anzeigen - inv.setItem(22, buildActionItem(Material.BOOK, "§e§lKommentare anzeigen", - List.of("§7Zeigt alle Nachrichten/Antworten", "§7zu diesem Ticket im Chat."))); + // ── Kommentare-Button ──────────────────────────────────────────── + inv.setItem(22, buildActionItem(Material.BOOK, + gi("btn-comments"), + List.of(gi("btn-comments-lore1"), gi("btn-comments-lore2")))); - // Slot 20: Priorität ändern (nur wenn priorities-enabled und nicht geschlossen) + // ── Priorität-Button ───────────────────────────────────────────── if (plugin.getConfig().getBoolean("priorities-enabled", true) && player.hasPermission("ticket.support") && ticket.getStatus() != TicketStatus.CLOSED) { TicketPriority cur = ticket.getPriority(); List prioLore = new ArrayList<>(); - prioLore.add("§7Aktuell: " + cur.getColored()); - prioLore.add("§8Klicken zum Wechseln"); + prioLore.add(gi("btn-prio-current", "{value}", cur.getColored())); + prioLore.add(gi("btn-prio-click")); prioLore.add("§8§m "); for (TicketPriority p : TicketPriority.values()) prioLore.add((p == cur ? "§a» " : "§7 ") + p.getColored()); - inv.setItem(20, buildActionItem(cur.getGuiMaterial(), "§6§lPriorität ändern", prioLore)); + inv.setItem(20, buildActionItem(cur.getGuiMaterial(), gi("btn-prio"), prioLore)); } - // Slot 16: Zurück - inv.setItem(16, buildActionItem(Material.ARROW, "§7§lZurück", - List.of("§7Zurück zur Ticket-Übersicht."))); + // ── Zurück-Button ──────────────────────────────────────────────── + inv.setItem(16, buildActionItem(Material.ARROW, + gi("btn-back"), + List.of(gi("btn-back-lore")))); fillEmpty(inv); detailTicketMap.put(player.getUniqueId(), ticket); @@ -288,10 +436,9 @@ public class TicketGUI implements Listener { int slot = event.getRawSlot(); if (slot < 0) return; - // ── Admin Haupt-Übersicht ─────────────────────────────────────────── if (title.equals(GUI_TITLE)) { handleAdminNavClick(player, slot, false); - if (slot < PAGE_SIZE) { + if (slot < (adminRows * 9) - 9) { Map slotMap = playerSlotMap.get(player.getUniqueId()); if (slotMap == null) return; Ticket ticket = slotMap.get(slot); @@ -304,10 +451,9 @@ public class TicketGUI implements Listener { return; } - // ── Admin Archiv ─────────────────────────────────────────────────── if (title.equals(CLOSED_GUI_TITLE)) { handleArchiveNavClick(player, slot); - if (slot < PAGE_SIZE) { + if (slot < (ARCHIVE_ROWS * 9) - 9) { Map slotMap = playerClosedSlotMap.get(player.getUniqueId()); if (slotMap == null) return; Ticket ticket = slotMap.get(slot); @@ -320,13 +466,17 @@ public class TicketGUI implements Listener { return; } - // ── Spieler-GUI ──────────────────────────────────────────────────── if (title.equals(PLAYER_GUI_TITLE)) { int curPage = playerPage.getOrDefault(player.getUniqueId(), 0); - if (slot == 45) { openPlayerGUI(player, curPage - 1); return; } - if (slot == 53) { openPlayerGUI(player, curPage + 1); return; } + + // Wir müssen hier auch prüfen, ob der geklickte Slot ein adaptierter Nav-Slot ist + int safePrev = getSafeSlot(playerNavPrev, playerRows * 9); + int safeNext = getSafeSlot(playerNavNext, playerRows * 9); - if (slot < PAGE_SIZE) { + if (slot == safePrev) { openPlayerGUI(player, curPage - 1); return; } + if (slot == safeNext) { openPlayerGUI(player, curPage + 1); return; } + + if (slot < (playerRows * 9) - 9) { Map slotMap = playerOwnSlotMap.get(player.getUniqueId()); if (slotMap == null) return; Ticket ticket = slotMap.get(slot); @@ -337,20 +487,20 @@ public class TicketGUI implements Listener { boolean success = plugin.getDatabaseManager().markAsPlayerDeleted(ticket.getId()); Bukkit.getScheduler().runTask(plugin, () -> { if (success) { - player.sendMessage(plugin.color("&aDein Ticket &e#" + ticket.getId() + " &awurde aus deiner Übersicht entfernt.")); + player.sendMessage(plugin.lang().format("gui.ticket-removed", + "{id}", String.valueOf(ticket.getId()))); openPlayerGUI(player); } else { - player.sendMessage(plugin.color("&cFehler beim Entfernen des Tickets.")); + player.sendMessage(plugin.lang().get("gui.ticket-remove-error")); } }); } else { - player.sendMessage(plugin.color("&cDu kannst dieses Ticket nicht löschen, da es bereits von einem Supporter bearbeitet wird.")); + player.sendMessage(plugin.lang().get("gui.ticket-remove-claimed")); } } return; } - // ── Admin Detail-GUI ─────────────────────────────────────────────── if (title.equals(DETAIL_GUI_TITLE)) { Ticket ticket = detailTicketMap.get(player.getUniqueId()); if (ticket == null) return; @@ -358,11 +508,10 @@ public class TicketGUI implements Listener { switch (slot) { case 10 -> handleDetailTeleport(player, ticket); case 12 -> { - if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) { + if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) handleDetailPermanentDelete(player, ticket); - } else { + else handleDetailClaim(player, ticket); - } } case 14 -> handleDetailClose(player, ticket); case 20 -> handleDetailCyclePriority(player, ticket); @@ -375,195 +524,175 @@ public class TicketGUI implements Listener { } } - // ─────────────────────────── Navigation-Handler ───────────────────────── + // ── Navigation-Handler ──────────────────────────────────────────────── private void handleAdminNavClick(Player player, int slot, boolean isArchive) { int curPage = adminPage.getOrDefault(player.getUniqueId(), 0); - switch (slot) { - case 45 -> openGUI(player, curPage - 1); - case 53 -> openGUI(player, curPage + 1); - case 49 -> { - if (player.hasPermission(ARCHIVE_PERMISSION)) openClosedGUI(player); - else player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen.")); + int invSize = adminRows * 9; + + int safePrev = getSafeSlot(adminNavPrev, invSize); + int safeNext = getSafeSlot(adminNavNext, invSize); + int safeArchive = getSafeSlot(adminNavArchive, invSize); + int safeFilter = getSafeSlot(adminNavFilter, invSize); + + if (slot == safePrev) { + openGUI(player, curPage - 1); + } else if (slot == safeNext) { + openGUI(player, curPage + 1); + } else if (slot == safeArchive) { + if (player.hasPermission(ARCHIVE_PERMISSION)) { + openClosedGUI(player); + } else { + player.sendMessage(plugin.lang().get("gui.no-archive-permission")); } - case 47 -> { - if (plugin.getConfig().getBoolean("categories-enabled", true)) { - cycleCategoryFilter(player); - openGUI(player, 0); - } + } else if (slot == safeFilter) { + if (plugin.getConfig().getBoolean("categories-enabled", true)) { + cycleCategoryFilter(player); + openGUI(player, 0); } } } private void handleArchiveNavClick(Player player, int slot) { int curPage = archivePage.getOrDefault(player.getUniqueId(), 0); - switch (slot) { - case 45 -> openClosedGUI(player, curPage - 1); - case 53 -> openClosedGUI(player, curPage + 1); - case 49 -> openGUI(player); - } + int invSize = ARCHIVE_ROWS * 9; + + int safePrev = getSafeSlot(archiveNavPrev, invSize); + int safeNext = getSafeSlot(archiveNavNext, invSize); + int safeBack = getSafeSlot(archiveNavBack, invSize); + + if (slot == safePrev) openClosedGUI(player, curPage - 1); + if (slot == safeNext) openClosedGUI(player, curPage + 1); + if (slot == safeBack) openGUI(player); } private void cycleCategoryFilter(Player player) { - CategoryManager cm = plugin.getCategoryManager(); + CategoryManager cm = plugin.getCategoryManager(); List all = cm.getAll(); - ConfigCategory current = categoryFilter.getOrDefault(player.getUniqueId(), null); + ConfigCategory current = categoryFilter.getOrDefault(player.getUniqueId(), null); if (current == null) { if (!all.isEmpty()) categoryFilter.put(player.getUniqueId(), all.get(0)); } else { - int idx = all.indexOf(current); + int idx = all.indexOf(current); int next = idx + 1; if (next >= all.size()) categoryFilter.remove(player.getUniqueId()); else categoryFilter.put(player.getUniqueId(), all.get(next)); } ConfigCategory newFilter = categoryFilter.getOrDefault(player.getUniqueId(), null); String filterName = newFilter != null ? newFilter.getColored() : "§7Alle"; - player.sendMessage(plugin.color("&7Filter: " + filterName)); + player.sendMessage(plugin.lang().format("gui.filter-label", "{filter}", filterName)); } - // ─────────────────────────── Detail-Aktionen ──────────────────────────── + // ── Detail-Aktionen ─────────────────────────────────────────────────── private void openTicketDetailAsync(Player player, Ticket currentTicket) { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Ticket fresh = plugin.getDatabaseManager().getTicketById(currentTicket.getId()); Bukkit.getScheduler().runTask(plugin, () -> { - if (fresh == null) { player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); return; } + if (fresh == null) { plugin.lang().send(player, "general.ticket-not-found"); return; } openDetailGUI(player, fresh); }); }); } - // ── BUG FIX: handleDetailTeleport ──────────────────────────────────────── - // Vorher: Teleport wurde immer ausgeführt – auch bei aktivem BungeeCord. - // ticket.getLocation() gibt null zurück wenn die World auf diesem - // Server nicht existiert → NullPointerException oder falscher Teleport. - // - // Fix: Bei bungeecord: true + bungee-teleport-enabled: true → - // 1. Zielposition in DB speichern (ticket_pending_teleport) - // 2. Spieler via Plugin Messaging Channel auf Ziel-Server schicken - // 3. PlayerJoinListener teleportiert ihn dort zur Position - // Bei bungeecord: true + bungee-teleport-enabled: false → gesperrt. - // Bei bungeecord: false → normaler lokaler Teleport wie bisher. - // - // Hinweis: Ist der Admin bereits auf dem Ziel-Server, wird direkt teleportiert. - // ───────────────────────────────────────────────────────────────────────── private void handleDetailTeleport(Player player, Ticket ticket) { if (!plugin.isBungeeCordEnabled()) { - // ── Standalone-Modus: direkt teleportieren ── if (ticket.getLocation() != null) { player.teleport(ticket.getLocation()); - player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert.")); + player.sendMessage(plugin.lang().format("gui.teleport-success", "{id}", String.valueOf(ticket.getId()))); } else { - player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!")); + player.sendMessage(plugin.lang().get("gui.world-not-loaded")); } openDetailGUI(player, ticket); return; } - // ── BungeeCord-Modus ────────────────────────────────────────────── boolean bungeeTP = plugin.getConfig().getBoolean("bungee-teleport-enabled", true); if (!bungeeTP) { String serverHint = !"unknown".equals(ticket.getServerName()) ? " §7(Server: §b" + ticket.getServerName() + "§7)" : ""; - player.sendMessage(plugin.color("&cServerübergreifender Teleport ist in der Config deaktiviert." + serverHint)); + player.sendMessage(plugin.lang().format("gui.teleport-disabled", "{hint}", serverHint)); openDetailGUI(player, ticket); return; } String targetServer = ticket.getServerName(); if ("unknown".equals(targetServer)) { - player.sendMessage(plugin.color("&cServer des Tickets unbekannt – Teleport nicht möglich.")); + player.sendMessage(plugin.lang().get("gui.teleport-unknown")); openDetailGUI(player, ticket); return; } String currentServer = plugin.getServerName(); - if (currentServer.equals(targetServer)) { - // ── Bereits auf dem richtigen Server: direkt teleportieren ──── if (ticket.getLocation() != null) { player.teleport(ticket.getLocation()); - player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert.")); + player.sendMessage(plugin.lang().format("gui.teleport-success", "{id}", String.valueOf(ticket.getId()))); } else { - player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!")); + player.sendMessage(plugin.lang().get("gui.world-not-loaded")); } openDetailGUI(player, ticket); } else { - // ── Anderer Server: Position in DB speichern + Server-Wechsel ─ Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { plugin.getDatabaseManager().setPendingTeleport( player.getUniqueId(), ticket.getWorldName(), ticket.getX(), ticket.getY(), ticket.getZ(), - ticket.getYaw(), ticket.getPitch() - ); + ticket.getYaw(), ticket.getPitch()); Bukkit.getScheduler().runTask(plugin, () -> { - // BungeeCord Plugin Messaging Channel: Spieler auf Ziel-Server schicken - player.sendMessage(plugin.color("&7Verbinde dich mit Server &b" + targetServer - + " &7für Ticket &e#" + ticket.getId() + "&7...")); + player.sendMessage(plugin.lang().format("gui.bungee-connect", + "{server}", targetServer, "{id}", String.valueOf(ticket.getId()))); try { - java.io.ByteArrayOutputStream b = new java.io.ByteArrayOutputStream(); - java.io.DataOutputStream out = new java.io.DataOutputStream(b); + java.io.ByteArrayOutputStream b = new java.io.ByteArrayOutputStream(); + java.io.DataOutputStream out = new java.io.DataOutputStream(b); out.writeUTF("Connect"); out.writeUTF(targetServer); player.sendPluginMessage(plugin, "BungeeCord", b.toByteArray()); } catch (Exception e) { - plugin.getLogger().warning("[TicketSystem] BungeeCord Connect fehlgeschlagen: " + e.getMessage()); - player.sendMessage(plugin.color("&cServer-Wechsel fehlgeschlagen. Bitte manuell verbinden.")); + plugin.getLogger().warning("[TicketSystem] BungeeCord Connect failed: " + e.getMessage()); + player.sendMessage(plugin.lang().get("gui.bungee-connect-fail")); } }); }); } } - // ── BUG FIX: handleDetailClaim ─────────────────────────────────────────── - // Vorher: Nach erfolgreichem Claim wurde immer teleportiert wenn - // ticket.getLocation() != null – unabhängig von BungeeCord. - // - // Fix: Teleport nach Claim nutzt dieselbe Logik wie handleDetailTeleport: - // Standalone → direkt, BungeeCord + enabled → Server-Wechsel + pending, - // BungeeCord + disabled → nur Nachricht, kein Teleport. - // ───────────────────────────────────────────────────────────────────────── private void handleDetailClaim(Player player, Ticket ticket) { if (ticket.getStatus() != TicketStatus.OPEN) { - player.sendMessage(plugin.formatMessage("messages.already-claimed")); - return; + plugin.lang().send(player, "general.already-claimed"); return; } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().claimTicket(ticket.getId(), player.getUniqueId(), player.getName()); + boolean success = plugin.getDatabaseManager().claimTicket( + ticket.getId(), player.getUniqueId(), player.getName()); Bukkit.getScheduler().runTask(plugin, () -> { - if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; } - player.sendMessage(plugin.formatMessage("messages.ticket-claimed") - .replace("{id}", String.valueOf(ticket.getId())) - .replace("{player}", ticket.getCreatorName())); + if (!success) { plugin.lang().send(player, "general.already-claimed"); return; } + player.sendMessage(plugin.lang().format("ticket.claimed", + "{id}", String.valueOf(ticket.getId()), "{player}", ticket.getCreatorName())); ticket.setClaimerUUID(player.getUniqueId()); ticket.setClaimerName(player.getName()); plugin.getTicketManager().notifyCreatorClaimed(ticket); - - // Teleport nach dem Claim entfernt – Teleport nur noch über das separate GUI-Item möglich. }); }); } private void handleDetailPermanentDelete(Player player, Ticket ticket) { if (!player.hasPermission(ARCHIVE_PERMISSION)) { - player.sendMessage(plugin.color("&cDu hast keine Berechtigung, Tickets permanent zu löschen.")); - return; + player.sendMessage(plugin.lang().get("gui.no-delete-permission")); return; } if (ticket.getStatus() != TicketStatus.CLOSED) { - player.sendMessage(plugin.color("&cNur geschlossene Tickets können permanent gelöscht werden.")); - return; + player.sendMessage(plugin.lang().get("gui.only-closed-deletable")); return; } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { boolean success = plugin.getDatabaseManager().deleteTicket(ticket.getId()); Bukkit.getScheduler().runTask(plugin, () -> { if (success) { - player.sendMessage(plugin.color("&aTicket &e#" + ticket.getId() + " &awurde permanent gelöscht.")); + player.sendMessage(plugin.lang().format("gui.ticket-deleted", + "{id}", String.valueOf(ticket.getId()))); viewingFromArchive.remove(player.getUniqueId()); openClosedGUI(player); } else { - player.sendMessage(plugin.color("&cFehler beim Löschen des Tickets.")); + player.sendMessage(plugin.lang().get("gui.ticket-delete-error")); openClosedGUI(player); } }); @@ -572,38 +701,35 @@ public class TicketGUI implements Listener { private void handleDetailClose(Player player, Ticket ticket) { if (ticket.getStatus() == TicketStatus.CLOSED) { - player.sendMessage(plugin.color("&cDieses Ticket ist bereits geschlossen.")); - return; + player.sendMessage(plugin.lang().get("gui.already-closed")); return; } awaitingComment.put(player.getUniqueId(), ticket.getId()); - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6Ticket #" + ticket.getId() + " schließen")); - player.sendMessage(plugin.color("&7Gib einen Kommentar ein (&e- &7für keinen).")); - player.sendMessage(plugin.color("&7Abbrechen mit &ccancel")); - player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().format("gui.close-prompt-header", "{id}", String.valueOf(ticket.getId()))); + player.sendMessage(plugin.lang().get("gui.close-prompt-hint")); + player.sendMessage(plugin.lang().get("gui.close-prompt-cancel")); + player.sendMessage(plugin.lang().get("general.separator")); } private void handleDetailCyclePriority(Player player, Ticket ticket) { if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.color("&cDu hast keine Berechtigung, die Priorität zu ändern.")); - return; + player.sendMessage(plugin.lang().get("gui.no-priority-permission")); return; } if (!plugin.getConfig().getBoolean("priorities-enabled", true)) return; if (ticket.getStatus() == TicketStatus.CLOSED) { - player.sendMessage(plugin.color("&cDie Priorität geschlossener Tickets kann nicht geändert werden.")); - return; + player.sendMessage(plugin.lang().get("gui.priority-closed")); return; } TicketPriority[] values = TicketPriority.values(); - TicketPriority next = values[(ticket.getPriority().ordinal() + 1) % values.length]; + TicketPriority next = values[(ticket.getPriority().ordinal() + 1) % values.length]; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { boolean success = plugin.getDatabaseManager().setTicketPriority(ticket.getId(), next); Bukkit.getScheduler().runTask(plugin, () -> { if (success) { ticket.setPriority(next); - player.sendMessage(plugin.color("&aPriorität auf " + next.getColored() + " &agesetzt.")); + player.sendMessage(plugin.lang().format("gui.priority-set", "{priority}", next.getColored())); openDetailGUI(player, ticket); } else { - player.sendMessage(plugin.color("&cFehler beim Ändern der Priorität.")); + player.sendMessage(plugin.lang().get("gui.priority-error")); openDetailGUI(player, ticket); } }); @@ -614,23 +740,25 @@ public class TicketGUI implements Listener { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { List comments = plugin.getDatabaseManager().getComments(ticket.getId()); Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6Kommentare zu Ticket #" + ticket.getId())); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().format("gui.comments-header", + "{id}", String.valueOf(ticket.getId()))); if (comments.isEmpty()) { - player.sendMessage(plugin.color("&7Noch keine Kommentare vorhanden.")); + player.sendMessage(plugin.lang().get("gui.comments-empty")); } else { for (TicketComment c : comments) { - String time = DATE_FORMAT.format(c.getCreatedAt()); - player.sendMessage(plugin.color("&e" + c.getAuthorName() + " &7(" + time + ")&8: &f" + c.getMessage())); + String time = c.getCreatedAt() != null ? DATE_FORMAT.format(c.getCreatedAt()) : "?"; + player.sendMessage(plugin.lang().format("gui.comments-entry", + "{author}", c.getAuthorName(), "{time}", time, "{message}", c.getMessage())); } } - player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.lang().get("general.separator")); openDetailGUI(player, ticket); }); }); } - // ─────────────────────────── Chat-Events ──────────────────────────────── + // ── Chat-Events ─────────────────────────────────────────────────────── @EventHandler(priority = EventPriority.LOWEST) public void onPlayerChat(AsyncPlayerChatEvent event) { @@ -638,11 +766,12 @@ public class TicketGUI implements Listener { if (!awaitingComment.containsKey(player.getUniqueId())) return; event.setCancelled(true); - int ticketId = awaitingComment.remove(player.getUniqueId()); - String input = event.getMessage().trim(); + int ticketId = awaitingComment.remove(player.getUniqueId()); + String input = event.getMessage().trim(); if (input.equalsIgnoreCase("cancel")) { - Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cAbgebrochen."))); + Bukkit.getScheduler().runTask(plugin, () -> + player.sendMessage(plugin.lang().get("gui.close-cancelled"))); return; } @@ -652,19 +781,14 @@ public class TicketGUI implements Listener { boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment); if (success) { Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); - - // ── FIX: Schließung in persistente Stats-Tabelle eintragen ────────── - // Vorher fehlte dieser Aufruf in der GUI – Bewertungen wurden dem - // schließenden Admin zugeordnet nur wenn /ticket close genutzt wurde. - // Jetzt wird player.getName() korrekt als closerName übergeben, - // unabhängig davon ob das Ticket vorher von jemand anderem geclaimed war. - if (ticket != null) { + if (ticket != null) plugin.getDatabaseManager().recordClosedTicket(ticket, player.getName()); - } - Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId))); - if (!comment.isEmpty()) player.sendMessage(plugin.color("&7Kommentar: &f" + comment)); + player.sendMessage(plugin.lang().format("ticket.closed", + "{id}", String.valueOf(ticketId))); + if (!comment.isEmpty()) + player.sendMessage(plugin.lang().format("gui.close-comment-echo", + "{comment}", comment)); if (ticket != null) { ticket.setCloseComment(comment); plugin.getTicketManager().notifyCreatorClosed(ticket, player.getName()); @@ -674,55 +798,99 @@ public class TicketGUI implements Listener { }); } - // ─────────────────────────── Item-Builder ────────────────────────────── + // ── Item-Builder ────────────────────────────────────────────────────── - private void fillAdminNavigation(Inventory inv, boolean isArchiveView, Player player, int page, int totalPages) { + private void fillAdminNavigation(Inventory inv, boolean isArchiveView, Player player, + int page, int totalPages, int invSize) { ItemStack glass = makeGlass(); - for (int i = 45; i < 54; i++) inv.setItem(i, glass); + // Letzte Reihe mit Glas füllen + for (int i = invSize - 9; i < invSize; i++) inv.setItem(i, glass); - if (page > 0) { - inv.setItem(45, buildActionItem(Material.ARROW, "§7§l◄ Zurück", - List.of("§7Seite " + page + " von " + totalPages))); - } - if (page < totalPages - 1) { - inv.setItem(53, buildActionItem(Material.ARROW, "§7§lWeiter ►", - List.of("§7Seite " + (page + 2) + " von " + totalPages))); - } + // Sichere Slots berechnen (Footer bleibt gleich) + int prev = isArchiveView ? getSafeSlot(archiveNavPrev, invSize) : getSafeSlot(adminNavPrev, invSize); + int next = isArchiveView ? getSafeSlot(archiveNavNext, invSize) : getSafeSlot(adminNavNext, invSize); + + int pageSize = invSize - 9; + + if (page > 0) + inv.setItem(prev, buildActionItem(matNavPrev, + gi("nav-prev"), + List.of(gi("nav-prev-lore", "{page}", String.valueOf(page), "{total}", String.valueOf(totalPages))))); + + if (page < totalPages - 1) + inv.setItem(next, buildActionItem(matNavNext, + gi("nav-next"), + List.of(gi("nav-next-lore", "{page}", String.valueOf(page + 2), "{total}", String.valueOf(totalPages))))); if (!isArchiveView) { - if (player.hasPermission(ARCHIVE_PERMISSION)) { - inv.setItem(49, buildActionItem(Material.CHEST, "§7§lGeschlossene Tickets", - List.of("§7Zeigt alle abgeschlossenen", "§7Tickets im Archiv an."))); - } + int archiveSlot = getSafeSlot(adminNavArchive, invSize); + int filterSlot = getSafeSlot(adminNavFilter, invSize); + int pageSlot = getSafeSlot(adminNavPage, invSize); + + if (player.hasPermission(ARCHIVE_PERMISSION)) + inv.setItem(archiveSlot, buildActionItem(matNavArchive, + gi("nav-archive"), + List.of(gi("nav-archive-lore1"), gi("nav-archive-lore2")))); + if (plugin.getConfig().getBoolean("categories-enabled", true)) { ConfigCategory currentFilter = categoryFilter.getOrDefault(player.getUniqueId(), null); - String filterLabel = currentFilter != null ? currentFilter.getColored() : "§7Alle"; + String filterLabel = currentFilter != null ? currentFilter.getColored() : gi("nav-filter-all"); List filterLore = new ArrayList<>(); - filterLore.add("§7Aktuell: " + filterLabel); - filterLore.add("§8Klicken zum Wechseln"); + filterLore.add(gi("nav-filter-current", "{value}", filterLabel)); + filterLore.add(gi("nav-filter-click")); filterLore.add("§8§m "); - for (ConfigCategory cat : plugin.getCategoryManager().getAll()) { + for (ConfigCategory cat : plugin.getCategoryManager().getAll()) filterLore.add((cat.equals(currentFilter) ? "§a» " : "§7 ") + cat.getColored()); - } - filterLore.add((currentFilter == null ? "§a» " : "§7 ") + "§7Alle (kein Filter)"); - inv.setItem(47, buildActionItem(Material.HOPPER, "§e§lKategorie-Filter", filterLore)); + filterLore.add((currentFilter == null ? "§a» " : "§7 ") + gi("nav-filter-all")); + inv.setItem(filterSlot, buildActionItem(matNavFilter, gi("nav-filter"), filterLore)); } + + // Seiten-Info + Map sm = playerSlotMap.get(player.getUniqueId()); + int count = sm != null ? sm.size() : 0; + inv.setItem(pageSlot, buildActionItem(matNavPage, + gi("nav-page", "{page}", String.valueOf(page + 1), "{total}", String.valueOf(totalPages)), + List.of(gi("nav-page-lore", "{count}", String.valueOf(count))))); } else { - inv.setItem(49, buildActionItem(Material.ARROW, "§7§lZurück zur Übersicht", - List.of("§7Zeigt alle offenen Tickets."))); + int backSlot = getSafeSlot(archiveNavBack, invSize); + inv.setItem(backSlot, buildActionItem(matNavBack, + gi("nav-back-overview"), + List.of(gi("nav-back-ov-lore")))); + + // Seiten-Info + Map sm = playerClosedSlotMap.get(player.getUniqueId()); + int count = sm != null ? sm.size() : 0; + // Im Archiv nutzen wir die Mitte der letzten Reihe (Slot 49 bei 6 Reihen) + int archivePageSlot = (ARCHIVE_ROWS - 1) * 9 + 4; + inv.setItem(archivePageSlot, buildActionItem(matNavPage, + gi("nav-page", "{page}", String.valueOf(page + 1), "{total}", String.valueOf(totalPages)), + List.of(gi("nav-page-lore", "{count}", String.valueOf(count))))); } - - inv.setItem(48, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages, - List.of("§7Gesamt: " + (playerSlotMap.containsKey(player.getUniqueId()) - ? playerSlotMap.get(player.getUniqueId()).size() + "+" : "?") + " Tickets auf dieser Seite"))); } - private void fillPlayerNavigation(Inventory inv, int page, int totalPages) { + private void fillPlayerNavigation(Inventory inv, int page, int totalPages, int invSize) { ItemStack glass = makeGlass(); - for (int i = 45; i < 54; i++) inv.setItem(i, glass); - if (page > 0) inv.setItem(45, buildActionItem(Material.ARROW, "§7§l◄ Zurück", List.of("§7Seite " + page + " von " + totalPages))); - if (page < totalPages - 1) inv.setItem(53, buildActionItem(Material.ARROW, "§7§lWeiter ►", List.of("§7Seite " + (page + 2) + " von " + totalPages))); - inv.setItem(49, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages, List.of())); + // Letzte Reihe mit Glas füllen + for (int i = invSize - 9; i < invSize; i++) inv.setItem(i, glass); + + // Sichere Slots berechnen: Wenn der konfigurierte Slot zu groß ist, wird er in die letzte Reihe verschoben + int prev = getSafeSlot(playerNavPrev, invSize); + int next = getSafeSlot(playerNavNext, invSize); + int pageSlot = getSafeSlot(playerNavPage, invSize); + + if (page > 0) + inv.setItem(prev, buildActionItem(matNavPrev, + gi("nav-prev"), + List.of(gi("nav-prev-lore", "{page}", String.valueOf(page), "{total}", String.valueOf(totalPages))))); + + if (page < totalPages - 1) + inv.setItem(next, buildActionItem(matNavNext, + gi("nav-next"), + List.of(gi("nav-next-lore", "{page}", String.valueOf(page + 2), "{total}", String.valueOf(totalPages))))); + + inv.setItem(pageSlot, buildActionItem(matNavPage, + gi("nav-page", "{page}", String.valueOf(page + 1), "{total}", String.valueOf(totalPages)), + List.of())); } private ItemStack buildAdminListItem(Ticket ticket) { @@ -739,28 +907,36 @@ public class TicketGUI implements Listener { } ItemStack item = new ItemStack(mat); - ItemMeta meta = item.getItemMeta(); + ItemMeta meta = item.getItemMeta(); if (meta == null) return item; String priorityPrefix = plugin.getConfig().getBoolean("priorities-enabled", true) ? ticket.getPriority().getColored() + " §8| " : ""; - meta.setDisplayName(priorityPrefix + "§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); + meta.setDisplayName(priorityPrefix + "§6§lTicket #" + ticket.getId() + + " §r" + ticket.getStatus().getColored()); + List lore = new ArrayList<>(); lore.add("§8§m "); - lore.add("§7Ersteller: §e" + ticket.getCreatorName()); - lore.add("§7Anliegen: §f" + ticket.getMessage()); - lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt())); + lore.add(gi("lore-creator", "{value}", ticket.getCreatorName())); + lore.add(gi("lore-message", "{value}", ticket.getMessage())); + lore.add(gi("lore-created", "{value}", ticket.getCreatedAt() != null ? DATE_FORMAT.format(ticket.getCreatedAt()) : "?")); + if (plugin.getConfig().getBoolean("categories-enabled", true)) { - ConfigCategory _cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); - lore.add("§7Kategorie: " + _cat.getColored()); + ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); + lore.add(gi("lore-category", "{value}", cat.getColored())); } if (plugin.getConfig().getBoolean("priorities-enabled", true)) - lore.add("§7Priorität: " + ticket.getPriority().getColored()); - if (ticket.getStatus() == TicketStatus.CLOSED && ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) - lore.add("§7Kommentar: §f" + ticket.getCloseComment()); - if (ticket.isPlayerDeleted()) lore.add("§cSpieler hat Ticket gelöscht."); + lore.add(gi("lore-priority", "{value}", ticket.getPriority().getColored())); + + if (ticket.getStatus() == TicketStatus.CLOSED && ticket.getCloseComment() != null + && !ticket.getCloseComment().isEmpty()) + lore.add(gi("lore-comment", "{value}", ticket.getCloseComment())); + + if (ticket.isPlayerDeleted()) + lore.add(gi("lore-player-deleted")); + lore.add("§8§m "); - lore.add("§e§l» KLICKEN für Details"); + lore.add(gi("list-click")); meta.setLore(lore); item.setItemMeta(meta); return item; @@ -773,41 +949,51 @@ public class TicketGUI implements Listener { case FORWARDED -> Material.ORANGE_DYE; case CLOSED -> Material.GRAY_DYE; }; + ItemStack item = new ItemStack(mat); - ItemMeta meta = item.getItemMeta(); + ItemMeta meta = item.getItemMeta(); if (meta == null) return item; meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); + List lore = new ArrayList<>(); lore.add("§8§m "); - lore.add("§7Ersteller: §e" + ticket.getCreatorName()); - lore.add("§7Anliegen: §f" + ticket.getMessage()); - lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt())); - if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) { - lore.add("§7Server: §b" + ticket.getServerName()); - } - lore.add("§7Welt: §e" + ticket.getWorldName()); - lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ())); + lore.add(gi("lore-creator", "{value}", ticket.getCreatorName())); + lore.add(gi("lore-message", "{value}", ticket.getMessage())); + lore.add(gi("lore-created", "{value}", ticket.getCreatedAt() != null ? DATE_FORMAT.format(ticket.getCreatedAt()) : "?")); + + if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) + lore.add(gi("lore-server", "{value}", ticket.getServerName())); + + lore.add(gi("lore-world", "{value}", ticket.getWorldName())); + lore.add(gi("lore-position", "{value}", + String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()))); + if (plugin.getConfig().getBoolean("categories-enabled", true)) { - ConfigCategory _cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); - lore.add("§7Kategorie: " + _cat.getColored()); + ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); + lore.add(gi("lore-category", "{value}", cat.getColored())); } if (plugin.getConfig().getBoolean("priorities-enabled", true)) - lore.add("§7Priorität: " + ticket.getPriority().getColored()); + lore.add(gi("lore-priority", "{value}", ticket.getPriority().getColored())); + if (ticket.getClaimerName() != null) { lore.add("§8§m "); - lore.add("§7Angenommen von: §a" + ticket.getClaimerName()); - if (ticket.getClaimedAt() != null) lore.add("§7Angenommen am: §a" + DATE_FORMAT.format(ticket.getClaimedAt())); + lore.add(gi("lore-claimed-by", "{value}", ticket.getClaimerName())); + if (ticket.getClaimedAt() != null) + lore.add(gi("lore-claimed-at", "{value}", DATE_FORMAT.format(ticket.getClaimedAt()))); } + if (ticket.getStatus() == TicketStatus.CLOSED) { - if (ticket.getClosedAt() != null) lore.add("§7Geschlossen am: §c" + DATE_FORMAT.format(ticket.getClosedAt())); + if (ticket.getClosedAt() != null) + lore.add(gi("lore-closed-at", "{value}", DATE_FORMAT.format(ticket.getClosedAt()))); if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) - lore.add("§7Kommentar: §f" + ticket.getCloseComment()); + lore.add(gi("lore-comment", "{value}", ticket.getCloseComment())); if (plugin.getConfig().getBoolean("rating-enabled", true)) { - String rating = ticket.getPlayerRating(); - String ratingStr = rating == null ? "§7Keine Bewertung" : - "THUMBS_UP".equals(rating) ? "§a👍 Positiv" : "§c👎 Negativ"; - lore.add("§7Bewertung: " + ratingStr); + String rating = ticket.getPlayerRating(); + String ratingStr = rating == null ? gi("lore-rating-none") + : "THUMBS_UP".equals(rating) ? gi("lore-rating-good") + : gi("lore-rating-bad"); + lore.add(gi("lore-rating-label", "{value}", ratingStr)); } } lore.add("§8§m "); @@ -823,40 +1009,55 @@ public class TicketGUI implements Listener { case FORWARDED -> Material.ORANGE_DYE; case CLOSED -> Material.GRAY_DYE; }; + ItemStack item = new ItemStack(mat); - ItemMeta meta = item.getItemMeta(); + ItemMeta meta = item.getItemMeta(); if (meta == null) return item; meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); + List lore = new ArrayList<>(); lore.add("§8§m "); - lore.add("§7Anliegen: §f" + ticket.getMessage()); - lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt())); - if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) { - lore.add("§7Server: §b" + ticket.getServerName()); - } - lore.add("§7Welt: §e" + ticket.getWorldName()); - lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ())); + lore.add(gi("lore-message", "{value}", ticket.getMessage())); + lore.add(gi("lore-created", "{value}", ticket.getCreatedAt() != null ? DATE_FORMAT.format(ticket.getCreatedAt()) : "?")); + + if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) + lore.add(gi("lore-server", "{value}", ticket.getServerName())); + + lore.add(gi("lore-world", "{value}", ticket.getWorldName())); + lore.add(gi("lore-position", "{value}", + String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()))); + if (plugin.getConfig().getBoolean("categories-enabled", true)) { - ConfigCategory _cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); - lore.add("§7Kategorie: " + _cat.getColored()); + ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); + lore.add(gi("lore-category", "{value}", cat.getColored())); } + if (ticket.getStatus() == TicketStatus.CLOSED) { if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { lore.add("§8§m "); - lore.add("§7Kommentar des Supports:"); + lore.add(gi("player-comment-label")); lore.add("§f" + ticket.getCloseComment()); } if (plugin.getConfig().getBoolean("rating-enabled", true)) { String rating = ticket.getPlayerRating(); - if (rating == null) lore.add("§e» /ticket rate " + ticket.getId() + " good/bad"); - else lore.add("§7Bewertet: " + ("THUMBS_UP".equals(rating) ? "§a👍" : "§c👎")); + if (rating == null) + lore.add(gi("player-rate-hint", "{id}", String.valueOf(ticket.getId()))); + else + lore.add("THUMBS_UP".equals(rating) ? gi("player-rated-good") : gi("player-rated-bad")); } } + lore.add("§8§m "); switch (ticket.getStatus()) { - case OPEN, CLOSED -> { lore.add("§c§l» KLICKEN zum Löschen"); lore.add("§7Entferne dieses Ticket aus deiner Übersicht."); } - default -> { lore.add("§e» Ticket wird bearbeitet..."); lore.add("§7Kann nicht mehr gelöscht werden."); } + case OPEN, CLOSED -> { + lore.add(gi("player-delete-hint")); + lore.add(gi("player-delete-desc")); + } + default -> { + lore.add(gi("player-in-progress")); + lore.add(gi("player-no-delete")); + } } meta.setLore(lore); item.setItemMeta(meta); @@ -865,7 +1066,7 @@ public class TicketGUI implements Listener { private ItemStack buildActionItem(Material material, String displayName, List lore) { ItemStack item = new ItemStack(material); - ItemMeta meta = item.getItemMeta(); + ItemMeta meta = item.getItemMeta(); if (meta == null) return item; meta.setDisplayName(displayName); meta.setLore(lore); @@ -875,13 +1076,14 @@ public class TicketGUI implements Listener { private ItemStack makeGlass() { ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); - ItemMeta meta = glass.getItemMeta(); + ItemMeta meta = glass.getItemMeta(); if (meta != null) { meta.setDisplayName(" "); glass.setItemMeta(meta); } return glass; } private void fillEmpty(Inventory inv) { ItemStack glass = makeGlass(); - for (int i = 0; i < inv.getSize(); i++) { if (inv.getItem(i) == null) inv.setItem(i, glass); } + for (int i = 0; i < inv.getSize(); i++) + if (inv.getItem(i) == null) inv.setItem(i, glass); } } \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java b/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java index 8c8fb3e..adbb6b2 100644 --- a/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java +++ b/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java @@ -5,7 +5,6 @@ import java.util.List; import de.ticketsystem.TicketPlugin; import de.ticketsystem.database.DatabaseManager; import de.ticketsystem.model.Ticket; -import de.ticketsystem.model.TicketStatus; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; @@ -33,18 +32,15 @@ public class PlayerJoinListener implements Listener { int count = plugin.getDatabaseManager().countOpenTickets(); if (count > 0) { Bukkit.getScheduler().runTaskLater(plugin, () -> { - String msg = plugin.formatMessage("messages.join-open-tickets") - .replace("{count}", String.valueOf(count)); - player.sendMessage(msg); - player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht.")); + player.sendMessage(plugin.lang().format("join.open-tickets", + "{count}", String.valueOf(count))); + player.sendMessage(plugin.lang().get("join.open-tickets-hint")); }, 40L); } }); } // ── BungeeCord: ausstehenden Teleport-Auftrag prüfen ───────────── - // Wenn ein Admin via GUI auf einen anderen Server geschickt wurde, - // liegt hier die Zielposition. Wir teleportieren ihn nach dem Spawn. if (plugin.isBungeeCordEnabled() && plugin.getConfig().getBoolean("bungee-teleport-enabled", true)) { Bukkit.getScheduler().runTaskLater(plugin, () -> { @@ -58,23 +54,20 @@ public class PlayerJoinListener implements Listener { if (!player.isOnline()) return; World world = Bukkit.getWorld(pt.world()); if (world == null) { - player.sendMessage(plugin.color( - "&cTeleport-Zielwelt &e" + pt.world() + " &cnicht gefunden!")); + player.sendMessage(plugin.lang().format("join.teleport-world-missing", + "{world}", pt.world())); return; } Location loc = new Location(world, pt.x(), pt.y(), pt.z(), pt.yaw(), pt.pitch()); player.teleport(loc); - player.sendMessage(plugin.color( - "&7Du wurdest zur Ticket-Position teleportiert. &8(" - + String.format("%.0f, %.0f, %.0f", pt.x(), pt.y(), pt.z()) + ")")); + String coords = String.format("%.0f, %.0f, %.0f", pt.x(), pt.y(), pt.z()); + player.sendMessage(plugin.lang().format("join.teleport-success", "{coords}", coords)); }); }); - // 40 Ticks (2 Sek) Verzögerung damit der Spieler vollständig gespawnt ist }, 40L); } // ── Ausstehende Kommentar-/Schließ-Benachrichtigungen anzeigen ──── - // (Nachrichten die ankamen während der Spieler offline war) Bukkit.getScheduler().runTaskLater(plugin, () -> { if (!player.isOnline()) return; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { @@ -82,36 +75,30 @@ public class PlayerJoinListener implements Listener { if (pending.isEmpty()) return; Bukkit.getScheduler().runTask(plugin, () -> { if (!player.isOnline()) return; - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6Ticket-Benachrichtigungen &7(während du offline warst):")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().get("join.pending-header")); for (String msg : pending) { - player.sendMessage(plugin.color(msg)); + player.sendMessage(plugin.lang().color(msg)); } - player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.lang().get("general.separator")); }); plugin.getDatabaseManager().clearPendingNotifications(player.getUniqueId()); }); }, 60L); - // ── [NEU] Spieler: Ticket-claimed-Benachrichtigung für Offline-Zeit ── - // Läuft mit 60 Ticks Verzögerung (3 Sek) damit der Spieler zuerst normal spawnt + // ── Spieler: Ticket-claimed-Benachrichtigung für Offline-Zeit ────── Bukkit.getScheduler().runTaskLater(plugin, () -> { if (!player.isOnline()) return; plugin.getTicketManager().notifyClaimedWhileOffline(player); }, 60L); - // ── Spieler: über geschlossene Tickets informieren (nur wenn noch nicht geschehen) ── - // Bug-Fix: Nutzt close_notified aus der DB statt in-memory Set. - // Verhindert Duplikate bei Server-Wechseln in BungeeCord-Netzwerken. + // ── Spieler: über geschlossene Tickets informieren ──────────────── Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + // Nur Tickets dieses Spielers laden (nicht ALLE closed Tickets) List closed = plugin.getDatabaseManager() - .getTicketsByStatus(TicketStatus.CLOSED); + .getUnnotifiedClosedTicketsByPlayer(player.getUniqueId()); for (Ticket t : closed) { - if (!t.getCreatorUUID().equals(player.getUniqueId())) continue; - // DB-Feld prüfen – funktioniert serverübergreifend - if (t.isCloseNotified()) continue; - Bukkit.getScheduler().runTask(plugin, () -> plugin.getTicketManager().notifyCreatorClosed(t)); } @@ -124,13 +111,13 @@ public class PlayerJoinListener implements Listener { new de.ticketsystem.UpdateChecker(plugin, resourceId).getVersion(version -> { String current = plugin.getDescription().getVersion(); if (!current.equals(version)) { - String bar = ChatColor.GOLD + "===================================================="; - player.sendMessage(bar); - player.sendMessage(ChatColor.GOLD + "[TicketSystem] " - + ChatColor.YELLOW + "NEUES UPDATE VERFÜGBAR: v" + version); - player.sendMessage(ChatColor.GOLD + "[TicketSystem] " - + ChatColor.YELLOW + "Download: https://www.spigotmc.org/resources/132757"); - player.sendMessage(bar); + String bar = plugin.lang().get("update.available-bar"); + String line1 = plugin.lang().format("update.available-line1", "{version}", version); + String line2 = plugin.lang().get("update.available-line2"); + player.sendMessage(ChatColor.GOLD + bar); + player.sendMessage(line1); + player.sendMessage(line2); + player.sendMessage(ChatColor.GOLD + bar); } }); }, 20L); diff --git a/src/main/java/de/ticketsystem/manager/FaqManager.java b/src/main/java/de/ticketsystem/manager/FaqManager.java index 505cb76..ff9ea03 100644 --- a/src/main/java/de/ticketsystem/manager/FaqManager.java +++ b/src/main/java/de/ticketsystem/manager/FaqManager.java @@ -93,9 +93,9 @@ public class FaqManager { writeEntry(4, "Wie kann ich meinen Support bewerten?", "Nach dem Schließen eines Tickets kannst du mit /ticket rate good/bad eine Bewertung abgeben."); nextId = 5; - // Sync entries list with what we just wrote + // Sync entries list with what we just wrote – Text muss identisch sein! entries.add(new FaqEntry(1, "Wie erstelle ich ein Ticket?", - "Nutze den Befehl /ticket create [Kategorie] [Beschreibung] um ein neues Ticket zu erstellen.")); + "Nutze den Befehl /ticket create [Kategorie] [Prio][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?", diff --git a/src/main/java/de/ticketsystem/manager/LanguageManager.java b/src/main/java/de/ticketsystem/manager/LanguageManager.java new file mode 100644 index 0000000..a61012b --- /dev/null +++ b/src/main/java/de/ticketsystem/manager/LanguageManager.java @@ -0,0 +1,346 @@ +package de.ticketsystem.manager; + +import de.ticketsystem.TicketPlugin; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.regex.Matcher; +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+. + * + * ┌─────────────────────────────────────────────────────────┐ + * │ Einziger Konfigurations-Schlüssel: language │ + * │ │ + * │ 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: + * {cmd_create} {cmd_list} {cmd_comment} {cmd_rate} + * {cmd_claim} {cmd_close} {cmd_forward} {cmd_reload} + * {cmd_stats} {cmd_archive} {cmd_migrate} {cmd_export} + * {cmd_import} {cmd_blacklist} {cmd_setpriority} {cmd_faq} {cmd_top} + */ +public class LanguageManager { + + // ── Konstanten ────────────────────────────────────────────────────────── + + private static final Set SUPPORTED = Set.of("de", "en", "both"); + private static final String FALLBACK = "de"; + + /** Nachrichten-Pfade die KEINEN Plugin-Prefix erhalten. */ + private static final String[] NO_PREFIX_PATHS = { + "general.separator", "help.", "stats.", "top.", "faq.list-", + "blacklist.list-", "gui.", "join.pending-header", "update." + }; + + // ── Befehlsnamen-Tabellen (statisch, ändern sich nie) ─────────────────── + + private static final LinkedHashMap DE = new LinkedHashMap<>(); + private static final LinkedHashMap EN = new LinkedHashMap<>(); + + static { + DE.put("create", "erstellen"); + DE.put("list", "liste"); + DE.put("comment", "kommentar"); + DE.put("rate", "bewerten"); + DE.put("claim", "übernehmen"); + DE.put("close", "schließen"); + DE.put("forward", "weiterleiten"); + DE.put("reload", "neuladen"); + DE.put("stats", "statistik"); + DE.put("archive", "archivieren"); + DE.put("migrate", "migrieren"); + DE.put("export", "exportieren"); + DE.put("import", "importieren"); + DE.put("blacklist", "sperrliste"); + DE.put("setpriority", "priorität"); + DE.put("faq", "faq"); + DE.put("top", "top"); + + EN.put("create", "create"); + EN.put("list", "list"); + EN.put("comment", "comment"); + EN.put("rate", "rate"); + EN.put("claim", "claim"); + EN.put("close", "close"); + EN.put("forward", "forward"); + EN.put("reload", "reload"); + EN.put("stats", "stats"); + EN.put("archive", "archive"); + EN.put("migrate", "migrate"); + EN.put("export", "export"); + EN.put("import", "import"); + EN.put("blacklist", "blacklist"); + EN.put("setpriority", "setpriority"); + EN.put("faq", "faq"); + EN.put("top", "top"); + } + + // ── Felder ─────────────────────────────────────────────────────────────── + + private final TicketPlugin plugin; + private YamlConfiguration lang; + private String prefix; + + /** + * Aktiver Sprachmodus – wird bei jedem load() DIREKT aus der Config gelesen. + * Kein Cache, kein Zwischenwert. Immer frisch nach reloadConfig(). + */ + private String activeLang; + + /** + * Dateiname-Kürzel: "de" oder "en". + * "both" verwendet die DE-Datei für die Texte. + */ + private String fileLang; + + /** + * Ersetzungsmap {cmd_X} → Anzeigename. + * Wird bei jedem load() komplett neu gebaut. + */ + private Map cmdNames = new LinkedHashMap<>(); + + // ── Konstruktor ────────────────────────────────────────────────────────── + + public LanguageManager(TicketPlugin plugin) { + this.plugin = 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 + String raw = plugin.getConfig().getString("language", FALLBACK) + .toLowerCase().trim(); + + if (!SUPPORTED.contains(raw)) { + plugin.getLogger().warning("[LanguageManager] Unbekannter Wert language='" + + raw + "' in config.yml – verwende '" + FALLBACK + "'."); + raw = FALLBACK; + } + + activeLang = raw; + fileLang = "en".equals(activeLang) ? "en" : "de"; + + // 2. Sprachdatei einlesen (ggf. aus JAR extrahieren) + String fileName = "lang_" + fileLang + ".yml"; + File file = new File(plugin.getDataFolder(), fileName); + + if (!file.exists()) { + try { + plugin.saveResource(fileName, false); + } catch (IllegalArgumentException ex) { + plugin.getLogger().severe("[LanguageManager] '" + fileName + + "' nicht im Plugin-JAR – Plugin neu installieren!"); + lang = new YamlConfiguration(); + prefix = color("&8[&6Ticket&8] &r"); + cmdNames = buildCmdNames(); + return; + } + } + + lang = YamlConfiguration.loadConfiguration(file); + + // 3. Fehlende Schlüssel aus JAR-Defaults ergänzen & ggf. speichern + InputStream defaultStream = plugin.getResource(fileName); + if (defaultStream != null) { + YamlConfiguration defaults = YamlConfiguration.loadConfiguration( + new InputStreamReader(defaultStream, StandardCharsets.UTF_8)); + lang.setDefaults(defaults); + + boolean changed = false; + for (String key : defaults.getKeys(true)) { + if (!lang.isSet(key)) { + lang.set(key, defaults.get(key)); + changed = true; + } + } + if (changed) { + try { lang.save(file); } + catch (IOException ex) { + plugin.getLogger().log(Level.WARNING, + "[LanguageManager] Konnte " + fileName + " nicht speichern.", ex); + } + } + } + + // 4. Prefix & Befehlsnamen aufbauen + prefix = color(lang.getString("prefix", "&8[&6Ticket&8] &r")); + cmdNames = buildCmdNames(); + + plugin.getLogger().info("[LanguageManager] Geladen: " + fileName + + " | language=" + activeLang + + " | Befehle: " + describeMode()); + } + + // ── 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 + }; + map.put("{cmd_" + key + "}", display); + } + return map; + } + + private String describeMode() { + return switch (activeLang) { + case "en" -> "Englisch (/ticket create ...)"; + case "both" -> "Beides (/ticket create (erstellen) ...)"; + default -> "Deutsch (/ticket erstellen ...)"; + }; + } + + // ── Befehlssprache-Abfragen (für TicketCommand) ────────────────────────── + + /** 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 ─────────────────────────────────────── + + private String applyCmdNames(String text) { + if (text == null) return ""; + for (Map.Entry e : cmdNames.entrySet()) + text = text.replace(e.getKey(), e.getValue()); + return text; + } + + // ── Public API ─────────────────────────────────────────────────────────── + + /** Roher Wert aus der Sprachdatei – ohne Farbe oder Platzhalter-Ersetzung. */ + public String getRaw(String key) { + String value = lang.getString(key); + if (value == null) { + plugin.getLogger().warning("[LanguageManager] Fehlender Schlüssel: " + key); + return key; + } + return value; + } + + /** Übersetzter, eingefärbter Text. {cmd_X}-Platzhalter werden ersetzt. */ + public String get(String key) { + return color(applyCmdNames(getRaw(key))); + } + + /** Übersetzter Text mit zusätzlichen {placeholder} → Wert Ersetzungen. */ + public String format(String key, String... replacements) { + String text = applyCmdNames(getRaw(key)); + if (replacements.length % 2 != 0) + plugin.getLogger().warning("[LanguageManager] format() benötigt eine gerade Anzahl an Argumenten für: " + key); + for (int i = 0; i + 1 < replacements.length; i += 2) + text = text.replace(replacements[i], replacements[i + 1]); + return color(text); + } + + /** Gibt prefix + format(...) zurück. */ + public String formatWithPrefix(String key, String... replacements) { + return prefix + format(key, replacements); + } + + /** Sendet eine Nachricht (mit Prefix wenn nötig) an einen CommandSender. */ + public void send(CommandSender sender, String key, String... replacements) { + sender.sendMessage(needsPrefix(key) + ? prefix + format(key, replacements) + : format(key, replacements)); + } + + /** Sendet die Trennlinie. */ + public void sendSeparator(CommandSender sender) { + sender.sendMessage(get("general.separator")); + } + + /** + * Gibt den vollständigen Befehlsstring zurück. + * Beispiel: getCmdName("create") → "/ticket create" bei language=en + */ + public String getCmdName(String internalKey) { + String full = cmdNames.get("{cmd_" + internalKey + "}"); + return full != null ? full : "/ticket " + internalKey; + } + + /** + * Übersetzt &-Farbcodes und Hex-Farbcodes (&#RRGGBB oder <#RRGGBB>) in §-Codes. + */ + public String color(String text) { + if (text == null || text.isEmpty()) return ""; + + // Regex für 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(); + + 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; } + + /** Relädt die Sprachdatei. Muss NACH plugin.reloadConfig() aufgerufen werden. */ + public void reload() { load(); } + + // ── Intern ─────────────────────────────────────────────────────────────── + + private boolean needsPrefix(String key) { + for (String p : NO_PREFIX_PATHS) if (key.startsWith(p)) return false; + return true; + } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/manager/TicketManager.java b/src/main/java/de/ticketsystem/manager/TicketManager.java index ef79e68..946c0fc 100644 --- a/src/main/java/de/ticketsystem/manager/TicketManager.java +++ b/src/main/java/de/ticketsystem/manager/TicketManager.java @@ -15,76 +15,68 @@ public class TicketManager { private final TicketPlugin plugin; - /** Cooldown Map: UUID → Zeitstempel letztes Ticket */ + /** UUID → Zeitstempel der letzten Ticket-Erstellung */ private final Map cooldowns = new HashMap<>(); public TicketManager(TicketPlugin plugin) { this.plugin = plugin; } - // ─────────────────────────── Cooldown ────────────────────────────────── + // ── Cooldown ────────────────────────────────────────────────────────── public boolean hasCooldown(UUID uuid) { if (!cooldowns.containsKey(uuid)) return false; - long cooldownSeconds = plugin.getConfig().getLong("ticket-cooldown", 60) * 1000L; - return (System.currentTimeMillis() - cooldowns.get(uuid)) < cooldownSeconds; + long cdMillis = plugin.getConfig().getLong("ticket-cooldown", 60) * 1000L; + return (System.currentTimeMillis() - cooldowns.get(uuid)) < cdMillis; } public long getRemainingCooldown(UUID uuid) { - long cooldownMillis = plugin.getConfig().getLong("ticket-cooldown", 60) * 1000L; - long elapsed = System.currentTimeMillis() - cooldowns.getOrDefault(uuid, 0L); - return Math.max(0, (cooldownMillis - elapsed) / 1000); + long cdMillis = plugin.getConfig().getLong("ticket-cooldown", 60) * 1000L; + long elapsed = System.currentTimeMillis() - cooldowns.getOrDefault(uuid, 0L); + return Math.max(0, (cdMillis - elapsed) / 1000); } - public void setCooldown(UUID uuid) { cooldowns.put(uuid, System.currentTimeMillis()); } + public void setCooldown(UUID uuid) { + cooldowns.put(uuid, System.currentTimeMillis()); + } - // ─────────────────────────── Benachrichtigungen ──────────────────────── + // ── Team-Benachrichtigungen ─────────────────────────────────────────── /** - * Benachrichtigt alle Supporter/Admins über ein neues Ticket – auch auf anderen Servern. - * - * Lokal online Spieler werden direkt angesprochen. - * Über BungeeCord werden alle anderen Server im Netzwerk ebenfalls benachrichtigt. - * Optional sendet der Discord-Webhook eine Nachricht. + * Benachrichtigt alle Supporter/Admins über ein neues Ticket. + * Bei BungeeCord wird die Nachricht an alle Server weitergeleitet. */ public void notifyTeam(Ticket ticket) { String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt"; String message = ticket.getMessage() != null ? ticket.getMessage() : ""; - // Kategorie & Priorität optional anzeigen String categoryInfo = ""; String priorityInfo = ""; + if (plugin.getConfig().getBoolean("categories-enabled", true)) { ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); - categoryInfo = " §7[§r" + cat.getColored() + "§7]"; + categoryInfo = plugin.lang().format("notify.team-category", "{category}", cat.getColored()); } if (plugin.getConfig().getBoolean("priorities-enabled", true)) { - priorityInfo = " §7Priorität: §r" + ticket.getPriority().getColored(); + priorityInfo = plugin.lang().format("notify.team-priority", "{priority}", ticket.getPriority().getColored()); } - // BungeeCord: Server-Herkunft anzeigen wenn BungeeCord aktiviert String serverInfo = ""; if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) { - serverInfo = " §7Server: §b" + ticket.getServerName(); + serverInfo = plugin.lang().format("notify.team-server", "{server}", ticket.getServerName()); } - String msg = plugin.formatMessage("messages.new-ticket-notify") - .replace("{player}", creatorName) - .replace("{message}", message) - .replace("{id}", String.valueOf(ticket.getId())) + String msg = plugin.lang().format("ticket.new-notify", + "{player}", creatorName, + "{message}", message, + "{id}", String.valueOf(ticket.getId())) + categoryInfo + priorityInfo + serverInfo; - String guiHint = plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen."); + String guiHint = plugin.lang().get("notify.gui-hint"); if (plugin.isBungeeCordEnabled()) { - // ─ BungeeCord-Modus: Team-Broadcast über alle Server ───────────────── - // BungeeMessenger sendet lokal direkt, dann per Forward an alle anderen Server. - // Beide Nachrichten werden zu einer zusammengefasst um ein einzelnes - // Forward-Paket zu erzeugen statt zwei (reduziert Netzwerklast und - // verhindert mögliche Reihenfolge-Probleme). plugin.getBungeeMessenger().broadcastTeamNotification(msg + "\n" + guiHint); } else { - // ─ Standalone-Modus: Nur lokal ─────────────────────────────── for (Player p : Bukkit.getOnlinePlayers()) { if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) { p.sendMessage(msg); @@ -96,239 +88,227 @@ public class TicketManager { plugin.getDiscordWebhook().sendNewTicket(ticket); } + // ── Ersteller-Benachrichtigungen ────────────────────────────────────── + /** - * Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde. - * Setzt claimer_notified = true und persistiert es. - * - * BungeeCord: Zustellung auch wenn der Spieler auf einem anderen Server ist. + * Benachrichtigt den Ersteller, dass sein Ticket angenommen wurde. */ public void notifyCreatorClaimed(Ticket ticket) { String claimerName = resolveClaimerName(ticket); - - String msg = plugin.formatMessage("messages.ticket-claimed-notify") - .replace("{id}", String.valueOf(ticket.getId())) - .replace("{claimer}", claimerName); - + String msg = plugin.lang().format("ticket.claimed-notify", + "{id}", String.valueOf(ticket.getId()), + "{claimer}", claimerName); deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg); - - // Persistiert setzen, damit Join-Listener weiß, dass Spieler bereits informiert ist plugin.getDatabaseManager().markClaimerNotified(ticket.getId()); } /** - * Wird beim Server-Join aufgerufen – informiert den Spieler über Tickets, - * die geclaimt oder weitergeleitet wurden während er offline war. + * Prüft beim Server-Join ob Tickets während der Offline-Zeit + * geclaimt oder weitergeleitet wurden, und informiert den Spieler. */ public void notifyClaimedWhileOffline(Player player) { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - var tickets = plugin.getDatabaseManager().getTicketsByStatus( - TicketStatus.CLAIMED, TicketStatus.FORWARDED); + var tickets = plugin.getDatabaseManager() + .getTicketsByStatus(TicketStatus.CLAIMED, TicketStatus.FORWARDED); for (Ticket t : tickets) { if (!t.getCreatorUUID().equals(player.getUniqueId())) continue; if (t.isClaimerNotified()) continue; - String claimerName = t.getClaimerName() != null ? t.getClaimerName() : "Support"; - final String name = claimerName; + final String name = t.getClaimerName() != null ? t.getClaimerName() : "Support"; Bukkit.getScheduler().runTask(plugin, () -> { if (!player.isOnline()) return; if (t.getStatus() == TicketStatus.CLAIMED) { - String msg = plugin.formatMessage("messages.ticket-claimed-notify") - .replace("{id}", String.valueOf(t.getId())) - .replace("{claimer}", name); - player.sendMessage(msg); + player.sendMessage(plugin.lang().format("ticket.claimed-notify", + "{id}", String.valueOf(t.getId()), "{claimer}", name)); } else { String forwardedTo = t.getForwardedToName() != null ? t.getForwardedToName() : "einen Supporter"; - String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify") - .replace("{id}", String.valueOf(t.getId())) - .replace("{supporter}", forwardedTo); - player.sendMessage(msg); + player.sendMessage(plugin.lang().format("ticket.forwarded-creator", + "{id}", String.valueOf(t.getId()), "{supporter}", forwardedTo)); } + // Flag NACH der Nachricht setzen – sicher im Hauptthread + Bukkit.getScheduler().runTaskAsynchronously(plugin, + () -> plugin.getDatabaseManager().markClaimerNotified(t.getId())); }); - - plugin.getDatabaseManager().markClaimerNotified(t.getId()); } }); } /** - * Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde. - * BungeeCord: Cross-Server-Zustellung. + * Benachrichtigt den Ersteller, dass sein Ticket weitergeleitet wurde. */ public void notifyCreatorForwarded(Ticket ticket) { String forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter"; - String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify") - .replace("{id}", String.valueOf(ticket.getId())) - .replace("{supporter}", forwardedTo); - + String msg = plugin.lang().format("ticket.forwarded-creator", + "{id}", String.valueOf(ticket.getId()), "{supporter}", forwardedTo); deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg); - - // Auch bei Weiterleitung notified setzen plugin.getDatabaseManager().markClaimerNotified(ticket.getId()); } /** - * Sendet dem weitergeleiteten Supporter eine Benachrichtigung. - * BungeeCord: Zustellung auch wenn der Supporter auf einem anderen Server ist. + * Benachrichtigt den Supporter, an den ein Ticket weitergeleitet wurde. */ public void notifyForwardedTo(Ticket ticket, String fromName) { if (ticket.getForwardedToUUID() == null) return; - String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt"; - String msg = plugin.formatMessage("messages.ticket-forwarded-notify") - .replace("{player}", creatorName) - .replace("{id}", String.valueOf(ticket.getId())); - + String msg = plugin.lang().format("ticket.forwarded-notify", + "{player}", creatorName, "{id}", String.valueOf(ticket.getId())); deliverToPlayer(ticket.getForwardedToUUID(), ticket.getForwardedToName(), msg); - plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName); } - /** - * Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde. - * BungeeCord: Cross-Server-Zustellung + Fallback in Pending-DB. - */ - public void notifyCreatorClosed(Ticket ticket) { notifyCreatorClosed(ticket, null); } + /** Benachrichtigt den Ersteller über die Schließung seines Tickets. */ + public void notifyCreatorClosed(Ticket ticket) { + notifyCreatorClosed(ticket, null); + } public void notifyCreatorClosed(Ticket ticket, String closerName) { - // Bug-Fix: close_notified wird in der DB gespeichert – kein In-Memory-Set mehr. - // Dadurch funktioniert der Check auch nach einem Server-Wechsel korrekt. Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> plugin.getDatabaseManager().markCloseNotified(ticket.getId())); String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) ? ticket.getCloseComment() : ""; - // Hauptnachricht - String msg = plugin.formatMessage("messages.ticket-closed-notify") - .replace("{id}", String.valueOf(ticket.getId())) - .replace("{comment}", comment); + String msg = plugin.lang().format("ticket.closed-notify", "{id}", String.valueOf(ticket.getId())); - // Bewertungsaufforderung + // Bewertungsaufforderung aufbauen String ratingMsg = null; if (plugin.getConfig().getBoolean("rating-enabled", true)) { - ratingMsg = plugin.color( - "&8&m &r\n" + - "&6Wie zufrieden bist du mit dem Support?\n" + - "&a/ticket rate " + ticket.getId() + " good &7– 👍 Gut\n" + - "&c/ticket rate " + ticket.getId() + " bad &7– 👎 Schlecht\n" + - "&8&m "); + String id = String.valueOf(ticket.getId()); + ratingMsg = plugin.lang().get("rating.prompt-header") + "\n" + + plugin.lang().get("rating.prompt-title") + "\n" + + plugin.lang().format("rating.prompt-good", "{id}", id) + "\n" + + plugin.lang().format("rating.prompt-bad", "{id}", id) + "\n" + + plugin.lang().get("rating.prompt-footer"); } - // Prüfen ob Ersteller lokal online ist Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); if (creator != null && creator.isOnline()) { - // ─ Lokal online: direkt zustellen ──────────────────────────── creator.sendMessage(msg); if (!comment.isEmpty()) - creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment)); - if (ratingMsg != null) creator.sendMessage(ratingMsg); + creator.sendMessage(plugin.lang().format("ticket.close-comment-label", "{comment}", comment)); + if (ratingMsg != null) + creator.sendMessage(ratingMsg); } else if (plugin.isBungeeCordEnabled()) { - // ─ BungeeCord: via Plugin-Messaging auf anderen Servern zustellen ─ - // KEIN savePendingClosedNotification hier! Das würde bei Server-Wechsel - // als "Offline-Nachricht" doppelt angezeigt werden. - // BungeeCord's "Message"-Kanal erreicht den Spieler netzwerkweit sofern er online ist. - // Ist er wirklich offline, sieht er beim nächsten Login via PlayerJoinListener - // eine frische Benachrichtigung (close_notified=true verhindert Duplikate). - plugin.getBungeeMessenger().sendMessageToPlayer( - ticket.getCreatorUUID(), ticket.getCreatorName(), msg); + plugin.getBungeeMessenger().sendMessageToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg); if (!comment.isEmpty()) plugin.getBungeeMessenger().sendMessageToPlayer( ticket.getCreatorUUID(), ticket.getCreatorName(), - plugin.color("&7Kommentar des Supports: &f" + comment)); + plugin.lang().format("ticket.close-comment-label", "{comment}", comment)); if (ratingMsg != null) plugin.getBungeeMessenger().sendMessageToPlayer( ticket.getCreatorUUID(), ticket.getCreatorName(), ratingMsg); } else { - // ─ Standalone, Spieler offline: in Pending-DB speichern ────── savePendingClosedNotification(ticket, comment); } - String closer = closerName != null ? closerName : "Unbekannt"; - plugin.getDiscordWebhook().sendTicketClosed(ticket, closer); + plugin.getDiscordWebhook().sendTicketClosed(ticket, + closerName != null ? closerName : "Unbekannt"); } /** - * Bug-Fix: Nutzt jetzt close_notified aus der DB statt ein In-Memory-Set. - * Funktioniert damit auch nach Server-Wechseln in BungeeCord-Netzwerken korrekt. - * - * @deprecated Bitte stattdessen ticket.isCloseNotified() direkt prüfen, - * da das Ticket-Objekt aus der DB bereits den korrekten Wert hat. + * @deprecated Bitte ticket.isCloseNotified() direkt verwenden. */ + @Deprecated public boolean wasClosedNotificationSent(int ticketId) { - // Direkt in der DB nachschlagen – kein In-Memory-Set, kein Server-gebundener State Ticket t = plugin.getDatabaseManager().getTicketById(ticketId); return t != null && t.isCloseNotified(); } - // ─────────────────────────── BungeeCord Hilfsmethoden ────────────────── + // ── Ticket-Limit ────────────────────────────────────────────────────── - // ── BUG FIX #2 ────────────────────────────────────────────────────────── - // Vorher: addPendingNotification() wurde IMMER asynchron ausgeführt – - // auch wenn der Spieler lokal online war oder BungeeCord die - // Nachricht bereits zugestellt hat. Das führte dazu, dass Spieler - // beim nächsten Login immer noch eine "verpasste Nachricht" sahen, - // obwohl sie die Nachricht bereits erhalten hatten. - // - // Fix: addPendingNotification() wird nur noch aufgerufen wenn: - // 1. Der Spieler NICHT lokal online ist, UND - // 2. BungeeCord NICHT aktiviert ist (Standalone-Fallback). - // Im BungeeCord-Modus ist der BungeeCord-"Message"-Kanal für die - // Zustellung zuständig. Offline-Spieler werden über close_notified - // und den PlayerJoinListener beim nächsten Login benachrichtigt. - // ──────────────────────────────────────────────────────────────────────── + public boolean hasReachedTicketLimit(UUID uuid) { + int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2); + if (max <= 0) return false; + return plugin.getDatabaseManager().countOpenTicketsByPlayer(uuid) >= max; + } + + // ── Hilfe-Nachricht ─────────────────────────────────────────────────── /** - * Zustellung einer Nachricht an einen Spieler. + * Sendet die Hilfe-Nachricht an den Spieler. * - * Ablauf: - * 1. Spieler lokal online → direkt - * 2. BungeeCord aktiv → via Plugin-Messaging (kein Pending-Eintrag) - * 3. Offline + Standalone → Pending-DB (Zustellung beim nächsten Login) + * Die Befehlsnamen in den lang.yml-Schlüsseln (z.B. help.create) enthalten + * {cmd_X}-Platzhalter. Der LanguageManager ersetzt diese automatisch + * anhand von language in config.yml: * - * @param uuid UUID des Empfängers - * @param name Spielername (für BungeeCord-Lookup) - * @param message Bereits color-übersetzter Text + * language: de → /ticket erstellen + * language: en → /ticket create + * language: both → /ticket create (erstellen) + * + * Hier muss kein manueller Sprachcode gelesen werden. + */ + public void sendHelpMessage(Player player) { + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().get("help.header")); + player.sendMessage(plugin.lang().get("general.separator")); + player.sendMessage(plugin.lang().get("help.create")); + player.sendMessage(plugin.lang().get("help.list")); + player.sendMessage(plugin.lang().get("help.comment")); + + if (plugin.getConfig().getBoolean("rating-enabled", true)) + player.sendMessage(plugin.lang().get("help.rate")); + + if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.lang().get("help.claim")); + player.sendMessage(plugin.lang().get("help.close")); + } + if (player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.lang().get("help.forward")); + player.sendMessage(plugin.lang().get("help.blacklist")); + player.sendMessage(plugin.lang().get("help.reload")); + player.sendMessage(plugin.lang().get("help.stats")); + } + player.sendMessage(plugin.lang().get("general.separator")); + + if (player.hasPermission("ticket.admin") && plugin.isBungeeCordEnabled()) + player.sendMessage(plugin.lang().format("help.bungee-status", "{server}", plugin.getServerName())); + } + + // ── Interne Hilfsmethoden ───────────────────────────────────────────── + + /** + * Zustellung einer Nachricht an einen Spieler: + * 1. Lokal online → direkt senden + * 2. BungeeCord → via Plugin-Messaging + * 3. Offline → in Pending-DB speichern */ private void deliverToPlayer(UUID uuid, String name, String message) { Player local = Bukkit.getPlayer(uuid); if (local != null && local.isOnline()) { - // Lokal online → direkt zustellen, fertig local.sendMessage(message); return; } - if (plugin.isBungeeCordEnabled()) { - // BungeeCord-Modus: Nachricht über Plugin-Messaging weiterleiten. - // KEIN Pending-Eintrag! BungeeCord übernimmt die Zustellung. - // Ist der Spieler wirklich offline, kümmert sich der PlayerJoinListener - // beim nächsten Login um die Benachrichtigung. plugin.getBungeeMessenger().sendMessageToPlayer(uuid, name, message); return; } - - // Standalone-Modus, Spieler offline → in Pending-DB speichern Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> plugin.getDatabaseManager().addPendingNotification(uuid, message)); } /** - * Speichert eine ausstehende Schließ-Benachrichtigung in der DB. + * Speichert eine Schließ-Benachrichtigung für einen Offline-Spieler + * in der Pending-DB, damit sie beim nächsten Login zugestellt wird. */ private void savePendingClosedNotification(Ticket ticket, String comment) { - String pendingMsg = "&e[Ticket #" + ticket.getId() + "] &7Dein Ticket wurde geschlossen." - + (comment.isEmpty() ? "" : " &7Kommentar: &f" + comment) - + (plugin.getConfig().getBoolean("rating-enabled", true) - ? " &7Bewertung: &e/ticket rate " + ticket.getId() + " good/bad" : ""); + String commentPart = comment.isEmpty() + ? "" + : plugin.lang().format("ticket.pending-closed-comment", "{comment}", comment); + String ratingPart = plugin.getConfig().getBoolean("rating-enabled", true) + ? plugin.lang().format("ticket.pending-closed-rating", "{id}", String.valueOf(ticket.getId())) + : ""; + String pendingMsg = plugin.lang().format("ticket.pending-closed", + "{id}", String.valueOf(ticket.getId()), + "{comment}", commentPart, + "{rating}", ratingPart); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), pendingMsg)); } - // ─────────────────────────── Hilfsmethoden ───────────────────────────── - private String resolveClaimerName(Ticket ticket) { if (ticket.getClaimerName() != null) return ticket.getClaimerName(); if (ticket.getClaimerUUID() != null) { @@ -337,40 +317,4 @@ public class TicketManager { } return "Support"; } - - public boolean hasReachedTicketLimit(UUID uuid) { - int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2); - if (max <= 0) return false; - return plugin.getDatabaseManager().countOpenTicketsByPlayer(uuid) >= max; - } - - public void sendHelpMessage(Player player) { - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&6TicketSystem &7– Befehle")); - player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&e/ticket create [Kategorie] &7– Neues Ticket erstellen")); - player.sendMessage(plugin.color("&e/ticket list &7– Deine Tickets ansehen (GUI)")); - player.sendMessage(plugin.color("&e/ticket comment &7– Nachricht zu einem Ticket")); - - if (plugin.getConfig().getBoolean("rating-enabled", true)) - player.sendMessage(plugin.color("&e/ticket rate &7– Support bewerten")); - - if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.color("&e/ticket claim &7– Ticket annehmen")); - player.sendMessage(plugin.color("&e/ticket close [Kommentar] &7– Ticket schließen")); - } - if (player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.color("&e/ticket forward &7– Ticket weiterleiten")); - player.sendMessage(plugin.color("&e/ticket blacklist [Spieler] [Grund] &7– Blacklist verwalten")); - player.sendMessage(plugin.color("&e/ticket reload &7– Konfiguration neu laden")); - player.sendMessage(plugin.color("&e/ticket stats &7– Statistiken anzeigen")); - } - player.sendMessage(plugin.color("&8&m ")); - - // BungeeCord-Status anzeigen - if (player.hasPermission("ticket.admin") && plugin.isBungeeCordEnabled()) { - player.sendMessage(plugin.color("&8[BungeeCord] &7Server: &b" + plugin.getServerName() - + " &8| Cross-Server-Benachrichtigungen &aaktiv")); - } - } } \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index fd350be..b124d88 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -7,14 +7,30 @@ # |___/ # # TicketSystem - Ein einfaches und effizientes Ticketsystem für Minecraft-Server -# Entwickelt von M_Viper +# Entwickelt von M_Viper +# +# HINWEIS: Alle Texte und Nachrichten befinden sich in lang_de.yml / lang_en.yml! # ============================================================ # --- GRUNDLEGEND --- # Version der Konfigurationsdatei. Nicht ändern! -version: "2.0" +version: "2.2" -# Debug-Modus (true = Logs in der Konsole) +# ---------------------------------------------------- +# SPRACHE / LANGUAGE +# ---------------------------------------------------- +# Steuert sowohl die Texte als auch die Befehlsnamen. +# +# de → deutsche Texte + /ticket erstellen, /ticket schließen ... +# en → englische Texte + /ticket create, /ticket close ... +# both → deutsche Texte + /ticket create (erstellen) ... +# +# Die passende Datei (lang_de.yml / lang_en.yml) wird automatisch +# im Plugin-Ordner erstellt und kann frei bearbeitet werden. +# ---------------------------------------------------- +language: de + +# Debug-Modus (true = zusätzliche Logs in der Konsole) debug: false # ---------------------------------------------------- @@ -62,11 +78,6 @@ mysql: pool-size: 10 # HikariCP Poolgröße connection-timeout: 30000 # Timeout in ms -# ---------------------------------------------------- -# PLUGIN-PRÄFIX (Chat) -# ---------------------------------------------------- -prefix: "&8[&6Ticket&8] &r" # Präfix für Chat-Ausgaben - # ---------------------------------------------------- # LIMITS & OPTIONEN # ---------------------------------------------------- @@ -79,6 +90,11 @@ max-open-tickets-per-player: 2 # Maximale offene Tickets pro Spieler (0 = unbeg # ---------------------------------------------------- auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus) +# ---------------------------------------------------- +# PERFORMANCE +# ---------------------------------------------------- +cache-ttl-seconds: 60 # Wie lange Tickets im In-Memory-Cache gehalten werden + # ---------------------------------------------------- # OPTIONALE FEATURES # ---------------------------------------------------- @@ -185,10 +201,10 @@ discord: title: "🔒 Ticket geschlossen" color: "15158332" # Rot footer: "TicketSystem" - show-category: true # Kategorie im Embed anzeigen - show-priority: true # Priorität im Embed anzeigen - show-server: true # BungeeCord: Server-Name im Embed anzeigen - role-ping: false # Rollen-Ping beim Schließen senden + show-category: true + show-priority: true + show-server: true + role-ping: false # ── Ticket weitergeleitet ─────────────────────────────────────────────── ticket-forwarded: @@ -196,70 +212,78 @@ discord: title: "🔀 Ticket weitergeleitet" color: "15105570" # Orange footer: "TicketSystem" - show-category: true # Kategorie im Embed anzeigen - show-priority: true # Priorität im Embed anzeigen - show-server: true # BungeeCord: Server-Name im Embed anzeigen - role-ping: false # Rollen-Ping beim Weiterleiten senden + show-category: true + show-priority: true + show-server: true + role-ping: false -# ---------------------------------------------------- -# SYSTEM-NACHRICHTEN (mit &-Farbcodes) -# ---------------------------------------------------- -messages: - # --- SYSTEM --- - export-success: "&aExport erfolgreich: &e{count} &aTickets nach &e{file} &aexportiert." - export-fail: "&cExport fehlgeschlagen oder keine Tickets gefunden." - import-success: "&aImport erfolgreich: &e{count} &aTickets importiert." - import-fail: "&cImport fehlgeschlagen oder keine Tickets gefunden." - migration-success: "&aMigration abgeschlossen: &e{count} &aTickets migriert." - migration-fail: "&cKeine Tickets migriert oder Fehler aufgetreten." - archive-success: "&aArchivierung abgeschlossen: &e{count} &aTickets archiviert." - archive-fail: "&cKeine geschlossenen Tickets zum Archivieren gefunden." - file-not-found: "&cDatei nicht gefunden: &e{file}" - unknown-mode: "&cUnbekannter Modus! Benutze: tomysql oder tofile" - validation-warning: "&cEs wurden &e{count} &cungültige Tickets beim Laden gefunden." +# ============================================================ +# GUI KONFIGURATION (Layouts, Slots, Items) +# ============================================================ +# Hier kannst du das Aussehen und die Anordnung der GUIs anpassen. +# WICHTIG: gui-settings muss ganz links stehen (keine Raute davor!). +gui-settings: + + # --- FAQ SYSTEM SETTINGS --- + faq: + # Größe des Inventars (4-6 Reihen, Minimum 4) + rows: 6 + # Content-Slots für FAQ-Items. + # Wenn leer: Automatisches Schachbrett-Muster (Items und leere Slots wechselnd, + # letzte Reihe = Navigation/Footer). + # Wenn gefüllt: Nur diese Slots werden für FAQs genutzt (Liste von Zahlen). + # Beispiel: content-slots: [1, 3, 5, 7, 10, 12, 14, 16] -> Nur ungerade Slots + content-slots: [] + + # Kopfeinstellungen + head-item: + # Material des FAQ-Items (z.B. PLAYER_HEAD, BOOK, PAPER) + material: PLAYER_HEAD + # Optional: Texture-URL für den Kopf (wenn Material PLAYER_HEAD) + texture: "http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e" - # --- TICKET-AKTIONEN --- - ticket-created: "&aTicket &e#{id} &awurde erfolgreich erstellt!" - ticket-claimed: "&aDu hast Ticket &e#{id} &avon &e{player} &ageclaimt." - ticket-claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen." - ticket-closed: "&aTicket &e#{id} &awurde geschlossen." - ticket-forwarded: "&aTicket &e#{id} &awurde an &e{player} &aweitergeleitet." - ticket-forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen. &7(ID: {id})" + # Navigations-Slots (Prev, Next, Add, Page) + nav: + prev: 45 + next: 53 + add: 50 + page: 49 - # --- BENACHRICHTIGUNGEN FÜR DEN TICKET-ERSTELLER --- - ticket-closed-notify: "&aDein Ticket &e#{id} &awurde geschlossen." - ticket-forwarded-creator-notify: "&eDein Ticket &6#{id} &ewurde an &b{supporter} &eweitergeleitet." + # --- TICKET GUI SETTINGS --- + ticket: + + # Spieler GUI + player: + rows: 6 + nav: + prev: 45 + next: 53 + page: 49 - # --- KATEGORIEN --- - # {category} wird durch den Anzeigenamen der gewählten Kategorie ersetzt - ticket-created-category: "&aTicket &e#{id} &aerstellt! &7Kategorie: {category}" - category-invalid: "&cUnbekannte Kategorie: &e{input}&c. Verfügbare Kategorien: &e{categories}" + # Admin / Team GUI + admin: + nav: + prev: 45 + next: 53 + page: 48 + archive: 49 + filter: 47 - # --- KOMMENTARE --- - comment-saved: "&aDein Kommentar zu Ticket &e#{id} &awurde gespeichert." - comment-notify: "&e[Ticket #{id}] &f{author} &7kommentiert: &f{message}" - comment-no-permission: "&cDu kannst nur deine eigenen Tickets kommentieren." + # Archiv GUI + archive: + nav: + prev: 45 + next: 53 + back: 49 - # --- BEWERTUNGEN --- - rating-saved-good: "&aDanke für deine Bewertung! &a👍 Positiv" - rating-saved-bad: "&aDanke für deine Bewertung! &c👎 Negativ" - rating-already-rated: "&cDu hast dieses Ticket bereits bewertet." - rating-not-yours: "&cDu kannst nur deine eigenen Tickets bewerten." - rating-disabled: "&cBewertungen sind aktuell deaktiviert." - rating-prompt: "&6Wie zufrieden bist du mit dem Support?\n&a/ticket rate {id} good &7– 👍 Gut\n&c/ticket rate {id} bad &7– 👎 Schlecht" - - # --- BLACKLIST --- - blacklist-added: "&a{player} &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e{reason}" - blacklist-removed: "&a{player} &awurde von der Blacklist entfernt." - blacklist-already: "&cSpieler ist bereits auf der Blacklist." - blacklist-not-found: "&cSpieler war nicht auf der Blacklist." - blacklist-blocked: "&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen." - - # --- FEHLER & HINWEISE --- - no-permission: "&cDu hast keine Berechtigung!" - no-open-tickets: "&aAktuell gibt es keine offenen Tickets." - join-open-tickets: "&eEs gibt noch &6{count} &eoffene Ticket(s)!" - new-ticket-notify: "&e{player} &ahat ein neues Ticket erstellt: &7{message} &7(ID: &e{id}&7)" - already-claimed: "&cDieses Ticket wurde bereits geclaimt!" - ticket-not-found: "&cTicket nicht gefunden!" - cooldown: "&cBitte warte &e{seconds} Sekunden &cbevor du ein neues Ticket erstellst." \ No newline at end of file + # --- GUI ITEM MATERIALS (Optional) --- + # Hier kannst du das Material der Navigations-Buttons ändern. + # Wenn nicht gesetzt, werden Standard-Werte genutzt. + items: + nav-prev: ARROW + nav-next: ARROW + nav-page: PAPER + nav-archive: CHEST + nav-back: ARROW + nav-filter: HOPPER + nav-add: LIME_WOOL \ No newline at end of file diff --git a/src/main/resources/lang_de.yml b/src/main/resources/lang_de.yml new file mode 100644 index 0000000..fd52787 --- /dev/null +++ b/src/main/resources/lang_de.yml @@ -0,0 +1,460 @@ +# ============================================================ +# TicketSystem – Sprachdatei Deutsch (de) +# +# Alle Texte des Plugins können hier angepasst werden. +# Farbcodes: & (z. B. &a = Grün, &c = Rot, &e = Gelb, &7 = Grau) +# HEX-Codes: &#RRGGBB (z. B. &#FFD700 = Gold) +# Platzhalter werden in geschweiften Klammern angegeben: {id}, {player}, ... +# +# Sprache in config.yml wechseln: language: de | en | both +# +# {cmd_X} wird automatisch je nach language ersetzt, z.B.: +# language: de → /ticket erstellen +# language: en → /ticket create +# language: both → /ticket create (erstellen) +# ============================================================ + +prefix: "&#FFAA00[&fTicket&#FFAA00] &r" + +# ============================================================ +# ALLGEMEINE FEHLER & HINWEISE +# ============================================================ +general: + no-permission: "&cDu hast keine Berechtigung!" + console-only: "&cDieser Befehl kann nur von Spielern ausgeführt werden." + invalid-id: "&cUngültige ID!" + invalid-player-id: "&cUngültige Ticket-ID: &e{id}" + player-not-found: "&cSpieler nicht gefunden!" + ticket-not-found: "&cTicket nicht gefunden!" + already-claimed: "&cDieses Ticket wurde bereits angenommen!" + no-open-tickets: "&aAktuell gibt es keine offenen Tickets." + cooldown: "&cBitte warte &e{seconds} Sekunden &cbevor du ein neues Ticket erstellst." + separator: "򇨣&m " + +# ============================================================ +# SYSTEM (Export, Import, Migration, Archiv, Validierung) +# ============================================================ +system: + export-success: "&aExport erfolgreich: &e{count} &aTickets nach &e{file} &aexportiert." + export-fail: "&cExport fehlgeschlagen oder keine Tickets gefunden." + import-success: "&aImport erfolgreich: &e{count} &aTickets importiert." + import-fail: "&cImport fehlgeschlagen oder keine Tickets gefunden." + migration-success: "&aMigration abgeschlossen: &e{count} &aTickets migriert." + migration-fail: "&cKeine Tickets migriert oder Fehler aufgetreten." + archive-success: "&aArchivierung abgeschlossen: &e{count} &aTickets archiviert." + archive-fail: "&cKeine geschlossenen Tickets zum Archivieren gefunden." + file-not-found: "&cDatei nicht gefunden: &e{file}" + unknown-mode: "&cUnbekannter Modus! Benutze: tomysql oder tofile" + validation-warning: "&cEs wurden &e{count} &cungültige Tickets beim Laden gefunden." + db-create-error: "&cFehler beim Erstellen des Tickets!" + +# ============================================================ +# TICKET-AKTIONEN +# ============================================================ +ticket: + created: "&aTicket &e#{id} &awurde erfolgreich erstellt!" + created-category: "&aTicket &e#{id} &aerstellt! &7Kategorie: {category}" + claimed: "&aDu hast Ticket &e#{id} &avon &e{player} &aangenommen." + claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen." + closed: "&aTicket &e#{id} &awurde geschlossen." + closed-notify: "&aDein Ticket &e#{id} &awurde geschlossen." + forwarded: "&aTicket &e#{id} &awurde an &e{player} &aweitergeleitet." + forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen. &7(ID: {id})" + forwarded-creator: "&eDein Ticket &6#{id} &ewurde an &b{supporter} &eweitergeleitet." + new-notify: "&e{player} &ahat ein neues Ticket erstellt: &7{message} &7(ID: &e{id}&7)" + close-comment-label: "&7Kommentar des Supports: &f{comment}" + close-comment-short: "&7Kommentar: &f{comment}" + pending-closed: "&e[Ticket #{id}] &7Dein Ticket wurde geschlossen.{comment}{rating}" + pending-closed-comment: " &7Kommentar: &f{comment}" + pending-closed-rating: " &7Bewertung: &e{cmd_rate} {id} good/bad" + +# ============================================================ +# BENACHRICHTIGUNGEN (Team / GUI-Hinweis) +# ============================================================ +notify: + gui-hint: "&7» Klicke &e{cmd_list} &7um die Übersicht zu öffnen." + team-category: " §7[§r{category}§7]" + team-priority: " §7Priorität: §r{priority}" + team-server: " §7Server: §b{server}" + +# ============================================================ +# TICKET ERSTELLEN +# ============================================================ +create: + usage: "&cBenutzung: {cmd_create} [Kategorie] [Priorität] " + categories-hint: "&7Kategorien: &ebug&7, &efrage&7, &ebeschwerde&7, &esonstiges&7, &eallgemein" + priorities-hint: "&7Prioritäten: &alow&7, &enormal&7, &6high&7, &curgent" + max-tickets: "&cDu hast bereits &e{max} &coffene Ticket(s). Bitte warte, bis dein Ticket bearbeitet wurde." + no-description: "&cBitte gib eine Beschreibung für dein Ticket an." + too-long: "&cDeine Beschreibung ist zu lang! Maximal {max} Zeichen." + blacklist-blocked: "&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen." + category-invalid: "&cUnbekannte Kategorie: &e{input}&c. Verfügbare Kategorien: &e{categories}" + +# ============================================================ +# CLAIM / CLOSE / FORWARD +# ============================================================ +claim: + usage: "&cBenutzung: {cmd_claim} " + +close: + usage: "&cBenutzung: {cmd_close} [Kommentar]" + +forward: + usage: "&cBenutzung: {cmd_forward} " + bungee-offline: "&7[BungeeCord] Spieler &e{player} &7ist auf diesem Server nicht online." + local-not-found: "&cSpieler nicht gefunden!" + +# ============================================================ +# KOMMENTARE +# ============================================================ +comment: + saved: "&aDein Kommentar zu Ticket &e#{id} &awurde gespeichert." + usage: "&cBenutzung: {cmd_comment} " + too-long: "&cNachricht zu lang! Maximal 500 Zeichen." + no-permission: "&cDu kannst nur deine eigenen Tickets kommentieren." + error: "&cFehler beim Speichern des Kommentars." + notify-online: "&e[Ticket #{id}] &f{author} &7hat kommentiert: &f{message}" + notify-offline: "&e[Ticket #{id}] &f{author} &7hat kommentiert (während du offline warst): &f{message}" + claimer-offline: "&e[Ticket #{id}] &f{author} &7hat auf dein bearbeitetes Ticket kommentiert (offline): &f{message}" + +# ============================================================ +# BEWERTUNGEN +# ============================================================ +rating: + saved-good: "&aDanke für deine Bewertung! &a👍 Positiv" + saved-bad: "&aDanke für deine Bewertung! &c👎 Negativ" + already-rated: "&cDu hast dieses Ticket bereits bewertet." + not-yours: "&cDu kannst nur deine eigenen Tickets bewerten." + disabled: "&cBewertungen sind aktuell deaktiviert." + not-closeable: "&cBewertung konnte nicht gespeichert werden. Ist das Ticket noch offen?" + usage: "&cBenutzung: {cmd_rate} " + invalid: "&cUngültige Bewertung! Benutze &egood &coder &ebad&c." + prompt-header: "򇨣&m " + prompt-title: "&6Wie zufrieden bist du mit dem Support?" + prompt-good: "&a{cmd_rate} {id} good &7– 👍 Gut" + prompt-bad: "&c{cmd_rate} {id} bad &7– 👎 Schlecht" + prompt-footer: "򇨣&m " + +# ============================================================ +# PRIORITÄT SETZEN +# ============================================================ +setpriority: + usage: "&cBenutzung: {cmd_setpriority} " + disabled: "&cDas Prioritäten-System ist deaktiviert." + invalid: "&cUngültige Priorität! Gültig: &alow&7, &enormal&7, &6high&7, &curgent" + success: "&aPriorität von Ticket &e#{id} &awurde auf {priority} &agesetzt." + not-found: "&cTicket &e#{id} &cwurde nicht gefunden." + +# ============================================================ +# BLACKLIST +# ============================================================ +blacklist: + added: "&a{player} &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e{reason}" + removed: "&a{player} &awurde von der Blacklist entfernt." + already: "&cSpieler ist bereits auf der Blacklist." + not-found: "&cSpieler war nicht auf der Blacklist." + usage: "&cBenutzung: {cmd_blacklist} [Spieler] [Grund]" + usage-add: "&cBenutzung: {cmd_blacklist} add [Grund]" + usage-remove: "&cBenutzung: {cmd_blacklist} remove " + list-header: "&6Ticket-Blacklist &7({count} Einträge)" + list-empty: "&7Keine gesperrten Spieler." + list-entry: "&e{player} &7– &f{reason} &7(gesperrt von &e{by}&7)" + +# ============================================================ +# STATISTIKEN +# ============================================================ +stats: + header: "&6Ticket Statistik" + total: "&eGesamt: &a{count}" + open: "&eOffen: &a{count}" + closed: "&eGeschlossen: &a{count} &7(historisch)" + forwarded: "&eWeitergeleitet: &a{count}" + ratings-header: "&6Support-Bewertungen &7(gesamt, historisch)" + ratings-summary: "&a👍 Positiv: &f{up} &c👎 Negativ: &f{down}" + ratings-percent: "&7Zufriedenheit: &e{percent}%" + staff-header: "&6Bewertungen nach Support-Mitarbeiter:" + staff-table-header: "&7 Name 👍 👎 Tickets Zufrieden" + staff-entry: "&e {name} &a{up} &c{down} &7{total} &e{percent}" + servers-header: "&6Tickets nach Server:" + server-entry: "&b {server}: &a{count}" + top-header: "&6Top-5 Ticket-Ersteller &7(historisch, persistent)" + top-empty: "&7Noch keine Daten vorhanden." + top-entry: " {medal} &f{name} &e{count} &7{label}" + top-ticket-label: "Ticket" + top-tickets-label: "Tickets" + cache-info: "&7Cache: &e{count} &7gecachte Ticket(s)" + +# ============================================================ +# TOP-ERSTELLER +# ============================================================ +top: + header: "&6&lTop-5 Ticket-Ersteller" + empty: "&7Noch keine Daten vorhanden." + entry: "{medal} &f{name} &e{count} &7{label}" + footer: "&7(Zähler bleiben auch nach dem Löschen von Tickets erhalten)" + +# ============================================================ +# RELOAD +# ============================================================ +reload: + success: "&aKonfiguration wurde neu geladen. &7(Kategorien, FAQs, Cache geleert)" + bungee-info: "&8[BungeeCord] &7Server: &b{server}" + +# ============================================================ +# MIGRATE / EXPORT / IMPORT +# ============================================================ +migrate: + usage: "&cBenutzung: {cmd_migrate} " + +export: + usage: "&cBenutzung: {cmd_export} " + +import: + usage: "&cBenutzung: {cmd_import} " + +# ============================================================ +# FAQ-SYSTEM +# ============================================================ +faq: + usage-add: "&cBenutzung: {cmd_faq} add | " + usage-add-example: "&7Beispiel: &e{cmd_faq} add Wie erstelle ich ein Ticket? | Nutze {cmd_create}." + usage-edit: "&cBenutzung: {cmd_faq} edit | " + usage-delete: "&cBenutzung: {cmd_faq} delete " + separator-missing: "&cTrenne Frage und Antwort mit &e|&c, z.B.:" + separator-example: "&e{cmd_faq} add Wie erstelle ich ein Ticket? | Nutze {cmd_create}." + separator-short: "&cTrenne Frage und Antwort mit &e|&c." + invalid-id: "&cUngültige FAQ-ID: &e{id}" + created: "&aFAQ &e#{id} &awurde erfolgreich erstellt!" + created-question: "&7Frage: &e{question}" + created-answer: "&7Antwort: &f{answer}" + updated: "&aFAQ &e#{id} &awurde erfolgreich aktualisiert!" + deleted: "&aFAQ &e#{id} &awurde gelöscht." + not-found: "&cFAQ &e#{id} &cwurde nicht gefunden." + reloaded: "&aFAQs wurden neu geladen. ({count} Einträge)" + list-header: "&6Häufige Fragen (FAQ) &7— {count} Einträge" + list-empty: "&7Noch keine FAQs vorhanden." + list-entry: "&e#{id} &f{question}" + list-answer: " &7→ &f{answer}" + list-admin-hint: "&7Befehle: &e{cmd_faq} add &8| &e{cmd_faq} edit &8| &e{cmd_faq} delete " + unknown-sub: "&cUnbekannter FAQ-Befehl." + hint-open: "&7Benutze &e{cmd_faq} &7zum Öffnen der GUI." + admin-commands: "&7Admin-Befehle: &e{cmd_faq} add | edit | delete | reload | list" + +# ============================================================ +# HILFE-MENÜ (/ticket ohne Argumente) +# ============================================================ +help: + header: "�FFFF&lTicketSystem &7– Befehle" + create: "&e{cmd_create} [Kategorie] &7– Neues Ticket erstellen" + list: "&e{cmd_list} &7– Deine Tickets ansehen (GUI)" + comment: "&e{cmd_comment} &7– Nachricht zu einem Ticket" + rate: "&e{cmd_rate} &7– Support bewerten" + claim: "&e{cmd_claim} &7– Ticket annehmen" + close: "&e{cmd_close} [Kommentar] &7– Ticket schließen" + forward: "&e{cmd_forward} &7– Ticket weiterleiten" + blacklist: "&e{cmd_blacklist} [Spieler] [Grund] &7– Blacklist verwalten" + reload: "&e{cmd_reload} &7– Konfiguration neu laden" + stats: "&e{cmd_stats} &7– Statistiken anzeigen" + bungee-status: "&8[BungeeCord] &7Server: &b{server} &8| Cross-Server-Benachrichtigungen &aaktiv" + +# ============================================================ +# GUI-TEXTE (TicketGUI) +# ============================================================ +gui: + # ── Chat-Nachrichten ──────────────────────────────────── + no-archive-permission: "&cDu hast keine Berechtigung, das Archiv zu öffnen." + no-tickets: "&aDu hast aktuell keine Tickets." + filter-label: "&7Filter: {filter}" + ticket-removed: "&aDein Ticket &e#{id} &awurde aus deiner Übersicht entfernt." + ticket-remove-error: "&cFehler beim Entfernen des Tickets." + ticket-remove-claimed: "&cDu kannst dieses Ticket nicht löschen, da es bereits von einem Supporter bearbeitet wird." + teleport-success: "&7Du wurdest zu Ticket &e#{id} &7teleportiert." + world-not-loaded: "&cDie Welt des Tickets ist nicht geladen!" + teleport-disabled: "&cServerübergreifender Teleport ist in der Config deaktiviert.{hint}" + teleport-unknown: "&cServer des Tickets unbekannt – Teleport nicht möglich." + bungee-connect: "&7Verbinde dich mit Server &b{server} &7für Ticket &e#{id}&7..." + bungee-connect-fail: "&cServer-Wechsel fehlgeschlagen. Bitte manuell verbinden." + no-delete-permission: "&cDu hast keine Berechtigung, Tickets permanent zu löschen." + only-closed-deletable: "&cNur geschlossene Tickets können permanent gelöscht werden." + ticket-deleted: "&aTicket &e#{id} &awurde permanent gelöscht." + ticket-delete-error: "&cFehler beim Löschen des Tickets." + already-closed: "&cDieses Ticket ist bereits geschlossen." + close-prompt-header: "&6Ticket #{id} schließen" + close-prompt-hint: "&7Gib einen Kommentar ein (&e- &7für keinen)." + close-prompt-cancel: "&7Abbrechen mit &ccancel" + close-cancelled: "&cAbgebrochen." + close-comment-echo: "&7Kommentar: &f{comment}" + no-priority-permission: "&cDu hast keine Berechtigung, die Priorität zu ändern." + priority-closed: "&cDie Priorität geschlossener Tickets kann nicht geändert werden." + priority-set: "&aPriorität auf {priority} &agesetzt." + priority-error: "&cFehler beim Ändern der Priorität." + comments-header: "&6Kommentare zu Ticket #{id}" + comments-empty: "&7Noch keine Kommentare vorhanden." + comments-entry: "&e{author} &7({time})&8: &f{message}" + + # ── Inventar-Titel ────────────────────────────────────── + item: + title-admin: "§8§lTicket-Übersicht" + title-archive: "§8§lTicket-Archiv" + title-player: "§8§lMeine Tickets" + title-detail: "§8§lTicket-Details" + + # ── Lore-Labels in Ticket-Items ───────────────────── + lore-creator: "§7Ersteller: §e{value}" + lore-message: "§7Anliegen: §f{value}" + lore-created: "§7Erstellt: §e{value}" + lore-server: "§7Server: §b{value}" + lore-world: "§7Welt: §e{value}" + lore-position: "§7Position: §e{value}" + lore-category: "§7Kategorie: {value}" + lore-priority: "§7Priorität: {value}" + lore-claimed-by: "§7Angenommen von: §a{value}" + lore-claimed-at: "§7Angenommen am: §a{value}" + lore-closed-at: "§7Geschlossen am: §c{value}" + lore-comment: "§7Kommentar: §f{value}" + lore-rating-none: "§7Keine Bewertung" + lore-rating-good: "§a👍 Positiv" + lore-rating-bad: "§c👎 Negativ" + lore-rating-label: "§7Bewertung: {value}" + lore-player-deleted: "§cSpieler hat Ticket gelöscht." + + # ── Admin-Listen-Item ─────────────────────────────── + list-click: "§e§l» KLICKEN für Details" + + # ── Spieler-Listen-Item ───────────────────────────── + player-delete-hint: "§c§l» KLICKEN zum Löschen" + player-delete-desc: "§7Entferne dieses Ticket aus deiner Übersicht." + player-in-progress: "§e» Ticket wird bearbeitet..." + player-no-delete: "§7Kann nicht mehr gelöscht werden." + player-rate-hint: "§e» /ticket rate {id} good/bad" + player-rated-good: "§7Bewertet: §a👍" + player-rated-bad: "§7Bewertet: §c👎" + player-comment-label: "§7Kommentar des Supports:" + + # ── Detail-Aktions-Buttons ────────────────────────── + btn-teleport: "§b§lTeleportieren" + btn-teleport-lore1: "§7Teleportiert dich zur" + btn-teleport-lore2: "§7Position des Tickets." + btn-teleport-bungee1: "§7Teleportiert dich zur Ticket-Position." + btn-teleport-same: "§7Dieser Server §a(direkt)" + btn-teleport-other: "§7Ziel-Server: §b{server}" + btn-teleport-local: "§8Lokaler Teleport" + btn-teleport-switch: "§8Server-Wechsel erforderlich" + btn-teleport-unknown: "§cServer unbekannt" + btn-teleport-disabled: "§8Teleport deaktiviert" + btn-teleport-dis1: "§7Im BungeeCord-Modus ist" + btn-teleport-dis2: "§7Teleportation deaktiviert." + btn-teleport-dis3: "§8(bungee-teleport-enabled: false)" + btn-teleport-server: "§7Ticket-Server: §b{server}" + btn-teleport-noserver: "§7Server unbekannt" + + btn-claim: "§a§lTicket annehmen" + btn-claim-lore1: "§7Nimmt dieses Ticket an" + btn-claim-lore2: "§7und markiert es als bearbeitet." + btn-claimed: "§8Bereits angenommen" + btn-claimed-lore1: "§7Dieses Ticket wurde bereits" + btn-claimed-lore2: "§7angenommen." + + btn-delete: "§4§lTicket permanent löschen" + btn-delete-lore1: "§7Löscht dieses Ticket" + btn-delete-lore2: "§7unwiderruflich aus der Datenbank." + btn-delete-warn: "§c§lACHTUNG: §cNicht rückgängig zu machen!" + + btn-close: "§c§lTicket schließen" + btn-close-lore1: "§7Schließt das Ticket." + btn-close-lore2: "§eKlick für Kommentar-Eingabe." + btn-closed: "§8Bereits geschlossen" + btn-closed-lore1: "§7Dieses Ticket ist bereits" + btn-closed-lore2: "§7geschlossen." + + btn-comments: "§e§lKommentare anzeigen" + btn-comments-lore1: "§7Zeigt alle Nachrichten/Antworten" + btn-comments-lore2: "§7zu diesem Ticket im Chat." + + btn-prio: "§6§lPriorität ändern" + btn-prio-current: "§7Aktuell: {value}" + btn-prio-click: "§8Klicken zum Wechseln" + + btn-back: "§7§lZurück" + btn-back-lore: "§7Zurück zur Ticket-Übersicht." + + # ── Navigation ────────────────────────────────────── + nav-prev: "§7§l◄ Zurück" + nav-prev-lore: "§7Seite {page} von {total}" + nav-next: "§7§lWeiter ►" + nav-next-lore: "§7Seite {page} von {total}" + nav-page: "§8Seite {page}/{total}" + nav-page-lore: "§7Gesamt: {count} Tickets auf dieser Seite" + + nav-archive: "§7§lGeschlossene Tickets" + nav-archive-lore1: "§7Zeigt alle abgeschlossenen" + nav-archive-lore2: "§7Tickets im Archiv an." + nav-back-overview: "§7§lZurück zur Übersicht" + nav-back-ov-lore: "§7Zeigt alle offenen Tickets." + + nav-filter: "§e§lKategorie-Filter" + nav-filter-current: "§7Aktuell: {value}" + nav-filter-click: "§8Klicken zum Wechseln" + nav-filter-all: "§7Alle (kein Filter)" + + # ── FAQ GUI Texte (Neu) ───────────────────────────────── + faq: + title: "&#FFD700&lHäufige Fragen (FAQ)" + admin-title: "§8§lFAQ verwalten" + action-title: "§8§lFAQ Aktionen" + + add-button: "§a§lNeues FAQ hinzufügen" + add-lore-1: "§7Fügt einen neuen FAQ-Eintrag hinzu." + add-lore-2: "§7Du wirst nach Frage und Antwort gefragt." + + edit-button: "§a§lFAQ bearbeiten" + edit-lore-1: "§7Ändere Frage und Antwort" + edit-lore-2: "§7dieses FAQ-Eintrags." + + delete-button: "§c§lFAQ löschen" + delete-lore-1: "§7Löscht diesen FAQ-Eintrag." + delete-lore-2: "§c§lACHTUNG: §cNicht rückgängig zu machen!" + delete-error: "§cFehler: FAQ #{id} konnte nicht gelöscht werden." + + back-button: "§7§lZurück" + back-lore: "§7Zurück zur FAQ-Übersicht." + + click-detail: "§e» Klicken für mehr Details im Chat" + click-edit: "§e» Klicken zum Bearbeiten / Löschen" + + nav-prev: "§7§l◄ Zurück" + nav-prev-lore: "§7Seite {page} von {total}" + nav-next: "§7§lWeiter ►" + nav-next-lore: "§7Seite {page} von {total}" + nav-page: "§8Seite {page}/{total}" + nav-page-lore: "§7Gesamt: {count} FAQ(s)" + + chat-create-title: "§6§lNeues FAQ erstellen" + chat-question-prompt: "§7Gib die §eFrage §7ein (oder §ccancel§7):" + chat-answer-prompt: "§7Gib jetzt die §eAntwort §7ein (oder §ccancel§7):" + chat-edit-title: "§6§lFAQ #{id} bearbeiten" + chat-current-question: "§7Aktuelle Frage: §e{question}" + + lore-id: "§7FAQ #{id}" + lore-separator: "§8§m " + question-set: "§7Frage gesetzt: §e{question}" + internal-error: "§cInterner Fehler beim Bearbeiten des FAQs." + +# ============================================================ +# JOIN-LISTENER +# ============================================================ +join: + open-tickets: "&eEs gibt noch &6{count} &eoffene Ticket(s)!" + open-tickets-hint: "&7» Tippe &e{cmd_list} &7für die Übersicht." + teleport-world-missing: "&cTeleport-Zielwelt &e{world} &cnicht gefunden!" + teleport-success: "&7Du wurdest zur Ticket-Position teleportiert. &8({coords})" + pending-header: "&6Ticket-Benachrichtigungen &7(während du offline warst):" + +# ============================================================ +# UPDATE-CHECKER +# ============================================================ +update: + available-console: "Neue Version verfügbar: {new} (aktuell: {current})" + available-bar: "====================================================" + available-line1: "&6[TicketSystem] &eNEUES UPDATE VERFÜGBAR: v{version}" + available-line2: "&6[TicketSystem] &eDownload: https://www.spigotmc.org/resources/132757" \ No newline at end of file diff --git a/src/main/resources/lang_en.yml b/src/main/resources/lang_en.yml new file mode 100644 index 0000000..a8b8da7 --- /dev/null +++ b/src/main/resources/lang_en.yml @@ -0,0 +1,461 @@ +# ============================================================ +# TicketSystem – Language File English (en) +# +# All plugin messages can be customized here. +# Color codes: & (e.g. &a = green, &c = red, &e = yellow, &7 = grey) +# HEX-Codes: &#RRGGBB (e.g. �AA00 = Green) +# Placeholders are written in curly braces: {id}, {player}, ... +# +# Switch language in config.yml: language: en +# Switch command language in config.yml: command-language: de | en | both +# +# {cmd_X} is automatically replaced based on command-language, e.g.: +# command-language: de → /ticket erstellen +# command-language: en → /ticket create +# command-language: both → /ticket create (erstellen) +# ============================================================ + +prefix: "ᖳFF[&fTicketᖳFF] &r" + +# ============================================================ +# GENERAL ERRORS & HINTS +# ============================================================ +general: + no-permission: "&cYou don't have permission to do this!" + console-only: "&cThis command can only be used by players." + invalid-id: "&cInvalid ID!" + invalid-player-id: "&cInvalid ticket ID: &e{id}" + player-not-found: "&cPlayer not found!" + ticket-not-found: "&cTicket not found!" + already-claimed: "&cThis ticket has already been claimed!" + no-open-tickets: "&aThere are no open tickets right now." + cooldown: "&cPlease wait &e{seconds} seconds &cbefore creating a new ticket." + separator: "򽸱&m " + +# ============================================================ +# SYSTEM (Export, Import, Migration, Archive, Validation) +# ============================================================ +system: + export-success: "&aExport successful: &e{count} &atickets exported to &e{file}&a." + export-fail: "&cExport failed or no tickets found." + import-success: "&aImport successful: &e{count} &atickets imported." + import-fail: "&cImport failed or no tickets found." + migration-success: "&aMigration complete: &e{count} &atickets migrated." + migration-fail: "&cNo tickets migrated or an error occurred." + archive-success: "&aArchiving complete: &e{count} &atickets archived." + archive-fail: "&cNo closed tickets found to archive." + file-not-found: "&cFile not found: &e{file}" + unknown-mode: "&cUnknown mode! Use: tomysql or tofile" + validation-warning: "&c&e{count} &cinvalid tickets were found during loading." + db-create-error: "&cFailed to create the ticket!" + +# ============================================================ +# TICKET ACTIONS +# ============================================================ +ticket: + created: "&aTicket &e#{id} &ahas been created successfully!" + created-category: "&aTicket &e#{id} &acreated! &7Category: {category}" + claimed: "&aYou have claimed ticket &e#{id} &afrom &e{player}&a." + claimed-notify: "&aYour ticket &e#{id} &ahas been claimed by &e{claimer}&a." + closed: "&aTicket &e#{id} &ahas been closed." + closed-notify: "&aYour ticket &e#{id} &ahas been closed." + forwarded: "&aTicket &e#{id} &ahas been forwarded to &e{player}&a." + forwarded-notify: "&eYou have received a forwarded ticket from &6{player}&e. &7(ID: {id})" + forwarded-creator: "&eYour ticket &6#{id} &ehas been forwarded to &b{supporter}&e." + new-notify: "&e{player} &acreated a new ticket: &7{message} &7(ID: &e{id}&7)" + close-comment-label: "&7Support comment: &f{comment}" + close-comment-short: "&7Comment: &f{comment}" + pending-closed: "&e[Ticket #{id}] &7Your ticket has been closed.{comment}{rating}" + pending-closed-comment: " &7Comment: &f{comment}" + pending-closed-rating: " &7Rating: &e{cmd_rate} {id} good/bad" + +# ============================================================ +# NOTIFICATIONS (Team / GUI hint) +# ============================================================ +notify: + gui-hint: "&7» Click &e{cmd_list} &7to open the overview." + team-category: " §7[§r{category}§7]" + team-priority: " §7Priority: §r{priority}" + team-server: " §7Server: §b{server}" + +# ============================================================ +# TICKET CREATE +# ============================================================ +create: + usage: "&cUsage: {cmd_create} [category] [priority] " + categories-hint: "&7Categories: &ebug&7, &equestion&7, &ecomplaint&7, &eother&7, &egeneral" + priorities-hint: "&7Priorities: &alow&7, &enormal&7, &6high&7, &curgent" + max-tickets: "&cYou already have &e{max} &copen ticket(s). Please wait until your ticket is processed." + no-description: "&cPlease provide a description for your ticket." + too-long: "&cYour description is too long! Maximum {max} characters." + blacklist-blocked: "&cYou have been banned from the ticket system and cannot create tickets." + category-invalid: "&cUnknown category: &e{input}&c. Available categories: &e{categories}" + +# ============================================================ +# CLAIM / CLOSE / FORWARD +# ============================================================ +claim: + usage: "&cUsage: {cmd_claim} " + +close: + usage: "&cUsage: {cmd_close} [comment]" + +forward: + usage: "&cUsage: {cmd_forward} " + bungee-offline: "&7[BungeeCord] Player &e{player} &7is not online on this server." + local-not-found: "&cPlayer not found!" + +# ============================================================ +# COMMENTS +# ============================================================ +comment: + saved: "&aYour comment on ticket &e#{id} &ahas been saved." + usage: "&cUsage: {cmd_comment} " + too-long: "&cMessage too long! Maximum 500 characters." + no-permission: "&cYou can only comment on your own tickets." + error: "&cFailed to save the comment." + notify-online: "&e[Ticket #{id}] &f{author} &7commented: &f{message}" + notify-offline: "&e[Ticket #{id}] &f{author} &7commented while you were offline: &f{message}" + claimer-offline: "&e[Ticket #{id}] &f{author} &7commented on your claimed ticket (offline): &f{message}" + +# ============================================================ +# RATINGS +# ============================================================ +rating: + saved-good: "&aThank you for your rating! &a👍 Positive" + saved-bad: "&aThank you for your rating! &c👎 Negative" + already-rated: "&cYou have already rated this ticket." + not-yours: "&cYou can only rate your own tickets." + disabled: "&cRatings are currently disabled." + not-closeable: "&cRating could not be saved. Is the ticket still open?" + usage: "&cUsage: {cmd_rate} " + invalid: "&cInvalid rating! Use &egood &cor &ebad&c." + prompt-header: "򽸱&m " + prompt-title: "&6How satisfied are you with the support?" + prompt-good: "&a{cmd_rate} {id} good &7– 👍 Good" + prompt-bad: "&c{cmd_rate} {id} bad &7– 👎 Bad" + prompt-footer: "򽸱&m " + +# ============================================================ +# SET PRIORITY +# ============================================================ +setpriority: + usage: "&cUsage: {cmd_setpriority} " + disabled: "&cThe priority system is disabled." + invalid: "&cInvalid priority! Valid: &alow&7, &enormal&7, &6high&7, &curgent" + success: "&aPriority of ticket &e#{id} &ahas been set to {priority}&a." + not-found: "&cTicket &e#{id} &cwas not found." + +# ============================================================ +# BLACKLIST +# ============================================================ +blacklist: + added: "&a{player} &ahas been added to the ticket blacklist. &7Reason: &e{reason}" + removed: "&a{player} &ahas been removed from the blacklist." + already: "&cPlayer is already on the blacklist." + not-found: "&cPlayer was not on the blacklist." + usage: "&cUsage: {cmd_blacklist} [player] [reason]" + usage-add: "&cUsage: {cmd_blacklist} add [reason]" + usage-remove: "&cUsage: {cmd_blacklist} remove " + list-header: "&6Ticket Blacklist &7({count} entries)" + list-empty: "&7No banned players." + list-entry: "&e{player} &7– &f{reason} &7(banned by &e{by}&7)" + +# ============================================================ +# STATISTICS +# ============================================================ +stats: + header: "&6Ticket Statistics" + total: "&eTotal: &a{count}" + open: "&eOpen: &a{count}" + closed: "&eClosed: &a{count} &7(historical)" + forwarded: "&eForwarded: &a{count}" + ratings-header: "&6Support Ratings &7(total, historical)" + ratings-summary: "&a👍 Positive: &f{up} &c👎 Negative: &f{down}" + ratings-percent: "&7Satisfaction: &e{percent}%" + staff-header: "&6Ratings by support staff:" + staff-table-header: "&7 Name 👍 👎 Tickets Satisfied" + staff-entry: "&e {name} &a{up} &c{down} &7{total} &e{percent}" + servers-header: "&6Tickets by server:" + server-entry: "&b {server}: &a{count}" + top-header: "&6Top-5 Ticket Creators &7(historical, persistent)" + top-empty: "&7No data available yet." + top-entry: " {medal} &f{name} &e{count} &7{label}" + top-ticket-label: "Ticket" + top-tickets-label: "Tickets" + cache-info: "&7Cache: &e{count} &7cached ticket(s)" + +# ============================================================ +# TOP CREATORS +# ============================================================ +top: + header: "&6&lTop-5 Ticket Creators" + empty: "&7No data available yet." + entry: "{medal} &f{name} &e{count} &7{label}" + footer: "&7(Counts are kept even after tickets are deleted)" + +# ============================================================ +# RELOAD +# ============================================================ +reload: + success: "&aConfiguration reloaded. &7(Categories, FAQs, cache cleared)" + bungee-info: "&8[BungeeCord] &7Server: &b{server}" + +# ============================================================ +# MIGRATE / EXPORT / IMPORT +# ============================================================ +migrate: + usage: "&cUsage: {cmd_migrate} " + +export: + usage: "&cUsage: {cmd_export} " + +import: + usage: "&cUsage: {cmd_import} " + +# ============================================================ +# FAQ SYSTEM +# ============================================================ +faq: + usage-add: "&cUsage: {cmd_faq} add | " + usage-add-example: "&7Example: &e{cmd_faq} add How do I create a ticket? | Use {cmd_create}." + usage-edit: "&cUsage: {cmd_faq} edit | " + usage-delete: "&cUsage: {cmd_faq} delete " + separator-missing: "&cSeparate question and answer with &e|&c, e.g.:" + separator-example: "&e{cmd_faq} add How do I create a ticket? | Use {cmd_create}." + separator-short: "&cSeparate question and answer with &e|&c." + invalid-id: "&cInvalid FAQ ID: &e{id}" + created: "&aFAQ &e#{id} &ahas been created successfully!" + created-question: "&7Question: &e{question}" + created-answer: "&7Answer: &f{answer}" + updated: "&aFAQ &e#{id} &ahas been updated successfully!" + deleted: "&aFAQ &e#{id} &ahas been deleted." + not-found: "&cFAQ &e#{id} &cwas not found." + reloaded: "&aFAQs reloaded. ({count} entries)" + list-header: "&6Frequently Asked Questions &7— {count} entries" + list-empty: "&7No FAQs available yet." + list-entry: "&e#{id} &f{question}" + list-answer: " &7→ &f{answer}" + list-admin-hint: "&7Commands: &e{cmd_faq} add &8| &e{cmd_faq} edit &8| &e{cmd_faq} delete " + unknown-sub: "&cUnknown FAQ command." + hint-open: "&7Use &e{cmd_faq} &7to open the GUI." + admin-commands: "&7Admin commands: &e{cmd_faq} add | edit | delete | reload | list" + +# ============================================================ +# HELP MENU (/ticket without arguments) +# ============================================================ +help: + header: "�FF00&lTicketSystem &7– Commands" + create: "&e{cmd_create} [category] &7– Create a new ticket" + list: "&e{cmd_list} &7– View your tickets (GUI)" + comment: "&e{cmd_comment} &7– Add a message to a ticket" + rate: "&e{cmd_rate} &7– Rate the support" + claim: "&e{cmd_claim} &7– Claim a ticket" + close: "&e{cmd_close} [comment] &7– Close a ticket" + forward: "&e{cmd_forward} &7– Forward a ticket" + blacklist: "&e{cmd_blacklist} [player] [reason] &7– Manage blacklist" + reload: "&e{cmd_reload} &7– Reload configuration" + stats: "&e{cmd_stats} &7– Show statistics" + bungee-status: "&8[BungeeCord] &7Server: &b{server} &8| Cross-server notifications &aactive" + +# ============================================================ +# GUI TEXTS (TicketGUI) +# ============================================================ +gui: + # ── Chat messages ──────────────────────────────────────── + no-archive-permission: "&cYou don't have permission to open the archive." + no-tickets: "&aYou don't have any tickets right now." + filter-label: "&7Filter: {filter}" + ticket-removed: "&aYour ticket &e#{id} &ahas been removed from your overview." + ticket-remove-error: "&cFailed to remove the ticket." + ticket-remove-claimed: "&cYou cannot delete this ticket because it is already being handled by a supporter." + teleport-success: "&7You have been teleported to ticket &e#{id}&7." + world-not-loaded: "&cThe world of this ticket is not loaded!" + teleport-disabled: "&cCross-server teleport is disabled in the config.{hint}" + teleport-unknown: "&cTicket server unknown – teleport not possible." + bungee-connect: "&7Connecting to server &b{server} &7for ticket &e#{id}&7..." + bungee-connect-fail: "&cServer switch failed. Please connect manually." + no-delete-permission: "&cYou don't have permission to permanently delete tickets." + only-closed-deletable: "&cOnly closed tickets can be permanently deleted." + ticket-deleted: "&aTicket &e#{id} &ahas been permanently deleted." + ticket-delete-error: "&cFailed to delete the ticket." + already-closed: "&cThis ticket is already closed." + close-prompt-header: "&6Close ticket #{id}" + close-prompt-hint: "&7Enter a comment (&e- &7for none)." + close-prompt-cancel: "&7Type &ccancel &7to abort." + close-cancelled: "&cCancelled." + close-comment-echo: "&7Comment: &f{comment}" + no-priority-permission: "&cYou don't have permission to change the priority." + priority-closed: "&cThe priority of closed tickets cannot be changed." + priority-set: "&aPriority set to {priority}&a." + priority-error: "&cFailed to change the priority." + comments-header: "&6Comments on ticket #{id}" + comments-empty: "&7No comments yet." + comments-entry: "&e{author} &7({time})&8: &f{message}" + + # ── Inventory titles ───────────────────────────────────── + item: + title-admin: "§8§lTicket Overview" + title-archive: "§8§lTicket Archive" + title-player: "§8§lMy Tickets" + title-detail: "§8§lTicket Details" + + # ── Lore labels in ticket items ───────────────────── + lore-creator: "§7Creator: §e{value}" + lore-message: "§7Message: §f{value}" + lore-created: "§7Created: §e{value}" + lore-server: "§7Server: §b{value}" + lore-world: "§7World: §e{value}" + lore-position: "§7Position: §e{value}" + lore-category: "§7Category: {value}" + lore-priority: "§7Priority: {value}" + lore-claimed-by: "§7Claimed by: §a{value}" + lore-claimed-at: "§7Claimed at: §a{value}" + lore-closed-at: "§7Closed at: §c{value}" + lore-comment: "§7Comment: §f{value}" + lore-rating-none: "§7No rating" + lore-rating-good: "§a👍 Positive" + lore-rating-bad: "§c👎 Negative" + lore-rating-label: "§7Rating: {value}" + lore-player-deleted: "§cPlayer deleted this ticket." + + # ── Admin list item ───────────────────────────────── + list-click: "§e§l» CLICK for details" + + # ── Player list item ──────────────────────────────── + player-delete-hint: "§c§l» CLICK to remove" + player-delete-desc: "§7Remove this ticket from your overview." + player-in-progress: "§e» Ticket is being processed..." + player-no-delete: "§7Cannot be deleted anymore." + player-rate-hint: "§e» /ticket rate {id} good/bad" + player-rated-good: "§7Rated: §a👍" + player-rated-bad: "§7Rated: §c👎" + player-comment-label: "§7Support comment:" + + # ── Detail action buttons ─────────────────────────── + btn-teleport: "§b§lTeleport" + btn-teleport-lore1: "§7Teleports you to the" + btn-teleport-lore2: "§7location of this ticket." + btn-teleport-bungee1: "§7Teleports you to the ticket location." + btn-teleport-same: "§7This server §a(direct)" + btn-teleport-other: "§7Target server: §b{server}" + btn-teleport-local: "§8Local teleport" + btn-teleport-switch: "§8Server switch required" + btn-teleport-unknown: "§cServer unknown" + btn-teleport-disabled: "§8Teleport disabled" + btn-teleport-dis1: "§7In BungeeCord mode" + btn-teleport-dis2: "§7teleportation is disabled." + btn-teleport-dis3: "§8(bungee-teleport-enabled: false)" + btn-teleport-server: "§7Ticket server: §b{server}" + btn-teleport-noserver: "§7Server unknown" + + btn-claim: "§a§lClaim ticket" + btn-claim-lore1: "§7Claims this ticket and" + btn-claim-lore2: "§7marks it as being processed." + btn-claimed: "§8Already claimed" + btn-claimed-lore1: "§7This ticket has already" + btn-claimed-lore2: "§7been claimed." + + btn-delete: "§4§lPermanently delete ticket" + btn-delete-lore1: "§7Deletes this ticket" + btn-delete-lore2: "§7irreversibly from the database." + btn-delete-warn: "§c§lWARNING: §cThis cannot be undone!" + + btn-close: "§c§lClose ticket" + btn-close-lore1: "§7Closes the ticket." + btn-close-lore2: "§eClick to enter a comment." + btn-closed: "§8Already closed" + btn-closed-lore1: "§7This ticket is already" + btn-closed-lore2: "§7closed." + + btn-comments: "§e§lShow comments" + btn-comments-lore1: "§7Shows all messages/replies" + btn-comments-lore2: "§7for this ticket in chat." + + btn-prio: "§6§lChange priority" + btn-prio-current: "§7Current: {value}" + btn-prio-click: "§8Click to cycle" + + btn-back: "§7§lBack" + btn-back-lore: "§7Back to ticket overview." + + # ── Navigation ────────────────────────────────────── + nav-prev: "§7§l◄ Previous" + nav-prev-lore: "§7Page {page} of {total}" + nav-next: "§7§lNext ►" + nav-next-lore: "§7Page {page} of {total}" + nav-page: "§8Page {page}/{total}" + nav-page-lore: "§7Total: {count} tickets on this page" + + nav-archive: "§7§lClosed Tickets" + nav-archive-lore1: "§7Shows all completed" + nav-archive-lore2: "§7tickets in the archive." + nav-back-overview: "§7§lBack to Overview" + nav-back-ov-lore: "§7Shows all open tickets." + + nav-filter: "§e§lCategory Filter" + nav-filter-current: "§7Current: {value}" + nav-filter-click: "§8Click to cycle" + nav-filter-all: "§7All (no filter)" + + # ── FAQ GUI Texts (New) ───────────────────────────────── + faq: + title: "�FF00&lFrequently Asked Questions (FAQ)" + admin-title: "§8§lManage FAQ" + action-title: "§8§lFAQ Actions" + + add-button: "§a§lAdd new FAQ" + add-lore-1: "§7Adds a new FAQ entry." + add-lore-2: "§7You will be asked for question and answer." + + edit-button: "§a§lEdit FAQ" + edit-lore-1: "§7Change question and answer" + edit-lore-2: "§7of this FAQ entry." + + delete-button: "§c§lDelete FAQ" + delete-lore-1: "§7Deletes this FAQ entry." + delete-lore-2: "§c§lWARNING: §cCannot be undone!" + delete-error: "§cError: FAQ #{id} could not be deleted." + + back-button: "§7§lBack" + back-lore: "§7Back to FAQ overview." + + click-detail: "§e» Click for more details in chat" + click-edit: "§e» Click to edit / delete" + + nav-prev: "§7§l◄ Previous" + nav-prev-lore: "§7Page {page} of {total}" + nav-next: "§7§lNext ►" + nav-next-lore: "§7Page {page} of {total}" + nav-page: "§8Page {page}/{total}" + nav-page-lore: "§7Total: {count} FAQ(s)" + + chat-create-title: "§6§lCreate new FAQ" + chat-question-prompt: "§7Enter the §eQuestion §7(or §ccancel§7):" + chat-answer-prompt: "§7Now enter the §eAnswer §7(or §ccancel§7):" + chat-edit-title: "§6§lEdit FAQ #{id}" + chat-current-question: "§7Current Question: §e{question}" + + lore-id: "§7FAQ #{id}" + lore-separator: "§8§m " + question-set: "§7Question set: §e{question}" + internal-error: "§cInternal error while editing the FAQ." + +# ============================================================ +# JOIN LISTENER +# ============================================================ +join: + open-tickets: "&eThere are still &6{count} &eopen ticket(s)!" + open-tickets-hint: "&7» Type &e{cmd_list} &7for the overview." + teleport-world-missing: "&cTeleport target world &e{world} &cnot found!" + teleport-success: "&7You have been teleported to the ticket location. &8({coords})" + pending-header: "&6Ticket notifications &7(while you were offline):" + +# ============================================================ +# UPDATE CHECKER +# ============================================================ +update: + available-console: "New version available: {new} (current: {current})" + available-bar: "====================================================" + available-line1: "&6[TicketSystem] &eNEW UPDATE AVAILABLE: v{version}" + available-line2: "&6[TicketSystem] &eDownload: https://www.spigotmc.org/resources/132757" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 0f11f9b..3ad07b4 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: TicketSystem -version: 1.0.7 +version: 1.0.8 main: de.ticketsystem.TicketPlugin api-version: 1.20 author: M_Viper