diff --git a/src/main/java/com/viper/autosortchest/Main.java b/src/main/java/com/viper/autosortchest/Main.java index a96e293..7ee265b 100644 --- a/src/main/java/com/viper/autosortchest/Main.java +++ b/src/main/java/com/viper/autosortchest/Main.java @@ -47,7 +47,7 @@ import java.util.stream.Collectors; import com.viper.autosortchest.MySQLManager; -public class Main extends JavaPlugin implements Listener, CommandExecutor { +public class Main extends JavaPlugin implements Listener, CommandExecutor, org.bukkit.command.TabCompleter { private boolean serverCrosslink = true; @@ -138,9 +138,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { @Override public void run() { cleanMessageTracker(); - flushPlayerData(); // FIX: Async-Flush alle 60s statt synchronem Save bei jeder Änderung + flushPlayerData(); // Async-Flush alle 30s } - }.runTaskTimer(this, 20L * 60, 20L * 60); + }.runTaskTimer(this, 20L * 30, 20L * 30); // ── BungeeCord NEU: Heartbeat alle 30 Sekunden (async) ──────────────── if (mysqlEnabled && mysqlManager != null && !serverName.isEmpty()) { @@ -226,6 +226,10 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { // group → { "input", "rest", "target" } → limit private Map> chestLimits; private boolean chestLimitsEnabled = true; + // FIX: Cache für bereits migrierte Target-Einträge – verhindert wiederholte YAML-Lookups + // im heißen Sort-Loop (isOldTargetFormat wird nach einmaliger Migration nie wieder true). + private final java.util.Set migratedTargetItems = new java.util.HashSet<>(); + private final Map> fullChestMessageTracker = new HashMap<>(); private static final long MESSAGE_COOLDOWN = 5 * 60 * 1000; @@ -330,7 +334,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { private volatile boolean playerDataDirty = false; private volatile boolean saveInProgress = false; - private static final String CONFIG_VERSION = "2.4"; + private static final String CONFIG_VERSION = "2.5"; private boolean updateAvailable = false; private String latestVersion = ""; @@ -369,6 +373,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&f- &b/asc import &f- Importiert Daten aus players.yml in MySQL (OP).\n" + "&f- &b/asc export &f- Exportiert Daten aus MySQL in players.yml (OP).\n" + "&f- &b/asc list &f- Zeigt Truhen-Übersicht eines Spielers (Admin).\n" + + "&f- &b/asc autosign [item|hand]\n" + + " &7Setzt automatisch ein ASC-Schild an die angeschaute Truhe.\n" + + " &7Beispiele: &b/asc autosign ziel IRON_ORE &7| &b/asc autosign ziel hand\n" + "&6&l========================"; private static final String HELP_EN = @@ -405,6 +412,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&f- &b/asc import &f- Imports data from players.yml into MySQL (OP only).\n" + "&f- &b/asc export &f- Exports data from MySQL into players.yml (OP only).\n" + "&f- &b/asc list &f- Shows chest overview of a player (Admin).\n" + + "&f- &b/asc autosign [item|hand]\n" + + " &7Automatically places an ASC sign on the chest you are looking at.\n" + + " &7Examples: &b/asc autosign target IRON_ORE &7| &b/asc autosign target hand\n" + "&6&l========================"; private static final String INFO_DE = @@ -663,6 +673,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { getServer().getPluginManager().registerEvents(this, this); this.getCommand("asc").setExecutor(this); + this.getCommand("asc").setTabCompleter(this); // Mülltruchen-Manager initialisieren trashChestManager = new TrashChestManager(this); @@ -734,6 +745,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { getLogger().info("AutoSortChest Plugin deaktiviert!"); } + @EventHandler + public void onPlayerQuit(org.bukkit.event.player.PlayerQuitEvent event) { + // Verhindert dauerhaftes Wachsen der openCustomInventories Map + openCustomInventories.remove(event.getPlayer().getUniqueId()); + } + @EventHandler public void onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent event) { if (!updateAvailable) return; @@ -991,6 +1008,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } private void loadPlayerData() { + migratedTargetItems.clear(); // Cache leeren damit Migrationen korrekt erkannt werden if (playerDataFile == null) { playerDataFile = new File(getDataFolder(), "players.yml"); } @@ -1837,6 +1855,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { /** Prüft ob ein Target-Eintrag noch im alten Flat-Format gespeichert ist (direkt world/x/y/z unter item). */ private boolean isOldTargetFormat(UUID playerUUID, String itemName) { + // Cache-Hit: bereits migriert → sofort false zurückgeben ohne YAML-Zugriff + if (migratedTargetItems.contains(playerUUID + ":" + itemName)) return false; String path = "players." + playerUUID + ".target-chests." + itemName; return playerData.contains(path + ".world"); } @@ -1862,6 +1882,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { playerData.set(base + ".0.y", y); playerData.set(base + ".0.z", z); playerData.set(base + ".0.public", pub); + // Im Cache markieren – kein erneuter YAML-Lookup mehr nötig + migratedTargetItems.add(playerUUID + ":" + itemName); savePlayerData(); if (isDebug()) getLogger().info("[YAML-Migration] target-chests." + itemName + " → Slot 0 für " + playerUUID); } @@ -1875,8 +1897,24 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { // Auto-Migrierung falls altes Format if (isOldTargetFormat(playerUUID, itemName)) migrateOldTargetChestEntry(playerUUID, itemName); if (!playerData.isConfigurationSection(base)) return result; - for (String slotKey : playerData.getConfigurationSection(base).getKeys(false)) { - String p = base + "." + slotKey; + // Slots mit Prio sammeln und nach prio aufsteigend sortieren (Prio 1 zuerst, 0 = nicht gesetzt → ganz hinten) + List slotPrios = new ArrayList<>(); // [slot-index, prio] + List slotKeys = new ArrayList<>(playerData.getConfigurationSection(base).getKeys(false)); + for (String slotKey : slotKeys) { + int prio = playerData.getInt(base + "." + slotKey + ".prio", 0); + int slotIdx = 0; + try { slotIdx = Integer.parseInt(slotKey); } catch (NumberFormatException ignored) {} + slotPrios.add(new int[]{slotIdx, prio}); + } + // Prio 1 zuerst, Prio 2 danach usw. — Prio 0 (nicht gesetzt) kommt ans Ende. + // Bei gleicher Prio: niedrigster Slot zuerst. + slotPrios.sort((a, b) -> { + int pa = a[1] == 0 ? Integer.MAX_VALUE : a[1]; + int pb = b[1] == 0 ? Integer.MAX_VALUE : b[1]; + return pa != pb ? Integer.compare(pa, pb) : Integer.compare(a[0], b[0]); + }); + for (int[] sp : slotPrios) { + String p = base + "." + sp[0]; World w = Bukkit.getWorld(playerData.getString(p + ".world", "")); if (w == null) continue; result.add(new Location(w, playerData.getInt(p + ".x"), playerData.getInt(p + ".y"), playerData.getInt(p + ".z"))); @@ -2074,7 +2112,131 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!command.getName().equalsIgnoreCase("asc")) return false; - + if (args.length == 0) { + // Keine Argumente → Hilfe anzeigen (kein Crash durch args[0]) + String lang = config != null ? config.getString("language", "de") : "de"; + if (sender instanceof Player p) { + String helpMessage = "en".equalsIgnoreCase(lang) ? HELP_EN : HELP_DE; + p.sendMessage(ChatColor.translateAlternateColorCodes('&', helpMessage).split("\n")); + } else { + sender.sendMessage(ChatColor.RED + "Verwendung: /asc [reload|import|export|list]"); + } + return true; + } + // ------------------------------------------------------- + // /asc priority – Priorität für Zieltruhe setzen + // ------------------------------------------------------- + if (args[0].equalsIgnoreCase("priority")) { + if (!(sender instanceof Player)) { + sender.sendMessage(getMessage("priority-player-only")); + return true; + } + Player player = (Player) sender; + if (!player.hasPermission("autosortchest.use")) { + player.sendMessage(getMessage("no-permission")); + return true; + } + if (args.length < 2) { + player.sendMessage(getMessage("priority-usage")); + return true; + } + int prio = 0; + try { + prio = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + player.sendMessage(getMessage("priority-invalid-number").replace("%input%", args[1])); + return true; + } + if (prio < 1 || prio > 20) { + player.sendMessage(getMessage("priority-out-of-range")); + return true; + } + // Block, den der Spieler anschaut (max. 5 Blöcke) + Block targetBlock = player.getTargetBlockExact(5); + if (targetBlock == null || !(targetBlock.getState() instanceof Sign)) { + player.sendMessage(getMessage("priority-no-sign")); + return true; + } + Sign sign = (Sign) targetBlock.getState(); + // Prüfen, ob es ein ASC-Ziel-Schild ist + if (!isAscSign(sign, "target")) { + player.sendMessage(getMessage("priority-wrong-sign")); + return true; + } + // Zieltruhe zu diesem Schild finden (WallSign-API, nicht deprecated getData()) + Block attached = null; + if (targetBlock.getBlockData() instanceof WallSign wallSignBD) { + attached = targetBlock.getRelative(wallSignBD.getFacing().getOppositeFace()); + } + if (attached == null || !(attached.getState() instanceof Chest)) { + player.sendMessage(getMessage("priority-not-attached")); + return true; + } + Chest chest = (Chest) attached.getState(); + Location chestLoc = chest.getLocation(); + UUID playerUUID = player.getUniqueId(); + // Item-Typ ermitteln: + // Normales Schild → Index 2 (Zeile 3) enthält den Item-Namen (z.B. "IRON_ORE") + // Clean-Schild → Item steht nicht auf dem Schild, per DB/YAML ermitteln + String itemType = null; + if (isCleanTargetSign(sign)) { + itemType = findItemForChestLocation(playerUUID, chestLoc); + if (itemType == null || itemType.isEmpty()) { + player.sendMessage(getMessage("priority-item-not-found")); + return true; + } + } else { + String line2 = sign.getLine(2); // Index 2 = dritte Zeile = Item-Name + if (line2 != null && !line2.trim().isEmpty()) { + itemType = ChatColor.stripColor(line2).trim().toUpperCase().replace(' ', '_'); + } + } + if (itemType == null || itemType.isEmpty()) { + player.sendMessage(getMessage("priority-item-unknown")); + return true; + } + boolean found = false; + if (mysqlEnabled && mysqlManager != null) { + // MySQL: Zieltruhe suchen und Prio setzen + List> targets = mysqlManager.getTargetChestsForItem(playerUUID.toString(), itemType); + for (Map t : targets) { + String w = (String) t.get("world"); + int x = (int) t.get("x"); + int y = (int) t.get("y"); + int z = (int) t.get("z"); + int slot = (int) t.get("slot"); + if (w.equals(chestLoc.getWorld().getName()) && x == chestLoc.getBlockX() && y == chestLoc.getBlockY() && z == chestLoc.getBlockZ()) { + mysqlManager.setTargetChestPrio(playerUUID.toString(), itemType, slot, prio); + found = true; + break; + } + } + } else { + // YAML: Slot der Zieltruhe finden und Prio setzen + String base = "players." + playerUUID + ".target-chests." + itemType; + if (playerData.contains(base) && playerData.isConfigurationSection(base)) { + for (String slotKey : playerData.getConfigurationSection(base).getKeys(false)) { + String p = base + "." + slotKey; + if (playerData.getString(p + ".world", "").equals(chestLoc.getWorld().getName()) && + playerData.getInt(p + ".x") == chestLoc.getBlockX() && + playerData.getInt(p + ".y") == chestLoc.getBlockY() && + playerData.getInt(p + ".z") == chestLoc.getBlockZ()) { + playerData.set(p + ".prio", prio); + savePlayerData(); + flushPlayerData(); // sofort schreiben, nicht erst nach 30s + found = true; + break; + } + } + } + } + if (found) { + player.sendMessage(getMessage("priority-success").replace("%prio%", String.valueOf(prio))); + } else { + player.sendMessage(getMessage("priority-not-found")); + } + return true; + } String lang = config != null ? config.getString("language", "de") : "de"; if (lang == null) lang = "de"; @@ -2084,7 +2246,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { && !args[0].equalsIgnoreCase("import") && !args[0].equalsIgnoreCase("export") && !args[0].equalsIgnoreCase("list"))) { - sender.sendMessage(ChatColor.RED + "Dieser Befehl ist nur für Spieler! (Konsole: reload, import, export, list)"); + sender.sendMessage(getMessage("console-only")); return true; } } @@ -2192,12 +2354,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return true; } - boolean isEn = lang.equalsIgnoreCase("en"); - if (args.length < 2) { - sender.sendMessage(ChatColor.RED + (isEn - ? "Usage: /asc list " - : "Verwendung: /asc list ")); + sender.sendMessage(getMessage("list-usage")); return true; } @@ -2219,9 +2377,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } if (target == null) { - sender.sendMessage(ChatColor.RED + (isEn - ? "Player '" + targetName + "' was not found!" - : "Spieler '" + targetName + "' wurde nicht gefunden!")); + sender.sendMessage(getMessage("list-player-not-found").replace("%name%", targetName)); return true; } @@ -2229,6 +2385,18 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { final String uuidStr = finalTarget.getUniqueId().toString(); final String displayName = finalTarget.getName() != null ? finalTarget.getName() : targetName; + // Labels vorab aus config lesen (thread-sicher, da Main-Thread) + final String cfgHeader = getMessage("list-header"); + final String cfgTitle = getMessage("list-title"); + final String cfgPlayerLabel = getMessage("list-player-label"); + final String cfgOffline = getMessage("list-offline"); + final String cfgInputLabel = getMessage("list-input-label"); + final String cfgTargetLabel = getMessage("list-target-label"); + final String cfgRestLabel = getMessage("list-rest-label"); + final String cfgTrashLabel = getMessage("list-trash-label"); + final String cfgUnlimited = getMessage("list-unlimited"); + final String cfgFooter = getMessage("list-footer"); + // Zählen (async, da ggf. DB-Abfragen) new BukkitRunnable() { @Override @@ -2266,59 +2434,49 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { trashCount = playerData.contains("players." + uuidStr + ".trash-chest.world") ? 1 : 0; } - // Limits bestimmen - // Spieler ist offline → Limits können nicht per Permission geprüft werden - // → nur die Anzahl anzeigen, kein " / X" final boolean isOffline = !finalTarget.isOnline(); String inputMax, targetMax, restMax; final String trashMax = "1"; if (isOffline || !chestLimitsEnabled) { - // Offline: kein Limit anzeigen - // Limits deaktiviert: unbegrenzt - inputMax = isOffline ? null : "*"; - targetMax = isOffline ? null : "*"; - restMax = isOffline ? null : "*"; + inputMax = isOffline ? null : cfgUnlimited; + targetMax = isOffline ? null : cfgUnlimited; + restMax = isOffline ? null : cfgUnlimited; } else { Player onlineTarget = (Player) finalTarget; if (onlineTarget.isOp() || onlineTarget.hasPermission("autosortchest.limit.bypass")) { - inputMax = "*"; - targetMax = "*"; - restMax = "*"; + inputMax = cfgUnlimited; + targetMax = cfgUnlimited; + restMax = cfgUnlimited; } else { int iMax = getChestLimitForPlayer(onlineTarget, "input"); int tMax = getChestLimitForPlayer(onlineTarget, "target"); int rMax = getChestLimitForPlayer(onlineTarget, "rest"); - inputMax = (iMax == Integer.MAX_VALUE) ? "*" : String.valueOf(iMax); - targetMax = (tMax == Integer.MAX_VALUE) ? "*" : String.valueOf(tMax); - restMax = (rMax == Integer.MAX_VALUE) ? "*" : String.valueOf(rMax); + inputMax = (iMax == Integer.MAX_VALUE) ? cfgUnlimited : String.valueOf(iMax); + targetMax = (tMax == Integer.MAX_VALUE) ? cfgUnlimited : String.valueOf(tMax); + restMax = (rMax == Integer.MAX_VALUE) ? cfgUnlimited : String.valueOf(rMax); } } - // Sprachabhängige Label - final String labelPlayer = isEn ? "Player: " : "Spieler: "; - final String labelTarget = isEn ? "Target: " : "Ziel: "; - final String labelTrash = isEn ? "Trash: " : "Müll: "; - final String fInputMax = inputMax, fTargetMax = targetMax, fRestMax = restMax; final int fIn = inputCount, fTa = targetCount, fRe = restCount, fTr = trashCount; new BukkitRunnable() { @Override public void run() { - sender.sendMessage(ChatColor.GOLD + "================================"); - sender.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "==== AutoSortChest Info ===="); - sender.sendMessage(ChatColor.YELLOW + labelPlayer + ChatColor.WHITE + displayName - + (isOffline ? ChatColor.GRAY + " (offline)" : "")); - sender.sendMessage(ChatColor.YELLOW + "Input: " + ChatColor.WHITE + sender.sendMessage(cfgHeader); + sender.sendMessage(cfgTitle); + sender.sendMessage(cfgPlayerLabel + ChatColor.WHITE + displayName + + (isOffline ? cfgOffline : "")); + sender.sendMessage(cfgInputLabel + ChatColor.WHITE + (fInputMax != null ? fIn + " / " + fInputMax : String.valueOf(fIn))); - sender.sendMessage(ChatColor.YELLOW + labelTarget + ChatColor.WHITE + sender.sendMessage(cfgTargetLabel + ChatColor.WHITE + (fTargetMax != null ? fTa + " / " + fTargetMax : String.valueOf(fTa))); - sender.sendMessage(ChatColor.YELLOW + "Rest: " + ChatColor.WHITE + sender.sendMessage(cfgRestLabel + ChatColor.WHITE + (fRestMax != null ? fRe + " / " + fRestMax : String.valueOf(fRe))); - sender.sendMessage(ChatColor.YELLOW + labelTrash + ChatColor.WHITE + sender.sendMessage(cfgTrashLabel + ChatColor.WHITE + (isOffline ? String.valueOf(fTr) : fTr + " / " + trashMax)); - sender.sendMessage(ChatColor.GOLD + "================================"); + sender.sendMessage(cfgFooter); } }.runTask(Main.this); } @@ -2335,17 +2493,17 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return true; } if (!mysqlEnabled || mysqlManager == null) { - sender.sendMessage(ChatColor.RED + "MySQL ist nicht aktiviert! Aktiviere MySQL in der config.yml zuerst."); + sender.sendMessage(getMessage("mysql-not-enabled-import")); return true; } if (playerData == null || playerData.getConfigurationSection("players") == null || playerData.getConfigurationSection("players").getKeys(false).isEmpty()) { - sender.sendMessage(ChatColor.RED + "Die players.yml ist leer oder enthält keine Spielerdaten!"); + sender.sendMessage(getMessage("yaml-empty")); return true; } - sender.sendMessage(ChatColor.YELLOW + "Importiere Daten aus players.yml nach MySQL..."); - sender.sendMessage(ChatColor.GRAY + "Bestehende MySQL-Daten werden nicht überschrieben (REPLACE INTO)."); + sender.sendMessage(getMessage("import-start")); + sender.sendMessage(getMessage("import-info")); new BukkitRunnable() { @Override @@ -2439,11 +2597,11 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { final int fp = playerCount, fi = inputCount, ft = targetCount, fr = restCount; Bukkit.getScheduler().runTask(Main.this, () -> { - sender.sendMessage(ChatColor.GREEN + "Import erfolgreich abgeschlossen!"); - sender.sendMessage(ChatColor.GRAY + " Spieler: " + ChatColor.WHITE + fp); - sender.sendMessage(ChatColor.GRAY + " Eingangstruhen: " + ChatColor.WHITE + fi); - sender.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft); - sender.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr); + sender.sendMessage(getMessage("import-success")); + sender.sendMessage(getMessage("import-stats-players").replace("%players%", String.valueOf(fp))); + sender.sendMessage(getMessage("import-stats-input") .replace("%input%", String.valueOf(fi))); + sender.sendMessage(getMessage("import-stats-target").replace("%target%", String.valueOf(ft))); + sender.sendMessage(getMessage("import-stats-rest") .replace("%rest%", String.valueOf(fr))); getLogger().info("Import durch " + sender.getName() + " abgeschlossen: " + fp + " Spieler, " + fi + " Input, " + ft + " Target, " + fr + " Rest."); }); @@ -2461,12 +2619,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return true; } if (!mysqlEnabled || mysqlManager == null) { - sender.sendMessage(ChatColor.RED + "MySQL ist nicht aktiviert! Der Export benötigt eine aktive MySQL-Verbindung."); + sender.sendMessage(getMessage("mysql-not-enabled-export")); return true; } - sender.sendMessage(ChatColor.YELLOW + "Exportiere Daten aus MySQL nach players.yml..."); - sender.sendMessage(ChatColor.GRAY + "Ein Backup der aktuellen players.yml wird erstellt."); + sender.sendMessage(getMessage("export-start")); + sender.sendMessage(getMessage("export-info")); new BukkitRunnable() { @Override @@ -2483,8 +2641,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { try { java.nio.file.Files.copy(playerDataFile.toPath(), backupFile.toPath()); } catch (IOException e) { + final String errMsg = e.getMessage(); Bukkit.getScheduler().runTask(Main.this, () -> - sender.sendMessage(ChatColor.RED + "Backup fehlgeschlagen: " + e.getMessage())); + sender.sendMessage(getMessage("backup-failed").replace("%error%", errMsg))); return; } } @@ -2515,7 +2674,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { List> targetChests = mysqlManager.getTargetChests(uuidString); for (Map chest : targetChests) { String item = (String) chest.get("item"); - // FIX: export in new slotted format (slot 0) String path = "players." + uuidString + ".target-chests." + item + ".0"; exportData.set(path + ".world", chest.get("world")); exportData.set(path + ".x", chest.get("x")); @@ -2540,30 +2698,31 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { exportData.save(playerDataFile); - final FileConfiguration finalExport = exportData; + final FileConfiguration finalExport = exportData; final int fp = playerCount, fi = inputCount, ft = targetCount, fr = restCount; final String finalBackupName = backupName; Bukkit.getScheduler().runTask(Main.this, () -> { playerData = finalExport; - sender.sendMessage(ChatColor.GREEN + "Export erfolgreich abgeschlossen!"); + sender.sendMessage(getMessage("export-success")); if (finalBackupName != null) { - sender.sendMessage(ChatColor.GRAY + " Backup: " + ChatColor.WHITE + finalBackupName); + sender.sendMessage(getMessage("export-backup").replace("%file%", finalBackupName)); } else { - sender.sendMessage(ChatColor.GRAY + " Backup: " + ChatColor.DARK_GRAY + "Übersprungen (players.yml war leer)"); + sender.sendMessage(getMessage("export-backup-skipped")); } - sender.sendMessage(ChatColor.GRAY + " Spieler: " + ChatColor.WHITE + fp); - sender.sendMessage(ChatColor.GRAY + " Eingangstruhen: " + ChatColor.WHITE + fi); - sender.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft); - sender.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr); + sender.sendMessage(getMessage("import-stats-players").replace("%players%", String.valueOf(fp))); + sender.sendMessage(getMessage("import-stats-input") .replace("%input%", String.valueOf(fi))); + sender.sendMessage(getMessage("import-stats-target").replace("%target%", String.valueOf(ft))); + sender.sendMessage(getMessage("import-stats-rest") .replace("%rest%", String.valueOf(fr))); getLogger().info("Export durch " + sender.getName() + " abgeschlossen: " + fp + " Spieler, " + fi + " Input, " + ft + " Target, " + fr + " Rest." + (finalBackupName != null ? " Backup: " + finalBackupName : " Kein Backup.")); }); } catch (Exception e) { + final String errMsg = e.getMessage(); Bukkit.getScheduler().runTask(Main.this, () -> - sender.sendMessage(ChatColor.RED + "Export fehlgeschlagen: " + e.getMessage())); + sender.sendMessage(getMessage("export-error").replace("%error%", errMsg))); getLogger().warning("Export fehlgeschlagen: " + e.getMessage()); } } @@ -2571,6 +2730,25 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return true; } + // ------------------------------------------------------- + // /asc autosign – Schild automatisch an Truhe setzen + // ------------------------------------------------------- + if (args[0].equalsIgnoreCase("autosign")) { + if (player == null) { + sender.sendMessage(getMessage("autosign-player-only")); + return true; + } + if (!player.hasPermission("autosortchest.use")) { + player.sendMessage(getMessage("no-permission")); + return true; + } + if (args.length < 2) { + player.sendMessage(getMessage("autosign-usage")); + return true; + } + return handleAutoSign(player, args); + } + // Unbekannter Befehl → Hilfe (nur für Spieler) if (player == null) { sender.sendMessage(ChatColor.RED + "Verwendung: /asc [reload|import|export]"); @@ -2582,6 +2760,416 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return true; } + // ═══════════════════════════════════════════════════════════════════════ + // TAB COMPLETER + // ═══════════════════════════════════════════════════════════════════════ + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (!command.getName().equalsIgnoreCase("asc")) return Collections.emptyList(); + + List result = new ArrayList<>(); + + if (args.length == 1) { + // /asc + List subs = new ArrayList<>(Arrays.asList( + "help", "info", "autosign", "priority")); + if (sender.hasPermission("autosortchest.reload")) subs.add("reload"); + if (sender.hasPermission("autosortchest.import")) subs.add("import"); + if (sender.hasPermission("autosortchest.export")) subs.add("export"); + if (sender.hasPermission("autosortchest.list")) subs.add("list"); + String partial = args[0].toLowerCase(); + for (String s : subs) { + if (s.toLowerCase().startsWith(partial)) result.add(s); + } + Collections.sort(result); + + } else if (args.length == 2) { + + if (args[0].equalsIgnoreCase("autosign")) { + // /asc autosign + String lang = config != null ? config.getString("language", "de") : "de"; + List types; + if ("en".equalsIgnoreCase(lang)) { + types = Arrays.asList("input", "target", "rest", "trash"); + } else { + types = Arrays.asList("input", "ziel", "rest", "trash"); + } + String partial = args[1].toLowerCase(); + for (String t : types) { + if (t.startsWith(partial)) result.add(t); + } + + } else if (args[0].equalsIgnoreCase("list")) { + // /asc list + String partial = args[1].toLowerCase(); + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.getName().toLowerCase().startsWith(partial)) result.add(p.getName()); + } + } else if (args[0].equalsIgnoreCase("priority")) { + // /asc priority + String partial = args[1]; + for (int i = 1; i <= 20; i++) { + String s = String.valueOf(i); + if (s.startsWith(partial)) result.add(s); + } + } + + } else if (args.length == 3 + && args[0].equalsIgnoreCase("autosign") + && (args[1].equalsIgnoreCase("ziel") || args[1].equalsIgnoreCase("target"))) { + // /asc autosign ziel + String partial = args[2].toLowerCase(); + result.add("hand"); + for (Material mat : Material.values()) { + if (mat.isAir() || !mat.isItem()) continue; + String name = mat.name().toLowerCase(); + if (name.startsWith(partial)) { + result.add(name); + if (result.size() >= 60) break; // Lag-Schutz + } + } + result = result.stream() + .filter(s -> s.startsWith(partial)) + .sorted() + .collect(Collectors.toList()); + } + + return result; + } + + // ═══════════════════════════════════════════════════════════════════════ + // /asc autosign – Implementierung + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Implementiert /asc autosign – platziert automatisch ein ASC-Schild + * an der Truhe, auf die der Spieler gerade schaut (max. 5 Blöcke). + * + * Syntax: /asc autosign [item|hand] + * – input / rest / trash : kein weiteres Argument nötig + * – ziel / target : [item] = Material-Name (z.B. IRON_ORE) + * [hand] = nimmt das Item in der Haupthand + * (kein Argument) = wie "hand" + */ + private boolean handleAutoSign(Player player, String[] args) { + + // ── 1. Ziel-Block ermitteln ────────────────────────────────────────── + Block targetBlock = player.getTargetBlockExact(5); + if (targetBlock == null || !(targetBlock.getState() instanceof Chest)) { + player.sendMessage(getMessage("autosign-no-chest")); + return true; + } + if (isWorldBlacklisted(targetBlock.getWorld())) { + player.sendMessage(getMessage("world-blacklisted")); + return true; + } + + // ── 2. Typ normalisieren ───────────────────────────────────────────── + String typRaw = args[1].toLowerCase(); + switch (typRaw) { + case "target": typRaw = "ziel"; break; + case "müll": case "muell": typRaw = "trash"; break; + default: break; + } + if (!typRaw.equals("input") && !typRaw.equals("ziel") + && !typRaw.equals("rest") && !typRaw.equals("trash")) { + player.sendMessage(getMessage("autosign-invalid-type")); + return true; + } + final String typ = typRaw; + + // ── 3. Material für Zieltruhe ermitteln ────────────────────────────── + Material targetMaterial = null; + if (typ.equals("ziel")) { + if (args.length >= 3 && !args[2].equalsIgnoreCase("hand")) { + // Expliziter Material-Name angegeben + targetMaterial = Material.matchMaterial(args[2].toUpperCase()); + if (targetMaterial == null || targetMaterial == Material.AIR) { + player.sendMessage(getMessage("autosign-unknown-item").replace("%item%", args[2])); + return true; + } + } else { + // Item in Haupthand verwenden + ItemStack handItem = player.getInventory().getItemInMainHand(); + if (handItem == null || handItem.getType() == Material.AIR) { + player.sendMessage(getMessage("no-item-in-hand")); + return true; + } + targetMaterial = handItem.getType(); + } + } + final Material finalMaterial = targetMaterial; + + + // ── 4. Freie Schildfläche an der Truhe suchen (Blickrichtung bevorzugen) ── + Chest chestState = (Chest) targetBlock.getState(); + List chestBlocks = getChestBlocks(chestState); + + org.bukkit.block.BlockFace[] faces = { + org.bukkit.block.BlockFace.NORTH, org.bukkit.block.BlockFace.SOUTH, + org.bukkit.block.BlockFace.EAST, org.bukkit.block.BlockFace.WEST + }; + + Block signBlock = null; + org.bukkit.block.BlockFace signFacing = null; + Block chestBlock = null; + + // Blickrichtung des Spielers zur Truhe bestimmen (von Truhe zum Spieler!) + org.bukkit.util.Vector eye = player.getEyeLocation().toVector(); + org.bukkit.util.Vector chest = targetBlock.getLocation().add(0.5, 0.5, 0.5).toVector(); + org.bukkit.util.Vector dir = eye.clone().subtract(chest).normalize(); + double maxDot = -2.0; + org.bukkit.block.BlockFace facingFace = null; + for (org.bukkit.block.BlockFace face : faces) { + org.bukkit.util.Vector faceVec = new org.bukkit.util.Vector(face.getModX(), face.getModY(), face.getModZ()); + double dot = dir.dot(faceVec); + if (dot > maxDot) { + maxDot = dot; + facingFace = face; + } + } + + // Zuerst versuchen, auf die Seite zu setzen, die der Spieler anschaut + outer: + for (Block cb : chestBlocks) { + Block adj = cb.getRelative(facingFace); + if (!(adj.getState() instanceof Sign existSign && isSignAttachedToChest(adj, cb)) && + (adj.getType() == Material.AIR || adj.getType() == Material.CAVE_AIR || adj.getType() == Material.VOID_AIR)) { + signBlock = adj; + signFacing = facingFace; + chestBlock = cb; + break outer; + } + } + + // Falls dort kein Platz ist, wie bisher freie Seite suchen + if (signBlock == null) { + outer2: + for (Block cb : chestBlocks) { + for (org.bukkit.block.BlockFace face : faces) { + Block adj = cb.getRelative(face); + if (adj.getState() instanceof Sign existSign && isSignAttachedToChest(adj, cb)) { + continue; + } + if (adj.getType() == Material.AIR || adj.getType() == Material.CAVE_AIR || adj.getType() == Material.VOID_AIR) { + signBlock = adj; + signFacing = face; + chestBlock = cb; + break outer2; + } + } + } + } + + if (signBlock == null) { + player.sendMessage(getMessage("autosign-no-space")); + return true; + } + + final Block finalChestBlock = chestBlock; + UUID playerUUID = player.getUniqueId(); + + // ── 5. Limit-Prüfung ──────────────────────────────────────────────── + if (chestLimitsEnabled + && !isAdmin(player) + && !player.hasPermission("autosortchest.limit.bypass")) { + + switch (typ) { + case "input": { + int maxInput = getChestLimitForPlayer(player, "input"); + if (maxInput == 0) { + player.sendMessage(getMessage("limit-no-permission")); + return true; + } + int currentInput = autoSignCountInputChests(playerUUID); + boolean alreadyInput = autoSignIsAlreadyInputChest(playerUUID, finalChestBlock); + if (!alreadyInput && currentInput >= maxInput) { + player.sendMessage(getMessage("limit-input-reached") + .replace("%max%", String.valueOf(maxInput))); + return true; + } + break; + } + case "rest": { + int maxRest = getChestLimitForPlayer(player, "rest"); + if (maxRest == 0) { + player.sendMessage(getMessage("limit-no-permission")); + return true; + } + int currentRest = autoSignCountRestChests(playerUUID); + if (currentRest >= maxRest) { + player.sendMessage(getMessage("limit-rest-reached") + .replace("%max%", String.valueOf(maxRest))); + return true; + } + break; + } + case "ziel": { + int maxChests = getChestLimitForPlayer(player, "target"); + int maxPerItem = getChestLimitForPlayer(player, "target_per_item"); + if (maxChests == 0) { + player.sendMessage(getMessage("limit-no-permission")); + return true; + } + Set uniqueLocs = new HashSet<>(); + int countForThisItem = 0; + String thisLocKey = finalChestBlock.getWorld().getName() + ":" + + finalChestBlock.getX() + ":" + finalChestBlock.getY() + + ":" + finalChestBlock.getZ(); + + if (mysqlEnabled && mysqlManager != null) { + for (Map map : mysqlManager.getTargetChests(playerUUID.toString())) { + String w = (String) map.get("world"); + int tx = (int) map.get("x"), ty = (int) map.get("y"), tz = (int) map.get("z"); + World bw = Bukkit.getWorld(w); + if (bw != null && bw.getBlockAt(tx, ty, tz).getState() instanceof Chest) { + uniqueLocs.add(w + ":" + tx + ":" + ty + ":" + tz); + if (finalMaterial.name().equals(map.get("item"))) countForThisItem++; + } + } + } else { + String basePath = "players." + playerUUID + ".target-chests"; + if (playerData.contains(basePath)) { + for (String item : playerData.getConfigurationSection(basePath).getKeys(false)) { + for (Location slotLoc : getTargetChestSlotsYaml(playerUUID, item)) { + if (slotLoc == null || slotLoc.getWorld() == null) continue; + int tx = slotLoc.getBlockX(), ty = slotLoc.getBlockY(), tz = slotLoc.getBlockZ(); + World bw = slotLoc.getWorld(); + if (bw.getBlockAt(tx, ty, tz).getState() instanceof Chest) { + uniqueLocs.add(bw.getName() + ":" + tx + ":" + ty + ":" + tz); + if (finalMaterial.name().equals(item)) countForThisItem++; + } + } + } + } + } + + boolean alreadyTarget = uniqueLocs.contains(thisLocKey); + if (!alreadyTarget && uniqueLocs.size() >= maxChests) { + player.sendMessage(getMessage("limit-target-reached") + .replace("%max%", String.valueOf(maxChests))); + return true; + } + if (!alreadyTarget && countForThisItem >= maxPerItem) { + player.sendMessage(getMessage("limit-target-per-item") + .replace("%max%", String.valueOf(maxPerItem)) + .replace("%item%", finalMaterial.name())); + return true; + } + break; + } + case "trash": { + // Trash-Limit: kein eigenes Limit-System, nur Permission-Check + if (chestLimitsEnabled + && getChestLimitForPlayer(player, "input") == 0 + && getChestLimitForPlayer(player, "target") == 0) { + player.sendMessage(getMessage("limit-no-permission")); + return true; + } + break; + } + } + } + + // ── 6. Schild-Block setzen ─────────────────────────────────────────── + signBlock.setType(Material.OAK_WALL_SIGN); + org.bukkit.block.data.type.WallSign wallSignData = + (org.bukkit.block.data.type.WallSign) signBlock.getBlockData(); + wallSignData.setFacing(signFacing); + signBlock.setBlockData(wallSignData); + + BlockState rawState = signBlock.getState(); + if (!(rawState instanceof Sign)) { + player.sendMessage(getMessage("autosign-place-error")); + signBlock.setType(Material.AIR); + return true; + } + Sign sign = (Sign) rawState; + + // ── 7. Schildtext + Registrierung ──────────────────────────────────── + Location chestLoc = finalChestBlock.getLocation(); + switch (typ) { + case "input": { + updateSignToCurrentStyle(sign, "input", null, player.getName(), false, false); + sign.update(); + setInputChestLocation(playerUUID, chestLoc); + player.sendMessage(getMessage("input-chest-set")); + break; + } + case "ziel": { + String itemDisplay = TrashChestManager.formatMaterialName(finalMaterial.name()); + if (itemDisplay.length() > 15) itemDisplay = itemDisplay.substring(0, 15); + updateSignToCurrentStyle(sign, "target", itemDisplay, player.getName(), false, false); + sign.update(); + setTargetChestLocation(playerUUID, chestLoc, finalMaterial); + player.sendMessage(getMessage("target-chest-set").replace("%item%", finalMaterial.name())); + break; + } + case "rest": { + updateSignToCurrentStyle(sign, "rest", null, player.getName(), false, false); + sign.update(); + setRestChestLocation(playerUUID, chestLoc); + player.sendMessage(getMessage("rest-chest-set")); + break; + } + case "trash": { + updateSignToCurrentStyle(sign, "trash", null, player.getName(), false, false); + sign.update(); + trashChestManager.setTrashChestLocation(playerUUID, chestLoc); + player.sendMessage(getMessage("trash-chest-set")); + player.sendMessage(getMessage("trash-chest-hint")); + break; + } + } + return true; + } + + // ── Hilfs-Methoden für Limit-Prüfung in handleAutoSign ─────────────────── + + private int autoSignCountInputChests(UUID playerUUID) { + if (mysqlEnabled && mysqlManager != null) { + return mysqlManager.getInputChests(playerUUID.toString()).size(); + } + String basePath = "players." + playerUUID + ".input-chests"; + return playerData.contains(basePath) + ? playerData.getConfigurationSection(basePath).getKeys(false).size() : 0; + } + + private boolean autoSignIsAlreadyInputChest(UUID playerUUID, Block chestBlock) { + if (mysqlEnabled && mysqlManager != null) { + return mysqlManager.getInputChests(playerUUID.toString()).stream().anyMatch(c -> { + String w = (String) c.get("world"); + return w != null && w.equals(chestBlock.getWorld().getName()) + && (int) c.get("x") == chestBlock.getX() + && (int) c.get("y") == chestBlock.getY() + && (int) c.get("z") == chestBlock.getZ(); + }); + } + String basePath = "players." + playerUUID + ".input-chests"; + if (playerData.contains(basePath)) { + for (String id : playerData.getConfigurationSection(basePath).getKeys(false)) { + String p = basePath + "." + id; + if (chestBlock.getWorld().getName().equals(playerData.getString(p + ".world")) + && chestBlock.getX() == playerData.getInt(p + ".x") + && chestBlock.getY() == playerData.getInt(p + ".y") + && chestBlock.getZ() == playerData.getInt(p + ".z")) return true; + } + } + return false; + } + + private int autoSignCountRestChests(UUID playerUUID) { + if (mysqlEnabled && mysqlManager != null) { + return mysqlManager.getRestChests(playerUUID.toString()).size(); + } + String basePath = "players." + playerUUID + ".rest-chests"; + if (playerData.contains(basePath) && playerData.isConfigurationSection(basePath)) { + return playerData.getConfigurationSection(basePath).getKeys(false).size(); + } + return 0; + } + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onSignChange(SignChangeEvent event) { Player player = event.getPlayer(); @@ -3574,8 +4162,20 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } + // MySQL-Fallback: Besitzer anhand der Location direkt in der DB suchen. + // Notwendig wenn players.yml leer ist (MySQL-Modus) oder der Name-Lookup fehlschlug. + if (ownerUUID == null && mysqlEnabled && mysqlManager != null) { + ownerUUID = mysqlManager.findOwnerByLocation( + chestLoc.getWorld().getName(), + chestLoc.getBlockX(), chestLoc.getBlockY(), chestLoc.getBlockZ()); + } + UUID uuidToDelete = (ownerUUID != null) ? ownerUUID : player.getUniqueId(); + // YAML-Public-Cache immer leeren, unabhängig vom Typ (kein Leak nach Abbau) + removeFromYamlPublicCache(chestLoc.getWorld().getName(), + chestLoc.getBlockX(), chestLoc.getBlockY(), chestLoc.getBlockZ()); + if (line1.equalsIgnoreCase("rest")) { if (mysqlEnabled && mysqlManager != null) { mysqlManager.removeRestChestByLocation(uuidToDelete.toString(), diff --git a/src/main/java/com/viper/autosortchest/MySQLManager.java b/src/main/java/com/viper/autosortchest/MySQLManager.java index 4f48d59..67990fb 100644 --- a/src/main/java/com/viper/autosortchest/MySQLManager.java +++ b/src/main/java/com/viper/autosortchest/MySQLManager.java @@ -4,6 +4,36 @@ import java.sql.*; import java.util.*; public class MySQLManager { + /** + * Setzt die Priorität (prio) für eine Zieltruhe (target chest) anhand von uuid, item, slot. + * Legt die Spalte prio an, falls sie noch nicht existiert. + */ + public void setTargetChestPrio(String uuid, String item, int slot, int prio) { + ensureConnected(); + // Spalte prio anlegen, falls sie fehlt + try (Statement st = connection.createStatement()) { + ResultSet rs = connection.getMetaData().getColumns(connection.getCatalog(), null, "asc_target_chests", "prio"); + if (!rs.next()) { + st.execute("ALTER TABLE asc_target_chests ADD COLUMN prio INT DEFAULT 0;"); + } + rs.close(); + } catch (SQLException e) { + if (!e.getMessage().toLowerCase().contains("duplicate column")) { + e.printStackTrace(); + } + } + // Prio setzen + try (PreparedStatement ps = connection.prepareStatement( + "UPDATE asc_target_chests SET prio=? WHERE uuid=? AND item=? AND slot=?;")) { + ps.setInt(1, prio); + ps.setString(2, uuid); + ps.setString(3, item); + ps.setInt(4, slot); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } public Connection getConnection() { return connection; } @@ -598,12 +628,24 @@ public class MySQLManager { return all.isEmpty() ? null : all.get(0); } - /** Gibt ALLE Zieltruhen für ein bestimmtes Item zurück, sortiert nach slot. */ + /** Gibt ALLE Zieltruhen für ein bestimmtes Item zurück, sortiert nach prio absteigend, dann slot aufsteigend. */ public List> getTargetChestsForItem(String uuid, String item) { ensureConnected(); List> list = new ArrayList<>(); - try (PreparedStatement ps = connection.prepareStatement( - "SELECT * FROM asc_target_chests WHERE uuid=? AND item=? ORDER BY slot ASC;")) { + // Prio 1 zuerst, Prio 2 danach usw. — Prio 0 (nicht gesetzt) kommt ans Ende. + // CASE WHEN vermeidet eine separate Abfrage ob die Spalte existiert. + String sql; + try { + if (columnExists("asc_target_chests", "prio")) { + sql = "SELECT * FROM asc_target_chests WHERE uuid=? AND item=? " + + "ORDER BY CASE WHEN COALESCE(prio,0)=0 THEN 999 ELSE COALESCE(prio,0) END ASC, slot ASC;"; + } else { + sql = "SELECT * FROM asc_target_chests WHERE uuid=? AND item=? ORDER BY slot ASC;"; + } + } catch (Exception e) { + sql = "SELECT * FROM asc_target_chests WHERE uuid=? AND item=? ORDER BY slot ASC;"; + } + try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, uuid); ps.setString(2, item); ResultSet rs = ps.executeQuery(); @@ -616,6 +658,7 @@ public class MySQLManager { map.put("y", rs.getInt("y")); map.put("z", rs.getInt("z")); map.put("public", rs.getBoolean("public")); + try { map.put("prio", rs.getInt("prio")); } catch (SQLException ignored) { map.put("prio", 0); } try { map.put("server", rs.getString("server")); } catch (SQLException ignored) { map.put("server", ""); } list.add(map); @@ -1051,6 +1094,45 @@ public class MySQLManager { // FIX: Einzelne UNION-Abfrage statt 3 separater Queries für isChestPublic() // Reduziert Main-Thread-Blockierung bei MySQL um ~66%. // ═══════════════════════════════════════════════════════════════════ + /** + * Sucht den Besitzer einer Truhen-Location in ALLEN ASC-Tabellen (UNION). + * Wird im onBlockBreak-Fallback verwendet wenn der Name-Lookup keine UUID liefert + * (z.B. MySQL-Modus mit leerer players.yml). + * + * @return UUID des Besitzers oder null wenn nicht gefunden. + */ + public UUID findOwnerByLocation(String world, int x, int y, int z) { + ensureConnected(); + String sql = + "(SELECT uuid FROM asc_input_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1) " + + "UNION ALL " + + "(SELECT uuid FROM asc_target_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1) " + + "UNION ALL " + + "(SELECT uuid FROM asc_rest_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1) " + + "UNION ALL " + + "(SELECT uuid FROM asc_trash_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1) " + + "LIMIT 1"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + for (int i = 0; i < 4; i++) { + int base = i * 4; + ps.setString(base + 1, world); + ps.setInt (base + 2, x); + ps.setInt (base + 3, y); + ps.setInt (base + 4, z); + } + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String uuidStr = rs.getString("uuid"); + rs.close(); + try { return UUID.fromString(uuidStr); } catch (Exception ignored) { return null; } + } + rs.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + /** * Prüft ob eine Truhen-Location in IRGENDEINER Tabelle als public markiert ist. * Kombiniert Input-, Target- und Rest-Tabelle in einer einzigen UNION-Abfrage. diff --git a/src/main/java/com/viper/autosortchest/TrashChestManager.java b/src/main/java/com/viper/autosortchest/TrashChestManager.java index f5b0515..869fb76 100644 --- a/src/main/java/com/viper/autosortchest/TrashChestManager.java +++ b/src/main/java/com/viper/autosortchest/TrashChestManager.java @@ -62,9 +62,32 @@ public class TrashChestManager { "http://textures.minecraft.net/texture/32518d04f9c06c95dd0edad617abb93d3d8657f01e659079d330cca6f65bccf7"; private String getGuiTitle() { - String colorPrefix = getChestTitleColor(); - String label = isEnglish() ? "Configure Trash Chest" : "Mülltruhe konfigurieren"; - return colorPrefix + ChatColor.BOLD + label; + return getGuiText("title"); + } + + /** + * Liest einen einzelnen Text aus dem trash-gui-Abschnitt der config.yml. + * Wählt automatisch die richtige Sprache (de/en). Farbcodes werden übersetzt. + */ + private String getGuiText(String key) { + String lang = isEnglish() ? "en" : "de"; + String val = plugin.getConfig().getString("trash-gui." + key + "." + lang); + if (val == null) val = plugin.getConfig().getString("trash-gui." + key + ".de", key); + return ChatColor.translateAlternateColorCodes('&', val); + } + + /** + * Liest eine mehrzeilige Lore-Liste aus dem trash-gui-Abschnitt der config.yml. + * Wählt automatisch die richtige Sprache (de/en). Farbcodes werden übersetzt. + */ + private List getGuiLore(String key) { + String lang = isEnglish() ? "en" : "de"; + List list = plugin.getConfig().getStringList("trash-gui." + key + "." + lang); + if (list.isEmpty()) list = plugin.getConfig().getStringList("trash-gui." + key + ".de"); + List result = new ArrayList<>(list.size()); + for (String line : list) + result.add(ChatColor.translateAlternateColorCodes('&', line)); + return result; } private boolean isEnglish() { @@ -273,21 +296,44 @@ public class TrashChestManager { saveTrashChest(uuid); } - public void removeTrashChest(UUID uuid) { - Location loc = trashChestLocations.remove(uuid); - if (loc != null) locationToOwner.remove(locKey(loc)); + /** + * Entfernt Mülltruhen-Eintrag für eine bestimmte Location (z.B. beim Abbau der Kiste). + * Falls mehrere Mülltruhen pro Spieler möglich sind, wird nur der passende Eintrag entfernt. + * Falls keine Location angegeben, wird wie bisher nach UUID gelöscht. + */ + public void removeTrashChest(UUID uuid, Location loc) { + trashChestLocations.remove(uuid); + locationToOwner.remove(locKey(loc)); trashFilterLists.remove(uuid); if (plugin.isMysqlEnabled() && plugin.getMysqlManager() != null) { plugin.getMysqlManager().removeTrashChest(uuid.toString()); plugin.getMysqlManager().removeAllTrashItems(uuid.toString()); } else { FileConfiguration data = plugin.getPlayerData(); - data.set("players." + uuid + ".trash-chest", null); - data.set("players." + uuid + ".trash-items", null); + String uuidStr = uuid.toString(); + String base = "players." + uuidStr + ".trash-chest"; + // Prüfe, ob die gespeicherte Location mit der zu entfernenden übereinstimmt + if (data.contains(base + ".world")) { + String w = data.getString(base + ".world"); + int x = data.getInt(base + ".x"); + int y = data.getInt(base + ".y"); + int z = data.getInt(base + ".z"); + if (w != null && loc != null && + w.equals(loc.getWorld().getName()) && + x == loc.getBlockX() && y == loc.getBlockY() && z == loc.getBlockZ()) { + data.set(base, null); + data.set("players." + uuidStr + ".trash-items", null); + } + } plugin.savePlayerDataPublic(); } } + // Für Kompatibilität: alter Aufruf ohne Location löscht wie bisher alles für die UUID + public void removeTrashChest(UUID uuid) { + removeTrashChest(uuid, trashChestLocations.get(uuid)); + } + public UUID getTrashChestOwner(Location loc) { return locationToOwner.get(locKey(loc)); } public Location getTrashChestLocation(UUID uuid) { return trashChestLocations.get(uuid); } public Map getAllTrashChests() { return new HashMap<>(trashChestLocations); } @@ -449,7 +495,6 @@ public class TrashChestManager { public void openConfigGui(Player player, UUID ownerUUID, int page) { openGuiOwners.put(player.getUniqueId(), ownerUUID); - boolean isEn = isEnglish(); List filter = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>()); @@ -469,15 +514,14 @@ public class TrashChestManager { // ── Items der aktuellen Seite ──────────────────────────────────────── int startIndex = page * ITEMS_PER_PAGE; int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, validItems.size()); + String removeHint = getGuiText("item-remove-hint"); for (int i = startIndex; i < endIndex; i++) { ItemStack filterItem = validItems.get(i); - // Clone mit Menge 1 ItemStack display = filterItem.clone(); display.setAmount(1); ItemMeta meta = display.getItemMeta(); if (meta != null) { - // Anzeige-Namen setzen wenn kein eigener vorhanden if (!meta.hasDisplayName()) { meta.setDisplayName(getSignColor("trash", "line1") + "" + ChatColor.BOLD + formatMaterialName(filterItem.getType().name())); @@ -507,7 +551,7 @@ public class TrashChestManager { } } - // ── Tränkeffekte (Trank, Wurftrank, Pfeil: PotionMeta) ────────── + // ── Tränkeffekte (PotionMeta) ──────────────────────────────────── if (meta instanceof PotionMeta pm) { List effects = pm.getCustomEffects(); if (!effects.isEmpty()) { @@ -539,7 +583,7 @@ public class TrashChestManager { lore.add(""); } - lore.add(ChatColor.RED + (isEn ? "▶ Right-click: Remove" : "▶ Rechtsklick: Entfernen")); + lore.add(removeHint); meta.setLore(lore); display.setItemMeta(meta); } @@ -557,10 +601,10 @@ public class TrashChestManager { ItemStack prev = new ItemStack(Material.ARROW); ItemMeta prevMeta = prev.getItemMeta(); if (prevMeta != null) { - prevMeta.setDisplayName(ChatColor.YELLOW + "" + ChatColor.BOLD - + (isEn ? "◀ Previous Page" : "◀ Vorherige Seite")); - prevMeta.setLore(Arrays.asList(ChatColor.GRAY - + (isEn ? "Page " : "Seite ") + page + (isEn ? " of " : " von ") + totalPages)); + prevMeta.setDisplayName(getGuiText("btn-prev-title")); + prevMeta.setLore(Arrays.asList(getGuiText("page-nav-lore") + .replace("%page%", String.valueOf(page)) + .replace("%total%", String.valueOf(totalPages)))); prev.setItemMeta(prevMeta); } gui.setItem(45, prev); @@ -570,10 +614,11 @@ public class TrashChestManager { ItemStack pageInfo = new ItemStack(Material.PAPER); ItemMeta pageMeta = pageInfo.getItemMeta(); if (pageMeta != null) { - pageMeta.setDisplayName(ChatColor.WHITE + "" + ChatColor.BOLD - + (isEn ? "Page " : "Seite ") + (page + 1) + " / " + totalPages); - pageMeta.setLore(Arrays.asList(ChatColor.GRAY + "" + validItems.size() - + (isEn ? " items in filter" : " Items im Filter"))); + pageMeta.setDisplayName(getGuiText("page-info-title") + .replace("%page%", String.valueOf(page + 1)) + .replace("%total%", String.valueOf(totalPages))); + pageMeta.setLore(Arrays.asList(getGuiText("page-info-lore") + .replace("%count%", String.valueOf(validItems.size())))); pageInfo.setItemMeta(pageMeta); } gui.setItem(46, pageInfo); @@ -583,10 +628,10 @@ public class TrashChestManager { ItemStack next = new ItemStack(Material.ARROW); ItemMeta nextMeta = next.getItemMeta(); if (nextMeta != null) { - nextMeta.setDisplayName(ChatColor.YELLOW + "" + ChatColor.BOLD - + (isEn ? "Next Page ▶" : "Nächste Seite ▶")); - nextMeta.setLore(Arrays.asList(ChatColor.GRAY - + (isEn ? "Page " : "Seite ") + (page + 2) + (isEn ? " of " : " von ") + totalPages)); + nextMeta.setDisplayName(getGuiText("btn-next-title")); + nextMeta.setLore(Arrays.asList(getGuiText("page-nav-lore") + .replace("%page%", String.valueOf(page + 2)) + .replace("%total%", String.valueOf(totalPages)))); next.setItemMeta(nextMeta); } gui.setItem(47, next); @@ -597,24 +642,11 @@ public class TrashChestManager { ItemMeta modeMeta = modeInfo.getItemMeta(); if (modeMeta != null) { if (filter.isEmpty()) { - modeMeta.setDisplayName(ChatColor.RED + "" + ChatColor.BOLD - + (isEn ? "✗ Status: Disabled" : "✗ Status: Deaktiviert")); - modeMeta.setLore(Arrays.asList( - ChatColor.GRAY + (isEn ? "No filter set –" : "Kein Filter gesetzt –"), - ChatColor.GRAY + (isEn ? "items will NOT be deleted." : "Items werden NICHT gelöscht."), - ChatColor.YELLOW + (isEn - ? "Add items to activate the trash chest." - : "Füge Items hinzu um die Mülltruhe zu aktivieren."))); + modeMeta.setDisplayName(getGuiText("status-disabled-title")); + modeMeta.setLore(getGuiLore("status-disabled-lore")); } else { - modeMeta.setDisplayName(ChatColor.GREEN + "" + ChatColor.BOLD - + (isEn ? "✔ Status: Active" : "✔ Status: Aktiv")); - modeMeta.setLore(Arrays.asList( - ChatColor.GRAY + (isEn - ? "Items are matched exactly:" - : "Items werden exakt verglichen:"), - ChatColor.GRAY + (isEn - ? "Type + enchantments + name must match." - : "Typ + Verzauberungen + Name müssen übereinstimmen."))); + modeMeta.setDisplayName(getGuiText("status-active-title")); + modeMeta.setLore(getGuiLore("status-active-lore")); } modeInfo.setItemMeta(modeMeta); } @@ -624,18 +656,8 @@ public class TrashChestManager { ItemStack addBtn = new ItemStack(Material.LIME_STAINED_GLASS_PANE); ItemMeta addMeta = addBtn.getItemMeta(); if (addMeta != null) { - addMeta.setDisplayName(ChatColor.GREEN + "" + ChatColor.BOLD - + (isEn ? "✚ Add Item" : "✚ Item hinzufügen")); - addMeta.setLore(Arrays.asList( - ChatColor.GRAY + (isEn - ? "Hold the exact item in your main hand" - : "Genau das Item in die Haupthand nehmen"), - ChatColor.GRAY + (isEn - ? "and click this button." - : "und diesen Button klicken."), - ChatColor.YELLOW + (isEn - ? "Enchantments & name are saved exactly." - : "Verzauberungen & Name werden exakt gespeichert."))); + addMeta.setDisplayName(getGuiText("btn-add-title")); + addMeta.setLore(getGuiLore("btn-add-lore")); addBtn.setItemMeta(addMeta); } gui.setItem(49, addBtn); @@ -648,10 +670,8 @@ public class TrashChestManager { } private ItemStack buildSkullButton() { - boolean isEn = isEnglish(); - String label = isEn ? "Empty Trash Chest" : "Mülltruhe leeren"; - String lore1 = isEn ? "Click to immediately" : "Klicken um alle Items"; - String lore2 = isEn ? "delete all items." : "sofort zu löschen."; + String label = getGuiText("btn-clear-title"); + List loreLines = getGuiLore("btn-clear-lore"); ItemStack skull; try { @@ -663,16 +683,16 @@ public class TrashChestManager { textures.setSkin(new URL(SKULL_TEXTURE)); profile.setTextures(textures); meta.setOwnerProfile(profile); - meta.setDisplayName(getChestTitleColor() + "" + ChatColor.BOLD + label); - meta.setLore(Arrays.asList(ChatColor.GRAY + lore1, ChatColor.GRAY + lore2)); + meta.setDisplayName(label); + meta.setLore(loreLines); skull.setItemMeta(meta); } } catch (Exception e) { skull = new ItemStack(Material.RED_DYE); ItemMeta meta = skull.getItemMeta(); if (meta != null) { - meta.setDisplayName(getChestTitleColor() + "" + ChatColor.BOLD + label); - meta.setLore(Arrays.asList(ChatColor.GRAY + lore1, ChatColor.GRAY + lore2)); + meta.setDisplayName(label); + meta.setLore(loreLines); skull.setItemMeta(meta); } } @@ -790,23 +810,6 @@ public class TrashChestManager { return ChatColor.translateAlternateColorCodes('&', raw); } - private String getChestTitleColor() { - boolean isEn = isEnglish(); - String lang = isEn ? "en" : "de"; - String full = plugin.getConfig().getString("chest-titles.trash." + lang, - isEn ? "&4Trash Chest" : "&4Mülltruhe"); - StringBuilder codes = new StringBuilder(); - for (int i = 0; i + 1 < full.length(); i++) { - if (full.charAt(i) == '&' && "0123456789abcdefklmnor".indexOf(full.charAt(i + 1)) >= 0) { - codes.append(full, i, i + 2); - i++; - } else { - break; - } - } - return ChatColor.translateAlternateColorCodes('&', codes.length() > 0 ? codes.toString() : "&4"); - } - /** * Lesbarer Anzeige-Name für Chat-Nachrichten. * Nutzt Custom Display Name falls vorhanden, sonst formatierten Material-Namen. diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 77defd3..e0cdd21 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -12,7 +12,7 @@ # ============================================================ # Version der Konfigurationsdatei – bitte nicht ändern! -version: "2.3" +version: "2.5" # Debug-Modus: true = Ausführliche Logs in der Konsole (nur zum Entwickeln) debug: false @@ -312,4 +312,158 @@ messages: target-chest-full: "&cZieltruhe für %item% ist voll! Koordinaten: (%x%, %y%, %z%)" mode-changed: "&aModus gewechselt: &e%mode%" mode-public: "&aÖffentlich" - mode-private: "&cPrivat" \ No newline at end of file + mode-private: "&cPrivat" + # --- Priorität-Befehl (/asc priority) --- + # Platzhalter: %input% = eingegebene Zahl, %prio% = gesetzte Priorität + priority-player-only: "&cDieser Befehl ist nur für Spieler!" + priority-usage: "&cVerwendung: /asc priority <1-20>" + priority-invalid-number: "&cUngültige Zahl: &e%input%" + priority-out-of-range: "&cPriorität muss zwischen 1 und 20 liegen!" + priority-no-sign: "&cDu schaust auf kein Schild!" + priority-wrong-sign: "&cDas ist kein Ziel-Schild!" + priority-not-attached: "&cDas Schild ist nicht an einer Truhe befestigt!" + priority-item-unknown: "&cKonnte Item-Typ auf dem Schild nicht erkennen!" + priority-item-not-found: "&cKonnte Item-Typ für dieses Schild nicht finden!" + priority-success: "&aPriorität für Zieltruhe gesetzt: &e%prio%" + priority-not-found: "&cKonnte die Zieltruhe zu diesem Schild nicht finden!" + + # --- AutoSign-Befehl (/asc autosign) --- + # Platzhalter: %item% = Item-Name + autosign-no-chest: "&cDu schaust auf keine Truhe! &7(max. 5 Blöcke)" + autosign-invalid-type:"&cUngültiger Typ! Nutze: input, ziel, rest, trash" + autosign-unknown-item:"&cUnbekanntes Item: &e%item%" + autosign-no-space: "&cKein freier Platz für ein Schild an dieser Truhe!" + autosign-place-error: "&cFehler beim Platzieren – bitte manuell versuchen." + + # --- /asc list Ausgabe --- + # Platzhalter: %name% = Spielername + list-usage: "&cVerwendung: /asc list " + list-player-not-found: "&cSpieler &e'%name%' &cwurde nicht gefunden!" + list-header: "&6================================" + list-title: "&6&l==== AutoSortChest Info ====" + list-player-label: "&eSpieler: " + list-offline: "&7 (offline)" + list-input-label: "&eInput: " + list-target-label: "&eZiel: " + list-rest-label: "&eRest: " + list-trash-label: "&eMüll: " + list-unlimited: "*" + list-footer: "&6================================" + + # --- Konsolen-Hinweis (wenn Spieler-Befehl per Konsole aufgerufen) --- + console-only: "&cDieser Befehl ist nur für Spieler! (Konsole: reload, import, export, list)" + +# ============================================================ +# MÜLLTRUHEN-GUI +# ============================================================ +# Alle Texte des Konfigurations-GUIs der Mülltruhe. +# Farbcodes: &0-&9, &a-&f | &l = Fett, &o = Kursiv +# Platzhalter: %page% = Seitennummer, %total% = Gesamtseiten, %count% = Anzahl Items + +trash-gui: + + # Fenstertitel + title: + de: "&4Mülltruhe konfigurieren" + en: "&4Configure Trash Chest" + + # Hinweis unter jedem Filter-Item (Rechtsklick zum Entfernen) + item-remove-hint: + de: "&c▶ Rechtsklick: Entfernen" + en: "&c▶ Right-click: Remove" + + # Navigations-Pfeile + btn-prev-title: + de: "&e&l◀ Vorherige Seite" + en: "&e&l◀ Previous Page" + btn-next-title: + de: "&e&lNächste Seite ▶" + en: "&e&lNext Page ▶" + + # Seitenanzeige (Slot 46) + page-info-title: + de: "&f&lSeite %page% / %total%" + en: "&f&lPage %page% / %total%" + page-info-lore: + de: "&7%count% Items im Filter" + en: "&7%count% items in filter" + page-nav-lore: + de: "Seite %page% von %total%" + en: "Page %page% of %total%" + + # Status-Anzeige: Deaktiviert (kein Filter gesetzt) + status-disabled-title: + de: "&c&l✗ Status: Deaktiviert" + en: "&c&l✗ Status: Disabled" + status-disabled-lore: + de: + - "&7Kein Filter gesetzt –" + - "&7Items werden NICHT gelöscht." + - "&eItems hinzufügen um zu aktivieren." + en: + - "&7No filter set –" + - "&7items will NOT be deleted." + - "&eAdd items to activate." + + # Status-Anzeige: Aktiv (Filter gesetzt) + status-active-title: + de: "&a&l✔ Status: Aktiv" + en: "&a&l✔ Status: Active" + status-active-lore: + de: + - "&7Items werden exakt verglichen:" + - "&7Typ + Verzauberungen + Name." + en: + - "&7Items are matched exactly:" + - "&7Type + enchantments + name." + + # Item hinzufügen (Slot 49) + btn-add-title: + de: "&a&l✚ Item hinzufügen" + en: "&a&l✚ Add Item" + btn-add-lore: + de: + - "&7Gewünschtes Item in die Haupthand nehmen" + - "&7und diesen Knopf klicken." + - "&eVerzauberungen & Name werden gespeichert." + en: + - "&7Hold the exact item in your main hand" + - "&7and click this button." + - "&eEnchantments & name are saved exactly." + + # Mülltruhe leeren (Slot 53 – Schädel-Button) + btn-clear-title: + de: "&4&lMülltruhe leeren" + en: "&4&lEmpty Trash Chest" + btn-clear-lore: + de: + - "&7Klicken um alle Items" + - "&7sofort zu löschen." + en: + - "&7Click to immediately" + - "&7delete all items." + + # --- /asc import / export (Admin-Befehle) --- + mysql-not-enabled-import: "&cMySQL ist nicht aktiviert! Aktiviere MySQL in der config.yml zuerst." + mysql-not-enabled-export: "&cMySQL ist nicht aktiviert! Der Export benötigt eine aktive MySQL-Verbindung." + yaml-empty: "&cDie players.yml ist leer oder enthält keine Spielerdaten!" + import-start: "&eImportiere Daten aus players.yml nach MySQL..." + import-info: "&7Bestehende MySQL-Daten werden nicht überschrieben (REPLACE INTO)." + import-success: "&aImport erfolgreich abgeschlossen!" + # Platzhalter: %players%, %input%, %target%, %rest% + import-stats-players: "&7 Spieler: &f%players%" + import-stats-input: "&7 Eingangstruhen: &f%input%" + import-stats-target: "&7 Zieltruhen: &f%target%" + import-stats-rest: "&7 Rest-Truhen: &f%rest%" + export-start: "&eExportiere Daten aus MySQL nach players.yml..." + export-info: "&7Ein Backup der aktuellen players.yml wird erstellt." + export-success: "&aExport erfolgreich abgeschlossen!" + # Platzhalter: %file% = Backup-Dateiname + export-backup: "&7 Backup: &f%file%" + export-backup-skipped: "&7 Backup: &8Übersprungen (players.yml war leer)" + export-error: "&cExport fehlgeschlagen: &e%error%" + backup-failed: "&cBackup fehlgeschlagen: &e%error%" + + # --- Autosign-Verwendungshinweis --- + autosign-player-only: "&cDieser Befehl ist nur für Spieler!" + autosign-usage: "&cVerwendung: /asc autosign [item|hand]" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index df20ed0..875d2d1 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: AutoSortChest -version: 2.6 +version: 2.7 main: com.viper.autosortchest.Main api-version: 1.21 authors: [M_Viper] @@ -7,11 +7,11 @@ description: Ein Plugin zum automatischen Sortieren von Items in Truhen commands: asc: description: AutoSortChest Befehle - usage: / [help|info|reload|import|export|list] + usage: / [help|info|reload|import|export|list|autosign|priority] aliases: [autosortchest] permissions: autosortchest.use: - description: Erlaubt das Erstellen von AutoSortChest-Schildern (Eingang, Ziel, Rest, Muelltruhe) + description: Erlaubt das Erstellen von AutoSortChest-Schildern (Eingang, Ziel, Rest, Muelltruhe) sowie die Verwendung von /asc autosign default: true autosortchest.reload: description: Erlaubt das Neuladen der Konfiguration mit /asc reload