diff --git a/src/main/java/com/viper/autosortchest/Main.java b/src/main/java/com/viper/autosortchest/Main.java index 4549cc9..b529fd5 100644 --- a/src/main/java/com/viper/autosortchest/Main.java +++ b/src/main/java/com/viper/autosortchest/Main.java @@ -49,21 +49,25 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { private boolean serverCrosslink = true; + // ── BungeeCord NEU: Eindeutiger Name dieses Servers (leer = Legacy-Modus) ─ + private String serverName = ""; + // ------------------------------------------------------- // 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 void loadOptionalSettings() { serverCrosslink = config.getBoolean("server_crosslink", true); - mysqlEnabled = config.getBoolean("mysql.enabled", false); - mysqlHost = config.getString("mysql.host", "localhost"); - mysqlPort = config.getInt("mysql.port", 3306); - mysqlDatabase = config.getString("mysql.database", "autosortchest"); - mysqlUser = config.getString("mysql.user", "root"); - mysqlPassword = config.getString("mysql.password", ""); - worldBlacklist = config.getStringList("world_blacklist"); + mysqlEnabled = config.getBoolean("mysql.enabled", false); + mysqlHost = config.getString("mysql.host", "localhost"); + mysqlPort = config.getInt("mysql.port", 3306); + mysqlDatabase = config.getString("mysql.database", "autosortchest"); + mysqlUser = config.getString("mysql.user", "root"); + mysqlPassword = config.getString("mysql.password", ""); + worldBlacklist = config.getStringList("world_blacklist"); sortIntervalTicks = config.getInt("sort_interval_ticks", 20); @@ -72,32 +76,55 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { sortIntervalTicks = 1; } + // ── BungeeCord NEU ──────────────────────────────────────────────────── + serverName = config.getString("server_name", "").trim(); + if (!serverName.isEmpty()) { + getLogger().info("[BungeeCord] server_name = \"" + serverName + "\""); + } + + chestLimitsEnabled = config.getBoolean("chest_limits.enabled", true); chestLimits = new HashMap<>(); if (config.isConfigurationSection("chest_limits")) { for (String group : config.getConfigurationSection("chest_limits").getKeys(false)) { - chestLimits.put(group, config.getInt("chest_limits." + group)); + if (group.equals("enabled")) continue; // kein Gruppen-Eintrag + Map limits = new HashMap<>(); + if (config.isConfigurationSection("chest_limits." + group)) { + // Neue verschachtelte Struktur: group.input / group.rest / group.target / group.target_per_item + limits.put("input", config.getInt("chest_limits." + group + ".input", 1)); + limits.put("rest", config.getInt("chest_limits." + group + ".rest", 1)); + limits.put("target", config.getInt("chest_limits." + group + ".target", 50)); + limits.put("target_per_item", config.getInt("chest_limits." + group + ".target_per_item", 1)); + } else { + // Legacy-Fallback + int legacy = config.getInt("chest_limits." + group, 50); + limits.put("input", 1); + limits.put("rest", 1); + limits.put("target", legacy); + limits.put("target_per_item", 1); + } + chestLimits.put(group, limits); } } } /** - * Startet oder startet den Sortier-Task und den Cleanup-Task neu. - * Bestehende Tasks werden vorher sauber gestoppt. + * 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 (sortTask != null) { sortTask.cancel(); sortTask = null; } + if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } + if (heartbeatTask != null) { heartbeatTask.cancel(); heartbeatTask = null; } // BungeeCord NEU 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(); + } } }.runTaskTimer(this, 20L, sortIntervalTicks > 0 ? sortIntervalTicks : 20L); @@ -107,17 +134,71 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { cleanMessageTracker(); } }.runTaskTimer(this, 20L * 60, 20L * 60); + + // ── BungeeCord NEU: Heartbeat alle 30 Sekunden (async) ──────────────── + if (mysqlEnabled && mysqlManager != null && !serverName.isEmpty()) { + heartbeatTask = new BukkitRunnable() { + @Override + public void run() { + mysqlManager.heartbeat(serverName); + } + }.runTaskTimerAsynchronously(this, 20L, 20L * 30); + } } - private int getChestLimitForPlayer(Player player) { + // ── 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) List groups = new ArrayList<>(chestLimits.keySet()); - groups.sort((a, b) -> Integer.compare(chestLimits.getOrDefault(b, 0), chestLimits.getOrDefault(a, 0))); + groups.sort((a, b) -> { + int la = chestLimits.getOrDefault(a, new HashMap<>()).getOrDefault("target", 0); + int lb = chestLimits.getOrDefault(b, new HashMap<>()).getOrDefault("target", 0); + return Integer.compare(lb, la); + }); for (String group : groups) { if (player.hasPermission("autosortchest.limit." + group)) { - return chestLimits.getOrDefault(group, chestLimits.getOrDefault("default", 50)); + Map limits = chestLimits.get(group); + if (limits != null) return limits.getOrDefault(type, 1); } } - return chestLimits.getOrDefault("default", 50); + // 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; + } + + /** Rueckwaertskompatibilitaet: type=target */ + private int getChestLimitForPlayer(Player player) { + return getChestLimitForPlayer(player, "target"); } private File playerDataFile; @@ -132,10 +213,16 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { private String mysqlPassword; private List worldBlacklist; private int sortIntervalTicks; - private Map chestLimits; + // group → { "input", "rest", "target" } → limit + private Map> chestLimits; + private boolean chestLimitsEnabled = true; private final Map> fullChestMessageTracker = new HashMap<>(); private static final long MESSAGE_COOLDOWN = 5 * 60 * 1000; - private static final String CONFIG_VERSION = "2.0"; + + 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.1"; // BungeeCord NEU: 2.0 → 2.1 private boolean updateAvailable = false; private String latestVersion = ""; @@ -159,6 +246,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&f2. Schreibe:\n" + " &7[asc]\n" + " &7rest\n" + + "&eBungeeCord:\n" + + "&fSetze 'server_name' in config.yml für serverübergreifendes Sortieren.\n" + "&eBefehle:\n" + "&f- &b/asc help &f- Zeigt diese Hilfe.\n" + "&f- &b/asc info &f- Zeigt Plugin-Informationen.\n" + @@ -186,6 +275,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&f2. Write:\n" + " &7[asc]\n" + " &7rest\n" + + "&eBungeeCord:\n" + + "&fSet 'server_name' in config.yml for cross-server sorting.\n" + "&eCommands:\n" + "&f- &b/asc help &f- Shows this help.\n" + "&f- &b/asc info &f- Shows plugin info.\n" + @@ -199,6 +290,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&ePlugin: &fAutoSortChest\n" + "&eVersion: &f%version%\n" + "&eConfig-Version: &f%config_version%\n" + + "&eServer-Name: &f%server_name%\n" + "&eErsteller: &fM_Viper\n" + "&eBeschreibung: &fAutomatisches Sortieren von Items in Truhen.\n" + "&6&l========================"; @@ -208,6 +300,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&ePlugin: &fAutoSortChest\n" + "&eVersion: &f%version%\n" + "&eConfig-Version: &f%config_version%\n" + + "&eServer Name: &f%server_name%\n" + "&eAuthor: &fM_Viper\n" + "&eDescription: &fAutomatically sorts items into chests.\n" + "&6&l========================"; @@ -230,7 +323,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (playerData.contains(inputListPath)) { for (String chestId : playerData.getConfigurationSection(inputListPath).getKeys(false)) { String path = inputListPath + "." + chestId; - String world = playerData.getString(path + ".world"); + String world = playerData.getString(path + ".world"); int x = playerData.getInt(path + ".x"); int y = playerData.getInt(path + ".y"); int z = playerData.getInt(path + ".z"); @@ -241,7 +334,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String targetPath = "players." + uuidString + ".target-chests"; if (playerData.contains(targetPath)) { for (String item : playerData.getConfigurationSection(targetPath).getKeys(false)) { - String path = targetPath + "." + item; + String path = targetPath + "." + item; String world = playerData.getString(path + ".world"); int x = playerData.getInt(path + ".x"); int y = playerData.getInt(path + ".y"); @@ -266,16 +359,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { // ------------------------------------------------------- // EXPORT: MySQL → YAML // ------------------------------------------------------- - /** - * Exportiert alle Daten aus MySQL in die players.yml. - * Erstellt vorher ein Backup der bestehenden players.yml. - * - * @return Anzahl migrierter Spieler, oder -1 bei Fehler - */ private int exportMySQLToYaml() { if (mysqlManager == null) return -1; - // Backup der aktuellen players.yml anlegen File backupFile = new File(getDataFolder(), "players_backup_" + System.currentTimeMillis() + ".yml"); if (playerDataFile != null && playerDataFile.exists()) { try { @@ -286,58 +372,51 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // Neue leere YAML-Struktur FileConfiguration exportData = new YamlConfiguration(); exportData.createSection("players"); int playerCount = 0; try { - // Alle Spieler aus MySQL laden List> allPlayers = mysqlManager.getAllPlayers(); for (Map playerRow : allPlayers) { String uuidString = (String) playerRow.get("uuid"); playerCount++; - // Input-Chests List> inputChests = mysqlManager.getInputChests(uuidString); for (Map chest : inputChests) { String chestId = (String) chest.get("chest_id"); String path = "players." + uuidString + ".input-chests." + chestId; - exportData.set(path + ".world", chest.get("world")); - exportData.set(path + ".x", chest.get("x")); - exportData.set(path + ".y", chest.get("y")); - exportData.set(path + ".z", chest.get("z")); + exportData.set(path + ".world", chest.get("world")); + exportData.set(path + ".x", chest.get("x")); + exportData.set(path + ".y", chest.get("y")); + exportData.set(path + ".z", chest.get("z")); exportData.set(path + ".public", chest.get("public")); } - // Target-Chests List> targetChests = mysqlManager.getTargetChests(uuidString); for (Map chest : targetChests) { String item = (String) chest.get("item"); String path = "players." + uuidString + ".target-chests." + item; - exportData.set(path + ".world", chest.get("world")); - exportData.set(path + ".x", chest.get("x")); - exportData.set(path + ".y", chest.get("y")); - exportData.set(path + ".z", chest.get("z")); + exportData.set(path + ".world", chest.get("world")); + exportData.set(path + ".x", chest.get("x")); + exportData.set(path + ".y", chest.get("y")); + exportData.set(path + ".z", chest.get("z")); exportData.set(path + ".public", chest.get("public")); } - // Rest-Chest Map restChest = mysqlManager.getRestChest(uuidString); if (restChest != null) { String path = "players." + uuidString + ".rest-chest"; - exportData.set(path + ".world", restChest.get("world")); - exportData.set(path + ".x", restChest.get("x")); - exportData.set(path + ".y", restChest.get("y")); - exportData.set(path + ".z", restChest.get("z")); + exportData.set(path + ".world", restChest.get("world")); + exportData.set(path + ".x", restChest.get("x")); + exportData.set(path + ".y", restChest.get("y")); + exportData.set(path + ".z", restChest.get("z")); exportData.set(path + ".public", restChest.get("public")); } } - // Exportierte Daten in players.yml schreiben exportData.save(playerDataFile); - // In-Memory-Daten aktualisieren playerData = exportData; getLogger().info("MySQL-Export nach YAML abgeschlossen. " + playerCount + " Spieler exportiert."); return playerCount; @@ -358,6 +437,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { mysqlManager = new MySQLManager(mysqlHost, mysqlPort, mysqlDatabase, mysqlUser, mysqlPassword); if (mysqlManager.connect()) { mysqlManager.setupTables(); + // BungeeCord NEU: Transfer-Tabelle mit Routing-Spalten anlegen/migrieren + mysqlManager.setupTransferTable(); getLogger().info("MySQL-Verbindung erfolgreich hergestellt."); migrateYamlToMySQL(); } else { @@ -405,7 +486,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { getLogger().info(" ___ _ _____ _ _____ _ _ "); getLogger().info(" / _ \\ | | / ___| | | / __ \\ | | | "); getLogger().info("/ /_\\ \\_ _| |_ ___ \\ `--. ___ _ __| |_| / \\/ |__ ___ ___| |_ "); - getLogger().info("| _ | | | | __/ _ \\ `--. \\/ _ \\| '__| __| | | '_ \\ / _ \\/ __| __|"); + getLogger().info("| _ | | | | __/ _ \\ `--. \\ / _ \\| '__| __| | | '_ \\ / _ \\/ __| __|"); getLogger().info("| | | | |_| | || (_) /\\__/ / (_) | | | |_| \\__/\\ | | | __/\\__ \\ |_ "); getLogger().info("\\_| |_/\\__,_|\\__\\___/\\____/ \\___/|_| \\__|\\____/_| |_|\\___||___/\\__|"); getLogger().info(""); @@ -413,6 +494,12 @@ 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 { + getLogger().info("[BungeeCord] Kein server_name gesetzt – Legacy-Modus (welt-basiert)."); + } } private boolean isWorldBlacklisted(World world) { @@ -422,8 +509,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { @Override public void onDisable() { - if (sortTask != null) { sortTask.cancel(); sortTask = null; } - if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } + if (sortTask != null) { sortTask.cancel(); sortTask = null; } + if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } + if (heartbeatTask != null) { heartbeatTask.cancel(); heartbeatTask = null; } // BungeeCord NEU savePlayerData(); if (mysqlEnabled && mysqlManager != null) { @@ -442,8 +530,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { public void run() { player.sendMessage(ChatColor.GOLD + "========================================"); player.sendMessage(ChatColor.GOLD + "[AutoSortChest] " + ChatColor.YELLOW + "Es ist ein Update verfügbar!"); - player.sendMessage(ChatColor.GRAY + "Deine Version: " + ChatColor.RED + getDescription().getVersion()); - player.sendMessage(ChatColor.GRAY + "Neue Version: " + ChatColor.GREEN + latestVersion); + player.sendMessage(ChatColor.GRAY + "Deine Version: " + ChatColor.RED + getDescription().getVersion()); + player.sendMessage(ChatColor.GRAY + "Neue Version: " + ChatColor.GREEN + latestVersion); player.sendMessage(ChatColor.GRAY + "Download: " + ChatColor.AQUA + "https://www.spigotmc.org/resources/" + 131309 + "/"); player.sendMessage(ChatColor.GOLD + "========================================"); } @@ -459,12 +547,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { InventoryHolder holder = chest.getInventory().getHolder(); if (holder instanceof DoubleChest) { DoubleChest doubleChest = (DoubleChest) holder; - if (doubleChest.getLeftSide() instanceof Chest) { - blocks.add(((Chest) doubleChest.getLeftSide()).getBlock()); - } - if (doubleChest.getRightSide() instanceof Chest) { - blocks.add(((Chest) doubleChest.getRightSide()).getBlock()); - } + if (doubleChest.getLeftSide() instanceof Chest) blocks.add(((Chest) doubleChest.getLeftSide()).getBlock()); + if (doubleChest.getRightSide() instanceof Chest) blocks.add(((Chest) doubleChest.getRightSide()).getBlock()); } else if (holder instanceof Chest) { blocks.add(chest.getBlock()); } @@ -533,15 +617,18 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { 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("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)); + // ── 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")); + config.set("effects.sound", defaultConfig.getBoolean("effects.sound", true)); + config.set("effects.type", defaultConfig.getString("effects.type", "DUST")); } if (!config.contains("sign-colors.input")) { @@ -594,24 +681,24 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { 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.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!")); - String targetChestFull = config.getString("messages.target-chest-full", ""); + String targetChestFull = config.getString("messages.target-chest-full", ""); String defaultTargetChestFull = defaultConfig.getString("messages.target-chest-full", "&cZieltruhe für %item% ist voll! Koordinaten: (%x%, %y%, %z%)"); if (!config.contains("messages.target-chest-full") || !targetChestFull.equals(defaultTargetChestFull)) { config.set("messages.target-chest-full", defaultTargetChestFull); } - 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.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!")); @@ -639,7 +726,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String oldPath = "players." + uuidString + ".input-chest"; if (playerData.contains(oldPath)) { try { - UUID uuid = UUID.fromString(uuidString); + UUID uuid = UUID.fromString(uuidString); String world = playerData.getString(oldPath + ".world"); int x = playerData.getInt(oldPath + ".x"); int y = playerData.getInt(oldPath + ".y"); @@ -666,15 +753,11 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { UUID playerUUID; - try { - playerUUID = UUID.fromString(uuidString); - } catch (IllegalArgumentException e) { - continue; - } + try { playerUUID = UUID.fromString(uuidString); } catch (IllegalArgumentException e) { continue; } - String basePath = "players." + uuidString; + String basePath = "players." + uuidString; String inputListPath = basePath + ".input-chests"; - String oldInputPath = basePath + ".input-chest"; + String oldInputPath = basePath + ".input-chest"; List inputLocs = new ArrayList<>(); if (playerData.contains(inputListPath)) { @@ -692,7 +775,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { 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)}) { + 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] : ""); @@ -721,7 +804,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { 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)}) { + 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] : ""); @@ -752,7 +835,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { 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)}) { + 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] : ""); @@ -780,7 +863,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } private boolean isChestPublic(Sign sign) { - String line3 = sign.getLine(3); + String line3 = sign.getLine(3); String line3Clean = ChatColor.stripColor(line3); if (line3Clean.toLowerCase().contains("[public]")) return true; @@ -790,8 +873,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } if (attachedBlock == null || !(attachedBlock.getState() instanceof Chest)) return false; - Location loc = attachedBlock.getLocation(); - String world = loc.getWorld().getName(); + Location loc = attachedBlock.getLocation(); + String world = loc.getWorld().getName(); int x = loc.getBlockX(); int y = loc.getBlockY(); int z = loc.getBlockZ(); @@ -878,19 +961,19 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { private boolean isSignAttachedToChest(Block signBlock, Block chestBlock) { if (!(signBlock.getBlockData() instanceof WallSign)) return false; - WallSign wallSign = (WallSign) signBlock.getBlockData(); + WallSign wallSign = (WallSign) signBlock.getBlockData(); Block attachedBlock = signBlock.getRelative(wallSign.getFacing().getOppositeFace()); return attachedBlock.equals(chestBlock); } private void addInputChestLocation(UUID playerUUID, Location location) { String basePath = "players." + playerUUID + ".input-chests"; - String id = UUID.randomUUID().toString(); - String path = basePath + "." + id; - 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()); + String id = UUID.randomUUID().toString(); + String path = basePath + "." + id; + 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", false); savePlayerData(); } @@ -913,8 +996,10 @@ 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); + location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), + isPublic, serverName); mysqlManager.savePlayer(playerUUID.toString(), Bukkit.getOfflinePlayer(playerUUID).getName()); } else { String basePath = "players." + playerUUID + ".input-chests"; @@ -931,12 +1016,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } } - String id = UUID.randomUUID().toString(); + String id = UUID.randomUUID().toString(); String path = basePath + "." + id; - 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 + ".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); savePlayerData(); } @@ -952,15 +1037,33 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return; } if (mysqlEnabled && mysqlManager != null) { - mysqlManager.setTargetChest(playerUUID.toString(), itemType.name(), - location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic); + // 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; + for (Map e : existing) { + String w = (String) e.get("world"); + if (w.equals(location.getWorld().getName()) + && (int) e.get("x") == location.getBlockX() + && (int) e.get("y") == location.getBlockY() + && (int) e.get("z") == location.getBlockZ()) { + slot = (int) e.get("slot"); + isUpdate = true; + break; + } + } + if (!isUpdate) slot = mysqlManager.getNextTargetSlot(playerUUID.toString(), itemType.name()); + mysqlManager.setTargetChest(playerUUID.toString(), itemType.name(), slot, + location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), + isPublic, serverName); mysqlManager.savePlayer(playerUUID.toString(), Bukkit.getOfflinePlayer(playerUUID).getName()); } else { String path = "players." + playerUUID + ".target-chests." + itemType.name(); - 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 + ".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); savePlayerData(); } @@ -972,15 +1075,17 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { private void setRestChestLocation(UUID playerUUID, Location location, boolean isPublic) { if (mysqlEnabled && mysqlManager != null) { + // ── BungeeCord NEU: serverName mitgeben ─────────────────────────── mysqlManager.setRestChest(playerUUID.toString(), - location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic); + location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), + isPublic, serverName); mysqlManager.savePlayer(playerUUID.toString(), Bukkit.getOfflinePlayer(playerUUID).getName()); } else { String path = "players." + playerUUID + ".rest-chest"; - 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 + ".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); savePlayerData(); } @@ -990,6 +1095,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (mysqlEnabled && mysqlManager != null) { Map map = mysqlManager.getRestChest(playerUUID.toString()); 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")); @@ -1007,6 +1114,8 @@ 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")); @@ -1027,6 +1136,25 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { messages.entrySet().removeIf(msg -> currentTime - msg.getValue() > MESSAGE_COOLDOWN); return messages.isEmpty(); }); + cleanFullChestCache(); + } + + private String locKey(Location loc) { + return loc.getWorld().getName() + ":" + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ(); + } + + private boolean isChestCachedFull(Location loc) { + Long ts = fullChestLocationCache.get(locKey(loc)); + return ts != null && (System.currentTimeMillis() - ts) < FULL_CHEST_CACHE_DURATION; + } + + private void markChestFull(Location loc) { + fullChestLocationCache.put(locKey(loc), System.currentTimeMillis()); + } + + private void cleanFullChestCache() { + long now = System.currentTimeMillis(); + fullChestLocationCache.entrySet().removeIf(e -> now - e.getValue() >= FULL_CHEST_CACHE_DURATION); } private boolean canSendFullChestMessage(UUID playerUUID, Material material) { @@ -1061,9 +1189,10 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (args[0].equalsIgnoreCase("info")) { String infoMessage = lang.equalsIgnoreCase("en") ? INFO_EN : INFO_DE; infoMessage = infoMessage - .replace("%version%", getDescription().getVersion()) + .replace("%version%", getDescription().getVersion()) .replace("%config_version%", config != null ? config.getString("version", CONFIG_VERSION) : CONFIG_VERSION) - .replace("%author%", String.join(", ", getDescription().getAuthors())); + .replace("%server_name%", serverName.isEmpty() ? "(nicht gesetzt)" : serverName) // BungeeCord NEU + .replace("%author%", String.join(", ", getDescription().getAuthors())); infoMessage = ChatColor.translateAlternateColorCodes('&', infoMessage); player.sendMessage(infoMessage.split("\n")); return true; @@ -1075,8 +1204,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return true; } - if (sortTask != null) { sortTask.cancel(); sortTask = null; } - if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } + if (sortTask != null) { sortTask.cancel(); sortTask = null; } + if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } + if (heartbeatTask != null) { heartbeatTask.cancel(); heartbeatTask = null; } // BungeeCord NEU reloadConfig(); config = getConfig(); @@ -1090,6 +1220,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 getLogger().info("MySQL-Verbindung nach Reload erfolgreich hergestellt."); } else { getLogger().warning("MySQL-Verbindung nach Reload fehlgeschlagen! Fallback auf YAML."); @@ -1111,7 +1242,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { player.sendMessage(getMessage("reload-success")); getLogger().info("Plugin erfolgreich neu geladen von " + player.getName() - + " (sort_interval_ticks=" + sortIntervalTicks + ")"); + + " (sort_interval_ticks=" + sortIntervalTicks + ", server_name=\"" + serverName + "\")"); return true; } @@ -1139,10 +1270,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { new BukkitRunnable() { @Override public void run() { - int playerCount = 0; - int inputCount = 0; - int targetCount = 0; - int restCount = 0; + int playerCount = 0, inputCount = 0, targetCount = 0, restCount = 0; for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { String name = uuidString; @@ -1155,12 +1283,11 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { mysqlManager.savePlayer(uuidString, name); playerCount++; - // Input-Chests String inputListPath = "players." + uuidString + ".input-chests"; if (playerData.contains(inputListPath)) { for (String chestId : playerData.getConfigurationSection(inputListPath).getKeys(false)) { - String path = inputListPath + "." + chestId; - String world = playerData.getString(path + ".world"); + String path = inputListPath + "." + chestId; + String world = playerData.getString(path + ".world"); int x = playerData.getInt(path + ".x"); int y = playerData.getInt(path + ".y"); int z = playerData.getInt(path + ".z"); @@ -1170,12 +1297,11 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // Target-Chests 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"); + 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"); @@ -1185,10 +1311,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // Rest-Chest String restPath = "players." + uuidString + ".rest-chest"; if (playerData.contains(restPath + ".world")) { - String world = playerData.getString(restPath + ".world"); + String world = playerData.getString(restPath + ".world"); int x = playerData.getInt(restPath + ".x"); int y = playerData.getInt(restPath + ".y"); int z = playerData.getInt(restPath + ".z"); @@ -1201,16 +1326,15 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { final int fp = playerCount, fi = inputCount, ft = targetCount, fr = restCount; Bukkit.getScheduler().runTask(Main.this, () -> { player.sendMessage(ChatColor.GREEN + "Import erfolgreich abgeschlossen!"); - player.sendMessage(ChatColor.GRAY + " Spieler: " + ChatColor.WHITE + fp); + player.sendMessage(ChatColor.GRAY + " Spieler: " + ChatColor.WHITE + fp); player.sendMessage(ChatColor.GRAY + " Eingangstruhen: " + ChatColor.WHITE + fi); - player.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft); - player.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr); + player.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft); + player.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr); getLogger().info("Import durch " + player.getName() + " abgeschlossen: " + fp + " Spieler, " + fi + " Input, " + ft + " Target, " + fr + " Rest."); }); } }.runTaskAsynchronously(this); - return true; } @@ -1233,7 +1357,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { new BukkitRunnable() { @Override public void run() { - // Backup nur anlegen wenn players.yml tatsächlich Spielerdaten enthält String backupName = null; boolean hasYamlData = playerDataFile != null && playerDataFile.exists() && playerData != null @@ -1252,14 +1375,10 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // Neue YAML-Struktur aufbauen FileConfiguration exportData = new YamlConfiguration(); exportData.createSection("players"); - int playerCount = 0; - int inputCount = 0; - int targetCount = 0; - int restCount = 0; + int playerCount = 0, inputCount = 0, targetCount = 0, restCount = 0; try { List> allPlayers = mysqlManager.getAllPlayers(); @@ -1267,40 +1386,37 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String uuidString = (String) playerRow.get("uuid"); playerCount++; - // Input-Chests List> inputChests = mysqlManager.getInputChests(uuidString); for (Map chest : inputChests) { String chestId = (String) chest.get("chest_id"); - String path = "players." + uuidString + ".input-chests." + chestId; - exportData.set(path + ".world", chest.get("world")); - exportData.set(path + ".x", chest.get("x")); - exportData.set(path + ".y", chest.get("y")); - exportData.set(path + ".z", chest.get("z")); + String path = "players." + uuidString + ".input-chests." + chestId; + exportData.set(path + ".world", chest.get("world")); + exportData.set(path + ".x", chest.get("x")); + exportData.set(path + ".y", chest.get("y")); + exportData.set(path + ".z", chest.get("z")); exportData.set(path + ".public", chest.get("public")); inputCount++; } - // Target-Chests List> targetChests = mysqlManager.getTargetChests(uuidString); for (Map chest : targetChests) { String item = (String) chest.get("item"); String path = "players." + uuidString + ".target-chests." + item; - exportData.set(path + ".world", chest.get("world")); - exportData.set(path + ".x", chest.get("x")); - exportData.set(path + ".y", chest.get("y")); - exportData.set(path + ".z", chest.get("z")); + exportData.set(path + ".world", chest.get("world")); + exportData.set(path + ".x", chest.get("x")); + exportData.set(path + ".y", chest.get("y")); + exportData.set(path + ".z", chest.get("z")); exportData.set(path + ".public", chest.get("public")); targetCount++; } - // Rest-Chest Map restChest = mysqlManager.getRestChest(uuidString); if (restChest != null) { String path = "players." + uuidString + ".rest-chest"; - exportData.set(path + ".world", restChest.get("world")); - exportData.set(path + ".x", restChest.get("x")); - exportData.set(path + ".y", restChest.get("y")); - exportData.set(path + ".z", restChest.get("z")); + exportData.set(path + ".world", restChest.get("world")); + exportData.set(path + ".x", restChest.get("x")); + exportData.set(path + ".y", restChest.get("y")); + exportData.set(path + ".z", restChest.get("z")); exportData.set(path + ".public", restChest.get("public")); restCount++; } @@ -1308,7 +1424,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { exportData.save(playerDataFile); - final FileConfiguration finalExport = exportData; + final FileConfiguration finalExport = exportData; final int fp = playerCount, fi = inputCount, ft = targetCount, fr = restCount; final String finalBackupName = backupName; @@ -1320,13 +1436,13 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } else { player.sendMessage(ChatColor.GRAY + " Backup: " + ChatColor.DARK_GRAY + "Übersprungen (players.yml war leer)"); } - player.sendMessage(ChatColor.GRAY + " Spieler: " + ChatColor.WHITE + fp); + player.sendMessage(ChatColor.GRAY + " Spieler: " + ChatColor.WHITE + fp); player.sendMessage(ChatColor.GRAY + " Eingangstruhen: " + ChatColor.WHITE + fi); - player.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft); - player.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr); + player.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft); + player.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr); getLogger().info("Export durch " + player.getName() + " abgeschlossen: " + fp + " Spieler, " + fi + " Input, " + ft + " Target, " + fr + " Rest." - + (finalBackupName != null ? " Backup: " + finalBackupName : " Kein Backup (players.yml war leer).")); + + (finalBackupName != null ? " Backup: " + finalBackupName : " Kein Backup.")); }); } catch (Exception e) { @@ -1336,7 +1452,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } }.runTaskAsynchronously(this); - return true; } @@ -1348,10 +1463,10 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onSignChange(SignChangeEvent event) { - Player player = event.getPlayer(); - UUID playerUUID = player.getUniqueId(); - Block signBlock = event.getBlock(); - String[] lines = event.getLines(); + Player player = event.getPlayer(); + UUID playerUUID = player.getUniqueId(); + Block signBlock = event.getBlock(); + String[] lines = event.getLines(); World world = player.getWorld(); if (worldBlacklist != null && worldBlacklist.contains(world.getName())) { @@ -1367,9 +1482,21 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { 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; + if (chestBlock == null) { player.sendMessage(getMessage("no-chest-near-sign")); return; } + // ── Limit-Pruefung: Rest-Truhe ───────────────────────────────────── + if (chestLimitsEnabled) { + int maxRest = getChestLimitForPlayer(player, "rest"); + boolean hasRestAlready = false; + if (mysqlEnabled && mysqlManager != null) { + hasRestAlready = mysqlManager.getRestChest(playerUUID.toString()) != null; + } else { + hasRestAlready = playerData.contains("players." + playerUUID + ".rest-chest"); + } + if (hasRestAlready && maxRest <= 1) { + player.sendMessage(ChatColor.RED + "Du hast bereits eine Rest-Truhe! (Limit: " + maxRest + ")"); + event.setCancelled(true); + return; + } } event.setLine(0, getSignColor("rest", "line1") + "[asc]"); event.setLine(1, getSignColor("rest", "line2") + "rest"); @@ -1386,9 +1513,34 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { 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; + 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; + if (mysqlEnabled && mysqlManager != null) { + currentInput = mysqlManager.getInputChests(playerUUID.toString()).size(); + } else { + String basePath = "players." + playerUUID + ".input-chests"; + if (playerData.contains(basePath)) { + 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) { + alreadyInput = mysqlManager.getInputChests(playerUUID.toString()).stream().anyMatch(c -> + c.get("world").equals(finalChestBlock.getWorld().getName()) + && (int)c.get("x") == finalChestBlock.getX() + && (int)c.get("y") == finalChestBlock.getY() + && (int)c.get("z") == finalChestBlock.getZ()); + } + if (!alreadyInput && currentInput >= maxInput) { + player.sendMessage(ChatColor.RED + "Du hast das Limit deiner Eingangstruhen erreicht! (" + maxInput + ")"); + event.setCancelled(true); + return; + } } event.setLine(0, getSignColor("input", "line1") + "[asc]"); event.setLine(1, getSignColor("input", "line2") + "input"); @@ -1411,10 +1563,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { 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; - } + if (chestBlock == null) { player.sendMessage(getMessage("no-chest-near-sign")); return; } event.setLine(0, "[asc]"); event.setLine(1, "ziel"); event.setLine(2, ""); @@ -1426,8 +1575,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { public void onPlayerInteract(PlayerInteractEvent event) { if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; - Player player = event.getPlayer(); - UUID playerUUID = player.getUniqueId(); + Player player = event.getPlayer(); + UUID playerUUID = player.getUniqueId(); Block clickedBlock = event.getClickedBlock(); ItemStack itemInHand = event.getItem(); @@ -1448,39 +1597,34 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return; } - String line1Clean = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); + String line1Clean = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); boolean isTargetOrRest = line1Clean.equalsIgnoreCase("ziel") || line1Clean.equalsIgnoreCase("rest"); if (isTargetOrRest) { - String line3Raw = lines[3] != null ? lines[3] : ""; - String line3Clean = ChatColor.stripColor(line3Raw); + String line3Raw = lines[3] != null ? lines[3] : ""; + String line3Clean = ChatColor.stripColor(line3Raw); String pureOwnerName = line3Clean.replace("[Public]", "").replace("[public]", "").trim(); - boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); + 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; - String newLine4; + boolean newPublic = !isPublicNow; + String newModeText, newLine4; String colorType = line1Clean.equalsIgnoreCase("rest") ? "rest" : "target"; - if (isPublicNow) { newModeText = getMessage("mode-private"); - newLine4 = getSignColor(colorType, "line4") + pureOwnerName; + newLine4 = getSignColor(colorType, "line4") + pureOwnerName; } else { newModeText = getMessage("mode-public"); - newLine4 = getSignColor(colorType, "line4") + pureOwnerName + " " + ChatColor.RESET + "[Public]"; + newLine4 = getSignColor(colorType, "line4") + pureOwnerName + " " + ChatColor.RESET + "[Public]"; } 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 (mat != null && mat != Material.AIR) setTargetChestLocation(playerUUID, chestBlock.getLocation(), mat, newPublic); } else if (line1Clean.equalsIgnoreCase("rest")) { setRestChestLocation(playerUUID, chestBlock.getLocation(), newPublic); } @@ -1496,38 +1640,49 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (line1Clean.equalsIgnoreCase("ziel")) { if (itemInHand != null && itemInHand.getType() != Material.AIR) { - int maxChests = getChestLimitForPlayer(player); - Set uniqueChestLocations = new HashSet<>(); - if (mysqlEnabled && mysqlManager != null) { - List> chests = mysqlManager.getTargetChests(playerUUID.toString()); - for (Map map : chests) { - 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); - } - } - } else { - 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"); + 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> chests = mysqlManager.getTargetChests(playerUUID.toString()); + for (Map map : chests) { + 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 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++; + } } } } - } - String thisLoc = chestBlock.getWorld().getName() + ":" + chestBlock.getX() + ":" + chestBlock.getY() + ":" + chestBlock.getZ(); - boolean alreadyTarget = uniqueChestLocations.contains(thisLoc); - if (!alreadyTarget && uniqueChestLocations.size() >= maxChests) { - player.sendMessage(ChatColor.RED + "Du hast das Limit deiner Zieltruhen erreicht! (" + maxChests + ")"); - event.setCancelled(true); - return; + String thisLoc = chestBlock.getWorld().getName() + ":" + chestBlock.getX() + ":" + chestBlock.getY() + ":" + chestBlock.getZ(); + boolean alreadyTarget = uniqueChestLocations.contains(thisLoc); + if (!alreadyTarget && uniqueChestLocations.size() >= maxChests) { + player.sendMessage(ChatColor.RED + "Du hast das Limit deiner Zieltruhen erreicht! (" + 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 + ")"); + event.setCancelled(true); + return; + } } if (!pureOwnerName.isEmpty() && !pureOwnerName.equalsIgnoreCase("Unknown") && !isOwner && !isAdmin(player)) { player.sendMessage(getMessage("not-your-chest")); @@ -1547,16 +1702,16 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } 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 + ".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()); - Chest chest = (Chest) chestBlock.getState(); + Chest chest = (Chest) chestBlock.getState(); boolean isFull = isInventoryFull(chest.getInventory()); String colorType = isFull ? "full" : "target"; sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); @@ -1588,23 +1743,22 @@ 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 line3Raw = lines[3] != null ? lines[3] : ""; + String line3Clean = ChatColor.stripColor(line3Raw); String pureOwnerName = line3Clean.replace("[Public]", "").replace("[public]", "").trim(); - boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); + 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; - String newLine4; + boolean newPublic = !isPublicNow; + String newModeText, newLine4; if (isPublicNow) { newModeText = getMessage("mode-private"); - newLine4 = getSignColor("input", "line4") + pureOwnerName; + newLine4 = getSignColor("input", "line4") + pureOwnerName; } else { newModeText = getMessage("mode-public"); - newLine4 = getSignColor("input", "line4") + pureOwnerName + " " + ChatColor.RESET + "[Public]"; + newLine4 = getSignColor("input", "line4") + pureOwnerName + " " + ChatColor.RESET + "[Public]"; } sign.setLine(3, newLine4); sign.update(); @@ -1635,12 +1789,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (clickedBlock.getState() instanceof Chest) { Block chestBlock = clickedBlock; - Block signBlock = null; + Block signBlock = null; List blocks = getChestBlocks((Chest) chestBlock.getState()); outerLoop: 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)}) { + 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]")) { @@ -1652,40 +1806,36 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } if (signBlock != null) { - Sign sign = (Sign) signBlock.getState(); - String[] lines = sign.getLines(); + Sign sign = (Sign) signBlock.getState(); + String[] lines = sign.getLines(); String line1Clean = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); if (line1Clean.equalsIgnoreCase("ziel") || line1Clean.equalsIgnoreCase("rest")) { - String line3Raw = lines[3] != null ? lines[3] : ""; - String line3Clean = ChatColor.stripColor(line3Raw); + String line3Raw = lines[3] != null ? lines[3] : ""; + String line3Clean = ChatColor.stripColor(line3Raw); String pureOwnerName = line3Clean.replace("[public]", "").replace("[Public]", "").trim(); - boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); + 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; - String newLine4; + boolean newPublic = !isPublicNow; + String newModeText, newLine4; String colorType = line1Clean.equalsIgnoreCase("rest") ? "rest" : "target"; - String baseName = getSignColor(colorType, "line4") + pureOwnerName; + String baseName = getSignColor(colorType, "line4") + pureOwnerName; if (isPublicNow) { newModeText = getMessage("mode-private"); - newLine4 = baseName; + newLine4 = baseName; } else { newModeText = getMessage("mode-public"); - newLine4 = baseName + " " + ChatColor.RESET + "[Public]"; + newLine4 = baseName + " " + ChatColor.RESET + "[Public]"; } 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 (mat != null && mat != Material.AIR) setTargetChestLocation(playerUUID, chestBlock.getLocation(), mat, newPublic); } else { setRestChestLocation(playerUUID, chestBlock.getLocation(), newPublic); } @@ -1713,7 +1863,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return; } removeOldTargetEntry(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType().name()); - Chest chest = (Chest) chestBlock.getState(); + Chest chest = (Chest) chestBlock.getState(); boolean isFull = isInventoryFull(chest.getInventory()); String colorType = isFull ? "full" : "target"; sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); @@ -1740,24 +1890,23 @@ 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 line3Raw = lines[3] != null ? lines[3] : ""; + String line3Clean = ChatColor.stripColor(line3Raw); String pureOwnerName = line3Clean.replace("[Public]", "").replace("[public]", "").trim(); - boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); + 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; - String newLine4; + boolean newPublic = !isPublicNow; + String newModeText, newLine4; String baseName = getSignColor("input", "line4") + pureOwnerName; if (isPublicNow) { newModeText = getMessage("mode-private"); - newLine4 = baseName; + newLine4 = baseName; } else { newModeText = getMessage("mode-public"); - newLine4 = baseName + " " + ChatColor.RESET + "[Public]"; + newLine4 = baseName + " " + ChatColor.RESET + "[Public]"; } sign.setLine(3, newLine4); sign.update(); @@ -1783,19 +1932,17 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { private void removeInputChestByLocation(UUID uuid, Location loc) { String basePath = "players." + uuid + ".input-chests"; - String oldPath = "players." + uuid + ".input-chest"; + 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; + String path = basePath + "." + chestId; Location savedLoc = getLocationFromPath(path); if (savedLoc != null && savedLoc.equals(loc)) { playerData.set(path, null); savePlayerData(); removed = true; - if (mysqlEnabled && mysqlManager != null) { - mysqlManager.removeInputChest(uuid.toString(), chestId); - } + if (mysqlEnabled && mysqlManager != null) mysqlManager.removeInputChest(uuid.toString(), chestId); return; } } @@ -1824,7 +1971,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { @EventHandler(priority = EventPriority.HIGHEST) public void onBlockBreak(BlockBreakEvent event) { - Block block = event.getBlock(); + Block block = event.getBlock(); Player player = event.getPlayer(); Block signBlock = null; String signOwner = ""; @@ -1836,8 +1983,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { (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"))) { - signBlock = block; - signOwner = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); + signBlock = block; + signOwner = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); } } @@ -1845,15 +1992,15 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { List blocks = getChestBlocks(chestBlock); outerLoop: 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)}) { + 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]") && (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"))) { - signBlock = face; - signOwner = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); + signBlock = face; + signOwner = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); isAscChest = true; break outerLoop; } @@ -1877,11 +2024,11 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } String[] lines = ((Sign) (isAscChest ? signBlock.getState() : block.getState())).getLines(); - String line1 = ChatColor.stripColor(lines[1]); + String line1 = ChatColor.stripColor(lines[1]); Location chestLoc = isAscChest ? block.getLocation() : ((Sign) block.getState()).getBlock() .getRelative(((WallSign) ((Sign) block.getState()).getBlockData()).getFacing().getOppositeFace()).getLocation(); - UUID ownerUUID = null; + UUID ownerUUID = null; String pureOwnerName = signOwner.replace("[Public]", "").replace("[public]", "").trim(); if (!pureOwnerName.isEmpty() && !pureOwnerName.equalsIgnoreCase("Unknown")) { OfflinePlayer op = Bukkit.getOfflinePlayer(pureOwnerName); @@ -1945,7 +2092,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { chestsToCheck.add((Chest) holder); } else if (holder instanceof DoubleChest) { DoubleChest dc = (DoubleChest) holder; - if (dc.getLeftSide() instanceof Chest) chestsToCheck.add((Chest) dc.getLeftSide()); + if (dc.getLeftSide() instanceof Chest) chestsToCheck.add((Chest) dc.getLeftSide()); if (dc.getRightSide() instanceof Chest) chestsToCheck.add((Chest) dc.getRightSide()); } else { return; @@ -1955,13 +2102,13 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { for (Chest chest : chestsToCheck) { Block chestBlock = chest.getBlock(); - Block signBlock = null; - String signType = "target"; + Block signBlock = null; + String signType = "target"; - for (Block face : new Block[]{chestBlock.getRelative(1, 0, 0), chestBlock.getRelative(-1, 0, 0), chestBlock.getRelative(0, 0, 1), chestBlock.getRelative(0, 0, -1)}) { + for (Block face : new Block[]{chestBlock.getRelative(1,0,0), chestBlock.getRelative(-1,0,0), chestBlock.getRelative(0,0,1), chestBlock.getRelative(0,0,-1)}) { if (face.getState() instanceof Sign sign && isSignAttachedToChest(face, chestBlock)) { - String[] lines = sign.getLines(); - String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); + 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"))) { signBlock = face; @@ -1973,15 +2120,15 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (signBlock == null) continue; - Sign sign = (Sign) signBlock.getState(); - String[] lines = sign.getLines(); + Sign sign = (Sign) signBlock.getState(); + String[] lines = sign.getLines(); String signOwner = 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()); + boolean isFull = isInventoryFull(chest.getInventory()); String configType = signType.equalsIgnoreCase("rest") ? "rest" : "target"; - String colorType = isFull ? "full" : configType; + String colorType = isFull ? "full" : configType; String currentLine1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); String currentLine3 = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); @@ -2019,19 +2166,19 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { end.getWorld().playSound(end, Sound.ENTITY_ITEM_PICKUP, 0.2f, 1.2f); } Location center = end.clone().add(0.5, 0.5, 0.5); - int count = 12; + int count = 12; double radius = 0.8; - float size = 1.2f; + float size = 1.2f; Color[] colors = { - Color.fromRGB(255, 0, 0), Color.fromRGB(0, 255, 0), Color.fromRGB(0, 0, 255), - Color.fromRGB(255, 255, 0), Color.fromRGB(0, 255, 255), Color.fromRGB(255, 0, 255) + Color.fromRGB(255,0,0), Color.fromRGB(0,255,0), Color.fromRGB(0,0,255), + Color.fromRGB(255,255,0), Color.fromRGB(0,255,255), Color.fromRGB(255,0,255) }; for (int i = 0; i < count; i++) { double angle = (2 * Math.PI / count) * i; - double dx = Math.cos(angle) * radius; - double dz = Math.sin(angle) * radius; + double dx = Math.cos(angle) * radius; + double dz = Math.sin(angle) * radius; Location particleLoc = center.clone().add(dx, -0.1, dz); - Color color = colors[(int) (Math.random() * colors.length)]; + Color color = colors[(int) (Math.random() * colors.length)]; double offsetX = (Math.random() - 0.5) * 0.2; double offsetZ = (Math.random() - 0.5) * 0.2; DustOptions options = new DustOptions(color, size); @@ -2039,56 +2186,243 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - private void checkInputChests() { - if (mysqlEnabled && mysqlManager != null && serverCrosslink) { - try (java.sql.Statement st = mysqlManager.getConnection().createStatement()) { - java.sql.ResultSet rs = st.executeQuery("SELECT uuid FROM asc_players"); - while (rs.next()) { - String uuidString = rs.getString("uuid"); - UUID ownerUUID; - try { ownerUUID = UUID.fromString(uuidString); } catch (IllegalArgumentException e) { continue; } - List> chests = mysqlManager.getInputChests(uuidString); - for (Map chest : chests) { - String worldName = (String) chest.get("world"); - World world = Bukkit.getWorld(worldName); + /** + * ── 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; - if (world != null) { - Location loc = new Location(world, (int) chest.get("x"), (int) chest.get("y"), (int) chest.get("z")); - checkSingleInputChest(ownerUUID, loc, (String) chest.get("chest_id"), false); - } else if (serverCrosslink) { - checkRemoteInputChest(ownerUUID, chest); + 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( + "SELECT DISTINCT uuid FROM asc_transfers WHERE target_server=? OR target_server=''")) { + psUuids.setString(1, currentServerName); + java.sql.ResultSet rs = psUuids.executeQuery(); + List uuids = new ArrayList<>(); + while (rs.next()) uuids.add(rs.getString("uuid")); + rs.close(); + + for (String uuidString : uuids) { + UUID ownerUUID; + try { ownerUUID = UUID.fromString(uuidString); } + catch (IllegalArgumentException e) { continue; } + + List> transfers = mysqlManager.getPendingTransfers(uuidString, currentServerName); + if (transfers.isEmpty()) continue; + + List> targetChestRaws = mysqlManager.getTargetChests(uuidString); + Map restChestRaw = mysqlManager.getRestChest(uuidString); + + OfflinePlayer op = Bukkit.getOfflinePlayer(ownerUUID); + String ownerName = op.getName() != null ? op.getName() : uuidString; + + workItems.add(new Object[]{ ownerUUID, ownerName, transfers, targetChestRaws, restChestRaw }); + } + } catch (Exception e) { + getLogger().warning("[CrossLink] DB-Fehler in processIncomingTransfers: " + e.getMessage()); + return; + } + + if (workItems.isEmpty()) return; + + // ── SYNC: World-Operationen auf dem Main Thread ─────────────────── + new BukkitRunnable() { + @SuppressWarnings("unchecked") + @Override + public void run() { + for (Object[] work : workItems) { + UUID ownerUUID = (UUID) work[0]; + String ownerName = (String) work[1]; + List> transfers = (List>) work[2]; + List> targetRaws = (List>) work[3]; + Map restRaw = (Map) work[4]; + + // Lokale Ziel-Truhen aufloesen (Multi-Slot) + Map> localTargets = new HashMap<>(); + for (Map tc : targetRaws) { + if (isRemoteChest(tc)) continue; + World world = Bukkit.getWorld((String) tc.get("world")); + if (world == null) continue; + localTargets.computeIfAbsent((String) tc.get("item"), k -> new ArrayList<>()) + .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)) { + 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")); + } + } + if (localRestChest == null) localRestChest = getRestChestLocation(ownerUUID); + + for (Map transfer : transfers) { + String itemName = (String) transfer.get("item"); + int amount = (int) transfer.get("amount"); + long transferId = (long) transfer.get("id"); + + Material mat = Material.matchMaterial(itemName); + if (mat == null) { + // Ungueltig: async loeschen + final long tid = transferId; + new BukkitRunnable() { @Override public void run() { + mysqlManager.deleteTransfer(tid); + }}.runTaskAsynchronously(Main.this); + continue; + } + + 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; + if (targetLoc == null) continue; + + if (!(targetLoc.getBlock().getState() instanceof Chest)) continue; + + Chest targetChest = (Chest) targetLoc.getBlock().getState(); + Map leftover = targetChest.getInventory().addItem(new ItemStack(mat, amount)); + + int transferred = amount - (leftover.isEmpty() ? 0 : leftover.get(0).getAmount()); + if (transferred > 0) { + spawnTransferParticles(null, targetLoc); + if (isDebug()) { + 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() { + if (remaining == 0) mysqlManager.deleteTransfer(tid); + else mysqlManager.updateTransferAmount(tid, remaining); + }}.runTaskAsynchronously(Main.this); + } + } } } - } - } catch (Exception e) { - getLogger().warning("Fehler beim Lesen der Spieler aus MySQL: " + e.getMessage()); + }.runTask(Main.this); } - } else if (mysqlEnabled && mysqlManager != null) { - try (java.sql.Statement st = mysqlManager.getConnection().createStatement()) { - java.sql.ResultSet rs = st.executeQuery("SELECT uuid FROM asc_players"); - while (rs.next()) { - String uuidString = rs.getString("uuid"); - UUID ownerUUID; - try { ownerUUID = UUID.fromString(uuidString); } catch (IllegalArgumentException e) { continue; } - List> chests = mysqlManager.getInputChests(uuidString); - for (Map chest : chests) { - World world = Bukkit.getWorld((String) chest.get("world")); - if (world == null) continue; - Location loc = new Location(world, (int) chest.get("x"), (int) chest.get("y"), (int) chest.get("z")); - checkSingleInputChest(ownerUUID, loc, (String) chest.get("chest_id"), false); + }.runTaskAsynchronously(this); + } + + 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"); + while (rs.next()) { + String uuidString = rs.getString("uuid"); + UUID ownerUUID; + try { ownerUUID = UUID.fromString(uuidString); } + catch (IllegalArgumentException e) { continue; } + + List> chests = mysqlManager.getInputChests(uuidString); + boolean anyLocal = chests.stream().anyMatch(c -> { + if (crosslink) { + String cs = (String) c.getOrDefault("server", ""); + if (!srvName.isEmpty() && !cs.isEmpty() && !cs.equals(srvName)) return false; + } + 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<>(); + Map preRest = anyLocal ? mysqlManager.getRestChest(uuidString) : null; + + for (Map chest : chests) { + if (crosslink) { + String chestServer = (String) chest.getOrDefault("server", ""); + if (!srvName.isEmpty() && !chestServer.isEmpty() && !chestServer.equals(srvName)) continue; + } + String worldName = (String) chest.get("world"); + boolean isLocal = Bukkit.getWorld(worldName) != null; + jobs.add(new Object[]{ ownerUUID, chest, isLocal, preTargets, preRest }); + } + } + } catch (Exception e) { + getLogger().warning("Fehler beim Lesen der Spieler aus MySQL: " + e.getMessage()); + return; } + + if (jobs.isEmpty()) return; + + // ── SYNC: World-Zugriffe auf Main Thread ───────────────────── + new BukkitRunnable() { + @SuppressWarnings("unchecked") + @Override + public void run() { + for (Object[] job : jobs) { + UUID ownerUUID = (UUID) job[0]; + Map chest = (Map) job[1]; + boolean isLocal = (boolean) job[2]; + @SuppressWarnings("unchecked") + List> preTargets = (List>) job[3]; + @SuppressWarnings("unchecked") + Map preRest = (Map) job[4]; + + if (isLocal) { + World world = Bukkit.getWorld((String) chest.get("world")); + if (world == null) continue; + 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; + World w = Bukkit.getWorld((String) tc.get("world")); + if (w == null) continue; + 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"))); + } + Location restLoc = null; + if (preRest != null && !isRemoteChest(preRest)) { + World w = Bukkit.getWorld((String) preRest.get("world")); + if (w != null) restLoc = new Location(w, + (int) preRest.get("x"), (int) preRest.get("y"), (int) preRest.get("z")); + } + + checkSingleInputChest(ownerUUID, loc, (String) chest.get("chest_id"), false, targetLocs, restLoc); + } else if (crosslink) { + checkRemoteInputChest(ownerUUID, chest); + } + } + } + }.runTask(Main.this); } - } catch (Exception e) { - getLogger().warning("Fehler beim Lesen der Spieler aus MySQL: " + e.getMessage()); - } + }.runTaskAsynchronously(this); } else { if (playerData == null || playerData.getConfigurationSection("players") == null) return; for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { UUID ownerUUID; try { ownerUUID = UUID.fromString(uuidString); } catch (IllegalArgumentException e) { continue; } String inputListPath = "players." + uuidString + ".input-chests"; - String oldInputPath = "players." + uuidString + ".input-chest"; + String oldInputPath = "players." + uuidString + ".input-chest"; List chestsToCheck = new ArrayList<>(); if (playerData.contains(inputListPath)) { chestsToCheck.addAll(playerData.getConfigurationSection(inputListPath).getKeys(false)); @@ -2099,8 +2433,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } if (chestsToCheck.isEmpty()) continue; for (String chestId : chestsToCheck) { - String path = inputListPath + "." + chestId; - Location loc = getLocationFromPath(path); + String path = inputListPath + "." + chestId; + Location loc = getLocationFromPath(path); if (loc == null) { playerData.set(path, null); continue; } boolean stillValid = checkSingleInputChest(ownerUUID, loc, chestId, false); if (!stillValid) { playerData.set(path, null); savePlayerData(); } @@ -2113,13 +2447,14 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (!mysqlEnabled || mysqlManager == null) return; List> targetChests = mysqlManager.getTargetChests(ownerUUID.toString()); - Map localTargets = new HashMap<>(); + Map> localTargets = new HashMap<>(); for (Map tc : targetChests) { + if (isRemoteChest(tc)) continue; String worldName = (String) tc.get("world"); World world = Bukkit.getWorld(worldName); if (world == null) continue; - localTargets.put((String) tc.get("item"), - new Location(world, (int) tc.get("x"), (int) tc.get("y"), (int) tc.get("z"))); + localTargets.computeIfAbsent((String) tc.get("item"), k -> new ArrayList<>()) + .add(new Location(world, (int) tc.get("x"), (int) tc.get("y"), (int) tc.get("z"))); } Location localRestChest = getRestChestLocation(ownerUUID); @@ -2134,19 +2469,20 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { private void distributeFromRemoteInputChest(UUID ownerUUID, String ownerName, Map inputChestData, - Map localTargets, + Map> localTargets, Location localRestChest) { if (!mysqlEnabled || mysqlManager == null) return; mysqlManager.setupTransferTable(); - List> pendingTransfers = mysqlManager.getPendingTransfers(ownerUUID.toString()); + // ── BungeeCord NEU: nach serverName filtern ─────────────────────────── + List> pendingTransfers = mysqlManager.getPendingTransfers(ownerUUID.toString(), serverName); for (Map transfer : pendingTransfers) { - String itemName = (String) transfer.get("item"); - int amount = (int) transfer.get("amount"); - long transferId = (long) transfer.get("id"); + String itemName = (String) transfer.get("item"); + int amount = (int) transfer.get("amount"); + long transferId = (long) transfer.get("id"); Material mat = Material.matchMaterial(itemName); if (mat == null) { @@ -2154,14 +2490,18 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { continue; } - Location targetLoc = localTargets.get(itemName); + List slots = localTargets.getOrDefault(itemName, new ArrayList<>()); + Location targetLoc = null; + for (Location l : slots) { + if (l != null && !isChestCachedFull(l)) { targetLoc = l; break; } + } if (targetLoc == null) targetLoc = localRestChest; if (targetLoc == null) continue; if (!(targetLoc.getBlock().getState() instanceof Chest)) continue; Chest targetChest = (Chest) targetLoc.getBlock().getState(); - ItemStack toAdd = new ItemStack(mat, amount); + ItemStack toAdd = new ItemStack(mat, amount); Map leftover = targetChest.getInventory().addItem(toAdd); int transferred = amount - (leftover.isEmpty() ? 0 : leftover.get(0).getAmount()); @@ -2173,34 +2513,68 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } spawnTransferParticles(null, targetLoc); if (isDebug()) { - getLogger().info("[CrossLink] " + transferred + "x " + itemName - + " für " + ownerName + " in lokale Zieltruhe übertragen."); + getLogger().info("[CrossLink→" + serverName + "] " + transferred + "x " + itemName + + " für " + ownerName + " angekommen."); } } } } + /** Legacy-Ueberladung: laedt Truhen-Locations synchron (nur fuer InventoryMoveItemEvent). */ + private void distributeItemsForOwner(UUID ownerUUID, Player ownerPlayer, Inventory sourceInventory, + String ownerNameOverride, Location sourceLocation) { + Location restLoc = getRestChestLocation(ownerUUID); + Map> targets = 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; + targets.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 { + 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"))); + } + } + } + distributeItemsForOwner(ownerUUID, ownerPlayer, sourceInventory, ownerNameOverride, sourceLocation, targets, restLoc); + } + private boolean checkSingleInputChest(UUID ownerUUID, Location location, String debugId, boolean crosslinkMode) { + return checkSingleInputChest(ownerUUID, location, debugId, crosslinkMode, null, null); + } + + private boolean checkSingleInputChest(UUID ownerUUID, Location location, String debugId, boolean crosslinkMode, + Map> preloadedTargets, Location preloadedRest) { if (isWorldBlacklisted(location.getWorld())) return true; if (!(location.getBlock().getState() instanceof Chest)) return false; - Chest chest = (Chest) location.getBlock().getState(); + Chest chest = (Chest) location.getBlock().getState(); Block inputSignBlock = null; - boolean isPublic = false; - String ownerName = "Unknown"; + boolean isPublic = false; + String ownerName = "Unknown"; List chestBlocks = getChestBlocks(chest); outerLoop: for (Block b : chestBlocks) { - 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)}) { + 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 line0 = ChatColor.stripColor(lines[0] != null ? lines[0] : ""); + String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); if (line0.equalsIgnoreCase("[asc]") && line1.equalsIgnoreCase("input")) { inputSignBlock = face; String line3Clean = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); - isPublic = line3Clean.toLowerCase().endsWith("[public]"); + isPublic = line3Clean.toLowerCase().endsWith("[public]"); ownerName = line3Clean.replace(" [Public]", "").replace(" [public]", "").trim(); break outerLoop; } @@ -2223,12 +2597,50 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (ownerPlayer == null || !ownerPlayer.isOnline()) ownerPlayer = null; } - distributeItemsForOwner(ownerUUID, ownerPlayer, chest.getInventory(), ownerName, location); + // Wenn preloadedTargets null: synchron laden (Fallback) + Map> effectiveTargets = preloadedTargets; + Location effectiveRest = preloadedRest; + if (effectiveTargets == null) { + effectiveTargets = 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; + String itemKey = (String) tc.get("item"); + effectiveTargets.computeIfAbsent(itemKey, k -> new ArrayList<>()) + .add(new Location(w, (int) tc.get("x"), (int) tc.get("y"), (int) tc.get("z"))); + } + } else { + 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"))); + } + } + } + } + if (effectiveRest == null && preloadedTargets == null) { + effectiveRest = getRestChestLocation(ownerUUID); + } + + distributeItemsForOwner(ownerUUID, ownerPlayer, chest.getInventory(), ownerName, location, effectiveTargets, effectiveRest); return true; } + /** + * Oeffentliche Variante mit vorgeladenen Truhen-Locations (kein DB-Hit im Main Thread). + * targetChestMap: Material-Name → Location (lokale Zieltruhen) + * restChestLoc: lokale Rest-Truhe oder null + */ private void distributeItemsForOwner(UUID ownerUUID, Player ownerPlayer, Inventory sourceInventory, - String ownerNameOverride, Location sourceLocation) { + String ownerNameOverride, Location sourceLocation, + Map> targetChestMap, Location restChestLoc) { boolean hasItems = false; for (ItemStack item : sourceInventory.getContents()) { @@ -2246,48 +2658,81 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (offlinePlayer.hasPlayedBefore()) ownerName = offlinePlayer.getName(); } + Location restChestLocation = restChestLoc; + boolean restChestKnownFull = (restChestLocation != null) && isChestCachedFull(restChestLocation); + for (int slot = 0; slot < sourceInventory.getSize(); slot++) { ItemStack item = sourceInventory.getItem(slot); if (item == null || item.getType() == Material.AIR) continue; - Location targetChestLocation = getTargetChestLocation(ownerUUID, item.getType()); - boolean isRestChest = false; - boolean isCrosslink = false; + // 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; if (targetChestLocation == null) { if (serverCrosslink && mysqlEnabled && mysqlManager != null) { Map raw = mysqlManager.getTargetChest(ownerUUID.toString(), item.getType().name()); - if (raw != null && Bukkit.getWorld((String) raw.get("world")) == null) { + if (raw != null && isRemoteChest(raw)) { + // ── BungeeCord NEU: expliziten Ziel-Server mitgeben ─────────── + String targetServerName = getChestServer(raw); isCrosslink = true; mysqlManager.setupTransferTable(); - mysqlManager.addTransfer(ownerUUID.toString(), item.getType().name(), item.getAmount(), (String) raw.get("world")); + mysqlManager.addTransfer(ownerUUID.toString(), item.getType().name(), item.getAmount(), + (String) raw.get("world"), targetServerName, serverName); sourceInventory.setItem(slot, null); if (isDebug()) { getLogger().info("[CrossLink] " + item.getAmount() + "x " + item.getType().name() - + " für " + ownerName + " in Transfer-DB geschrieben (Zieltruhe auf anderem Server)."); + + " → Server:'" + (targetServerName.isEmpty() ? "?" : targetServerName) + + "' Welt:'" + raw.get("world") + "' (Transfer-DB)"); } continue; } } if (!isCrosslink) { - targetChestLocation = getRestChestLocation(ownerUUID); - if (targetChestLocation != null) { + if (restChestKnownFull) continue; + + if (restChestLocation != null) { + targetChestLocation = restChestLocation; isRestChest = true; } else if (serverCrosslink && mysqlEnabled && mysqlManager != null) { Map raw = mysqlManager.getRestChest(ownerUUID.toString()); - if (raw != null && Bukkit.getWorld((String) raw.get("world")) == null) { + if (raw != null && isRemoteChest(raw)) { + // ── BungeeCord NEU: expliziten Ziel-Server mitgeben ───── + String targetServerName = getChestServer(raw); mysqlManager.setupTransferTable(); - mysqlManager.addTransfer(ownerUUID.toString(), item.getType().name(), item.getAmount(), (String) raw.get("world")); + mysqlManager.addTransfer(ownerUUID.toString(), item.getType().name(), item.getAmount(), + (String) raw.get("world"), targetServerName, serverName); sourceInventory.setItem(slot, null); if (isDebug()) { getLogger().info("[CrossLink] " + item.getAmount() + "x " + item.getType().name() - + " für " + ownerName + " in Transfer-DB geschrieben (Rest-Truhe auf anderem Server)."); + + " → Server:'" + (targetServerName.isEmpty() ? "?" : targetServerName) + + "' Welt:'" + raw.get("world") + "' (Rest-Transfer-DB)"); } continue; } } } + } else { + // Bei voller Truhe: nächsten Slot in der Liste versuchen + if (isChestCachedFull(targetChestLocation)) { + Location nextSlot = null; + for (Location loc : targetChestList) { + if (loc != null && !isChestCachedFull(loc) && !loc.equals(targetChestLocation)) { + nextSlot = loc; break; + } + } + if (nextSlot != null) { targetChestLocation = nextSlot; } + else continue; // Alle Slots voll + } } if (targetChestLocation == null) continue; @@ -2302,26 +2747,26 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { continue; } - Chest targetChest = (Chest) targetChestLocation.getBlock().getState(); + Chest targetChest = (Chest) targetChestLocation.getBlock().getState(); Inventory targetInventory = targetChest.getInventory(); boolean isValidTarget = false; - Block signBlock = null; + Block signBlock = null; List chestBlocks = getChestBlocks(targetChest); outerLoop: for (Block b : chestBlocks) { - 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)}) { + 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[] lines = sign.getLines(); + 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"); String signOwnerName = line3Clean.replace("[Public]", "").replace("[public]", "").trim(); if (line0.equalsIgnoreCase("[asc]") && typeMatches) { if (signOwnerName.equalsIgnoreCase(ownerName) || signOwnerName.equalsIgnoreCase("Unknown")) { isValidTarget = true; - signBlock = face; + signBlock = face; break outerLoop; } } @@ -2333,18 +2778,23 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { ItemStack itemToTransfer = item.clone(); Map leftover = targetInventory.addItem(itemToTransfer); - boolean isFull = isInventoryFull(targetInventory); + boolean isFull = !leftover.isEmpty(); + + if (isFull) { + markChestFull(targetChestLocation); + if (isRestChest) restChestKnownFull = true; + } if (signBlock != null) { - Sign sign = (Sign) signBlock.getState(); - 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] : ""); + Sign sign = (Sign) signBlock.getState(); + 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 line3Raw = lines[3] != null ? lines[3] : ""; if (line0.equalsIgnoreCase("[asc]") && (line1.equalsIgnoreCase("ziel") || line1.equalsIgnoreCase("rest"))) { String configType = line1.equalsIgnoreCase("rest") ? "rest" : "target"; - String colorType = isFull ? "full" : configType; + String colorType = isFull ? "full" : configType; sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); sign.setLine(1, getSignColor(colorType, "line2") + line1); sign.setLine(2, getSignColor(colorType, "line3") + line2); @@ -2353,10 +2803,32 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - if (leftover.isEmpty()) { + if (!isFull) { sourceInventory.setItem(slot, null); spawnTransferParticles(null, targetChestLocation); - } else { + } 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; + } + } + 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); + } else { + item.setAmount(leftover2.get(0).getAmount()); + sourceInventory.setItem(slot, item); + } + } + } + if (isFull && isRestChest) { if (ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { String message = getMessage("target-chest-full") .replace("%item%", item.getType().name()) @@ -2381,27 +2853,27 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { Inventory destination = event.getDestination(); if (!(destination.getHolder() instanceof Chest)) return; - final Chest destChest = (Chest) destination.getHolder(); + final Chest destChest = (Chest) destination.getHolder(); List blocksToScan = new ArrayList<>(); - InventoryHolder holder = destination.getHolder(); + InventoryHolder holder = destination.getHolder(); if (holder instanceof DoubleChest) { DoubleChest dc = (DoubleChest) holder; - if (dc.getLeftSide() instanceof Chest) blocksToScan.add(((Chest) dc.getLeftSide()).getBlock()); + if (dc.getLeftSide() instanceof Chest) blocksToScan.add(((Chest) dc.getLeftSide()).getBlock()); if (dc.getRightSide() instanceof Chest) blocksToScan.add(((Chest) dc.getRightSide()).getBlock()); } else if (holder instanceof Chest) { blocksToScan.add(destChest.getBlock()); } - UUID ownerUUID = null; + UUID ownerUUID = null; String ownerName = null; 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)}) { + 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 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(); @@ -2415,7 +2887,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (ownerUUID == null || ownerName == null || ownerName.isEmpty()) return; - final UUID finalOwnerUUID = ownerUUID; + final UUID finalOwnerUUID = ownerUUID; final String finalOwnerName = ownerName; new BukkitRunnable() { @Override diff --git a/src/main/java/com/viper/autosortchest/MySQLManager.java b/src/main/java/com/viper/autosortchest/MySQLManager.java index e8030cd..956d197 100644 --- a/src/main/java/com/viper/autosortchest/MySQLManager.java +++ b/src/main/java/com/viper/autosortchest/MySQLManager.java @@ -36,6 +36,40 @@ public class MySQLManager { } } + /** + * Migriert den PRIMARY KEY von (uuid, item) auf (uuid, item, slot) falls nötig. + * Nutzt DATABASE() um nur die aktuelle DB zu prüfen (kein Cross-DB-Problem). + */ + private void migrateTargetChestPrimaryKey(Statement st) { + try { + // Slot-Spalte zuerst sicherstellen (muss vor PK-Änderung existieren) + // tryAlterColumn wurde bereits aufgerufen, hier nochmal sicher prüfen + if (!columnExists("asc_target_chests", "slot")) { + st.execute("ALTER TABLE asc_target_chests ADD COLUMN slot INT NOT NULL DEFAULT 0;"); + } + + // Prüfen ob slot bereits im PRIMARY KEY ist + try (PreparedStatement ps = connection.prepareStatement( + "SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE " + + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'asc_target_chests' " + + "AND CONSTRAINT_NAME = 'PRIMARY' AND COLUMN_NAME = 'slot';")) { + ResultSet rs = ps.executeQuery(); + if (rs.next() && rs.getInt(1) > 0) { + rs.close(); + return; // Bereits migriert + } + rs.close(); + } + + // Alten PK droppen und neuen mit slot anlegen + st.execute("ALTER TABLE asc_target_chests DROP PRIMARY KEY, ADD PRIMARY KEY(uuid, item, slot);"); + } catch (SQLException e) { + if (!e.getMessage().toLowerCase().contains("primary")) { + e.printStackTrace(); + } + } + } + public void disconnect() { try { if (connection != null && !connection.isClosed()) connection.close(); @@ -44,70 +78,238 @@ public class MySQLManager { } } + // ═══════════════════════════════════════════════════════════════════ + // BungeeCord-Erweiterung: Sanfte Schema-Migration + // Fügt eine Spalte hinzu, falls sie noch nicht existiert. + // ═══════════════════════════════════════════════════════════════════ + /** + * Fügt eine Spalte hinzu, falls sie noch nicht existiert. + * Nutzt DatabaseMetaData statt IF NOT EXISTS (kompatibel mit allen MySQL/MariaDB-Versionen). + */ + private void tryAlterColumn(Statement st, String table, String column, String definition) { + try { + ResultSet rs = st.getConnection().getMetaData().getColumns(null, null, table, column); + if (!rs.next()) { + st.execute("ALTER TABLE " + table + " ADD COLUMN " + column + " " + definition + ";"); + } + rs.close(); + } catch (SQLException e) { + if (!e.getMessage().toLowerCase().contains("duplicate column")) { + e.printStackTrace(); + } + } + } + + /** + * Prüft ob eine Spalte in einer Tabelle existiert. + */ + private boolean columnExists(String table, String column) { + try { + ResultSet rs = connection.getMetaData().getColumns(null, null, table, column); + boolean exists = rs.next(); + rs.close(); + return exists; + } catch (SQLException e) { + return false; + } + } + + /** + * Prüft ob ein Index/Constraint auf einer Tabelle existiert. + */ + private boolean indexExists(String table, String indexName) { + try (PreparedStatement ps = connection.prepareStatement( + "SELECT COUNT(*) FROM information_schema.STATISTICS " + + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?;")) { + ps.setString(1, table); + ps.setString(2, indexName); + ResultSet rs = ps.executeQuery(); + boolean exists = rs.next() && rs.getInt(1) > 0; + rs.close(); + return exists; + } catch (SQLException e) { + return false; + } + } + + /** + * Erstellt alle Tabellen und führt automatisch alle Schema-Migrationen durch. + * Bestehende Datenbanken älterer Versionen werden vollständig und sicher migriert: + * + * Alte Version → Neue Spalten: + * asc_input_chests : +server + * asc_rest_chests : +server + * asc_target_chests : +slot, +server, PRIMARY KEY (uuid,item) → (uuid,item,slot) + * asc_transfers : +target_server, +source_server + * Neue Tabellen (falls fehlend): + * asc_servers (BungeeCord Heartbeat) + * asc_transfers (CrossLink Transfers) + */ public void setupTables() { try (Statement st = connection.createStatement()) { - st.execute("CREATE TABLE IF NOT EXISTS asc_players (uuid VARCHAR(36) PRIMARY KEY, name VARCHAR(32));"); - st.execute("CREATE TABLE IF NOT EXISTS asc_input_chests (uuid VARCHAR(36), chest_id VARCHAR(36), world VARCHAR(32), x INT, y INT, z INT, `public` BOOLEAN DEFAULT FALSE, PRIMARY KEY(uuid, chest_id));"); - st.execute("CREATE TABLE IF NOT EXISTS asc_target_chests (uuid VARCHAR(36), item VARCHAR(64), world VARCHAR(32), x INT, y INT, z INT, `public` BOOLEAN DEFAULT FALSE, PRIMARY KEY(uuid, item));"); - st.execute("CREATE TABLE IF NOT EXISTS asc_rest_chests (uuid VARCHAR(36), world VARCHAR(32), x INT, y INT, z INT, `public` BOOLEAN DEFAULT FALSE, PRIMARY KEY(uuid));"); + + // ── Basis-Tabellen anlegen (falls nicht vorhanden) ──────────────────── + st.execute("CREATE TABLE IF NOT EXISTS asc_players (" + + "uuid VARCHAR(36) PRIMARY KEY, name VARCHAR(32)" + + ");"); + + st.execute("CREATE TABLE IF NOT EXISTS asc_input_chests (" + + "uuid VARCHAR(36), chest_id VARCHAR(36), world VARCHAR(32)," + + "x INT, y INT, z INT, `public` BOOLEAN DEFAULT FALSE," + + "PRIMARY KEY(uuid, chest_id)" + + ");"); + + // asc_target_chests: neue Installs bekommen slot direkt im Schema. + // Alte Installs werden weiter unten per Migration angepasst. + st.execute("CREATE TABLE IF NOT EXISTS asc_target_chests (" + + "uuid VARCHAR(36), item VARCHAR(64), slot INT NOT NULL DEFAULT 0," + + "world VARCHAR(32), x INT, y INT, z INT, `public` BOOLEAN DEFAULT FALSE," + + "PRIMARY KEY(uuid, item, slot)" + + ");"); + + st.execute("CREATE TABLE IF NOT EXISTS asc_rest_chests (" + + "uuid VARCHAR(36), world VARCHAR(32)," + + "x INT, y INT, z INT, `public` BOOLEAN DEFAULT FALSE," + + "PRIMARY KEY(uuid)" + + ");"); + + // ── asc_transfers: immer beim Start sicherstellen ───────────────────── + // (früher lazy – führte dazu dass target_server/source_server fehlten) + st.execute("CREATE TABLE IF NOT EXISTS asc_transfers (" + + "id BIGINT AUTO_INCREMENT PRIMARY KEY," + + "uuid VARCHAR(36) NOT NULL," + + "item VARCHAR(64) NOT NULL," + + "amount INT NOT NULL," + + "target_world VARCHAR(32) NOT NULL," + + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + + ");"); + + // ── asc_servers: BungeeCord Heartbeat ──────────────────────────────── + st.execute("CREATE TABLE IF NOT EXISTS asc_servers (" + + "server_name VARCHAR(64) PRIMARY KEY," + + "last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" + + ");"); + + // ══════════════════════════════════════════════════════════════════════ + // SCHEMA-MIGRATIONEN (für bestehende Datenbanken älterer Versionen) + // Jede Migration ist idempotent – mehrfaches Ausführen schadet nicht. + // ══════════════════════════════════════════════════════════════════════ + + // v1 → v2 (BungeeCord): server-Spalten + tryAlterColumn(st, "asc_input_chests", "server", "VARCHAR(64) DEFAULT ''"); + tryAlterColumn(st, "asc_target_chests", "server", "VARCHAR(64) DEFAULT ''"); + tryAlterColumn(st, "asc_rest_chests", "server", "VARCHAR(64) DEFAULT ''"); + + // v1 → v2 (BungeeCord): Transfer-Routing-Spalten + tryAlterColumn(st, "asc_transfers", "target_server", "VARCHAR(64) DEFAULT ''"); + tryAlterColumn(st, "asc_transfers", "source_server", "VARCHAR(64) DEFAULT ''"); + + // v2 → v3 (Multi-Target): slot-Spalte + PRIMARY KEY Migration + migrateTargetChestPrimaryKey(st); + } catch (SQLException e) { e.printStackTrace(); } } /** - * Transfer-Tabelle für serverübergreifende Sortierung. + * setupTransferTable() bleibt für Rückwärtskompatibilität erhalten, + * delegiert aber nur noch an setupTables() da alles konsolidiert wurde. */ public void setupTransferTable() { - try (Statement st = connection.createStatement()) { - st.execute( - "CREATE TABLE IF NOT EXISTS asc_transfers (" + - " id BIGINT AUTO_INCREMENT PRIMARY KEY," + - " uuid VARCHAR(36) NOT NULL," + - " item VARCHAR(64) NOT NULL," + - " amount INT NOT NULL," + - " target_world VARCHAR(32) NOT NULL," + - " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + - ");" - ); - } catch (SQLException e) { - e.printStackTrace(); - } + // Transfers werden jetzt immer in setupTables() angelegt. + // Diese Methode existiert nur noch damit bestehende Aufrufe nicht brechen. } - public void addTransfer(String uuid, String item, int amount, String targetWorld) { + // ── BungeeCord NEU: Heartbeat ───────────────────────────────────────────── + /** + * Schreibt / aktualisiert den Heartbeat-Eintrag für diesen Server. + * Sollte alle 30 Sekunden asynchron aufgerufen werden. + */ + public void heartbeat(String serverName) { + if (serverName == null || serverName.isEmpty()) return; try (PreparedStatement ps = connection.prepareStatement( - "INSERT INTO asc_transfers (uuid, item, amount, target_world) VALUES (?, ?, ?, ?);")) { - ps.setString(1, uuid); - ps.setString(2, item); - ps.setInt(3, amount); - ps.setString(4, targetWorld); + "REPLACE INTO asc_servers (server_name, last_seen) VALUES (?, NOW());")) { + ps.setString(1, serverName); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } - public List> getPendingTransfers(String uuid) { - List> list = new ArrayList<>(); + // ── BungeeCord NEU: Transfer mit Server-Routing ─────────────────────────── + /** + * Legt einen Transfer-Eintrag mit explizitem Ziel-Server an. + */ + public void addTransfer(String uuid, String item, int amount, + String targetWorld, String targetServer, String sourceServer) { try (PreparedStatement ps = connection.prepareStatement( - "SELECT id, item, amount, target_world FROM asc_transfers WHERE uuid=? ORDER BY created_at ASC;")) { + "INSERT INTO asc_transfers (uuid, item, amount, target_world, target_server, source_server) " + + "VALUES (?, ?, ?, ?, ?, ?);")) { ps.setString(1, uuid); + ps.setString(2, item); + ps.setInt(3, amount); + ps.setString(4, targetWorld); + ps.setString(5, targetServer != null ? targetServer : ""); + ps.setString(6, sourceServer != null ? sourceServer : ""); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * Legacy-Überladung (ohne Server-Routing) – Rückwärtskompatibilität. + */ + public void addTransfer(String uuid, String item, int amount, String targetWorld) { + addTransfer(uuid, item, amount, targetWorld, "", ""); + } + + /** + * Gibt ausstehende Transfers zurück, gefiltert nach serverName. + * serverName leer/null → alle Transfers (Legacy). + * serverName gesetzt → nur Transfers mit target_server == serverName ODER target_server == '' (Legacy). + */ + public List> getPendingTransfers(String uuid, String serverName) { + List> list = new ArrayList<>(); + try { + PreparedStatement ps; + if (serverName == null || serverName.isEmpty()) { + ps = connection.prepareStatement( + "SELECT id, item, amount, target_world, target_server, source_server " + + "FROM asc_transfers WHERE uuid=? ORDER BY created_at ASC;"); + ps.setString(1, uuid); + } else { + ps = connection.prepareStatement( + "SELECT id, item, amount, target_world, target_server, source_server " + + "FROM asc_transfers WHERE uuid=? AND (target_server=? OR target_server='') " + + "ORDER BY created_at ASC;"); + ps.setString(1, uuid); + ps.setString(2, serverName); + } ResultSet rs = ps.executeQuery(); while (rs.next()) { Map map = new HashMap<>(); - map.put("id", rs.getLong("id")); - map.put("item", rs.getString("item")); - map.put("amount", rs.getInt("amount")); - map.put("target_world", rs.getString("target_world")); + map.put("id", rs.getLong("id")); + map.put("item", rs.getString("item")); + map.put("amount", rs.getInt("amount")); + map.put("target_world", rs.getString("target_world")); + map.put("target_server", rs.getString("target_server")); + map.put("source_server", rs.getString("source_server")); list.add(map); } + ps.close(); } catch (SQLException e) { e.printStackTrace(); } return list; } + /** Legacy-Überladung ohne serverName. */ + public List> getPendingTransfers(String uuid) { + return getPendingTransfers(uuid, ""); + } + public void deleteTransfer(long id) { try (PreparedStatement ps = connection.prepareStatement( "DELETE FROM asc_transfers WHERE id=?;")) { @@ -142,12 +344,6 @@ public class MySQLManager { } } - /** - * Gibt alle Spieler aus der asc_players-Tabelle zurück. - * Wird für den Export (MySQL → YAML) benötigt. - * - * @return Liste mit uuid und name je Spieler - */ public List> getAllPlayers() { List> list = new ArrayList<>(); try (Statement st = connection.createStatement(); @@ -171,8 +367,15 @@ public class MySQLManager { } public void addInputChest(String uuid, String chestId, String world, int x, int y, int z, boolean isPublic) { + addInputChest(uuid, chestId, world, x, y, z, isPublic, ""); + } + + /** BungeeCord-Überladung mit serverName. */ + public void addInputChest(String uuid, String chestId, String world, + int x, int y, int z, boolean isPublic, String serverName) { try (PreparedStatement ps = connection.prepareStatement( - "REPLACE INTO asc_input_chests (uuid, chest_id, world, x, y, z, `public`) VALUES (?, ?, ?, ?, ?, ?, ?);")) { + "REPLACE INTO asc_input_chests (uuid, chest_id, world, x, y, z, `public`, server) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?);")) { ps.setString(1, uuid); ps.setString(2, chestId); ps.setString(3, world); @@ -180,6 +383,7 @@ public class MySQLManager { ps.setInt(5, y); ps.setInt(6, z); ps.setBoolean(7, isPublic); + ps.setString(8, serverName != null ? serverName : ""); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); @@ -206,11 +410,13 @@ public class MySQLManager { while (rs.next()) { Map map = new HashMap<>(); map.put("chest_id", rs.getString("chest_id")); - 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("public", rs.getBoolean("public")); + 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("public", rs.getBoolean("public")); + try { map.put("server", rs.getString("server")); } + catch (SQLException ignored) { map.put("server", ""); } list.add(map); } } catch (SQLException e) { @@ -226,40 +432,118 @@ public class MySQLManager { } public void setTargetChest(String uuid, String item, String world, int x, int y, int z, boolean isPublic) { + setTargetChest(uuid, item, world, x, y, z, isPublic, ""); + } + + /** BungeeCord-Überladung mit serverName. Slot=0 (Standard, erster Platz). */ + public void setTargetChest(String uuid, String item, String world, + int x, int y, int z, boolean isPublic, String serverName) { + setTargetChest(uuid, item, 0, world, x, y, z, isPublic, serverName); + } + + /** 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) { try (PreparedStatement ps = connection.prepareStatement( - "REPLACE INTO asc_target_chests (uuid, item, world, x, y, z, `public`) VALUES (?, ?, ?, ?, ?, ?, ?);")) { + "REPLACE INTO asc_target_chests (uuid, item, slot, world, x, y, z, `public`, server) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);")) { ps.setString(1, uuid); ps.setString(2, item); - ps.setString(3, world); - ps.setInt(4, x); - ps.setInt(5, y); - ps.setInt(6, z); - ps.setBoolean(7, isPublic); + ps.setInt(3, slot); + ps.setString(4, world); + ps.setInt(5, x); + ps.setInt(6, y); + ps.setInt(7, z); + ps.setBoolean(8, isPublic); + ps.setString(9, serverName != null ? serverName : ""); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } - public Map getTargetChest(String uuid, String item) { + /** Löscht eine spezifische Slot-Zieltruhe und verschiebt höhere Slots nach unten. */ + public void removeTargetChestSlot(String uuid, String item, int slot) { try (PreparedStatement ps = connection.prepareStatement( - "SELECT * FROM asc_target_chests WHERE uuid=? AND item=?;")) { + "DELETE FROM asc_target_chests WHERE uuid=? AND item=? AND slot=?;")) { + ps.setString(1, uuid); + ps.setString(2, item); + ps.setInt(3, slot); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + // Slots nach dem gelöschten nach unten schieben + try (PreparedStatement ps = connection.prepareStatement( + "UPDATE asc_target_chests SET slot=slot-1 WHERE uuid=? AND item=? AND slot>?;")) { + ps.setString(1, uuid); + ps.setString(2, item); + ps.setInt(3, slot); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** Gibt den nächsten freien Slot für ein Item zurück. */ + public int getNextTargetSlot(String uuid, String item) { + try (PreparedStatement ps = connection.prepareStatement( + "SELECT COALESCE(MAX(slot)+1, 0) AS next_slot FROM asc_target_chests WHERE uuid=? AND item=?;")) { ps.setString(1, uuid); ps.setString(2, item); ResultSet rs = ps.executeQuery(); - if (rs.next()) { + if (rs.next()) return rs.getInt("next_slot"); + } catch (SQLException e) { + e.printStackTrace(); + } + return 0; + } + + /** Zählt wie viele Zieltruhen für ein bestimmtes Item registriert sind. */ + public int countTargetChestsForItem(String uuid, String item) { + try (PreparedStatement ps = connection.prepareStatement( + "SELECT COUNT(*) FROM asc_target_chests WHERE uuid=? AND item=?;")) { + ps.setString(1, uuid); + ps.setString(2, item); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return rs.getInt(1); + } catch (SQLException e) { + e.printStackTrace(); + } + return 0; + } + + /** Gibt die erste (slot=0) Zieltruhe für ein Item zurück (Legacy-Kompatibilität). */ + public Map getTargetChest(String uuid, String item) { + List> all = getTargetChestsForItem(uuid, item); + return all.isEmpty() ? null : all.get(0); + } + + /** Gibt ALLE Zieltruhen für ein bestimmtes Item zurück, sortiert nach slot. */ + public List> getTargetChestsForItem(String uuid, String item) { + List> list = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT * FROM asc_target_chests WHERE uuid=? AND item=? ORDER BY slot ASC;")) { + ps.setString(1, uuid); + ps.setString(2, item); + ResultSet rs = ps.executeQuery(); + while (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("item", rs.getString("item")); + map.put("slot", rs.getInt("slot")); + 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("public", rs.getBoolean("public")); - return map; + try { map.put("server", rs.getString("server")); } + catch (SQLException ignored) { map.put("server", ""); } + list.add(map); } } catch (SQLException e) { e.printStackTrace(); } - return null; + return list; } public void removeTargetChest(String uuid, String item) { @@ -273,20 +557,24 @@ public class MySQLManager { } } + /** Gibt alle Zieltruhen eines Spielers zurück, sortiert nach item + slot. */ public List> getTargetChests(String uuid) { List> list = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement( - "SELECT * FROM asc_target_chests WHERE uuid=?;")) { + "SELECT * FROM asc_target_chests WHERE uuid=? ORDER BY item ASC, slot ASC;")) { ps.setString(1, uuid); ResultSet rs = ps.executeQuery(); while (rs.next()) { Map map = new HashMap<>(); - map.put("item", rs.getString("item")); - 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("item", rs.getString("item")); + map.put("slot", rs.getInt("slot")); + 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("public", rs.getBoolean("public")); + try { map.put("server", rs.getString("server")); } + catch (SQLException ignored) { map.put("server", ""); } list.add(map); } } catch (SQLException e) { @@ -302,14 +590,22 @@ public class MySQLManager { } public void setRestChest(String uuid, String world, int x, int y, int z, boolean isPublic) { + setRestChest(uuid, world, x, y, z, isPublic, ""); + } + + /** BungeeCord-Überladung mit serverName. */ + public void setRestChest(String uuid, String world, int x, int y, int z, + boolean isPublic, String serverName) { try (PreparedStatement ps = connection.prepareStatement( - "REPLACE INTO asc_rest_chests (uuid, world, x, y, z, `public`) VALUES (?, ?, ?, ?, ?, ?);")) { + "REPLACE INTO asc_rest_chests (uuid, world, x, y, z, `public`, server) " + + "VALUES (?, ?, ?, ?, ?, ?, ?);")) { ps.setString(1, uuid); ps.setString(2, world); ps.setInt(3, x); ps.setInt(4, y); ps.setInt(5, z); ps.setBoolean(6, isPublic); + ps.setString(7, serverName != null ? serverName : ""); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); @@ -323,11 +619,13 @@ public class MySQLManager { 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("world", rs.getString("world")); + map.put("x", rs.getInt("x")); + map.put("y", rs.getInt("y")); + map.put("z", rs.getInt("z")); map.put("public", rs.getBoolean("public")); + try { map.put("server", rs.getString("server")); } + catch (SQLException ignored) { map.put("server", ""); } return map; } } catch (SQLException e) { @@ -346,7 +644,7 @@ public class MySQLManager { } } - // --- Hilfsmethoden für serverCrosslink --- + // --- Hilfsmethoden für serverCrosslink (unverändert) --- public List> getAllInputChests() { List> list = new ArrayList<>(); @@ -355,12 +653,14 @@ public class MySQLManager { while (rs.next()) { Map map = new HashMap<>(); map.put("chest_id", rs.getString("chest_id")); - 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("public", rs.getBoolean("public")); + 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("public", rs.getBoolean("public")); + try { map.put("server", rs.getString("server")); } + catch (SQLException ignored) { map.put("server", ""); } list.add(map); } } catch (SQLException e) { @@ -375,13 +675,15 @@ public class MySQLManager { ResultSet rs = ps.executeQuery(); while (rs.next()) { Map map = new HashMap<>(); - map.put("item", rs.getString("item")); - 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("item", rs.getString("item")); + 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("public", rs.getBoolean("public")); + try { map.put("server", rs.getString("server")); } + catch (SQLException ignored) { map.put("server", ""); } list.add(map); } } catch (SQLException e) { @@ -396,12 +698,14 @@ public class MySQLManager { ResultSet rs = ps.executeQuery(); if (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("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("public", rs.getBoolean("public")); + try { map.put("server", rs.getString("server")); } + catch (SQLException ignored) { map.put("server", ""); } return map; } } catch (SQLException e) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 80f7fdc..0af9fef 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -32,6 +32,28 @@ mysql: # Soll serverübergreifendes Sortieren (mit MySQL) erlaubt sein? server_crosslink: true +# --------------------------------------------------- +# BUNGEE CORD / MULTI-SERVER SETUP +# --------------------------------------------------- +# Eindeutiger Name dieses Servers im BungeeCord-Netzwerk. +# WICHTIG: Jeder Server braucht einen anderen Namen! +# +# Beispiele: +# server_name: "lobby" +# server_name: "survival" +# server_name: "creative" +# +# Leer lassen = Legacy-Modus (welt-basierte Erkennung, kein BungeeCord). +# Fuer BungeeCord MUSS mysql.enabled: true gesetzt sein! +# +# Setup-Schritte: +# 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. + +server_name: "" + # --------------------------------------------------- # SPRACHE (Language) # --------------------------------------------------- @@ -84,7 +106,6 @@ effects: # 30+ = SPARSAM (>1,5 Sekunden) # Für sehr große Server mit schwacher Hardware. # -sort_interval_ticks: 5 sort_interval_ticks: 5 @@ -94,11 +115,35 @@ sort_interval_ticks: 5 # Maximale Anzahl an Sortierkisten pro Spielergruppe chest_limits: - default: 50 - vip: 100 - # Beispiel für weitere Gruppen: - # supporter: 150 - # admin: 200 + # 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. + + default: + input: 1 # Eingangstruhen (Input) + rest: 1 # Rest-Truhen (Fallback) + target: 50 # Zieltruhen gesamt + target_per_item: 1 # Wie viele Zieltruhen pro Item-Typ erlaubt sind + + vip: + input: 2 + rest: 2 + target: 100 + target_per_item: 3 + + # Weitere Gruppen: + # supporter: + # input: 3 + # rest: 2 + # target: 150 + # target_per_item: 5 + # admin: + # input: 5 + # rest: 3 + # target: 200 + # target_per_item: 10 # --------------------------------------------------- # SCHILDFARBEN (Farbcodes wie im Chat) @@ -157,5 +202,4 @@ messages: target-chest-full: "&cZieltruhe für %item% ist voll! Koordinaten: (%x%, %y%, %z%)" mode-changed: "&aModus gewechselt: &e%mode%" mode-public: "&aÖffentlich" - mode-private: "&cPrivat" - + mode-private: "&cPrivat" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 30a5076..23c759e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: AutoSortChest -version: 2.0 +version: 2.2 main: com.viper.autosortchest.Main api-version: 1.21 authors: [M_Viper]