package de.ticketsystem.manager; import de.ticketsystem.TicketPlugin; import de.ticketsystem.model.ConfigCategory; import de.ticketsystem.model.Ticket; import de.ticketsystem.model.TicketStatus; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import java.util.HashMap; import java.util.Map; import java.util.UUID; public class TicketManager { private final TicketPlugin plugin; /** Cooldown Map: UUID → Zeitstempel letztes Ticket */ private final Map cooldowns = new HashMap<>(); public TicketManager(TicketPlugin plugin) { this.plugin = plugin; } // ─────────────────────────── 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; } 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); } public void setCooldown(UUID uuid) { cooldowns.put(uuid, System.currentTimeMillis()); } // ─────────────────────────── 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. */ public void notifyTeam(Ticket ticket) { String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt"; String message = ticket.getMessage() != null ? ticket.getMessage() : ""; // Kategorie & Priorität optional anzeigen String categoryInfo = ""; String priorityInfo = ""; if (plugin.getConfig().getBoolean("categories-enabled", true)) { ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); categoryInfo = " §7[§r" + cat.getColored() + "§7]"; } if (plugin.getConfig().getBoolean("priorities-enabled", true)) { priorityInfo = " §7Priorität: §r" + ticket.getPriority().getColored(); } // BungeeCord: Server-Herkunft anzeigen wenn BungeeCord aktiviert String serverInfo = ""; if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) { serverInfo = " §7Server: §b" + ticket.getServerName(); } String msg = plugin.formatMessage("messages.new-ticket-notify") .replace("{player}", creatorName) .replace("{message}", message) .replace("{id}", String.valueOf(ticket.getId())) + categoryInfo + priorityInfo + serverInfo; String guiHint = plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen."); 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); p.sendMessage(guiHint); } } } plugin.getDiscordWebhook().sendNewTicket(ticket); } /** * Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde. * Setzt claimer_notified = true und persistiert es. * * BungeeCord: Zustellung auch wenn der Spieler auf einem anderen Server ist. */ public void notifyCreatorClaimed(Ticket ticket) { String claimerName = resolveClaimerName(ticket); String msg = plugin.formatMessage("messages.ticket-claimed-notify") .replace("{id}", String.valueOf(ticket.getId())) .replace("{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. */ public void notifyClaimedWhileOffline(Player player) { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { var tickets = plugin.getDatabaseManager().getTicketsByStatus( TicketStatus.CLAIMED, TicketStatus.FORWARDED); for (Ticket t : tickets) { if (!t.getCreatorUUID().equals(player.getUniqueId())) continue; if (t.isClaimerNotified()) continue; String claimerName = t.getClaimerName() != null ? t.getClaimerName() : "Support"; final String name = claimerName; Bukkit.getScheduler().runTask(plugin, () -> { if (!player.isOnline()) return; if (t.getStatus() == TicketStatus.CLAIMED) { String msg = plugin.formatMessage("messages.ticket-claimed-notify") .replace("{id}", String.valueOf(t.getId())) .replace("{claimer}", name); player.sendMessage(msg); } else { String forwardedTo = t.getForwardedToName() != null ? t.getForwardedToName() : "einen Supporter"; String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify") .replace("{id}", String.valueOf(t.getId())) .replace("{supporter}", forwardedTo); player.sendMessage(msg); } }); plugin.getDatabaseManager().markClaimerNotified(t.getId()); } }); } /** * Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde. * BungeeCord: Cross-Server-Zustellung. */ public void notifyCreatorForwarded(Ticket ticket) { String forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter"; String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify") .replace("{id}", String.valueOf(ticket.getId())) .replace("{supporter}", forwardedTo); deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg); // Auch bei Weiterleitung notified setzen plugin.getDatabaseManager().markClaimerNotified(ticket.getId()); } /** * Sendet dem weitergeleiteten Supporter eine Benachrichtigung. * BungeeCord: Zustellung auch wenn der Supporter auf einem anderen Server ist. */ 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())); deliverToPlayer(ticket.getForwardedToUUID(), ticket.getForwardedToName(), msg); plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName); } /** * Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde. * BungeeCord: Cross-Server-Zustellung + Fallback in Pending-DB. */ public void notifyCreatorClosed(Ticket ticket) { notifyCreatorClosed(ticket, null); } public void notifyCreatorClosed(Ticket ticket, String closerName) { // Bug-Fix: close_notified wird in der DB gespeichert – kein In-Memory-Set mehr. // Dadurch funktioniert der Check auch nach einem Server-Wechsel korrekt. Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> plugin.getDatabaseManager().markCloseNotified(ticket.getId())); String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) ? ticket.getCloseComment() : ""; // Hauptnachricht String msg = plugin.formatMessage("messages.ticket-closed-notify") .replace("{id}", String.valueOf(ticket.getId())) .replace("{comment}", comment); // Bewertungsaufforderung String ratingMsg = null; if (plugin.getConfig().getBoolean("rating-enabled", true)) { ratingMsg = plugin.color( "&8&m &r\n" + "&6Wie zufrieden bist du mit dem Support?\n" + "&a/ticket rate " + ticket.getId() + " good &7– 👍 Gut\n" + "&c/ticket rate " + ticket.getId() + " bad &7– 👎 Schlecht\n" + "&8&m "); } // Prüfen ob Ersteller lokal online ist Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); if (creator != null && creator.isOnline()) { // ─ Lokal online: direkt zustellen ──────────────────────────── creator.sendMessage(msg); if (!comment.isEmpty()) creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment)); if (ratingMsg != null) creator.sendMessage(ratingMsg); } else if (plugin.isBungeeCordEnabled()) { // ─ BungeeCord: via Plugin-Messaging auf anderen Servern zustellen ─ // KEIN savePendingClosedNotification hier! Das würde bei Server-Wechsel // als "Offline-Nachricht" doppelt angezeigt werden. // BungeeCord's "Message"-Kanal erreicht den Spieler netzwerkweit sofern er online ist. // Ist er wirklich offline, sieht er beim nächsten Login via PlayerJoinListener // eine frische Benachrichtigung (close_notified=true verhindert Duplikate). plugin.getBungeeMessenger().sendMessageToPlayer( ticket.getCreatorUUID(), ticket.getCreatorName(), msg); if (!comment.isEmpty()) plugin.getBungeeMessenger().sendMessageToPlayer( ticket.getCreatorUUID(), ticket.getCreatorName(), plugin.color("&7Kommentar des Supports: &f" + comment)); if (ratingMsg != null) plugin.getBungeeMessenger().sendMessageToPlayer( ticket.getCreatorUUID(), ticket.getCreatorName(), ratingMsg); } else { // ─ Standalone, Spieler offline: in Pending-DB speichern ────── savePendingClosedNotification(ticket, comment); } String closer = closerName != null ? closerName : "Unbekannt"; plugin.getDiscordWebhook().sendTicketClosed(ticket, closer); } /** * Bug-Fix: Nutzt jetzt close_notified aus der DB statt ein In-Memory-Set. * Funktioniert damit auch nach Server-Wechseln in BungeeCord-Netzwerken korrekt. * * @deprecated Bitte stattdessen ticket.isCloseNotified() direkt prüfen, * da das Ticket-Objekt aus der DB bereits den korrekten Wert hat. */ public boolean wasClosedNotificationSent(int ticketId) { // Direkt in der DB nachschlagen – kein In-Memory-Set, kein Server-gebundener State Ticket t = plugin.getDatabaseManager().getTicketById(ticketId); return t != null && t.isCloseNotified(); } // ─────────────────────────── BungeeCord Hilfsmethoden ────────────────── // ── BUG FIX #2 ────────────────────────────────────────────────────────── // Vorher: addPendingNotification() wurde IMMER asynchron ausgeführt – // auch wenn der Spieler lokal online war oder BungeeCord die // Nachricht bereits zugestellt hat. Das führte dazu, dass Spieler // beim nächsten Login immer noch eine "verpasste Nachricht" sahen, // obwohl sie die Nachricht bereits erhalten hatten. // // Fix: addPendingNotification() wird nur noch aufgerufen wenn: // 1. Der Spieler NICHT lokal online ist, UND // 2. BungeeCord NICHT aktiviert ist (Standalone-Fallback). // Im BungeeCord-Modus ist der BungeeCord-"Message"-Kanal für die // Zustellung zuständig. Offline-Spieler werden über close_notified // und den PlayerJoinListener beim nächsten Login benachrichtigt. // ──────────────────────────────────────────────────────────────────────── /** * Zustellung einer Nachricht an einen Spieler. * * Ablauf: * 1. Spieler lokal online → direkt * 2. BungeeCord aktiv → via Plugin-Messaging (kein Pending-Eintrag) * 3. Offline + Standalone → Pending-DB (Zustellung beim nächsten Login) * * @param uuid UUID des Empfängers * @param name Spielername (für BungeeCord-Lookup) * @param message Bereits color-übersetzter Text */ private void deliverToPlayer(UUID uuid, String name, String message) { Player local = Bukkit.getPlayer(uuid); if (local != null && local.isOnline()) { // Lokal online → direkt zustellen, fertig local.sendMessage(message); return; } if (plugin.isBungeeCordEnabled()) { // BungeeCord-Modus: Nachricht über Plugin-Messaging weiterleiten. // KEIN Pending-Eintrag! BungeeCord übernimmt die Zustellung. // Ist der Spieler wirklich offline, kümmert sich der PlayerJoinListener // beim nächsten Login um die Benachrichtigung. plugin.getBungeeMessenger().sendMessageToPlayer(uuid, name, message); return; } // Standalone-Modus, Spieler offline → in Pending-DB speichern Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> plugin.getDatabaseManager().addPendingNotification(uuid, message)); } /** * Speichert eine ausstehende Schließ-Benachrichtigung in der DB. */ private void savePendingClosedNotification(Ticket ticket, String comment) { String pendingMsg = "&e[Ticket #" + ticket.getId() + "] &7Dein Ticket wurde geschlossen." + (comment.isEmpty() ? "" : " &7Kommentar: &f" + comment) + (plugin.getConfig().getBoolean("rating-enabled", true) ? " &7Bewertung: &e/ticket rate " + ticket.getId() + " good/bad" : ""); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), pendingMsg)); } // ─────────────────────────── Hilfsmethoden ───────────────────────────── private String resolveClaimerName(Ticket ticket) { if (ticket.getClaimerName() != null) return ticket.getClaimerName(); if (ticket.getClaimerUUID() != null) { String name = Bukkit.getOfflinePlayer(ticket.getClaimerUUID()).getName(); if (name != null) return name; } return "Support"; } public boolean hasReachedTicketLimit(UUID uuid) { 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")); } } }