diff --git a/src/main/java/de/ticketsystem/TicketPlugin.java b/src/main/java/de/ticketsystem/TicketPlugin.java new file mode 100644 index 0000000..ed14952 --- /dev/null +++ b/src/main/java/de/ticketsystem/TicketPlugin.java @@ -0,0 +1,135 @@ + +package de.ticketsystem; + +import de.ticketsystem.commands.TicketCommand; +import de.ticketsystem.database.DatabaseManager; +import de.ticketsystem.gui.TicketGUI; +import de.ticketsystem.listeners.PlayerJoinListener; +import de.ticketsystem.manager.TicketManager; +import org.bukkit.ChatColor; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.Objects; + +public class TicketPlugin extends JavaPlugin { + + private static TicketPlugin instance; + + private boolean debug; + private DatabaseManager databaseManager; + private TicketManager ticketManager; + private TicketGUI ticketGUI; + + @Override + public void onEnable() { + instance = this; + + // Config speichern falls nicht vorhanden + saveDefaultConfig(); + + // Update-Checker (Spigot Resource-ID anpassen!) + int resourceId = 132757; + new UpdateChecker(this, resourceId).getVersion(version -> { + String current = getDescription().getVersion(); + if (!current.equals(version)) { + String msg = ChatColor.translateAlternateColorCodes('&', + "&6[TicketSystem] &eEs ist eine neue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")"); + getLogger().info("Es ist eine neue Version verfügbar: " + version + " (aktuell: " + current + ")"); + // Sende Nachricht an alle Admins (online) mit 1 Sekunde Verzögerung + getServer().getScheduler().runTaskLater(this, () -> { + getServer().getOnlinePlayers().stream() + .filter(p -> p.hasPermission("ticket.admin")) + .forEach(p -> p.sendMessage(msg)); + }, 20L); // 20 Ticks = 1 Sekunde + } else { + getLogger().info("TicketSystem ist aktuell (Version " + current + ")"); + } + }); + + // Versionsprüfung + String configVersion = getConfig().getString("version", ""); + String expectedVersion = "2.0"; + if (!expectedVersion.equals(configVersion)) { + getLogger().warning("[WARNUNG] Die Version deiner config.yml (" + configVersion + ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein! Bitte prüfe, ob deine Konfiguration aktuell ist."); + } + + // Debug-Status aus Config lesen + debug = getConfig().getBoolean("debug", false); + + // Datenbankverbindung aufbauen + databaseManager = new DatabaseManager(this); + if (!databaseManager.connect()) { + getLogger().severe("Konnte keine Datenbankverbindung herstellen! Plugin läuft im Datei-Modus weiter."); + if (isDebug()) getLogger().warning("[DEBUG] DatabaseManager.connect() fehlgeschlagen, Datei-Modus aktiviert."); + // Plugin bleibt aktiv, DatabaseManager wechselt auf Datei-Storage + } + + // Manager und GUI initialisieren + ticketManager = new TicketManager(this); + ticketGUI = new TicketGUI(this); + + // Commands registrieren + TicketCommand ticketCommand = new TicketCommand(this); + Objects.requireNonNull(getCommand("ticket")).setExecutor(ticketCommand); + Objects.requireNonNull(getCommand("ticket")).setTabCompleter(ticketCommand); + + // Events registrieren + getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); + getServer().getPluginManager().registerEvents(ticketGUI, this); + + // Automatische Archivierung nach Zeitplan (Intervall in Stunden, Standard: 24h) + int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24); + if (archiveIntervalH > 0) { + long ticks = archiveIntervalH * 60L * 60L * 20L; // Stunden → Ticks + getServer().getScheduler().runTaskTimer(this, () -> { + int archived = databaseManager.archiveClosedTickets(); + if (archived > 0) { + getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert."); + if (isDebug()) getLogger().info("[DEBUG] Archivierung ausgeführt, " + archived + " Tickets verschoben."); + } + }, ticks, ticks); + getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert."); + if (isDebug()) getLogger().info("[DEBUG] Archivierungs-Timer gesetzt: alle " + archiveIntervalH + " Stunden."); + } + + getLogger().info("TicketSystem erfolgreich gestartet!"); + } + + @Override + public void onDisable() { + if (databaseManager != null) { + databaseManager.disconnect(); + } + getLogger().info("TicketSystem wurde deaktiviert."); + } + + // ─────────────────────────── Hilfsmethoden ───────────────────────────── + + /** + * Formatiert eine Nachricht aus der Config mit Prefix und Farben. + */ + 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); + } + + /** + * Konvertiert Farbcodes (&x → §x). + */ + public String color(String text) { + return ChatColor.translateAlternateColorCodes('&', text); + } + + // ─────────────────────────── Getter ──────────────────────────────────── + + public static TicketPlugin getInstance() { return instance; } + public DatabaseManager getDatabaseManager() { return databaseManager; } + public TicketManager getTicketManager() { return ticketManager; } + public TicketGUI getTicketGUI() { return ticketGUI; } + + /** + * Gibt zurück, ob der Debug-Modus aktiv ist (aus config.yml) + */ + public boolean isDebug() { return debug; } +} diff --git a/src/main/java/de/ticketsystem/UpdateChecker.java b/src/main/java/de/ticketsystem/UpdateChecker.java new file mode 100644 index 0000000..fd7768c --- /dev/null +++ b/src/main/java/de/ticketsystem/UpdateChecker.java @@ -0,0 +1,36 @@ +package de.ticketsystem; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Scanner; +import java.util.function.Consumer; + +/** + * UpdateChecker für SpigotMC-Plugins. + * Prüft asynchron, ob eine neue Version verfügbar ist. + * Quelle: https://www.spigotmc.org/wiki/creating-an-update-checker-that-checks-for-updates + */ +public class UpdateChecker { + private final JavaPlugin plugin; + private final int resourceId; + + public UpdateChecker(JavaPlugin plugin, int resourceId) { + this.plugin = plugin; + this.resourceId = resourceId; + } + + public void getVersion(final Consumer consumer) { + Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { + try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId).openStream(); Scanner scann = new Scanner(is)) { + if (scann.hasNext()) { + consumer.accept(scann.next()); + } + } catch (IOException e) { + plugin.getLogger().info("Unable to check for updates: " + e.getMessage()); + } + }); + } +} diff --git a/src/main/java/de/ticketsystem/commands/TicketCommand.java b/src/main/java/de/ticketsystem/commands/TicketCommand.java new file mode 100644 index 0000000..b8cc4d2 --- /dev/null +++ b/src/main/java/de/ticketsystem/commands/TicketCommand.java @@ -0,0 +1,422 @@ + +package de.ticketsystem.commands; + +import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.Ticket; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class TicketCommand implements CommandExecutor, TabCompleter { + // Platzhalter für Admin-Kommandos + 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; + } + 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; + } + int finalMigrated = migrated; + Bukkit.getScheduler().runTask(plugin, () -> { + if (finalMigrated > 0) { + player.sendMessage(plugin.formatMessage("messages.migration-success") + .replace("{count}", String.valueOf(finalMigrated))); + } else { + player.sendMessage(plugin.formatMessage("messages.migration-fail")); + } + }); + }); + } + + 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); + 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")); + } + }); + }); + } + + 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; + } + 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")); + } + }); + }); + } + + private void handleStats(Player player) { + if (!player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); + return; + } + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + var stats = plugin.getDatabaseManager().getTicketStats(); + Bukkit.getScheduler().runTask(plugin, () -> { + player.sendMessage("§6--- Ticket Statistik ---"); + player.sendMessage("§eGesamt: §a" + stats.total + " §7| §eOffen: §a" + stats.open + " §7| §eGeschlossen: §a" + stats.closed + " §7| §eWeitergeleitet: §a" + stats.forwarded); + player.sendMessage("§6Top Ersteller:"); + stats.byPlayer.entrySet().stream().sorted((a,b)->b.getValue()-a.getValue()).limit(5).forEach(e -> + player.sendMessage("§e" + e.getKey() + ": §a" + e.getValue()) + ); + }); + }); + } + + // ─────────────────────────── /ticket archive ──────────────────────────── + private void handleArchive(Player player) { + if (!player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.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")); + } + }); + }); + } + + private final TicketPlugin plugin; + + public TicketCommand(TicketPlugin plugin) { + this.plugin = plugin; + } + + @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."); + return true; + } + + if (args.length == 0) { + plugin.getTicketManager().sendHelpMessage(player); + return true; + } + + switch (args[0].toLowerCase()) { + case "create" -> handleCreate(player, args); + case "list" -> handleList(player); + case "claim" -> handleClaim(player, args); + case "close" -> handleClose(player, args); + case "forward" -> handleForward(player, args); + case "reload" -> handleReload(player); + case "migrate" -> handleMigrate(player, args); + case "export" -> handleExport(player, args); + case "import" -> handleImport(player, args); + case "stats" -> handleStats(player); + case "archive" -> handleArchive(player); + default -> plugin.getTicketManager().sendHelpMessage(player); + } + return true; + } + + + // Methoden wie handleMigrate, handleCreate, handleList, handleClaim, handleClose, handleForward, handleReload, handleStats müssen auf Klassenebene stehen und dürfen nicht innerhalb von onCommand oder anderen Methoden verschachtelt sein. + // Entferne alle verschachtelten Methoden und stelle sicher, dass jede Methode nur einmal und auf Klassenebene existiert. + + // ─────────────────────────── /ticket create ──────────────────────────── + + private void handleCreate(Player player, String[] args) { + if (!player.hasPermission("ticket.create")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); + return; + } + if (args.length < 2) { + player.sendMessage(plugin.color("&cBenutzung: /ticket create ")); + return; + } + + // Cooldown-Check + if (plugin.getTicketManager().hasCooldown(player.getUniqueId())) { + long remaining = plugin.getTicketManager().getRemainingCooldown(player.getUniqueId()); + player.sendMessage(plugin.formatMessage("messages.cooldown") + .replace("{seconds}", String.valueOf(remaining))); + return; + } + + // Ticket-Limit-Check + 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.")); + return; + } + + // Nachricht zusammenbauen + String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); + int maxLen = plugin.getConfig().getInt("max-description-length", 100); + if (message.length() > maxLen) { + player.sendMessage(plugin.color("&cDeine Beschreibung ist zu lang! Maximal " + maxLen + " Zeichen.")); + return; + } + + // Ticket asynchron in DB speichern + Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation()); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + int id = plugin.getDatabaseManager().createTicket(ticket); + if (id == -1) { + player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets! Bitte wende dich an einen Admin.")); + return; + } + ticket.setId(id); + plugin.getTicketManager().setCooldown(player.getUniqueId()); + + Bukkit.getScheduler().runTask(plugin, () -> { + player.sendMessage(plugin.formatMessage("messages.ticket-created") + .replace("{id}", String.valueOf(id))); + + // Team benachrichtigen + plugin.getTicketManager().notifyTeam(ticket); + }); + }); + } + + // ─────────────────────────── /ticket list ────────────────────────────── + + private void handleList(Player player) { + if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); + return; + } + // GUI öffnen (synchron, Datenbankabfrage läuft darin async) + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> + Bukkit.getScheduler().runTask(plugin, () -> + plugin.getTicketGUI().openGUI(player))); + } + + // ─────────────────────────── /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; + } + if (args.length < 2) { + player.sendMessage(plugin.color("&cBenutzung: /ticket claim ")); + return; + } + + int id; + try { id = Integer.parseInt(args[1]); } + catch (NumberFormatException e) { + player.sendMessage(plugin.color("&cUngültige ID!")); + return; + } + + final int ticketId = id; + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + boolean success = plugin.getDatabaseManager().claimTicket( + ticketId, player.getUniqueId(), player.getName()); + + Bukkit.getScheduler().runTask(plugin, () -> { + if (!success) { + player.sendMessage(plugin.formatMessage("messages.already-claimed")); + return; + } + Ticket ticket = plugin.getDatabaseManager().getTicketById(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); + + // Zur Ticket-Position teleportieren + if (ticket.getLocation() != null) { + player.teleport(ticket.getLocation()); + } + }); + }); + } + + // ─────────────────────────── /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; + } + if (args.length < 2) { + player.sendMessage(plugin.color("&cBenutzung: /ticket close ")); + return; + } + + int id; + try { id = Integer.parseInt(args[1]); } + catch (NumberFormatException e) { + player.sendMessage(plugin.color("&cUngültige ID!")); + return; + } + + final int ticketId = id; + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + boolean success = plugin.getDatabaseManager().closeTicket(ticketId); + Bukkit.getScheduler().runTask(plugin, () -> { + if (success) { + player.sendMessage(plugin.formatMessage("messages.ticket-closed") + .replace("{id}", String.valueOf(ticketId))); + } else { + player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); + } + }); + }); + } + + // ─────────────────────────── /ticket forward ─────────────────────────── + + private void handleForward(Player player, String[] args) { + if (!player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); + return; + } + if (args.length < 3) { + player.sendMessage(plugin.color("&cBenutzung: /ticket forward ")); + return; + } + + int id; + try { id = Integer.parseInt(args[1]); } + catch (NumberFormatException e) { + player.sendMessage(plugin.color("&cUngültige ID!")); + return; + } + + Player target = Bukkit.getPlayer(args[2]); + if (target == null || !target.isOnline()) { + player.sendMessage(plugin.color("&cSpieler &e" + args[2] + " &cist nicht online!")); + return; + } + + final int ticketId = id; + final Player finalTarget = target; + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + boolean success = plugin.getDatabaseManager().forwardTicket( + ticketId, finalTarget.getUniqueId(), finalTarget.getName()); + + if (success) { + Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); + Bukkit.getScheduler().runTask(plugin, () -> { + player.sendMessage(plugin.formatMessage("messages.ticket-forwarded") + .replace("{id}", String.valueOf(ticketId)) + .replace("{player}", finalTarget.getName())); + if (ticket != null) plugin.getTicketManager().notifyForwardedTo(ticket); + }); + } else { + Bukkit.getScheduler().runTask(plugin, () -> + player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); + } + }); + } + + // ─────────────────────────── /ticket reload ──────────────────────────── + + private void handleReload(Player player) { + if (!player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); + return; + } + plugin.reloadConfig(); + player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen.")); + } + + // ─────────────────────────── 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; + + if (args.length == 1) { + List subs = new ArrayList<>(); + subs.add("create"); + if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { + subs.addAll(List.of("list", "claim", "close")); + } + if (player.hasPermission("ticket.admin")) { + subs.addAll(List.of("forward", "reload")); + } + for (String s : subs) { + if (s.startsWith(args[0].toLowerCase())) completions.add(s); + } + } else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) { + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) + completions.add(p.getName()); + } + } + return completions; + } +} diff --git a/src/main/java/de/ticketsystem/database/DatabaseManager.java b/src/main/java/de/ticketsystem/database/DatabaseManager.java new file mode 100644 index 0000000..130e548 --- /dev/null +++ b/src/main/java/de/ticketsystem/database/DatabaseManager.java @@ -0,0 +1,763 @@ + +package de.ticketsystem.database; + +import java.io.File; +import java.io.IOException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.Ticket; +import de.ticketsystem.model.TicketStatus; +import java.sql.*; +import java.util.ArrayList; + +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import java.io.FileReader; +import java.io.FileWriter; +import org.bukkit.Bukkit; + +public class DatabaseManager { + // Test-Konstruktor für Unit-Tests (ohne Bukkit/Plugin) + public DatabaseManager(File dataFile, YamlConfiguration dataConfig) { + this.plugin = null; + this.useMySQL = false; + this.useJson = false; + this.dataFileName = dataFile.getName(); + this.archiveFileName = "archive.json"; + this.dataFile = dataFile; + this.dataConfig = dataConfig; + validateLoadedTickets(); + } + /** + * Archiviert alle geschlossenen Tickets in eine separate Datei und entfernt sie aus dem aktiven Speicher. + * @return Anzahl archivierter Tickets + */ + public int archiveClosedTickets() { + List all = getAllTickets(); + List toArchive = new ArrayList<>(); + for (Ticket t : all) { + if (t.getStatus() == TicketStatus.CLOSED) toArchive.add(t); + } + if (toArchive.isEmpty()) return 0; + File archiveFile = new File(plugin.getDataFolder(), archiveFileName); + JSONArray arr = new JSONArray(); + // Bestehendes Archiv laden + if (archiveFile.exists()) { + try (FileReader fr = new FileReader(archiveFile)) { + JSONParser parser = new JSONParser(); + Object parsed = parser.parse(fr); + if (parsed instanceof JSONArray oldArr) arr.addAll(oldArr); + } catch (Exception ignored) {} + } + for (Ticket t : toArchive) arr.add(ticketToJson(t)); + try (FileWriter fw = new FileWriter(archiveFile)) { + fw.write(arr.toJSONString()); + } catch (Exception e) { + sendError("Fehler beim Archivieren: " + e.getMessage()); + return 0; + } + // Entferne archivierte Tickets aus aktivem Speicher + int removed = 0; + if (useMySQL) { + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement("DELETE FROM tickets WHERE id = ?")) { + for (Ticket t : toArchive) { + ps.setInt(1, t.getId()); + ps.executeUpdate(); + removed++; + } + } catch (Exception e) { + sendError("Fehler beim Entfernen archivierter Tickets: " + e.getMessage()); + } + } else { + for (Ticket t : toArchive) { + dataConfig.set("tickets." + t.getId(), null); + removed++; + } + try { dataConfig.save(dataFile); } catch (Exception e) { sendError("Fehler beim Speichern nach Archivierung: " + e.getMessage()); } + } + return removed; + } + /** + * Liefert Statistiken über Tickets. + */ + public TicketStats getTicketStats() { + List all = getAllTickets(); + int open = 0, closed = 0, forwarded = 0; + java.util.Map byPlayer = new java.util.HashMap<>(); + for (Ticket t : all) { + switch (t.getStatus()) { + case OPEN -> open++; + case CLOSED -> closed++; + case FORWARDED -> forwarded++; + } + byPlayer.merge(t.getCreatorName(), 1, Integer::sum); + } + return new TicketStats(all.size(), open, closed, forwarded, byPlayer); + } + + public static class TicketStats { + public final int total, open, closed, forwarded; + public final java.util.Map byPlayer; + public TicketStats(int total, int open, int closed, int forwarded, java.util.Map byPlayer) { + this.total = total; this.open = open; this.closed = closed; this.forwarded = forwarded; this.byPlayer = byPlayer; + } + } + /** + * Exportiert alle Tickets als JSON-Datei. + * @param exportFile Ziel-Datei + * @return Anzahl exportierter Tickets + */ + public int exportTickets(File exportFile) { + List tickets = getAllTickets(); + JSONArray arr = new JSONArray(); + for (Ticket t : tickets) { + arr.add(ticketToJson(t)); + } + try (FileWriter fw = new FileWriter(exportFile)) { + fw.write(arr.toJSONString()); + return tickets.size(); + } catch (IOException e) { + sendError("Fehler beim Export: " + e.getMessage()); + return 0; + } + } + + /** + * Importiert Tickets aus einer JSON-Datei. + * @param importFile Quell-Datei + * @return Anzahl importierter Tickets + */ + public int importTickets(File importFile) { + int imported = 0; + try (FileReader fr = new FileReader(importFile)) { + JSONParser parser = new JSONParser(); + JSONArray arr = (JSONArray) parser.parse(fr); + for (Object o : arr) { + JSONObject obj = (JSONObject) o; + Ticket t = ticketFromJson(obj); + if (t != null) { + int id = createTicket(t); + if (id != -1) imported++; + } + } + } catch (Exception e) { + sendError("Fehler beim Import: " + e.getMessage()); + } + return imported; + } + + /** + * Gibt alle Tickets (egal welcher Status) zurück. + */ + public List getAllTickets() { + List list = new ArrayList<>(); + if (useMySQL) { + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery("SELECT * FROM tickets"); + while (rs.next()) list.add(mapRow(rs)); + } catch (SQLException e) { + sendError("Fehler beim Abrufen aller Tickets: " + e.getMessage()); + } + } else { + if (dataConfig.contains("tickets")) { + for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { + Ticket t = (Ticket) dataConfig.get("tickets." + key); + if (t != null) list.add(t); + } + } + } + return list; + } + + // Hilfsmethoden für JSON-Konvertierung + private JSONObject ticketToJson(Ticket t) { + JSONObject obj = new JSONObject(); + obj.put("id", t.getId()); + obj.put("creatorUUID", t.getCreatorUUID().toString()); + obj.put("creatorName", t.getCreatorName()); + obj.put("message", t.getMessage()); + obj.put("world", t.getWorldName()); + obj.put("x", t.getX()); + obj.put("y", t.getY()); + obj.put("z", t.getZ()); + obj.put("yaw", t.getYaw()); + obj.put("pitch", t.getPitch()); + obj.put("status", t.getStatus().name()); + obj.put("createdAt", t.getCreatedAt() != null ? t.getCreatedAt().getTime() : null); + obj.put("claimedAt", t.getClaimedAt() != null ? t.getClaimedAt().getTime() : null); + obj.put("closedAt", t.getClosedAt() != null ? t.getClosedAt().getTime() : null); + if (t.getClaimerUUID() != null) obj.put("claimerUUID", t.getClaimerUUID().toString()); + if (t.getClaimerName() != null) obj.put("claimerName", t.getClaimerName()); + if (t.getForwardedToUUID() != null) obj.put("forwardedToUUID", t.getForwardedToUUID().toString()); + if (t.getForwardedToName() != null) obj.put("forwardedToName", t.getForwardedToName()); + return obj; + } + + private Ticket ticketFromJson(JSONObject obj) { + try { + Ticket t = new Ticket(); + t.setId(((Long)obj.get("id")).intValue()); + t.setCreatorUUID(UUID.fromString((String)obj.get("creatorUUID"))); + t.setCreatorName((String)obj.get("creatorName")); + t.setMessage((String)obj.get("message")); + t.setWorldName((String)obj.get("world")); + t.setX((Double)obj.get("x")); + t.setY((Double)obj.get("y")); + t.setZ((Double)obj.get("z")); + t.setYaw(((Double)obj.get("yaw")).floatValue()); + t.setPitch(((Double)obj.get("pitch")).floatValue()); + t.setStatus(TicketStatus.valueOf((String)obj.get("status"))); + if (obj.get("createdAt") != null) t.setCreatedAt(new java.sql.Timestamp((Long)obj.get("createdAt"))); + if (obj.get("claimedAt") != null) t.setClaimedAt(new java.sql.Timestamp((Long)obj.get("claimedAt"))); + if (obj.get("closedAt") != null) t.setClosedAt(new java.sql.Timestamp((Long)obj.get("closedAt"))); + if (obj.get("claimerUUID") != null) t.setClaimerUUID(UUID.fromString((String)obj.get("claimerUUID"))); + if (obj.get("claimerName") != null) t.setClaimerName((String)obj.get("claimerName")); + if (obj.get("forwardedToUUID") != null) t.setForwardedToUUID(UUID.fromString((String)obj.get("forwardedToUUID"))); + if (obj.get("forwardedToName") != null) t.setForwardedToName((String)obj.get("forwardedToName")); + return t; + } catch (Exception e) { + plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage()); + return null; + } + } + /** + * Migriert alle Tickets aus data.yml nach MySQL. + */ + public int migrateToMySQL() { + if (useMySQL || dataConfig == null) return 0; + int migrated = 0; + try { + for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { + Ticket t = (Ticket) dataConfig.get("tickets." + key); + if (t != null) { + // Ticket in MySQL speichern + useMySQL = true; + int id = createTicket(t); + useMySQL = false; + if (id != -1) migrated++; + } + } + } catch (Exception e) { + plugin.getLogger().severe("Fehler bei Migration zu MySQL: " + e.getMessage()); + } + return migrated; + } + + /** + * Migriert alle Tickets aus MySQL nach data.yml. + */ + public int migrateToFile() { + if (!useMySQL) return 0; + int migrated = 0; + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery("SELECT * FROM tickets"); + while (rs.next()) { + Ticket t = mapRow(rs); + if (t != null) { + useMySQL = false; + int id = createTicket(t); + useMySQL = true; + if (id != -1) migrated++; + } + } + } catch (Exception e) { + plugin.getLogger().severe("Fehler bei Migration zu Datei: " + e.getMessage()); + } + return migrated; + } + private String dataFileName; + private String archiveFileName; + // Prüft geladene Tickets auf Korrektheit (Platzhalter) + private void validateLoadedTickets() { + if (dataConfig == null || !dataConfig.contains("tickets")) return; + int invalid = 0; + for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { + Object obj = dataConfig.get("tickets." + key); + if (!(obj instanceof Ticket t)) { + sendError("Ungültiges Ticket-Objekt bei ID: " + key); + invalid++; + continue; + } + if (t.getCreatorUUID() == null || t.getCreatorName() == null || t.getMessage() == null || t.getStatus() == null) { + sendError("Ticket mit fehlenden Pflichtfeldern: ID " + key); + invalid++; + } + try { UUID.fromString(t.getCreatorUUID().toString()); } catch (Exception e) { + sendError("Ungültige UUID bei Ticket ID: " + key); + invalid++; + } + try { TicketStatus.valueOf(t.getStatus().name()); } catch (Exception e) { + sendError("Ungültiger Status bei Ticket ID: " + key); + invalid++; + } + } + if (invalid > 0) { + String msg = plugin != null ? plugin.formatMessage("messages.validation-warning").replace("{count}", String.valueOf(invalid)) : (invalid + " ungültige Tickets beim Laden gefunden."); + sendError(msg); + } + } + + // Backup der MySQL-Datenbank (Platzhalter) + private void backupMySQL() { + // TODO: Implementiere Backup-Logik für MySQL + } + + // Backup der Datei-basierten Daten (Platzhalter) + private void backupDataFile() { + // TODO: Implementiere Backup-Logik für data.yml/data.json + } + private final TicketPlugin plugin; + private HikariDataSource dataSource; + private boolean useMySQL; + private boolean useJson; + private File dataFile; + private YamlConfiguration dataConfig; + private JSONArray dataJson; + + public DatabaseManager(TicketPlugin plugin) { + this.plugin = plugin; + this.useMySQL = plugin.getConfig().getBoolean("use-mysql", true); + this.useJson = plugin.getConfig().getBoolean("use-json", false); + if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] DatabaseManager initialisiert. useMySQL=" + useMySQL + ", useJson=" + useJson); + // Speicherpfade aus config.yml (absolut oder relativ zum Plugin-Ordner) + String dataPath = plugin.getConfig().getString("data-file", useJson ? "data.json" : "data.yml"); + String archivePath = plugin.getConfig().getString("archive-file", "archive.json"); + this.dataFileName = dataPath; + this.archiveFileName = archivePath; + if (!useMySQL) { + if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] Datei-Speicher wird verwendet: " + dataPath); + if (useJson) { + dataFile = resolvePath(dataPath); + if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] JSON-Datei: " + dataFile.getAbsolutePath()); + if (!dataFile.exists()) { + try { + dataFile.getParentFile().mkdirs(); + dataFile.createNewFile(); + dataJson = new JSONArray(); + } catch (IOException e) { + sendError("Konnte " + dataPath + " nicht erstellen: " + e.getMessage()); + } + } else { + try { + JSONParser parser = new JSONParser(); + dataJson = (JSONArray) parser.parse(new java.io.FileReader(dataFile)); + } catch (Exception e) { + sendError("Konnte " + dataPath + " nicht laden: " + e.getMessage()); + dataJson = new JSONArray(); + } + } + } else { + dataFile = resolvePath(dataPath); + if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] YAML-Datei: " + dataFile.getAbsolutePath()); + if (!dataFile.exists()) { + try { + dataFile.getParentFile().mkdirs(); + dataFile.createNewFile(); + } catch (IOException e) { + sendError("Konnte " + dataPath + " nicht erstellen: " + e.getMessage()); + } + } + dataConfig = YamlConfiguration.loadConfiguration(dataFile); + } + validateLoadedTickets(); + } + } + + // Hilfsfunktion: Absoluten oder relativen Pfad auflösen + private File resolvePath(String path) { + File f = new File(path); + if (f.isAbsolute()) return f; + return new File(plugin.getDataFolder(), path); + } + + // Fehlerausgabe im Chat und Log + private void sendError(String msg) { + if (plugin != null) plugin.getLogger().severe(msg); + // Fehler an alle Admins im Chat senden + Bukkit.getOnlinePlayers().stream().filter(p -> p.hasPermission("ticket.admin")).forEach(p -> p.sendMessage("§c[TicketSystem] " + msg)); + } + + // ─────────────────────────── Verbindung ──────────────────────────────── + + public boolean connect() { + if (useMySQL) { + try { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s?useSSL=false&autoReconnect=true&characterEncoding=UTF-8", + plugin.getConfig().getString("mysql.host"), + plugin.getConfig().getInt("mysql.port"), + plugin.getConfig().getString("mysql.database"))); + config.setUsername(plugin.getConfig().getString("mysql.username")); + config.setPassword(plugin.getConfig().getString("mysql.password")); + config.setMaximumPoolSize(plugin.getConfig().getInt("mysql.pool-size", 10)); + config.setConnectionTimeout(plugin.getConfig().getLong("mysql.connection-timeout", 30000)); + config.setPoolName("TicketSystem-Pool"); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + + dataSource = new HikariDataSource(config); + createTables(); + plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt."); + return true; + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e); + plugin.getLogger().warning("Weiche auf Datei-Speicherung (data.yml) aus!"); + useMySQL = false; + // Datei-Storage initialisieren + dataFile = new File(plugin.getDataFolder(), "data.yml"); + if (!dataFile.exists()) { + try { + dataFile.getParentFile().mkdirs(); + dataFile.createNewFile(); + } catch (IOException ex) { + plugin.getLogger().severe("Konnte data.yml nicht erstellen: " + ex.getMessage()); + } + } + dataConfig = YamlConfiguration.loadConfiguration(dataFile); + return true; + } + } else { + plugin.getLogger().info("MySQL deaktiviert. Verwende Datei-Speicherung (data.yml)."); + return true; + } + } + + public void disconnect() { + if (useMySQL && dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + plugin.getLogger().info("MySQL-Verbindung getrennt."); + } + // Bei Datei-Storage nichts zu tun + } + + private Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } + + // ─────────────────────────── Tabellen erstellen ──────────────────────── + + private void createTables() { + String sql = """ + CREATE TABLE IF NOT EXISTS tickets ( + id INT AUTO_INCREMENT PRIMARY KEY, + creator_uuid VARCHAR(36) NOT NULL, + creator_name VARCHAR(16) NOT NULL, + message VARCHAR(255) NOT NULL, + world VARCHAR(64) NOT NULL, + x DOUBLE NOT NULL, + y DOUBLE NOT NULL, + z DOUBLE NOT NULL, + yaw FLOAT NOT NULL DEFAULT 0, + pitch FLOAT NOT NULL DEFAULT 0, + status VARCHAR(16) NOT NULL DEFAULT 'OPEN', + claimer_uuid VARCHAR(36), + claimer_name VARCHAR(16), + forwarded_to_uuid VARCHAR(36), + forwarded_to_name VARCHAR(16), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + claimed_at TIMESTAMP, + closed_at TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """; + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + stmt.execute(sql); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), e); + } + } + + // ─────────────────────────── CRUD ────────────────────────────────────── + + /** + * Speichert ein neues Ticket in der DB und gibt die generierte ID zurück. + */ + public int createTicket(Ticket ticket) { + if (useMySQL) { + String sql = """ + INSERT INTO tickets (creator_uuid, creator_name, message, world, x, y, z, yaw, pitch) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, ticket.getCreatorUUID().toString()); + ps.setString(2, ticket.getCreatorName()); + ps.setString(3, ticket.getMessage()); + ps.setString(4, ticket.getWorldName()); + ps.setDouble(5, ticket.getX()); + ps.setDouble(6, ticket.getY()); + ps.setDouble(7, ticket.getZ()); + ps.setFloat(8, ticket.getYaw()); + ps.setFloat(9, ticket.getPitch()); + ps.executeUpdate(); + + ResultSet rs = ps.getGeneratedKeys(); + if (rs.next()) { + backupMySQL(); + return rs.getInt(1); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen des Tickets: " + e.getMessage(), e); + } + return -1; + } else { + // Datei-Storage: Ticket-ID generieren + int id = dataConfig.getInt("lastId", 0) + 1; + ticket.setId(id); + dataConfig.set("lastId", id); + dataConfig.set("tickets." + id, ticket); + try { + dataConfig.save(dataFile); + backupDataFile(); + } catch (IOException e) { + plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); + Bukkit.getOnlinePlayers().stream().filter(p -> p.hasPermission("ticket.admin")).forEach(p -> p.sendMessage("§c[TicketSystem] Fehler beim Speichern von data.yml: " + e.getMessage())); + } + return id; + } + } + + /** + * Claimt ein Ticket (Status → CLAIMED). + */ + public boolean claimTicket(int ticketId, UUID claimerUUID, String claimerName) { + if (useMySQL) { + String sql = """ + UPDATE tickets SET status = 'CLAIMED', claimer_uuid = ?, claimer_name = ?, claimed_at = NOW() + WHERE id = ? AND status = 'OPEN' + """; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, claimerUUID.toString()); + ps.setString(2, claimerName); + ps.setInt(3, ticketId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Claimen des Tickets: " + e.getMessage(), e); + } + return false; + } else { + Ticket t = getTicketById(ticketId); + if (t == null || t.getStatus() != TicketStatus.OPEN) return false; + t.setStatus(TicketStatus.CLAIMED); + t.setClaimerUUID(claimerUUID); + t.setClaimerName(claimerName); + t.setClaimedAt(new java.sql.Timestamp(System.currentTimeMillis())); + dataConfig.set("tickets." + ticketId, t); + try { + dataConfig.save(dataFile); + backupDataFile(); + } catch (IOException e) { + plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); + } + return true; + } + } + + /** + * Schließt ein Ticket (Status → CLOSED). + */ + public boolean closeTicket(int ticketId) { + if (useMySQL) { + String sql = "UPDATE tickets SET status = 'CLOSED', closed_at = NOW() WHERE id = ? AND status != 'CLOSED'"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, ticketId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Schließen des Tickets: " + e.getMessage(), e); + } + return false; + } else { + Ticket t = getTicketById(ticketId); + if (t == null || t.getStatus() == TicketStatus.CLOSED) return false; + t.setStatus(TicketStatus.CLOSED); + t.setClosedAt(new java.sql.Timestamp(System.currentTimeMillis())); + dataConfig.set("tickets." + ticketId, t); + try { + dataConfig.save(dataFile); + backupDataFile(); + } catch (IOException e) { + plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); + } + return true; + } + } + + /** + * Leitet ein Ticket an einen anderen Supporter weiter (Status → FORWARDED). + */ + public boolean forwardTicket(int ticketId, UUID toUUID, String toName) { + if (useMySQL) { + String sql = """ + UPDATE tickets SET status = 'FORWARDED', forwarded_to_uuid = ?, forwarded_to_name = ? + WHERE id = ? AND status != 'CLOSED' + """; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, toUUID.toString()); + ps.setString(2, toName); + ps.setInt(3, ticketId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Weiterleiten des Tickets: " + e.getMessage(), e); + } + return false; + } else { + Ticket t = getTicketById(ticketId); + if (t == null || t.getStatus() == TicketStatus.CLOSED) return false; + t.setStatus(TicketStatus.FORWARDED); + t.setForwardedToUUID(toUUID); + t.setForwardedToName(toName); + dataConfig.set("tickets." + ticketId, t); + try { + dataConfig.save(dataFile); + backupDataFile(); + } catch (IOException e) { + plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); + } + return true; + } + } + + /** + * Gibt alle Tickets mit einem bestimmten Status zurück. + */ + public List getTicketsByStatus(TicketStatus... statuses) { + List list = new ArrayList<>(); + if (statuses.length == 0) return list; + if (useMySQL) { + StringBuilder placeholders = new StringBuilder("?"); + for (int i = 1; i < statuses.length; i++) placeholders.append(",?"); + String sql = "SELECT * FROM tickets WHERE status IN (" + placeholders + ") ORDER BY created_at ASC"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + for (int i = 0; i < statuses.length; i++) ps.setString(i + 1, statuses[i].name()); + 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 { + // Datei-Storage: Alle Tickets filtern + if (dataConfig.contains("tickets")) { + for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { + Ticket t = (Ticket) dataConfig.get("tickets." + key); + for (TicketStatus status : statuses) { + if (t != null && t.getStatus() == status) list.add(t); + } + } + } + return list; + } + } + + /** + * Gibt ein einzelnes Ticket anhand der ID zurück. + */ + public Ticket getTicketById(int id) { + if (useMySQL) { + String sql = "SELECT * FROM tickets WHERE id = ?"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, id); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return mapRow(rs); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Abrufen des Tickets: " + e.getMessage(), e); + } + return null; + } else { + if (dataConfig.contains("tickets." + id)) { + return (Ticket) dataConfig.get("tickets." + id); + } + return null; + } + } + + /** + * Anzahl offener Tickets (OPEN + FORWARDED) – für Join-Benachrichtigung. + */ + public int countOpenTickets() { + if (useMySQL) { + String sql = "SELECT COUNT(*) FROM tickets WHERE status IN ('OPEN', 'FORWARDED', 'CLAIMED')"; + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery(sql); + if (rs.next()) return rs.getInt(1); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Zählen der Tickets: " + e.getMessage(), e); + } + return 0; + } else { + int count = 0; + if (dataConfig.contains("tickets")) { + for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { + Ticket t = (Ticket) dataConfig.get("tickets." + key); + if (t != null && (t.getStatus() == TicketStatus.OPEN || t.getStatus() == TicketStatus.FORWARDED || t.getStatus() == TicketStatus.CLAIMED)) count++; + } + } + return count; + } + } + + /** + * Anzahl offener Tickets eines bestimmten Spielers. + */ + public int countOpenTicketsByPlayer(UUID uuid) { + if (useMySQL) { + String sql = "SELECT COUNT(*) FROM tickets WHERE creator_uuid = ? AND status IN ('OPEN', 'CLAIMED', 'FORWARDED')"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return rs.getInt(1); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler: " + e.getMessage(), e); + } + return 0; + } else { + int count = 0; + if (dataConfig.contains("tickets")) { + 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.OPEN || t.getStatus() == TicketStatus.CLAIMED || t.getStatus() == TicketStatus.FORWARDED)) count++; + } + } + return count; + } + } + + // ─────────────────────────── Mapping ─────────────────────────────────── + + private Ticket mapRow(ResultSet rs) throws SQLException { + File archiveFile = new File(plugin.getDataFolder(), archiveFileName); + Ticket t = new Ticket(); + t.setId(rs.getInt("id")); + t.setCreatorUUID(UUID.fromString(rs.getString("creator_uuid"))); + t.setCreatorName(rs.getString("creator_name")); + t.setMessage(rs.getString("message")); + t.setWorldName(rs.getString("world")); + t.setX(rs.getDouble("x")); + t.setY(rs.getDouble("y")); + t.setZ(rs.getDouble("z")); + t.setYaw(rs.getFloat("yaw")); + t.setPitch(rs.getFloat("pitch")); + t.setStatus(TicketStatus.valueOf(rs.getString("status"))); + t.setCreatedAt(rs.getTimestamp("created_at")); + t.setClaimedAt(rs.getTimestamp("claimed_at")); + t.setClosedAt(rs.getTimestamp("closed_at")); + + String claimerUUID = rs.getString("claimer_uuid"); + if (claimerUUID != null) { + t.setClaimerUUID(UUID.fromString(claimerUUID)); + t.setClaimerName(rs.getString("claimer_name")); + } + String fwdUUID = rs.getString("forwarded_to_uuid"); + if (fwdUUID != null) { + t.setForwardedToUUID(UUID.fromString(fwdUUID)); + t.setForwardedToName(rs.getString("forwarded_to_name")); + } + return t; + } +} diff --git a/src/main/java/de/ticketsystem/gui/TicketGUI.java b/src/main/java/de/ticketsystem/gui/TicketGUI.java new file mode 100644 index 0000000..9bf0ce4 --- /dev/null +++ b/src/main/java/de/ticketsystem/gui/TicketGUI.java @@ -0,0 +1,188 @@ +package de.ticketsystem.gui; + +import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.Ticket; +import de.ticketsystem.model.TicketStatus; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class TicketGUI implements Listener { + + private static final String GUI_TITLE = "§8§lTicket-Übersicht"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm"); + + private final TicketPlugin plugin; + + // Speichert welcher Spieler welches Ticket an welchem Slot hat + private final Map> playerSlotMap = new HashMap<>(); + + public TicketGUI(TicketPlugin plugin) { + this.plugin = plugin; + } + + // ─────────────────────────── GUI öffnen ──────────────────────────────── + + public void openGUI(Player player) { + List tickets = plugin.getDatabaseManager().getTicketsByStatus( + TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED); + + if (tickets.isEmpty()) { + player.sendMessage(plugin.formatMessage("messages.no-open-tickets")); + return; + } + + // Inventar-Größe: nächste Vielfaches von 9 (max. 54 Slots) + int size = Math.min(54, (int) (Math.ceil(tickets.size() / 9.0) * 9)); + if (size < 9) size = 9; + + Inventory inv = Bukkit.createInventory(null, size, GUI_TITLE); + Map slotMap = new HashMap<>(); + + for (int i = 0; i < tickets.size() && i < 54; i++) { + Ticket ticket = tickets.get(i); + ItemStack item = buildTicketItem(ticket); + inv.setItem(i, item); + slotMap.put(i, ticket); + } + + // Trennlinie am Ende, wenn Platz + fillEmpty(inv); + + playerSlotMap.put(player.getUniqueId(), slotMap); + player.openInventory(inv); + } + + // ─────────────────────────── Item bauen ──────────────────────────────── + + private ItemStack buildTicketItem(Ticket ticket) { + // Material je nach Status + Material mat; + switch (ticket.getStatus()) { + case OPEN -> mat = Material.PAPER; + case CLAIMED -> mat = Material.YELLOW_DYE; + case FORWARDED -> mat = Material.ORANGE_DYE; + default -> mat = Material.PAPER; + } + + ItemStack item = new ItemStack(mat); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + + // Display-Name + meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); + + // Lore aufbauen + 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("§7Welt: §e" + ticket.getWorldName()); + lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ())); + + if (ticket.getClaimerName() != null) { + lore.add("§8§m "); + lore.add("§7Geclaimt von: §a" + ticket.getClaimerName()); + if (ticket.getClaimedAt() != null) + lore.add("§7Geclaimt am: §a" + DATE_FORMAT.format(ticket.getClaimedAt())); + } + if (ticket.getForwardedToName() != null) { + lore.add("§7Weitergeleitet an: §6" + ticket.getForwardedToName()); + } + + lore.add("§8§m "); + if (ticket.getStatus() == TicketStatus.OPEN) { + lore.add("§a§l» KLICKEN zum Claimen & Teleportieren"); + } else { + lore.add("§e§l» KLICKEN zum Teleportieren"); + } + + meta.setLore(lore); + item.setItemMeta(meta); + return item; + } + + private void fillEmpty(Inventory inv) { + ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); + ItemMeta meta = glass.getItemMeta(); + if (meta != null) { meta.setDisplayName(" "); glass.setItemMeta(meta); } + for (int i = 0; i < inv.getSize(); i++) { + if (inv.getItem(i) == null) inv.setItem(i, glass); + } + } + + // ─────────────────────────── Klick-Event ─────────────────────────────── + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!event.getView().getTitle().equals(GUI_TITLE)) return; + + event.setCancelled(true); + + Map slotMap = playerSlotMap.get(player.getUniqueId()); + if (slotMap == null) return; + + int slot = event.getRawSlot(); + Ticket ticket = slotMap.get(slot); + if (ticket == null) return; + + player.closeInventory(); + + // Asynchron aus DB neu laden (aktuelle Daten) + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId()); + if (fresh == null) { + player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); + return; + } + + Bukkit.getScheduler().runTask(plugin, () -> handleTicketClick(player, fresh)); + }); + } + + private void handleTicketClick(Player player, Ticket ticket) { + // Versuche zu claimen, wenn noch OPEN + if (ticket.getStatus() == TicketStatus.OPEN) { + boolean success = plugin.getDatabaseManager().claimTicket( + ticket.getId(), player.getUniqueId(), player.getName()); + + if (success) { + ticket.setStatus(TicketStatus.CLAIMED); + ticket.setClaimerUUID(player.getUniqueId()); + ticket.setClaimerName(player.getName()); + + player.sendMessage(plugin.formatMessage("messages.ticket-claimed") + .replace("{id}", String.valueOf(ticket.getId())) + .replace("{player}", ticket.getCreatorName())); + + plugin.getTicketManager().notifyCreatorClaimed(ticket); + } else { + player.sendMessage(plugin.formatMessage("messages.already-claimed")); + } + } + + // Teleportation zur Ticket-Position + if (ticket.getLocation() != null) { + player.teleport(ticket.getLocation()); + player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert.")); + } else { + player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!")); + } + } +} diff --git a/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java b/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java new file mode 100644 index 0000000..420b829 --- /dev/null +++ b/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java @@ -0,0 +1,39 @@ +package de.ticketsystem.listeners; + +import de.ticketsystem.TicketPlugin; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +public class PlayerJoinListener implements Listener { + + private final TicketPlugin plugin; + + public PlayerJoinListener(TicketPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + + // Nur Supporter und Admins erhalten die Join-Benachrichtigung + if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) return; + + // Verzögerung von 2 Sekunden damit die Join-Sequenz abgeschlossen ist + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + 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.")); + }, 40L); // 40 Ticks = 2 Sekunden + } + }); + } +} diff --git a/src/main/java/de/ticketsystem/manager/TicketManager.java b/src/main/java/de/ticketsystem/manager/TicketManager.java new file mode 100644 index 0000000..33a2f6d --- /dev/null +++ b/src/main/java/de/ticketsystem/manager/TicketManager.java @@ -0,0 +1,116 @@ +package de.ticketsystem.manager; + +import de.ticketsystem.TicketPlugin; +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 → Zeit in Millis, wann das letzte Ticket erstellt wurde + 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 Online-Supporter und Admins über ein neues Ticket. + */ + public void notifyTeam(Ticket ticket) { + String msg = plugin.formatMessage("messages.new-ticket-notify") + .replace("{player}", ticket.getCreatorName()) + .replace("{message}", ticket.getMessage()) + .replace("{id}", String.valueOf(ticket.getId())); + + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) { + p.sendMessage(msg); + + // Klickbaren Hinweis senden (Bukkit Chat-Component) + p.sendMessage(plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen.")); + } + } + } + + /** + * Benachrichtigt den Ersteller des Tickets, wenn es geclaimt wurde. + */ + public void notifyCreatorClaimed(Ticket ticket) { + Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); + if (creator != null && creator.isOnline()) { + String msg = plugin.formatMessage("messages.ticket-claimed-notify") + .replace("{id}", String.valueOf(ticket.getId())) + .replace("{claimer}", ticket.getClaimerName()); + creator.sendMessage(msg); + } + } + + /** + * Sendet dem weitergeleiteten Supporter eine Benachrichtigung. + */ + public void notifyForwardedTo(Ticket ticket) { + Player target = Bukkit.getPlayer(ticket.getForwardedToUUID()); + if (target != null && target.isOnline()) { + String msg = plugin.formatMessage("messages.ticket-forwarded-notify") + .replace("{player}", ticket.getCreatorName()) + .replace("{id}", String.valueOf(ticket.getId())); + target.sendMessage(msg); + } + } + + // ─────────────────────────── Hilfsmethoden ───────────────────────────── + + /** + * Prüft, ob ein Spieler zu viele offene Tickets hat. + */ + 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 &7– Neues Ticket erstellen")); + if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.color("&e/ticket list &7– Ticket-Übersicht (GUI)")); + player.sendMessage(plugin.color("&e/ticket claim &7– Ticket annehmen")); + player.sendMessage(plugin.color("&e/ticket close &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 reload &7– Konfiguration neu laden")); + } + player.sendMessage(plugin.color("&8&m ")); + } +} diff --git a/src/main/java/de/ticketsystem/model/Ticket.java b/src/main/java/de/ticketsystem/model/Ticket.java new file mode 100644 index 0000000..8836c6e --- /dev/null +++ b/src/main/java/de/ticketsystem/model/Ticket.java @@ -0,0 +1,108 @@ +package de.ticketsystem.model; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; + +import java.sql.Timestamp; +import java.util.UUID; + +public class Ticket { + + private int id; + private UUID creatorUUID; + private String creatorName; + private String message; + + // Location-Felder (werden separat gespeichert) + private String worldName; + private double x, y, z; + private float yaw, pitch; + + private TicketStatus status; + private UUID claimerUUID; + private String claimerName; + private UUID forwardedToUUID; + private String forwardedToName; + private Timestamp createdAt; + private Timestamp claimedAt; + private Timestamp closedAt; + + public Ticket() {} + + public Ticket(UUID creatorUUID, String creatorName, String message, Location location) { + this.creatorUUID = creatorUUID; + this.creatorName = creatorName; + this.message = message; + this.worldName = location.getWorld().getName(); + this.x = location.getX(); + this.y = location.getY(); + this.z = location.getZ(); + this.yaw = location.getYaw(); + this.pitch = location.getPitch(); + this.status = TicketStatus.OPEN; + this.createdAt = new Timestamp(System.currentTimeMillis()); + } + + public Location getLocation() { + World world = Bukkit.getWorld(worldName); + if (world == null) return null; + return new Location(world, x, y, z, yaw, pitch); + } + + // ─────────────────────────── Getter & Setter ──────────────────────────── + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + + public UUID getCreatorUUID() { return creatorUUID; } + public void setCreatorUUID(UUID creatorUUID) { this.creatorUUID = creatorUUID; } + + public String getCreatorName() { return creatorName; } + public void setCreatorName(String creatorName) { this.creatorName = creatorName; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + + public String getWorldName() { return worldName; } + public void setWorldName(String worldName) { this.worldName = worldName; } + + public double getX() { return x; } + public void setX(double x) { this.x = x; } + + public double getY() { return y; } + public void setY(double y) { this.y = y; } + + public double getZ() { return z; } + public void setZ(double z) { this.z = z; } + + public float getYaw() { return yaw; } + public void setYaw(float yaw) { this.yaw = yaw; } + + public float getPitch() { return pitch; } + public void setPitch(float pitch) { this.pitch = pitch; } + + public TicketStatus getStatus() { return status; } + public void setStatus(TicketStatus status) { this.status = status; } + + public UUID getClaimerUUID() { return claimerUUID; } + public void setClaimerUUID(UUID claimerUUID) { this.claimerUUID = claimerUUID; } + + public String getClaimerName() { return claimerName; } + public void setClaimerName(String claimerName) { this.claimerName = claimerName; } + + public UUID getForwardedToUUID() { return forwardedToUUID; } + public void setForwardedToUUID(UUID forwardedToUUID) { this.forwardedToUUID = forwardedToUUID; } + + public String getForwardedToName() { return forwardedToName; } + public void setForwardedToName(String forwardedToName) { this.forwardedToName = forwardedToName; } + + public Timestamp getCreatedAt() { return createdAt; } + public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } + + public Timestamp getClaimedAt() { return claimedAt; } + public void setClaimedAt(Timestamp claimedAt) { this.claimedAt = claimedAt; } + + public Timestamp getClosedAt() { return closedAt; } + public void setClosedAt(Timestamp closedAt) { this.closedAt = closedAt; } +} diff --git a/src/main/java/de/ticketsystem/model/TicketStatus.java b/src/main/java/de/ticketsystem/model/TicketStatus.java new file mode 100644 index 0000000..6e8ba5d --- /dev/null +++ b/src/main/java/de/ticketsystem/model/TicketStatus.java @@ -0,0 +1,20 @@ +package de.ticketsystem.model; + +public enum TicketStatus { + OPEN("Offen", "§a"), + CLAIMED("Angenommen", "§e"), + FORWARDED("Weitergeleitet", "§6"), + CLOSED("Geschlossen", "§c"); + + private final String displayName; + private final String color; + + TicketStatus(String displayName, String color) { + this.displayName = displayName; + this.color = color; + } + + public String getDisplayName() { return displayName; } + public String getColor() { return color; } + public String getColored() { return color + displayName; } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..4b68bc3 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,94 @@ +# ============================================================ +# _____ _ _ _ ____ _ +# |_ _(_) ___| | _____| |_/ ___| _ _ ___| |_ ___ _ __ ___ +# | | | |/ __| |/ / _ \ __\___ \| | | / __| __/ _ \ '_ ` _ \ +# | | | | (__| < __/ |_ ___) | |_| \__ \ || __/ | | | | | +# |_| |_|\___|_|\_\___|\__|____/ \__, |___/\__\___|_| |_| |_| +# |___/ +# +# TicketSystem - Ein einfaches und effizientes Ticketsystem für Minecraft-Server +# Entwickelt von M_Viper +# ============================================================ + +# --- GRUNDLEGEND --- +# Version der Konfigurationsdatei. Nicht ändern! +version: "2.0" + +# Debug-Modus (true = Logs in der Konsole) +debug: false + +# ---------------------------------------------------- +# SPEICHERPFAD & ARCHIV +# ---------------------------------------------------- +data-file: "data.yml" # Datei für Tickets (YAML/JSON) +archive-file: "archive.yml" # Datei für Archiv (YAML/JSON) + +# ---------------------------------------------------- +# SPEICHER-MODUS +# ---------------------------------------------------- +use-mysql: false # true = MySQL, false = Datei +use-json: false # true = JSON, false = YAML (nur bei Datei) + +# ---------------------------------------------------- +# MYSQL-DATENBANK (Optional) +# ---------------------------------------------------- +mysql: + enabled: false + host: "localhost" + port: 3306 + database: "ticketsystem" + username: "root" + password: "password" + 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 +# ---------------------------------------------------- +ticket-cooldown: 60 # Cooldown in Sekunden zwischen Ticket-Erstellungen +max-description-length: 100 # Maximale Ticket-Beschreibungslänge +max-open-tickets-per-player: 2 # Maximale offene Tickets pro Spieler (0 = unbegrenzt) + +# ---------------------------------------------------- +# AUTOMATISCHE ARCHIVIERUNG +# ---------------------------------------------------- +auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus) + +# ---------------------------------------------------- +# 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." + + # --- 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." + + # --- 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}" + 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." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..8ced8e5 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,25 @@ +name: TicketSystem +version: 1.0.1 +main: de.ticketsystem.TicketPlugin +api-version: 1.20 +author: M_Viper +description: Ingame Support Ticket System with MySQL + +commands: + ticket: + description: Ticket System Hauptbefehl + usage: /ticket + aliases: [t, support] + +permissions: + ticket.create: + description: Spieler kann Tickets erstellen + default: true + + ticket.support: + description: Supporter kann Tickets einsehen und claimen + default: false + + ticket.admin: + description: Admin hat vollen Zugriff inkl. Weiterleitung und Reload + default: op diff --git a/src/test/java/de/ticketsystem/database/DatabaseManagerFileTest.java b/src/test/java/de/ticketsystem/database/DatabaseManagerFileTest.java new file mode 100644 index 0000000..b6ec02f --- /dev/null +++ b/src/test/java/de/ticketsystem/database/DatabaseManagerFileTest.java @@ -0,0 +1,74 @@ +package de.ticketsystem.database; + +import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.Ticket; +import de.ticketsystem.model.TicketStatus; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.*; +import java.io.File; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class DatabaseManagerFileTest { + // Dummy-Plugin-Implementierung für Tests (vermeidet Mockito) + private DatabaseManager db; + private File testFile; + private org.bukkit.configuration.file.YamlConfiguration config; + + @BeforeEach + void setUp() { + testFile = new File("build/testdata/testdata.yml"); + if (testFile.exists()) testFile.delete(); + config = new org.bukkit.configuration.file.YamlConfiguration(); + db = new DatabaseManager(testFile, config); + } + + @Test + void testCreateAndGetTicket() { + Ticket t = new Ticket(); + t.setCreatorUUID(UUID.randomUUID()); + t.setCreatorName("Tester"); + t.setMessage("Testnachricht"); + t.setWorldName("world"); + t.setX(1.0); + t.setY(2.0); + t.setZ(3.0); + t.setYaw(0); + t.setPitch(0); + t.setStatus(TicketStatus.OPEN); + t.setCreatedAt(new java.sql.Timestamp(System.currentTimeMillis())); + int id = db.createTicket(t); + assertTrue(id > 0); + Ticket loaded = db.getTicketById(id); + assertNotNull(loaded); + assertEquals("Tester", loaded.getCreatorName()); + } + + @Test + void testExportImportTickets() { + Ticket t = new Ticket(); + t.setCreatorUUID(UUID.randomUUID()); + t.setCreatorName("ExportUser"); + t.setMessage("ExportTest"); + t.setWorldName("world"); + t.setX(1.0); + t.setY(2.0); + t.setZ(3.0); + t.setYaw(0); + t.setPitch(0); + t.setStatus(TicketStatus.OPEN); + t.setCreatedAt(new java.sql.Timestamp(System.currentTimeMillis())); + db.createTicket(t); + File exportFile = new File("build/testdata/export.json"); + int exported = db.exportTickets(exportFile); + assertTrue(exported > 0); + db.importTickets(exportFile); // sollte keine Exception werfen + } + + @AfterEach + void tearDown() { + if (testFile.exists()) testFile.delete(); + new File("build/testdata/export.json").delete(); + } +}