From 62e5c3d7f15238745ad11e0013956c700b232664 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Wed, 25 Feb 2026 19:02:12 +0100 Subject: [PATCH] Update from Git Manager GUI --- .../java/com/viper/autosortchest/Main.java | 617 +++++++++++++----- .../com/viper/autosortchest/MySQLManager.java | 195 +++++- src/main/resources/config.yml | 2 +- src/main/resources/plugin.yml | 10 +- 4 files changed, 628 insertions(+), 196 deletions(-) diff --git a/src/main/java/com/viper/autosortchest/Main.java b/src/main/java/com/viper/autosortchest/Main.java index b529fd5..2ec8ee9 100644 --- a/src/main/java/com/viper/autosortchest/Main.java +++ b/src/main/java/com/viper/autosortchest/Main.java @@ -222,7 +222,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { private final Map fullChestLocationCache = new HashMap<>(); private static final long FULL_CHEST_CACHE_DURATION = 10_000L; // 10 Sekunden - private static final String CONFIG_VERSION = "2.1"; // BungeeCord NEU: 2.0 → 2.1 + private static final String CONFIG_VERSION = "2.2"; // Multi-Rest: 2.1 → 2.2 private boolean updateAvailable = false; private String latestVersion = ""; @@ -343,14 +343,29 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { mysqlManager.setTargetChest(uuidString, item, world, x, y, z, isPublic); } } - String restPath = "players." + uuidString + ".rest-chest"; - if (playerData.contains(restPath)) { - String world = playerData.getString(restPath + ".world"); - int x = playerData.getInt(restPath + ".x"); - int y = playerData.getInt(restPath + ".y"); - int z = playerData.getInt(restPath + ".z"); - boolean isPublic = playerData.getBoolean(restPath + ".public", false); - mysqlManager.setRestChest(uuidString, world, x, y, z, isPublic); + // BUG FIX: Mehrere Rest-Truhen migrieren (neues Format + Legacy) + String restBasePath = "players." + uuidString + ".rest-chests"; + if (playerData.contains(restBasePath)) { + for (String slotKey : playerData.getConfigurationSection(restBasePath).getKeys(false)) { + String rPath = restBasePath + "." + slotKey; + String world = playerData.getString(rPath + ".world"); + int x = playerData.getInt(rPath + ".x"); + int y = playerData.getInt(rPath + ".y"); + int z = playerData.getInt(rPath + ".z"); + boolean isPublic = playerData.getBoolean(rPath + ".public", false); + mysqlManager.setRestChest(uuidString, world, x, y, z, isPublic); + } + } else { + // Legacy: altes single rest-chest Format + String restPath = "players." + uuidString + ".rest-chest"; + if (playerData.contains(restPath)) { + String world = playerData.getString(restPath + ".world"); + int x = playerData.getInt(restPath + ".x"); + int y = playerData.getInt(restPath + ".y"); + int z = playerData.getInt(restPath + ".z"); + boolean isPublic = playerData.getBoolean(restPath + ".public", false); + mysqlManager.setRestChest(uuidString, world, x, y, z, isPublic); + } } } getLogger().info("Migration der YAML-Daten nach MySQL abgeschlossen."); @@ -405,9 +420,11 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { exportData.set(path + ".public", chest.get("public")); } - Map restChest = mysqlManager.getRestChest(uuidString); - if (restChest != null) { - String path = "players." + uuidString + ".rest-chest"; + // BUG FIX: Alle Rest-Truhen exportieren + List> restChests = mysqlManager.getRestChests(uuidString); + for (Map restChest : restChests) { + int restSlot = (int) restChest.get("slot"); + String path = "players." + uuidString + ".rest-chests." + restSlot; exportData.set(path + ".world", restChest.get("world")); exportData.set(path + ".x", restChest.get("x")); exportData.set(path + ".y", restChest.get("y")); @@ -689,10 +706,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { 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 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.target-chest-full")) { + config.set("messages.target-chest-full", defaultConfig.getString("messages.target-chest-full", "&cZieltruhe für %item% ist voll! Koordinaten: (%x%, %y%, %z%)")); } if (!config.contains("messages.mode-changed")) config.set("messages.mode-changed", defaultConfig.getString("messages.mode-changed", "&aModus gewechselt: &e%mode%")); @@ -825,9 +840,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - String restPath = basePath + ".rest-chest"; - if (playerData.contains(restPath)) { - Location chestLocation = getLocationFromPath(restPath); + // BUG FIX: Alle Rest-Truhen iterieren (neues Multi-Format + Legacy) + List restLocations = getRestChestLocations(playerUUID); + for (Location chestLocation : restLocations) { if (chestLocation == null) continue; Block chestBlock = chestLocation.getBlock(); if (!(chestBlock.getState() instanceof Chest)) continue; @@ -928,13 +943,27 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } } - String restPath = "players." + uuid + ".rest-chest"; - if (playerData.contains(restPath + ".world")) { - if (world.equals(playerData.getString(restPath + ".world")) - && x == playerData.getInt(restPath + ".x") - && y == playerData.getInt(restPath + ".y") - && z == playerData.getInt(restPath + ".z")) { - if (playerData.getBoolean(restPath + ".public", false)) return true; + // BUG FIX: Neues Multi-Format für Rest-Truhen + String restBasePath = "players." + uuid + ".rest-chests"; + if (playerData.contains(restBasePath)) { + for (String slotKey : playerData.getConfigurationSection(restBasePath).getKeys(false)) { + String rPath = restBasePath + "." + slotKey; + if (world.equals(playerData.getString(rPath + ".world")) + && x == playerData.getInt(rPath + ".x") + && y == playerData.getInt(rPath + ".y") + && z == playerData.getInt(rPath + ".z")) { + if (playerData.getBoolean(rPath + ".public", false)) return true; + } + } + } + // Legacy + String legacyRestPath = "players." + uuid + ".rest-chest"; + if (playerData.contains(legacyRestPath + ".world")) { + if (world.equals(playerData.getString(legacyRestPath + ".world")) + && x == playerData.getInt(legacyRestPath + ".x") + && y == playerData.getInt(legacyRestPath + ".y") + && z == playerData.getInt(legacyRestPath + ".z")) { + if (playerData.getBoolean(legacyRestPath + ".public", false)) return true; } } } @@ -987,7 +1016,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String chestId = null; List> chests = mysqlManager.getInputChests(playerUUID.toString()); for (Map chest : chests) { - if (chest.get("world").equals(location.getWorld().getName()) + String chestWorld = (String) chest.get("world"); + if (chestWorld == null) continue; // NPE-Schutz: korrupter DB-Eintrag + if (chestWorld.equals(location.getWorld().getName()) && ((int) chest.get("x")) == location.getBlockX() && ((int) chest.get("y")) == location.getBlockY() && ((int) chest.get("z")) == location.getBlockZ()) { @@ -1075,13 +1106,34 @@ 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 ─────────────────────────── + // ── BungeeCord + Multi-Rest: serverName und auto-slot ───────────────── mysqlManager.setRestChest(playerUUID.toString(), location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic, serverName); mysqlManager.savePlayer(playerUUID.toString(), Bukkit.getOfflinePlayer(playerUUID).getName()); } else { - String path = "players." + playerUUID + ".rest-chest"; + // YAML: mehrere Rest-Truhen unter rest-chests. + String basePath = "players." + playerUUID + ".rest-chests"; + // Prüfen ob diese Location schon registriert ist (Update) + if (playerData.contains(basePath)) { + for (String slotKey : playerData.getConfigurationSection(basePath).getKeys(false)) { + String path = basePath + "." + slotKey; + if (location.getWorld().getName().equals(playerData.getString(path + ".world")) + && location.getBlockX() == playerData.getInt(path + ".x") + && location.getBlockY() == playerData.getInt(path + ".y") + && location.getBlockZ() == playerData.getInt(path + ".z")) { + playerData.set(path + ".public", isPublic); + savePlayerData(); + return; + } + } + } + // Neuen Slot anlegen + int nextSlot = 0; + if (playerData.contains(basePath)) { + nextSlot = playerData.getConfigurationSection(basePath).getKeys(false).size(); + } + String path = basePath + "." + nextSlot; playerData.set(path + ".world", location.getWorld().getName()); playerData.set(path + ".x", location.getBlockX()); playerData.set(path + ".y", location.getBlockY()); @@ -1091,23 +1143,48 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - private Location getRestChestLocation(UUID playerUUID) { + /** Gibt alle Rest-Truhen-Locations zurück (für mehrere Rest-Truhen). */ + private List getRestChestLocations(UUID playerUUID) { + List result = new ArrayList<>(); 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")); + List> maps = mysqlManager.getRestChests(playerUUID.toString()); + for (Map map : maps) { + if (isRemoteChest(map)) continue; + World w = Bukkit.getWorld((String) map.get("world")); + if (w == null) continue; + result.add(new Location(w, (int) map.get("x"), (int) map.get("y"), (int) map.get("z"))); + } } else { - String path = "players." + playerUUID + ".rest-chest"; - if (!playerData.contains(path)) return null; - String worldName = playerData.getString(path + ".world"); - World world = getServer().getWorld(worldName); - if (world == null) return null; - return new Location(world, playerData.getInt(path + ".x"), playerData.getInt(path + ".y"), playerData.getInt(path + ".z")); + // Neues Multi-Format + String basePath = "players." + playerUUID + ".rest-chests"; + if (playerData.contains(basePath)) { + for (String slotKey : playerData.getConfigurationSection(basePath).getKeys(false)) { + String path = basePath + "." + slotKey; + String worldName = playerData.getString(path + ".world"); + World world = getServer().getWorld(worldName); + if (world == null) continue; + result.add(new Location(world, playerData.getInt(path + ".x"), + playerData.getInt(path + ".y"), playerData.getInt(path + ".z"))); + } + } + // Legacy-Fallback: altes single rest-chest Format + String legacyPath = "players." + playerUUID + ".rest-chest"; + if (result.isEmpty() && playerData.contains(legacyPath)) { + String worldName = playerData.getString(legacyPath + ".world"); + World world = getServer().getWorld(worldName); + if (world != null) { + result.add(new Location(world, playerData.getInt(legacyPath + ".x"), + playerData.getInt(legacyPath + ".y"), playerData.getInt(legacyPath + ".z"))); + } + } } + return result; + } + + /** Legacy: Gibt die erste Rest-Truhe zurück (oder null). */ + private Location getRestChestLocation(UUID playerUUID) { + List locs = getRestChestLocations(playerUUID); + return locs.isEmpty() ? null : locs.get(0); } private Location getTargetChestLocation(UUID playerUUID, Material itemType) { @@ -1171,15 +1248,25 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!command.getName().equalsIgnoreCase("asc")) return false; - if (!(sender instanceof Player)) { - sender.sendMessage(ChatColor.RED + "Dieser Befehl ist nur für Spieler!"); - return true; - } - Player player = (Player) sender; - String lang = config.getString("language", "de"); + + String lang = config != null ? config.getString("language", "de") : "de"; if (lang == null) lang = "de"; + // reload, import und export sind auch von der Konsole erlaubt + boolean isPlayer = sender instanceof Player; + if (!isPlayer) { + if (args.length == 0 || (!args[0].equalsIgnoreCase("reload") + && !args[0].equalsIgnoreCase("import") + && !args[0].equalsIgnoreCase("export"))) { + sender.sendMessage(ChatColor.RED + "Dieser Befehl ist nur für Spieler! (Konsole: reload, import, export)"); + return true; + } + } + + Player player = isPlayer ? (Player) sender : null; + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + if (player == null) { sender.sendMessage(ChatColor.RED + "Verwendung: /asc [reload|import|export]"); return true; } String helpMessage = lang.equalsIgnoreCase("en") ? HELP_EN : HELP_DE; helpMessage = ChatColor.translateAlternateColorCodes('&', helpMessage); player.sendMessage(helpMessage.split("\n")); @@ -1194,13 +1281,13 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { .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")); + sender.sendMessage(infoMessage.split("\n")); return true; } if (args[0].equalsIgnoreCase("reload")) { - if (!player.hasPermission("autosortchest.reload")) { - player.sendMessage(getMessage("no-permission")); + if (!sender.hasPermission("autosortchest.reload")) { + sender.sendMessage(getMessage("no-permission")); return true; } @@ -1240,8 +1327,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { migrateInputChests(); startTasks(); - player.sendMessage(getMessage("reload-success")); - getLogger().info("Plugin erfolgreich neu geladen von " + player.getName() + sender.sendMessage(getMessage("reload-success")); + getLogger().info("Plugin erfolgreich neu geladen von " + sender.getName() + " (sort_interval_ticks=" + sortIntervalTicks + ", server_name=\"" + serverName + "\")"); return true; } @@ -1250,22 +1337,22 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { // /asc import – YAML → MySQL // ------------------------------------------------------- if (args[0].equalsIgnoreCase("import")) { - if (!player.hasPermission("autosortchest.import")) { - player.sendMessage(getMessage("no-permission")); + if (!sender.hasPermission("autosortchest.import")) { + sender.sendMessage(getMessage("no-permission")); return true; } if (!mysqlEnabled || mysqlManager == null) { - player.sendMessage(ChatColor.RED + "MySQL ist nicht aktiviert! Aktiviere MySQL in der config.yml zuerst."); + sender.sendMessage(ChatColor.RED + "MySQL ist nicht aktiviert! Aktiviere MySQL in der config.yml zuerst."); return true; } if (playerData == null || playerData.getConfigurationSection("players") == null || playerData.getConfigurationSection("players").getKeys(false).isEmpty()) { - player.sendMessage(ChatColor.RED + "Die players.yml ist leer oder enthält keine Spielerdaten!"); + sender.sendMessage(ChatColor.RED + "Die players.yml ist leer oder enthält keine Spielerdaten!"); return true; } - player.sendMessage(ChatColor.YELLOW + "Importiere Daten aus players.yml nach MySQL..."); - player.sendMessage(ChatColor.GRAY + "Bestehende MySQL-Daten werden nicht überschrieben (REPLACE INTO)."); + sender.sendMessage(ChatColor.YELLOW + "Importiere Daten aus players.yml nach MySQL..."); + sender.sendMessage(ChatColor.GRAY + "Bestehende MySQL-Daten werden nicht überschrieben (REPLACE INTO)."); new BukkitRunnable() { @Override @@ -1311,26 +1398,41 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - String restPath = "players." + uuidString + ".rest-chest"; - if (playerData.contains(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"); - boolean isPublic = playerData.getBoolean(restPath + ".public", false); - mysqlManager.setRestChest(uuidString, world, x, y, z, isPublic); - restCount++; + // BUG FIX: Mehrere Rest-Truhen importieren (neues Format + Legacy) + String restBasePath2 = "players." + uuidString + ".rest-chests"; + if (playerData.contains(restBasePath2)) { + for (String slotKey : playerData.getConfigurationSection(restBasePath2).getKeys(false)) { + String rPath = restBasePath2 + "." + slotKey; + String world = playerData.getString(rPath + ".world"); + int x = playerData.getInt(rPath + ".x"); + int y = playerData.getInt(rPath + ".y"); + int z = playerData.getInt(rPath + ".z"); + boolean isPublic = playerData.getBoolean(rPath + ".public", false); + mysqlManager.setRestChest(uuidString, world, x, y, z, isPublic); + restCount++; + } + } else { + String restPath = "players." + uuidString + ".rest-chest"; + if (playerData.contains(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"); + boolean isPublic = playerData.getBoolean(restPath + ".public", false); + mysqlManager.setRestChest(uuidString, world, x, y, z, isPublic); + restCount++; + } } } 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 + " Eingangstruhen: " + ChatColor.WHITE + fi); - player.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft); - player.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr); - getLogger().info("Import durch " + player.getName() + " abgeschlossen: " + sender.sendMessage(ChatColor.GREEN + "Import erfolgreich abgeschlossen!"); + sender.sendMessage(ChatColor.GRAY + " Spieler: " + ChatColor.WHITE + fp); + sender.sendMessage(ChatColor.GRAY + " Eingangstruhen: " + ChatColor.WHITE + fi); + sender.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft); + sender.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr); + getLogger().info("Import durch " + sender.getName() + " abgeschlossen: " + fp + " Spieler, " + fi + " Input, " + ft + " Target, " + fr + " Rest."); }); } @@ -1342,17 +1444,17 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { // /asc export – MySQL → YAML // ------------------------------------------------------- if (args[0].equalsIgnoreCase("export")) { - if (!player.hasPermission("autosortchest.export")) { - player.sendMessage(getMessage("no-permission")); + if (!sender.hasPermission("autosortchest.export")) { + sender.sendMessage(getMessage("no-permission")); return true; } if (!mysqlEnabled || mysqlManager == null) { - player.sendMessage(ChatColor.RED + "MySQL ist nicht aktiviert! Der Export benötigt eine aktive MySQL-Verbindung."); + sender.sendMessage(ChatColor.RED + "MySQL ist nicht aktiviert! Der Export benötigt eine aktive MySQL-Verbindung."); return true; } - player.sendMessage(ChatColor.YELLOW + "Exportiere Daten aus MySQL nach players.yml..."); - player.sendMessage(ChatColor.GRAY + "Ein Backup der aktuellen players.yml wird erstellt."); + sender.sendMessage(ChatColor.YELLOW + "Exportiere Daten aus MySQL nach players.yml..."); + sender.sendMessage(ChatColor.GRAY + "Ein Backup der aktuellen players.yml wird erstellt."); new BukkitRunnable() { @Override @@ -1370,7 +1472,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { java.nio.file.Files.copy(playerDataFile.toPath(), backupFile.toPath()); } catch (IOException e) { Bukkit.getScheduler().runTask(Main.this, () -> - player.sendMessage(ChatColor.RED + "Backup fehlgeschlagen: " + e.getMessage())); + sender.sendMessage(ChatColor.RED + "Backup fehlgeschlagen: " + e.getMessage())); return; } } @@ -1410,9 +1512,11 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { targetCount++; } - Map restChest = mysqlManager.getRestChest(uuidString); - if (restChest != null) { - String path = "players." + uuidString + ".rest-chest"; + // BUG FIX: Alle Rest-Truhen exportieren + List> restChests = mysqlManager.getRestChests(uuidString); + for (Map restChest : restChests) { + int restSlot = (int) restChest.get("slot"); + String path = "players." + uuidString + ".rest-chests." + restSlot; exportData.set(path + ".world", restChest.get("world")); exportData.set(path + ".x", restChest.get("x")); exportData.set(path + ".y", restChest.get("y")); @@ -1430,24 +1534,24 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { Bukkit.getScheduler().runTask(Main.this, () -> { playerData = finalExport; - player.sendMessage(ChatColor.GREEN + "Export erfolgreich abgeschlossen!"); + sender.sendMessage(ChatColor.GREEN + "Export erfolgreich abgeschlossen!"); if (finalBackupName != null) { - player.sendMessage(ChatColor.GRAY + " Backup: " + ChatColor.WHITE + finalBackupName); + sender.sendMessage(ChatColor.GRAY + " Backup: " + ChatColor.WHITE + finalBackupName); } else { - player.sendMessage(ChatColor.GRAY + " Backup: " + ChatColor.DARK_GRAY + "Übersprungen (players.yml war leer)"); + sender.sendMessage(ChatColor.GRAY + " Backup: " + ChatColor.DARK_GRAY + "Übersprungen (players.yml war leer)"); } - 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); - getLogger().info("Export durch " + player.getName() + " abgeschlossen: " + sender.sendMessage(ChatColor.GRAY + " Spieler: " + ChatColor.WHITE + fp); + sender.sendMessage(ChatColor.GRAY + " Eingangstruhen: " + ChatColor.WHITE + fi); + sender.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft); + sender.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr); + getLogger().info("Export durch " + sender.getName() + " abgeschlossen: " + fp + " Spieler, " + fi + " Input, " + ft + " Target, " + fr + " Rest." + (finalBackupName != null ? " Backup: " + finalBackupName : " Kein Backup.")); }); } catch (Exception e) { Bukkit.getScheduler().runTask(Main.this, () -> - player.sendMessage(ChatColor.RED + "Export fehlgeschlagen: " + e.getMessage())); + sender.sendMessage(ChatColor.RED + "Export fehlgeschlagen: " + e.getMessage())); getLogger().warning("Export fehlgeschlagen: " + e.getMessage()); } } @@ -1455,6 +1559,11 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return true; } + // Unbekannter Befehl → Hilfe (nur für Spieler) + if (player == null) { + sender.sendMessage(ChatColor.RED + "Verwendung: /asc [reload|import|export]"); + return true; + } String helpMessage = lang.equalsIgnoreCase("en") ? HELP_EN : HELP_DE; helpMessage = ChatColor.translateAlternateColorCodes('&', helpMessage); player.sendMessage(helpMessage.split("\n")); @@ -1486,14 +1595,40 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { // ── Limit-Pruefung: Rest-Truhe ───────────────────────────────────── if (chestLimitsEnabled) { int maxRest = getChestLimitForPlayer(player, "rest"); - boolean hasRestAlready = false; + int currentRest = 0; + final Block finalChestBlockRest = chestBlock; + boolean alreadyRest = false; if (mysqlEnabled && mysqlManager != null) { - hasRestAlready = mysqlManager.getRestChest(playerUUID.toString()) != null; + currentRest = mysqlManager.countRestChests(playerUUID.toString()); + // Prüfen ob diese Truhe bereits als Rest-Truhe registriert ist (Update erlaubt) + alreadyRest = mysqlManager.getRestSlotForLocation( + playerUUID.toString(), + finalChestBlockRest.getWorld().getName(), + finalChestBlockRest.getX(), + finalChestBlockRest.getY(), + finalChestBlockRest.getZ()) >= 0; } else { - hasRestAlready = playerData.contains("players." + playerUUID + ".rest-chest"); + String basePath = "players." + playerUUID + ".rest-chests"; + if (playerData.contains(basePath)) { + for (String slotKey : playerData.getConfigurationSection(basePath).getKeys(false)) { + String rPath = basePath + "." + slotKey; + if (finalChestBlockRest.getWorld().getName().equals(playerData.getString(rPath + ".world")) + && finalChestBlockRest.getX() == playerData.getInt(rPath + ".x") + && finalChestBlockRest.getY() == playerData.getInt(rPath + ".y") + && finalChestBlockRest.getZ() == playerData.getInt(rPath + ".z")) { + alreadyRest = true; + break; + } + } + if (!alreadyRest) currentRest = playerData.getConfigurationSection(basePath).getKeys(false).size(); + } + // Legacy fallback + if (!alreadyRest && currentRest == 0 && playerData.contains("players." + playerUUID + ".rest-chest")) { + currentRest = 1; + } } - if (hasRestAlready && maxRest <= 1) { - player.sendMessage(ChatColor.RED + "Du hast bereits eine Rest-Truhe! (Limit: " + maxRest + ")"); + if (!alreadyRest && currentRest >= maxRest) { + player.sendMessage(ChatColor.RED + "Du hast das Limit deiner Rest-Truhen erreicht! (" + maxRest + ")"); event.setCancelled(true); return; } @@ -1530,11 +1665,14 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { 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()); + alreadyInput = mysqlManager.getInputChests(playerUUID.toString()).stream().anyMatch(c -> { + String cWorld = (String) c.get("world"); + return cWorld != null + && cWorld.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 + ")"); @@ -1564,8 +1702,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (attachedBlock.getState() instanceof Chest) chestBlock = attachedBlock; } if (chestBlock == null) { player.sendMessage(getMessage("no-chest-near-sign")); return; } - event.setLine(0, "[asc]"); - event.setLine(1, "ziel"); + event.setLine(0, getSignColor("target", "line1") + "[asc]"); + event.setLine(1, getSignColor("target", "line2") + "ziel"); event.setLine(2, ""); event.setLine(3, ""); } @@ -2017,7 +2155,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { event.setCancelled(true); return; } - if (!player.isSneaking() && !isAdmin(player)) { + // BUG FIX: autosortchest.bypass erlaubt Abbau ohne Shift-Taste + if (!player.isSneaking() && !isAdmin(player) && !player.hasPermission("autosortchest.bypass")) { player.sendMessage(getMessage("sign-break-denied")); event.setCancelled(true); return; @@ -2052,8 +2191,10 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } if (ownerUUID == null) { - Location restLoc = getRestChestLocation(uuid); - if (chestLoc.equals(restLoc)) ownerUUID = uuid; + // BUG FIX: Alle Rest-Truhen des Spielers prüfen (nicht nur erste) + for (Location restLoc : getRestChestLocations(uuid)) { + if (chestLoc.equals(restLoc)) { ownerUUID = uuid; break; } + } } if (ownerUUID != null) break; } @@ -2062,9 +2203,33 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { UUID uuidToDelete = (ownerUUID != null) ? ownerUUID : player.getUniqueId(); if (line1.equalsIgnoreCase("rest")) { - playerData.set("players." + uuidToDelete + ".rest-chest", null); - savePlayerData(); - if (mysqlEnabled && mysqlManager != null) mysqlManager.removeRestChest(uuidToDelete.toString()); + if (mysqlEnabled && mysqlManager != null) { + // BUG FIX: Nur die spezifische Location löschen, nicht alle Rest-Truhen + mysqlManager.removeRestChestByLocation(uuidToDelete.toString(), + chestLoc.getWorld().getName(), chestLoc.getBlockX(), chestLoc.getBlockY(), chestLoc.getBlockZ()); + } else { + // YAML: Spezifische Location aus rest-chests entfernen + boolean removedFromNew = false; + String basePath = "players." + uuidToDelete + ".rest-chests"; + if (playerData.contains(basePath)) { + for (String slotKey : new ArrayList<>(playerData.getConfigurationSection(basePath).getKeys(false))) { + String rPath = basePath + "." + slotKey; + if (chestLoc.getWorld().getName().equals(playerData.getString(rPath + ".world")) + && chestLoc.getBlockX() == playerData.getInt(rPath + ".x") + && chestLoc.getBlockY() == playerData.getInt(rPath + ".y") + && chestLoc.getBlockZ() == playerData.getInt(rPath + ".z")) { + playerData.set(rPath, null); + removedFromNew = true; + break; + } + } + } + // Legacy-Fallback + if (!removedFromNew) { + playerData.set("players." + uuidToDelete + ".rest-chest", null); + } + savePlayerData(); + } } else if (line1.equalsIgnoreCase("input")) { removeInputChestByLocation(uuidToDelete, chestLoc); } else if (line1.equalsIgnoreCase("ziel")) { @@ -2072,11 +2237,24 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (!line2.isEmpty()) { Material mat = Material.matchMaterial(line2); if (mat != null) { - Location savedLoc = getTargetChestLocation(uuidToDelete, mat); - if (savedLoc != null && savedLoc.equals(chestLoc)) { - playerData.set("players." + uuidToDelete + ".target-chests." + mat.name(), null); - savePlayerData(); - if (mysqlEnabled && mysqlManager != null) mysqlManager.removeTargetChest(uuidToDelete.toString(), mat.name()); + if (mysqlEnabled && mysqlManager != null) { + // BUG FIX: Nur bei MySQL → kein playerData-Zugriff nötig + mysqlManager.removeTargetChest(uuidToDelete.toString(), mat.name()); + } else { + // YAML: Slot mit passender Location finden und löschen + String basePath = "players." + uuidToDelete + ".target-chests"; + if (playerData.contains(basePath)) { + String path = basePath + "." + mat.name(); + if (playerData.contains(path)) { + Location savedLoc = getLocationFromPath(path); + if (savedLoc != null && savedLoc.getBlockX() == chestLoc.getBlockX() + && savedLoc.getBlockY() == chestLoc.getBlockY() + && savedLoc.getBlockZ() == chestLoc.getBlockZ()) { + playerData.set(path, null); + savePlayerData(); + } + } + } } } } @@ -2144,16 +2322,36 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } private void removeOldTargetEntry(UUID uuid, Location loc, String newItemType) { + if (mysqlEnabled && mysqlManager != null) { + // MySQL: Alle Zieltruhen dieser Location löschen, die ein anderes Item haben + List> existing = mysqlManager.getTargetChests(uuid.toString()); + for (Map e : existing) { + String existingItem = (String) e.get("item"); + if (existingItem.equalsIgnoreCase(newItemType)) continue; + String w = (String) e.get("world"); + if (w.equals(loc.getWorld().getName()) + && (int) e.get("x") == loc.getBlockX() + && (int) e.get("y") == loc.getBlockY() + && (int) e.get("z") == loc.getBlockZ()) { + mysqlManager.removeTargetChest(uuid.toString(), existingItem); + } + } + return; + } + // YAML-Modus String basePath = "players." + uuid + ".target-chests"; if (!playerData.contains(basePath)) return; for (String existingType : playerData.getConfigurationSection(basePath).getKeys(false)) { if (existingType.equalsIgnoreCase(newItemType)) continue; String path = basePath + "." + existingType; - if (playerData.getString(path + ".world").equals(loc.getWorld().getName()) + String savedWorld = playerData.getString(path + ".world"); + if (savedWorld == null) continue; // NPE-Schutz: korrupter Eintrag überspringen + if (savedWorld.equals(loc.getWorld().getName()) && playerData.getInt(path + ".x") == loc.getBlockX() && playerData.getInt(path + ".y") == loc.getBlockY() && playerData.getInt(path + ".z") == loc.getBlockZ()) { playerData.set(path, null); + savePlayerData(); // Änderung direkt auf Disk schreiben break; } } @@ -2352,7 +2550,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { }); // 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; + // BUG FIX: Alle Rest-Truhen vorladen (nicht nur eine) + List> preRests = anyLocal ? mysqlManager.getRestChests(uuidString) : new ArrayList<>(); for (Map chest : chests) { if (crosslink) { @@ -2361,7 +2560,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } String worldName = (String) chest.get("world"); boolean isLocal = Bukkit.getWorld(worldName) != null; - jobs.add(new Object[]{ ownerUUID, chest, isLocal, preTargets, preRest }); + jobs.add(new Object[]{ ownerUUID, chest, isLocal, preTargets, preRests }); } } } catch (Exception e) { @@ -2383,7 +2582,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { @SuppressWarnings("unchecked") List> preTargets = (List>) job[3]; @SuppressWarnings("unchecked") - Map preRest = (Map) job[4]; + // BUG FIX: preRests ist jetzt eine Liste + List> preRests = (List>) job[4]; if (isLocal) { World world = Bukkit.getWorld((String) chest.get("world")); @@ -2400,14 +2600,17 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { targetLocs.computeIfAbsent((String) tc.get("item"), k -> new ArrayList<>()) .add(new Location(w, (int) tc.get("x"), (int) tc.get("y"), (int) tc.get("z"))); } - 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")); + // BUG FIX: Alle Rest-Truhen als Liste konvertieren + List restLocs = new ArrayList<>(); + for (Map preRest : preRests) { + if (preRest != null && !isRemoteChest(preRest)) { + World w = Bukkit.getWorld((String) preRest.get("world")); + if (w != null) restLocs.add(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); + checkSingleInputChest(ownerUUID, loc, (String) chest.get("chest_id"), false, targetLocs, restLocs); } else if (crosslink) { checkRemoteInputChest(ownerUUID, chest); } @@ -2457,20 +2660,21 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { .add(new Location(world, (int) tc.get("x"), (int) tc.get("y"), (int) tc.get("z"))); } - Location localRestChest = getRestChestLocation(ownerUUID); + // BUG FIX: Alle lokalen Rest-Truhen laden + List localRestChests = getRestChestLocations(ownerUUID); - if (localTargets.isEmpty() && localRestChest == null) return; + if (localTargets.isEmpty() && localRestChests.isEmpty()) return; OfflinePlayer op = Bukkit.getOfflinePlayer(ownerUUID); String ownerName = op.getName() != null ? op.getName() : ownerUUID.toString(); - distributeFromRemoteInputChest(ownerUUID, ownerName, inputChestData, localTargets, localRestChest); + distributeFromRemoteInputChest(ownerUUID, ownerName, inputChestData, localTargets, localRestChests); } private void distributeFromRemoteInputChest(UUID ownerUUID, String ownerName, Map inputChestData, Map> localTargets, - Location localRestChest) { + List localRestChests) { if (!mysqlEnabled || mysqlManager == null) return; @@ -2495,7 +2699,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { for (Location l : slots) { if (l != null && !isChestCachedFull(l)) { targetLoc = l; break; } } - if (targetLoc == null) targetLoc = localRestChest; + // BUG FIX: Alle lokalen Rest-Truhen als Fallback versuchen + if (targetLoc == null) { + for (Location restLoc : localRestChests) { + if (restLoc != null && !isChestCachedFull(restLoc)) { targetLoc = restLoc; break; } + } + } if (targetLoc == null) continue; if (!(targetLoc.getBlock().getState() instanceof Chest)) continue; @@ -2523,7 +2732,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { /** 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); + List restLocs = getRestChestLocations(ownerUUID); Map> targets = new HashMap<>(); if (mysqlEnabled && mysqlManager != null) { for (Map tc : mysqlManager.getTargetChests(ownerUUID.toString())) { @@ -2546,7 +2755,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } } - distributeItemsForOwner(ownerUUID, ownerPlayer, sourceInventory, ownerNameOverride, sourceLocation, targets, restLoc); + distributeItemsForOwner(ownerUUID, ownerPlayer, sourceInventory, ownerNameOverride, sourceLocation, targets, restLocs); } private boolean checkSingleInputChest(UUID ownerUUID, Location location, String debugId, boolean crosslinkMode) { @@ -2554,7 +2763,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } private boolean checkSingleInputChest(UUID ownerUUID, Location location, String debugId, boolean crosslinkMode, - Map> preloadedTargets, Location preloadedRest) { + Map> preloadedTargets, List preloadedRests) { if (isWorldBlacklisted(location.getWorld())) return true; if (!(location.getBlock().getState() instanceof Chest)) return false; @@ -2599,7 +2808,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { // Wenn preloadedTargets null: synchron laden (Fallback) Map> effectiveTargets = preloadedTargets; - Location effectiveRest = preloadedRest; + List effectiveRests = preloadedRests; if (effectiveTargets == null) { effectiveTargets = new HashMap<>(); if (mysqlEnabled && mysqlManager != null) { @@ -2625,22 +2834,23 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } } - if (effectiveRest == null && preloadedTargets == null) { - effectiveRest = getRestChestLocation(ownerUUID); + // BUG FIX: Rest-Truhen als Liste laden wenn nicht vorgeladen + if (effectiveRests == null) { + effectiveRests = getRestChestLocations(ownerUUID); } - distributeItemsForOwner(ownerUUID, ownerPlayer, chest.getInventory(), ownerName, location, effectiveTargets, effectiveRest); + distributeItemsForOwner(ownerUUID, ownerPlayer, chest.getInventory(), ownerName, location, effectiveTargets, effectiveRests); 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 + * restChestLocs: lokale Rest-Truhen oder leere Liste */ private void distributeItemsForOwner(UUID ownerUUID, Player ownerPlayer, Inventory sourceInventory, String ownerNameOverride, Location sourceLocation, - Map> targetChestMap, Location restChestLoc) { + Map> targetChestMap, List restChestLocs) { boolean hasItems = false; for (ItemStack item : sourceInventory.getContents()) { @@ -2658,8 +2868,11 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (offlinePlayer.hasPlayedBefore()) ownerName = offlinePlayer.getName(); } - Location restChestLocation = restChestLoc; - boolean restChestKnownFull = (restChestLocation != null) && isChestCachedFull(restChestLocation); + // BUG FIX: Rest-Truhen als Liste verwalten + List restChestLocations = (restChestLocs != null) ? restChestLocs : new ArrayList<>(); + // Alle vollen Rest-Truhen ermitteln + boolean allRestChestsFull = !restChestLocations.isEmpty() + && restChestLocations.stream().allMatch(l -> l != null && isChestCachedFull(l)); for (int slot = 0; slot < sourceInventory.getSize(); slot++) { ItemStack item = sourceInventory.getItem(slot); @@ -2676,12 +2889,16 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } boolean isRestChest = false; boolean isCrosslink = false; + // Aktive Rest-Truhe für dieses Item (erste nicht-volle) + Location activeRestChestLocation = null; + for (Location loc : restChestLocations) { + if (loc != null && !isChestCachedFull(loc)) { activeRestChestLocation = loc; break; } + } if (targetChestLocation == null) { if (serverCrosslink && mysqlEnabled && mysqlManager != null) { Map raw = mysqlManager.getTargetChest(ownerUUID.toString(), item.getType().name()); if (raw != null && isRemoteChest(raw)) { - // ── BungeeCord NEU: expliziten Ziel-Server mitgeben ─────────── String targetServerName = getChestServer(raw); isCrosslink = true; mysqlManager.setupTransferTable(); @@ -2698,27 +2915,31 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } if (!isCrosslink) { - if (restChestKnownFull) continue; + // BUG FIX: Alle Rest-Truhen prüfen, nicht nur eine + if (allRestChestsFull) continue; - if (restChestLocation != null) { - targetChestLocation = restChestLocation; + if (activeRestChestLocation != null) { + targetChestLocation = activeRestChestLocation; isRestChest = true; } else if (serverCrosslink && mysqlEnabled && mysqlManager != null) { - Map raw = mysqlManager.getRestChest(ownerUUID.toString()); - 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"), targetServerName, serverName); - sourceInventory.setItem(slot, null); - if (isDebug()) { - getLogger().info("[CrossLink] " + item.getAmount() + "x " + item.getType().name() - + " → Server:'" + (targetServerName.isEmpty() ? "?" : targetServerName) - + "' Welt:'" + raw.get("world") + "' (Rest-Transfer-DB)"); + // Crosslink: Remote Rest-Truhe suchen + for (Map raw : mysqlManager.getRestChests(ownerUUID.toString())) { + if (raw != null && isRemoteChest(raw)) { + String targetServerName = getChestServer(raw); + mysqlManager.setupTransferTable(); + 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() + + " → Server:'" + (targetServerName.isEmpty() ? "?" : targetServerName) + + "' Welt:'" + raw.get("world") + "' (Rest-Transfer-DB)"); + } + isCrosslink = true; + break; } - continue; } + if (isCrosslink) continue; } } } else { @@ -2741,9 +2962,36 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { ownerPlayer.sendMessage(getMessage("target-chest-missing").replace("%item%", isRestChest ? "Rest-Truhe" : item.getType().name())); } - if (isRestChest) playerData.set("players." + ownerUUID + ".rest-chest", null); - else playerData.set("players." + ownerUUID + ".target-chests." + item.getType().name(), null); - savePlayerData(); + // BUG FIX: Beim MySQL-Modus playerData nicht anfassen; bei YAML korrekte Pfade + if (isRestChest) { + if (mysqlEnabled && mysqlManager != null) { + mysqlManager.removeRestChestByLocation(ownerUUID.toString(), + targetChestLocation.getWorld().getName(), + targetChestLocation.getBlockX(), targetChestLocation.getBlockY(), targetChestLocation.getBlockZ()); + } else { + String basePath = "players." + ownerUUID + ".rest-chests"; + if (playerData.contains(basePath)) { + for (String slotKey : new ArrayList<>(playerData.getConfigurationSection(basePath).getKeys(false))) { + String rPath = basePath + "." + slotKey; + if (targetChestLocation.getWorld().getName().equals(playerData.getString(rPath + ".world")) + && targetChestLocation.getBlockX() == playerData.getInt(rPath + ".x") + && targetChestLocation.getBlockY() == playerData.getInt(rPath + ".y") + && targetChestLocation.getBlockZ() == playerData.getInt(rPath + ".z")) { + playerData.set(rPath, null); + break; + } + } + } + // Legacy + playerData.set("players." + ownerUUID + ".rest-chest", null); + savePlayerData(); + } + } else { + if (!mysqlEnabled) { + playerData.set("players." + ownerUUID + ".target-chests." + item.getType().name(), null); + savePlayerData(); + } + } continue; } @@ -2782,7 +3030,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (isFull) { markChestFull(targetChestLocation); - if (isRestChest) restChestKnownFull = true; } if (signBlock != null) { @@ -2829,19 +3076,41 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } if (isFull && isRestChest) { - if (ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { - String message = getMessage("target-chest-full") - .replace("%item%", item.getType().name()) - .replace("%x%", String.valueOf(targetChestLocation.getBlockX())) - .replace("%y%", String.valueOf(targetChestLocation.getBlockY())) - .replace("%z%", String.valueOf(targetChestLocation.getBlockZ())); - ownerPlayer.sendMessage(message); + // BUG FIX: Nächste Rest-Truhe versuchen + Location nextRestLoc = null; + for (Location loc : restChestLocations) { + if (loc != null && !isChestCachedFull(loc) && !loc.equals(targetChestLocation)) { + nextRestLoc = loc; break; + } } - for (ItemStack leftoverItem : leftover.values()) { - if (leftoverItem != null && leftoverItem.getType() == item.getType()) { - item.setAmount(leftoverItem.getAmount()); + if (nextRestLoc != null && nextRestLoc.getBlock().getState() instanceof Chest) { + Chest nextRestChest = (Chest) nextRestLoc.getBlock().getState(); + Map leftover2 = nextRestChest.getInventory().addItem( + leftover.isEmpty() ? new ItemStack(item.getType(), 0) : leftover.get(0).clone()); + if (leftover2.isEmpty()) { + sourceInventory.setItem(slot, null); + spawnTransferParticles(null, nextRestLoc); + continue; + } else { + item.setAmount(leftover2.get(0).getAmount()); sourceInventory.setItem(slot, item); - break; + } + } else { + // Alle Rest-Truhen voll: Nachricht senden + if (ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { + String message = getMessage("target-chest-full") + .replace("%item%", item.getType().name()) + .replace("%x%", String.valueOf(targetChestLocation.getBlockX())) + .replace("%y%", String.valueOf(targetChestLocation.getBlockY())) + .replace("%z%", String.valueOf(targetChestLocation.getBlockZ())); + ownerPlayer.sendMessage(message); + } + for (ItemStack leftoverItem : leftover.values()) { + if (leftoverItem != null && leftoverItem.getType() == item.getType()) { + item.setAmount(leftoverItem.getAmount()); + sourceInventory.setItem(slot, item); + break; + } } } } diff --git a/src/main/java/com/viper/autosortchest/MySQLManager.java b/src/main/java/com/viper/autosortchest/MySQLManager.java index 956d197..5cdaa90 100644 --- a/src/main/java/com/viper/autosortchest/MySQLManager.java +++ b/src/main/java/com/viper/autosortchest/MySQLManager.java @@ -36,6 +36,39 @@ public class MySQLManager { } } + /** + * Migriert den PRIMARY KEY der asc_rest_chests von (uuid) auf (uuid, slot) falls nötig. + * Ermöglicht mehrere Rest-Truhen pro Spieler. + */ + private void migrateRestChestPrimaryKey(Statement st) { + try { + // Slot-Spalte zuerst sicherstellen + if (!columnExists("asc_rest_chests", "slot")) { + st.execute("ALTER TABLE asc_rest_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_rest_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_rest_chests DROP PRIMARY KEY, ADD PRIMARY KEY(uuid, slot);"); + } catch (SQLException e) { + if (!e.getMessage().toLowerCase().contains("primary")) { + e.printStackTrace(); + } + } + } + /** * 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). @@ -88,7 +121,7 @@ public class MySQLManager { */ private void tryAlterColumn(Statement st, String table, String column, String definition) { try { - ResultSet rs = st.getConnection().getMetaData().getColumns(null, null, table, column); + ResultSet rs = st.getConnection().getMetaData().getColumns(connection.getCatalog(), null, table, column); if (!rs.next()) { st.execute("ALTER TABLE " + table + " ADD COLUMN " + column + " " + definition + ";"); } @@ -105,7 +138,7 @@ public class MySQLManager { */ private boolean columnExists(String table, String column) { try { - ResultSet rs = connection.getMetaData().getColumns(null, null, table, column); + ResultSet rs = connection.getMetaData().getColumns(connection.getCatalog(), null, table, column); boolean exists = rs.next(); rs.close(); return exists; @@ -168,9 +201,10 @@ public class MySQLManager { ");"); st.execute("CREATE TABLE IF NOT EXISTS asc_rest_chests (" + - "uuid VARCHAR(36), world VARCHAR(32)," + + "uuid VARCHAR(36), slot INT NOT NULL DEFAULT 0," + + "world VARCHAR(32)," + "x INT, y INT, z INT, `public` BOOLEAN DEFAULT FALSE," + - "PRIMARY KEY(uuid)" + + "PRIMARY KEY(uuid, slot)" + ");"); // ── asc_transfers: immer beim Start sicherstellen ───────────────────── @@ -207,6 +241,9 @@ public class MySQLManager { // v2 → v3 (Multi-Target): slot-Spalte + PRIMARY KEY Migration migrateTargetChestPrimaryKey(st); + // v2 → v3 (Multi-Rest): slot-Spalte + PRIMARY KEY Migration fuer Rest-Truhen + migrateRestChestPrimaryKey(st); + } catch (SQLException e) { e.printStackTrace(); } @@ -593,32 +630,93 @@ public class MySQLManager { setRestChest(uuid, world, x, y, z, isPublic, ""); } - /** BungeeCord-Überladung mit serverName. */ + /** BungeeCord-Überladung mit serverName – ermittelt automatisch den nächsten freien Slot. */ public void setRestChest(String uuid, String world, int x, int y, int z, boolean isPublic, String serverName) { + // Prüfen ob diese exakte Location bereits als Rest-Truhe registriert ist (Update) + int slot = getRestSlotForLocation(uuid, world, x, y, z); + if (slot < 0) { + // Neue Truhe: nächsten freien Slot ermitteln + slot = getNextRestSlot(uuid); + } + setRestChest(uuid, slot, world, x, y, z, isPublic, serverName); + } + + /** Vollständige Überladung mit explizitem slot. */ + public void setRestChest(String uuid, int slot, String world, int x, int y, int z, + boolean isPublic, String serverName) { try (PreparedStatement ps = connection.prepareStatement( - "REPLACE INTO asc_rest_chests (uuid, world, x, y, z, `public`, server) " + - "VALUES (?, ?, ?, ?, ?, ?, ?);")) { + "REPLACE INTO asc_rest_chests (uuid, slot, 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.setInt(2, slot); + ps.setString(3, world); + ps.setInt(4, x); + 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(); } } - public Map getRestChest(String uuid) { + /** Gibt den nächsten freien Slot für Rest-Truhen zurück. */ + public int getNextRestSlot(String uuid) { try (PreparedStatement ps = connection.prepareStatement( - "SELECT * FROM asc_rest_chests WHERE uuid=?;")) { + "SELECT COALESCE(MAX(slot)+1, 0) AS next_slot FROM asc_rest_chests WHERE uuid=?;")) { ps.setString(1, uuid); ResultSet rs = ps.executeQuery(); - if (rs.next()) { + if (rs.next()) return rs.getInt("next_slot"); + } catch (SQLException e) { + e.printStackTrace(); + } + return 0; + } + + /** + * Gibt den Slot zurück, den diese Location bereits belegt, oder -1 wenn nicht gefunden. + */ + public int getRestSlotForLocation(String uuid, String world, int x, int y, int z) { + try (PreparedStatement ps = connection.prepareStatement( + "SELECT slot FROM asc_rest_chests WHERE uuid=? AND world=? AND x=? AND y=? AND z=?;")) { + ps.setString(1, uuid); + ps.setString(2, world); + ps.setInt(3, x); + ps.setInt(4, y); + ps.setInt(5, z); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return rs.getInt("slot"); + } catch (SQLException e) { + e.printStackTrace(); + } + return -1; + } + + /** Zählt wie viele Rest-Truhen ein Spieler hat. */ + public int countRestChests(String uuid) { + try (PreparedStatement ps = connection.prepareStatement( + "SELECT COUNT(*) FROM asc_rest_chests WHERE uuid=?;")) { + ps.setString(1, uuid); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return rs.getInt(1); + } catch (SQLException e) { + e.printStackTrace(); + } + return 0; + } + + /** Gibt ALLE Rest-Truhen eines Spielers zurück, sortiert nach slot. */ + public List> getRestChests(String uuid) { + List> list = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT * FROM asc_rest_chests WHERE uuid=? ORDER BY slot ASC;")) { + ps.setString(1, uuid); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { Map map = new HashMap<>(); + map.put("slot", rs.getInt("slot")); map.put("world", rs.getString("world")); map.put("x", rs.getInt("x")); map.put("y", rs.getInt("y")); @@ -626,14 +724,72 @@ public class MySQLManager { map.put("public", rs.getBoolean("public")); try { map.put("server", rs.getString("server")); } catch (SQLException ignored) { map.put("server", ""); } - return map; + list.add(map); } } catch (SQLException e) { e.printStackTrace(); } - return null; + return list; } + /** Gibt die erste Rest-Truhe (slot=0) zurück – Legacy-Kompatibilität. */ + public Map getRestChest(String uuid) { + List> all = getRestChests(uuid); + return all.isEmpty() ? null : all.get(0); + } + + /** Löscht eine spezifische Rest-Truhe anhand ihrer Location. */ + public void removeRestChestByLocation(String uuid, String world, int x, int y, int z) { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM asc_rest_chests WHERE uuid=? AND world=? AND x=? AND y=? AND z=?;")) { + ps.setString(1, uuid); + ps.setString(2, world); + ps.setInt(3, x); + ps.setInt(4, y); + ps.setInt(5, z); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + // Slots nach dem gelöschten nach unten schieben um Lücken zu vermeiden + compactRestSlots(uuid); + } + + /** Löscht eine spezifische Rest-Truhe anhand des Slots. */ + public void removeRestChestSlot(String uuid, int slot) { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM asc_rest_chests WHERE uuid=? AND slot=?;")) { + ps.setString(1, uuid); + ps.setInt(2, slot); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + compactRestSlots(uuid); + } + + /** Schiebt Rest-Slots nach dem gelöschten nach unten (keine Lücken). */ + private void compactRestSlots(String uuid) { + try { + List> remaining = getRestChests(uuid); + for (int i = 0; i < remaining.size(); i++) { + int currentSlot = (int) remaining.get(i).get("slot"); + if (currentSlot != i) { + try (PreparedStatement ps = connection.prepareStatement( + "UPDATE asc_rest_chests SET slot=? WHERE uuid=? AND slot=?;")) { + ps.setInt(1, i); + ps.setString(2, uuid); + ps.setInt(3, currentSlot); + ps.executeUpdate(); + } + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** Löscht ALLE Rest-Truhen eines Spielers. */ public void removeRestChest(String uuid) { try (PreparedStatement ps = connection.prepareStatement( "DELETE FROM asc_rest_chests WHERE uuid=?;")) { @@ -694,11 +850,12 @@ public class MySQLManager { public Map getAnyRestChest() { try (PreparedStatement ps = connection.prepareStatement( - "SELECT * FROM asc_rest_chests WHERE public=1 LIMIT 1;")) { + "SELECT * FROM asc_rest_chests WHERE `public`=1 ORDER BY uuid, slot LIMIT 1;")) { ResultSet rs = ps.executeQuery(); if (rs.next()) { Map map = new HashMap<>(); map.put("uuid", rs.getString("uuid")); + map.put("slot", rs.getInt("slot")); map.put("world", rs.getString("world")); map.put("x", rs.getInt("x")); map.put("y", rs.getInt("y")); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 0af9fef..da9f8b8 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -10,7 +10,7 @@ # --- GRUNDLEGUNG --- # Version der Konfigurationsdatei. Nicht ändern, um Fehler zu vermeiden! -version: "2.0" +version: "2.2" # Debug-Modus (true = Ausführliche Logs in der Server-Konsole, nur zum Entwickeln nutzen) diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 23c759e..bce22a0 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: AutoSortChest -version: 2.2 +version: 2.3 main: com.viper.autosortchest.Main api-version: 1.21 authors: [M_Viper] @@ -24,4 +24,10 @@ permissions: default: op autosortchest.admin: description: Erlaubt OPs/Admins Zugriff auf fremde AutoSortChest-Truhen (Öffnen, Entnehmen, Abbauen) - default: op \ No newline at end of file + default: op + autosortchest.limit.: + description: > + Limits fuer eine benutzerdefinierte Gruppe aus der config.yml. + Ersetze durch den Gruppennamen (z.B. autosortchest.limit.vip). + Die Gruppen und ihre Limits werden ausschliesslich in der config.yml definiert. + default: false \ No newline at end of file