From 4d72b6486611e5ac957607968e880d587eba6b26 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Wed, 4 Mar 2026 11:41:13 +0100 Subject: [PATCH] Update from Git Manager GUI --- .../java/com/viper/autosortchest/Main.java | 2745 +++++++++++++---- .../com/viper/autosortchest/MySQLManager.java | 243 ++ .../autosortchest/TrashChestManager.java | 512 +++ .../viper/autosortchest/UpdateChecker.java | 2 +- src/main/resources/config.yml | 364 ++- src/main/resources/plugin.yml | 15 +- 6 files changed, 3162 insertions(+), 719 deletions(-) create mode 100644 src/main/java/com/viper/autosortchest/TrashChestManager.java diff --git a/src/main/java/com/viper/autosortchest/Main.java b/src/main/java/com/viper/autosortchest/Main.java index 2ec8ee9..0e35910 100644 --- a/src/main/java/com/viper/autosortchest/Main.java +++ b/src/main/java/com/viper/autosortchest/Main.java @@ -11,6 +11,7 @@ import org.bukkit.Particle.DustOptions; import org.bukkit.Sound; import org.bukkit.World; import org.bukkit.block.Block; +import org.bukkit.block.BlockState; import org.bukkit.block.Chest; import org.bukkit.block.DoubleChest; import org.bukkit.block.Sign; @@ -42,6 +43,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.*; +import java.util.stream.Collectors; import com.viper.autosortchest.MySQLManager; @@ -55,9 +57,13 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { // ------------------------------------------------------- // Task-Referenzen für sauberen Reload // ------------------------------------------------------- - private BukkitTask sortTask = null; - private BukkitTask cleanTask = null; + private BukkitTask sortTask = null; + private BukkitTask cleanTask = null; private BukkitTask heartbeatTask = null; // BungeeCord NEU + private BukkitTask restResortTask = null; // Rest-Truhe Nachsortierung + + // ── Mülltruche ──────────────────────────────────────────────────────────── + private TrashChestManager trashChestManager; private void loadOptionalSettings() { serverCrosslink = config.getBoolean("server_crosslink", true); @@ -76,6 +82,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { sortIntervalTicks = 1; } + restResortIntervalTicks = config.getInt("rest_resort_interval_ticks", 0); + // ── BungeeCord NEU ──────────────────────────────────────────────────── serverName = config.getString("server_name", "").trim(); if (!serverName.isEmpty()) { @@ -111,16 +119,14 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { * Startet oder startet den Sortier-Task, Cleanup-Task und (neu) Heartbeat-Task neu. */ private void startTasks() { - if (sortTask != null) { sortTask.cancel(); sortTask = null; } - if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } - if (heartbeatTask != null) { heartbeatTask.cancel(); heartbeatTask = null; } // BungeeCord NEU + if (sortTask != null) { sortTask.cancel(); sortTask = null; } + if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } + if (heartbeatTask != null) { heartbeatTask.cancel(); heartbeatTask = null; } + if (restResortTask != null) { restResortTask.cancel(); restResortTask = null; } sortTask = new BukkitRunnable() { @Override public void run() { - // checkInputChests() und processIncomingTransfers() starten jeweils - // selbst einen async-Thread für DB-Arbeit und kommen für World-Ops - // per runTask() zurück auf den Main Thread. checkInputChests(); if (mysqlEnabled && mysqlManager != null && !serverName.isEmpty()) { processIncomingTransfers(); @@ -132,6 +138,7 @@ 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 } }.runTaskTimer(this, 20L * 60, 20L * 60); @@ -144,40 +151,44 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } }.runTaskTimerAsynchronously(this, 20L, 20L * 30); } + + // ── Rest-Truhe Nachsortierung ───────────────────────────────────────── + if (restResortIntervalTicks > 0) { + restResortTask = new BukkitRunnable() { + @Override + public void run() { + resortRestChests(); + } + }.runTaskTimer(this, 20L * 5, restResortIntervalTicks); + } + + // Mülltruchen Auto-Clear nach Reload neu starten + if (trashChestManager != null) { + trashChestManager.startAutoTrashTask(); + } } // ── BungeeCord NEU: Hilfsmethode ────────────────────────────────────────── - /** - * Prüft, ob eine Truhe (aus einem MySQL-Map) auf einem ANDEREN Server liegt. - * - * Logik: - * 1. Wenn server_name gesetzt UND Truhe hat einen "server"-Eintrag UND dieser - * unterscheidet sich → eindeutig Remote. - * 2. Sonst: Welt nicht geladen → Remote (Legacy-Verhalten). - * 3. Sonst: Lokal. - */ private boolean isRemoteChest(Map chestMap) { if (chestMap == null) return false; String chestServer = (String) chestMap.getOrDefault("server", ""); - // Explizites Server-Routing (bevorzugt) if (!serverName.isEmpty() && !chestServer.isEmpty()) { return !chestServer.equals(serverName); } - // Legacy: Welt nicht geladen String worldName = (String) chestMap.get("world"); return Bukkit.getWorld(worldName) == null; } - /** - * Gibt den Server-Namen einer Truhe zurück (leer = unbekannt / Legacy). - */ private String getChestServer(Map chestMap) { if (chestMap == null) return ""; return (String) chestMap.getOrDefault("server", ""); } private int getChestLimitForPlayer(Player player, String type) { - // Gruppen nach höchstem target-Limit sortieren (bevorzugt höhere Rechte) + // OPs und Spieler mit autosortchest.limit.bypass haben kein Limit + if (isAdmin(player) || player.hasPermission("autosortchest.limit.bypass")) { + return Integer.MAX_VALUE; + } List groups = new ArrayList<>(chestLimits.keySet()); groups.sort((a, b) -> { int la = chestLimits.getOrDefault(a, new HashMap<>()).getOrDefault("target", 0); @@ -190,10 +201,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (limits != null) return limits.getOrDefault(type, 1); } } - // Fallback: default-Gruppe - Map def = chestLimits.get("default"); - if (def != null) return def.getOrDefault(type, type.equals("target") ? 50 : 1); - return type.equals("target") ? 50 : 1; + // Kein Limit-Permission gefunden → kein Rang → keine Truhen erlaubt (wenn Limits aktiv) + return 0; } /** Rueckwaertskompatibilitaet: type=target */ @@ -213,6 +222,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { private String mysqlPassword; private List worldBlacklist; private int sortIntervalTicks; + private int restResortIntervalTicks; // 0 = deaktiviert // group → { "input", "rest", "target" } → limit private Map> chestLimits; private boolean chestLimitsEnabled = true; @@ -222,7 +232,105 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { private final Map fullChestLocationCache = new HashMap<>(); private static final long FULL_CHEST_CACHE_DURATION = 10_000L; // 10 Sekunden - private static final String CONFIG_VERSION = "2.2"; // Multi-Rest: 2.1 → 2.2 + // FIX: TTL-Cache für isChestPublic() – verhindert synchrone SQL-Queries bei jedem Click-Event + private final Map chestPublicCache = new HashMap<>(); + private final Map chestPublicCacheTimestamps = new HashMap<>(); + private static final long CHEST_PUBLIC_CACHE_TTL = 5_000L; // 5 Sekunden + + // FIX: YAML-Public-Cache – verhindert O(N)-Iteration über alle Spieler pro Sign-Klick + // Wird bei loadPlayerData() aufgebaut und bei jeder Änderung aktualisiert. + private final Map yamlPublicLocationCache = new HashMap<>(); + + /** Gibt true zurück wenn eine Location im YAML-Public-Cache als public markiert ist. */ + private boolean isPublicInYamlCache(String world, int x, int y, int z) { + return yamlPublicLocationCache.getOrDefault(world + ":" + x + ":" + y + ":" + z, false); + } + + /** Setzt/aktualisiert einen Eintrag im YAML-Public-Cache. */ + private void updateYamlPublicCache(String world, int x, int y, int z, boolean isPublic) { + String key = world + ":" + x + ":" + y + ":" + z; + if (isPublic) { + yamlPublicLocationCache.put(key, true); + } else { + yamlPublicLocationCache.remove(key); + } + } + + /** Entfernt einen Eintrag aus dem YAML-Public-Cache (beim Abbauen). */ + private void removeFromYamlPublicCache(String world, int x, int y, int z) { + yamlPublicLocationCache.remove(world + ":" + x + ":" + y + ":" + z); + } + + /** + * Baut den YAML-Public-Cache komplett neu auf. + * Wird nach loadPlayerData() und nach /asc reload aufgerufen. + */ + private void buildYamlPublicCache() { + yamlPublicLocationCache.clear(); + if (playerData == null || playerData.getConfigurationSection("players") == null) return; + for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { + // Input-Truhen + String inputBase = "players." + uuidString + ".input-chests"; + if (playerData.contains(inputBase)) { + for (String chestId : playerData.getConfigurationSection(inputBase).getKeys(false)) { + String p = inputBase + "." + chestId; + if (playerData.getBoolean(p + ".public", false)) { + updateYamlPublicCache(playerData.getString(p + ".world", ""), + playerData.getInt(p + ".x"), playerData.getInt(p + ".y"), playerData.getInt(p + ".z"), true); + } + } + } + // Ziel-Truhen + String targetBase = "players." + uuidString + ".target-chests"; + if (playerData.contains(targetBase)) { + for (String item : playerData.getConfigurationSection(targetBase).getKeys(false)) { + String itemBase = targetBase + "." + item; + if (playerData.isConfigurationSection(itemBase)) { + for (String slotKey : playerData.getConfigurationSection(itemBase).getKeys(false)) { + String p = itemBase + "." + slotKey; + if (playerData.getBoolean(p + ".public", false)) { + updateYamlPublicCache(playerData.getString(p + ".world", ""), + playerData.getInt(p + ".x"), playerData.getInt(p + ".y"), playerData.getInt(p + ".z"), true); + } + } + } else if (playerData.getBoolean(itemBase + ".public", false)) { + // Legacy flat format + updateYamlPublicCache(playerData.getString(itemBase + ".world", ""), + playerData.getInt(itemBase + ".x"), playerData.getInt(itemBase + ".y"), playerData.getInt(itemBase + ".z"), true); + } + } + } + // Rest-Truhen + String restBase = "players." + uuidString + ".rest-chests"; + if (playerData.contains(restBase)) { + for (String slotKey : playerData.getConfigurationSection(restBase).getKeys(false)) { + String p = restBase + "." + slotKey; + if (playerData.getBoolean(p + ".public", false)) { + updateYamlPublicCache(playerData.getString(p + ".world", ""), + playerData.getInt(p + ".x"), playerData.getInt(p + ".y"), playerData.getInt(p + ".z"), true); + } + } + } + } + if (isDebug()) getLogger().info("[YamlPublicCache] " + yamlPublicLocationCache.size() + " öffentliche Locations geladen."); + } + + /** Verhindert Spam-Klick-Bypass des Truhen-Limits (beide Hände): Location+Player → last timestamp */ + private final Map signInteractCooldown = new HashMap<>(); + private static final long SIGN_INTERACT_COOLDOWN_MS = 300L; // 300ms Sperre + + /** + * Merkt sich für jeden Spieler die Location der Truhe, die er gerade + * mit einem Custom-Titel geöffnet hat. Wird beim Schließen ausgelesen + * um den Inhalt der virtuellen Inventory zurück in die echte Truhe zu kopieren. + */ + private final Map openCustomInventories = new HashMap<>(); + + // FIX: Async-Save mit Dirty-Flag – kein File-I/O mehr auf dem Main-Thread + private volatile boolean playerDataDirty = false; + private volatile boolean saveInProgress = false; + + private static final String CONFIG_VERSION = "2.4"; private boolean updateAvailable = false; private String latestVersion = ""; @@ -246,6 +354,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&f2. Schreibe:\n" + " &7[asc]\n" + " &7rest\n" + + "&eMülltruhe erstellen:\n" + + "&f1. Platziere ein Schild an einer Truhe.\n" + + "&f2. Schreibe:\n" + + " &7[asc]\n" + + " &7trash\n" + + "&f3. Rechtsklicke das Schild zum Konfigurieren.\n" + "&eBungeeCord:\n" + "&fSetze 'server_name' in config.yml für serverübergreifendes Sortieren.\n" + "&eBefehle:\n" + @@ -275,6 +389,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&f2. Write:\n" + " &7[asc]\n" + " &7rest\n" + + "&eCreate Trash Chest:\n" + + "&f1. Place a sign on a chest.\n" + + "&f2. Write:\n" + + " &7[asc]\n" + + " &7trash\n" + + "&f3. Right-click the sign to configure.\n" + "&eBungeeCord:\n" + "&fSet 'server_name' in config.yml for cross-server sorting.\n" + "&eCommands:\n" + @@ -305,6 +425,34 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&eDescription: &fAutomatically sorts items into chests.\n" + "&6&l========================"; + // ------------------------------------------------------- + // PUBLIC ACCESSORS FÜR TrashChestManager + // ------------------------------------------------------- + /** Gibt das playerData-Konfigurationsobjekt zurück (für TrashChestManager). */ + public FileConfiguration getPlayerData() { + return playerData; + } + + /** Speichert playerData öffentlich zugänglich (für TrashChestManager). */ + public void savePlayerDataPublic() { + savePlayerData(); + } + + /** Gibt zurück ob MySQL aktiv ist (für TrashChestManager). */ + public boolean isMysqlEnabled() { + return mysqlEnabled; + } + + /** Gibt den MySQLManager zurück (für TrashChestManager). */ + public MySQLManager getMysqlManager() { + return mysqlManager; + } + + /** Gibt den Server-Namen zurück (für TrashChestManager). */ + public String getServerName() { + return serverName; + } + // ------------------------------------------------------- // IMPORT: YAML → MySQL // ------------------------------------------------------- @@ -331,16 +479,34 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { mysqlManager.addInputChest(uuidString, chestId, world, x, y, z, isPublic); } } + // BUG FIX: Slotted-Format (GRASS_BLOCK.0.world) UND altes Flat-Format (GRASS_BLOCK.world) unterstützen String targetPath = "players." + uuidString + ".target-chests"; if (playerData.contains(targetPath)) { for (String item : playerData.getConfigurationSection(targetPath).getKeys(false)) { - String path = targetPath + "." + item; - String world = playerData.getString(path + ".world"); - int x = playerData.getInt(path + ".x"); - int y = playerData.getInt(path + ".y"); - int z = playerData.getInt(path + ".z"); - boolean isPublic = playerData.getBoolean(path + ".public", false); - mysqlManager.setTargetChest(uuidString, item, world, x, y, z, isPublic); + String itemBase = targetPath + "." + item; + if (playerData.contains(itemBase + ".world")) { + // Altes Flat-Format: GRASS_BLOCK.world / x / y / z + String world = playerData.getString(itemBase + ".world"); + int x = playerData.getInt(itemBase + ".x"); + int y = playerData.getInt(itemBase + ".y"); + int z = playerData.getInt(itemBase + ".z"); + boolean isPublic = playerData.getBoolean(itemBase + ".public", false); + mysqlManager.setTargetChest(uuidString, item, 0, world, x, y, z, isPublic, ""); + } else if (playerData.isConfigurationSection(itemBase)) { + // Neues Slotted-Format: GRASS_BLOCK.0.world / 1.world / ... + for (String slotKey : playerData.getConfigurationSection(itemBase).getKeys(false)) { + String slotPath = itemBase + "." + slotKey; + if (!playerData.contains(slotPath + ".world")) continue; + String world = playerData.getString(slotPath + ".world"); + int x = playerData.getInt(slotPath + ".x"); + int y = playerData.getInt(slotPath + ".y"); + int z = playerData.getInt(slotPath + ".z"); + boolean isPublic = playerData.getBoolean(slotPath + ".public", false); + int slot = 0; + try { slot = Integer.parseInt(slotKey); } catch (NumberFormatException ignored2) {} + mysqlManager.setTargetChest(uuidString, item, slot, world, x, y, z, isPublic, ""); + } + } } } // BUG FIX: Mehrere Rest-Truhen migrieren (neues Format + Legacy) @@ -367,6 +533,19 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { mysqlManager.setRestChest(uuidString, world, x, y, z, isPublic); } } + // Mülltruchen migrieren + String trashPath = "players." + uuidString + ".trash-chest"; + if (playerData.contains(trashPath + ".world")) { + String world = playerData.getString(trashPath + ".world"); + int x = playerData.getInt(trashPath + ".x"); + int y = playerData.getInt(trashPath + ".y"); + int z = playerData.getInt(trashPath + ".z"); + mysqlManager.setTrashChest(uuidString, world, x, y, z, serverName); + } + List trashItems = playerData.getStringList("players." + uuidString + ".trash-items"); + if (!trashItems.isEmpty()) { + mysqlManager.setTrashItems(uuidString, trashItems); + } } getLogger().info("Migration der YAML-Daten nach MySQL abgeschlossen."); } @@ -412,7 +591,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { List> targetChests = mysqlManager.getTargetChests(uuidString); for (Map chest : targetChests) { String item = (String) chest.get("item"); - String path = "players." + uuidString + ".target-chests." + 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")); exportData.set(path + ".y", chest.get("y")); @@ -457,7 +637,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { // BungeeCord NEU: Transfer-Tabelle mit Routing-Spalten anlegen/migrieren mysqlManager.setupTransferTable(); getLogger().info("MySQL-Verbindung erfolgreich hergestellt."); - migrateYamlToMySQL(); } else { getLogger().warning("MySQL-Verbindung fehlgeschlagen! Fallback auf YAML."); mysqlEnabled = false; @@ -469,10 +648,23 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { saveResource("players.yml", false); } loadPlayerData(); + // Migration YAML → MySQL: nur einmalig wenn MySQL noch keine Spielerdaten enthält. + // Verhindert destruktives Überschreiben von MySQL-Daten bei jedem Serverstart. + if (mysqlEnabled && mysqlManager != null) { + if (mysqlManager.getAllPlayers().isEmpty()) { + getLogger().info("Leere MySQL-Datenbank erkannt – migriere YAML-Daten einmalig..."); + migrateYamlToMySQL(); + } else { + getLogger().info("MySQL enthält bereits Spielerdaten – automatische Migration übersprungen."); + } + } getServer().getPluginManager().registerEvents(this, this); this.getCommand("asc").setExecutor(this); + // Mülltruchen-Manager initialisieren + trashChestManager = new TrashChestManager(this); + startTasks(); updateConfig(); @@ -511,7 +703,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (serverCrosslink) { getLogger().info("Serverübergreifende Sortierung ist aktiviert (server_crosslink=true)."); } - // BungeeCord NEU if (!serverName.isEmpty()) { getLogger().info("[BungeeCord] Dieser Server läuft als: \"" + serverName + "\""); } else { @@ -528,9 +719,13 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { public void onDisable() { if (sortTask != null) { sortTask.cancel(); sortTask = null; } if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } - if (heartbeatTask != null) { heartbeatTask.cancel(); heartbeatTask = null; } // BungeeCord NEU + if (heartbeatTask != null) { heartbeatTask.cancel(); heartbeatTask = null; } + if (restResortTask != null) { restResortTask.cancel(); restResortTask = null; } - savePlayerData(); + // Mülltruchen Auto-Task stoppen + if (trashChestManager != null) trashChestManager.stopAutoTrashTask(); + + savePlayerDataSync(); // Sync-Save beim Shutdown, damit keine Daten verloren gehen if (mysqlEnabled && mysqlManager != null) { mysqlManager.disconnect(); } @@ -559,6 +754,183 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return player.isOp() || player.hasPermission("autosortchest.admin"); } + /** Gibt true zurück wenn der Schild-Text eine Zieltruche kennzeichnet ("ziel" oder "target"). */ + private boolean isZiel(String text) { + return text.equalsIgnoreCase("ziel") || text.equalsIgnoreCase("target"); + } + + /** Gibt true zurück wenn der Schild-Text eine Mülltruhe kennzeichnet ("müll", "mull" oder "trash"). */ + private boolean isMuell(String text) { + return text.equalsIgnoreCase("trash") || text.equalsIgnoreCase("müll") || text.equalsIgnoreCase("mull"); + } + + /** + * Gibt einen sprachabhängigen Schildtext zurück (basierend auf config.language). + * Schlüssel: + * "target" → DE: "ziel" | EN: "target" + * "input_clean" → DE: "Eingang" | EN: "Input" + * "trash_clean" → DE: "Müll" | EN: "Trash" + * "public" → DE: "Öffentlich" | EN: "Public" + * "private" → DE: "Privat" | EN: "Private" + * Alle anderen Keys werden unverändert zurückgegeben ("input", "rest", "trash" sind sprachidentisch). + */ + private String getSignLabel(String key) { + boolean isEn = "en".equalsIgnoreCase(config != null ? config.getString("language", "de") : "de"); + switch (key) { + case "target": return isEn ? "target" : "ziel"; + case "input_clean": return isEn ? "Input" : "Eingang"; + case "trash_clean": return isEn ? "Trash" : "Müll"; + case "trash": return isEn ? "trash" : "müll"; + case "public": return isEn ? "Public" : "Öffentlich"; + case "private": return isEn ? "Private" : "Privat"; + default: return key; + } + } + + /** Gibt true zurück wenn der "Saubere Schild"-Modus für Zieltruhen aktiviert ist. */ + private boolean isCleanSignMode() { + return getConfig().getBoolean("sign-style.clean-target", false); + } + + /** + * Marker der in Zeile 4 des sauberen Schildes steht, damit das Plugin + * den Schildtyp noch erkennen kann. §8 = dunkelgrau, kaum sichtbar. + * Format: "§8[asc:target]" oder "§8[asc:target][Public]" + */ + private static final String CLEAN_SIGN_MARKER = "\u00a78[asc:target]"; + private static final String CLEAN_SIGN_MARKER_INPUT = "\u00a78[asc:input]"; + private static final String CLEAN_SIGN_MARKER_REST = "\u00a78[asc:rest]"; + private static final String CLEAN_SIGN_MARKER_TRASH = "\u00a78[asc:trash]"; + + /** Gibt true zurück wenn ein Schild im "sauberen" Format ist (Marker auf Zeile 4). */ + private boolean isCleanTargetSign(Sign sign) { + String line3 = ChatColor.stripColor(sign.getLine(3)); + return line3.contains("[asc:target]"); + } + + private boolean isCleanInputSign(Sign sign) { + String line3 = ChatColor.stripColor(sign.getLine(3)); + return line3.contains("[asc:input]"); + } + + private boolean isCleanRestSign(Sign sign) { + String line3 = ChatColor.stripColor(sign.getLine(3)); + return line3.contains("[asc:rest]"); + } + + private boolean isCleanTrashSign(Sign sign) { + String line3 = ChatColor.stripColor(sign.getLine(3)); + return line3.contains("[asc:trash]"); + } + + /** Gibt true zurück wenn ein Schild IRGENDEINEN Clean-Marker trägt. */ + private boolean isAnyCleanSign(Sign sign) { + String line3 = ChatColor.stripColor(sign.getLine(3)); + return line3.contains("[asc:target]") || line3.contains("[asc:input]") + || line3.contains("[asc:rest]") || line3.contains("[asc:trash]"); + } + + /** Liest den Typ aus einem beliebigen Clean-Schild ("target","input","rest","trash") oder null. */ + private String getCleanSignType(Sign sign) { + String line3 = ChatColor.stripColor(sign.getLine(3)); + if (line3.contains("[asc:target]")) return "target"; + if (line3.contains("[asc:input]")) return "input"; + if (line3.contains("[asc:rest]")) return "rest"; + if (line3.contains("[asc:trash]")) return "trash"; + return null; + } + + /** + * Liest den Besitzernamen aus einem beliebigen Clean-Schild. + * Target: Zeile 1; Input/Rest/Trash: Zeile 0. + */ + private String getCleanSignOwnerAny(Sign sign) { + String type = getCleanSignType(sign); + if ("target".equals(type)) return ChatColor.stripColor(sign.getLine(1)); + return ChatColor.stripColor(sign.getLine(0)); + } + + /** Schreibt ein Input-Schild im sauberen Format. */ + private void applyCleanInputSign(Sign sign, String ownerName, boolean isPublic) { + String publicLabel = getCleanSignColor("input", "line3") + (isPublic ? getSignLabel("public") : getSignLabel("private")); + String line3marker = CLEAN_SIGN_MARKER_INPUT + (isPublic ? "[Public]" : ""); + sign.setLine(0, getCleanSignColor("input", "line1") + ownerName); + sign.setLine(1, getCleanSignColor("input", "line2") + getSignLabel("input_clean")); + sign.setLine(2, publicLabel); + sign.setLine(3, line3marker); + } + + /** Schreibt ein Rest-Schild im sauberen Format. */ + private void applyCleanRestSign(Sign sign, String ownerName, boolean isPublic, boolean isFull) { + String colorType = isFull ? "full" : "rest"; + String publicLabel = getCleanSignColor(colorType, "line3") + (isPublic ? getSignLabel("public") : getSignLabel("private")); + String line3marker = CLEAN_SIGN_MARKER_REST + (isPublic ? "[Public]" : ""); + sign.setLine(0, getCleanSignColor(colorType, "line1") + ownerName); + sign.setLine(1, getCleanSignColor(colorType, "line2") + "Rest"); + sign.setLine(2, publicLabel); + sign.setLine(3, line3marker); + } + + /** Schreibt ein Trash-Schild im sauberen Format. */ + private void applyCleanTrashSign(Sign sign, String ownerName) { + sign.setLine(0, getCleanSignColor("trash", "line1") + ownerName); + sign.setLine(1, getCleanSignColor("trash", "line2") + getSignLabel("trash_clean")); + sign.setLine(2, ""); + sign.setLine(3, CLEAN_SIGN_MARKER_TRASH); + } + + /** Schreibt ein Schild im sauberen Format. */ + private void applyCleanTargetSign(Sign sign, String itemName, String ownerName, boolean isPublic, boolean isFull) { + String colorType = isFull ? "full" : "target"; + + // Formatiert "IRON_ORE" -> "Iron Ore", begrenzt auf 15 Zeichen fuer das Schild + String displayItem = TrashChestManager.formatMaterialName(itemName); + if (displayItem.length() > 15) displayItem = displayItem.substring(0, 15); + + String publicLabel = getCleanSignColor(colorType, "line3") + (isPublic ? getSignLabel("public") : getSignLabel("private")); + String line3marker = CLEAN_SIGN_MARKER + (isPublic ? "[Public]" : ""); + + sign.setLine(0, getCleanSignColor(colorType, "line1") + displayItem); + sign.setLine(1, getCleanSignColor(colorType, "line2") + ownerName); + sign.setLine(2, publicLabel); + sign.setLine(3, line3marker); // Typ-Marker + Public-Flag (kaum sichtbar) + } + + /** Liest den Spielernamen aus einem sauberen Target-Schild (Zeile 2). */ + private String getCleanSignOwner(Sign sign) { + return ChatColor.stripColor(sign.getLine(1)); + } + + /** + * Findet den Item-Typ, der einer Truhen-Location als Zieltruhe zugeordnet ist. + * Wird im Clean-Sign-Modus benötigt, da der Item-Name nicht mehr auf dem Schild steht. + * Gibt den Material-Namen (z. B. "IRON_ORE") oder null zurück. + */ + private String findItemForChestLocation(UUID ownerUUID, Location loc) { + if (loc == null || ownerUUID == null) return null; + // FIX: MySQL-Pfad war bisher leer – jetzt echte DB-Abfrage + if (mysqlEnabled && mysqlManager != null) { + return mysqlManager.getItemForLocation( + ownerUUID.toString(), + loc.getWorld().getName(), + loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + } + // YAML – alle Slots aller Items durchsuchen + Map> allTargets = getAllTargetChestSlotsYaml(ownerUUID); + for (Map.Entry> entry : allTargets.entrySet()) { + for (Location stored : entry.getValue()) { + if (stored != null && stored.getWorld() != null + && stored.getWorld().equals(loc.getWorld()) + && stored.getBlockX() == loc.getBlockX() + && stored.getBlockY() == loc.getBlockY() + && stored.getBlockZ() == loc.getBlockZ()) { + return entry.getKey(); + } + } + } + return null; + } + private List getChestBlocks(Chest chest) { List blocks = new ArrayList<>(); InventoryHolder holder = chest.getInventory().getHolder(); @@ -577,6 +949,38 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { getLogger().warning("Kann players.yml nicht speichern: playerData oder playerDataFile ist null"); return; } + // FIX: Dirty-Flag setzen statt sofort synchron schreiben. + // Der eigentliche Schreibvorgang erfolgt asynchron über flushPlayerData(). + playerDataDirty = true; + } + + /** + * Schreibt players.yml asynchron auf Disk, wenn dirty. + * Wird vom cleanTask (alle 60s) und von onDisable() aufgerufen. + * Das `saveInProgress`-Flag verhindert parallele Schreibvorgänge. + */ + private void flushPlayerData() { + if (!playerDataDirty || playerData == null || playerDataFile == null) return; + if (saveInProgress) return; + saveInProgress = true; + playerDataDirty = false; + final FileConfiguration snapshot = playerData; + final File target = playerDataFile; + Bukkit.getScheduler().runTaskAsynchronously(this, () -> { + try { + snapshot.save(target); + } catch (IOException e) { + getLogger().warning("Fehler beim asynchronen Speichern von players.yml: " + e.getMessage()); + playerDataDirty = true; // Erneuter Versuch beim nächsten Flush + } finally { + saveInProgress = false; + } + }); + } + + /** Speichert players.yml sofort synchron (nur für onDisable!). */ + private void savePlayerDataSync() { + if (playerData == null || playerDataFile == null) return; try { playerData.save(playerDataFile); } catch (IOException e) { @@ -597,135 +1001,117 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (playerData.getConfigurationSection("players") == null) { getLogger().warning("Abschnitt 'players' in players.yml fehlt. Initialisiere leer."); playerData.createSection("players"); - savePlayerData(); + savePlayerDataSync(); } else { int playerCount = playerData.getConfigurationSection("players").getKeys(false).size(); getLogger().info("players.yml geladen mit " + playerCount + " Spieler-Einträgen"); } + buildYamlPublicCache(); } catch (Exception e) { getLogger().warning("Fehler beim Laden von players.yml: " + e.getMessage()); playerData = new YamlConfiguration(); playerData.createSection("players"); - savePlayerData(); + savePlayerDataSync(); } } + /** + * Aktualisiert die config.yml des Servers: + * - Fehlende Keys werden aus der mitgelieferten Standard-config.yml ergänzt. + * - Bestehende Einstellungen des Users werden NICHT überschrieben. + * - Die config-version wird auf CONFIG_VERSION aktualisiert. + * + * Dank des generischen Ansatzes müssen bei neuen Keys in config.yml + * KEINE Codeänderungen mehr vorgenommen werden – sie werden automatisch erkannt. + */ private void updateConfig() { File configFile = new File(getDataFolder(), "config.yml"); + + // Standard-Config aus dem Jar laden InputStream defaultConfigStream = getResource("config.yml"); FileConfiguration defaultConfig; - if (defaultConfigStream == null) { - getLogger().warning("Standard-config.yml nicht gefunden in Plugin-Ressourcen! Verwende Fallback-Werte."); - defaultConfig = new YamlConfiguration(); - } else { - try { - defaultConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(defaultConfigStream)); - getLogger().info("Standard-config.yml erfolgreich geladen."); - } catch (Exception e) { - getLogger().warning("Fehler beim Laden von Standard-config.yml: " + e.getMessage()); - defaultConfig = new YamlConfiguration(); - } + getLogger().warning("Standard-config.yml nicht im Jar gefunden! Überspringe Config-Update."); + return; + } + try { + defaultConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(defaultConfigStream)); + } catch (Exception e) { + getLogger().warning("Fehler beim Laden der Standard-config.yml: " + e.getMessage()); + return; } + // Config-Version aktualisieren (immer überschreiben, da interne Versionsnummer) String currentVersion = config.getString("version", "1.0"); if (!currentVersion.equals(CONFIG_VERSION)) { - getLogger().info("Aktualisiere config.yml von Version " + currentVersion + " auf " + CONFIG_VERSION); + getLogger().info("Config-Version wird von " + currentVersion + " auf " + CONFIG_VERSION + " aktualisiert."); config.set("version", CONFIG_VERSION); } - if (!config.contains("debug")) config.set("debug", defaultConfig.getBoolean("debug", false)); - if (!config.contains("language")) config.set("language", defaultConfig.getString("language", "de")); - if (!config.contains("server_crosslink")) config.set("server_crosslink", defaultConfig.getBoolean("server_crosslink", true)); + // Alle Keys der Standard-Config rekursiv in die User-Config übernehmen, + // sofern sie dort noch nicht vorhanden sind. + int addedKeys = mergeDefaults(defaultConfig, config); - // ── BungeeCord NEU: server_name ─────────────────────────────────────── - if (!config.contains("server_name")) config.set("server_name", defaultConfig.getString("server_name", "")); - - if (!config.contains("effects")) { - config.createSection("effects"); - config.set("effects.enabled", defaultConfig.getBoolean("effects.enabled", true)); - config.set("effects.sound", defaultConfig.getBoolean("effects.sound", true)); - config.set("effects.type", defaultConfig.getString("effects.type", "DUST")); + if (addedKeys > 0) { + getLogger().info(addedKeys + " neue Config-Key(s) aus der Standard-config.yml ergänzt."); } - if (!config.contains("sign-colors.input")) { - config.createSection("sign-colors.input"); - config.set("sign-colors.input.line1", defaultConfig.getString("sign-colors.input.line1", "&6")); - config.set("sign-colors.input.line2", defaultConfig.getString("sign-colors.input.line2", "&0")); - config.set("sign-colors.input.line4", defaultConfig.getString("sign-colors.input.line4", "&1")); - } else { - if (!config.contains("sign-colors.input.line1")) config.set("sign-colors.input.line1", defaultConfig.getString("sign-colors.input.line1", "&6")); - if (!config.contains("sign-colors.input.line2")) config.set("sign-colors.input.line2", defaultConfig.getString("sign-colors.input.line2", "&0")); - if (!config.contains("sign-colors.input.line4")) config.set("sign-colors.input.line4", defaultConfig.getString("sign-colors.input.line4", "&1")); + // Gespeichert wird nur wenn nötig (Version geändert ODER neue Keys hinzugefügt) + if (!currentVersion.equals(CONFIG_VERSION) || addedKeys > 0) { + try { + config.save(configFile); + getLogger().info("config.yml erfolgreich gespeichert."); + } catch (IOException e) { + getLogger().warning("Fehler beim Speichern der config.yml: " + e.getMessage()); + } } + } - if (!config.contains("sign-colors.target")) { - config.createSection("sign-colors.target"); - config.set("sign-colors.target.line1", defaultConfig.getString("sign-colors.target.line1", "&6")); - config.set("sign-colors.target.line2", defaultConfig.getString("sign-colors.target.line2", "&0")); - config.set("sign-colors.target.line3", defaultConfig.getString("sign-colors.target.line3", "&f")); - config.set("sign-colors.target.line4", defaultConfig.getString("sign-colors.target.line4", "&1")); - } else { - if (!config.contains("sign-colors.target.line1")) config.set("sign-colors.target.line1", defaultConfig.getString("sign-colors.target.line1", "&6")); - if (!config.contains("sign-colors.target.line2")) config.set("sign-colors.target.line2", defaultConfig.getString("sign-colors.target.line2", "&0")); - if (!config.contains("sign-colors.target.line3")) config.set("sign-colors.target.line3", defaultConfig.getString("sign-colors.target.line3", "&f")); - if (!config.contains("sign-colors.target.line4")) config.set("sign-colors.target.line4", defaultConfig.getString("sign-colors.target.line4", "&1")); - } - - if (!config.contains("sign-colors.full")) { - config.createSection("sign-colors.full"); - config.set("sign-colors.full.line1", defaultConfig.getString("sign-colors.full.line1", "&c")); - config.set("sign-colors.full.line2", defaultConfig.getString("sign-colors.full.line2", "&4")); - config.set("sign-colors.full.line3", defaultConfig.getString("sign-colors.full.line3", "&e")); - config.set("sign-colors.full.line4", defaultConfig.getString("sign-colors.full.line4", "&1")); - } else { - if (!config.contains("sign-colors.full.line1")) config.set("sign-colors.full.line1", defaultConfig.getString("sign-colors.full.line1", "&c")); - if (!config.contains("sign-colors.full.line2")) config.set("sign-colors.full.line2", defaultConfig.getString("sign-colors.full.line2", "&4")); - if (!config.contains("sign-colors.full.line3")) config.set("sign-colors.full.line3", defaultConfig.getString("sign-colors.full.line3", "&e")); - if (!config.contains("sign-colors.full.line4")) config.set("sign-colors.full.line4", defaultConfig.getString("sign-colors.full.line4", "&1")); - } - - if (!config.contains("sign-colors.rest")) { - config.createSection("sign-colors.rest"); - config.set("sign-colors.rest.line1", defaultConfig.getString("sign-colors.rest.line1", "&6")); - config.set("sign-colors.rest.line2", defaultConfig.getString("sign-colors.rest.line2", "&0")); - config.set("sign-colors.rest.line3", defaultConfig.getString("sign-colors.rest.line3", "&f")); - config.set("sign-colors.rest.line4", defaultConfig.getString("sign-colors.rest.line4", "&1")); - } else { - if (!config.contains("sign-colors.rest.line1")) config.set("sign-colors.rest.line1", defaultConfig.getString("sign-colors.rest.line1", "&6")); - if (!config.contains("sign-colors.rest.line2")) config.set("sign-colors.rest.line2", defaultConfig.getString("sign-colors.rest.line2", "&0")); - if (!config.contains("sign-colors.rest.line3")) config.set("sign-colors.rest.line3", defaultConfig.getString("sign-colors.rest.line3", "&f")); - if (!config.contains("sign-colors.rest.line4")) config.set("sign-colors.rest.line4", defaultConfig.getString("sign-colors.rest.line4", "&1")); - } - - if (!config.contains("messages.no-chest-near-sign")) config.set("messages.no-chest-near-sign", defaultConfig.getString("messages.no-chest-near-sign", "&cKeine Truhe in der Nähe des Schildes!")); - if (!config.contains("messages.no-item-in-hand")) config.set("messages.no-item-in-hand", defaultConfig.getString("messages.no-item-in-hand", "&cDu musst ein Item in der Hand halten!")); - if (!config.contains("messages.not-your-chest")) config.set("messages.not-your-chest", defaultConfig.getString("messages.not-your-chest", "&cDiese Truhe gehört dir nicht!")); - if (!config.contains("messages.input-chest-set")) config.set("messages.input-chest-set", defaultConfig.getString("messages.input-chest-set", "&aEingangstruhe erfolgreich gesetzt!")); - if (!config.contains("messages.target-chest-set")) config.set("messages.target-chest-set", defaultConfig.getString("messages.target-chest-set", "&aZieltruhe erfolgreich für %item% eingerichtet!")); - if (!config.contains("messages.rest-chest-set")) config.set("messages.rest-chest-set", defaultConfig.getString("messages.rest-chest-set", "&aRest-Truhe (Fallback) erfolgreich gesetzt!")); - if (!config.contains("messages.target-chest-missing")) config.set("messages.target-chest-missing", defaultConfig.getString("messages.target-chest-missing", "&cZieltruhe für %item% fehlt!")); - - if (!config.contains("messages.target-chest-full")) { - config.set("messages.target-chest-full", defaultConfig.getString("messages.target-chest-full", "&cZieltruhe für %item% ist voll! Koordinaten: (%x%, %y%, %z%)")); - } - - if (!config.contains("messages.mode-changed")) config.set("messages.mode-changed", defaultConfig.getString("messages.mode-changed", "&aModus gewechselt: &e%mode%")); - if (!config.contains("messages.mode-public")) config.set("messages.mode-public", defaultConfig.getString("messages.mode-public", "&aÖffentlich")); - if (!config.contains("messages.mode-private")) config.set("messages.mode-private", defaultConfig.getString("messages.mode-private", "&cPrivat")); - if (!config.contains("messages.no-permission")) config.set("messages.no-permission", defaultConfig.getString("messages.no-permission", "&cDu hast keine Berechtigung für diesen Befehl!")); - if (!config.contains("messages.reload-success")) config.set("messages.reload-success", defaultConfig.getString("messages.reload-success", "&aKonfiguration erfolgreich neu geladen!")); - if (!config.contains("messages.sign-break-denied")) config.set("messages.sign-break-denied", defaultConfig.getString("messages.sign-break-denied", "&cDu musst Shift gedrückt halten, um dieses Schild oder die Truhe abzubauen!")); - - try { - config.save(configFile); - } catch (IOException e) { - getLogger().warning("Fehler beim Speichern der config.yml: " + e.getMessage()); + /** + * Fügt rekursiv alle Keys aus {@code defaults} in {@code target} ein, + * die dort noch nicht vorhanden sind. Sections werden dabei korrekt + * als Abschnitte behandelt, nicht als einfache Werte. + * + * @param defaults Die Standard-Konfiguration (aus dem Jar). + * @param target Die Ziel-Konfiguration (config.yml des Servers). + * @return Anzahl der hinzugefügten Keys. + */ + private int mergeDefaults(FileConfiguration defaults, FileConfiguration target) { + return mergeSection(defaults, target, ""); + } + + private int mergeSection(org.bukkit.configuration.ConfigurationSection defaults, + org.bukkit.configuration.ConfigurationSection target, + String pathPrefix) { + int count = 0; + for (String key : defaults.getKeys(false)) { + String fullPath = pathPrefix.isEmpty() ? key : pathPrefix + "." + key; + + if (defaults.isConfigurationSection(key)) { + // Abschnitt: tiefer rekursieren, section anlegen falls nötig + org.bukkit.configuration.ConfigurationSection defaultSection = defaults.getConfigurationSection(key); + org.bukkit.configuration.ConfigurationSection targetSection = target.getConfigurationSection(key); + if (targetSection == null) { + targetSection = target.createSection(key); + } + count += mergeSection(defaultSection, targetSection, fullPath); + } else { + // Einzelner Wert: nur einfügen wenn noch nicht vorhanden + if (!target.contains(key)) { + target.set(key, defaults.get(key)); + if (getConfig().getBoolean("debug", false)) { + getLogger().info("[Config-Update] Neuer Key ergänzt: " + fullPath + " = " + defaults.get(key)); + } + count++; + } + } } + return count; } private Location getLocationFromPath(String path) { String worldName = playerData.getString(path + ".world"); + if (worldName == null) return null; // FIX: prevent NPE when world key missing World world = getServer().getWorld(worldName); if (world == null) return null; int x = playerData.getInt(path + ".x"); @@ -762,10 +1148,18 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (migrated) savePlayerData(); } + /** + * Aktualisiert alle vorhandenen ASC-Schilder auf dem Server: + * - Wendet die aktuelle Sprache (config.language) an. + * - Konvertiert Schilder zwischen normalem und sauberem Format (sign-style.clean-target). + * - Wird bei Plugin-Start und bei /asc reload automatisch aufgerufen. + */ private void updateExistingSigns() { if (playerData == null) return; if (playerData.getConfigurationSection("players") == null) return; + int updatedSigns = 0; + for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { UUID playerUUID; try { playerUUID = UUID.fromString(uuidString); } catch (IllegalArgumentException e) { continue; } @@ -773,8 +1167,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String basePath = "players." + uuidString; String inputListPath = basePath + ".input-chests"; String oldInputPath = basePath + ".input-chest"; - List inputLocs = new ArrayList<>(); + // ── Eingangstruhen ──────────────────────────────────────────────── + List inputLocs = new ArrayList<>(); if (playerData.contains(inputListPath)) { for (String chestId : playerData.getConfigurationSection(inputListPath).getKeys(false)) { Location loc = getLocationFromPath(inputListPath + "." + chestId); @@ -785,89 +1180,203 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (loc != null) inputLocs.add(loc); } - for (Location chestLocation : inputLocs) { - Block chestBlock = chestLocation.getBlock(); + for (Location loc : inputLocs) { + Block chestBlock = loc.getBlock(); if (!(chestBlock.getState() instanceof Chest)) continue; - List blocks = getChestBlocks((Chest) chestBlock.getState()); - for (Block b : blocks) { - for (Block face : new Block[]{b.getRelative(1,0,0), b.getRelative(-1,0,0), b.getRelative(0,0,1), b.getRelative(0,0,-1)}) { - if (face.getState() instanceof Sign sign && isSignAttachedToChest(face, b)) { - String[] lines = sign.getLines(); - String line0 = ChatColor.stripColor(lines[0] != null ? lines[0] : ""); - String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); - String line3 = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); - if (line0.equalsIgnoreCase("[asc]") && line1.equalsIgnoreCase("input")) { - sign.setLine(0, getSignColor("input", "line1") + "[asc]"); - sign.setLine(1, getSignColor("input", "line2") + "input"); - sign.setLine(3, getSignColor("input", "line4") + line3); - sign.update(); - } - } + for (Block b : getChestBlocks((Chest) chestBlock.getState())) { + for (Block face : adjacentFaces(b)) { + if (!(face.getState() instanceof Sign sign)) continue; + if (!isSignAttachedToChest(face, b)) continue; + if (!isAscSign(sign, "input")) continue; + String owner = getOwnerFromSign(sign); + boolean pub = isChestPublic(sign); + updateSignToCurrentStyle(sign, "input", null, owner, pub, false); + updatedSigns++; } } } + // ── Zieltruhen ──────────────────────────────────────────────────── String targetPath = basePath + ".target-chests"; if (playerData.contains(targetPath)) { for (String itemType : playerData.getConfigurationSection(targetPath).getKeys(false)) { - String targetChestPath = targetPath + "." + itemType; - Location chestLocation = getLocationFromPath(targetChestPath); - if (chestLocation == null) continue; - Block chestBlock = chestLocation.getBlock(); - if (!(chestBlock.getState() instanceof Chest)) continue; - Chest chest = (Chest) chestBlock.getState(); - boolean isFull = isInventoryFull(chest.getInventory()); - List blocks = getChestBlocks(chest); - for (Block b : blocks) { - for (Block face : new Block[]{b.getRelative(1,0,0), b.getRelative(-1,0,0), b.getRelative(0,0,1), b.getRelative(0,0,-1)}) { - if (face.getState() instanceof Sign sign && isSignAttachedToChest(face, b)) { - String[] lines = sign.getLines(); - String line0 = ChatColor.stripColor(lines[0] != null ? lines[0] : ""); - String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); - String line2 = ChatColor.stripColor(lines[2] != null ? lines[2] : ""); - String line3 = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); - if (line0.equalsIgnoreCase("[asc]") && line1.equalsIgnoreCase("ziel")) { - String colorType = isFull ? "full" : "target"; - sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); - sign.setLine(1, getSignColor(colorType, "line2") + "ziel"); - sign.setLine(2, getSignColor(colorType, "line3") + line2); - sign.setLine(3, getSignColor(colorType, "line4") + line3); - sign.update(); - } + // FIX: support old flat + new slotted format + for (Location loc : getTargetChestSlotsYaml(playerUUID, itemType)) { + if (loc == null) continue; + Block chestBlock = loc.getBlock(); + if (!(chestBlock.getState() instanceof Chest)) continue; + Chest chest = (Chest) chestBlock.getState(); + boolean isFull = isInventoryFull(chest.getInventory()); + for (Block b : getChestBlocks(chest)) { + for (Block face : adjacentFaces(b)) { + if (!(face.getState() instanceof Sign sign)) continue; + if (!isSignAttachedToChest(face, b)) continue; + if (!isAscSign(sign, "target")) continue; + String owner = getOwnerFromSign(sign); + boolean pub = isChestPublic(sign); + updateSignToCurrentStyle(sign, "target", itemType, owner, pub, isFull); + updatedSigns++; } } } } } - // BUG FIX: Alle Rest-Truhen iterieren (neues Multi-Format + Legacy) - List restLocations = getRestChestLocations(playerUUID); - for (Location chestLocation : restLocations) { - if (chestLocation == null) continue; - Block chestBlock = chestLocation.getBlock(); + // ── Rest-Truhen ─────────────────────────────────────────────────── + for (Location loc : getRestChestLocations(playerUUID)) { + if (loc == null) continue; + Block chestBlock = loc.getBlock(); if (!(chestBlock.getState() instanceof Chest)) continue; - Chest chest = (Chest) chestBlock.getState(); + Chest chest = (Chest) chestBlock.getState(); boolean isFull = isInventoryFull(chest.getInventory()); - List blocks = getChestBlocks(chest); - for (Block b : blocks) { - for (Block face : new Block[]{b.getRelative(1,0,0), b.getRelative(-1,0,0), b.getRelative(0,0,1), b.getRelative(0,0,-1)}) { - if (face.getState() instanceof Sign sign && isSignAttachedToChest(face, b)) { - String[] lines = sign.getLines(); - String line0 = ChatColor.stripColor(lines[0] != null ? lines[0] : ""); - String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); - String line3 = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); - if (line0.equalsIgnoreCase("[asc]") && line1.equalsIgnoreCase("rest")) { - String colorType = isFull ? "full" : "rest"; - sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); - sign.setLine(1, getSignColor(colorType, "line2") + "rest"); - sign.setLine(3, getSignColor(colorType, "line4") + line3); - sign.update(); - } - } + for (Block b : getChestBlocks(chest)) { + for (Block face : adjacentFaces(b)) { + if (!(face.getState() instanceof Sign sign)) continue; + if (!isSignAttachedToChest(face, b)) continue; + if (!isAscSign(sign, "rest")) continue; + String owner = getOwnerFromSign(sign); + boolean pub = isChestPublic(sign); + updateSignToCurrentStyle(sign, "rest", null, owner, pub, isFull); + updatedSigns++; } } } } + + // ── Mülltruhen ──────────────────────────────────────────────────────── + if (trashChestManager != null) { + for (Map.Entry entry : trashChestManager.getAllTrashChests().entrySet()) { + UUID ownerUUID = entry.getKey(); + Location loc = entry.getValue(); + if (loc == null) continue; + Block chestBlock = loc.getBlock(); + if (!(chestBlock.getState() instanceof Chest)) continue; + String ownerName = ""; + org.bukkit.OfflinePlayer op = Bukkit.getOfflinePlayer(ownerUUID); + if (op.getName() != null) ownerName = op.getName(); + for (Block b : getChestBlocks((Chest) chestBlock.getState())) { + for (Block face : adjacentFaces(b)) { + if (!(face.getState() instanceof Sign sign)) continue; + if (!isSignAttachedToChest(face, b)) continue; + if (!isAscSign(sign, "trash")) continue; + updateSignToCurrentStyle(sign, "trash", null, ownerName, false, false); + updatedSigns++; + } + } + } + } + + if (updatedSigns > 0) { + getLogger().info(updatedSigns + " ASC-Schild(er) aktualisiert (Sprache/Stil)."); + } + } + + /** + * Schreibt ein ASC-Schild im aktuell konfigurierten Stil (clean oder normal) + * mit der aktuellen Sprache neu. + * Konvertiert dabei automatisch zwischen den Formaten, falls nötig. + * + * @param sign Das zu aktualisierende Schild. + * @param type "input", "target", "rest" oder "trash". + * @param itemName Item-Name (nur für Zieltruchen relevant, sonst null). + * @param ownerName Spielername des Besitzers. + * @param isPublic Ob die Truhe öffentlich ist. + * @param isFull Ob die Truhe als voll markiert werden soll. + */ + private void updateSignToCurrentStyle(Sign sign, String type, String itemName, + String ownerName, boolean isPublic, boolean isFull) { + if (isCleanSignMode()) { + // ── Sauberes Format ─────────────────────────────────────────────── + switch (type) { + case "input": applyCleanInputSign(sign, ownerName, isPublic); break; + case "target": applyCleanTargetSign(sign, itemName != null ? itemName : "", ownerName, isPublic, isFull); break; + case "rest": applyCleanRestSign(sign, ownerName, isPublic, isFull); break; + case "trash": applyCleanTrashSign(sign, ownerName); break; + } + } else { + // ── Normales Format ─────────────────────────────────────────────── + String pubSuffix = isPublic ? " [Public]" : ""; + switch (type) { + case "input": { + sign.setLine(0, getSignColor("input", "line1") + "[asc]"); + sign.setLine(1, getSignColor("input", "line2") + "input"); + sign.setLine(2, ""); + sign.setLine(3, getSignColor("input", "line4") + ownerName + pubSuffix); + break; + } + case "target": { + String ct = isFull ? "full" : "target"; + sign.setLine(0, getSignColor(ct, "line1") + "[asc]"); + sign.setLine(1, getSignColor(ct, "line2") + getSignLabel("target")); + sign.setLine(2, getSignColor(ct, "line3") + (itemName != null ? itemName : "")); + sign.setLine(3, getSignColor(ct, "line4") + ownerName + pubSuffix); + break; + } + case "rest": { + String ct = isFull ? "full" : "rest"; + sign.setLine(0, getSignColor(ct, "line1") + "[asc]"); + sign.setLine(1, getSignColor(ct, "line2") + "rest"); + sign.setLine(2, ""); + sign.setLine(3, getSignColor(ct, "line4") + ownerName); + break; + } + case "trash": { + sign.setLine(0, getSignColor("trash", "line1") + "[asc]"); + sign.setLine(1, getSignColor("trash", "line2") + getSignLabel("trash")); + sign.setLine(2, ""); + sign.setLine(3, getSignColor("trash", "line4") + ownerName); + break; + } + } + } + sign.update(); + } + + /** + * Liest den Besitzernamen aus einem ASC-Schild, unabhängig davon ob + * es im normalen oder sauberen Format vorliegt. + * Entfernt automatisch den [Public]-Marker und Farbcodes. + */ + private String getOwnerFromSign(Sign sign) { + if (isAnyCleanSign(sign)) { + return getCleanSignOwnerAny(sign); + } + // Normales Format: Besitzer steht auf Zeile 4 (Index 3) + String line3 = ChatColor.stripColor(sign.getLine(3) != null ? sign.getLine(3) : ""); + return line3.replace("[Public]", "").replace("[public]", "").trim(); + } + + /** + * Gibt true zurück wenn ein Schild ein ASC-Schild des angegebenen Typs ist + * (entweder normales Format oder sauberes Clean-Format). + * + * @param sign Das zu prüfende Schild. + * @param type "input", "target", "rest" oder "trash". + */ + private boolean isAscSign(Sign sign, String type) { + // Sauberes Format prüfen + String cleanType = getCleanSignType(sign); + if (cleanType != null) return cleanType.equals(type); + + // Normales Format prüfen + String line0 = ChatColor.stripColor(sign.getLine(0) != null ? sign.getLine(0) : ""); + String line1 = ChatColor.stripColor(sign.getLine(1) != null ? sign.getLine(1) : ""); + if (!line0.equalsIgnoreCase("[asc]")) return false; + switch (type) { + case "input": return line1.equalsIgnoreCase("input"); + case "target": return isZiel(line1); + case "rest": return line1.equalsIgnoreCase("rest"); + case "trash": return isMuell(line1); + default: return false; + } + } + + /** Gibt die 4 horizontal angrenzenden Blöcke zurück (für Schildsuche an Truhen). */ + private Block[] adjacentFaces(Block b) { + return new Block[]{ + b.getRelative(1, 0, 0), b.getRelative(-1, 0, 0), + b.getRelative(0, 0, 1), b.getRelative(0, 0, -1) + }; } private boolean isInventoryFull(Inventory inventory) { @@ -894,88 +1403,246 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { int y = loc.getBlockY(); int z = loc.getBlockZ(); + // FIX: MySQL – TTL-Cache + eine einzige UNION-Query statt 3 separater Queries if (mysqlEnabled && mysqlManager != null) { - try (java.sql.PreparedStatement ps = mysqlManager.getConnection().prepareStatement( - "SELECT `public` FROM asc_input_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1")) { - ps.setString(1, world); ps.setInt(2, x); ps.setInt(3, y); ps.setInt(4, z); - java.sql.ResultSet rs = ps.executeQuery(); - if (rs.next() && rs.getBoolean("public")) return true; - } catch (Exception e) { getLogger().warning("isChestPublic input SQL: " + e.getMessage()); } - - try (java.sql.PreparedStatement ps = mysqlManager.getConnection().prepareStatement( - "SELECT `public` FROM asc_target_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1")) { - ps.setString(1, world); ps.setInt(2, x); ps.setInt(3, y); ps.setInt(4, z); - java.sql.ResultSet rs = ps.executeQuery(); - if (rs.next() && rs.getBoolean("public")) return true; - } catch (Exception e) { getLogger().warning("isChestPublic target SQL: " + e.getMessage()); } - - try (java.sql.PreparedStatement ps = mysqlManager.getConnection().prepareStatement( - "SELECT `public` FROM asc_rest_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1")) { - ps.setString(1, world); ps.setInt(2, x); ps.setInt(3, y); ps.setInt(4, z); - java.sql.ResultSet rs = ps.executeQuery(); - if (rs.next() && rs.getBoolean("public")) return true; - } catch (Exception e) { getLogger().warning("isChestPublic rest SQL: " + e.getMessage()); } + String cacheKey = world + ":" + x + ":" + y + ":" + z; + Long ts = chestPublicCacheTimestamps.get(cacheKey); + if (ts != null && (System.currentTimeMillis() - ts) < CHEST_PUBLIC_CACHE_TTL) { + return chestPublicCache.getOrDefault(cacheKey, false); + } + // Eine einzige UNION-Abfrage statt drei – 66% weniger DB-Overhead + boolean result = mysqlManager.isLocationPublic(world, x, y, z); + chestPublicCache.put(cacheKey, result); + chestPublicCacheTimestamps.put(cacheKey, System.currentTimeMillis()); + return result; } else { - if (playerData != null && playerData.contains("players")) { - for (String uuid : playerData.getConfigurationSection("players").getKeys(false)) { - String inputListPath = "players." + uuid + ".input-chests"; - if (playerData.contains(inputListPath)) { - for (String chestId : playerData.getConfigurationSection(inputListPath).getKeys(false)) { - String path = inputListPath + "." + chestId; - if (world.equals(playerData.getString(path + ".world")) - && x == playerData.getInt(path + ".x") - && y == playerData.getInt(path + ".y") - && z == playerData.getInt(path + ".z")) { - if (playerData.getBoolean(path + ".public", false)) return true; - } - } - } - String targetBase = "players." + uuid + ".target-chests"; - if (playerData.contains(targetBase)) { - for (String item : playerData.getConfigurationSection(targetBase).getKeys(false)) { - String tPath = targetBase + "." + item; - if (world.equals(playerData.getString(tPath + ".world")) - && x == playerData.getInt(tPath + ".x") - && y == playerData.getInt(tPath + ".y") - && z == playerData.getInt(tPath + ".z")) { - if (playerData.getBoolean(tPath + ".public", false)) return true; - } - } - } - // BUG FIX: Neues Multi-Format für Rest-Truhen - String restBasePath = "players." + uuid + ".rest-chests"; - if (playerData.contains(restBasePath)) { - for (String slotKey : playerData.getConfigurationSection(restBasePath).getKeys(false)) { - String rPath = restBasePath + "." + slotKey; - if (world.equals(playerData.getString(rPath + ".world")) - && x == playerData.getInt(rPath + ".x") - && y == playerData.getInt(rPath + ".y") - && z == playerData.getInt(rPath + ".z")) { - if (playerData.getBoolean(rPath + ".public", false)) return true; - } - } - } - // Legacy - String legacyRestPath = "players." + uuid + ".rest-chest"; - if (playerData.contains(legacyRestPath + ".world")) { - if (world.equals(playerData.getString(legacyRestPath + ".world")) - && x == playerData.getInt(legacyRestPath + ".x") - && y == playerData.getInt(legacyRestPath + ".y") - && z == playerData.getInt(legacyRestPath + ".z")) { - if (playerData.getBoolean(legacyRestPath + ".public", false)) return true; - } - } - } - } + // FIX: YAML – O(1) Cache-Lookup statt O(N)-Iteration über alle Spieler + return isPublicInYamlCache(world, x, y, z); } - return false; } private boolean isDebug() { return config != null && config.getBoolean("debug", false); } + // ═══════════════════════════════════════════════════════════════════════ + // Custom-Titel-Inventories + // Wir öffnen die echte Truhen-Inventory (→ Deckel-Animation korrekt) + // und schicken danach per NMS-Reflection ein ClientboundOpenScreenPacket + // mit dem gewünschten Titel nach. Schlägt die Reflection fehl (z.B. bei + // unbekannter Serverversion), erscheint der Standard-Titel "Chest" – + // Animation und Funktionalität bleiben in jedem Fall erhalten. + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Leitet den Fenstertitel für eine ASC-Truhe ab: + * target-Truhe → formatierter Item-Name (z.B. "Iron Ore") + * input-Truhe → "Eingangstruhe" / "Input Chest" + * rest-Truhe → "Rest-Truhe" / "Rest Chest" + * + * @param sign Das Schild an der Truhe (darf null sein). + * @param type "target", "input" oder "rest". + * @return Titel-String mit §-Farbcodes (bereits übersetzt). + */ + private String buildChestTitle(Sign sign, String type) { + boolean isEn = "en".equalsIgnoreCase( + config != null ? config.getString("language", "de") : "de"); + String lang = isEn ? "en" : "de"; + + // Fallback-Werte falls config-Eintrag fehlt + String fallback; + switch (type) { + case "target": fallback = isEn ? "&6%item%" : "&6%item%"; break; + case "input": fallback = isEn ? "&6Input Chest" : "&6Eingangstruhe"; break; + case "rest": fallback = isEn ? "&6Rest Chest" : "&6Rest-Truhe"; break; + case "trash": fallback = isEn ? "&4Trash Chest" : "&4Mülltruhe"; break; + default: fallback = isEn ? "&6Chest" : "&6Truhe"; break; + } + + String template = config != null + ? config.getString("chest-titles." + type + "." + lang, fallback) + : fallback; + + // %item%-Platzhalter für Zieltruhen ersetzen + if (template.contains("%item%")) { + String raw = ""; + if (sign != null) { + raw = isCleanTargetSign(sign) + ? ChatColor.stripColor(sign.getLine(0)) + : ChatColor.stripColor(sign.getLine(2)); + } + String itemLabel = (raw != null && !raw.isEmpty()) + ? TrashChestManager.formatMaterialName(raw) + : (isEn ? "Target Chest" : "Zieltruhe"); + template = template.replace("%item%", itemLabel); + } + + return ChatColor.translateAlternateColorCodes('&', template); + } + + /** + * Öffnet eine ASC-Truhe mit einem benutzerdefinierten Fenstertitel. + *

+ * Strategie: + * 1. Echte Truhen-Inventory öffnen → Deckel-Animation (BLOCK_ACTION-Paket) wird korrekt + * gesendet; {@link #onInventoryClose} erkennt den Holder als Chest und aktualisiert + * das Schild (volle Truhe) ohne separate Inhalts-Synchronisation. + * 2. Einen Tick später wird ein {@code ClientboundOpenScreenPacket} mit dem gewünschten + * Titel per NMS-Reflection nachgeschickt. Schlägt die Reflection fehl, bleibt der + * Standard-Titel "Chest" – alle anderen Funktionen sind davon nicht betroffen. + * + * @param player Der Spieler, für den die Truhe geöffnet wird. + * @param chestBlock Der Truhen-Block. + * @param title Der fertige Fenstertitel (&-Farbcodes werden übersetzt). + */ + private void openTitledChestInventory(Player player, Block chestBlock, String title) { + BlockState state = chestBlock.getState(); + if (!(state instanceof Chest chest)) return; + + // Echte Inventory öffnen → korrekte Deckel-Animation + InventoryHolder für onInventoryClose + player.openInventory(chest.getInventory()); + + // Fenstertitel 1 Tick nach dem Open-Paket per NMS nachsetzen. + // Nach dem ClientboundOpenScreenPacket muss der Inhalt erneut gesendet werden, + // da der Client das Fenster intern neu öffnet und noch keine Items kennt. + if (title != null && !title.isEmpty()) { + final String finalTitle = title; + Bukkit.getScheduler().runTaskLater(this, () -> { + if (player.isOnline()) { + trySetInventoryTitle(player, finalTitle); + // Inhalt erneut schicken damit die Items sofort sichtbar sind + player.updateInventory(); + } + }, 1L); + } + } + + /** + * Versucht den Titel des aktuell geöffneten Inventarfensters per NMS-Reflection + * umzubenennen (Spigot/Paper 1.17–1.21). + * Schlägt ein Schritt fehl, wird die Ausnahme im Debug-Modus geloggt; + * der Spieler sieht dann den Standard-Titel "Chest". + */ + private void trySetInventoryTitle(Player player, String rawTitle) { + try { + String colorTitle = ChatColor.translateAlternateColorCodes('&', rawTitle); + + // 1. NMS-Spieler-Instanz holen + Object nmsPlayer = player.getClass().getMethod("getHandle").invoke(player); + + // 2. Aktuell offenes Container-Menü (Feldname je nach MC-Version/Mapping) + Object menu = reflectFieldAny(nmsPlayer, + "containerMenu", "activeContainer", "bW", "bV", "bU", "bT", "bS"); + if (menu == null) return; + + // 3. Container-ID (> 0; 0 = Spieler-Inventar selbst) + int windowId = reflectIntFieldAny(menu, "containerId", "j", "w", "windowId"); + if (windowId <= 0) return; + + // 4. MenuType des Containers + Object menuType = menu.getClass().getMethod("getType").invoke(menu); + + // 5. NMS-Chat-Komponente für den Titel bauen + Object titleComp = buildNmsComponent(colorTitle); + if (titleComp == null) return; + + // 6. ClientboundOpenScreenPacket instantiieren (3-Parameter-Konstruktor) + Object packet = buildOpenScreenPacket(windowId, menuType, titleComp); + if (packet == null) return; + + // 7. Paket über die Player-Connection senden + Object connection = reflectFieldAny(nmsPlayer, + "connection", "playerConnection", "c", "b"); + if (connection == null) return; + for (java.lang.reflect.Method m : connection.getClass().getMethods()) { + if ("send".equals(m.getName()) && m.getParameterCount() == 1) { + m.invoke(connection, packet); + break; + } + } + + } catch (Exception e) { + if (isDebug()) getLogger().warning( + "[ASC] Titel-Packet fehlgeschlagen (" + e.getClass().getSimpleName() + "): " + e.getMessage()); + } + } + + /** + * Sucht ein Feld anhand mehrerer möglicher Namen in der Klassenhierarchie + * des Objekts und gibt seinen Wert zurück (public zuerst, dann declared). + */ + private Object reflectFieldAny(Object obj, String... names) { + // Public fields (schnellster Pfad, funktioniert für Mojang-gemappte Paper/Spigot 1.17+) + for (String name : names) { + try { return obj.getClass().getField(name).get(obj); } + catch (NoSuchFieldException ignored) {} + catch (Exception e) { break; } + } + // Declared fields über die gesamte Klassenhierarchie (private/protected) + for (Class clazz = obj.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + for (java.lang.reflect.Field f : clazz.getDeclaredFields()) { + for (String name : names) { + if (f.getName().equals(name)) { + try { f.setAccessible(true); return f.get(obj); } + catch (Exception ignored) {} + } + } + } + } + return null; + } + + /** Wie {@link #reflectFieldAny} aber gibt den Wert als int zurück (-1 bei Fehler). */ + private int reflectIntFieldAny(Object obj, String... names) { + Object val = reflectFieldAny(obj, names); + return (val instanceof Integer) ? (int) val : -1; + } + + /** + * Baut eine NMS-Chat-Komponente aus einem Plain-Text-String. + * Versucht nacheinander {@code Component.literal()} (1.19+) und + * {@code ChatComponentText} (1.17–1.18) über Reflection. + */ + private Object buildNmsComponent(String text) { + // Versuch 1: net.minecraft.network.chat.Component.literal(String) – Paper/Spigot 1.19+ + for (String cls : new String[]{ + "net.minecraft.network.chat.Component", + "net.minecraft.network.chat.MutableComponent" }) { + try { + Class c = Class.forName(cls); + try { return c.getMethod("literal", String.class).invoke(null, text); } + catch (Exception ignored) {} // NoSuchMethodException, IllegalAccessException, InvocationTargetException + } catch (ClassNotFoundException ignored) {} + } + // Versuch 2: ChatComponentText(String) – ältere Spigot-Builds 1.17–1.18 + try { + return Class.forName("net.minecraft.network.chat.ChatComponentText") + .getConstructor(String.class).newInstance(text); + } catch (Exception ignored) {} + return null; + } + + /** + * Erstellt ein {@code ClientboundOpenScreenPacket} über Reflection. + * Nimmt den ersten Konstruktor mit genau 3 Parametern (windowId, menuType, title). + */ + private Object buildOpenScreenPacket(int windowId, Object menuType, Object titleComp) { + try { + Class packetClass = Class.forName( + "net.minecraft.network.protocol.game.ClientboundOpenScreenPacket"); + for (java.lang.reflect.Constructor ctor : packetClass.getConstructors()) { + if (ctor.getParameterCount() == 3) { + return ctor.newInstance(windowId, menuType, titleComp); + } + } + } catch (Exception e) { + if (isDebug()) getLogger().warning( + "[ASC] buildOpenScreenPacket fehlgeschlagen: " + e.getMessage()); + } + return null; + } + private String getMessage(String key) { if (config == null) return ChatColor.RED + "Fehlende Konfiguration: " + key; String message = config.getString("messages." + key, "Fehlende Nachricht: " + key); @@ -988,6 +1655,39 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return ChatColor.translateAlternateColorCodes('&', color); } + /** + * Liest eine Farbe aus sign-colors-clean.. (line1/line2/line3). + * Fallbacks spiegeln das bisherige Verhalten wider. + * + * Layout Clean-Modus: + * input / rest / trash -> line1=Spielername, line2=Label, line3=Oeffentlich/Privat + * target / full -> line1=Item-Name, line2=Spielername, line3=Oeffentlich/Privat + */ + private String getCleanSignColor(String type, String line) { + if (config == null) return ChatColor.WHITE.toString(); + String fallback; + switch (type + "." + line) { + case "input.line1": fallback = config.getString("sign-colors.input.line4", "&1"); break; + case "input.line2": fallback = config.getString("sign-colors.input.line2", "&0"); break; + case "input.line3": fallback = "&f"; break; + case "target.line1": fallback = config.getString("sign-colors.target.line3", "&f"); break; + case "target.line2": fallback = config.getString("sign-colors.target.line4", "&1"); break; + case "target.line3": fallback = config.getString("sign-colors.target.line4", "&1"); break; + case "full.line1": fallback = config.getString("sign-colors.full.line3", "&c"); break; + case "full.line2": fallback = config.getString("sign-colors.full.line4", "&1"); break; + case "full.line3": fallback = config.getString("sign-colors.full.line4", "&1"); break; + case "rest.line1": fallback = config.getString("sign-colors.rest.line4", "&1"); break; + case "rest.line2": fallback = config.getString("sign-colors.rest.line2", "&0"); break; + case "rest.line3": fallback = "&f"; break; + case "trash.line1": fallback = config.getString("sign-colors.trash.line4", "&1"); break; + case "trash.line2": fallback = config.getString("sign-colors.trash.line2", "&0"); break; + default: fallback = "&f"; break; + } + String color = config.getString("sign-colors-clean." + type + "." + line, fallback); + return ChatColor.translateAlternateColorCodes('&', color); + } + + private boolean isSignAttachedToChest(Block signBlock, Block chestBlock) { if (!(signBlock.getBlockData() instanceof WallSign)) return false; WallSign wallSign = (WallSign) signBlock.getBlockData(); @@ -1017,7 +1717,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { List> chests = mysqlManager.getInputChests(playerUUID.toString()); for (Map chest : chests) { String chestWorld = (String) chest.get("world"); - if (chestWorld == null) continue; // NPE-Schutz: korrupter DB-Eintrag + if (chestWorld == null) continue; if (chestWorld.equals(location.getWorld().getName()) && ((int) chest.get("x")) == location.getBlockX() && ((int) chest.get("y")) == location.getBlockY() @@ -1027,7 +1727,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } if (chestId == null) chestId = UUID.randomUUID().toString(); - // ── BungeeCord NEU: serverName mitgeben ─────────────────────────── mysqlManager.addInputChest(playerUUID.toString(), chestId, location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic, serverName); @@ -1042,6 +1741,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { && location.getBlockY() == playerData.getInt(path + ".y") && location.getBlockZ() == playerData.getInt(path + ".z")) { playerData.set(path + ".public", isPublic); + updateYamlPublicCache(location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic); savePlayerData(); return; } @@ -1054,6 +1754,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { playerData.set(path + ".y", location.getBlockY()); playerData.set(path + ".z", location.getBlockZ()); playerData.set(path + ".public", isPublic); + updateYamlPublicCache(location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic); savePlayerData(); } } @@ -1068,8 +1769,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return; } if (mysqlEnabled && mysqlManager != null) { - // Nächsten freien Slot für dieses Item ermitteln - // Prüfen ob diese Truhe schon einen Slot hat (Update) int slot = 0; List> existing = mysqlManager.getTargetChestsForItem(playerUUID.toString(), itemType.name()); boolean isUpdate = false; @@ -1090,31 +1789,148 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { isPublic, serverName); mysqlManager.savePlayer(playerUUID.toString(), Bukkit.getOfflinePlayer(playerUUID).getName()); } else { - String path = "players." + playerUUID + ".target-chests." + itemType.name(); + // FIX: YAML-Modus unterstützt jetzt Multi-Target via Slot-Format (wie MySQL). + // Altes Flat-Format wird bei erstem Zugriff automatisch zu Slot 0 migriert. + String base = "players." + playerUUID + ".target-chests." + itemType.name(); + if (isOldTargetFormat(playerUUID, itemType.name())) { + migrateOldTargetChestEntry(playerUUID, itemType.name()); + } + // Prüfen ob diese Location bereits als Slot existiert → Update + int slot = -1; + String slotsBase = base; + if (playerData.contains(slotsBase) && playerData.isConfigurationSection(slotsBase)) { + for (String slotKey : playerData.getConfigurationSection(slotsBase).getKeys(false)) { + // Überspringe alte Flat-Keys (world/x/y/z/public direkt unter item) + if (slotKey.equals("world") || slotKey.equals("x") || slotKey.equals("y") || slotKey.equals("z") || slotKey.equals("public")) continue; + String p = slotsBase + "." + slotKey; + if (location.getWorld().getName().equals(playerData.getString(p + ".world")) + && location.getBlockX() == playerData.getInt(p + ".x") + && location.getBlockY() == playerData.getInt(p + ".y") + && location.getBlockZ() == playerData.getInt(p + ".z")) { + try { slot = Integer.parseInt(slotKey); } catch (NumberFormatException ignored) {} + break; + } + } + } + if (slot < 0) { + // Neuer Slot: nächste freie Nummer + slot = 0; + if (playerData.contains(slotsBase) && playerData.isConfigurationSection(slotsBase)) { + for (String sk : playerData.getConfigurationSection(slotsBase).getKeys(false)) { + if (sk.equals("world") || sk.equals("x") || sk.equals("y") || sk.equals("z") || sk.equals("public")) continue; + try { slot = Math.max(slot, Integer.parseInt(sk) + 1); } catch (NumberFormatException ignored) {} + } + } + } + String path = slotsBase + "." + slot; playerData.set(path + ".world", location.getWorld().getName()); playerData.set(path + ".x", location.getBlockX()); playerData.set(path + ".y", location.getBlockY()); playerData.set(path + ".z", location.getBlockZ()); playerData.set(path + ".public", isPublic); + updateYamlPublicCache(location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic); savePlayerData(); } } + /** 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) { + String path = "players." + playerUUID + ".target-chests." + itemName; + return playerData.contains(path + ".world"); + } + + /** Migriert einen alten Flat-Target-Eintrag zu Slot 0 (idempotent). */ + private void migrateOldTargetChestEntry(UUID playerUUID, String itemName) { + String base = "players." + playerUUID + ".target-chests." + itemName; + String w = playerData.getString(base + ".world"); + if (w == null) return; + int x = playerData.getInt(base + ".x"); + int y = playerData.getInt(base + ".y"); + int z = playerData.getInt(base + ".z"); + boolean pub = playerData.getBoolean(base + ".public", false); + // Alte Flat-Keys entfernen + playerData.set(base + ".world", null); + playerData.set(base + ".x", null); + playerData.set(base + ".y", null); + playerData.set(base + ".z", null); + playerData.set(base + ".public", null); + // Als Slot 0 neu schreiben + playerData.set(base + ".0.world", w); + playerData.set(base + ".0.x", x); + playerData.set(base + ".0.y", y); + playerData.set(base + ".0.z", z); + playerData.set(base + ".0.public", pub); + savePlayerData(); + if (isDebug()) getLogger().info("[YAML-Migration] target-chests." + itemName + " → Slot 0 für " + playerUUID); + } + + /** Gibt alle YAML-Target-Chest-Locations für einen Spieler und Item-Typ zurück. */ + private List getTargetChestSlotsYaml(UUID playerUUID, String itemName) { + List result = new ArrayList<>(); + if (playerData == null) return result; + String base = "players." + playerUUID + ".target-chests." + itemName; + if (!playerData.contains(base)) return result; + // 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; + 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"))); + } + return result; + } + + /** Gibt alle YAML-Target-Chests als Map (itemName → List) zurück. */ + private Map> getAllTargetChestSlotsYaml(UUID playerUUID) { + Map> map = new HashMap<>(); + if (playerData == null) return map; + String base = "players." + playerUUID + ".target-chests"; + if (!playerData.contains(base) || !playerData.isConfigurationSection(base)) return map; + for (String itemName : playerData.getConfigurationSection(base).getKeys(false)) { + List locs = getTargetChestSlotsYaml(playerUUID, itemName); + if (!locs.isEmpty()) map.put(itemName, locs); + } + return map; + } + + /** Entfernt einen spezifischen YAML-Target-Slot anhand der Location. */ + private void removeTargetChestSlotYaml(UUID playerUUID, String itemName, Location loc) { + if (playerData == null) return; + String base = "players." + playerUUID + ".target-chests." + itemName; + if (!playerData.contains(base)) return; + if (isOldTargetFormat(playerUUID, itemName)) migrateOldTargetChestEntry(playerUUID, itemName); + if (!playerData.isConfigurationSection(base)) return; + for (String slotKey : new ArrayList<>(playerData.getConfigurationSection(base).getKeys(false))) { + String p = base + "." + slotKey; + if (loc.getWorld().getName().equals(playerData.getString(p + ".world")) + && loc.getBlockX() == playerData.getInt(p + ".x") + && loc.getBlockY() == playerData.getInt(p + ".y") + && loc.getBlockZ() == playerData.getInt(p + ".z")) { + playerData.set(p, null); + // Abschnitt löschen wenn leer + if (playerData.isConfigurationSection(base) && playerData.getConfigurationSection(base).getKeys(false).isEmpty()) { + playerData.set(base, null); + } + savePlayerData(); + return; + } + } + } + private void setRestChestLocation(UUID playerUUID, Location location) { setRestChestLocation(playerUUID, location, false); } private void setRestChestLocation(UUID playerUUID, Location location, boolean isPublic) { if (mysqlEnabled && mysqlManager != null) { - // ── BungeeCord + Multi-Rest: serverName und auto-slot ───────────────── mysqlManager.setRestChest(playerUUID.toString(), location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic, serverName); mysqlManager.savePlayer(playerUUID.toString(), Bukkit.getOfflinePlayer(playerUUID).getName()); } else { - // YAML: mehrere Rest-Truhen unter rest-chests. String basePath = "players." + playerUUID + ".rest-chests"; - // Prüfen ob diese Location schon registriert ist (Update) if (playerData.contains(basePath)) { for (String slotKey : playerData.getConfigurationSection(basePath).getKeys(false)) { String path = basePath + "." + slotKey; @@ -1123,15 +1939,20 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { && location.getBlockY() == playerData.getInt(path + ".y") && location.getBlockZ() == playerData.getInt(path + ".z")) { playerData.set(path + ".public", isPublic); + updateYamlPublicCache(location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic); savePlayerData(); return; } } } - // Neuen Slot anlegen + // BUG FIX: size() gibt falsche Slot-Nummer wenn Lücken existieren (z.B. nach Löschen). + // Stattdessen: höchsten vorhandenen Slot + 1 verwenden. int nextSlot = 0; if (playerData.contains(basePath)) { - nextSlot = playerData.getConfigurationSection(basePath).getKeys(false).size(); + for (String sk : playerData.getConfigurationSection(basePath).getKeys(false)) { + try { nextSlot = Math.max(nextSlot, Integer.parseInt(sk) + 1); } + catch (NumberFormatException ignored) {} + } } String path = basePath + "." + nextSlot; playerData.set(path + ".world", location.getWorld().getName()); @@ -1139,6 +1960,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { playerData.set(path + ".y", location.getBlockY()); playerData.set(path + ".z", location.getBlockZ()); playerData.set(path + ".public", isPublic); + updateYamlPublicCache(location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic); savePlayerData(); } } @@ -1155,7 +1977,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { result.add(new Location(w, (int) map.get("x"), (int) map.get("y"), (int) map.get("z"))); } } else { - // Neues Multi-Format String basePath = "players." + playerUUID + ".rest-chests"; if (playerData.contains(basePath)) { for (String slotKey : playerData.getConfigurationSection(basePath).getKeys(false)) { @@ -1167,7 +1988,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { playerData.getInt(path + ".y"), playerData.getInt(path + ".z"))); } } - // Legacy-Fallback: altes single rest-chest Format String legacyPath = "players." + playerUUID + ".rest-chest"; if (result.isEmpty() && playerData.contains(legacyPath)) { String worldName = playerData.getString(legacyPath + ".world"); @@ -1191,18 +2011,14 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (mysqlEnabled && mysqlManager != null) { Map map = mysqlManager.getTargetChest(playerUUID.toString(), itemType.name()); if (map == null) return null; - // ── BungeeCord NEU: Remote-Truhen nicht lokal auflösen ───────────── if (isRemoteChest(map)) return null; World w = Bukkit.getWorld((String) map.get("world")); if (w == null) return null; return new Location(w, (int) map.get("x"), (int) map.get("y"), (int) map.get("z")); } else { - String path = "players." + playerUUID + ".target-chests." + itemType.name(); - if (!playerData.contains(path)) return null; - String worldName = playerData.getString(path + ".world"); - World world = getServer().getWorld(worldName); - if (world == null) return null; - return new Location(world, playerData.getInt(path + ".x"), playerData.getInt(path + ".y"), playerData.getInt(path + ".z")); + // FIX: Nutzt neuen Slot-Helper; gibt erste verfügbare Location zurück + List slots = getTargetChestSlotsYaml(playerUUID, itemType.name()); + return slots.isEmpty() ? null : slots.get(0); } } @@ -1214,6 +2030,14 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return messages.isEmpty(); }); cleanFullChestCache(); + // FIX: Abgelaufene Einträge aus dem isChestPublic-Cache entfernen + chestPublicCacheTimestamps.entrySet().removeIf(e -> { + if (currentTime - e.getValue() >= CHEST_PUBLIC_CACHE_TTL) { + chestPublicCache.remove(e.getKey()); + return true; + } + return false; + }); } private String locKey(Location loc) { @@ -1252,7 +2076,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String lang = config != null ? config.getString("language", "de") : "de"; if (lang == null) lang = "de"; - // reload, import und export sind auch von der Konsole erlaubt boolean isPlayer = sender instanceof Player; if (!isPlayer) { if (args.length == 0 || (!args[0].equalsIgnoreCase("reload") @@ -1278,7 +2101,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { infoMessage = infoMessage .replace("%version%", getDescription().getVersion()) .replace("%config_version%", config != null ? config.getString("version", CONFIG_VERSION) : CONFIG_VERSION) - .replace("%server_name%", serverName.isEmpty() ? "(nicht gesetzt)" : serverName) // BungeeCord NEU + .replace("%server_name%", serverName.isEmpty() ? "(nicht gesetzt)" : serverName) .replace("%author%", String.join(", ", getDescription().getAuthors())); infoMessage = ChatColor.translateAlternateColorCodes('&', infoMessage); sender.sendMessage(infoMessage.split("\n")); @@ -1293,10 +2116,16 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (sortTask != null) { sortTask.cancel(); sortTask = null; } if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } - if (heartbeatTask != null) { heartbeatTask.cancel(); heartbeatTask = null; } // BungeeCord NEU + if (heartbeatTask != null) { heartbeatTask.cancel(); heartbeatTask = null; } + if (restResortTask != null) { restResortTask.cancel(); restResortTask = null; } + + // Vorherige Einstellungen JETZT merken (vor dem Reload), um Änderungen zu erkennen + String previousLanguage = config.getString("language", "de"); + boolean previousCleanMode = config.getBoolean("sign-style.clean-target", false); reloadConfig(); config = getConfig(); + loadOptionalSettings(); if (mysqlEnabled) { @@ -1307,7 +2136,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { mysqlManager = new MySQLManager(mysqlHost, mysqlPort, mysqlDatabase, mysqlUser, mysqlPassword); if (mysqlManager.connect()) { mysqlManager.setupTables(); - mysqlManager.setupTransferTable(); // BungeeCord NEU + mysqlManager.setupTransferTable(); getLogger().info("MySQL-Verbindung nach Reload erfolgreich hergestellt."); } else { getLogger().warning("MySQL-Verbindung nach Reload fehlgeschlagen! Fallback auf YAML."); @@ -1323,9 +2152,27 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { loadPlayerData(); updateConfig(); + buildYamlPublicCache(); // FIX: YAML-Public-Cache nach Reload neu aufbauen + + // Änderungen erkennen und loggen + String newLanguage = config.getString("language", "de"); + boolean newCleanMode = config.getBoolean("sign-style.clean-target", false); + if (!newLanguage.equalsIgnoreCase(previousLanguage)) { + getLogger().info("Sprache geändert: " + previousLanguage + " → " + newLanguage + " – Schilder werden aktualisiert."); + } + if (newCleanMode != previousCleanMode) { + getLogger().info("sign-style.clean-target geändert: " + previousCleanMode + " → " + newCleanMode + " – Schilder werden aktualisiert."); + } + updateExistingSigns(); migrateInputChests(); - startTasks(); + + // TrashChestManager neu initialisieren damit geänderte Einstellungen + // (z.B. auto_clear_interval_seconds) sofort wirksam werden + if (trashChestManager != null) trashChestManager.stopAutoTrashTask(); + trashChestManager = new TrashChestManager(this); + + startTasks(); // ruft auch trashChestManager.startAutoTrashTask() auf sender.sendMessage(getMessage("reload-success")); getLogger().info("Plugin erfolgreich neu geladen von " + sender.getName() @@ -1384,17 +2231,36 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } + // BUG FIX: Slotted-Format (0/1/2) UND altes Flat-Format bei Zieltruhen String targetPath = "players." + uuidString + ".target-chests"; if (playerData.contains(targetPath)) { for (String item : playerData.getConfigurationSection(targetPath).getKeys(false)) { - String path = targetPath + "." + item; - String world = playerData.getString(path + ".world"); - int x = playerData.getInt(path + ".x"); - int y = playerData.getInt(path + ".y"); - int z = playerData.getInt(path + ".z"); - boolean isPublic = playerData.getBoolean(path + ".public", false); - mysqlManager.setTargetChest(uuidString, item, world, x, y, z, isPublic); - targetCount++; + String itemBase = targetPath + "." + item; + if (playerData.contains(itemBase + ".world")) { + // Altes Flat-Format + String world = playerData.getString(itemBase + ".world"); + int x = playerData.getInt(itemBase + ".x"); + int y = playerData.getInt(itemBase + ".y"); + int z = playerData.getInt(itemBase + ".z"); + boolean isPublic = playerData.getBoolean(itemBase + ".public", false); + mysqlManager.setTargetChest(uuidString, item, 0, world, x, y, z, isPublic, ""); + targetCount++; + } else if (playerData.isConfigurationSection(itemBase)) { + // Neues Slotted-Format + for (String slotKey : playerData.getConfigurationSection(itemBase).getKeys(false)) { + String slotPath = itemBase + "." + slotKey; + if (!playerData.contains(slotPath + ".world")) continue; + String world = playerData.getString(slotPath + ".world"); + int x = playerData.getInt(slotPath + ".x"); + int y = playerData.getInt(slotPath + ".y"); + int z = playerData.getInt(slotPath + ".z"); + boolean isPublic = playerData.getBoolean(slotPath + ".public", false); + int slot = 0; + try { slot = Integer.parseInt(slotKey); } catch (NumberFormatException ignored2) {} + mysqlManager.setTargetChest(uuidString, item, slot, world, x, y, z, isPublic, ""); + targetCount++; + } + } } } @@ -1503,7 +2369,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { List> targetChests = mysqlManager.getTargetChests(uuidString); for (Map chest : targetChests) { String item = (String) chest.get("item"); - String path = "players." + uuidString + ".target-chests." + 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")); exportData.set(path + ".y", chest.get("y")); @@ -1512,7 +2379,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { targetCount++; } - // BUG FIX: Alle Rest-Truhen exportieren List> restChests = mysqlManager.getRestChests(uuidString); for (Map restChest : restChests) { int restSlot = (int) restChest.get("slot"); @@ -1580,7 +2446,51 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { World world = player.getWorld(); if (worldBlacklist != null && worldBlacklist.contains(world.getName())) { event.setCancelled(true); - player.sendMessage(ChatColor.RED + "In dieser Welt kannst du keine AutoSortChest erstellen!"); + player.sendMessage(getMessage("world-blacklisted")); + return; + } + + // ── PERMISSION CHECK ────────────────────────────────────────────────── + if (lines.length >= 2 && lines[0].equalsIgnoreCase("[asc]")) { + if (!player.hasPermission("autosortchest.use")) { + event.setCancelled(true); + player.sendMessage(getMessage("no-permission")); + return; + } + } + + // ── MÜLLTRUHE: [asc] / trash ────────────────────────────────────────── + if (lines.length >= 2 && lines[0].equalsIgnoreCase("[asc]") && isMuell(lines[1])) { + event.setCancelled(false); + Block chestBlock = null; + if (signBlock.getBlockData() instanceof WallSign wallSign) { + Block attachedBlock = signBlock.getRelative(wallSign.getFacing().getOppositeFace()); + if (attachedBlock.getState() instanceof Chest) chestBlock = attachedBlock; + } + if (chestBlock == null) { player.sendMessage(getMessage("no-chest-near-sign")); return; } + // Limit-Check: Kein Rang = keine Truhe + if (chestLimitsEnabled && getChestLimitForPlayer(player, "input") == 0 + && getChestLimitForPlayer(player, "target") == 0) { + player.sendMessage(getMessage("limit-no-permission")); + event.setCancelled(true); + return; + } + + if (isCleanSignMode()) { + // Clean-Modus: Schild wird nach dem Place via Sign-State aktualisiert + event.setLine(0, getCleanSignColor("trash", "line1") + player.getName()); + event.setLine(1, getCleanSignColor("trash", "line2") + getSignLabel("trash_clean")); + event.setLine(2, ""); + event.setLine(3, CLEAN_SIGN_MARKER_TRASH); + } else { + event.setLine(0, getSignColor("trash", "line1") + "[asc]"); + event.setLine(1, getSignColor("trash", "line2") + lines[1]); // "müll" oder "trash" beibehalten + event.setLine(2, ""); + event.setLine(3, getSignColor("trash", "line4") + player.getName()); + } + trashChestManager.setTrashChestLocation(playerUUID, chestBlock.getLocation()); + player.sendMessage(getMessage("trash-chest-set")); + player.sendMessage(getMessage("trash-chest-hint")); return; } @@ -1600,7 +2510,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { boolean alreadyRest = false; if (mysqlEnabled && mysqlManager != null) { currentRest = mysqlManager.countRestChests(playerUUID.toString()); - // Prüfen ob diese Truhe bereits als Rest-Truhe registriert ist (Update erlaubt) alreadyRest = mysqlManager.getRestSlotForLocation( playerUUID.toString(), finalChestBlockRest.getWorld().getName(), @@ -1622,20 +2531,26 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } if (!alreadyRest) currentRest = playerData.getConfigurationSection(basePath).getKeys(false).size(); } - // Legacy fallback if (!alreadyRest && currentRest == 0 && playerData.contains("players." + playerUUID + ".rest-chest")) { currentRest = 1; } } if (!alreadyRest && currentRest >= maxRest) { - player.sendMessage(ChatColor.RED + "Du hast das Limit deiner Rest-Truhen erreicht! (" + maxRest + ")"); + player.sendMessage(getMessage("limit-rest-reached").replace("%max%", String.valueOf(maxRest))); event.setCancelled(true); return; } } + if (isCleanSignMode()) { + event.setLine(0, getCleanSignColor("rest", "line1") + player.getName()); + event.setLine(1, getCleanSignColor("rest", "line2") + "Rest"); + event.setLine(2, getCleanSignColor("rest", "line3") + getSignLabel("private")); + event.setLine(3, CLEAN_SIGN_MARKER_REST); + } else { event.setLine(0, getSignColor("rest", "line1") + "[asc]"); event.setLine(1, getSignColor("rest", "line2") + "rest"); event.setLine(3, getSignColor("rest", "line4") + player.getName()); + } setRestChestLocation(playerUUID, chestBlock.getLocation()); player.sendMessage(getMessage("rest-chest-set")); return; @@ -1649,7 +2564,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (attachedBlock.getState() instanceof Chest) chestBlock = attachedBlock; } if (chestBlock == null) { player.sendMessage(getMessage("no-chest-near-sign")); return; } - // ── Limit-Pruefung: Input-Truhe ──────────────────────────────────── if (chestLimitsEnabled) { int maxInput = getChestLimitForPlayer(player, "input"); int currentInput = 0; @@ -1661,7 +2575,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { currentInput = playerData.getConfigurationSection(basePath).getKeys(false).size(); } } - // Pruefen ob diese Truhe schon als Input registriert ist (Update erlaubt) final Block finalChestBlock = chestBlock; boolean alreadyInput = false; if (mysqlEnabled && mysqlManager != null) { @@ -1673,28 +2586,53 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { && (int)c.get("y") == finalChestBlock.getY() && (int)c.get("z") == finalChestBlock.getZ(); }); + } else { + // BUG FIX: alreadyInput-Check auch im YAML-Modus, + // damit bestehende Input-Truhen re-konfiguriert werden können ohne Limit-Fehler. + String basePath = "players." + playerUUID + ".input-chests"; + if (playerData.contains(basePath)) { + for (String chestId : playerData.getConfigurationSection(basePath).getKeys(false)) { + String p = basePath + "." + chestId; + if (finalChestBlock.getWorld().getName().equals(playerData.getString(p + ".world")) + && finalChestBlock.getX() == playerData.getInt(p + ".x") + && finalChestBlock.getY() == playerData.getInt(p + ".y") + && finalChestBlock.getZ() == playerData.getInt(p + ".z")) { + alreadyInput = true; + break; + } + } + } } if (!alreadyInput && currentInput >= maxInput) { - player.sendMessage(ChatColor.RED + "Du hast das Limit deiner Eingangstruhen erreicht! (" + maxInput + ")"); + player.sendMessage(getMessage("limit-input-reached").replace("%max%", String.valueOf(maxInput))); event.setCancelled(true); return; } } - event.setLine(0, getSignColor("input", "line1") + "[asc]"); - event.setLine(1, getSignColor("input", "line2") + "input"); boolean isPublic = false; if (lines.length >= 4 && lines[3] != null && ChatColor.stripColor(lines[3]).toLowerCase().contains("[public]")) { isPublic = true; + } + if (isCleanSignMode()) { + event.setLine(0, getCleanSignColor("input", "line1") + player.getName()); + event.setLine(1, getCleanSignColor("input", "line2") + getSignLabel("input_clean")); + event.setLine(2, getCleanSignColor("input", "line3") + (isPublic ? getSignLabel("public") : getSignLabel("private"))); + event.setLine(3, CLEAN_SIGN_MARKER_INPUT + (isPublic ? "[Public]" : "")); + } else { + event.setLine(0, getSignColor("input", "line1") + "[asc]"); + event.setLine(1, getSignColor("input", "line2") + "input"); + if (isPublic) { event.setLine(3, getSignColor("input", "line4") + player.getName() + " [Public]"); } else { event.setLine(3, getSignColor("input", "line4") + player.getName()); } + } setInputChestLocation(playerUUID, chestBlock.getLocation(), isPublic); player.sendMessage(getMessage("input-chest-set")); return; } - if (lines.length >= 2 && lines[0].equalsIgnoreCase("[asc]") && lines[1].equalsIgnoreCase("ziel")) { + if (lines.length >= 2 && lines[0].equalsIgnoreCase("[asc]") && isZiel(lines[1])) { event.setCancelled(false); Block chestBlock = null; if (signBlock.getBlockData() instanceof WallSign wallSign) { @@ -1703,13 +2641,13 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } if (chestBlock == null) { player.sendMessage(getMessage("no-chest-near-sign")); return; } event.setLine(0, getSignColor("target", "line1") + "[asc]"); - event.setLine(1, getSignColor("target", "line2") + "ziel"); + event.setLine(1, getSignColor("target", "line2") + lines[1]); // "ziel" oder "target" beibehalten event.setLine(2, ""); event.setLine(3, ""); } } - @EventHandler + @EventHandler(priority = EventPriority.HIGH) public void onPlayerInteract(PlayerInteractEvent event) { if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; @@ -1720,9 +2658,35 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (clickedBlock == null) return; + // ── Frühzeitiges Abbrechen: Sneak+Rechtsklick auf ASC-Schild verhindert Sign-Editor ── + if (player.isSneaking() && clickedBlock.getState() instanceof Sign earlySign) { + String earlyLine0 = ChatColor.stripColor(earlySign.getLine(0) != null ? earlySign.getLine(0) : ""); + if (earlyLine0.equalsIgnoreCase("[asc]") || isAnyCleanSign(earlySign)) { + event.setCancelled(true); + } + } + if (clickedBlock.getState() instanceof Sign sign) { String[] lines = sign.getLines(); - if (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]")) { + // Erkenne sowohl normale [asc]-Schilder als auch Clean-Schilder (Marker in Z.4) + boolean isAscSign = (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]")) + || isAnyCleanSign(sign); + + if (isAscSign) { + + // ── Spam-Klick-Schutz (beide Hände): max. 1 Interaktion alle 300ms pro Schild ── + String cooldownKey = player.getUniqueId() + ":" + clickedBlock.getX() + ":" + clickedBlock.getY() + ":" + clickedBlock.getZ(); + long now = System.currentTimeMillis(); + Long lastInteract = signInteractCooldown.get(cooldownKey); + if (lastInteract != null && (now - lastInteract) < SIGN_INTERACT_COOLDOWN_MS) { + event.setCancelled(true); + return; + } + signInteractCooldown.put(cooldownKey, now); + // Cooldown-Map periodisch säubern (einmal pro Spieler, wenn > 200 Einträge) + if (signInteractCooldown.size() > 200) { + signInteractCooldown.entrySet().removeIf(e -> (now - e.getValue()) > SIGN_INTERACT_COOLDOWN_MS * 10); + } Block chestBlock = null; if (sign.getBlockData() instanceof WallSign wallSign) { @@ -1735,36 +2699,82 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return; } - String line1Clean = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); - boolean isTargetOrRest = line1Clean.equalsIgnoreCase("ziel") || line1Clean.equalsIgnoreCase("rest"); + // Für Clean-Schilder den Typ aus dem Marker lesen + String cleanType = getCleanSignType(sign); + String line1Clean; + if (cleanType != null) { + line1Clean = cleanType.equals("target") ? getSignLabel("target") : cleanType; + } else { + line1Clean = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); + } + + // ── MÜLLTRUHE: Rechtsklick auf Müll-Schild → GUI öffnen ────────── + if (isMuell(line1Clean)) { + String ownerName; + if (isCleanTrashSign(sign)) { + ownerName = getCleanSignOwnerAny(sign); + } else { + String line3Raw = lines[3] != null ? lines[3] : ""; + ownerName = ChatColor.stripColor(line3Raw).trim(); + } + boolean isOwner = ownerName.equalsIgnoreCase(player.getName()); + if (!isOwner && !isAdmin(player)) { + player.sendMessage(getMessage("not-your-chest")); + event.setCancelled(true); + return; + } + UUID ownerUUID2 = playerUUID; + if (!isOwner) { + OfflinePlayer op2 = Bukkit.getOfflinePlayer(ownerName); + if (op2.hasPlayedBefore() && op2.getUniqueId() != null) ownerUUID2 = op2.getUniqueId(); + } + event.setCancelled(true); + trashChestManager.openConfigGui(player, ownerUUID2); + return; + } + + boolean isTargetOrRest = isZiel(line1Clean) || line1Clean.equalsIgnoreCase("rest"); if (isTargetOrRest) { String line3Raw = lines[3] != null ? lines[3] : ""; String line3Clean = ChatColor.stripColor(line3Raw); - String pureOwnerName = line3Clean.replace("[Public]", "").replace("[public]", "").trim(); + // Clean-Sign: Spielername steht auf Zeile 2 (index 1) für target, Zeile 1 (index 0) für rest + boolean isCleanSign = isCleanTargetSign(sign) || isCleanRestSign(sign); + String pureOwnerName = isCleanSign + ? getCleanSignOwnerAny(sign) + : line3Clean.replace("[Public]", "").replace("[public]", "").trim(); boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); if (player.isSneaking() && (itemInHand == null || itemInHand.getType() == Material.AIR)) { if (isOwner || isAdmin(player)) { boolean isPublicNow = isChestPublic(sign); boolean newPublic = !isPublicNow; - String newModeText, newLine4; + String newModeText; String colorType = line1Clean.equalsIgnoreCase("rest") ? "rest" : "target"; - if (isPublicNow) { - newModeText = getMessage("mode-private"); - newLine4 = getSignColor(colorType, "line4") + pureOwnerName; + newModeText = newPublic ? getMessage("mode-public") : getMessage("mode-private"); + if (isCleanTargetSign(sign)) { + // In clean mode: Zeile 3 = Öffentlich/Privat, Marker + Public-Flag auf Zeile 4 + String itemName = ChatColor.stripColor(sign.getLine(0)); + applyCleanTargetSign(sign, itemName, pureOwnerName, newPublic, + chestBlock != null && chestBlock.getState() instanceof Chest c2 && isInventoryFull(c2.getInventory())); + } else if (isCleanRestSign(sign)) { + applyCleanRestSign(sign, pureOwnerName, newPublic, + chestBlock != null && chestBlock.getState() instanceof Chest c3 && isInventoryFull(c3.getInventory())); } else { - newModeText = getMessage("mode-public"); - newLine4 = getSignColor(colorType, "line4") + pureOwnerName + " " + ChatColor.RESET + "[Public]"; - } + String newLine4 = newPublic + ? getSignColor(colorType, "line4") + pureOwnerName + " " + ChatColor.RESET + "[Public]" + : getSignColor(colorType, "line4") + pureOwnerName; sign.setLine(3, newLine4); + } sign.update(); - if (line1Clean.equalsIgnoreCase("ziel")) { - String line2Clean = ChatColor.stripColor(lines[2] != null ? lines[2] : ""); - Material mat = Material.matchMaterial(line2Clean); - if (mat != null && mat != Material.AIR) setTargetChestLocation(playerUUID, chestBlock.getLocation(), mat, newPublic); + if (isZiel(line1Clean)) { + String line2Clean = isCleanTargetSign(sign) + ? findItemForChestLocation(playerUUID, chestBlock != null ? chestBlock.getLocation() : null) + : ChatColor.stripColor(lines[2] != null ? lines[2] : ""); + Material mat = Material.matchMaterial(line2Clean != null ? line2Clean : ""); + if (mat != null && mat != Material.AIR) setTargetChestLocation(playerUUID, chestBlock != null ? chestBlock.getLocation() : null, mat, newPublic); } else if (line1Clean.equalsIgnoreCase("rest")) { - setRestChestLocation(playerUUID, chestBlock.getLocation(), newPublic); + setRestChestLocation(playerUUID, chestBlock != null ? chestBlock.getLocation() : null, newPublic); } player.sendMessage(getMessage("mode-changed").replace("%mode%", newModeText)); event.setCancelled(true); @@ -1776,7 +2786,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - if (line1Clean.equalsIgnoreCase("ziel")) { + if (isZiel(line1Clean)) { if (itemInHand != null && itemInHand.getType() != Material.AIR) { if (chestLimitsEnabled) { int maxChests = getChestLimitForPlayer(player, "target"); @@ -1798,26 +2808,53 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String basePath = "players." + playerUUID + ".target-chests"; if (playerData.contains(basePath)) { for (String item : playerData.getConfigurationSection(basePath).getKeys(false)) { - String path = basePath + "." + item; - String worldName = playerData.getString(path + ".world"); - int x = playerData.getInt(path + ".x"); int y = playerData.getInt(path + ".y"); int z = playerData.getInt(path + ".z"); - World w = Bukkit.getWorld(worldName); - if (w != null && w.getBlockAt(x, y, z).getState() instanceof Chest) { - uniqueChestLocations.add(worldName + ":" + x + ":" + y + ":" + z); - if (itemInHand.getType().name().equals(item)) countForThisItem++; + // FIX: use getTargetChestSlotsYaml to handle both old flat + new slotted format + for (Location slotLoc : getTargetChestSlotsYaml(playerUUID, item)) { + if (slotLoc == null || slotLoc.getWorld() == null) continue; + World w = slotLoc.getWorld(); + int x = slotLoc.getBlockX(), y = slotLoc.getBlockY(), z = slotLoc.getBlockZ(); + if (w.getBlockAt(x, y, z).getState() instanceof Chest) { + uniqueChestLocations.add(w.getName() + ":" + x + ":" + y + ":" + z); + if (itemInHand.getType().name().equals(item)) countForThisItem++; + } } } } } String thisLoc = chestBlock.getWorld().getName() + ":" + chestBlock.getX() + ":" + chestBlock.getY() + ":" + chestBlock.getZ(); boolean alreadyTarget = uniqueChestLocations.contains(thisLoc); + // FIX: Prüfe separat ob diese Location BEREITS für diesen Item-Typ registriert ist. + // alreadyTarget (irgendein Item) darf nicht den per-item-Limit-Check umgehen. + boolean alreadyThisItem = false; + if (mysqlEnabled && mysqlManager != null) { + List> sameItemChests = mysqlManager.getTargetChestsForItem(playerUUID.toString(), itemInHand.getType().name()); + for (Map e : sameItemChests) { + String w = (String) e.get("world"); + if (w != null && w.equals(chestBlock.getWorld().getName()) + && (int)e.get("x") == chestBlock.getX() + && (int)e.get("y") == chestBlock.getY() + && (int)e.get("z") == chestBlock.getZ()) { + alreadyThisItem = true; break; + } + } + } else { + for (Location sl : getTargetChestSlotsYaml(playerUUID, itemInHand.getType().name())) { + if (sl != null && sl.getWorld() != null + && sl.getWorld().getName().equals(chestBlock.getWorld().getName()) + && sl.getBlockX() == chestBlock.getX() + && sl.getBlockY() == chestBlock.getY() + && sl.getBlockZ() == chestBlock.getZ()) { + alreadyThisItem = true; break; + } + } + } if (!alreadyTarget && uniqueChestLocations.size() >= maxChests) { - player.sendMessage(ChatColor.RED + "Du hast das Limit deiner Zieltruhen erreicht! (" + maxChests + ")"); + player.sendMessage(getMessage("limit-target-reached").replace("%max%", String.valueOf(maxChests))); event.setCancelled(true); return; } - if (!alreadyTarget && countForThisItem >= maxPerItem) { - player.sendMessage(ChatColor.RED + "Du hast das Limit für " + itemInHand.getType().name() + "-Truhen erreicht! (" + maxPerItem + ")"); + if (!alreadyThisItem && countForThisItem >= maxPerItem) { + player.sendMessage(getMessage("limit-target-per-item").replace("%item%", itemInHand.getType().name()).replace("%max%", String.valueOf(maxPerItem))); event.setCancelled(true); return; } @@ -1827,39 +2864,26 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { event.setCancelled(true); return; } - if (!mysqlEnabled) { - String basePath = "players." + playerUUID + ".target-chests"; - if (playerData.contains(basePath)) { - for (String item : new HashSet<>(playerData.getConfigurationSection(basePath).getKeys(false))) { - String path = basePath + "." + item; - String worldName = playerData.getString(path + ".world"); - int x = playerData.getInt(path + ".x"); int y = playerData.getInt(path + ".y"); int z = playerData.getInt(path + ".z"); - if (worldName != null && worldName.equals(chestBlock.getWorld().getName()) && x == chestBlock.getX() && y == chestBlock.getY() && z == chestBlock.getZ()) { - playerData.set(path, null); - } - } - } - String path = basePath + "." + itemInHand.getType().name(); - playerData.set(path + ".world", chestBlock.getWorld().getName()); - playerData.set(path + ".x", chestBlock.getX()); - playerData.set(path + ".y", chestBlock.getY()); - playerData.set(path + ".z", chestBlock.getZ()); - playerData.set(path + ".public", false); - savePlayerData(); - } - if (mysqlEnabled) removeOldTargetEntry(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType().name()); + // FIX: removeOldTargetEntry handles both YAML and MySQL correctly + removeOldTargetEntry(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType().name()); Chest chest = (Chest) chestBlock.getState(); boolean isFull = isInventoryFull(chest.getInventory()); String colorType = isFull ? "full" : "target"; + if (isCleanSignMode()) { + String ownerForSign = (pureOwnerName.isEmpty() || pureOwnerName.equalsIgnoreCase("Unknown")) + ? player.getName() : pureOwnerName; + applyCleanTargetSign(sign, itemInHand.getType().name(), ownerForSign, false, isFull); + } else { sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); - sign.setLine(1, getSignColor(colorType, "line2") + "ziel"); + sign.setLine(1, getSignColor(colorType, "line2") + getSignLabel("target")); sign.setLine(2, getSignColor(colorType, "line3") + itemInHand.getType().name()); String finalLine4 = line3Raw; if (pureOwnerName.isEmpty() || pureOwnerName.equalsIgnoreCase("Unknown")) { finalLine4 = getSignColor("target", "line4") + player.getName(); } sign.setLine(3, finalLine4); + } sign.update(); setTargetChestLocation(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType()); player.sendMessage(getMessage("target-chest-set").replace("%item%", itemInHand.getType().name())); @@ -1875,7 +2899,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } event.setCancelled(true); if (chestBlock.getState() instanceof Chest) { - player.openInventory(((Chest) chestBlock.getState()).getInventory()); + // Typ bestimmen: Zeile 1 des Schildes ("ziel"/"target"/"rest") + String chestType = line1Clean.equalsIgnoreCase("rest") ? "rest" : "target"; + openTitledChestInventory(player, chestBlock, buildChestTitle(sign, chestType)); } return; } @@ -1883,22 +2909,26 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (line1Clean.equalsIgnoreCase("input")) { String line3Raw = lines[3] != null ? lines[3] : ""; String line3Clean = ChatColor.stripColor(line3Raw); - String pureOwnerName = line3Clean.replace("[Public]", "").replace("[public]", "").trim(); + // Clean-Input-Schild: Besitzer auf Zeile 1 (index 0) + boolean isCleanInputSgn = isCleanInputSign(sign); + String pureOwnerName = isCleanInputSgn + ? getCleanSignOwnerAny(sign) + : line3Clean.replace("[Public]", "").replace("[public]", "").trim(); boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); if (player.isSneaking() && (itemInHand == null || itemInHand.getType() == Material.AIR)) { if (isOwner || isAdmin(player)) { boolean isPublicNow = isChestPublic(sign); boolean newPublic = !isPublicNow; - String newModeText, newLine4; - if (isPublicNow) { - newModeText = getMessage("mode-private"); - newLine4 = getSignColor("input", "line4") + pureOwnerName; + String newModeText = newPublic ? getMessage("mode-public") : getMessage("mode-private"); + if (isCleanInputSgn) { + applyCleanInputSign(sign, pureOwnerName, newPublic); } else { - newModeText = getMessage("mode-public"); - newLine4 = getSignColor("input", "line4") + pureOwnerName + " " + ChatColor.RESET + "[Public]"; - } + String newLine4 = newPublic + ? getSignColor("input", "line4") + pureOwnerName + " " + ChatColor.RESET + "[Public]" + : getSignColor("input", "line4") + pureOwnerName; sign.setLine(3, newLine4); + } sign.update(); setInputChestLocation(playerUUID, chestBlock.getLocation(), newPublic); player.sendMessage(getMessage("mode-changed").replace("%mode%", newModeText)); @@ -1917,7 +2947,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } event.setCancelled(true); if (chestBlock.getState() instanceof Chest) { - player.openInventory(((Chest) chestBlock.getState()).getInventory()); + openTitledChestInventory(player, chestBlock, buildChestTitle(sign, "input")); } return; } @@ -1927,6 +2957,37 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (clickedBlock.getState() instanceof Chest) { Block chestBlock = clickedBlock; + + // ── MÜLLTRUHE: Direktklick auf die Truhe ────────────────────────── + // Zeige Filter-Info als ActionBar und öffne die Truhe normal. + UUID trashOwnerDirect = trashChestManager.getTrashChestOwner(chestBlock.getLocation()); + if (trashOwnerDirect != null) { + // Nur Besitzer oder Admin darf öffnen + OfflinePlayer trashOwnerOp = Bukkit.getOfflinePlayer(trashOwnerDirect); + boolean isOwnerDirect = trashOwnerDirect.equals(playerUUID); + if (!isOwnerDirect && !isAdmin(player)) { + player.sendMessage(getMessage("not-your-chest")); + event.setCancelled(true); + return; + } + // Filter-Info in der Action-Bar anzeigen + List filter = trashChestManager.getFilter(trashOwnerDirect); + String filterInfo; + if (filter.isEmpty()) { + filterInfo = getMessage("trash-info-empty"); + } else { + String itemList = String.join(", ", + filter.stream().map(TrashChestManager::formatMaterialName).collect(java.util.stream.Collectors.toList())); + filterInfo = getMessage("trash-info-filter").replace("%items%", itemList); + } + player.sendMessage(filterInfo); + // Truhe mit Custom-Titel "Mülltruhe" / "Trash Chest" öffnen + event.setCancelled(true); + openTitledChestInventory(player, chestBlock, buildChestTitle(null, "trash")); + return; + } + // ────────────────────────────────────────────────────────────────── + Block signBlock = null; List blocks = getChestBlocks((Chest) chestBlock.getState()); @@ -1935,7 +2996,10 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { for (Block face : new Block[]{b.getRelative(1,0,0), b.getRelative(-1,0,0), b.getRelative(0,0,1), b.getRelative(0,0,-1)}) { if (face.getState() instanceof Sign sign && isSignAttachedToChest(face, b)) { String[] lines = sign.getLines(); - if (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]")) { + // Normales Schild: [asc] auf Zeile 1; Clean-Schild: beliebiger Marker auf Zeile 4 + if (lines.length >= 2 && ( + ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]") + || isAnyCleanSign(sign))) { signBlock = face; break outerLoop; } @@ -1946,33 +3010,52 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (signBlock != null) { Sign sign = (Sign) signBlock.getState(); String[] lines = sign.getLines(); - String line1Clean = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); + // Im Clean-Modus steht kein Typ auf Zeile 2 — wir lesen den Typ aus dem Marker + String cleanTypeChest = getCleanSignType(sign); + String line1Clean; + if (cleanTypeChest != null) { + line1Clean = cleanTypeChest.equals("target") ? getSignLabel("target") : cleanTypeChest; + } else { + line1Clean = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); + } + boolean isCleanSign = isAnyCleanSign(sign); - if (line1Clean.equalsIgnoreCase("ziel") || line1Clean.equalsIgnoreCase("rest")) { + if (isZiel(line1Clean) || line1Clean.equalsIgnoreCase("rest")) { String line3Raw = lines[3] != null ? lines[3] : ""; String line3Clean = ChatColor.stripColor(line3Raw); - String pureOwnerName = line3Clean.replace("[public]", "").replace("[Public]", "").trim(); + String pureOwnerName = isCleanSign + ? getCleanSignOwnerAny(sign) + : line3Clean.replace("[public]", "").replace("[Public]", "").trim(); boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); if (player.isSneaking() && (itemInHand == null || itemInHand.getType() == Material.AIR)) { if (isOwner || isAdmin(player)) { boolean isPublicNow = isChestPublic(sign); boolean newPublic = !isPublicNow; - String newModeText, newLine4; + String newModeText = newPublic ? getMessage("mode-public") : getMessage("mode-private"); String colorType = line1Clean.equalsIgnoreCase("rest") ? "rest" : "target"; - String baseName = getSignColor(colorType, "line4") + pureOwnerName; - if (isPublicNow) { - newModeText = getMessage("mode-private"); - newLine4 = baseName; + if (isCleanTargetSign(sign)) { + String itemName = ChatColor.stripColor(sign.getLine(0)); + Chest c2 = chestBlock.getState() instanceof Chest cx ? cx : null; + boolean isFull2 = c2 != null && isInventoryFull(c2.getInventory()); + applyCleanTargetSign(sign, itemName, pureOwnerName, newPublic, isFull2); + } else if (isCleanRestSign(sign)) { + Chest c3 = chestBlock.getState() instanceof Chest cx ? cx : null; + boolean isFull3 = c3 != null && isInventoryFull(c3.getInventory()); + applyCleanRestSign(sign, pureOwnerName, newPublic, isFull3); } else { - newModeText = getMessage("mode-public"); - newLine4 = baseName + " " + ChatColor.RESET + "[Public]"; - } + String baseName = getSignColor(colorType, "line4") + pureOwnerName; + String newLine4 = newPublic + ? baseName + " " + ChatColor.RESET + "[Public]" + : baseName; sign.setLine(3, newLine4); + } sign.update(); - if (line1Clean.equalsIgnoreCase("ziel")) { - String line2Clean = ChatColor.stripColor(lines[2] != null ? lines[2] : ""); - Material mat = Material.matchMaterial(line2Clean); + if (isZiel(line1Clean)) { + String itemKey = isCleanTargetSign(sign) + ? findItemForChestLocation(playerUUID, chestBlock.getLocation()) + : ChatColor.stripColor(lines[2] != null ? lines[2] : ""); + Material mat = Material.matchMaterial(itemKey != null ? itemKey : ""); if (mat != null && mat != Material.AIR) setTargetChestLocation(playerUUID, chestBlock.getLocation(), mat, newPublic); } else { setRestChestLocation(playerUUID, chestBlock.getLocation(), newPublic); @@ -1987,7 +3070,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - if (line1Clean.equalsIgnoreCase("ziel")) { + if (isZiel(line1Clean)) { String line2Clean = ChatColor.stripColor(lines[2] != null ? lines[2] : ""); if (line2Clean.isEmpty()) { if (itemInHand == null || itemInHand.getType() == Material.AIR) { @@ -2000,18 +3083,93 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { event.setCancelled(true); return; } + // FIX: Limit-Check auch beim Direkt-Klick auf die Truhe (Chest-Block) + if (chestLimitsEnabled) { + int maxChests = getChestLimitForPlayer(player, "target"); + int maxPerItem = getChestLimitForPlayer(player, "target_per_item"); + Set uniqueChestLocations = new HashSet<>(); + int countForThisItem = 0; + if (mysqlEnabled && mysqlManager != null) { + List> allChests = mysqlManager.getTargetChests(playerUUID.toString()); + for (Map map : allChests) { + String worldName = (String) map.get("world"); + int x = (int) map.get("x"); int y = (int) map.get("y"); int z = (int) map.get("z"); + World w = Bukkit.getWorld(worldName); + if (w != null && w.getBlockAt(x, y, z).getState() instanceof Chest) { + uniqueChestLocations.add(worldName + ":" + x + ":" + y + ":" + z); + if (itemInHand.getType().name().equals(map.get("item"))) countForThisItem++; + } + } + } else { + String basePath2 = "players." + playerUUID + ".target-chests"; + if (playerData.contains(basePath2)) { + for (String itemKey : playerData.getConfigurationSection(basePath2).getKeys(false)) { + for (Location slotLoc : getTargetChestSlotsYaml(playerUUID, itemKey)) { + if (slotLoc == null || slotLoc.getWorld() == null) continue; + World w = slotLoc.getWorld(); + int x = slotLoc.getBlockX(), y = slotLoc.getBlockY(), z = slotLoc.getBlockZ(); + if (w.getBlockAt(x, y, z).getState() instanceof Chest) { + uniqueChestLocations.add(w.getName() + ":" + x + ":" + y + ":" + z); + if (itemInHand.getType().name().equals(itemKey)) countForThisItem++; + } + } + } + } + } + String thisLoc2 = chestBlock.getWorld().getName() + ":" + chestBlock.getX() + ":" + chestBlock.getY() + ":" + chestBlock.getZ(); + boolean alreadyTarget2 = uniqueChestLocations.contains(thisLoc2); + boolean alreadyThisItem2 = false; + if (mysqlEnabled && mysqlManager != null) { + List> sameItemChests2 = mysqlManager.getTargetChestsForItem(playerUUID.toString(), itemInHand.getType().name()); + for (Map e : sameItemChests2) { + String w = (String) e.get("world"); + if (w != null && w.equals(chestBlock.getWorld().getName()) + && (int)e.get("x") == chestBlock.getX() + && (int)e.get("y") == chestBlock.getY() + && (int)e.get("z") == chestBlock.getZ()) { + alreadyThisItem2 = true; break; + } + } + } else { + for (Location sl : getTargetChestSlotsYaml(playerUUID, itemInHand.getType().name())) { + if (sl != null && sl.getWorld() != null + && sl.getWorld().getName().equals(chestBlock.getWorld().getName()) + && sl.getBlockX() == chestBlock.getX() + && sl.getBlockY() == chestBlock.getY() + && sl.getBlockZ() == chestBlock.getZ()) { + alreadyThisItem2 = true; break; + } + } + } + if (!alreadyTarget2 && uniqueChestLocations.size() >= maxChests) { + player.sendMessage(getMessage("limit-target-reached").replace("%max%", String.valueOf(maxChests))); + event.setCancelled(true); + return; + } + if (!alreadyThisItem2 && countForThisItem >= maxPerItem) { + player.sendMessage(getMessage("limit-target-per-item").replace("%item%", itemInHand.getType().name()).replace("%max%", String.valueOf(maxPerItem))); + event.setCancelled(true); + return; + } + } removeOldTargetEntry(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType().name()); Chest chest = (Chest) chestBlock.getState(); boolean isFull = isInventoryFull(chest.getInventory()); String colorType = isFull ? "full" : "target"; + if (isCleanSignMode()) { + String ownerForSign2 = (pureOwnerName.isEmpty() || pureOwnerName.equalsIgnoreCase("Unknown")) + ? player.getName() : pureOwnerName; + applyCleanTargetSign(sign, itemInHand.getType().name(), ownerForSign2, false, isFull); + } else { sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); - sign.setLine(1, getSignColor(colorType, "line2") + "ziel"); + sign.setLine(1, getSignColor(colorType, "line2") + getSignLabel("target")); sign.setLine(2, getSignColor(colorType, "line3") + itemInHand.getType().name()); String finalLine4 = line3Raw; if (pureOwnerName.isEmpty() || pureOwnerName.equalsIgnoreCase("Unknown")) { finalLine4 = getSignColor("target", "line4") + player.getName(); } sign.setLine(3, finalLine4); + } sign.update(); setTargetChestLocation(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType()); player.sendMessage(getMessage("target-chest-set").replace("%item%", itemInHand.getType().name())); @@ -2030,23 +3188,26 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (line1Clean.equalsIgnoreCase("input")) { String line3Raw = lines[3] != null ? lines[3] : ""; String line3Clean = ChatColor.stripColor(line3Raw); - String pureOwnerName = line3Clean.replace("[Public]", "").replace("[public]", "").trim(); + boolean isCleanInputChestSign = isCleanInputSign(sign); + String pureOwnerName = isCleanInputChestSign + ? getCleanSignOwnerAny(sign) + : line3Clean.replace("[Public]", "").replace("[public]", "").trim(); boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); if (player.isSneaking() && (itemInHand == null || itemInHand.getType() == Material.AIR)) { if (isOwner || isAdmin(player)) { boolean isPublicNow = isChestPublic(sign); boolean newPublic = !isPublicNow; - String newModeText, newLine4; - String baseName = getSignColor("input", "line4") + pureOwnerName; - if (isPublicNow) { - newModeText = getMessage("mode-private"); - newLine4 = baseName; + String newModeText = newPublic ? getMessage("mode-public") : getMessage("mode-private"); + if (isCleanInputChestSign) { + applyCleanInputSign(sign, pureOwnerName, newPublic); } else { - newModeText = getMessage("mode-public"); - newLine4 = baseName + " " + ChatColor.RESET + "[Public]"; - } + String baseName = getSignColor("input", "line4") + pureOwnerName; + String newLine4 = newPublic + ? baseName + " " + ChatColor.RESET + "[Public]" + : baseName; sign.setLine(3, newLine4); + } sign.update(); setInputChestLocation(playerUUID, chestBlock.getLocation(), newPublic); player.sendMessage(getMessage("mode-changed").replace("%mode%", newModeText)); @@ -2065,13 +3226,47 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } } + + // ── Alle Permission-Checks bestanden → Truhe mit Custom-Titel öffnen ── + if (signBlock != null && chestBlock.getState() instanceof Chest) { + Sign sign2 = (Sign) signBlock.getState(); + String[] lines2 = sign2.getLines(); + String cleanType2 = getCleanSignType(sign2); + String l1; + if (cleanType2 != null) { + l1 = cleanType2.equals("target") ? getSignLabel("target") : cleanType2; + } else { + l1 = ChatColor.stripColor(lines2[1] != null ? lines2[1] : ""); + } + String chestType2 = l1.equalsIgnoreCase("rest") ? "rest" + : isMuell(l1) ? "trash" + : (isZiel(l1) ? "target" : "input"); + event.setCancelled(true); + openTitledChestInventory(player, chestBlock, buildChestTitle(sign2, chestType2)); + } } } private void removeInputChestByLocation(UUID uuid, Location loc) { + // MySQL-Modus: Direkt per Location suchen und löschen, kein YAML-I/O nötig. + if (mysqlEnabled && mysqlManager != null) { + List> chests = mysqlManager.getInputChests(uuid.toString()); + for (Map chest : chests) { + if (chest == null) continue; + String w = (String) chest.get("world"); + if (w != null && w.equals(loc.getWorld().getName()) + && loc.getBlockX() == (int) chest.get("x") + && loc.getBlockY() == (int) chest.get("y") + && loc.getBlockZ() == (int) chest.get("z")) { + mysqlManager.removeInputChest(uuid.toString(), (String) chest.get("chest_id")); + return; + } + } + return; + } + // YAML-Modus String basePath = "players." + uuid + ".input-chests"; String oldPath = "players." + uuid + ".input-chest"; - boolean removed = false; if (playerData.contains(basePath)) { for (String chestId : playerData.getConfigurationSection(basePath).getKeys(false)) { String path = basePath + "." + chestId; @@ -2079,8 +3274,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (savedLoc != null && savedLoc.equals(loc)) { playerData.set(path, null); savePlayerData(); - removed = true; - if (mysqlEnabled && mysqlManager != null) mysqlManager.removeInputChest(uuid.toString(), chestId); return; } } @@ -2090,19 +3283,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (oldLoc != null && oldLoc.equals(loc)) { playerData.set(oldPath, null); savePlayerData(); - removed = true; - } - } - if (!removed && mysqlEnabled && mysqlManager != null) { - List> chests = mysqlManager.getInputChests(uuid.toString()); - for (Map chest : chests) { - if (chest != null - && loc.getWorld().getName().equals(chest.get("world")) - && loc.getBlockX() == (int) chest.get("x") - && loc.getBlockY() == (int) chest.get("y") - && loc.getBlockZ() == (int) chest.get("z")) { - mysqlManager.removeInputChest(uuid.toString(), (String) chest.get("chest_id")); - } } } } @@ -2117,10 +3297,16 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (block.getState() instanceof Sign sign) { String[] lines = sign.getLines(); - if (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]") && + // Normales Schild: [asc] auf Z.1 + Typ auf Z.2; Clean-Schild: Marker auf Z.4 + boolean isCleanTs = isAnyCleanSign(sign); + if (isCleanTs) { + signBlock = block; + signOwner = getCleanSignOwnerAny(sign); + } else if (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]") && (ChatColor.stripColor(lines[1] != null ? lines[1] : "").equalsIgnoreCase("input") || - ChatColor.stripColor(lines[1] != null ? lines[1] : "").equalsIgnoreCase("ziel") || - ChatColor.stripColor(lines[1] != null ? lines[1] : "").equalsIgnoreCase("rest"))) { + isZiel(ChatColor.stripColor(lines[1] != null ? lines[1] : "")) || + ChatColor.stripColor(lines[1] != null ? lines[1] : "").equalsIgnoreCase("rest") || + isMuell(ChatColor.stripColor(lines[1] != null ? lines[1] : "")))) { signBlock = block; signOwner = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); } @@ -2133,10 +3319,17 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { for (Block face : new Block[]{b.getRelative(1,0,0), b.getRelative(-1,0,0), b.getRelative(0,0,1), b.getRelative(0,0,-1)}) { if (face.getState() instanceof Sign sign && isSignAttachedToChest(face, b)) { String[] lines = sign.getLines(); - if (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]") && + boolean isCleanTs2 = isAnyCleanSign(sign); + if (isCleanTs2) { + signBlock = face; + signOwner = getCleanSignOwnerAny(sign); + isAscChest = true; + break outerLoop; + } else if (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]") && (ChatColor.stripColor(lines[1] != null ? lines[1] : "").equalsIgnoreCase("input") || - ChatColor.stripColor(lines[1] != null ? lines[1] : "").equalsIgnoreCase("ziel") || - ChatColor.stripColor(lines[1] != null ? lines[1] : "").equalsIgnoreCase("rest"))) { + isZiel(ChatColor.stripColor(lines[1] != null ? lines[1] : "")) || + ChatColor.stripColor(lines[1] != null ? lines[1] : "").equalsIgnoreCase("rest") || + isMuell(ChatColor.stripColor(lines[1] != null ? lines[1] : "")))) { signBlock = face; signOwner = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); isAscChest = true; @@ -2155,7 +3348,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { event.setCancelled(true); return; } - // BUG FIX: autosortchest.bypass erlaubt Abbau ohne Shift-Taste if (!player.isSneaking() && !isAdmin(player) && !player.hasPermission("autosortchest.bypass")) { player.sendMessage(getMessage("sign-break-denied")); event.setCancelled(true); @@ -2163,9 +3355,30 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } String[] lines = ((Sign) (isAscChest ? signBlock.getState() : block.getState())).getLines(); - String line1 = ChatColor.stripColor(lines[1]); - Location chestLoc = isAscChest ? block.getLocation() : ((Sign) block.getState()).getBlock() - .getRelative(((WallSign) ((Sign) block.getState()).getBlockData()).getFacing().getOppositeFace()).getLocation(); + Sign breakSign = (Sign) (isAscChest ? signBlock.getState() : block.getState()); + // Im Clean-Modus steht kein Typ auf Z.2 — wir erkennen den Typ am Marker + String cleanBreakType = getCleanSignType(breakSign); + String line1; + if (cleanBreakType != null) { + line1 = cleanBreakType; // "target", "input", "rest", "trash" + } else { + line1 = ChatColor.stripColor(lines[1]); + } + // Alias: "target" wie "ziel" behandeln + if ("target".equalsIgnoreCase(line1)) line1 = "ziel"; + // FIX: instanceof-Check vor dem WallSign-Cast, da stehende Schilder (Sign-Posts) keine WallSigns sind. + Location chestLoc; + if (isAscChest) { + chestLoc = block.getLocation(); + } else { + org.bukkit.block.data.BlockData bd = ((Sign) block.getState()).getBlockData(); + if (!(bd instanceof WallSign)) { + // Kein WallSign (z.B. stehender Sign-Post) → kein Abbau blockieren + return; + } + chestLoc = ((Sign) block.getState()).getBlock() + .getRelative(((WallSign) bd).getFacing().getOppositeFace()).getLocation(); + } UUID ownerUUID = null; String pureOwnerName = signOwner.replace("[Public]", "").replace("[public]", "").trim(); @@ -2185,17 +3398,25 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (ownerUUID == null) { String targetPath = "players." + uuidString + ".target-chests"; if (playerData.contains(targetPath)) { + outer_tgt: for (String itemType : playerData.getConfigurationSection(targetPath).getKeys(false)) { - if (chestLoc.equals(getLocationFromPath(targetPath + "." + itemType))) { ownerUUID = uuid; break; } + // FIX: iterate slots for new slotted format + for (Location tLoc : getTargetChestSlotsYaml(uuid, itemType)) { + if (chestLoc.equals(tLoc)) { ownerUUID = uuid; break outer_tgt; } + } } } } if (ownerUUID == null) { - // BUG FIX: Alle Rest-Truhen des Spielers prüfen (nicht nur erste) for (Location restLoc : getRestChestLocations(uuid)) { if (chestLoc.equals(restLoc)) { ownerUUID = uuid; break; } } } + if (ownerUUID == null) { + // Mülltruhen prüfen + UUID trashOwner = trashChestManager.getTrashChestOwner(chestLoc); + if (trashOwner != null && trashOwner.equals(uuid)) ownerUUID = uuid; + } if (ownerUUID != null) break; } } @@ -2204,11 +3425,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (line1.equalsIgnoreCase("rest")) { if (mysqlEnabled && mysqlManager != null) { - // BUG FIX: Nur die spezifische Location löschen, nicht alle Rest-Truhen mysqlManager.removeRestChestByLocation(uuidToDelete.toString(), chestLoc.getWorld().getName(), chestLoc.getBlockX(), chestLoc.getBlockY(), chestLoc.getBlockZ()); } else { - // YAML: Spezifische Location aus rest-chests entfernen boolean removedFromNew = false; String basePath = "players." + uuidToDelete + ".rest-chests"; if (playerData.contains(basePath)) { @@ -2224,7 +3443,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } } - // Legacy-Fallback if (!removedFromNew) { playerData.set("players." + uuidToDelete + ".rest-chest", null); } @@ -2232,37 +3450,61 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } else if (line1.equalsIgnoreCase("input")) { removeInputChestByLocation(uuidToDelete, chestLoc); - } else if (line1.equalsIgnoreCase("ziel")) { - String line2 = ChatColor.stripColor(lines[2]); + } else if (isZiel(line1)) { + // Clean-Schild: Item steht nicht mehr auf Z.3 → location-basierte Suche + boolean isCleanBreakSign = cleanBreakType != null; + String line2 = isCleanBreakSign + ? (findItemForChestLocation(uuidToDelete, chestLoc) != null ? findItemForChestLocation(uuidToDelete, chestLoc) : "") + : ChatColor.stripColor(lines[2]); if (!line2.isEmpty()) { Material mat = Material.matchMaterial(line2); if (mat != null) { if (mysqlEnabled && mysqlManager != null) { - // BUG FIX: Nur bei MySQL → kein playerData-Zugriff nötig - mysqlManager.removeTargetChest(uuidToDelete.toString(), mat.name()); - } else { - // YAML: Slot mit passender Location finden und löschen - String basePath = "players." + uuidToDelete + ".target-chests"; - if (playerData.contains(basePath)) { - String path = basePath + "." + mat.name(); - if (playerData.contains(path)) { - Location savedLoc = getLocationFromPath(path); - if (savedLoc != null && savedLoc.getBlockX() == chestLoc.getBlockX() - && savedLoc.getBlockY() == chestLoc.getBlockY() - && savedLoc.getBlockZ() == chestLoc.getBlockZ()) { - playerData.set(path, null); - savePlayerData(); - } + // BUG FIX: Nur den spezifischen Slot dieser Location löschen, + // NICHT alle Slots des Items (removeTargetChest würde alle löschen). + List> itemSlots = mysqlManager.getTargetChestsForItem( + uuidToDelete.toString(), mat.name()); + int slotToRemove = -1; + for (Map s : itemSlots) { + String w = (String) s.get("world"); + if (w != null && w.equals(chestLoc.getWorld().getName()) + && (int) s.get("x") == chestLoc.getBlockX() + && (int) s.get("y") == chestLoc.getBlockY() + && (int) s.get("z") == chestLoc.getBlockZ()) { + slotToRemove = (int) s.get("slot"); + break; } } + if (slotToRemove >= 0) { + mysqlManager.removeTargetChestSlot(uuidToDelete.toString(), mat.name(), slotToRemove); + } + } else { + // FIX: removeTargetChestSlotYaml handles both old flat + new slotted format + removeTargetChestSlotYaml(uuidToDelete, mat.name(), chestLoc); } } } + } else if (isMuell(line1)) { + // ── MÜLLTRUHE abbauen: Daten entfernen ────────────────────────────── + trashChestManager.removeTrashChest(uuidToDelete); } } @EventHandler public void onInventoryClose(InventoryCloseEvent event) { + // ── Custom-Titel-Inventory: Inhalte in echte Truhe zurückschreiben ──── + UUID pid = event.getPlayer().getUniqueId(); + Location customLoc = openCustomInventories.remove(pid); + if (customLoc != null) { + BlockState bs = customLoc.getBlock().getState(); + if (bs instanceof Chest realChest) { + // Inhalte des virtuellen Inventars in die echte Truhe übertragen + realChest.getInventory().setContents(event.getInventory().getContents().clone()); + } + // Kein return – normaler Schließ-Handler soll danach weiterlaufen + // (z.B. Mülltruhe-Processing, Sort-Trigger etc.) + } + InventoryHolder holder = event.getInventory().getHolder(); List chestsToCheck = new ArrayList<>(); @@ -2280,6 +3522,23 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { for (Chest chest : chestsToCheck) { Block chestBlock = chest.getBlock(); + + // ── Mülltruche: beim Schließen Items verarbeiten ────────────────── + UUID trashOwner = trashChestManager.getTrashChestOwner(chestBlock.getLocation()); + if (trashOwner != null) { + final UUID finalTrashOwner = trashOwner; + final Location finalLoc = chestBlock.getLocation(); + new BukkitRunnable() { + @Override + public void run() { + if (finalLoc.getBlock().getState() instanceof Chest freshChest) { + trashChestManager.processTrashChestInventory(finalTrashOwner, freshChest.getInventory()); + } + } + }.runTaskLater(this, 1L); + continue; // Kein weiterer Sign-Check für Mülltruhe nötig + } + Block signBlock = null; String signType = "target"; @@ -2287,8 +3546,13 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (face.getState() instanceof Sign sign && isSignAttachedToChest(face, chestBlock)) { String[] lines = sign.getLines(); String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); - if (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]") - && (line1.equalsIgnoreCase("ziel") || line1.equalsIgnoreCase("rest"))) { + // Clean-Schilder (Marker auf Z.4) oder normale [asc]-Schilder erkennen + if (isCleanTargetSign(sign)) { + signBlock = face; break; + } else if (isCleanRestSign(sign)) { + signBlock = face; signType = "rest"; break; + } else if (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]") + && (isZiel(line1) || line1.equalsIgnoreCase("rest"))) { signBlock = face; if (line1.equalsIgnoreCase("rest")) signType = "rest"; break; @@ -2298,32 +3562,46 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (signBlock == null) continue; - Sign sign = (Sign) signBlock.getState(); - String[] lines = sign.getLines(); - String signOwner = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); + Sign sign = (Sign) signBlock.getState(); + String[] lines = sign.getLines(); + boolean isCleanInvSign = isAnyCleanSign(sign); + String cleanInvType = getCleanSignType(sign); + String signOwner = isCleanInvSign + ? getCleanSignOwnerAny(sign) + : ChatColor.stripColor(lines[3] != null ? lines[3] : ""); boolean isPublic = isChestPublic(sign); if (!signOwner.equalsIgnoreCase(player.getName()) && !isPublic && !isAdmin(player)) continue; boolean isFull = isInventoryFull(chest.getInventory()); + + if ("target".equals(cleanInvType)) { + // Clean-Target-Schild: Farben und Öffentlich/Privat-Status aktualisieren + String itemName = ChatColor.stripColor(sign.getLine(0)); + applyCleanTargetSign(sign, itemName, signOwner, isPublic, isFull); + sign.update(); + } else if ("rest".equals(cleanInvType)) { + applyCleanRestSign(sign, signOwner, isPublic, isFull); + sign.update(); + } else { String configType = signType.equalsIgnoreCase("rest") ? "rest" : "target"; String colorType = isFull ? "full" : configType; String currentLine1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); String currentLine3 = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); if (ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]") - && (currentLine1.equalsIgnoreCase("ziel") || currentLine1.equalsIgnoreCase("rest"))) { + && (isZiel(currentLine1) || currentLine1.equalsIgnoreCase("rest"))) { sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); sign.setLine(1, getSignColor(colorType, "line2") + currentLine1); sign.setLine(2, getSignColor(colorType, "line3") + ChatColor.stripColor(lines[2] != null ? lines[2] : "")); sign.setLine(3, getSignColor(colorType, "line4") + currentLine3); sign.update(); } + } } } private void removeOldTargetEntry(UUID uuid, Location loc, String newItemType) { if (mysqlEnabled && mysqlManager != null) { - // MySQL: Alle Zieltruhen dieser Location löschen, die ein anderes Item haben List> existing = mysqlManager.getTargetChests(uuid.toString()); for (Map e : existing) { String existingItem = (String) e.get("item"); @@ -2338,20 +3616,26 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } return; } - // YAML-Modus String basePath = "players." + uuid + ".target-chests"; if (!playerData.contains(basePath)) return; for (String existingType : playerData.getConfigurationSection(basePath).getKeys(false)) { if (existingType.equalsIgnoreCase(newItemType)) continue; - String path = basePath + "." + existingType; - String savedWorld = playerData.getString(path + ".world"); - if (savedWorld == null) continue; // NPE-Schutz: korrupter Eintrag überspringen - if (savedWorld.equals(loc.getWorld().getName()) - && playerData.getInt(path + ".x") == loc.getBlockX() - && playerData.getInt(path + ".y") == loc.getBlockY() - && playerData.getInt(path + ".z") == loc.getBlockZ()) { - playerData.set(path, null); - savePlayerData(); // Änderung direkt auf Disk schreiben + // FIX: handle both old flat + new slotted format + String itemBase = basePath + "." + existingType; + if (playerData.contains(itemBase + ".world")) { + // Old flat format + String savedWorld = playerData.getString(itemBase + ".world"); + if (savedWorld != null && savedWorld.equals(loc.getWorld().getName()) + && playerData.getInt(itemBase + ".x") == loc.getBlockX() + && playerData.getInt(itemBase + ".y") == loc.getBlockY() + && playerData.getInt(itemBase + ".z") == loc.getBlockZ()) { + playerData.set(itemBase, null); + savePlayerData(); + break; + } + } else if (playerData.isConfigurationSection(itemBase)) { + // New slotted format + removeTargetChestSlotYaml(uuid, existingType, loc); break; } } @@ -2384,16 +3668,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - - /** - * ── BungeeCord NEU ──────────────────────────────────────────────────────── - * Verarbeitet ALLE eingehenden Transfers für diesen Server aus asc_transfers. - * Laeuft unabhaengig von Remote-Input-Truhen und Spieler-Online-Status. - */ - /** - * Verarbeitet eingehende Transfers fuer diesen Server. - * Pattern: Main Thread startet Async-DB-Read, dann zurueck auf Main Thread fuer World-Ops. - */ private void processIncomingTransfers() { if (!mysqlEnabled || mysqlManager == null || serverName.isEmpty()) return; final String currentServerName = serverName; @@ -2401,8 +3675,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { new BukkitRunnable() { @Override public void run() { - // ── ASYNC: Alle DB-Daten sammeln ───────────────────────────────── - // Struktur: uuid → { transfers, targetChestRaws, restChestRaw } final List workItems = new ArrayList<>(); try (java.sql.PreparedStatement psUuids = mysqlManager.getConnection().prepareStatement( @@ -2422,12 +3694,13 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (transfers.isEmpty()) continue; List> targetChestRaws = mysqlManager.getTargetChests(uuidString); - Map restChestRaw = mysqlManager.getRestChest(uuidString); + // BUG FIX: getRestChests() (plural) statt getRestChest() – alle Rest-Truhen laden + List> restChestRaws = mysqlManager.getRestChests(uuidString); OfflinePlayer op = Bukkit.getOfflinePlayer(ownerUUID); String ownerName = op.getName() != null ? op.getName() : uuidString; - workItems.add(new Object[]{ ownerUUID, ownerName, transfers, targetChestRaws, restChestRaw }); + workItems.add(new Object[]{ ownerUUID, ownerName, transfers, targetChestRaws, restChestRaws }); } } catch (Exception e) { getLogger().warning("[CrossLink] DB-Fehler in processIncomingTransfers: " + e.getMessage()); @@ -2436,7 +3709,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (workItems.isEmpty()) return; - // ── SYNC: World-Operationen auf dem Main Thread ─────────────────── new BukkitRunnable() { @SuppressWarnings("unchecked") @Override @@ -2446,9 +3718,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String ownerName = (String) work[1]; List> transfers = (List>) work[2]; List> targetRaws = (List>) work[3]; - Map restRaw = (Map) work[4]; + // BUG FIX: List statt einzelner Map (alle Rest-Truhen) + List> restRaws = (List>) work[4]; - // Lokale Ziel-Truhen aufloesen (Multi-Slot) Map> localTargets = new HashMap<>(); for (Map tc : targetRaws) { if (isRemoteChest(tc)) continue; @@ -2458,16 +3730,17 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { .add(new Location(world, (int) tc.get("x"), (int) tc.get("y"), (int) tc.get("z"))); } - // Rest-Truhe aufloesen - Location localRestChest = null; - if (restRaw != null && !isRemoteChest(restRaw)) { + // BUG FIX: Alle lokalen Rest-Truhen in eine Liste bauen (nicht nur slot 0) + List localRestChests = new ArrayList<>(); + for (Map restRaw : restRaws) { + if (isRemoteChest(restRaw)) continue; World world = Bukkit.getWorld((String) restRaw.get("world")); if (world != null) { - localRestChest = new Location(world, (int) restRaw.get("x"), - (int) restRaw.get("y"), (int) restRaw.get("z")); + localRestChests.add(new Location(world, (int) restRaw.get("x"), + (int) restRaw.get("y"), (int) restRaw.get("z"))); } } - if (localRestChest == null) localRestChest = getRestChestLocation(ownerUUID); + if (localRestChests.isEmpty()) localRestChests = getRestChestLocations(ownerUUID); for (Map transfer : transfers) { String itemName = (String) transfer.get("item"); @@ -2476,7 +3749,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { Material mat = Material.matchMaterial(itemName); if (mat == null) { - // Ungueltig: async loeschen final long tid = transferId; new BukkitRunnable() { @Override public void run() { mysqlManager.deleteTransfer(tid); @@ -2485,12 +3757,16 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } List targetLocs2 = localTargets.getOrDefault(itemName, new ArrayList<>()); - // Ersten nicht-vollen Slot nehmen Location targetLoc = null; for (Location l : targetLocs2) { if (l != null && !isChestCachedFull(l)) { targetLoc = l; break; } } - if (targetLoc == null) targetLoc = localRestChest; + // BUG FIX: Alle Rest-Truhen als Fallback prüfen (nicht nur slot 0) + if (targetLoc == null) { + for (Location restLoc : localRestChests) { + if (restLoc != null && !isChestCachedFull(restLoc)) { targetLoc = restLoc; break; } + } + } if (targetLoc == null) continue; if (!(targetLoc.getBlock().getState() instanceof Chest)) continue; @@ -2505,7 +3781,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { getLogger().info("[CrossLink<-" + currentServerName + "] " + transferred + "x " + itemName + " fuer " + ownerName + " angekommen."); } - // DB-Update async damit Main Thread nicht wartet final long tid = transferId; final int remaining = leftover.isEmpty() ? 0 : leftover.get(0).getAmount(); new BukkitRunnable() { @Override public void run() { @@ -2521,16 +3796,175 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { }.runTaskAsynchronously(this); } + /** + * Durchsucht alle Rest-Truhen und schiebt Items, für die eine Zieltruhe existiert, + * direkt in die passende Zieltruhe. Wird periodisch durch restResortTask aufgerufen. + * Funktioniert mit YAML (kein MySQL) und MySQL. + */ + private void resortRestChests() { + if (playerData == null && !mysqlEnabled) return; + + if (mysqlEnabled && mysqlManager != null) { + // FIX: MySQL-Abfrage async ausführen, dann Hauptthread für Welt-Zugriffe + new BukkitRunnable() { + @Override + public void run() { + List playerUUIDs = new ArrayList<>(); + try (java.sql.Statement st = mysqlManager.getConnection().createStatement()) { + java.sql.ResultSet rs = st.executeQuery("SELECT uuid FROM asc_players"); + while (rs.next()) { + try { playerUUIDs.add(UUID.fromString(rs.getString("uuid"))); } catch (Exception ignored) {} + } + } catch (Exception e) { + getLogger().warning("[ResortRest] DB-Fehler: " + e.getMessage()); + return; + } + new BukkitRunnable() { + @Override public void run() { resortRestChestsForPlayers(playerUUIDs); } + }.runTask(Main.this); + } + }.runTaskAsynchronously(this); + } else { + // YAML: direkt im Hauptthread + if (playerData.getConfigurationSection("players") == null) return; + List playerUUIDs = new ArrayList<>(); + for (String s : playerData.getConfigurationSection("players").getKeys(false)) { + try { playerUUIDs.add(UUID.fromString(s)); } catch (Exception ignored) {} + } + resortRestChestsForPlayers(playerUUIDs); + } + } + + /** Führt den eigentlichen Rest-Truhen-Resort im Hauptthread aus. */ + private void resortRestChestsForPlayers(List playerUUIDs) { + for (UUID ownerUUID : playerUUIDs) { + List restLocs = getRestChestLocations(ownerUUID); + if (restLocs.isEmpty()) continue; + + // Zieltruhen für diesen Spieler laden + Map> targetMap = new HashMap<>(); + if (mysqlEnabled && mysqlManager != null) { + for (Map tc : mysqlManager.getTargetChests(ownerUUID.toString())) { + if (isRemoteChest(tc)) continue; + World w = Bukkit.getWorld((String) tc.get("world")); + if (w == null) continue; + targetMap.computeIfAbsent((String) tc.get("item"), k -> new ArrayList<>()) + .add(new Location(w, (int) tc.get("x"), (int) tc.get("y"), (int) tc.get("z"))); + } + } else { + // FIX: Nutzt neuen Slot-Helper für Multi-Target-Support im YAML-Modus + targetMap = getAllTargetChestSlotsYaml(ownerUUID); + } + if (targetMap.isEmpty()) continue; + + // Besitzername für Schild-Validierung + String ownerName = "Unknown"; + OfflinePlayer op = Bukkit.getOfflinePlayer(ownerUUID); + if (op.getName() != null) ownerName = op.getName(); + final String finalOwnerName = ownerName; + + // Alle Rest-Truhen durchgehen + for (Location restLoc : restLocs) { + if (restLoc == null) continue; + if (isWorldBlacklisted(restLoc.getWorld())) continue; + if (!(restLoc.getBlock().getState() instanceof Chest restChest)) continue; + + Inventory restInv = restChest.getInventory(); + boolean anyMoved = false; + + for (int slot = 0; slot < restInv.getSize(); slot++) { + ItemStack item = restInv.getItem(slot); + if (item == null || item.getType() == Material.AIR) continue; + + List targets = targetMap.getOrDefault(item.getType().name(), new ArrayList<>()); + Location targetLoc = null; + for (Location t : targets) { + if (t != null && !isChestCachedFull(t)) { targetLoc = t; break; } + } + if (targetLoc == null) continue; + if (!(targetLoc.getBlock().getState() instanceof Chest targetChest)) continue; + + // Schild-Validierung: Zieltruhe muss dem Besitzer gehören + boolean validTarget = false; + for (Block b2 : getChestBlocks(targetChest)) { + for (Block face2 : new Block[]{b2.getRelative(1,0,0), b2.getRelative(-1,0,0), b2.getRelative(0,0,1), b2.getRelative(0,0,-1)}) { + if (face2.getState() instanceof Sign ts2 && isSignAttachedToChest(face2, b2)) { + String sOwner; + if (isCleanTargetSign(ts2)) { + sOwner = getCleanSignOwner(ts2); + } else { + String l0 = ChatColor.stripColor(ts2.getLine(0) != null ? ts2.getLine(0) : ""); + String l1 = ChatColor.stripColor(ts2.getLine(1) != null ? ts2.getLine(1) : ""); + String l3 = ChatColor.stripColor(ts2.getLine(3) != null ? ts2.getLine(3) : ""); + if (!l0.equalsIgnoreCase("[asc]") || !isZiel(l1)) continue; + sOwner = l3.replace("[Public]", "").replace("[public]", "").trim(); + } + if (sOwner.equalsIgnoreCase(finalOwnerName)) { validTarget = true; break; } + } + } + if (validTarget) break; + } + if (!validTarget) continue; + + Map leftover = targetChest.getInventory().addItem(item.clone()); + boolean full = !leftover.isEmpty(); + if (full) { + markChestFull(targetLoc); + int moved = item.getAmount() - leftover.get(0).getAmount(); + if (moved > 0) { + item.setAmount(leftover.get(0).getAmount()); + restInv.setItem(slot, item); + spawnTransferParticles(null, targetLoc); + anyMoved = true; + } + } else { + restInv.setItem(slot, null); + spawnTransferParticles(null, targetLoc); + anyMoved = true; + } + + // Schild der Zieltruhe aktualisieren (voll/nicht voll) + for (Block b2 : getChestBlocks(targetChest)) { + for (Block face2 : new Block[]{b2.getRelative(1,0,0), b2.getRelative(-1,0,0), b2.getRelative(0,0,1), b2.getRelative(0,0,-1)}) { + if (face2.getState() instanceof Sign ts2 && isSignAttachedToChest(face2, b2)) { + boolean isFull2 = isInventoryFull(targetChest.getInventory()); + if (isCleanTargetSign(ts2)) { + String cOwner = getCleanSignOwner(ts2); + boolean cPub = isChestPublic(ts2); + String cItem = ChatColor.stripColor(ts2.getLine(0)); + applyCleanTargetSign(ts2, cItem, cOwner, cPub, isFull2); + ts2.update(); + } else { + String l0 = ChatColor.stripColor(ts2.getLine(0) != null ? ts2.getLine(0) : ""); + String l1 = ChatColor.stripColor(ts2.getLine(1) != null ? ts2.getLine(1) : ""); + if (l0.equalsIgnoreCase("[asc]") && isZiel(l1)) { + String ct = isFull2 ? "full" : "target"; + ts2.setLine(0, getSignColor(ct, "line1") + "[asc]"); + ts2.setLine(1, getSignColor(ct, "line2") + l1); + ts2.setLine(2, getSignColor(ct, "line3") + ChatColor.stripColor(ts2.getLine(2) != null ? ts2.getLine(2) : "")); + ts2.setLine(3, ts2.getLine(3) != null ? ts2.getLine(3) : ""); + ts2.update(); + } + } + } + } + } + } + + if (anyMoved && isDebug()) { + getLogger().info("[ResortRest] Rest-Truhe von " + finalOwnerName + " bei " + restLoc.getBlockX() + "," + restLoc.getBlockY() + "," + restLoc.getBlockZ() + " nachsortiert."); + } + } + } + } + private void checkInputChests() { if (mysqlEnabled && mysqlManager != null) { - // Async: DB-Daten lesen, dann sync: World-Ops ausfuehren final boolean crosslink = serverCrosslink; final String srvName = serverName; new BukkitRunnable() { @Override public void run() { - // ── ASYNC: Alle Input-Chest-Daten aus DB lesen ────────────── - // Liste von: [ownerUUID, chestMap, isLocal(bool)] final List jobs = new ArrayList<>(); try (java.sql.Statement st = mysqlManager.getConnection().createStatement()) { java.sql.ResultSet rs = st.executeQuery("SELECT uuid FROM asc_players"); @@ -2548,9 +3982,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } return Bukkit.getWorld((String) c.get("world")) != null; }); - // Preload Zieltruhen+Rest nur wenn es lokale Input-Truhen gibt List> preTargets = anyLocal ? mysqlManager.getTargetChests(uuidString) : new ArrayList<>(); - // BUG FIX: Alle Rest-Truhen vorladen (nicht nur eine) List> preRests = anyLocal ? mysqlManager.getRestChests(uuidString) : new ArrayList<>(); for (Map chest : chests) { @@ -2570,7 +4002,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (jobs.isEmpty()) return; - // ── SYNC: World-Zugriffe auf Main Thread ───────────────────── new BukkitRunnable() { @SuppressWarnings("unchecked") @Override @@ -2579,10 +4010,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { UUID ownerUUID = (UUID) job[0]; Map chest = (Map) job[1]; boolean isLocal = (boolean) job[2]; - @SuppressWarnings("unchecked") List> preTargets = (List>) job[3]; - @SuppressWarnings("unchecked") - // BUG FIX: preRests ist jetzt eine Liste List> preRests = (List>) job[4]; if (isLocal) { @@ -2591,7 +4019,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { Location loc = new Location(world, (int) chest.get("x"), (int) chest.get("y"), (int) chest.get("z")); - // Preloaded-Daten in Locations umwandeln (Main Thread, kein DB-Hit) Map> targetLocs = new HashMap<>(); for (Map tc : preTargets) { if (isRemoteChest(tc)) continue; @@ -2600,7 +4027,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { targetLocs.computeIfAbsent((String) tc.get("item"), k -> new ArrayList<>()) .add(new Location(w, (int) tc.get("x"), (int) tc.get("y"), (int) tc.get("z"))); } - // BUG FIX: Alle Rest-Truhen als Liste konvertieren List restLocs = new ArrayList<>(); for (Map preRest : preRests) { if (preRest != null && !isRemoteChest(preRest)) { @@ -2660,7 +4086,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { .add(new Location(world, (int) tc.get("x"), (int) tc.get("y"), (int) tc.get("z"))); } - // BUG FIX: Alle lokalen Rest-Truhen laden List localRestChests = getRestChestLocations(ownerUUID); if (localTargets.isEmpty() && localRestChests.isEmpty()) return; @@ -2680,7 +4105,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { mysqlManager.setupTransferTable(); - // ── BungeeCord NEU: nach serverName filtern ─────────────────────────── List> pendingTransfers = mysqlManager.getPendingTransfers(ownerUUID.toString(), serverName); for (Map transfer : pendingTransfers) { @@ -2699,7 +4123,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { for (Location l : slots) { if (l != null && !isChestCachedFull(l)) { targetLoc = l; break; } } - // BUG FIX: Alle lokalen Rest-Truhen als Fallback versuchen if (targetLoc == null) { for (Location restLoc : localRestChests) { if (restLoc != null && !isChestCachedFull(restLoc)) { targetLoc = restLoc; break; } @@ -2746,12 +4169,10 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String basePath = "players." + ownerUUID + ".target-chests"; if (playerData.contains(basePath)) { for (String item : playerData.getConfigurationSection(basePath).getKeys(false)) { - String path = basePath + "." + item; - World w = Bukkit.getWorld(playerData.getString(path + ".world")); - if (w == null) continue; - targets.computeIfAbsent(item, k -> new ArrayList<>()) - .add(new Location(w, playerData.getInt(path + ".x"), - playerData.getInt(path + ".y"), playerData.getInt(path + ".z"))); + // FIX: handle both old flat + new slotted format + for (Location tLoc : getTargetChestSlotsYaml(ownerUUID, item)) { + targets.computeIfAbsent(item, k -> new ArrayList<>()).add(tLoc); + } } } } @@ -2787,6 +4208,14 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { ownerName = line3Clean.replace(" [Public]", "").replace(" [public]", "").trim(); break outerLoop; } + // Clean-Input-Schild: Marker auf Zeile 4, Besitzer auf Zeile 1 + if (isCleanInputSign(sign)) { + inputSignBlock = face; + String line3Clean = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); + isPublic = line3Clean.toLowerCase().contains("[public]"); + ownerName = ChatColor.stripColor(lines[0] != null ? lines[0] : "").trim(); + break outerLoop; + } } } } @@ -2806,7 +4235,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (ownerPlayer == null || !ownerPlayer.isOnline()) ownerPlayer = null; } - // Wenn preloadedTargets null: synchron laden (Fallback) Map> effectiveTargets = preloadedTargets; List effectiveRests = preloadedRests; if (effectiveTargets == null) { @@ -2824,17 +4252,14 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String basePath = "players." + ownerUUID + ".target-chests"; if (playerData.contains(basePath)) { for (String item : playerData.getConfigurationSection(basePath).getKeys(false)) { - String path = basePath + "." + item; - World w = Bukkit.getWorld(playerData.getString(path + ".world")); - if (w == null) continue; - effectiveTargets.computeIfAbsent(item, k -> new ArrayList<>()) - .add(new Location(w, playerData.getInt(path + ".x"), - playerData.getInt(path + ".y"), playerData.getInt(path + ".z"))); + // FIX: handle both old flat + new slotted format + for (Location tLoc : getTargetChestSlotsYaml(ownerUUID, item)) { + effectiveTargets.computeIfAbsent(item, k -> new ArrayList<>()).add(tLoc); + } } } } } - // BUG FIX: Rest-Truhen als Liste laden wenn nicht vorgeladen if (effectiveRests == null) { effectiveRests = getRestChestLocations(ownerUUID); } @@ -2843,11 +4268,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return true; } - /** - * Oeffentliche Variante mit vorgeladenen Truhen-Locations (kein DB-Hit im Main Thread). - * targetChestMap: Material-Name → Location (lokale Zieltruhen) - * restChestLocs: lokale Rest-Truhen oder leere Liste - */ private void distributeItemsForOwner(UUID ownerUUID, Player ownerPlayer, Inventory sourceInventory, String ownerNameOverride, Location sourceLocation, Map> targetChestMap, List restChestLocs) { @@ -2868,9 +4288,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (offlinePlayer.hasPlayedBefore()) ownerName = offlinePlayer.getName(); } - // BUG FIX: Rest-Truhen als Liste verwalten List restChestLocations = (restChestLocs != null) ? restChestLocs : new ArrayList<>(); - // Alle vollen Rest-Truhen ermitteln boolean allRestChestsFull = !restChestLocations.isEmpty() && restChestLocations.stream().allMatch(l -> l != null && isChestCachedFull(l)); @@ -2878,23 +4296,33 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { ItemStack item = sourceInventory.getItem(slot); if (item == null || item.getType() == Material.AIR) continue; - // Vorab geladene lokale Zieltruhen (kein DB-Hit) – Liste für Multi-Slot List targetChestList = (targetChestMap != null) ? targetChestMap.getOrDefault(item.getType().name(), new ArrayList<>()) : java.util.Collections.singletonList(getTargetChestLocation(ownerUUID, item.getType())); - // Gefüllte Truhen überspringen: erste nicht-volle nehmen Location targetChestLocation = null; for (Location loc : targetChestList) { if (loc != null && !isChestCachedFull(loc)) { targetChestLocation = loc; break; } } boolean isRestChest = false; boolean isCrosslink = false; - // Aktive Rest-Truhe für dieses Item (erste nicht-volle) + boolean isTrashDest = false; Location activeRestChestLocation = null; for (Location loc : restChestLocations) { if (loc != null && !isChestCachedFull(loc)) { activeRestChestLocation = loc; break; } } + // ── MÜLLTRUHE-ROUTING: Trash-Items zur Mülltruhe schicken ───────── + // Wenn kein Ziel-Chest vorhanden: prüfen ob Item zur Mülltruche gehört. + // Mülltruhe hat Vorrang vor Rest-Truhe. + if (targetChestLocation == null && trashChestManager != null) { + Location trashLoc = trashChestManager.getTrashChestLocation(ownerUUID); + if (trashLoc != null && trashChestManager.isTrashItem(ownerUUID, item.getType())) { + targetChestLocation = trashLoc; + isTrashDest = true; + } + } + // ───────────────────────────────────────────────────────────────── + if (targetChestLocation == null) { if (serverCrosslink && mysqlEnabled && mysqlManager != null) { Map raw = mysqlManager.getTargetChest(ownerUUID.toString(), item.getType().name()); @@ -2915,14 +4343,22 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } if (!isCrosslink) { - // BUG FIX: Alle Rest-Truhen prüfen, nicht nur eine - if (allRestChestsFull) continue; + if (allRestChestsFull) { + if (!targetChestList.isEmpty() && ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { + Location fullLoc = targetChestList.get(0); + ownerPlayer.sendMessage(getMessage("target-chest-full") + .replace("%item%", item.getType().name()) + .replace("%x%", String.valueOf(fullLoc.getBlockX())) + .replace("%y%", String.valueOf(fullLoc.getBlockY())) + .replace("%z%", String.valueOf(fullLoc.getBlockZ()))); + } + continue; + } if (activeRestChestLocation != null) { targetChestLocation = activeRestChestLocation; isRestChest = true; } else if (serverCrosslink && mysqlEnabled && mysqlManager != null) { - // Crosslink: Remote Rest-Truhe suchen for (Map raw : mysqlManager.getRestChests(ownerUUID.toString())) { if (raw != null && isRemoteChest(raw)) { String targetServerName = getChestServer(raw); @@ -2943,7 +4379,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } } else { - // Bei voller Truhe: nächsten Slot in der Liste versuchen if (isChestCachedFull(targetChestLocation)) { Location nextSlot = null; for (Location loc : targetChestList) { @@ -2951,19 +4386,47 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { nextSlot = loc; break; } } - if (nextSlot != null) { targetChestLocation = nextSlot; } - else continue; // Alle Slots voll + if (nextSlot != null) { + targetChestLocation = nextSlot; + } else { + // Alle Zieltruhen gecacht-voll → Fallback auf Rest-Truhe + if (allRestChestsFull) { + if (ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { + ownerPlayer.sendMessage(getMessage("target-chest-full") + .replace("%item%", item.getType().name()) + .replace("%x%", String.valueOf(targetChestLocation.getBlockX())) + .replace("%y%", String.valueOf(targetChestLocation.getBlockY())) + .replace("%z%", String.valueOf(targetChestLocation.getBlockZ()))); + } + continue; + } + if (activeRestChestLocation != null) { + targetChestLocation = activeRestChestLocation; + isRestChest = true; + } else { + if (ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { + ownerPlayer.sendMessage(getMessage("target-chest-full") + .replace("%item%", item.getType().name()) + .replace("%x%", String.valueOf(targetChestLocation.getBlockX())) + .replace("%y%", String.valueOf(targetChestLocation.getBlockY())) + .replace("%z%", String.valueOf(targetChestLocation.getBlockZ()))); + } + continue; + } + } } } if (targetChestLocation == null) continue; if (!(targetChestLocation.getBlock().getState() instanceof Chest)) { - if (ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { + if (!isTrashDest && ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { ownerPlayer.sendMessage(getMessage("target-chest-missing").replace("%item%", isRestChest ? "Rest-Truhe" : item.getType().name())); } - // BUG FIX: Beim MySQL-Modus playerData nicht anfassen; bei YAML korrekte Pfade - if (isRestChest) { + if (isTrashDest) { + // Mülltruhe existiert nicht mehr → überspringen, kein Datenlösch-Cleanup nötig + continue; + } else if (isRestChest) { if (mysqlEnabled && mysqlManager != null) { mysqlManager.removeRestChestByLocation(ownerUUID.toString(), targetChestLocation.getWorld().getName(), @@ -2982,7 +4445,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } } - // Legacy playerData.set("players." + ownerUUID + ".rest-chest", null); savePlayerData(); } @@ -3000,6 +4462,10 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { boolean isValidTarget = false; Block signBlock = null; + if (isTrashDest) { + // Mülltruhe braucht kein [asc]-Schild als Ziel – direkt als gültig markieren + isValidTarget = true; + } else { List chestBlocks = getChestBlocks(targetChest); outerLoop: for (Block b : chestBlocks) { @@ -3009,7 +4475,23 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String line0 = ChatColor.stripColor(lines[0] != null ? lines[0] : ""); String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); String line3Clean = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); - boolean typeMatches = isRestChest ? line1.equalsIgnoreCase("rest") : line1.equalsIgnoreCase("ziel"); + // Clean-Schild: kein [asc]/ziel, aber Marker auf Z.4 + if (isCleanTargetSign(sign)) { + String signOwnerName = getCleanSignOwner(sign); + if (signOwnerName.equalsIgnoreCase(ownerName) || signOwnerName.equalsIgnoreCase("Unknown")) { + isValidTarget = true; + signBlock = face; + break outerLoop; + } + } else if (isCleanRestSign(sign) && isRestChest) { + String signOwnerName = getCleanSignOwnerAny(sign); + if (signOwnerName.equalsIgnoreCase(ownerName) || signOwnerName.equalsIgnoreCase("Unknown")) { + isValidTarget = true; + signBlock = face; + break outerLoop; + } + } else { + boolean typeMatches = isRestChest ? line1.equalsIgnoreCase("rest") : isZiel(line1); String signOwnerName = line3Clean.replace("[Public]", "").replace("[public]", "").trim(); if (line0.equalsIgnoreCase("[asc]") && typeMatches) { if (signOwnerName.equalsIgnoreCase(ownerName) || signOwnerName.equalsIgnoreCase("Unknown")) { @@ -3018,11 +4500,25 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { break outerLoop; } } + } } } } + } // end else (nicht isTrashDest) - if (!isValidTarget) continue; + if (!isValidTarget) { + // Kein gültiges ASC-Schild an der registrierten Zieltruhe + // → Fallback auf Rest-Truhe statt Item zu überspringen + if (allRestChestsFull) continue; + if (activeRestChestLocation != null) { + targetChestLocation = activeRestChestLocation; + isRestChest = true; + if (!(targetChestLocation.getBlock().getState() instanceof Chest)) continue; + targetInventory = ((Chest) targetChestLocation.getBlock().getState()).getInventory(); + } else { + continue; + } + } ItemStack itemToTransfer = item.clone(); Map leftover = targetInventory.addItem(itemToTransfer); @@ -3030,6 +4526,14 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (isFull) { markChestFull(targetChestLocation); + // Meldung IMMER senden wenn eine Zieltruhe voll ist – auch wenn Items noch in die Rest-Truhe wandern + if (!isRestChest && !isTrashDest && ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { + ownerPlayer.sendMessage(getMessage("target-chest-full") + .replace("%item%", item.getType().name()) + .replace("%x%", String.valueOf(targetChestLocation.getBlockX())) + .replace("%y%", String.valueOf(targetChestLocation.getBlockY())) + .replace("%z%", String.valueOf(targetChestLocation.getBlockZ()))); + } } if (signBlock != null) { @@ -3039,7 +4543,18 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); String line2 = ChatColor.stripColor(lines[2] != null ? lines[2] : ""); String line3Raw = lines[3] != null ? lines[3] : ""; - if (line0.equalsIgnoreCase("[asc]") && (line1.equalsIgnoreCase("ziel") || line1.equalsIgnoreCase("rest"))) { + if (isCleanTargetSign(sign)) { + // Clean-Schild: Item-Name und Öffentlich-Status bleiben, nur Farbe aktualisieren + String cleanOwner = getCleanSignOwner(sign); + boolean cleanPublic = isChestPublic(sign); + applyCleanTargetSign(sign, line0, cleanOwner, cleanPublic, isFull); + sign.update(); + } else if (isCleanRestSign(sign)) { + String cleanOwner = getCleanSignOwnerAny(sign); + boolean cleanPublic = isChestPublic(sign); + applyCleanRestSign(sign, cleanOwner, cleanPublic, isFull); + sign.update(); + } else if (line0.equalsIgnoreCase("[asc]") && (isZiel(line1) || line1.equalsIgnoreCase("rest"))) { String configType = line1.equalsIgnoreCase("rest") ? "rest" : "target"; String colorType = isFull ? "full" : configType; sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); @@ -3054,29 +4569,59 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { sourceInventory.setItem(slot, null); spawnTransferParticles(null, targetChestLocation); } else if (!isRestChest) { - // Truhe voll: Restmenge in nächsten Slot übertragen - final Location fullLoc = targetChestLocation; - Location nextSlot = null; - for (Location loc : targetChestList) { - if (loc != null && !isChestCachedFull(loc) && !loc.equals(fullLoc)) { - nextSlot = loc; break; + // Zieltruhe lief während addItem voll – versuche alle weiteren Zieltruhen der Reihe nach + ItemStack remaining = leftover.isEmpty() ? null : leftover.get(0).clone(); + boolean placed = (remaining == null); + + if (!placed) { + final Location fullLoc = targetChestLocation; + for (Location loc : targetChestList) { + if (loc == null || isChestCachedFull(loc) || loc.equals(fullLoc)) continue; + if (!(loc.getBlock().getState() instanceof Chest)) continue; + Chest nextChest = (Chest) loc.getBlock().getState(); + Map leftover2 = nextChest.getInventory().addItem(remaining.clone()); + if (leftover2.isEmpty()) { + sourceInventory.setItem(slot, null); + spawnTransferParticles(null, loc); + placed = true; + break; + } else { + markChestFull(loc); + remaining = leftover2.get(0).clone(); + } } } - if (nextSlot != null && nextSlot.getBlock().getState() instanceof Chest) { - Chest nextChest = (Chest) nextSlot.getBlock().getState(); - Map leftover2 = nextChest.getInventory().addItem( - leftover.isEmpty() ? new ItemStack(item.getType(), 0) : leftover.get(0).clone()); - if (leftover2.isEmpty()) { - sourceInventory.setItem(slot, null); - spawnTransferParticles(null, nextSlot); + + if (!placed) { + // Alle Zieltruhen voll → Fallback auf Rest-Truhe + if (activeRestChestLocation != null && activeRestChestLocation.getBlock().getState() instanceof Chest) { + Chest restChest = (Chest) activeRestChestLocation.getBlock().getState(); + Map leftover3 = restChest.getInventory().addItem(remaining.clone()); + if (leftover3.isEmpty()) { + sourceInventory.setItem(slot, null); + spawnTransferParticles(null, activeRestChestLocation); + } else { + markChestFull(activeRestChestLocation); + item.setAmount(leftover3.get(0).getAmount()); + sourceInventory.setItem(slot, item); + } } else { - item.setAmount(leftover2.get(0).getAmount()); + // Keine Rest-Truhe verfügbar – verbleibenden Stack in Input-Truhe schreiben + // + Spieler über volle Zieltruhe informieren + if (ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { + String message = getMessage("target-chest-full") + .replace("%item%", item.getType().name()) + .replace("%x%", String.valueOf(targetChestLocation.getBlockX())) + .replace("%y%", String.valueOf(targetChestLocation.getBlockY())) + .replace("%z%", String.valueOf(targetChestLocation.getBlockZ())); + ownerPlayer.sendMessage(message); + } + item.setAmount(remaining.getAmount()); sourceInventory.setItem(slot, item); } } } if (isFull && isRestChest) { - // BUG FIX: Nächste Rest-Truhe versuchen Location nextRestLoc = null; for (Location loc : restChestLocations) { if (loc != null && !isChestCachedFull(loc) && !loc.equals(targetChestLocation)) { @@ -3092,11 +4637,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { spawnTransferParticles(null, nextRestLoc); continue; } else { + // BUG FIX: nextRestLoc als voll markieren + markChestFull(nextRestLoc); item.setAmount(leftover2.get(0).getAmount()); sourceInventory.setItem(slot, item); } } else { - // Alle Rest-Truhen voll: Nachricht senden if (ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { String message = getMessage("target-chest-full") .replace("%item%", item.getType().name()) @@ -3117,14 +4663,14 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onInventoryMoveItem(InventoryMoveItemEvent event) { Inventory destination = event.getDestination(); - if (!(destination.getHolder() instanceof Chest)) return; + InventoryHolder holder = destination.getHolder(); + if (!(holder instanceof Chest) && !(holder instanceof DoubleChest)) return; - final Chest destChest = (Chest) destination.getHolder(); + final Chest destChest = (holder instanceof Chest) ? (Chest) holder : null; List blocksToScan = new ArrayList<>(); - InventoryHolder holder = destination.getHolder(); if (holder instanceof DoubleChest) { DoubleChest dc = (DoubleChest) holder; if (dc.getLeftSide() instanceof Chest) blocksToScan.add(((Chest) dc.getLeftSide()).getBlock()); @@ -3133,36 +4679,59 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { blocksToScan.add(destChest.getBlock()); } - UUID ownerUUID = null; + // ── Mülltruche: Items via Hopper → sofort löschen ──────────────────── + // Für die Mülltruchen-Prüfung benötigen wir eine repräsentative Location. + // Bei DoubleChest nehmen wir den ersten Block aus blocksToScan. + final Location destLocation = !blocksToScan.isEmpty() + ? blocksToScan.get(0).getLocation() + : (destChest != null ? destChest.getLocation() : null); + if (destLocation != null) { + final UUID trashOwnerUUID = trashChestManager.getTrashChestOwner(destLocation); + if (trashOwnerUUID != null) { + new BukkitRunnable() { + @Override + public void run() { + if (destLocation.getBlock().getState() instanceof Chest fresh) { + trashChestManager.processTrashChestInventory(trashOwnerUUID, fresh.getInventory()); + } + } + }.runTaskLater(Main.this, 1L); + return; + } + } + + // ── Schilder prüfen: ASC-Schilder an der Zieltruhe finden ──────────── + UUID ownerUUID = null; String ownerName = null; + boolean isInputChest = false; outerLoop: for (Block b : blocksToScan) { for (Block face : new Block[]{b.getRelative(1,0,0), b.getRelative(-1,0,0), b.getRelative(0,0,1), b.getRelative(0,0,-1)}) { - if (face.getState() instanceof Sign sign && isSignAttachedToChest(face, b)) { - String[] lines = sign.getLines(); - String line0 = ChatColor.stripColor(lines[0] != null ? lines[0] : ""); - String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); - if (line0.equalsIgnoreCase("[asc]") && line1.equalsIgnoreCase("input")) { - String line3Clean = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); - ownerName = line3Clean.replace(" [Public]", "").replace(" [public]", "").trim(); - OfflinePlayer op = Bukkit.getOfflinePlayer(ownerName); - if (op.hasPlayedBefore()) ownerUUID = op.getUniqueId(); - break outerLoop; - } + if (!(face.getState() instanceof Sign sign)) continue; + if (!isSignAttachedToChest(face, b)) continue; + + String[] lines = sign.getLines(); + String line0 = ChatColor.stripColor(lines[0] != null ? lines[0] : ""); + String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); + + // ── Rest-Truhe: Hopper KOMPLETT blockieren ──────────────────── + if ((line0.equalsIgnoreCase("[asc]") && line1.equalsIgnoreCase("rest")) + || isCleanRestSign(sign)) { + event.setCancelled(true); + return; + } + + // ── Eingangs-Truhe: Hopper ebenfalls blockieren ─────────────── + // (Sorting läuft über den eigenen Mechanismus, nicht via Hopper) + if ((line0.equalsIgnoreCase("[asc]") && line1.equalsIgnoreCase("input")) + || isCleanInputSign(sign)) { + event.setCancelled(true); + return; } } } - if (ownerUUID == null || ownerName == null || ownerName.isEmpty()) return; - - final UUID finalOwnerUUID = ownerUUID; - final String finalOwnerName = ownerName; - new BukkitRunnable() { - @Override - public void run() { - distributeItemsForOwner(finalOwnerUUID, null, destChest.getInventory(), finalOwnerName, destChest.getLocation()); - } - }.runTaskLater(this, 1L); + // Kein Input- oder Rest-Schild gefunden → nichts zu tun } } \ No newline at end of file diff --git a/src/main/java/com/viper/autosortchest/MySQLManager.java b/src/main/java/com/viper/autosortchest/MySQLManager.java index 5cdaa90..4f48d59 100644 --- a/src/main/java/com/viper/autosortchest/MySQLManager.java +++ b/src/main/java/com/viper/autosortchest/MySQLManager.java @@ -111,6 +111,26 @@ public class MySQLManager { } } + /** + * Stellt sicher, dass die Verbindung offen ist. + * Reconnect-Logik für den Fall, dass die MySQL-Verbindung abgelaufen ist + * (z.B. nach wait_timeout, Netzwerk-Unterbrechung). + * Wird vor jeder DB-Operation aufgerufen. + */ + private void ensureConnected() { + try { + if (connection == null || connection.isClosed() || !connection.isValid(2)) { + connection = DriverManager.getConnection( + "jdbc:mysql://" + host + ":" + port + "/" + database + + "?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&serverTimezone=UTC", + user, password + ); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + // ═══════════════════════════════════════════════════════════════════ // BungeeCord-Erweiterung: Sanfte Schema-Migration // Fügt eine Spalte hinzu, falls sie noch nicht existiert. @@ -179,6 +199,7 @@ public class MySQLManager { * asc_transfers (CrossLink Transfers) */ public void setupTables() { + ensureConnected(); try (Statement st = connection.createStatement()) { // ── Basis-Tabellen anlegen (falls nicht vorhanden) ──────────────────── @@ -229,6 +250,17 @@ public class MySQLManager { // Jede Migration ist idempotent – mehrfaches Ausführen schadet nicht. // ══════════════════════════════════════════════════════════════════════ + // ── Mülltruchen-Tabellen ────────────────────────────────────────────── + st.execute("CREATE TABLE IF NOT EXISTS asc_trash_chests (" + + "uuid VARCHAR(36) PRIMARY KEY, world VARCHAR(32)," + + "x INT, y INT, z INT, server VARCHAR(64) DEFAULT ''" + + ");"); + + st.execute("CREATE TABLE IF NOT EXISTS asc_trash_items (" + + "uuid VARCHAR(36), item VARCHAR(64)," + + "PRIMARY KEY(uuid, item)" + + ");"); + // v1 → v2 (BungeeCord): server-Spalten tryAlterColumn(st, "asc_input_chests", "server", "VARCHAR(64) DEFAULT ''"); tryAlterColumn(st, "asc_target_chests", "server", "VARCHAR(64) DEFAULT ''"); @@ -265,6 +297,7 @@ public class MySQLManager { */ public void heartbeat(String serverName) { if (serverName == null || serverName.isEmpty()) return; + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "REPLACE INTO asc_servers (server_name, last_seen) VALUES (?, NOW());")) { ps.setString(1, serverName); @@ -280,6 +313,7 @@ public class MySQLManager { */ public void addTransfer(String uuid, String item, int amount, String targetWorld, String targetServer, String sourceServer) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "INSERT INTO asc_transfers (uuid, item, amount, target_world, target_server, source_server) " + "VALUES (?, ?, ?, ?, ?, ?);")) { @@ -348,6 +382,7 @@ public class MySQLManager { } public void deleteTransfer(long id) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "DELETE FROM asc_transfers WHERE id=?;")) { ps.setLong(1, id); @@ -358,6 +393,7 @@ public class MySQLManager { } public void updateTransferAmount(long id, int newAmount) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "UPDATE asc_transfers SET amount=? WHERE id=?;")) { ps.setInt(1, newAmount); @@ -371,6 +407,7 @@ public class MySQLManager { // --- Spieler --- public void savePlayer(String uuid, String name) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "REPLACE INTO asc_players (uuid, name) VALUES (?, ?);")) { ps.setString(1, uuid); @@ -382,6 +419,7 @@ public class MySQLManager { } public List> getAllPlayers() { + ensureConnected(); List> list = new ArrayList<>(); try (Statement st = connection.createStatement(); ResultSet rs = st.executeQuery("SELECT uuid, name FROM asc_players;")) { @@ -410,6 +448,7 @@ public class MySQLManager { /** BungeeCord-Überladung mit serverName. */ public void addInputChest(String uuid, String chestId, String world, int x, int y, int z, boolean isPublic, String serverName) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "REPLACE INTO asc_input_chests (uuid, chest_id, world, x, y, z, `public`, server) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?);")) { @@ -428,6 +467,7 @@ public class MySQLManager { } public void removeInputChest(String uuid, String chestId) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "DELETE FROM asc_input_chests WHERE uuid=? AND chest_id=?;")) { ps.setString(1, uuid); @@ -439,6 +479,7 @@ public class MySQLManager { } public List> getInputChests(String uuid) { + ensureConnected(); List> list = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement( "SELECT * FROM asc_input_chests WHERE uuid=?;")) { @@ -481,6 +522,7 @@ public class MySQLManager { /** Vollständige Überladung mit slot für Multi-Target-Support. */ public void setTargetChest(String uuid, String item, int slot, String world, int x, int y, int z, boolean isPublic, String serverName) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "REPLACE INTO asc_target_chests (uuid, item, slot, world, x, y, z, `public`, server) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);")) { @@ -558,6 +600,7 @@ public class MySQLManager { /** Gibt ALLE Zieltruhen für ein bestimmtes Item zurück, sortiert nach slot. */ 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;")) { @@ -584,6 +627,7 @@ public class MySQLManager { } public void removeTargetChest(String uuid, String item) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "DELETE FROM asc_target_chests WHERE uuid=? AND item=?;")) { ps.setString(1, uuid); @@ -596,6 +640,7 @@ public class MySQLManager { /** Gibt alle Zieltruhen eines Spielers zurück, sortiert nach item + slot. */ public List> getTargetChests(String uuid) { + ensureConnected(); List> list = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement( "SELECT * FROM asc_target_chests WHERE uuid=? ORDER BY item ASC, slot ASC;")) { @@ -645,6 +690,7 @@ public class MySQLManager { /** Vollständige Überladung mit explizitem slot. */ public void setRestChest(String uuid, int slot, String world, int x, int y, int z, boolean isPublic, String serverName) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "REPLACE INTO asc_rest_chests (uuid, slot, world, x, y, z, `public`, server) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?);")) { @@ -696,6 +742,7 @@ public class MySQLManager { /** Zählt wie viele Rest-Truhen ein Spieler hat. */ public int countRestChests(String uuid) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "SELECT COUNT(*) FROM asc_rest_chests WHERE uuid=?;")) { ps.setString(1, uuid); @@ -709,6 +756,7 @@ public class MySQLManager { /** Gibt ALLE Rest-Truhen eines Spielers zurück, sortiert nach slot. */ public List> getRestChests(String uuid) { + ensureConnected(); List> list = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement( "SELECT * FROM asc_rest_chests WHERE uuid=? ORDER BY slot ASC;")) { @@ -740,6 +788,7 @@ public class MySQLManager { /** Löscht eine spezifische Rest-Truhe anhand ihrer Location. */ public void removeRestChestByLocation(String uuid, String world, int x, int y, int z) { + ensureConnected(); try (PreparedStatement ps = connection.prepareStatement( "DELETE FROM asc_rest_chests WHERE uuid=? AND world=? AND x=? AND y=? AND z=?;")) { ps.setString(1, uuid); @@ -870,4 +919,198 @@ public class MySQLManager { } return null; } + + // ══════════════════════════════════════════════════════════════════════════ + // MÜLLTRUCHEN (asc_trash_chests + asc_trash_items) + // ══════════════════════════════════════════════════════════════════════════ + + /** Legt eine Mülltruche an oder aktualisiert sie. */ + public void setTrashChest(String uuid, String world, int x, int y, int z, String server) { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO asc_trash_chests (uuid, world, x, y, z, server) VALUES (?,?,?,?,?,?) " + + "ON DUPLICATE KEY UPDATE world=VALUES(world), x=VALUES(x), y=VALUES(y), z=VALUES(z), server=VALUES(server)")) { + ps.setString(1, uuid); + ps.setString(2, world); + ps.setInt(3, x); + ps.setInt(4, y); + ps.setInt(5, z); + ps.setString(6, server == null ? "" : server); + ps.executeUpdate(); + } catch (SQLException e) { e.printStackTrace(); } + } + + /** Gibt die Mülltruche eines Spielers zurück (nur die des eigenen Servers). */ + public Map getTrashChest(String uuid, String serverName) { + try (PreparedStatement ps = connection.prepareStatement( + "SELECT * FROM asc_trash_chests WHERE uuid=? AND (server=? OR server='') LIMIT 1")) { + ps.setString(1, uuid); + ps.setString(2, serverName == null ? "" : serverName); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map map = new HashMap<>(); + map.put("world", rs.getString("world")); + map.put("x", rs.getInt("x")); + map.put("y", rs.getInt("y")); + map.put("z", rs.getInt("z")); + map.put("server", rs.getString("server")); + return map; + } + } catch (SQLException e) { e.printStackTrace(); } + return null; + } + + /** Gibt ALLE Mülltruchen zurück (für sign-update oder cross-server). */ + public List> getAllTrashChests() { + List> list = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement("SELECT * FROM asc_trash_chests")) { + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map map = new HashMap<>(); + map.put("uuid", rs.getString("uuid")); + map.put("world", rs.getString("world")); + map.put("x", rs.getInt("x")); + map.put("y", rs.getInt("y")); + map.put("z", rs.getInt("z")); + map.put("server", rs.getString("server")); + list.add(map); + } + } catch (SQLException e) { e.printStackTrace(); } + return list; + } + + /** Entfernt die Mülltruche eines Spielers. */ + public void removeTrashChest(String uuid) { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM asc_trash_chests WHERE uuid=?")) { + ps.setString(1, uuid); + ps.executeUpdate(); + } catch (SQLException e) { e.printStackTrace(); } + } + + /** Speichert die komplette Filter-Liste (ersetzt alte Einträge). */ + public void setTrashItems(String uuid, List items) { + try (PreparedStatement del = connection.prepareStatement( + "DELETE FROM asc_trash_items WHERE uuid=?")) { + del.setString(1, uuid); + del.executeUpdate(); + } catch (SQLException e) { e.printStackTrace(); } + if (items == null || items.isEmpty()) return; + try (PreparedStatement ps = connection.prepareStatement( + "INSERT IGNORE INTO asc_trash_items (uuid, item) VALUES (?,?)")) { + for (String item : items) { + ps.setString(1, uuid); + ps.setString(2, item); + ps.addBatch(); + } + ps.executeBatch(); + } catch (SQLException e) { e.printStackTrace(); } + } + + /** Lädt die Filter-Liste eines Spielers. */ + public List getTrashItems(String uuid) { + List list = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT item FROM asc_trash_items WHERE uuid=?")) { + ps.setString(1, uuid); + ResultSet rs = ps.executeQuery(); + while (rs.next()) list.add(rs.getString("item")); + } catch (SQLException e) { e.printStackTrace(); } + return list; + } + + /** Fügt ein einzelnes Item zur Filter-Liste hinzu. */ + public void addTrashItem(String uuid, String item) { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT IGNORE INTO asc_trash_items (uuid, item) VALUES (?,?)")) { + ps.setString(1, uuid); + ps.setString(2, item); + ps.executeUpdate(); + } catch (SQLException e) { e.printStackTrace(); } + } + + /** Entfernt ein einzelnes Item aus der Filter-Liste. */ + public void removeTrashItem(String uuid, String item) { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM asc_trash_items WHERE uuid=? AND item=?")) { + ps.setString(1, uuid); + ps.setString(2, item); + ps.executeUpdate(); + } catch (SQLException e) { e.printStackTrace(); } + } + + /** Entfernt alle Filter-Items eines Spielers. */ + public void removeAllTrashItems(String uuid) { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM asc_trash_items WHERE uuid=?")) { + ps.setString(1, uuid); + ps.executeUpdate(); + } catch (SQLException e) { e.printStackTrace(); } + } + + // ═══════════════════════════════════════════════════════════════════ + // FIX: Einzelne UNION-Abfrage statt 3 separater Queries für isChestPublic() + // Reduziert Main-Thread-Blockierung bei MySQL um ~66%. + // ═══════════════════════════════════════════════════════════════════ + /** + * Prüft ob eine Truhen-Location in IRGENDEINER Tabelle als public markiert ist. + * Kombiniert Input-, Target- und Rest-Tabelle in einer einzigen UNION-Abfrage. + * @return true wenn mindestens eine Zeile `public=TRUE` enthält. + */ + public boolean isLocationPublic(String world, int x, int y, int z) { + ensureConnected(); + // MySQL erfordert Klammern um einzelne SELECT-Statements wenn LIMIT verwendet wird + String sql = + "(SELECT 1 FROM asc_input_chests WHERE world=? AND x=? AND y=? AND z=? AND `public`=TRUE LIMIT 1) " + + "UNION ALL " + + "(SELECT 1 FROM asc_target_chests WHERE world=? AND x=? AND y=? AND z=? AND `public`=TRUE LIMIT 1) " + + "UNION ALL " + + "(SELECT 1 FROM asc_rest_chests WHERE world=? AND x=? AND y=? AND z=? AND `public`=TRUE LIMIT 1)"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + for (int i = 0; i < 3; 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(); + boolean result = rs.next(); + rs.close(); + return result; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // FIX: MySQL-Implementierung für findItemForChestLocation() + // Wurde bisher übersprungen – Clean-Sign-Modus + MySQL war dadurch broken. + // ═══════════════════════════════════════════════════════════════════ + /** + * Findet den Item-Typ (z.B. "IRON_ORE") der einer Truhen-Location als Zieltruhe + * zugewiesen ist. Wird von findItemForChestLocation() im Clean-Sign-Modus benötigt. + * @return Material-Name oder null wenn nicht gefunden. + */ + public String getItemForLocation(String uuid, String world, int x, int y, int z) { + ensureConnected(); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT item FROM asc_target_chests WHERE uuid=? AND world=? AND x=? AND y=? AND z=? LIMIT 1;")) { + ps.setString(1, uuid); + ps.setString(2, world); + ps.setInt(3, x); + ps.setInt(4, y); + ps.setInt(5, z); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String item = rs.getString("item"); + rs.close(); + return item; + } + rs.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } } \ No newline at end of file diff --git a/src/main/java/com/viper/autosortchest/TrashChestManager.java b/src/main/java/com/viper/autosortchest/TrashChestManager.java new file mode 100644 index 0000000..bde4380 --- /dev/null +++ b/src/main/java/com/viper/autosortchest/TrashChestManager.java @@ -0,0 +1,512 @@ +package com.viper.autosortchest; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Chest; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Verwaltet Mülltruhen für das AutoSortChest-Plugin. + * + * Speicherung: + * MySQL (wenn aktiv): + * asc_trash_chests : uuid, world, x, y, z, server + * asc_trash_items : uuid, item + * YAML (Fallback): + * players..trash-chest.world / x / y / z + * players..trash-items (StringList) + */ +public class TrashChestManager { + + private static final String SKULL_TEXTURE = + "http://textures.minecraft.net/texture/942e7fb9b8eae22d55e32b8222f38eca7b2c41948b15d769b716d80f9d113611"; + + /** Gibt den sprachabhängigen GUI-Titel zurück (Farbe aus chest-titles.trash). */ + private String getGuiTitle() { + boolean isEn = "en".equalsIgnoreCase(plugin.getConfig().getString("language", "de")); + String colorPrefix = getChestTitleColor(); + String label = isEn ? "Configure Trash Chest" : "Mülltruhe konfigurieren"; + return colorPrefix + ChatColor.BOLD + label; + } + + private final Main plugin; + + /** UUID → Truhen-Location (In-Memory-Cache für diesen Server) */ + private final Map trashChestLocations = new HashMap<>(); + /** UUID → Filter-Liste */ + private final Map> trashFilterLists = new HashMap<>(); + /** Location-Key → Besitzer-UUID */ + private final Map locationToOwner = new HashMap<>(); + /** Spieler-UUID → Truhen-Besitzer-UUID (offene GUIs) */ + private final Map openGuiOwners = new HashMap<>(); + + private BukkitTask autoTrashTask = null; + + public TrashChestManager(Main plugin) { + this.plugin = plugin; + loadAllTrashChests(); + plugin.getServer().getPluginManager().registerEvents(new GuiListener(), plugin); + } + + // ══════════════════════════════════════════════════════════════════════════ + // LADEN + // ══════════════════════════════════════════════════════════════════════════ + + private void loadAllTrashChests() { + if (plugin.isMysqlEnabled() && plugin.getMysqlManager() != null) { + loadMySQL(); + } else { + loadYaml(); + } + } + + private void loadMySQL() { + MySQLManager db = plugin.getMysqlManager(); + String myServer = plugin.getServerName(); + for (Map row : db.getAllTrashChests()) { + String srv = (String) row.getOrDefault("server", ""); + if (!srv.isEmpty() && !srv.equals(myServer)) continue; + try { + UUID uuid = UUID.fromString((String) row.get("uuid")); + World world = Bukkit.getWorld((String) row.get("world")); + if (world == null) continue; + Location loc = new Location(world, (int) row.get("x"), (int) row.get("y"), (int) row.get("z")); + trashChestLocations.put(uuid, loc); + locationToOwner.put(locKey(loc), uuid); + trashFilterLists.put(uuid, new ArrayList<>(db.getTrashItems(uuid.toString()))); + } catch (IllegalArgumentException ignored) {} + } + } + + private void loadYaml() { + FileConfiguration data = plugin.getPlayerData(); + if (data == null || data.getConfigurationSection("players") == null) return; + for (String uuidStr : data.getConfigurationSection("players").getKeys(false)) { + try { + UUID uuid = UUID.fromString(uuidStr); + String base = "players." + uuidStr + ".trash-chest"; + if (!data.contains(base + ".world")) continue; + World world = Bukkit.getWorld(data.getString(base + ".world")); + if (world == null) continue; + Location loc = new Location(world, + data.getInt(base + ".x"), data.getInt(base + ".y"), data.getInt(base + ".z")); + trashChestLocations.put(uuid, loc); + locationToOwner.put(locKey(loc), uuid); + trashFilterLists.put(uuid, new ArrayList<>(data.getStringList("players." + uuidStr + ".trash-items"))); + } catch (IllegalArgumentException ignored) {} + } + } + + // ══════════════════════════════════════════════════════════════════════════ + // SPEICHERN + // ══════════════════════════════════════════════════════════════════════════ + + private void saveTrashChest(UUID uuid) { + if (plugin.isMysqlEnabled() && plugin.getMysqlManager() != null) { + saveMySQL(uuid); + } else { + saveYaml(uuid); + } + } + + private void saveMySQL(UUID uuid) { + MySQLManager db = plugin.getMysqlManager(); + String uuidStr = uuid.toString(); + Location loc = trashChestLocations.get(uuid); + if (loc != null && loc.getWorld() != null) { + db.setTrashChest(uuidStr, loc.getWorld().getName(), + loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), + plugin.getServerName()); + db.savePlayer(uuidStr, Bukkit.getOfflinePlayer(uuid).getName()); + } else { + db.removeTrashChest(uuidStr); + } + db.setTrashItems(uuidStr, trashFilterLists.getOrDefault(uuid, new ArrayList<>())); + } + + private void saveYaml(UUID uuid) { + FileConfiguration data = plugin.getPlayerData(); + String uuidStr = uuid.toString(); + Location loc = trashChestLocations.get(uuid); + if (loc != null && loc.getWorld() != null) { + String path = "players." + uuidStr + ".trash-chest"; + data.set(path + ".world", loc.getWorld().getName()); + data.set(path + ".x", loc.getBlockX()); + data.set(path + ".y", loc.getBlockY()); + data.set(path + ".z", loc.getBlockZ()); + } else { + data.set("players." + uuidStr + ".trash-chest", null); + } + data.set("players." + uuidStr + ".trash-items", + trashFilterLists.getOrDefault(uuid, new ArrayList<>())); + plugin.savePlayerDataPublic(); + } + + // ══════════════════════════════════════════════════════════════════════════ + // TRUHE VERWALTEN + // ══════════════════════════════════════════════════════════════════════════ + + public void setTrashChestLocation(UUID uuid, Location loc) { + Location old = trashChestLocations.get(uuid); + if (old != null) locationToOwner.remove(locKey(old)); + trashChestLocations.put(uuid, loc); + locationToOwner.put(locKey(loc), uuid); + trashFilterLists.putIfAbsent(uuid, new ArrayList<>()); + saveTrashChest(uuid); + } + + public void removeTrashChest(UUID uuid) { + Location loc = trashChestLocations.remove(uuid); + if (loc != null) 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); + plugin.savePlayerDataPublic(); + } + } + + 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); + } + + public boolean isTrashItem(UUID uuid, Material mat) { + if (!trashChestLocations.containsKey(uuid)) return false; + List filter = trashFilterLists.getOrDefault(uuid, new ArrayList<>()); + // Leerer Filter = keine Items werden weitergeleitet (Mülltruhe deaktiviert) + return !filter.isEmpty() && filter.contains(mat.name()); + } + + // ══════════════════════════════════════════════════════════════════════════ + // ITEM-VERARBEITUNG + // ══════════════════════════════════════════════════════════════════════════ + + public void processTrashChestInventory(UUID ownerUUID, Inventory inv) { + List filter = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>()); + // Leerer Filter = keine Items löschen (Mülltruhe deaktiviert bis Items konfiguriert sind) + if (filter.isEmpty()) return; + for (int i = 0; i < inv.getSize(); i++) { + ItemStack item = inv.getItem(i); + if (item == null || item.getType() == Material.AIR) continue; + if (filter.contains(item.getType().name())) inv.setItem(i, null); + } + } + + public void clearTrashChest(UUID ownerUUID) { + Location loc = trashChestLocations.get(ownerUUID); + if (loc == null || loc.getWorld() == null) return; + if (loc.getBlock().getState() instanceof Chest chest) chest.getInventory().clear(); + } + + // ══════════════════════════════════════════════════════════════════════════ + // FILTER-LISTE + // ══════════════════════════════════════════════════════════════════════════ + + public boolean addToFilter(UUID uuid, Material mat) { + List filter = trashFilterLists.computeIfAbsent(uuid, k -> new ArrayList<>()); + if (filter.contains(mat.name())) return false; + filter.add(mat.name()); + if (plugin.isMysqlEnabled() && plugin.getMysqlManager() != null) { + plugin.getMysqlManager().addTrashItem(uuid.toString(), mat.name()); + } else { + saveTrashChest(uuid); + } + return true; + } + + public boolean removeFromFilter(UUID uuid, Material mat) { + List filter = trashFilterLists.get(uuid); + if (filter == null) return false; + boolean removed = filter.remove(mat.name()); + if (removed) { + if (plugin.isMysqlEnabled() && plugin.getMysqlManager() != null) { + plugin.getMysqlManager().removeTrashItem(uuid.toString(), mat.name()); + } else { + saveTrashChest(uuid); + } + } + return removed; + } + + public List getFilter(UUID uuid) { + return trashFilterLists.getOrDefault(uuid, new ArrayList<>()); + } + + // ══════════════════════════════════════════════════════════════════════════ + // AUTO-CLEAR-TASK + // ══════════════════════════════════════════════════════════════════════════ + + public void startAutoTrashTask() { + stopAutoTrashTask(); + int intervalSeconds = plugin.getConfig().getInt("trash.auto_clear_interval_seconds", 0); + if (intervalSeconds <= 0) return; + long ticks = intervalSeconds * 20L; + autoTrashTask = new BukkitRunnable() { + @Override + public void run() { + for (Map.Entry entry : new HashMap<>(trashChestLocations).entrySet()) { + Location loc = entry.getValue(); + if (loc == null || loc.getWorld() == null) continue; + if (loc.getBlock().getState() instanceof Chest chest) { + processTrashChestInventory(entry.getKey(), chest.getInventory()); + } + } + } + }.runTaskTimer(plugin, ticks, ticks); + } + + public void stopAutoTrashTask() { + if (autoTrashTask != null) { autoTrashTask.cancel(); autoTrashTask = null; } + } + + // ══════════════════════════════════════════════════════════════════════════ + // KONFIGURATIONS-GUI + // ══════════════════════════════════════════════════════════════════════════ + + public void openConfigGui(Player player, UUID ownerUUID) { + openGuiOwners.put(player.getUniqueId(), ownerUUID); + Inventory gui = Bukkit.createInventory(null, 54, getGuiTitle()); + + List filter = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>()); + int displaySlot = 0; + for (String matName : filter) { + if (displaySlot >= 45) break; + Material mat = Material.matchMaterial(matName); + if (mat == null || mat == Material.AIR) continue; + ItemStack display = new ItemStack(mat, 1); + ItemMeta meta = display.getItemMeta(); + if (meta != null) { + meta.setDisplayName(getSignColor("trash", "line1") + formatMaterialName(matName)); + meta.setLore(Arrays.asList(getSignColor("trash", "line4") + "Rechtsklick: Entfernen")); + display.setItemMeta(meta); + } + gui.setItem(displaySlot++, display); + } + + ItemStack filler = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); + ItemMeta fillerMeta = filler.getItemMeta(); + if (fillerMeta != null) { fillerMeta.setDisplayName(" "); filler.setItemMeta(fillerMeta); } + for (int i = 45; i <= 53; i++) gui.setItem(i, filler.clone()); + + ItemStack modeInfo = new ItemStack(filter.isEmpty() ? Material.BARRIER : Material.WATER_BUCKET); + ItemMeta modeMeta = modeInfo.getItemMeta(); + if (modeMeta != null) { + if (filter.isEmpty()) { + modeMeta.setDisplayName(getSignColor("trash", "line2") + "" + ChatColor.BOLD + "Modus: Deaktiviert"); + modeMeta.setLore(Arrays.asList( + ChatColor.GRAY + "Kein Filter gesetzt –", + ChatColor.GRAY + "Items werden NICHT gelöscht.", + getSignColor("trash", "line1") + "Füge Items hinzu um die Mülltruhe", + getSignColor("trash", "line1") + "zu aktivieren.")); + } else { + modeMeta.setDisplayName(getSignColor("trash", "line4") + "" + ChatColor.BOLD + "Modus: Filter aktiv"); + modeMeta.setLore(Arrays.asList( + ChatColor.GRAY + "Nur gefilterte Items", + ChatColor.GRAY + "werden gelöscht.")); + } + modeInfo.setItemMeta(modeMeta); + } + gui.setItem(48, modeInfo); + + ItemStack addBtn = new ItemStack(Material.LIME_STAINED_GLASS_PANE); + ItemMeta addMeta = addBtn.getItemMeta(); + if (addMeta != null) { + addMeta.setDisplayName(getSignColor("trash", "line1") + "" + ChatColor.BOLD + "Item hinzufügen"); + addMeta.setLore(Arrays.asList( + ChatColor.GRAY + "Item in die Hand nehmen", + ChatColor.GRAY + "und diesen Button klicken.")); + addBtn.setItemMeta(addMeta); + } + gui.setItem(49, addBtn); + gui.setItem(53, buildSkullButton()); + player.openInventory(gui); + } + + public UUID getGuiOwner(UUID playerUUID) { + return openGuiOwners.get(playerUUID); + } + + private ItemStack buildSkullButton() { + ItemStack skull; + try { + skull = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) skull.getItemMeta(); + if (meta != null) { + PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID(), "TrashBtn"); + PlayerTextures textures = profile.getTextures(); + textures.setSkin(new URL(SKULL_TEXTURE)); + profile.setTextures(textures); + meta.setOwnerProfile(profile); + meta.setDisplayName(getChestTitleColor() + "" + ChatColor.BOLD + "Mülltruhe leeren"); + meta.setLore(Arrays.asList( + ChatColor.GRAY + "Klicken um alle Items", + ChatColor.GRAY + "sofort zu löschen.")); + skull.setItemMeta(meta); + } + } catch (Exception e) { + skull = new ItemStack(Material.RED_DYE); + ItemMeta meta = skull.getItemMeta(); + if (meta != null) { + meta.setDisplayName(getChestTitleColor() + "" + ChatColor.BOLD + "Mülltruhe leeren"); + meta.setLore(Arrays.asList( + ChatColor.GRAY + "Klicken um alle Items", + ChatColor.GRAY + "sofort zu löschen.")); + skull.setItemMeta(meta); + } + } + return skull; + } + + // ══════════════════════════════════════════════════════════════════════════ + // GUI-LISTENER + // ══════════════════════════════════════════════════════════════════════════ + + private class GuiListener implements Listener { + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!getGuiTitle().equals(event.getView().getTitle())) return; + event.setCancelled(true); + + int clickedSlot = event.getRawSlot(); + UUID ownerUUID = openGuiOwners.get(player.getUniqueId()); + if (ownerUUID == null) return; + + if (clickedSlot == 53) { + clearTrashChest(ownerUUID); + player.sendMessage(getMessage("trash-cleared")); + return; + } + + if (clickedSlot == 49) { + ItemStack inHand = player.getInventory().getItemInMainHand(); + if (inHand.getType() == Material.AIR) { + player.sendMessage(getMessage("no-item-in-hand")); + return; + } + Material mat = inHand.getType(); + if (addToFilter(ownerUUID, mat)) { + player.sendMessage(getMessage("trash-item-added").replace("%item%", formatMaterialName(mat.name()))); + } else { + player.sendMessage(getMessage("trash-item-already")); + } + openConfigGui(player, ownerUUID); + return; + } + + if (clickedSlot >= 45) return; + + if (event.isRightClick()) { + ItemStack clicked = event.getCurrentItem(); + if (clicked == null || clicked.getType() == Material.AIR) return; + Material mat = clicked.getType(); + if (removeFromFilter(ownerUUID, mat)) { + player.sendMessage(getMessage("trash-item-removed").replace("%item%", formatMaterialName(mat.name()))); + openConfigGui(player, ownerUUID); + } + } + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + if (event.getPlayer() instanceof Player player) { + openGuiOwners.remove(player.getUniqueId()); + } + } + } + + // ══════════════════════════════════════════════════════════════════════════ + // HILFSMETHODEN + // ══════════════════════════════════════════════════════════════════════════ + + private String getMessage(String key) { + String msg = plugin.getConfig().getString("messages." + key, "messages." + key + " fehlt"); + return org.bukkit.ChatColor.translateAlternateColorCodes('&', msg); + } + + /** + * Liest eine Schildfarbe aus sign-colors.<type>.<line> in der Config. + * Gibt übersetzten §-Code zurück (z.B. §6 für &6). + */ + private String getSignColor(String type, String line) { + String raw = plugin.getConfig().getString("sign-colors." + type + "." + line, "&f"); + return ChatColor.translateAlternateColorCodes('&', raw); + } + + /** + * Liest nur den Farb-Präfix aus chest-titles.trash (ohne Titeltext). + * Gibt übersetzten §-Code zurück. + */ + private String getChestTitleColor() { + boolean isEn = "en".equalsIgnoreCase(plugin.getConfig().getString("language", "de")); + String lang = isEn ? "en" : "de"; + String full = plugin.getConfig().getString("chest-titles.trash." + lang, + isEn ? "&4Trash Chest" : "&4Mülltruhe"); + // Nur die führenden &X / &l Codes extrahieren + 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++; // Zeichen überspringen + } else { + break; // erster Nicht-Code-Zeichenblock → abbrechen + } + } + return ChatColor.translateAlternateColorCodes('&', codes.length() > 0 ? codes.toString() : "&4"); + } + + private String locKey(Location loc) { + return loc.getWorld().getName() + ":" + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ(); + } + + public static String formatMaterialName(String name) { + if (name == null || name.isEmpty()) return ""; + String[] parts = name.split("_"); + StringBuilder sb = new StringBuilder(); + for (String part : parts) { + if (sb.length() > 0) sb.append(' '); + if (!part.isEmpty()) { + sb.append(Character.toUpperCase(part.charAt(0))); + if (part.length() > 1) sb.append(part.substring(1).toLowerCase()); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/viper/autosortchest/UpdateChecker.java b/src/main/java/com/viper/autosortchest/UpdateChecker.java index faa7afa..18eb7c8 100644 --- a/src/main/java/com/viper/autosortchest/UpdateChecker.java +++ b/src/main/java/com/viper/autosortchest/UpdateChecker.java @@ -21,7 +21,7 @@ public class UpdateChecker { 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(); + 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()); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index da9f8b8..77defd3 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -7,19 +7,24 @@ # # ============================================================ -# --- GRUNDLEGUNG --- -# Version der Konfigurationsdatei. Nicht ändern, um Fehler zu vermeiden! +# ============================================================ +# ALLGEMEIN +# ============================================================ -version: "2.2" - -# Debug-Modus (true = Ausführliche Logs in der Server-Konsole, nur zum Entwickeln nutzen) +# Version der Konfigurationsdatei – bitte nicht ändern! +version: "2.3" +# Debug-Modus: true = Ausführliche Logs in der Konsole (nur zum Entwickeln) debug: false -# --------------------------------------------------- -# DATENBANK (MySQL/MariaDB) - Optional -# --------------------------------------------------- -# Aktiviere MySQL/MariaDB Speicherung (true/false) +# Sprache der Benutzeroberfläche +# Mögliche Werte: 'de' (Deutsch) oder 'en' (Englisch) +# Betrifft: /asc help, /asc info und die Truhen-Fenstertitel +language: "de" + +# ============================================================ +# DATENBANK (MySQL / MariaDB) – Optional +# ============================================================ mysql: enabled: false @@ -29,111 +34,80 @@ mysql: user: "autosortchest" password: "autosortchest" -# Soll serverübergreifendes Sortieren (mit MySQL) erlaubt sein? -server_crosslink: true - -# --------------------------------------------------- +# ============================================================ # BUNGEE CORD / MULTI-SERVER SETUP -# --------------------------------------------------- +# ============================================================ +# Soll serverübergreifendes Sortieren (über MySQL) erlaubt sein? +server_crosslink: false + # Eindeutiger Name dieses Servers im BungeeCord-Netzwerk. # WICHTIG: Jeder Server braucht einen anderen Namen! # -# Beispiele: -# server_name: "lobby" -# server_name: "survival" -# server_name: "creative" +# Beispiele: lobby | survival | creative # # Leer lassen = Legacy-Modus (welt-basierte Erkennung, kein BungeeCord). -# Fuer BungeeCord MUSS mysql.enabled: true gesetzt sein! +# Für BungeeCord MUSS mysql.enabled: true gesetzt sein! # -# Setup-Schritte: +# Setup: # 1. Gleiche MySQL-Datenbank auf allen Servern eintragen. # 2. Auf jedem Server einen einzigartigen server_name setzen. # 3. mysql.enabled: true und server_crosslink: true setzen. -# 4. Alle Server neu starten - Schema wird automatisch migriert. +# 4. Alle Server neu starten – Schema wird automatisch migriert. server_name: "" -# --------------------------------------------------- -# SPRACHE (Language) -# --------------------------------------------------- -# Mögliche Werte: 'de' für Deutsch oder 'en' für Englisch -# Ändert den Text von /asc help und /asc info -language: "de" - -# --------------------------------------------------- -# BLACKLIST FÜR WELTEN (Optional) -# --------------------------------------------------- +# ============================================================ +# WELTEN-BLACKLIST +# ============================================================ # Welten, in denen AutoSortChest NICHT funktioniert world_blacklist: - "world_nether" - "world_the_end" -# --------------------------------------------------- -# VISUELLE EFFEKTE (PARTIKEL & TÖNE) -# --------------------------------------------------- - -# Einstellungen für den Regenbogen-Effekt beim Sortieren -effects: - # Sollen Effekte angezeigt werden? - enabled: false - # Soll ein Ton gespielt werden, wenn Items ankommen? - sound: false - # Der Partikel-Typ. - # 'DUST' ist zwingend für den bunten Regenbogen-Effekt im aktuellen Code. - type: "DUST" - -# --------------------------------------------------- +# ============================================================ # SORTIER-INTERVALL (Ticks) -# --------------------------------------------------- -# Wie oft soll sortiert werden? (1 Tick = 0,05s) -# -# Wähle hier je nach Server-Leistung: -# -# 1 = SEHR SCHNELL (Items verschwinden sofort) -# WARNUNG: Kann bei vielen Truhen Lagg verursachen! -# -# 5 = SCHNELL (Sehr flüssig, gute Balance) -# Empfohlen für schnelle Server. -# -# 10 = FLÜSSIG (0,5s Verzögerung) -# Spart Ressourcen, fühlt sich noch schnell an. -# -# 20 = STANDARD (1 Sekunde) -# Standard-Wert, minimale Last. -# -# 30+ = SPARSAM (>1,5 Sekunden) -# Für sehr große Server mit schwacher Hardware. +# ============================================================ +# Wie oft soll sortiert werden? (1 Tick = 0,05 Sekunden) # +# 1 = Sehr schnell – Items verschwinden sofort +# WARNUNG: Kann bei vielen Truhen Lag verursachen! +# 5 = Schnell – Sehr flüssig, gute Balance +# 10 = Flüssig – 0,5s Verzögerung, spart Ressourcen <- Empfohlen +# 20 = Standard – 1 Sekunde, minimale Last +# 30+ = Sparsam – Für große Server mit schwacher Hardware -sort_interval_ticks: 5 +sort_interval_ticks: 10 -# --------------------------------------------------- -# LIMITS FÜR SORTIERKISTEN (Optional) -# --------------------------------------------------- -# Maximale Anzahl an Sortierkisten pro Spielergruppe +# Wie oft werden Rest-Truhen neu einsortiert? (in Ticks) +rest_resort_interval_ticks: 400 # 400 Ticks = 20 Sekunden + +# ============================================================ +# TRUHEN-LIMITS +# ============================================================ +# Maximale Anzahl an Sortiertruhen pro Spielergruppe. +# Sollen Limits aktiv sein? (true = ja, false = keine Beschränkung) chest_limits: - # Sollen Truhen-Limits aktiv sein? (true = ja, false = keine Beschraenkung) - enabled: true - - # Jede Gruppe hat eigene Limits fuer input, rest und target. - # Spieler benoetigen die Permission: autosortchest.limit. + enabled: false + # Spieler benötigen die Permission: autosortchest.limit. + # WICHTIG: Spieler OHNE jegliche autosortchest.limit.*-Permission können bei + # aktivierten Limits KEINE Truhen erstellen (Limit = 0). + # Vergib autosortchest.limit.default an alle normalen Spieler (z.B. in LuckPerms). default: - input: 1 # Eingangstruhen (Input) + input: 1 # Eingangstruhen rest: 1 # Rest-Truhen (Fallback) target: 50 # Zieltruhen gesamt - target_per_item: 1 # Wie viele Zieltruhen pro Item-Typ erlaubt sind + target_per_item: 1 # Zieltruhen pro Item-Typ vip: input: 2 rest: 2 target: 100 target_per_item: 3 - - # Weitere Gruppen: + + # Weitere Gruppen (auskommentiert): # supporter: # input: 3 # rest: 2 @@ -145,61 +119,197 @@ chest_limits: # target: 200 # target_per_item: 10 -# --------------------------------------------------- -# SCHILDFARBEN (Farbcodes wie im Chat) -# &c = Rot, &a = Grün, &e = Gelb, &6 = Gold, &f = Weiß, &0 = Schwarz -# --------------------------------------------------- +# ============================================================ +# MÜLLTRUHE +# ============================================================ +# auto_clear_interval_seconds: +# 0 = Deaktiviert (Truhe wird nur beim Schließen geleert) +# 300 = Alle 5 Minuten +# 3600 = Stündlich + +trash: + auto_clear_interval_seconds: 0 + +# ============================================================ +# VISUELLE EFFEKTE (Partikel & Töne) +# ============================================================ + +effects: + # Sollen Partikel-Effekte angezeigt werden? + enabled: false + # Soll ein Ton gespielt werden, wenn Items ankommen? + sound: false + # Partikel-Typ ('DUST' = bunter Regenbogen-Effekt) + type: "DUST" + +# ============================================================ +# SCHILD-STIL +# ============================================================ + +sign-style: + # Saubere Zieltruhen-Schilder (ohne Item-Namen auf dem Schild) + # true = Clean-Modus aktiviert + clean-target: false + +# ============================================================ +# SCHILDFARBEN +# ============================================================ +# Farbcodes: &0-&9, &a-&f | &l = Fett, &o = Kursiv, &r = Reset +# +# &0 Schwarz &1 Dunkelblau &2 Dunkelgrün &3 Cyan +# &4 Dunkelrot &5 Lila &6 Gold &7 Grau +# &8 Dunkelgrau &9 Blau &a Grün &b Aqua +# &c Rot &d Pink &e Gelb &f Weiß sign-colors: - # Farben für die Eingangstruhe ([asc] / input) - input: - line1: "&6" # Zeile 1: [asc] - line2: "&0" # Zeile 2: input - line4: "&1" # Zeile 4: Spielername - - # Farben für die Zieltruhe ([asc] / ziel) - target: - line1: "&6" # Zeile 1: [asc] - line2: "&0" # Zeile 2: ziel - line3: "&f" # Zeile 3: Item-Name - line4: "&1" # Zeile 4: Spielername - - # Farben für volle Truhen (Automatische Erkennung) - full: - line1: "&c" # Zeile 1: [asc] - line2: "&4" # Zeile 2: ziel / rest (Rot) - line3: "&e" # Zeile 3: Item-Name (Gelb) - line4: "&1" # Zeile 4: Spielername - - # Farben für die Rest-Truhe ([asc] / rest) - rest: - line1: "&6" # Zeile 1: [asc] - line2: "&0" # Zeile 2: rest - line3: "&f" # Zeile 3: (Leer) - line4: "&1" # Zeile 4: Spielername -# --------------------------------------------------- -# SYSTEM NACHRICHTEN (Spieler-Feedback) + # Eingangstruhe ([asc] / input) + input: + line1: "&6" # Zeile 1: [asc] + line2: "&0" # Zeile 2: input + line4: "&1" # Zeile 4: Spielername + + # Zieltruhe ([asc] / ziel) + target: + line1: "&6" # Zeile 1: [asc] + line2: "&0" # Zeile 2: ziel + line3: "&f" # Zeile 3: Item-Name + line4: "&1" # Zeile 4: Spielername + + # Volle Zieltruhe (automatische Erkennung) + full: + line1: "&c" # Zeile 1: [asc] + line2: "&4" # Zeile 2: ziel / rest + line3: "&e" # Zeile 3: Item-Name + line4: "&1" # Zeile 4: Spielername + + # Rest-Truhe ([asc] / rest) + rest: + line1: "&6" # Zeile 1: [asc] + line2: "&0" # Zeile 2: rest + line3: "&f" # Zeile 3: (leer) + line4: "&1" # Zeile 4: Spielername + + # Mülltruhe ([asc] / trash) + trash: + line1: "&6" # Zeile 1: [asc] + line2: "&0" # Zeile 2: trash + line4: "&1" # Zeile 4: Spielername + +# ============================================================ +# CLEAN-MODUS SCHILDFARBEN (sign-style.clean-target: true) +# ============================================================ +# Eigene Farben für den sauberen Schild-Stil. +# Farbcodes: &0-&9, &a-&f | &l = Fett, &o = Kursiv, &r = Reset +# +# Layout im Clean-Modus (Zeile 4 = versteckter Typ-Marker, nicht konfigurierbar): +# +# input → Z1: Spielername | Z2: "Eingang/Input" | Z3: Öffentlich/Privat +# target → Z1: Item-Name | Z2: Spielername | Z3: Öffentlich/Privat +# full → Z1: Item-Name | Z2: Spielername | Z3: Öffentlich/Privat (volle Truhe) +# rest → Z1: Spielername | Z2: "Rest" | Z3: Öffentlich/Privat +# trash → Z1: Spielername | Z2: "Müll/Trash" | Z3: (leer) + +sign-colors-clean: + + # Eingangstruhe (Clean) + input: + line1: "&1" # Zeile 1: Spielername + line2: "&0" # Zeile 2: "Eingang" / "Input" + line3: "&a" # Zeile 3: Öffentlich / Privat + + # Zieltruhe (Clean) + target: + line1: "&f" # Zeile 1: Item-Name + line2: "&1" # Zeile 2: Spielername + line3: "&a" # Zeile 3: Öffentlich / Privat + + # Volle Zieltruhe (Clean) + full: + line1: "&c" # Zeile 1: Item-Name (volle Truhe) → Rot + line2: "&1" # Zeile 2: Spielername + line3: "&a" # Zeile 3: Öffentlich / Privat + + # Rest-Truhe (Clean) + rest: + line1: "&1" # Zeile 1: Spielername + line2: "&0" # Zeile 2: "Rest" + line3: "&a" # Zeile 3: Öffentlich / Privat + + # Mülltruhe (Clean) + trash: + line1: "&1" # Zeile 1: Spielername + line2: "&0" # Zeile 2: "Müll" / "Trash" + +# ============================================================ +# TRUHEN-FENSTERTITEL +# ============================================================ +# Farbe & Text des Titels, wenn ein Spieler eine ASC-Truhe öffnet. +# Farbcodes wie oben. +# +# Platzhalter für Zieltruhen: +# %item% -> wird durch den Item-Namen ersetzt (z.B. "Iron Ore") +# Beispiel: "&6%item%" zeigt "Iron Ore" in Gold + +chest-titles: + input: + de: "&6Eingangstruhe" + en: "&6Input Chest" + target: + de: "&6%item%" + en: "&6%item%" + rest: + de: "&6Rest-Truhe" + en: "&6Rest Chest" + trash: + de: "&4Mülltruhe" + en: "&4Trash Chest" + +# ============================================================ +# SYSTEM-NACHRICHTEN (Spieler-Feedback) +# ============================================================ # Platzhalter: %player%, %item%, %x%, %y%, %z%, %mode% -# --------------------------------------------------- messages: - # --- FEHLERMELDUNGEN --- - no-chest-near-sign: "&cKeine Truhe in der Nähe des Schildes!" - no-item-in-hand: "&cDu musst ein Item in der Hand halten!" - not-your-chest: "&cDiese Truhe gehört dir nicht!" + + # --- Fehlermeldungen --- + no-chest-near-sign: "&cKeine Truhe in der Nähe des Schildes!" + no-item-in-hand: "&cDu musst ein Item in der Hand halten!" + not-your-chest: "&cDiese Truhe gehört dir nicht!" target-chest-missing: "&cZieltruhe für %item% fehlt!" - sign-break-denied: "&cDu musst Shift gedrückt halten, um dieses Schild oder die Truhe abzubauen!" - no-permission: "&cDu hast keine Berechtigung für diesen Befehl!" + sign-break-denied: "&cDu musst Shift gedrückt halten, um dieses Schild oder die Truhe abzubauen!" + no-permission: "&cDu hast keine Berechtigung für diesen Befehl!" + world-blacklisted: "&cIn dieser Welt kannst du keine AutoSortChest erstellen!" - # --- ERFOLGSMELDUNGEN --- - input-chest-set: "&aEingangstruhe erfolgreich gesetzt!" - target-chest-set: "&aZieltruhe erfolgreich für %item% eingerichtet!" - rest-chest-set: "&aRest-Truhe (Fallback) erfolgreich gesetzt!" - reload-success: "&aKonfiguration erfolgreich neu geladen!" + # --- Limit-Fehlermeldungen --- + # Platzhalter: %max% = erlaubtes Limit, %item% = Item-Name + limit-input-reached: "&cDu hast das Limit deiner Eingangstruhen erreicht! &7(%max%)" + limit-rest-reached: "&cDu hast das Limit deiner Rest-Truhen erreicht! &7(%max%)" + limit-target-reached: "&cDu hast das Limit deiner Zieltruhen erreicht! &7(%max%)" + limit-target-per-item: "&cDu hast das Limit für %item%-Truhen erreicht! &7(%max%)" + limit-no-permission: "&cDu hast keine Berechtigung um Truhen zu erstellen!" - # --- HINWEIS MELDUNGEN --- + # --- Erfolgsmeldungen --- + input-chest-set: "&aEingangstruhe erfolgreich gesetzt!" + target-chest-set: "&aZieltruhe erfolgreich für %item% eingerichtet!" + rest-chest-set: "&aRest-Truhe (Fallback) erfolgreich gesetzt!" + trash-chest-set: "&aMülltruhe erfolgreich eingerichtet!" + trash-chest-hint: "&7Rechtsklicke das Schild um Items zu konfigurieren." + reload-success: "&aKonfiguration erfolgreich neu geladen!" + + # --- Mülltruhe GUI --- + trash-cleared: "&a✔ Mülltruhe wurde geleert!" + trash-item-added: "&a✔ &e%item% &azur Müll-Liste hinzugefügt." + trash-item-already: "&eDiseses Item ist bereits in der Müll-Liste." + trash-item-removed: "&c✖ &e%item% &caus der Müll-Liste entfernt." + + # --- Mülltruhe Info (erscheint im Chat beim Öffnen der Truhe) --- + # Platzhalter: %items% = kommagetrennte Item-Liste + trash-info-empty: "&4Mülltruhe &8(Deaktiviert) &7– Rechtsklick Schild zum Konfigurieren" + trash-info-filter: "&4Müll: &f%items% &8| Schild-Rechtsklick: Konfigurieren" + + # --- Hinweise --- 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-changed: "&aModus gewechselt: &e%mode%" + mode-public: "&aÖffentlich" + mode-private: "&cPrivat" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index bce22a0..ee8cfcb 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: AutoSortChest -version: 2.3 +version: 2.5 main: com.viper.autosortchest.Main api-version: 1.21 authors: [M_Viper] @@ -10,6 +10,9 @@ commands: usage: / [help|info|reload|import|export] aliases: [autosortchest] permissions: + autosortchest.use: + description: Erlaubt das Erstellen von AutoSortChest-Schildern (Eingang, Ziel, Rest, Muelltruhe) + default: true autosortchest.reload: description: Erlaubt das Neuladen der Konfiguration mit /asc reload default: op @@ -20,14 +23,20 @@ permissions: description: Erlaubt den Export von MySQL nach players.yml mit /asc export default: op autosortchest.bypass: - description: Erlaubt das Abbauen von ASC-Schildern ohne Shift-Taste und unabhängig vom Besitzer + description: Erlaubt das Abbauen von ASC-Schildern ohne Shift-Taste und unabhaengig vom Besitzer + default: op + autosortchest.limit.bypass: + description: Umgeht alle Truhen-Limits (input, rest, target) – OPs haben dies automatisch default: op autosortchest.admin: - description: Erlaubt OPs/Admins Zugriff auf fremde AutoSortChest-Truhen (Öffnen, Entnehmen, Abbauen) + description: Erlaubt OPs/Admins Zugriff auf fremde AutoSortChest-Truhen default: op autosortchest.limit.: description: > Limits fuer eine benutzerdefinierte Gruppe aus der config.yml. Ersetze durch den Gruppennamen (z.B. autosortchest.limit.vip). Die Gruppen und ihre Limits werden ausschliesslich in der config.yml definiert. + WICHTIG: Spieler ohne jegliche autosortchest.limit.*-Permission koennen bei + aktivierten Limits (chest_limits.enabled: true) KEINE Truhen erstellen. + Vergib autosortchest.limit.default fuer den Standard-Rang. default: false \ No newline at end of file