diff --git a/src/main/java/com/viper/autosortchest/Main.java b/src/main/java/com/viper/autosortchest/Main.java index 5de87e4..4549cc9 100644 --- a/src/main/java/com/viper/autosortchest/Main.java +++ b/src/main/java/com/viper/autosortchest/Main.java @@ -35,28 +35,111 @@ import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.util.Vector; +import org.bukkit.scheduler.BukkitTask; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.*; +import com.viper.autosortchest.MySQLManager; + public class Main extends JavaPlugin implements Listener, CommandExecutor { + + private boolean serverCrosslink = true; + + // ------------------------------------------------------- + // Task-Referenzen für sauberen Reload + // ------------------------------------------------------- + private BukkitTask sortTask = null; + private BukkitTask cleanTask = null; + + 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"); + + sortIntervalTicks = config.getInt("sort_interval_ticks", 20); + + if (sortIntervalTicks < 1) { + getLogger().warning("sort_interval_ticks war zu niedrig (" + sortIntervalTicks + ")! Wird auf 1 gesetzt."); + sortIntervalTicks = 1; + } + + 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)); + } + } + } + + /** + * Startet oder startet den Sortier-Task und den Cleanup-Task neu. + * Bestehende Tasks werden vorher sauber gestoppt. + */ + private void startTasks() { + if (sortTask != null) { + sortTask.cancel(); + sortTask = null; + } + if (cleanTask != null) { + cleanTask.cancel(); + cleanTask = null; + } + + sortTask = new BukkitRunnable() { + @Override + public void run() { + checkInputChests(); + } + }.runTaskTimer(this, 20L, sortIntervalTicks > 0 ? sortIntervalTicks : 20L); + + cleanTask = new BukkitRunnable() { + @Override + public void run() { + cleanMessageTracker(); + } + }.runTaskTimer(this, 20L * 60, 20L * 60); + } + + private int getChestLimitForPlayer(Player player) { + List groups = new ArrayList<>(chestLimits.keySet()); + groups.sort((a, b) -> Integer.compare(chestLimits.getOrDefault(b, 0), chestLimits.getOrDefault(a, 0))); + for (String group : groups) { + if (player.hasPermission("autosortchest.limit." + group)) { + return chestLimits.getOrDefault(group, chestLimits.getOrDefault("default", 50)); + } + } + return chestLimits.getOrDefault("default", 50); + } + private File playerDataFile; private FileConfiguration playerData; private FileConfiguration config; + private MySQLManager mysqlManager; + private boolean mysqlEnabled; + private String mysqlHost; + private int mysqlPort; + private String mysqlDatabase; + private String mysqlUser; + private String mysqlPassword; + private List worldBlacklist; + private int sortIntervalTicks; + private Map chestLimits; private final Map> fullChestMessageTracker = new HashMap<>(); - private static final long MESSAGE_COOLDOWN = 5 * 60 * 1000; // 5 Minuten in Millisekunden - private static final String CONFIG_VERSION = "1.8"; + private static final long MESSAGE_COOLDOWN = 5 * 60 * 1000; + private static final String CONFIG_VERSION = "2.0"; - // --- NEU: Update Checker Variabben --- private boolean updateAvailable = false; private String latestVersion = ""; - // -------------------------------------------------- - // --- Hardcodierte Nachrichten (DE & EN) für Hilfe und Info (Safe in Java) --- private static final String HELP_DE = "&6&l=== AutoSortChest Hilfe ===\n" + "&eEingangstruhe erstellen:\n" + @@ -80,6 +163,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&f- &b/asc help &f- Zeigt diese Hilfe.\n" + "&f- &b/asc info &f- Zeigt Plugin-Informationen.\n" + "&f- &b/asc reload &f- Lädt die Konfiguration neu (OP).\n" + + "&f- &b/asc import &f- Importiert Daten aus players.yml in MySQL (OP).\n" + + "&f- &b/asc export &f- Exportiert Daten aus MySQL in players.yml (OP).\n" + "&6&l========================"; private static final String HELP_EN = @@ -105,6 +190,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&f- &b/asc help &f- Shows this help.\n" + "&f- &b/asc info &f- Shows plugin info.\n" + "&f- &b/asc reload &f- Reloads the config (OP only).\n" + + "&f- &b/asc import &f- Imports data from players.yml into MySQL (OP only).\n" + + "&f- &b/asc export &f- Exports data from MySQL into players.yml (OP only).\n" + "&6&l========================"; private static final String INFO_DE = @@ -112,7 +199,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&ePlugin: &fAutoSortChest\n" + "&eVersion: &f%version%\n" + "&eConfig-Version: &f%config_version%\n" + - "&eErsteller: &f%author%\n" + + "&eErsteller: &fM_Viper\n" + "&eBeschreibung: &fAutomatisches Sortieren von Items in Truhen.\n" + "&6&l========================"; @@ -121,64 +208,188 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&ePlugin: &fAutoSortChest\n" + "&eVersion: &f%version%\n" + "&eConfig-Version: &f%config_version%\n" + - "&eAuthor: &f%author%\n" + + "&eAuthor: &fM_Viper\n" + "&eDescription: &fAutomatically sorts items into chests.\n" + "&6&l========================"; - // ---------------------------------------------------------------------------------- + + // ------------------------------------------------------- + // IMPORT: YAML → MySQL + // ------------------------------------------------------- + private void migrateYamlToMySQL() { + if (playerData == null || mysqlManager == null) return; + if (playerData.getConfigurationSection("players") == null) return; + for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { + String name = uuidString; + try { + UUID uuid = UUID.fromString(uuidString); + OfflinePlayer offline = getServer().getOfflinePlayer(uuid); + if (offline != null && offline.getName() != null) name = offline.getName(); + } catch (Exception ignored) {} + mysqlManager.savePlayer(uuidString, name); + 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"); + int x = playerData.getInt(path + ".x"); + int y = playerData.getInt(path + ".y"); + int z = playerData.getInt(path + ".z"); + boolean isPublic = playerData.getBoolean(path + ".public", false); + mysqlManager.addInputChest(uuidString, chestId, world, x, y, z, isPublic); + } + } + String targetPath = "players." + uuidString + ".target-chests"; + if (playerData.contains(targetPath)) { + for (String item : playerData.getConfigurationSection(targetPath).getKeys(false)) { + String path = targetPath + "." + item; + String world = playerData.getString(path + ".world"); + int x = playerData.getInt(path + ".x"); + int y = playerData.getInt(path + ".y"); + int z = playerData.getInt(path + ".z"); + boolean isPublic = playerData.getBoolean(path + ".public", false); + mysqlManager.setTargetChest(uuidString, item, world, x, y, z, isPublic); + } + } + String 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."); + } + + // ------------------------------------------------------- + // 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 { + java.nio.file.Files.copy(playerDataFile.toPath(), backupFile.toPath()); + getLogger().info("Backup der players.yml erstellt: " + backupFile.getName()); + } catch (IOException e) { + getLogger().warning("Backup fehlgeschlagen: " + e.getMessage()); + } + } + + // 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 + ".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 + ".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 + ".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; + + } catch (Exception e) { + getLogger().warning("Fehler beim MySQL-Export: " + e.getMessage()); + return -1; + } + } @Override public void onEnable() { - // Lade Standard-Konfiguration saveDefaultConfig(); config = getConfig(); + loadOptionalSettings(); + + if (mysqlEnabled) { + mysqlManager = new MySQLManager(mysqlHost, mysqlPort, mysqlDatabase, mysqlUser, mysqlPassword); + if (mysqlManager.connect()) { + mysqlManager.setupTables(); + getLogger().info("MySQL-Verbindung erfolgreich hergestellt."); + migrateYamlToMySQL(); + } else { + getLogger().warning("MySQL-Verbindung fehlgeschlagen! Fallback auf YAML."); + mysqlEnabled = false; + } + } - // Lade players.yml playerDataFile = new File(getDataFolder(), "players.yml"); if (!playerDataFile.exists()) { saveResource("players.yml", false); } loadPlayerData(); - // Registriere Events getServer().getPluginManager().registerEvents(this, this); - - // Registriere Befehle this.getCommand("asc").setExecutor(this); - // Starte periodischen Task - new BukkitRunnable() { - @Override - public void run() { - checkInputChests(); - } - }.runTaskTimer(this, 20L, 20L); // Alle 1 Sekunde (20 Ticks) + startTasks(); - // Starte Task zum Bereinigen des Message-Trackers - new BukkitRunnable() { - @Override - public void run() { - cleanMessageTracker(); - } - }.runTaskTimer(this, 20L * 60, 20L * 60); // Alle Minute - - // Aktualisiere Konfiguration und Schilder updateConfig(); updateExistingSigns(); - - // ERWEITERUNG: Migration alter Daten in das neue Listen-System migrateInputChests(); - // --- NEU: Update Checker Start (Ganz oben in onEnable) --- new UpdateChecker(this, 131309).getVersion(version -> { Bukkit.getScheduler().runTask(this, () -> { - if (this.getDescription().getVersion().equals(version)) { + this.latestVersion = version; + if (UpdateChecker.isCurrentVersionUpToDate(this.getDescription().getVersion(), version)) { this.updateAvailable = false; - this.latestVersion = version; - getLogger().info("Es ist kein neues Update verfügbar."); + getLogger().info("Es ist kein neues Update verfügbar. (Laufend: " + + this.getDescription().getVersion() + " / SpigotMC: " + version + ")"); } else { this.updateAvailable = true; - this.latestVersion = version; - getLogger().info(" "); getLogger().info("========================================"); getLogger().info("Eine neue Version ist verfügbar: " + version); @@ -189,9 +400,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } }); }); - // ---------------------------------------------------- - // ASCII ART LOGO getLogger().info(""); getLogger().info(" ___ _ _____ _ _____ _ _ "); getLogger().info(" / _ \\ | | / ___| | | / __ \\ | | | "); @@ -200,28 +409,34 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { getLogger().info("| | | | |_| | || (_) /\\__/ / (_) | | | |_| \\__/\\ | | | __/\\__ \\ |_ "); getLogger().info("\\_| |_/\\__,_|\\__\\___/\\____/ \\___/|_| \\__|\\____/_| |_|\\___||___/\\__|"); getLogger().info(""); - getLogger().info("AutoSortChest Plugin aktiviert! Version: " + getDescription().getVersion()); + if (serverCrosslink) { + getLogger().info("Serverübergreifende Sortierung ist aktiviert (server_crosslink=true)."); + } + } + + private boolean isWorldBlacklisted(World world) { + if (world == null) return false; + return worldBlacklist != null && worldBlacklist.contains(world.getName()); } @Override public void onDisable() { + if (sortTask != null) { sortTask.cancel(); sortTask = null; } + if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } + savePlayerData(); + if (mysqlEnabled && mysqlManager != null) { + mysqlManager.disconnect(); + } getLogger().info("AutoSortChest Plugin deaktiviert!"); } - // --- NEU: In-Game Update Nachricht (Hier platziert wie gewünscht) --- @EventHandler public void onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent event) { - // Nur wenn ein Update verfügbar ist if (!updateAvailable) return; - Player player = event.getPlayer(); - - // Nur an Admins senden if (!isAdmin(player)) return; - - // Kurze Verzögerung (1 Sekunde), damit die Chat-Nachricht nicht im "Welcome" Spam untergeht new BukkitRunnable() { @Override public void run() { @@ -232,24 +447,18 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { player.sendMessage(ChatColor.GRAY + "Download: " + ChatColor.AQUA + "https://www.spigotmc.org/resources/" + 131309 + "/"); player.sendMessage(ChatColor.GOLD + "========================================"); } - }.runTaskLater(this, 20L * 1); // 1 Sekunde Verzögerung + }.runTaskLater(this, 20L); } - // -------------------------------------------------------------- - // --- NEU: Admin Prüfung --- private boolean isAdmin(Player player) { return player.isOp() || player.hasPermission("autosortchest.admin"); } - // ------------------------ - // --- HILFSMETHODE FÜR DOPPELTRUHEN --- private List getChestBlocks(Chest chest) { List blocks = new ArrayList<>(); InventoryHolder holder = chest.getInventory().getHolder(); - if (holder instanceof DoubleChest) { DoubleChest doubleChest = (DoubleChest) holder; - // Finde beide Blöcke der Doppeltruhe if (doubleChest.getLeftSide() instanceof Chest) { blocks.add(((Chest) doubleChest.getLeftSide()).getBlock()); } @@ -261,7 +470,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } return blocks; } - // ----------------------------------------- private void savePlayerData() { if (playerData == null || playerDataFile == null) { @@ -270,7 +478,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } try { playerData.save(playerDataFile); - getLogger().info("players.yml erfolgreich gespeichert"); } catch (IOException e) { getLogger().warning("Fehler beim Speichern der players.yml: " + e.getMessage()); } @@ -307,7 +514,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { InputStream defaultConfigStream = getResource("config.yml"); FileConfiguration defaultConfig; - // Lade Standardkonfiguration if (defaultConfigStream == null) { getLogger().warning("Standard-config.yml nicht gefunden in Plugin-Ressourcen! Verwende Fallback-Werte."); defaultConfig = new YamlConfiguration(); @@ -321,225 +527,101 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // Prüfe Konfigurationsversion String currentVersion = config.getString("version", "1.0"); - boolean versionUpdated = false; if (!currentVersion.equals(CONFIG_VERSION)) { getLogger().info("Aktualisiere config.yml von Version " + currentVersion + " auf " + CONFIG_VERSION); config.set("version", CONFIG_VERSION); - versionUpdated = true; } - // Setze Standardwerte für fehlende Schlüssel - if (!config.contains("debug")) { - config.set("debug", defaultConfig.getBoolean("debug", false)); - getLogger().info("Setze Standardwert: debug = " + config.getBoolean("debug")); - } + 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)); - // --- NEU: Sprache --- - if (!config.contains("language")) { - config.set("language", defaultConfig.getString("language", "de")); - getLogger().info("Setze Standardwert: language = " + config.getString("language")); - } - // ------------------- - - // --- NEU: Effekte Konfiguration (Partikel + Sound) --- 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")); - getLogger().info("Setze Standardwerte für effects"); } - // ---------------------------------- - // Prüfe und setze sign-colors.input if (!config.contains("sign-colors.input")) { config.createSection("sign-colors.input"); config.set("sign-colors.input.line1", defaultConfig.getString("sign-colors.input.line1", "&6")); config.set("sign-colors.input.line2", defaultConfig.getString("sign-colors.input.line2", "&0")); config.set("sign-colors.input.line4", defaultConfig.getString("sign-colors.input.line4", "&1")); - getLogger().info("Setze Standardwerte für sign-colors.input"); } else { - if (!config.contains("sign-colors.input.line1")) { - config.set("sign-colors.input.line1", defaultConfig.getString("sign-colors.input.line1", "&6")); - getLogger().info("Setze Standardwert: sign-colors.input.line1 = " + config.getString("sign-colors.input.line1")); - } - if (!config.contains("sign-colors.input.line2")) { - config.set("sign-colors.input.line2", defaultConfig.getString("sign-colors.input.line2", "&0")); - getLogger().info("Setze Standardwert: sign-colors.input.line2 = " + config.getString("sign-colors.input.line2")); - } - if (!config.contains("sign-colors.input.line4")) { - config.set("sign-colors.input.line4", defaultConfig.getString("sign-colors.input.line4", "&1")); - getLogger().info("Setze Standardwert: sign-colors.input.line4 = " + config.getString("sign-colors.input.line4")); - } + if (!config.contains("sign-colors.input.line1")) config.set("sign-colors.input.line1", defaultConfig.getString("sign-colors.input.line1", "&6")); + if (!config.contains("sign-colors.input.line2")) config.set("sign-colors.input.line2", defaultConfig.getString("sign-colors.input.line2", "&0")); + if (!config.contains("sign-colors.input.line4")) config.set("sign-colors.input.line4", defaultConfig.getString("sign-colors.input.line4", "&1")); } - // Prüfe und setze sign-colors.target if (!config.contains("sign-colors.target")) { config.createSection("sign-colors.target"); config.set("sign-colors.target.line1", defaultConfig.getString("sign-colors.target.line1", "&6")); config.set("sign-colors.target.line2", defaultConfig.getString("sign-colors.target.line2", "&0")); config.set("sign-colors.target.line3", defaultConfig.getString("sign-colors.target.line3", "&f")); config.set("sign-colors.target.line4", defaultConfig.getString("sign-colors.target.line4", "&1")); - getLogger().info("Setze Standardwerte für sign-colors.target"); } else { - if (!config.contains("sign-colors.target.line1")) { - config.set("sign-colors.target.line1", defaultConfig.getString("sign-colors.target.line1", "&6")); - getLogger().info("Setze Standardwert: sign-colors.target.line1 = " + config.getString("sign-colors.target.line1")); - } - if (!config.contains("sign-colors.target.line2")) { - config.set("sign-colors.target.line2", defaultConfig.getString("sign-colors.target.line2", "&0")); - getLogger().info("Setze Standardwert: sign-colors.target.line2 = " + config.getString("sign-colors.target.line2")); - } - if (!config.contains("sign-colors.target.line3")) { - config.set("sign-colors.target.line3", defaultConfig.getString("sign-colors.target.line3", "&f")); - getLogger().info("Setze Standardwert: sign-colors.target.line3 = " + config.getString("sign-colors.target.line3")); - } - if (!config.contains("sign-colors.target.line4")) { - config.set("sign-colors.target.line4", defaultConfig.getString("sign-colors.target.line4", "&1")); - getLogger().info("Setze Standardwert: sign-colors.target.line4 = " + config.getString("sign-colors.target.line4")); - } + if (!config.contains("sign-colors.target.line1")) config.set("sign-colors.target.line1", defaultConfig.getString("sign-colors.target.line1", "&6")); + if (!config.contains("sign-colors.target.line2")) config.set("sign-colors.target.line2", defaultConfig.getString("sign-colors.target.line2", "&0")); + if (!config.contains("sign-colors.target.line3")) config.set("sign-colors.target.line3", defaultConfig.getString("sign-colors.target.line3", "&f")); + if (!config.contains("sign-colors.target.line4")) config.set("sign-colors.target.line4", defaultConfig.getString("sign-colors.target.line4", "&1")); } - // Prüfe und setze sign-colors.full if (!config.contains("sign-colors.full")) { config.createSection("sign-colors.full"); config.set("sign-colors.full.line1", defaultConfig.getString("sign-colors.full.line1", "&c")); config.set("sign-colors.full.line2", defaultConfig.getString("sign-colors.full.line2", "&4")); config.set("sign-colors.full.line3", defaultConfig.getString("sign-colors.full.line3", "&e")); config.set("sign-colors.full.line4", defaultConfig.getString("sign-colors.full.line4", "&1")); - getLogger().info("Setze Standardwerte für sign-colors.full"); } else { - if (!config.contains("sign-colors.full.line1")) { - config.set("sign-colors.full.line1", defaultConfig.getString("sign-colors.full.line1", "&c")); - getLogger().info("Setze Standardwert: sign-colors.full.line1 = " + config.getString("sign-colors.full.line1")); - } - if (!config.contains("sign-colors.full.line2")) { - config.set("sign-colors.full.line2", defaultConfig.getString("sign-colors.full.line2", "&4")); - getLogger().info("Setze Standardwert: sign-colors.full.line2 = " + config.getString("sign-colors.full.line2")); - } - if (!config.contains("sign-colors.full.line3")) { - config.set("sign-colors.full.line3", defaultConfig.getString("sign-colors.full.line3", "&e")); - getLogger().info("Setze Standardwert: sign-colors.full.line3 = " + config.getString("sign-colors.full.line3")); - } - if (!config.contains("sign-colors.full.line4")) { - config.set("sign-colors.full.line4", defaultConfig.getString("sign-colors.full.line4", "&1")); - getLogger().info("Setze Standardwert: sign-colors.full.line4 = " + config.getString("sign-colors.full.line4")); - } + if (!config.contains("sign-colors.full.line1")) config.set("sign-colors.full.line1", defaultConfig.getString("sign-colors.full.line1", "&c")); + if (!config.contains("sign-colors.full.line2")) config.set("sign-colors.full.line2", defaultConfig.getString("sign-colors.full.line2", "&4")); + if (!config.contains("sign-colors.full.line3")) config.set("sign-colors.full.line3", defaultConfig.getString("sign-colors.full.line3", "&e")); + if (!config.contains("sign-colors.full.line4")) config.set("sign-colors.full.line4", defaultConfig.getString("sign-colors.full.line4", "&1")); } - // --- NEU: Prüfe und setze sign-colors.rest --- if (!config.contains("sign-colors.rest")) { config.createSection("sign-colors.rest"); config.set("sign-colors.rest.line1", defaultConfig.getString("sign-colors.rest.line1", "&6")); config.set("sign-colors.rest.line2", defaultConfig.getString("sign-colors.rest.line2", "&0")); config.set("sign-colors.rest.line3", defaultConfig.getString("sign-colors.rest.line3", "&f")); config.set("sign-colors.rest.line4", defaultConfig.getString("sign-colors.rest.line4", "&1")); - getLogger().info("Setze Standardwerte für sign-colors.rest"); } else { - if (!config.contains("sign-colors.rest.line1")) { - config.set("sign-colors.rest.line1", defaultConfig.getString("sign-colors.rest.line1", "&6")); - getLogger().info("Setze Standardwert: sign-colors.rest.line1 = " + config.getString("sign-colors.rest.line1")); - } - if (!config.contains("sign-colors.rest.line2")) { - config.set("sign-colors.rest.line2", defaultConfig.getString("sign-colors.rest.line2", "&0")); - getLogger().info("Setze Standardwert: sign-colors.rest.line2 = " + config.getString("sign-colors.rest.line2")); - } - if (!config.contains("sign-colors.rest.line3")) { - config.set("sign-colors.rest.line3", defaultConfig.getString("sign-colors.rest.line3", "&f")); - getLogger().info("Setze Standardwert: sign-colors.rest.line3 = " + config.getString("sign-colors.rest.line3")); - } - if (!config.contains("sign-colors.rest.line4")) { - config.set("sign-colors.rest.line4", defaultConfig.getString("sign-colors.rest.line4", "&1")); - getLogger().info("Setze Standardwert: sign-colors.rest.line4 = " + config.getString("sign-colors.rest.line4")); - } + if (!config.contains("sign-colors.rest.line1")) config.set("sign-colors.rest.line1", defaultConfig.getString("sign-colors.rest.line1", "&6")); + if (!config.contains("sign-colors.rest.line2")) config.set("sign-colors.rest.line2", defaultConfig.getString("sign-colors.rest.line2", "&0")); + if (!config.contains("sign-colors.rest.line3")) config.set("sign-colors.rest.line3", defaultConfig.getString("sign-colors.rest.line3", "&f")); + if (!config.contains("sign-colors.rest.line4")) config.set("sign-colors.rest.line4", defaultConfig.getString("sign-colors.rest.line4", "&1")); } - // ---------------------------------------------- - if (!config.contains("messages.no-chest-near-sign")) { - config.set("messages.no-chest-near-sign", defaultConfig.getString("messages.no-chest-near-sign", "&cKeine Truhe in der Nähe des Schildes!")); - getLogger().info("Setze Standardwert: messages.no-chest-near-sign"); - } - 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!")); - getLogger().info("Setze Standardwert: messages.no-item-in-hand"); - } - 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!")); - getLogger().info("Setze Standardwert: messages.not-your-chest"); - } - if (!config.contains("messages.input-chest-set")) { - config.set("messages.input-chest-set", defaultConfig.getString("messages.input-chest-set", "&aEingangstruhe erfolgreich gesetzt!")); - getLogger().info("Setze Standardwert: messages.input-chest-set"); - } - 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!")); - getLogger().info("Setze Standardwert: messages.target-chest-set"); - } - // --- NEU: Message für Rest-Truhe --- - if (!config.contains("messages.rest-chest-set")) { - config.set("messages.rest-chest-set", defaultConfig.getString("messages.rest-chest-set", "&aRest-Truhe (Fallback) erfolgreich gesetzt!")); - getLogger().info("Setze Standardwert: messages.rest-chest-set"); - } - // ---------------------------------- + if (!config.contains("messages.no-chest-near-sign")) config.set("messages.no-chest-near-sign", defaultConfig.getString("messages.no-chest-near-sign", "&cKeine Truhe in der Nähe des Schildes!")); + if (!config.contains("messages.no-item-in-hand")) config.set("messages.no-item-in-hand", defaultConfig.getString("messages.no-item-in-hand", "&cDu musst ein Item in der Hand halten!")); + if (!config.contains("messages.not-your-chest")) config.set("messages.not-your-chest", defaultConfig.getString("messages.not-your-chest", "&cDiese Truhe gehört dir nicht!")); + if (!config.contains("messages.input-chest-set")) config.set("messages.input-chest-set", defaultConfig.getString("messages.input-chest-set", "&aEingangstruhe erfolgreich gesetzt!")); + if (!config.contains("messages.target-chest-set")) config.set("messages.target-chest-set", defaultConfig.getString("messages.target-chest-set", "&aZieltruhe erfolgreich für %item% eingerichtet!")); + if (!config.contains("messages.rest-chest-set")) config.set("messages.rest-chest-set", defaultConfig.getString("messages.rest-chest-set", "&aRest-Truhe (Fallback) erfolgreich gesetzt!")); + if (!config.contains("messages.target-chest-missing")) config.set("messages.target-chest-missing", defaultConfig.getString("messages.target-chest-missing", "&cZieltruhe für %item% fehlt!")); - if (!config.contains("messages.target-chest-missing")) { - config.set("messages.target-chest-missing", defaultConfig.getString("messages.target-chest-missing", "&cZieltruhe für %item% fehlt!")); - getLogger().info("Setze Standardwert: messages.target-chest-missing"); - } 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); - getLogger().info("Setze oder aktualisiere Standardwert: messages.target-chest-full = " + defaultTargetChestFull); } - // --- NEUE NACHRICHTEN FÜR ÖFFENTLICHEN MODUS --- - if (!config.contains("messages.mode-changed")) { - config.set("messages.mode-changed", defaultConfig.getString("messages.mode-changed", "&aModus gewechselt: &e%mode%")); - getLogger().info("Setze Standardwert: messages.mode-changed"); - } - if (!config.contains("messages.mode-public")) { - config.set("messages.mode-public", defaultConfig.getString("messages.mode-public", "&aÖffentlich")); - getLogger().info("Setze Standardwert: messages.mode-public"); - } - if (!config.contains("messages.mode-private")) { - config.set("messages.mode-private", defaultConfig.getString("messages.mode-private", "&cPrivat")); - getLogger().info("Setze Standardwert: messages.mode-private"); - } - // ---------------------------------------------- + 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!")); - // HILFE UND INFO NICHT MEHR IN CONFIG SPEICHERN! - // Wir benutzen jetzt die statischen Strings HELP_DE, HELP_EN, INFO_DE, INFO_EN aus der Java-Datei. - - if (!config.contains("messages.no-permission")) { - config.set("messages.no-permission", defaultConfig.getString("messages.no-permission", "&cDu hast keine Berechtigung für diesen Befehl!")); - getLogger().info("Setze Standardwert: messages.no-permission"); - } - if (!config.contains("messages.reload-success")) { - config.set("messages.reload-success", defaultConfig.getString("messages.reload-success", "&aKonfiguration erfolgreich neu geladen!")); - getLogger().info("Setze Standardwert: messages.reload-success"); - } - 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!")); - getLogger().info("Setze Standardwert: messages.sign-break-denied"); - } - - // Speichere die aktualisierte Konfiguration try { config.save(configFile); - if (configFile.exists() && configFile.length() > 0) { - getLogger().info("config.yml erfolgreich aktualisiert und gespeichert auf Version " + CONFIG_VERSION); - } else { - getLogger().warning("config.yml wurde nicht korrekt gespeichert (Datei existiert nicht oder ist leer)"); - } } catch (IOException e) { getLogger().warning("Fehler beim Speichern der config.yml: " + e.getMessage()); } } - // --- ERWEITERUNG: Hilfsmethode zum Laden von Location aus einem beliebigen Pfad --- private Location getLocationFromPath(String path) { String worldName = playerData.getString(path + ".world"); World world = getServer().getWorld(worldName); @@ -549,16 +631,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { int z = playerData.getInt(path + ".z"); return new Location(world, x, y, z); } - // ----------------------------------------------------------------------- - // --- ERWEITERUNG: Methode zum Migrieren alter Daten --- private void migrateInputChests() { if (playerData.getConfigurationSection("players") == null) return; boolean migrated = false; - for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { String oldPath = "players." + uuidString + ".input-chest"; - // Prüfen, ob der alte Eintrag existiert if (playerData.contains(oldPath)) { try { UUID uuid = UUID.fromString(uuidString); @@ -567,12 +645,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { int y = playerData.getInt(oldPath + ".y"); int z = playerData.getInt(oldPath + ".z"); World w = Bukkit.getWorld(world); - if (w != null) { Location loc = new Location(w, x, y, z); - // Neue Methode aufrufen, die in die Liste speichert addInputChestLocation(uuid, loc); - // Alten Eintrag löschen playerData.set(oldPath, null); migrated = true; getLogger().info("Eingangstruhe für Spieler " + uuidString + " in neues System migriert."); @@ -582,38 +657,24 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } } - if (migrated) { - savePlayerData(); - } + if (migrated) savePlayerData(); } - // ----------------------------------------------------------------------- private void updateExistingSigns() { - if (playerData == null) { - getLogger().warning("playerData ist null. Kann bestehende Schilder nicht aktualisieren."); - return; - } - if (playerData.getConfigurationSection("players") == null) { - getLogger().warning("Abschnitt 'players' in players.yml fehlt. Keine Schilder zum Aktualisieren."); - return; - } + if (playerData == null) return; + if (playerData.getConfigurationSection("players") == null) return; for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { UUID playerUUID; try { playerUUID = UUID.fromString(uuidString); } catch (IllegalArgumentException e) { - getLogger().warning("Ungültige UUID in players.yml: " + uuidString); continue; } String basePath = "players." + uuidString; - - // --- ERWEITERUNG: Eingangstruhe (Jetzt Liste) --- String inputListPath = basePath + ".input-chests"; - // Fallback für altes Format (sollte durch Migration eigentlich weg sein, aber zur Sicherheit) String oldInputPath = basePath + ".input-chest"; - List inputLocs = new ArrayList<>(); if (playerData.contains(inputListPath)) { @@ -629,68 +690,44 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { for (Location chestLocation : inputLocs) { Block chestBlock = chestLocation.getBlock(); if (!(chestBlock.getState() instanceof Chest)) continue; - List blocks = getChestBlocks((Chest) chestBlock.getState()); for (Block b : blocks) { - for (Block face : new Block[] { - b.getRelative(1, 0, 0), - b.getRelative(-1, 0, 0), - b.getRelative(0, 0, 1), - b.getRelative(0, 0, -1) - }) { + 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((String) (lines[0] != null ? lines[0] : "")); - String line1 = ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")); - String line3 = ChatColor.stripColor((String) (lines[3] != null ? lines[3] : "")); + String line0 = ChatColor.stripColor(lines[0] != null ? lines[0] : ""); + String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); + String line3 = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); if (line0.equalsIgnoreCase("[asc]") && line1.equalsIgnoreCase("input")) { sign.setLine(0, getSignColor("input", "line1") + "[asc]"); sign.setLine(1, getSignColor("input", "line2") + "input"); sign.setLine(3, getSignColor("input", "line4") + line3); sign.update(); - if (isDebug()) { - getLogger().fine("Eingangsschild bei " + face.getLocation() + " für Spieler UUID " + uuidString + " aktualisiert"); - } } } } } } - // ---------------------------------------------- - // Zieltruhen String targetPath = basePath + ".target-chests"; if (playerData.contains(targetPath)) { for (String itemType : playerData.getConfigurationSection(targetPath).getKeys(false)) { String targetChestPath = targetPath + "." + itemType; Location chestLocation = getLocationFromPath(targetChestPath); if (chestLocation == null) continue; - Block chestBlock = chestLocation.getBlock(); - - if (!(chestBlock.getState() instanceof Chest)) { - getLogger().warning("Zieltruhe für Item " + itemType + " bei " + chestLocation + " ist keine Truhe"); - continue; - } - + if (!(chestBlock.getState() instanceof Chest)) continue; Chest chest = (Chest) chestBlock.getState(); - Inventory inventory = chest.getInventory(); - boolean isFull = isInventoryFull(inventory); - + 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((String) (lines[0] != null ? lines[0] : "")); - String line1 = ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")); - String line2 = ChatColor.stripColor((String) (lines[2] != null ? lines[2] : "")); - String line3 = ChatColor.stripColor((String) (lines[3] != null ? lines[3] : "")); + String line0 = ChatColor.stripColor(lines[0] != null ? lines[0] : ""); + String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); + String line2 = ChatColor.stripColor(lines[2] != null ? lines[2] : ""); + String line3 = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); if (line0.equalsIgnoreCase("[asc]") && line1.equalsIgnoreCase("ziel")) { String colorType = isFull ? "full" : "target"; sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); @@ -698,9 +735,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { sign.setLine(2, getSignColor(colorType, "line3") + line2); sign.setLine(3, getSignColor(colorType, "line4") + line3); sign.update(); - if (isDebug()) { - getLogger().fine("Zieltruhe-Schild für Item " + itemType + " bei " + face.getLocation() + " aktualisiert (voll: " + isFull + ")"); - } } } } @@ -708,67 +742,122 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // --- NEU: Rest-Truhe aktualisieren --- String restPath = basePath + ".rest-chest"; if (playerData.contains(restPath)) { Location chestLocation = getLocationFromPath(restPath); if (chestLocation == null) continue; - Block chestBlock = chestLocation.getBlock(); - - if (!(chestBlock.getState() instanceof Chest)) { - getLogger().warning("Rest-Truhe bei " + chestLocation + " ist keine Truhe"); - continue; - } - + if (!(chestBlock.getState() instanceof Chest)) continue; Chest chest = (Chest) chestBlock.getState(); - Inventory inventory = chest.getInventory(); - boolean isFull = isInventoryFull(inventory); - + 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((String) (lines[0] != null ? lines[0] : "")); - String line1 = ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")); - String line3 = ChatColor.stripColor((String) (lines[3] != null ? lines[3] : "")); + String line0 = ChatColor.stripColor(lines[0] != null ? lines[0] : ""); + String line1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); + String line3 = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); if (line0.equalsIgnoreCase("[asc]") && line1.equalsIgnoreCase("rest")) { String colorType = isFull ? "full" : "rest"; sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); sign.setLine(1, getSignColor(colorType, "line2") + "rest"); sign.setLine(3, getSignColor(colorType, "line4") + line3); sign.update(); - if (isDebug()) { - getLogger().fine("Rest-Truhe-Schild bei " + face.getLocation() + " für Spieler UUID " + uuidString + " aktualisiert"); - } } } } } } - // ------------------------------- } } private boolean isInventoryFull(Inventory inventory) { for (ItemStack item : inventory.getContents()) { - if (item == null || item.getAmount() < item.getMaxStackSize()) { - return false; - } + if (item == null || item.getAmount() < item.getMaxStackSize()) return false; } return true; } private boolean isChestPublic(Sign sign) { - String line3 = sign.getLine(3); // MIT Farbcodes! + String line3 = sign.getLine(3); String line3Clean = ChatColor.stripColor(line3); - return line3Clean.toLowerCase().contains("[public]"); + if (line3Clean.toLowerCase().contains("[public]")) return true; + + Block attachedBlock = null; + if (sign.getBlockData() instanceof WallSign wallSign) { + attachedBlock = sign.getBlock().getRelative(wallSign.getFacing().getOppositeFace()); + } + if (attachedBlock == null || !(attachedBlock.getState() instanceof Chest)) return false; + + Location loc = attachedBlock.getLocation(); + String world = loc.getWorld().getName(); + int x = loc.getBlockX(); + int y = loc.getBlockY(); + int z = loc.getBlockZ(); + + if (mysqlEnabled && mysqlManager != null) { + try (java.sql.PreparedStatement ps = mysqlManager.getConnection().prepareStatement( + "SELECT `public` FROM asc_input_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1")) { + ps.setString(1, world); ps.setInt(2, x); ps.setInt(3, y); ps.setInt(4, z); + java.sql.ResultSet rs = ps.executeQuery(); + if (rs.next() && rs.getBoolean("public")) return true; + } catch (Exception e) { getLogger().warning("isChestPublic input SQL: " + e.getMessage()); } + + try (java.sql.PreparedStatement ps = mysqlManager.getConnection().prepareStatement( + "SELECT `public` FROM asc_target_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1")) { + ps.setString(1, world); ps.setInt(2, x); ps.setInt(3, y); ps.setInt(4, z); + java.sql.ResultSet rs = ps.executeQuery(); + if (rs.next() && rs.getBoolean("public")) return true; + } catch (Exception e) { getLogger().warning("isChestPublic target SQL: " + e.getMessage()); } + + try (java.sql.PreparedStatement ps = mysqlManager.getConnection().prepareStatement( + "SELECT `public` FROM asc_rest_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1")) { + ps.setString(1, world); ps.setInt(2, x); ps.setInt(3, y); ps.setInt(4, z); + java.sql.ResultSet rs = ps.executeQuery(); + if (rs.next() && rs.getBoolean("public")) return true; + } catch (Exception e) { getLogger().warning("isChestPublic rest SQL: " + e.getMessage()); } + + } else { + if (playerData != null && playerData.contains("players")) { + for (String uuid : playerData.getConfigurationSection("players").getKeys(false)) { + String inputListPath = "players." + uuid + ".input-chests"; + if (playerData.contains(inputListPath)) { + for (String chestId : playerData.getConfigurationSection(inputListPath).getKeys(false)) { + String path = inputListPath + "." + chestId; + if (world.equals(playerData.getString(path + ".world")) + && x == playerData.getInt(path + ".x") + && y == playerData.getInt(path + ".y") + && z == playerData.getInt(path + ".z")) { + if (playerData.getBoolean(path + ".public", false)) return true; + } + } + } + String targetBase = "players." + uuid + ".target-chests"; + if (playerData.contains(targetBase)) { + for (String item : playerData.getConfigurationSection(targetBase).getKeys(false)) { + String tPath = targetBase + "." + item; + if (world.equals(playerData.getString(tPath + ".world")) + && x == playerData.getInt(tPath + ".x") + && y == playerData.getInt(tPath + ".y") + && z == playerData.getInt(tPath + ".z")) { + if (playerData.getBoolean(tPath + ".public", false)) return true; + } + } + } + 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; + } + } + } + } + } + return false; } private boolean isDebug() { @@ -776,110 +865,159 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } private String getMessage(String key) { - if (config == null) { - return ChatColor.RED + "Fehlende Konfiguration: " + key; - } + if (config == null) return ChatColor.RED + "Fehlende Konfiguration: " + key; String message = config.getString("messages." + key, "Fehlende Nachricht: " + key); return ChatColor.translateAlternateColorCodes('&', message); } private String getSignColor(String type, String line) { - if (config == null) { - return "&f"; - } + if (config == null) return "&f"; String color = config.getString("sign-colors." + type + "." + line, "&f"); return ChatColor.translateAlternateColorCodes('&', color); } private boolean isSignAttachedToChest(Block signBlock, Block chestBlock) { - if (!(signBlock.getBlockData() instanceof WallSign)) { - if (isDebug()) getLogger().fine("Schild bei " + signBlock.getLocation() + " ist kein Wandschild"); - return false; - } + if (!(signBlock.getBlockData() instanceof WallSign)) return false; WallSign wallSign = (WallSign) signBlock.getBlockData(); Block attachedBlock = signBlock.getRelative(wallSign.getFacing().getOppositeFace()); - boolean attached = attachedBlock.equals(chestBlock); - if (!attached && isDebug()) { - getLogger().fine("Schild bei " + signBlock.getLocation() + " ist nicht an Truhe bei " + chestBlock.getLocation() + " angebracht"); - } - return attached; + return attachedBlock.equals(chestBlock); } - // --- ERWEITERUNG: Methode zum Hinzufügen einer Input Chest (Liste) --- private void addInputChestLocation(UUID playerUUID, Location location) { String basePath = "players." + playerUUID + ".input-chests"; - String id = UUID.randomUUID().toString(); // Eindeutige ID für jede Truhe + 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(); } - // Original-Methode als Wrapper beibehalten (damit onSignChange nicht geändert werden muss) private void setInputChestLocation(UUID playerUUID, Location location) { - addInputChestLocation(playerUUID, location); + setInputChestLocation(playerUUID, location, false); + } + + private void setInputChestLocation(UUID playerUUID, Location location, boolean isPublic) { + if (mysqlEnabled && mysqlManager != null) { + String chestId = null; + List> chests = mysqlManager.getInputChests(playerUUID.toString()); + for (Map chest : chests) { + if (chest.get("world").equals(location.getWorld().getName()) + && ((int) chest.get("x")) == location.getBlockX() + && ((int) chest.get("y")) == location.getBlockY() + && ((int) chest.get("z")) == location.getBlockZ()) { + chestId = (String) chest.get("chest_id"); + break; + } + } + if (chestId == null) chestId = UUID.randomUUID().toString(); + mysqlManager.addInputChest(playerUUID.toString(), chestId, + location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic); + mysqlManager.savePlayer(playerUUID.toString(), Bukkit.getOfflinePlayer(playerUUID).getName()); + } else { + String basePath = "players." + playerUUID + ".input-chests"; + if (playerData.contains(basePath)) { + for (String chestId : playerData.getConfigurationSection(basePath).getKeys(false)) { + String path = basePath + "." + chestId; + 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; + } + } + } + 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", isPublic); + savePlayerData(); + } } - // ----------------------------------------------------------------- private void setTargetChestLocation(UUID playerUUID, Location location, Material itemType) { - 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()); - savePlayerData(); - getLogger().info("Zieltruhe für " + getServer().getPlayer(playerUUID).getName() + " (" + itemType.name() + ") gesetzt bei " + location); + setTargetChestLocation(playerUUID, location, itemType, false); + } + + private void setTargetChestLocation(UUID playerUUID, Location location, Material itemType, boolean isPublic) { + if (itemType == null || itemType == Material.AIR) { + getLogger().warning("setTargetChestLocation: Kein gültiges Item (AIR) – Zieltruhe wird nicht angelegt!"); + return; + } + if (mysqlEnabled && mysqlManager != null) { + mysqlManager.setTargetChest(playerUUID.toString(), itemType.name(), + location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic); + 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 + ".public", isPublic); + savePlayerData(); + } } - // --- NEU: Methoden für Rest-Truhe --- private void setRestChestLocation(UUID playerUUID, Location location) { - 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()); - savePlayerData(); - getLogger().info("Rest-Truhe für " + getServer().getPlayer(playerUUID).getName() + " gesetzt bei " + location); + setRestChestLocation(playerUUID, location, false); + } + + private void setRestChestLocation(UUID playerUUID, Location location, boolean isPublic) { + if (mysqlEnabled && mysqlManager != null) { + mysqlManager.setRestChest(playerUUID.toString(), + location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), isPublic); + 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 + ".public", isPublic); + savePlayerData(); + } } private Location getRestChestLocation(UUID playerUUID) { - String path = "players." + playerUUID + ".rest-chest"; - if (!playerData.contains(path)) { - return null; + if (mysqlEnabled && mysqlManager != null) { + Map map = mysqlManager.getRestChest(playerUUID.toString()); + if (map == null) return null; + World w = Bukkit.getWorld((String) map.get("world")); + if (w == null) return null; + return new Location(w, (int) map.get("x"), (int) map.get("y"), (int) map.get("z")); + } else { + String path = "players." + playerUUID + ".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")); } - String worldName = playerData.getString(path + ".world"); - World world = getServer().getWorld(worldName); - if (world == null) return null; - - int x = playerData.getInt(path + ".x"); - int y = playerData.getInt(path + ".y"); - int z = playerData.getInt(path + ".z"); - return new Location(world, x, y, z); } - // ---------------------------------- private Location getTargetChestLocation(UUID playerUUID, Material itemType) { - String path = "players." + playerUUID + ".target-chests." + itemType.name(); - if (!playerData.contains(path)) { - if (isDebug()) getLogger().fine("Keine Zieltruhe für Item " + itemType.name() + " für Spieler UUID " + playerUUID); - return null; + if (mysqlEnabled && mysqlManager != null) { + Map map = mysqlManager.getTargetChest(playerUUID.toString(), itemType.name()); + if (map == null) return null; + World w = Bukkit.getWorld((String) map.get("world")); + if (w == null) return null; + return new Location(w, (int) map.get("x"), (int) map.get("y"), (int) map.get("z")); + } else { + String path = "players." + playerUUID + ".target-chests." + itemType.name(); + if (!playerData.contains(path)) return null; + String worldName = playerData.getString(path + ".world"); + World world = getServer().getWorld(worldName); + if (world == null) return null; + return new Location(world, playerData.getInt(path + ".x"), playerData.getInt(path + ".y"), playerData.getInt(path + ".z")); } - - String worldName = playerData.getString(path + ".world"); - World world = getServer().getWorld(worldName); - if (world == null) { - getLogger().warning("Welt " + worldName + " für Zieltruhe von Item " + itemType.name() + " nicht gefunden"); - return null; - } - - int x = playerData.getInt(path + ".x"); - int y = playerData.getInt(path + ".y"); - int z = playerData.getInt(path + ".z"); - - return new Location(world, x, y, z); } private void cleanMessageTracker() { @@ -895,7 +1033,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { Map playerMessages = fullChestMessageTracker.computeIfAbsent(playerUUID, k -> new HashMap<>()); Long lastMessageTime = playerMessages.get(material); long currentTime = System.currentTimeMillis(); - if (lastMessageTime == null || currentTime - lastMessageTime > MESSAGE_COOLDOWN) { playerMessages.put(material, currentTime); return true; @@ -903,133 +1040,385 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return false; } - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!command.getName().equalsIgnoreCase("asc")) return false; + @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"); + if (lang == null) lang = "de"; - if (!(sender instanceof Player)) { - sender.sendMessage(ChatColor.RED + "Dieser Befehl ist nur für Spieler!"); + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + String helpMessage = lang.equalsIgnoreCase("en") ? HELP_EN : HELP_DE; + helpMessage = ChatColor.translateAlternateColorCodes('&', helpMessage); + player.sendMessage(helpMessage.split("\n")); + return true; + } + + if (args[0].equalsIgnoreCase("info")) { + String infoMessage = lang.equalsIgnoreCase("en") ? INFO_EN : INFO_DE; + infoMessage = infoMessage + .replace("%version%", getDescription().getVersion()) + .replace("%config_version%", config != null ? config.getString("version", CONFIG_VERSION) : CONFIG_VERSION) + .replace("%author%", String.join(", ", getDescription().getAuthors())); + infoMessage = ChatColor.translateAlternateColorCodes('&', infoMessage); + player.sendMessage(infoMessage.split("\n")); + return true; + } + + if (args[0].equalsIgnoreCase("reload")) { + if (!player.hasPermission("autosortchest.reload")) { + player.sendMessage(getMessage("no-permission")); return true; } - Player player = (Player) sender; - - // --- NEU: Sprache aus Config lesen --- - String lang = config.getString("language", "de"); // Standard Deutsch - if (lang == null) lang = "de"; + if (sortTask != null) { sortTask.cancel(); sortTask = null; } + if (cleanTask != null) { cleanTask.cancel(); cleanTask = null; } - if (args.length == 0 || args[0].equalsIgnoreCase("help")) { - // Sprache prüfen und statischen String wählen - String helpMessage = lang.equalsIgnoreCase("en") ? HELP_EN : HELP_DE; - - // --- FIX: Farben übersetzen (&6 -> §6) --- - // Bukkit versteht & nicht als Farbe, wir müssen es umwandeln - helpMessage = ChatColor.translateAlternateColorCodes('&', helpMessage); - // ---------------------------------------- + reloadConfig(); + config = getConfig(); + loadOptionalSettings(); - player.sendMessage(helpMessage.split("\n")); - return true; - } - - if (args[0].equalsIgnoreCase("info")) { - // Sprache prüfen und statischen String wählen - String infoMessage = lang.equalsIgnoreCase("en") ? INFO_EN : INFO_DE; - - // Platzhalter ersetzen - infoMessage = infoMessage - .replace("%version%", getDescription().getVersion()) - .replace("%config_version%", config != null ? config.getString("version", CONFIG_VERSION) : CONFIG_VERSION) - .replace("%author%", String.join(", ", getDescription().getAuthors())); - - // --- FIX: Farben übersetzen (&6 -> §6) --- - infoMessage = ChatColor.translateAlternateColorCodes('&', infoMessage); - // ---------------------------------------- - - player.sendMessage(infoMessage.split("\n")); - return true; - } - - if (args[0].equalsIgnoreCase("reload")) { - if (!player.hasPermission("autosortchest.reload")) { - player.sendMessage(getMessage("no-permission")); - return true; + if (mysqlEnabled) { + if (mysqlManager != null) { + mysqlManager.disconnect(); + mysqlManager = null; } - reloadConfig(); - config = getConfig(); - loadPlayerData(); - updateConfig(); - updateExistingSigns(); - migrateInputChests(); - player.sendMessage(getMessage("reload-success")); - getLogger().info("Konfiguration und Spielerdaten neu geladen durch " + player.getName()); + mysqlManager = new MySQLManager(mysqlHost, mysqlPort, mysqlDatabase, mysqlUser, mysqlPassword); + if (mysqlManager.connect()) { + mysqlManager.setupTables(); + getLogger().info("MySQL-Verbindung nach Reload erfolgreich hergestellt."); + } else { + getLogger().warning("MySQL-Verbindung nach Reload fehlgeschlagen! Fallback auf YAML."); + mysqlEnabled = false; + mysqlManager = null; + } + } else { + if (mysqlManager != null) { + mysqlManager.disconnect(); + mysqlManager = null; + } + } + + loadPlayerData(); + updateConfig(); + updateExistingSigns(); + migrateInputChests(); + startTasks(); + + player.sendMessage(getMessage("reload-success")); + getLogger().info("Plugin erfolgreich neu geladen von " + player.getName() + + " (sort_interval_ticks=" + sortIntervalTicks + ")"); + return true; + } + + // ------------------------------------------------------- + // /asc import – YAML → MySQL + // ------------------------------------------------------- + if (args[0].equalsIgnoreCase("import")) { + if (!player.hasPermission("autosortchest.import")) { + player.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."); + 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!"); return true; } - // Default: Hilfe zeigen + player.sendMessage(ChatColor.YELLOW + "Importiere Daten aus players.yml nach MySQL..."); + player.sendMessage(ChatColor.GRAY + "Bestehende MySQL-Daten werden nicht überschrieben (REPLACE INTO)."); + + new BukkitRunnable() { + @Override + public void run() { + int playerCount = 0; + int inputCount = 0; + int targetCount = 0; + int restCount = 0; + + for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { + String name = uuidString; + try { + UUID uuid = UUID.fromString(uuidString); + OfflinePlayer offline = getServer().getOfflinePlayer(uuid); + if (offline != null && offline.getName() != null) name = offline.getName(); + } catch (Exception ignored) {} + + 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"); + int x = playerData.getInt(path + ".x"); + int y = playerData.getInt(path + ".y"); + int z = playerData.getInt(path + ".z"); + boolean isPublic = playerData.getBoolean(path + ".public", false); + mysqlManager.addInputChest(uuidString, chestId, world, x, y, z, isPublic); + inputCount++; + } + } + + // 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"); + int x = playerData.getInt(path + ".x"); + int y = playerData.getInt(path + ".y"); + int z = playerData.getInt(path + ".z"); + boolean isPublic = playerData.getBoolean(path + ".public", false); + mysqlManager.setTargetChest(uuidString, item, world, x, y, z, isPublic); + targetCount++; + } + } + + // Rest-Chest + 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: " + + fp + " Spieler, " + fi + " Input, " + ft + " Target, " + fr + " Rest."); + }); + } + }.runTaskAsynchronously(this); + + return true; + } + + // ------------------------------------------------------- + // /asc export – MySQL → YAML + // ------------------------------------------------------- + if (args[0].equalsIgnoreCase("export")) { + if (!player.hasPermission("autosortchest.export")) { + player.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."); + 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."); + + 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 + && playerData.getConfigurationSection("players") != null + && !playerData.getConfigurationSection("players").getKeys(false).isEmpty(); + + if (hasYamlData) { + backupName = "players_backup_" + System.currentTimeMillis() + ".yml"; + File backupFile = new File(getDataFolder(), backupName); + try { + 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())); + return; + } + } + + // Neue YAML-Struktur aufbauen + FileConfiguration exportData = new YamlConfiguration(); + exportData.createSection("players"); + + int playerCount = 0; + int inputCount = 0; + int targetCount = 0; + int restCount = 0; + + try { + 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 + ".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 + ".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 + ".public", restChest.get("public")); + restCount++; + } + } + + exportData.save(playerDataFile); + + final FileConfiguration finalExport = exportData; + final int fp = playerCount, fi = inputCount, ft = targetCount, fr = restCount; + final String finalBackupName = backupName; + + Bukkit.getScheduler().runTask(Main.this, () -> { + playerData = finalExport; + player.sendMessage(ChatColor.GREEN + "Export erfolgreich abgeschlossen!"); + if (finalBackupName != null) { + player.sendMessage(ChatColor.GRAY + " Backup: " + ChatColor.WHITE + finalBackupName); + } 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 + " 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: " + + fp + " Spieler, " + fi + " Input, " + ft + " Target, " + fr + " Rest." + + (finalBackupName != null ? " Backup: " + finalBackupName : " Kein Backup (players.yml war leer).")); + }); + + } catch (Exception e) { + Bukkit.getScheduler().runTask(Main.this, () -> + player.sendMessage(ChatColor.RED + "Export fehlgeschlagen: " + e.getMessage())); + getLogger().warning("Export fehlgeschlagen: " + e.getMessage()); + } + } + }.runTaskAsynchronously(this); + + return true; + } + String helpMessage = lang.equalsIgnoreCase("en") ? HELP_EN : HELP_DE; helpMessage = ChatColor.translateAlternateColorCodes('&', helpMessage); player.sendMessage(helpMessage.split("\n")); return true; } - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + @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(); - // --- NEU: Sign Change für Rest-Truhe --- - if (lines.length >= 2 && lines[0].equalsIgnoreCase("[asc]") && lines[1].equalsIgnoreCase("rest")) { - event.setCancelled(false); // <- HIER: WorldGuard Cancel rückgängig machen + World world = player.getWorld(); + if (worldBlacklist != null && worldBlacklist.contains(world.getName())) { + event.setCancelled(true); + player.sendMessage(ChatColor.RED + "In dieser Welt kannst du keine AutoSortChest erstellen!"); + return; + } + if (lines.length >= 2 && lines[0].equalsIgnoreCase("[asc]") && lines[1].equalsIgnoreCase("rest")) { + event.setCancelled(false); Block chestBlock = null; if (signBlock.getBlockData() instanceof WallSign wallSign) { Block attachedBlock = signBlock.getRelative(wallSign.getFacing().getOppositeFace()); - if (attachedBlock.getState() instanceof Chest) { - chestBlock = attachedBlock; - } + if (attachedBlock.getState() instanceof Chest) chestBlock = attachedBlock; } - if (chestBlock == null) { player.sendMessage(getMessage("no-chest-near-sign")); - getLogger().warning("Keine Truhe an Schild bei " + signBlock.getLocation() + " für Spieler " + player.getName()); return; } - event.setLine(0, getSignColor("rest", "line1") + "[asc]"); event.setLine(1, getSignColor("rest", "line2") + "rest"); event.setLine(3, getSignColor("rest", "line4") + player.getName()); setRestChestLocation(playerUUID, chestBlock.getLocation()); player.sendMessage(getMessage("rest-chest-set")); - getLogger().info("Rest-Truhe für " + player.getName() + " gesetzt bei " + chestBlock.getLocation()); return; } - // ------------------------------- if (lines.length >= 2 && lines[0].equalsIgnoreCase("[asc]") && lines[1].equalsIgnoreCase("input")) { - event.setCancelled(false); // <- HIER: WorldGuard Cancel rückgängig machen - + event.setCancelled(false); Block chestBlock = null; if (signBlock.getBlockData() instanceof WallSign wallSign) { Block attachedBlock = signBlock.getRelative(wallSign.getFacing().getOppositeFace()); - if (attachedBlock.getState() instanceof Chest) { - chestBlock = attachedBlock; - } + if (attachedBlock.getState() instanceof Chest) chestBlock = attachedBlock; } - if (chestBlock == null) { player.sendMessage(getMessage("no-chest-near-sign")); - getLogger().warning("Keine Truhe an Schild bei " + signBlock.getLocation() + " für Spieler " + player.getName()); return; } - event.setLine(0, getSignColor("input", "line1") + "[asc]"); event.setLine(1, getSignColor("input", "line2") + "input"); - event.setLine(3, getSignColor("input", "line4") + player.getName()); - setInputChestLocation(playerUUID, chestBlock.getLocation()); + boolean isPublic = false; + if (lines.length >= 4 && lines[3] != null && ChatColor.stripColor(lines[3]).toLowerCase().contains("[public]")) { + isPublic = true; + event.setLine(3, getSignColor("input", "line4") + player.getName() + " [Public]"); + } else { + event.setLine(3, getSignColor("input", "line4") + player.getName()); + } + setInputChestLocation(playerUUID, chestBlock.getLocation(), isPublic); player.sendMessage(getMessage("input-chest-set")); - getLogger().info("Eingangstruhe für " + player.getName() + " gesetzt bei " + chestBlock.getLocation()); + return; + } + + if (lines.length >= 2 && lines[0].equalsIgnoreCase("[asc]") && lines[1].equalsIgnoreCase("ziel")) { + event.setCancelled(false); + Block chestBlock = null; + if (signBlock.getBlockData() instanceof WallSign wallSign) { + Block attachedBlock = signBlock.getRelative(wallSign.getFacing().getOppositeFace()); + if (attachedBlock.getState() instanceof Chest) chestBlock = attachedBlock; + } + if (chestBlock == null) { + player.sendMessage(getMessage("no-chest-near-sign")); + return; + } + event.setLine(0, "[asc]"); + event.setLine(1, "ziel"); + event.setLine(2, ""); + event.setLine(3, ""); } } @@ -1044,59 +1433,57 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (clickedBlock == null) return; - // Schild-Interaktion if (clickedBlock.getState() instanceof Sign sign) { String[] lines = sign.getLines(); - if (lines.length >= 2 && ChatColor.stripColor((String) (lines[0] != null ? lines[0] : "")).equalsIgnoreCase("[asc]")) { + if (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]")) { - // Truhe finden Block chestBlock = null; if (sign.getBlockData() instanceof WallSign wallSign) { Block attachedBlock = sign.getBlock().getRelative(wallSign.getFacing().getOppositeFace()); - if (attachedBlock.getState() instanceof Chest) { - chestBlock = attachedBlock; - } + if (attachedBlock.getState() instanceof Chest) chestBlock = attachedBlock; } - if (chestBlock == null) { player.sendMessage(getMessage("no-chest-near-sign")); event.setCancelled(true); return; } - String line1Clean = ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")); + String line1Clean = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); boolean isTargetOrRest = line1Clean.equalsIgnoreCase("ziel") || line1Clean.equalsIgnoreCase("rest"); - // --- LOGIK FÜR ZIELTRUHEN (ZIEL) UND REST-TRUHEN --- if (isTargetOrRest) { 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()); - // 1. FALL: MODUS WECHSELN (Shift + Klick + Leere hand) if (player.isSneaking() && (itemInHand == null || itemInHand.getType() == Material.AIR)) { - // Admin Check hinzugefügt if (isOwner || isAdmin(player)) { - boolean isPublic = isChestPublic(sign); + boolean isPublicNow = isChestPublic(sign); + boolean newPublic = !isPublicNow; String newModeText; String newLine4; - String colorType = line1Clean.equalsIgnoreCase("rest") ? "rest" : "target"; - if (isPublic) { - // Wechsel zu Privat + if (isPublicNow) { newModeText = getMessage("mode-private"); newLine4 = getSignColor(colorType, "line4") + pureOwnerName; } else { - // Wechsel zu Öffentlich newModeText = getMessage("mode-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); + } + } else if (line1Clean.equalsIgnoreCase("rest")) { + setRestChestLocation(playerUUID, chestBlock.getLocation(), newPublic); + } player.sendMessage(getMessage("mode-changed").replace("%mode%", newModeText)); event.setCancelled(true); return; @@ -1107,36 +1494,80 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // 2. FALL: ITEM ZUWEISEN / AKTUALISIEREN (Nur bei ZIEL, nicht bei REST) if (line1Clean.equalsIgnoreCase("ziel")) { if (itemInHand != null && itemInHand.getType() != Material.AIR) { - // Admin Check hinzugefügt für Modifikation + 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"); + World w = Bukkit.getWorld(worldName); + if (w != null && w.getBlockAt(x, y, z).getState() instanceof Chest) { + uniqueChestLocations.add(worldName + ":" + x + ":" + y + ":" + z); + } + } + } + } + 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 (!pureOwnerName.isEmpty() && !pureOwnerName.equalsIgnoreCase("Unknown") && !isOwner && !isAdmin(player)) { player.sendMessage(getMessage("not-your-chest")); event.setCancelled(true); return; } - - // --- FIX: Alten Eintrag für diese Truhe löschen --- - removeOldTargetEntry(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType().name()); - // ------------------------------------------------ + if (!mysqlEnabled) { + String basePath = "players." + playerUUID + ".target-chests"; + if (playerData.contains(basePath)) { + for (String item : new HashSet<>(playerData.getConfigurationSection(basePath).getKeys(false))) { + String path = basePath + "." + item; + String worldName = playerData.getString(path + ".world"); + int x = playerData.getInt(path + ".x"); int y = playerData.getInt(path + ".y"); int z = playerData.getInt(path + ".z"); + if (worldName != null && worldName.equals(chestBlock.getWorld().getName()) && x == chestBlock.getX() && y == chestBlock.getY() && z == chestBlock.getZ()) { + playerData.set(path, null); + } + } + } + String path = basePath + "." + itemInHand.getType().name(); + playerData.set(path + ".world", chestBlock.getWorld().getName()); + playerData.set(path + ".x", chestBlock.getX()); + playerData.set(path + ".y", chestBlock.getY()); + playerData.set(path + ".z", chestBlock.getZ()); + playerData.set(path + ".public", false); + savePlayerData(); + } + if (mysqlEnabled) removeOldTargetEntry(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType().name()); Chest chest = (Chest) chestBlock.getState(); boolean isFull = isInventoryFull(chest.getInventory()); String colorType = isFull ? "full" : "target"; - sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); sign.setLine(1, getSignColor(colorType, "line2") + "ziel"); - sign.setLine(2, getSignColor(colorType, "line3") + itemInHand.getType().name()); - + sign.setLine(2, getSignColor(colorType, "line3") + itemInHand.getType().name()); String finalLine4 = line3Raw; if (pureOwnerName.isEmpty() || pureOwnerName.equalsIgnoreCase("Unknown")) { finalLine4 = getSignColor("target", "line4") + player.getName(); } - sign.setLine(3, finalLine4); sign.update(); - setTargetChestLocation(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType()); player.sendMessage(getMessage("target-chest-set").replace("%item%", itemInHand.getType().name())); event.setCancelled(true); @@ -1144,48 +1575,40 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // Zugriffsschutz für konfigurierte Truhen (wenn kein Item in Hand zum Updaten) - // Wenn nicht Owner und NICHT öffentlich UND NICHT Admin -> Zugriff verweigern if (!isOwner && !isChestPublic(sign) && !isAdmin(player)) { player.sendMessage(getMessage("not-your-chest")); event.setCancelled(true); return; } - - // --- FIX: Zugriff erlaubt -> Event canceln und Truhe öffnen --- - // Dies verhindert das Öffnen des Schild-Editors und erzwingt das Öffnen der Truhe event.setCancelled(true); if (chestBlock.getState() instanceof Chest) { player.openInventory(((Chest) chestBlock.getState()).getInventory()); } return; - // ----------------------------------------------------------- } - // --- LOGIK FÜR EINGANGSTRUHEN (INPUT) --- if (line1Clean.equalsIgnoreCase("input")) { String line3Raw = lines[3] != null ? lines[3] : ""; String line3Clean = ChatColor.stripColor(line3Raw); String pureOwnerName = line3Clean.replace("[Public]", "").replace("[public]", "").trim(); boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); - // 1. FALL: MODUS WECHSELN (Shift + Klick + Leere Hand) if (player.isSneaking() && (itemInHand == null || itemInHand.getType() == Material.AIR)) { if (isOwner || isAdmin(player)) { - boolean isPublic = isChestPublic(sign); + boolean isPublicNow = isChestPublic(sign); + boolean newPublic = !isPublicNow; String newModeText; String newLine4; - - if (isPublic) { + if (isPublicNow) { newModeText = getMessage("mode-private"); newLine4 = getSignColor("input", "line4") + pureOwnerName; } else { newModeText = getMessage("mode-public"); newLine4 = getSignColor("input", "line4") + pureOwnerName + " " + ChatColor.RESET + "[Public]"; } - sign.setLine(3, newLine4); sign.update(); + setInputChestLocation(playerUUID, chestBlock.getLocation(), newPublic); player.sendMessage(getMessage("mode-changed").replace("%mode%", newModeText)); event.setCancelled(true); return; @@ -1195,47 +1618,34 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return; } } - - // Zugriffsschutz - Admin Check hinzugefügt if (!isOwner && !isChestPublic(sign) && !isAdmin(player)) { player.sendMessage(getMessage("not-your-chest")); event.setCancelled(true); return; } - - // --- FIX: Zugriff erlaubt -> Event canceln und Truhe öffnen --- event.setCancelled(true); if (chestBlock.getState() instanceof Chest) { player.openInventory(((Chest) chestBlock.getState()).getInventory()); } return; - // ----------------------------------------------------------- } } return; } - // Truhe-Interaktion (wenn man direkt auf die Truhe klickt und nicht auf das Schild) if (clickedBlock.getState() instanceof Chest) { Block chestBlock = clickedBlock; Block signBlock = null; - - // Suche Schild an beiden Hälften falls Doppeltruhe 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((String) (lines[0] != null ? lines[0] : "")).equalsIgnoreCase("[asc]")) { + if (lines.length >= 2 && ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]")) { signBlock = face; - break outerLoop; + break outerLoop; } } } @@ -1244,35 +1654,41 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (signBlock != null) { Sign sign = (Sign) signBlock.getState(); String[] lines = sign.getLines(); - String line1Clean = ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")); + String line1Clean = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); - // NUR FÜR ZIELTRUHEN und REST (hier nur Moduswechsel und Item zuweisen) if (line1Clean.equalsIgnoreCase("ziel") || line1Clean.equalsIgnoreCase("rest")) { String line3Raw = lines[3] != null ? lines[3] : ""; String line3Clean = ChatColor.stripColor(line3Raw); - String pureOwnerName = line3Clean.replace("[public]", "").replace("[public]", "").trim(); + String pureOwnerName = line3Clean.replace("[public]", "").replace("[Public]", "").trim(); boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); - // 1. FALL: MODUS WECHSELN if (player.isSneaking() && (itemInHand == null || itemInHand.getType() == Material.AIR)) { if (isOwner || isAdmin(player)) { - boolean isPublic = isChestPublic(sign); + boolean isPublicNow = isChestPublic(sign); + boolean newPublic = !isPublicNow; String newModeText; String newLine4; - String colorType = line1Clean.equalsIgnoreCase("rest") ? "rest" : "target"; String baseName = getSignColor(colorType, "line4") + pureOwnerName; - - if (isPublic) { + if (isPublicNow) { newModeText = getMessage("mode-private"); newLine4 = baseName; } else { newModeText = getMessage("mode-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); + } + } else { + setRestChestLocation(playerUUID, chestBlock.getLocation(), newPublic); + } player.sendMessage(getMessage("mode-changed").replace("%mode%", newModeText)); event.setCancelled(true); return; @@ -1283,43 +1699,32 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // 2. FALL: ITEM ZUWEISEN (Nur bei ZIEL, nicht bei REST, und nur wenn Zeile 2 leer ist) if (line1Clean.equalsIgnoreCase("ziel")) { - String line2Clean = ChatColor.stripColor((String) (lines[2] != null ? lines[2] : "")); + String line2Clean = ChatColor.stripColor(lines[2] != null ? lines[2] : ""); if (line2Clean.isEmpty()) { if (itemInHand == null || itemInHand.getType() == Material.AIR) { player.sendMessage(getMessage("no-item-in-hand")); event.setCancelled(true); return; } - - // Admin Check für Modifikation if (!pureOwnerName.isEmpty() && !pureOwnerName.equalsIgnoreCase("Unknown") && !isOwner && !isAdmin(player)) { player.sendMessage(getMessage("not-your-chest")); event.setCancelled(true); return; } - - // --- FIX: Alten Eintrag auch beim Klick auf die Truhe löschen --- removeOldTargetEntry(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType().name()); - // ----------------------------------------------------------- - Chest chest = (Chest) chestBlock.getState(); boolean isFull = isInventoryFull(chest.getInventory()); String colorType = isFull ? "full" : "target"; - sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); sign.setLine(1, getSignColor(colorType, "line2") + "ziel"); sign.setLine(2, getSignColor(colorType, "line3") + itemInHand.getType().name()); - String finalLine4 = line3Raw; if (pureOwnerName.isEmpty() || pureOwnerName.equalsIgnoreCase("Unknown")) { finalLine4 = getSignColor("target", "line4") + player.getName(); } - sign.setLine(3, finalLine4); sign.update(); - setTargetChestLocation(player.getUniqueId(), chestBlock.getLocation(), itemInHand.getType()); player.sendMessage(getMessage("target-chest-set").replace("%item%", itemInHand.getType().name())); event.setCancelled(true); @@ -1327,32 +1732,27 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // Wenn nicht Owner und nicht öffentlich und nicht Admin -> Zugriff verweigern if (!isOwner && !isChestPublic(sign) && !isAdmin(player)) { player.sendMessage(getMessage("not-your-chest")); event.setCancelled(true); return; } - // Wenn erlaubt, machen wir nichts, damit die Truhe ganz normal geöffnet wird } - // FALL: EINGANGSTRUHE if (line1Clean.equalsIgnoreCase("input")) { String line3Raw = lines[3] != null ? lines[3] : ""; String line3Clean = ChatColor.stripColor(line3Raw); String pureOwnerName = line3Clean.replace("[Public]", "").replace("[public]", "").trim(); boolean isOwner = pureOwnerName.equalsIgnoreCase(player.getName()); - // Moduswechsel if (player.isSneaking() && (itemInHand == null || itemInHand.getType() == Material.AIR)) { if (isOwner || isAdmin(player)) { - boolean isPublic = isChestPublic(sign); + boolean isPublicNow = isChestPublic(sign); + boolean newPublic = !isPublicNow; String newModeText; String newLine4; - String baseName = getSignColor("input", "line4") + pureOwnerName; - - if (isPublic) { + if (isPublicNow) { newModeText = getMessage("mode-private"); newLine4 = baseName; } else { @@ -1361,6 +1761,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } sign.setLine(3, newLine4); sign.update(); + setInputChestLocation(playerUUID, chestBlock.getLocation(), newPublic); player.sendMessage(getMessage("mode-changed").replace("%mode%", newModeText)); event.setCancelled(true); return; @@ -1370,25 +1771,20 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return; } } - - // Admin Check für Zugriff if (!isOwner && !isChestPublic(sign) && !isAdmin(player)) { player.sendMessage(getMessage("not-your-chest")); event.setCancelled(true); return; } - // Wenn erlaubt, lassen wir die Truhe ganz normal öffnen } } } } - // --- ERWEITERUNG: Hilfsmethode zum Entfernen einer Input Chest --- private void removeInputChestByLocation(UUID uuid, Location loc) { String basePath = "players." + uuid + ".input-chests"; String oldPath = "players." + uuid + ".input-chest"; - - // Prüfe neues Listen-Format + boolean removed = false; if (playerData.contains(basePath)) { for (String chestId : playerData.getConfigurationSection(basePath).getKeys(false)) { String path = basePath + "." + chestId; @@ -1396,22 +1792,35 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (savedLoc != null && savedLoc.equals(loc)) { playerData.set(path, null); savePlayerData(); - if (isDebug()) getLogger().info("Eingangstruhe entfernt bei " + loc); + removed = true; + if (mysqlEnabled && mysqlManager != null) { + mysqlManager.removeInputChest(uuid.toString(), chestId); + } return; } } } - - // Prüfe altes Einzel-Format (Fallback) if (playerData.contains(oldPath)) { Location oldLoc = getLocationFromPath(oldPath); if (oldLoc != null && oldLoc.equals(loc)) { playerData.set(oldPath, null); savePlayerData(); + removed = true; + } + } + if (!removed && mysqlEnabled && mysqlManager != null) { + List> chests = mysqlManager.getInputChests(uuid.toString()); + for (Map chest : chests) { + if (chest != null + && loc.getWorld().getName().equals(chest.get("world")) + && loc.getBlockX() == (int) chest.get("x") + && loc.getBlockY() == (int) chest.get("y") + && loc.getBlockZ() == (int) chest.get("z")) { + mysqlManager.removeInputChest(uuid.toString(), (String) chest.get("chest_id")); + } } } } - // --------------------------------------------------------------- @EventHandler(priority = EventPriority.HIGHEST) public void onBlockBreak(BlockBreakEvent event) { @@ -1421,50 +1830,31 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { String signOwner = ""; boolean isAscChest = false; - if (isDebug()) { - getLogger().fine("BlockBreakEvent ausgelöst bei " + block.getLocation() + " durch Spieler " + player.getName() + ", GameMode: " + player.getGameMode() + ", Sneaking: " + player.isSneaking() + ", OP: " + player.isOp() + ", Berechtigung autosortchest.bypass: " + player.hasPermission("autosortchest.bypass")); - } - - // Fall 1: Schild wird abgebaut if (block.getState() instanceof Sign sign) { String[] lines = sign.getLines(); - if (lines.length >= 2 && ChatColor.stripColor((String) (lines[0] != null ? lines[0] : "")).equalsIgnoreCase("[asc]") && - (ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")).equalsIgnoreCase("input") || - ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")).equalsIgnoreCase("ziel") || - ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")).equalsIgnoreCase("rest"))) { + 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 = block; - signOwner = ChatColor.stripColor((String) (lines[3] != null ? lines[3] : "")); - if (isDebug()) { - getLogger().fine("Schild erkannt bei " + block.getLocation() + ", Besitzer: " + signOwner); - } + signOwner = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); } } - // Fall 2: Truhe wird abgebaut if (block.getState() instanceof Chest chestBlock) { - // Prüfe beide Hälften auf Schilder 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((String) (lines[0] != null ? lines[0] : "")).equalsIgnoreCase("[asc]") && - (ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")).equalsIgnoreCase("input") || - ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")).equalsIgnoreCase("ziel") || - ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")).equalsIgnoreCase("rest"))) { + 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((String) (lines[3] != null ? lines[3] : "")); + signOwner = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); isAscChest = true; - if (isDebug()) { - getLogger().fine("Truhe mit [asc]-Schild erkannt bei " + block.getLocation() + ", Schild bei " + face.getLocation() + ", Besitzer: " + signOwner); - } break outerLoop; } } @@ -1472,121 +1862,78 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // Kein [asc]-Schild oder keine Truhe mit [asc]-Schild - if (signBlock == null) { - if (isDebug()) { - getLogger().fine("Kein [asc]-Schild oder Truhe mit [asc]-Schild bei " + block.getLocation()); - } - return; - } + if (signBlock == null) return; - // Prüfe Besitzer - Admin Check hinzugefügt boolean isOwner = signOwner.isEmpty() || signOwner.equalsIgnoreCase(player.getName()); - if (!isOwner && !isAdmin(player)) { player.sendMessage(getMessage("not-your-chest")); event.setCancelled(true); - getLogger().warning("Spieler " + player.getName() + " versuchte, " + (isAscChest ? "Truhe" : "Schild") + " bei " + block.getLocation() + " abzubauen, das nicht ihm gehört (Besitzer: " + signOwner + ")"); return; } - - // Prüfe Shift-Taste (Berechtigungen ignorieren) - Admin Check hinzugefügt if (!player.isSneaking() && !isAdmin(player)) { player.sendMessage(getMessage("sign-break-denied")); event.setCancelled(true); - if (isDebug()) { - getLogger().fine("Spieler " + player.getName() + " versuchte, " + (isAscChest ? "Truhe" : "Schild") + " bei " + block.getLocation() + " ohne Shift abzubauen"); - } return; } - // --- Daten bereinigen beim Abbauen --- String[] lines = ((Sign) (isAscChest ? signBlock.getState() : block.getState())).getLines(); String line1 = ChatColor.stripColor(lines[1]); - - // Hole Location der Truhe, die abgebaut wird (für Datenabgleich) - Location chestLoc = isAscChest ? block.getLocation() : ((Sign)block.getState()).getBlock().getRelative(((WallSign)((Sign)block.getState()).getBlockData()).getFacing().getOppositeFace()).getLocation(); - - // Wenn Admin abbaut, müssen wir wissen, WEM die Truhe gehört, um die richtigen Daten zu löschen. - // signOwner ist der Name auf dem Schild. + Location chestLoc = isAscChest ? block.getLocation() : ((Sign) block.getState()).getBlock() + .getRelative(((WallSign) ((Sign) block.getState()).getBlockData()).getFacing().getOppositeFace()).getLocation(); + UUID ownerUUID = null; - if (!signOwner.isEmpty() && !signOwner.equalsIgnoreCase("Unknown")) { - OfflinePlayer op = Bukkit.getOfflinePlayer(signOwner); - if (op.hasPlayedBefore()) { - ownerUUID = op.getUniqueId(); - } - } else { - // Fallback: Wenn Name "Unknown" ist, versuchen wir es über den aktuellen Spieler (wenn er Owner ist) oder wir iterieren (teuer). - // Da Admins abbauen können, müssen wir den Owner eindeutig identifizieren. - // Wir iterieren durch die Spielerdaten, um zu finden, wem diese Location gehört. - if (playerData.getConfigurationSection("players") != null) { - for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { - UUID uuid = UUID.fromString(uuidString); - // Check inputs - String inputListPath = "players." + uuidString + ".input-chests"; - if (playerData.contains(inputListPath)) { - for (String chestId : playerData.getConfigurationSection(inputListPath).getKeys(false)) { - if (chestLoc.equals(getLocationFromPath(inputListPath + "." + chestId))) { - ownerUUID = uuid; - break; - } - } - } - // Check targets - if (ownerUUID == null) { - String targetPath = "players." + uuidString + ".target-chests"; - if (playerData.contains(targetPath)) { - for (String itemType : playerData.getConfigurationSection(targetPath).getKeys(false)) { - if (chestLoc.equals(getLocationFromPath(targetPath + "." + itemType))) { - ownerUUID = uuid; - break; - } - } - } - } - // Check rest - if (ownerUUID == null) { - Location restLoc = getRestChestLocation(uuid); - if (chestLoc.equals(restLoc)) { - ownerUUID = uuid; - } - } - if (ownerUUID != null) break; + String pureOwnerName = signOwner.replace("[Public]", "").replace("[public]", "").trim(); + if (!pureOwnerName.isEmpty() && !pureOwnerName.equalsIgnoreCase("Unknown")) { + OfflinePlayer op = Bukkit.getOfflinePlayer(pureOwnerName); + if (op.hasPlayedBefore()) ownerUUID = op.getUniqueId(); + } + if (ownerUUID == null && playerData.getConfigurationSection("players") != null) { + for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { + UUID uuid = UUID.fromString(uuidString); + String inputListPath = "players." + uuidString + ".input-chests"; + if (playerData.contains(inputListPath)) { + for (String chestId : playerData.getConfigurationSection(inputListPath).getKeys(false)) { + if (chestLoc.equals(getLocationFromPath(inputListPath + "." + chestId))) { ownerUUID = uuid; break; } + } } + if (ownerUUID == null) { + String targetPath = "players." + uuidString + ".target-chests"; + if (playerData.contains(targetPath)) { + for (String itemType : playerData.getConfigurationSection(targetPath).getKeys(false)) { + if (chestLoc.equals(getLocationFromPath(targetPath + "." + itemType))) { ownerUUID = uuid; break; } + } + } + } + if (ownerUUID == null) { + Location restLoc = getRestChestLocation(uuid); + if (chestLoc.equals(restLoc)) ownerUUID = uuid; + } + if (ownerUUID != null) break; } } - - // Wenn wir die UUID gefunden haben (oder es ist der Spieler selbst), löschen wir die Daten + UUID uuidToDelete = (ownerUUID != null) ? ownerUUID : player.getUniqueId(); if (line1.equalsIgnoreCase("rest")) { playerData.set("players." + uuidToDelete + ".rest-chest", null); savePlayerData(); - if (isDebug()) getLogger().info("Rest-Truhe Daten gelöscht für " + (ownerUUID != null ? "Owner " + signOwner : "Self")); + if (mysqlEnabled && mysqlManager != null) mysqlManager.removeRestChest(uuidToDelete.toString()); } else if (line1.equalsIgnoreCase("input")) { - // ERWEITERUNG: Input Chest aus Liste löschen removeInputChestByLocation(uuidToDelete, chestLoc); } else if (line1.equalsIgnoreCase("ziel")) { - // Wir müssen wissen, welches Item auf dem Schild stand, um es zu löschen String line2 = ChatColor.stripColor(lines[2]); if (!line2.isEmpty()) { Material mat = Material.matchMaterial(line2); if (mat != null) { - // Sicherheitscheck, ob die Location auch wirklich diesem Item zugeordnet ist Location savedLoc = getTargetChestLocation(uuidToDelete, mat); if (savedLoc != null && savedLoc.equals(chestLoc)) { - playerData.set("players." + uuidToDelete + ".target-chests." + mat.name(), null); - savePlayerData(); - if (isDebug()) getLogger().info("Zieltruhe Daten für Item " + mat.name() + " gelöscht."); + playerData.set("players." + uuidToDelete + ".target-chests." + mat.name(), null); + savePlayerData(); + if (mysqlEnabled && mysqlManager != null) mysqlManager.removeTargetChest(uuidToDelete.toString(), mat.name()); } } } } - // -------------------------------- - - if (isDebug()) { - getLogger().fine("Spieler " + player.getName() + " hat " + (isAscChest ? "Truhe" : "Schild") + " bei " + block.getLocation() + " erfolgreich abgebaut"); - } } @EventHandler @@ -1594,7 +1941,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { InventoryHolder holder = event.getInventory().getHolder(); List chestsToCheck = new ArrayList<>(); - // FIX: Double Chest Handling if (holder instanceof Chest) { chestsToCheck.add((Chest) holder); } else if (holder instanceof DoubleChest) { @@ -1610,19 +1956,14 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { for (Chest chest : chestsToCheck) { Block chestBlock = chest.getBlock(); Block signBlock = null; - String signType = "target"; // default + String signType = "target"; - // Suche nach einem an die Truhe angehängten Schild - 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((String) (lines[1] != null ? lines[1] : "")); - if (lines.length >= 2 && ChatColor.stripColor((String) (lines[0] != null ? lines[0] : "")).equalsIgnoreCase("[asc]") && (line1.equalsIgnoreCase("ziel") || line1.equalsIgnoreCase("rest"))) { + 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; if (line1.equalsIgnoreCase("rest")) signType = "rest"; break; @@ -1630,333 +1971,217 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - if (signBlock == null) { - continue; - } + if (signBlock == null) continue; Sign sign = (Sign) signBlock.getState(); String[] lines = sign.getLines(); - String signOwner = ChatColor.stripColor((String) (lines[3] != null ? lines[3] : "")); - - // FIX: Auch öffentliche Truhen updaten, nicht nur Owner - // ERWEITERUNG: Auch Admins updaten, damit Schildfarben stimmen wenn Admin Items rausnimmt + String signOwner = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); boolean isPublic = isChestPublic(sign); - if (!signOwner.equalsIgnoreCase(player.getName()) && !isPublic && !isAdmin(player)) { - continue; - } + if (!signOwner.equalsIgnoreCase(player.getName()) && !isPublic && !isAdmin(player)) continue; boolean isFull = isInventoryFull(chest.getInventory()); - - // FIX: Mapping 'ziel' -> 'target' String configType = signType.equalsIgnoreCase("rest") ? "rest" : "target"; String colorType = isFull ? "full" : configType; + String currentLine1 = ChatColor.stripColor(lines[1] != null ? lines[1] : ""); + String currentLine3 = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); - String currentLine0 = ChatColor.stripColor((String) (lines[0] != null ? lines[0] : "")); - String currentLine1 = ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")); - String currentLine3 = ChatColor.stripColor((String) (lines[3] != null ? lines[3] : "")); - - if (currentLine0.equalsIgnoreCase("[asc]") && (currentLine1.equalsIgnoreCase("ziel") || currentLine1.equalsIgnoreCase("rest"))) { + if (ChatColor.stripColor(lines[0] != null ? lines[0] : "").equalsIgnoreCase("[asc]") + && (currentLine1.equalsIgnoreCase("ziel") || currentLine1.equalsIgnoreCase("rest"))) { sign.setLine(0, getSignColor(colorType, "line1") + "[asc]"); - sign.setLine(1, getSignColor(colorType, "line2") + currentLine1); // Behält "ziel" oder "rest" bei - sign.setLine(2, getSignColor(colorType, "line3") + lines[2]); // Zeile 2 (Item bei Ziel, leer bei Rest) + sign.setLine(1, getSignColor(colorType, "line2") + currentLine1); + sign.setLine(2, getSignColor(colorType, "line3") + ChatColor.stripColor(lines[2] != null ? lines[2] : "")); sign.setLine(3, getSignColor(colorType, "line4") + currentLine3); sign.update(); - if (isDebug()) { - getLogger().fine(signType + "-Truhe Schild bei " + signBlock.getLocation() + " aktualisiert nach Inventar-Schließung (voll: " + isFull + ")"); - } } } } - // --- HILFSMETHODE zum Entfernen alter Einträge --- private void removeOldTargetEntry(UUID uuid, Location loc, String newItemType) { String basePath = "players." + uuid + ".target-chests"; if (!playerData.contains(basePath)) return; - for (String existingType : playerData.getConfigurationSection(basePath).getKeys(false)) { - if (existingType.equalsIgnoreCase(newItemType)) continue; // Gleiches Item ignorieren - + if (existingType.equalsIgnoreCase(newItemType)) continue; String path = basePath + "." + existingType; - if (playerData.getString(path + ".world").equals(loc.getWorld().getName()) && - playerData.getInt(path + ".x") == loc.getBlockX() && - playerData.getInt(path + ".y") == loc.getBlockY() && - playerData.getInt(path + ".z") == loc.getBlockZ()) { - + if (playerData.getString(path + ".world").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); - if (isDebug()) getLogger().info("Altes Zieltruhen-Item '" + existingType + "' für " + uuid + " entfernt (Update)."); - // Nicht speichern hier, savePlayerData() wird am Ende von setTargetChestLocation aufgerufen - break; // Eine Truhe kann nur ein Item sein (nicht Rest) - } - } - } - // ---------------------------------------------- - - private void distributeItems(Player player, Inventory sourceInventory) { - UUID playerUUID = player.getUniqueId(); - Block chestBlock = ((Chest) sourceInventory.getHolder()).getBlock(); - - // Prüfe, ob die Truhe leer ist - boolean hasItems = false; - for (ItemStack item : sourceInventory.getContents()) { - if (item != null && item.getType() != Material.AIR) { - hasItems = true; break; } } - if (!hasItems) { - if (isDebug()) getLogger().fine("Eingangstruhe bei " + chestBlock.getLocation() + " ist leer"); - return; - } - - for (int slot = 0; slot < sourceInventory.getSize(); slot++) { - ItemStack item = sourceInventory.getItem(slot); - if (item == null || item.getType() == Material.AIR) continue; - - Location targetChestLocation = getTargetChestLocation(playerUUID, item.getType()); - boolean isRestChest = false; - - // Wenn kein spezifisches Ziel, versuche Rest-Truhe - if (targetChestLocation == null) { - targetChestLocation = getRestChestLocation(playerUUID); - if (targetChestLocation != null) { - isRestChest = true; - } - } - - if (targetChestLocation == null) { - if (isDebug()) getLogger().fine("Keine Zieltruhe und keine Rest-Truhe für Item " + item.getType().name() + " für Spieler " + player.getName()); - continue; - } - - if (!(targetChestLocation.getBlock().getState() instanceof Chest)) { - String msgKey = isRestChest ? "target-chest-missing" : "target-chest-missing"; // Kann gleiche Nachricht nutzen - if (canSendFullChestMessage(playerUUID, item.getType())) { - player.sendMessage(getMessage(msgKey).replace("%item%", (isRestChest ? "Rest-Truhe" : item.getType().name()))); - } - - if (isRestChest) { - playerData.set("players." + playerUUID + ".rest-chest", null); - } else { - playerData.set("players." + playerUUID + ".target-chests." + item.getType().name(), null); - } - savePlayerData(); - getLogger().warning("Zieltruhe (" + (isRestChest ? "Rest" : item.getType().name()) + ") fehlt bei " + targetChestLocation); - continue; - } - - Chest targetChest = (Chest) targetChestLocation.getBlock().getState(); - Inventory targetInventory = targetChest.getInventory(); - - boolean isValidTarget = false; - String signOwner = "Unbekannt"; - Block signBlock = null; - - // Sign Check erlaubt sowohl "ziel" als auch "rest" und prüft beide Hälften bei Doppeltruhen - 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) - }) { - if (face.getState() instanceof Sign sign && isSignAttachedToChest(face, b)) { - String[] lines = sign.getLines(); - String line0 = ChatColor.stripColor((String) (lines[0] != null ? lines[0] : "")); - String line1 = ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")); - String line3 = ChatColor.stripColor((String) (lines[3] != null ? lines[3] : "")); - - // Prüfen ob Schild-Typ zum erwarteten Ziel passt (Rest-Truhe muss "rest" Schild haben, Ziel muss "ziel" haben) - boolean typeMatches = isRestChest ? line1.equalsIgnoreCase("rest") : line1.equalsIgnoreCase("ziel"); - - if (isDebug()) { - getLogger().fine("Prüfe Zieltruhe-Schild bei " + face.getLocation() + ": Zeile 1='" + line0 + "', Zeile 2='" + line1 + "', Zeile 4='" + line3 + "' für Spieler " + player.getName()); - } - if (line0.equalsIgnoreCase("[asc]") && typeMatches) { - signOwner = line3.isEmpty() ? "Unbekannt" : line3; - if (line3.equalsIgnoreCase(player.getName())) { - isValidTarget = true; - signBlock = face; - if (isDebug()) { - getLogger().fine("Gültiges Zieltruhe-Schild gefunden bei " + face.getLocation() + " für Spieler " + player.getName()); - } - break outerLoop; - } - } else if (isDebug()) { - getLogger().fine("Zieltruhe-Schild bei " + face.getLocation() + " hat ungültige Zeilen oder Typ passt nicht: [asc]=" + line0 + ", typ=" + line1); - } - } - } - } - - if (!isValidTarget) { - if (canSendFullChestMessage(playerUUID, item.getType())) { - player.sendMessage(getMessage("not-your-chest")); - } - getLogger().warning("Zieltruhe bei " + targetChestLocation + " gehört nicht Spieler " + player.getName() + " (Besitzer: " + signOwner + ")"); - continue; - } - - ItemStack itemToTransfer = item.clone(); - Map leftover = targetInventory.addItem(itemToTransfer); - boolean isFull = isInventoryFull(targetInventory); - - if (signBlock != null) { - Sign sign = (Sign) signBlock.getState(); - String[] lines = sign.getLines(); - String line0 = ChatColor.stripColor((String) (lines[0] != null ? lines[0] : "")); - String line1 = ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")); - String line2 = ChatColor.stripColor((String) (lines[2] != null ? lines[2] : "")); - String line3 = ChatColor.stripColor((String) (lines[3] != null ? lines[3] : "")); - - if (line0.equalsIgnoreCase("[asc]") && (line1.equalsIgnoreCase("ziel") || line1.equalsIgnoreCase("rest"))) { - // FIX: Mapping 'ziel' -> 'target' - String configType = line1.equalsIgnoreCase("rest") ? "rest" : "target"; - 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); - sign.setLine(3, getSignColor(colorType, "line4") + line3); - sign.update(); - if (isDebug()) { - getLogger().fine("Zieltruhe-Schild bei " + signBlock.getLocation() + " aktualisiert (voll: " + isFull + ")"); - } - } - } - - if (leftover.isEmpty()) { - sourceInventory.setItem(slot, null); - } else { - if (canSendFullChestMessage(playerUUID, 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())); - player.sendMessage(message); - } - for (ItemStack leftoverItem : leftover.values()) { - if (leftoverItem != null && leftoverItem.getType() == item.getType()) { - item.setAmount(leftoverItem.getAmount()); - sourceInventory.setItem(slot, item); - break; - } - } - } - } } - // --- NEU: BUNTE PARTIKEL UM DIE TRUHE (NUR ZIELTRUHE) --- private void spawnTransferParticles(Location start, Location end) { if (!config.getBoolean("effects.enabled", true)) return; - if (end.getWorld() == null) return; - - // 1. Sound abspielen (am Zielort) + if (end == null || end.getWorld() == null) return; if (config.getBoolean("effects.sound", true)) { - end.getWorld().playSound(end, Sound.ENTITY_ITEM_PICKUP, 0.2f, 1.2f); + end.getWorld().playSound(end, Sound.ENTITY_ITEM_PICKUP, 0.2f, 1.2f); } - - // 2. Bunter Kreis aus Partikeln am Zielort - Location center = end.clone().add(0.5, 0.5, 0.5); // Zentrum der Truhe (etwas erhöht) - - // Wir spawnen einen Kreis aus Partikeln - int count = 12; // Anzahl der Partikel im Kreis - double radius = 0.8; // Radius um den Block herum + Location center = end.clone().add(0.5, 0.5, 0.5); + int count = 12; + double radius = 0.8; float size = 1.2f; - - // Farbpalette (Rot, Grün, Blau, Gelb, Cyan, Magenta) Color[] colors = { - Color.fromRGB(255, 0, 0), // Rot - Color.fromRGB(0, 255, 0), // Grün - Color.fromRGB(0, 0, 255), // Blau - Color.fromRGB(255, 255, 0), // Gelb - Color.fromRGB(0, 255, 255), // Cyan - Color.fromRGB(255, 0, 255) // Magenta + 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; - - // Partikel am Boden oder leicht erhöht Location particleLoc = center.clone().add(dx, -0.1, dz); - - // Zufällige Farbe aus der Liste für "Bunt" - Color color = colors[(int)(Math.random() * colors.length)]; - - // Wir mischen ein wenig Offset, damit es nicht so perfekt wirkt + 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); end.getWorld().spawnParticle(Particle.DUST, particleLoc.clone().add(offsetX, 0, offsetZ), 1, 0, 0, 0, 0.02, options); } } - // ----------------------------------- private void checkInputChests() { - if (playerData == null) { - getLogger().warning("playerData ist null. Kann Eingangstruhe nicht prüfen."); - return; - } - if (playerData.getConfigurationSection("players") == null) { - getLogger().warning("Abschnitt 'players' in players.yml fehlt. Keine Eingangstruhe zu prüfen."); - return; - } + 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; } - for (String uuidString : playerData.getConfigurationSection("players").getKeys(false)) { - UUID ownerUUID; - try { - ownerUUID = UUID.fromString(uuidString); - } catch (IllegalArgumentException e) { - getLogger().warning("Ungültige UUID in players.yml: " + uuidString); - continue; - } + List> chests = mysqlManager.getInputChests(uuidString); + for (Map chest : chests) { + String worldName = (String) chest.get("world"); + World world = Bukkit.getWorld(worldName); - // --- ERWEITERUNG: Eingangstruhe (Jetzt Liste) --- - String inputListPath = "players." + uuidString + ".input-chests"; - // Fallback für altes Format (falls Migration noch nicht lief oder Fehler) - String oldInputPath = "players." + uuidString + ".input-chest"; - - List chestsToCheck = new ArrayList<>(); - - if (playerData.contains(inputListPath)) { - chestsToCheck.addAll(playerData.getConfigurationSection(inputListPath).getKeys(false)); - } else if (playerData.contains(oldInputPath)) { - Location loc = getLocationFromPath(oldInputPath); - if (loc != null) { - checkSingleInputChest(ownerUUID, loc, "legacy"); + 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); + } + } } - continue; + } catch (Exception e) { + getLogger().warning("Fehler beim Lesen der Spieler aus MySQL: " + e.getMessage()); } - - if (chestsToCheck.isEmpty()) continue; - - for (String chestId : chestsToCheck) { - String path = inputListPath + "." + chestId; - Location loc = getLocationFromPath(path); - - if (loc == null) { - playerData.set(path, null); // Ungültigen Eintrag löschen + } 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); + } + } + } catch (Exception e) { + getLogger().warning("Fehler beim Lesen der Spieler aus MySQL: " + e.getMessage()); + } + } 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"; + List chestsToCheck = new ArrayList<>(); + if (playerData.contains(inputListPath)) { + chestsToCheck.addAll(playerData.getConfigurationSection(inputListPath).getKeys(false)); + } else if (playerData.contains(oldInputPath)) { + Location loc = getLocationFromPath(oldInputPath); + if (loc != null) checkSingleInputChest(ownerUUID, loc, "legacy", false); continue; } - - boolean stillValid = checkSingleInputChest(ownerUUID, loc, chestId); - if (!stillValid) { - playerData.set(path, null); - savePlayerData(); + if (chestsToCheck.isEmpty()) continue; + for (String chestId : chestsToCheck) { + 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(); } } } - // ------------------------------------------------- } } - - // --- ERWEITERUNG: Hilfsmethode zum Prüfen einzelner Eingangstruhe --- - private boolean checkSingleInputChest(UUID ownerUUID, Location location, String debugId) { + + private void checkRemoteInputChest(UUID ownerUUID, Map inputChestData) { + if (!mysqlEnabled || mysqlManager == null) return; + List> targetChests = mysqlManager.getTargetChests(ownerUUID.toString()); + + Map localTargets = new HashMap<>(); + for (Map tc : targetChests) { + 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"))); + } + + Location localRestChest = getRestChestLocation(ownerUUID); + + if (localTargets.isEmpty() && localRestChest == null) return; + + OfflinePlayer op = Bukkit.getOfflinePlayer(ownerUUID); + String ownerName = op.getName() != null ? op.getName() : ownerUUID.toString(); + + distributeFromRemoteInputChest(ownerUUID, ownerName, inputChestData, localTargets, localRestChest); + } + + private void distributeFromRemoteInputChest(UUID ownerUUID, String ownerName, + Map inputChestData, + Map localTargets, + Location localRestChest) { + + if (!mysqlEnabled || mysqlManager == null) return; + + mysqlManager.setupTransferTable(); + + List> pendingTransfers = mysqlManager.getPendingTransfers(ownerUUID.toString()); + + for (Map transfer : pendingTransfers) { + 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) { + mysqlManager.deleteTransfer(transferId); + continue; + } + + Location targetLoc = localTargets.get(itemName); + 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); + Map leftover = targetChest.getInventory().addItem(toAdd); + + int transferred = amount - (leftover.isEmpty() ? 0 : leftover.get(0).getAmount()); + if (transferred > 0) { + if (leftover.isEmpty()) { + mysqlManager.deleteTransfer(transferId); + } else { + mysqlManager.updateTransferAmount(transferId, leftover.get(0).getAmount()); + } + spawnTransferParticles(null, targetLoc); + if (isDebug()) { + getLogger().info("[CrossLink] " + transferred + "x " + itemName + + " für " + ownerName + " in lokale Zieltruhe übertragen."); + } + } + } + } + + private boolean checkSingleInputChest(UUID ownerUUID, Location location, String debugId, boolean crosslinkMode) { + if (isWorldBlacklisted(location.getWorld())) return true; if (!(location.getBlock().getState() instanceof Chest)) return false; Chest chest = (Chest) location.getBlock().getState(); @@ -1964,27 +2189,17 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { boolean isPublic = false; String ownerName = "Unknown"; - // FIX: Suche Schild an beiden Hälften bei Doppeltruhen 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((String) (lines[0] != null ? lines[0] : "")); - String line1 = ChatColor.stripColor((String) (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 line3Raw = lines[3] != null ? lines[3] : ""; - String line3Clean = ChatColor.stripColor(line3Raw); - + String line3Clean = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); isPublic = line3Clean.toLowerCase().endsWith("[public]"); ownerName = line3Clean.replace(" [Public]", "").replace(" [public]", "").trim(); break outerLoop; @@ -1993,70 +2208,42 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - if (inputSignBlock == null) { - if (isDebug()) getLogger().fine("Kein Eingangsschild an Truhe " + debugId); - return false; - } + if (inputSignBlock == null) return false; - // Wenn das Inventar leer ist, brauchen wir nichts tun boolean hasItems = false; for (ItemStack item : chest.getInventory().getContents()) { - if (item != null && item.getType() != Material.AIR) { - hasItems = true; - break; - } + if (item != null && item.getType() != Material.AIR) { hasItems = true; break; } } if (!hasItems) return true; - // Jetzt kommt der entscheidende Teil für die Multiplayer-Funktionalität: - // Wenn PRIVATE: Nur sortieren, wenn der Owner ONLINE ist. - // Wenn ÖFFENTLICH: Immer sortieren, aber wir brauchen einen "fiktiven" Player für die Fehlermeldungen - // oder wir senden Meldungen an alle Online-Spieler, die in der Nähe sind? - // Der Einfachheit halber: Wenn Öffentlich, sortieren wir stumm oder senden Meldungen an den Owner wenn er online ist. - Player ownerPlayer = getServer().getPlayer(ownerUUID); - if (!isPublic) { - // Privat: Nur wenn Owner online if (ownerPlayer == null || !ownerPlayer.isOnline()) return true; } else { - // Öffentlich: Wenn Owner offline, können wir keine "Truhe voll" Nachrichten an den Owner senden. - // Wir sortieren trotzdem. - // Wir setzen ownerPlayer auf null, damit distributeItemsForOwner weiß, dass niemand Besitzer ist (für Messages). - if (ownerPlayer == null || !ownerPlayer.isOnline()) { - ownerPlayer = null; - } + if (ownerPlayer == null || !ownerPlayer.isOnline()) ownerPlayer = null; } - // Wir rufen distributeItemsForOwner auf. distributeItemsForOwner(ownerUUID, ownerPlayer, chest.getInventory(), ownerName, location); return true; } - // ---------------------------------------------------------------------- - // 4. KORRIGIERTE distributeItemsForOwner Methode: - private void distributeItemsForOwner(UUID ownerUUID, Player ownerPlayer, Inventory sourceInventory, String ownerNameOverride, Location sourceLocation) { + private void distributeItemsForOwner(UUID ownerUUID, Player ownerPlayer, Inventory sourceInventory, + String ownerNameOverride, Location sourceLocation) { + boolean hasItems = false; for (ItemStack item : sourceInventory.getContents()) { - if (item != null && item.getType() != Material.AIR) { - hasItems = true; - break; - } + if (item != null && item.getType() != Material.AIR) { hasItems = true; break; } } if (!hasItems) return; - // Owner-Name ermitteln String ownerName = "Unknown"; if (ownerNameOverride != null && !ownerNameOverride.isEmpty()) { ownerName = ownerNameOverride; } else if (ownerPlayer != null) { ownerName = ownerPlayer.getName(); } else { - // Offline-Namen aus PlayerData holen wenn möglich OfflinePlayer offlinePlayer = getServer().getOfflinePlayer(ownerUUID); - if (offlinePlayer.hasPlayedBefore()) { - ownerName = offlinePlayer.getName(); - } + if (offlinePlayer.hasPlayedBefore()) ownerName = offlinePlayer.getName(); } for (int slot = 0; slot < sourceInventory.getSize(); slot++) { @@ -2065,62 +2252,72 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { Location targetChestLocation = getTargetChestLocation(ownerUUID, item.getType()); boolean isRestChest = false; + boolean isCrosslink = false; - // FALLBACK LOGIK: Wenn kein Ziel definiert, suche Rest-Truhe if (targetChestLocation == null) { - targetChestLocation = getRestChestLocation(ownerUUID); - if (targetChestLocation != null) { - isRestChest = true; + if (serverCrosslink && mysqlEnabled && mysqlManager != null) { + Map raw = mysqlManager.getTargetChest(ownerUUID.toString(), item.getType().name()); + if (raw != null && Bukkit.getWorld((String) raw.get("world")) == null) { + isCrosslink = true; + mysqlManager.setupTransferTable(); + mysqlManager.addTransfer(ownerUUID.toString(), item.getType().name(), item.getAmount(), (String) raw.get("world")); + 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)."); + } + continue; + } + } + + if (!isCrosslink) { + targetChestLocation = getRestChestLocation(ownerUUID); + if (targetChestLocation != null) { + isRestChest = true; + } else if (serverCrosslink && mysqlEnabled && mysqlManager != null) { + Map raw = mysqlManager.getRestChest(ownerUUID.toString()); + if (raw != null && Bukkit.getWorld((String) raw.get("world")) == null) { + mysqlManager.setupTransferTable(); + mysqlManager.addTransfer(ownerUUID.toString(), item.getType().name(), item.getAmount(), (String) raw.get("world")); + 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)."); + } + continue; + } + } } } - if (targetChestLocation == null) { - if (isDebug()) getLogger().fine("Kein Ziel und keine Rest-Truhe für Item " + item.getType().name()); - continue; - } + if (targetChestLocation == null) continue; if (!(targetChestLocation.getBlock().getState() instanceof Chest)) { 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); + 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(); continue; } Chest targetChest = (Chest) targetChestLocation.getBlock().getState(); Inventory targetInventory = targetChest.getInventory(); - boolean isValidTarget = false; Block signBlock = null; - // FIX: Suche Schild an beiden Hälften 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((String) (lines[0] != null ? lines[0] : "")); - String line1 = ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")); - String line3Clean = ChatColor.stripColor((String) (lines[3] != null ? lines[3] : "")); - - // Typ-Check: Wenn isRestChest true, muss Schild "rest" sagen, sonst "ziel" + 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; @@ -2141,29 +2338,24 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (signBlock != null) { Sign sign = (Sign) signBlock.getState(); String[] lines = sign.getLines(); - String line0 = ChatColor.stripColor((String) (lines[0] != null ? lines[0] : "")); - String line1 = ChatColor.stripColor((String) (lines[1] != null ? lines[1] : "")); - String line2 = ChatColor.stripColor((String) (lines[2] != null ? lines[2] : "")); + 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"))) { - // FIX: Mapping 'ziel' -> 'target' String configType = line1.equalsIgnoreCase("rest") ? "rest" : "target"; 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); - sign.setLine(3, line3Raw); // Original behalten (mit Farbe) + sign.setLine(3, line3Raw); sign.update(); } } if (leftover.isEmpty()) { sourceInventory.setItem(slot, null); - // --- NEU: Effekte (Nur am Zielort) --- spawnTransferParticles(null, targetChestLocation); - // ------------------------ } else { if (ownerPlayer != null && canSendFullChestMessage(ownerUUID, item.getType())) { String message = getMessage("target-chest-full") @@ -2184,22 +2376,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { } } - // --- NEU: Hopper-Automatisierung --- @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onInventoryMoveItem(InventoryMoveItemEvent event) { Inventory destination = event.getDestination(); - - // Prüfen, ob das Ziel eine Truhe ist - if (!(destination.getHolder() instanceof Chest)) { - return; - } + if (!(destination.getHolder() instanceof Chest)) return; final Chest destChest = (Chest) destination.getHolder(); - - // Wir müssen herausfinden, ob diese Truhe eine Input-Truhe ist. - // Wir scannen die Blöcke der Truhe nach einem [asc] input Schild. - - // Unterstützung für Doppeltruhen List blocksToScan = new ArrayList<>(); InventoryHolder holder = destination.getHolder(); if (holder instanceof DoubleChest) { @@ -2212,57 +2394,34 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { UUID ownerUUID = null; String ownerName = null; - boolean isPublic = false; outerLoop: for (Block b : blocksToScan) { - for (Block face : new Block[] { - b.getRelative(1, 0, 0), - b.getRelative(-1, 0, 0), - b.getRelative(0, 0, 1), - b.getRelative(0, 0, -1) - }) { + 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((String) (lines[0] != null ? lines[0] : "")); - String line1 = ChatColor.stripColor((String) (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")) { - // Schild gefunden! Ermittle Owner aus Zeile 4 - String line3Raw = lines[3] != null ? lines[3] : ""; - String line3Clean = ChatColor.stripColor(line3Raw); - - isPublic = line3Clean.toLowerCase().endsWith("[public]"); + String line3Clean = ChatColor.stripColor(lines[3] != null ? lines[3] : ""); ownerName = line3Clean.replace(" [Public]", "").replace(" [public]", "").trim(); - - // Name zu UUID auflösen OfflinePlayer op = Bukkit.getOfflinePlayer(ownerName); - if (op.hasPlayedBefore()) { - ownerUUID = op.getUniqueId(); - } - break outerLoop; // Wir haben alles was wir brauchen + if (op.hasPlayedBefore()) ownerUUID = op.getUniqueId(); + break outerLoop; } } } } - // Wenn kein [asc] input Schild oder Owner unbekannt -> Abbruch - if (ownerUUID == null || ownerName == null || ownerName.isEmpty()) { - return; - } + if (ownerUUID == null || ownerName == null || ownerName.isEmpty()) return; - // Schedule Task um sicherzustellen, dass der Hopper das Item fertig platziert hat final UUID finalOwnerUUID = ownerUUID; final String finalOwnerName = ownerName; - new BukkitRunnable() { @Override public void run() { - // Wir sortieren auch, wenn der Owner Offline ist (Automation) - // ownerPlayer wird übergeben als null, da es keinen aktiven Spielerkontext gibt distributeItemsForOwner(finalOwnerUUID, null, destChest.getInventory(), finalOwnerName, destChest.getLocation()); } }.runTaskLater(this, 1L); } - // ----------------------------------- } \ No newline at end of file diff --git a/src/main/java/com/viper/autosortchest/MySQLManager.java b/src/main/java/com/viper/autosortchest/MySQLManager.java new file mode 100644 index 0000000..e8030cd --- /dev/null +++ b/src/main/java/com/viper/autosortchest/MySQLManager.java @@ -0,0 +1,412 @@ +package com.viper.autosortchest; + +import java.sql.*; +import java.util.*; + +public class MySQLManager { + public Connection getConnection() { + return connection; + } + + private final String host, database, user, password; + private final int port; + private Connection connection; + + public MySQLManager(String host, int port, String database, String user, String password) { + this.host = host; + this.port = port; + this.database = database; + this.user = user; + this.password = password; + } + + public boolean connect() { + try { + if (connection != null && !connection.isClosed()) return true; + Class.forName("com.mysql.cj.jdbc.Driver"); + connection = DriverManager.getConnection( + "jdbc:mysql://" + host + ":" + port + "/" + database + + "?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&serverTimezone=UTC", + user, password + ); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public void disconnect() { + try { + if (connection != null && !connection.isClosed()) connection.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + 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));"); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * Transfer-Tabelle für serverübergreifende Sortierung. + */ + 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(); + } + } + + public void addTransfer(String uuid, String item, int amount, String targetWorld) { + 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); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> getPendingTransfers(String uuid) { + List> list = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT id, item, amount, target_world FROM asc_transfers WHERE uuid=? ORDER BY created_at ASC;")) { + ps.setString(1, uuid); + 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")); + list.add(map); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deleteTransfer(long id) { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM asc_transfers WHERE id=?;")) { + ps.setLong(1, id); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void updateTransferAmount(long id, int newAmount) { + try (PreparedStatement ps = connection.prepareStatement( + "UPDATE asc_transfers SET amount=? WHERE id=?;")) { + ps.setInt(1, newAmount); + ps.setLong(2, id); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // --- Spieler --- + + public void savePlayer(String uuid, String name) { + try (PreparedStatement ps = connection.prepareStatement( + "REPLACE INTO asc_players (uuid, name) VALUES (?, ?);")) { + ps.setString(1, uuid); + ps.setString(2, name); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 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(); + ResultSet rs = st.executeQuery("SELECT uuid, name FROM asc_players;")) { + while (rs.next()) { + Map map = new HashMap<>(); + map.put("uuid", rs.getString("uuid")); + map.put("name", rs.getString("name")); + list.add(map); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // --- Input-Chest --- + + public void addInputChest(String uuid, String chestId, String world, int x, int y, int z) { + addInputChest(uuid, chestId, world, x, y, z, false); + } + + public void addInputChest(String uuid, String chestId, String world, int x, int y, int z, boolean isPublic) { + try (PreparedStatement ps = connection.prepareStatement( + "REPLACE INTO asc_input_chests (uuid, chest_id, world, x, y, z, `public`) VALUES (?, ?, ?, ?, ?, ?, ?);")) { + ps.setString(1, uuid); + ps.setString(2, chestId); + ps.setString(3, world); + ps.setInt(4, x); + ps.setInt(5, y); + ps.setInt(6, z); + ps.setBoolean(7, isPublic); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void removeInputChest(String uuid, String chestId) { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM asc_input_chests WHERE uuid=? AND chest_id=?;")) { + ps.setString(1, uuid); + ps.setString(2, chestId); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> getInputChests(String uuid) { + List> list = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT * FROM asc_input_chests WHERE uuid=?;")) { + ps.setString(1, uuid); + ResultSet rs = ps.executeQuery(); + 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")); + list.add(map); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // --- Target-Chest --- + + public void setTargetChest(String uuid, String item, String world, int x, int y, int z) { + setTargetChest(uuid, item, world, x, y, z, false); + } + + public void setTargetChest(String uuid, String item, String world, int x, int y, int z, boolean isPublic) { + try (PreparedStatement ps = connection.prepareStatement( + "REPLACE INTO asc_target_chests (uuid, item, world, x, y, z, `public`) 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.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map getTargetChest(String uuid, String item) { + try (PreparedStatement ps = connection.prepareStatement( + "SELECT * FROM asc_target_chests WHERE uuid=? AND item=?;")) { + ps.setString(1, uuid); + ps.setString(2, item); + 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("public", rs.getBoolean("public")); + return map; + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void removeTargetChest(String uuid, String item) { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM asc_target_chests WHERE uuid=? AND item=?;")) { + ps.setString(1, uuid); + ps.setString(2, item); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> getTargetChests(String uuid) { + List> list = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT * FROM asc_target_chests WHERE uuid=?;")) { + 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("public", rs.getBoolean("public")); + list.add(map); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // --- Rest-Chest --- + + public void setRestChest(String uuid, String world, int x, int y, int z) { + setRestChest(uuid, world, x, y, z, false); + } + + public void setRestChest(String uuid, String world, int x, int y, int z, boolean isPublic) { + try (PreparedStatement ps = connection.prepareStatement( + "REPLACE INTO asc_rest_chests (uuid, world, x, y, z, `public`) 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.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map getRestChest(String uuid) { + try (PreparedStatement ps = connection.prepareStatement( + "SELECT * FROM asc_rest_chests WHERE uuid=?;")) { + ps.setString(1, uuid); + 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("public", rs.getBoolean("public")); + return map; + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void removeRestChest(String uuid) { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM asc_rest_chests WHERE uuid=?;")) { + ps.setString(1, uuid); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // --- Hilfsmethoden für serverCrosslink --- + + public List> getAllInputChests() { + List> list = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement("SELECT * FROM asc_input_chests;")) { + ResultSet rs = ps.executeQuery(); + 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")); + list.add(map); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> getAllTargetChests() { + List> list = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement("SELECT * FROM asc_target_chests;")) { + 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("public", rs.getBoolean("public")); + list.add(map); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public Map getAnyRestChest() { + try (PreparedStatement ps = connection.prepareStatement( + "SELECT * FROM asc_rest_chests WHERE public=1 LIMIT 1;")) { + 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("public", rs.getBoolean("public")); + return map; + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/viper/autosortchest/UpdateChecker.java b/src/main/java/com/viper/autosortchest/UpdateChecker.java index 0edfbd0..faa7afa 100644 --- a/src/main/java/com/viper/autosortchest/UpdateChecker.java +++ b/src/main/java/com/viper/autosortchest/UpdateChecker.java @@ -21,7 +21,8 @@ public class UpdateChecker { public void getVersion(final Consumer consumer) { Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { - try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId + "/~").openStream(); Scanner scann = new Scanner(is)) { + try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId + "/~").openStream(); + Scanner scann = new Scanner(is)) { if (scann.hasNext()) { consumer.accept(scann.next()); } @@ -30,4 +31,47 @@ public class UpdateChecker { } }); } + + /** + * Vergleicht zwei Versions-Strings semantisch (z.B. "2.0" vs "1.9"). + * Gibt true zurück wenn currentVersion NEUER ODER GLEICH latestVersion ist. + * Gibt false zurück wenn latestVersion neuer ist (= Update verfügbar). + * + * Beispiele: + * isCurrentVersionUpToDate("2.0", "1.9") → true (wir sind neuer, kein Update nötig) + * isCurrentVersionUpToDate("1.9", "1.9") → true (gleich, kein Update nötig) + * isCurrentVersionUpToDate("1.8", "1.9") → false (Update verfügbar) + */ + public static boolean isCurrentVersionUpToDate(String currentVersion, String latestVersion) { + try { + String[] current = currentVersion.trim().split("[.\\-]"); + String[] latest = latestVersion.trim().split("[.\\-]"); + + int length = Math.max(current.length, latest.length); + for (int i = 0; i < length; i++) { + int c = i < current.length ? parseVersionPart(current[i]) : 0; + int l = i < latest.length ? parseVersionPart(latest[i]) : 0; + if (c > l) return true; // Aktuelle Version ist neuer → kein Update nötig + if (c < l) return false; // Veröffentlichte Version ist neuer → Update verfügbar + } + return true; // Versionen sind identisch + } catch (Exception e) { + // Im Zweifel: kein Update anzeigen um false-positives zu vermeiden + return true; + } + } + + private static int parseVersionPart(String part) { + try { + // Nur die führenden Ziffern parsen (z.B. "1-SNAPSHOT" → 1) + StringBuilder digits = new StringBuilder(); + for (char ch : part.toCharArray()) { + if (Character.isDigit(ch)) digits.append(ch); + else break; + } + return digits.length() > 0 ? Integer.parseInt(digits.toString()) : 0; + } catch (NumberFormatException e) { + return 0; + } + } } \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 0fe5c58..80f7fdc 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -9,10 +9,29 @@ # --- GRUNDLEGUNG --- # Version der Konfigurationsdatei. Nicht ändern, um Fehler zu vermeiden! -version: "1.8" + +version: "2.0" + # Debug-Modus (true = Ausführliche Logs in der Server-Konsole, nur zum Entwickeln nutzen) + debug: false +# --------------------------------------------------- +# DATENBANK (MySQL/MariaDB) - Optional +# --------------------------------------------------- +# Aktiviere MySQL/MariaDB Speicherung (true/false) + +mysql: + enabled: false + host: "localhost" + port: 3306 + database: "autosortchest" + user: "autosortchest" + password: "autosortchest" + +# Soll serverübergreifendes Sortieren (mit MySQL) erlaubt sein? +server_crosslink: true + # --------------------------------------------------- # SPRACHE (Language) # --------------------------------------------------- @@ -20,9 +39,19 @@ debug: false # Ändert den Text von /asc help und /asc info language: "de" +# --------------------------------------------------- +# BLACKLIST FÜR WELTEN (Optional) +# --------------------------------------------------- +# Welten, in denen AutoSortChest NICHT funktioniert + +world_blacklist: + - "world_nether" + - "world_the_end" + # --------------------------------------------------- # VISUELLE EFFEKTE (PARTIKEL & TÖNE) # --------------------------------------------------- + # Einstellungen für den Regenbogen-Effekt beim Sortieren effects: # Sollen Effekte angezeigt werden? @@ -33,10 +62,49 @@ effects: # 'DUST' ist zwingend für den bunten Regenbogen-Effekt im aktuellen Code. type: "DUST" +# --------------------------------------------------- +# SORTIER-INTERVALL (Ticks) +# --------------------------------------------------- +# Wie oft soll sortiert werden? (1 Tick = 0,05s) +# +# Wähle hier je nach Server-Leistung: +# +# 1 = SEHR SCHNELL (Items verschwinden sofort) +# WARNUNG: Kann bei vielen Truhen Lagg verursachen! +# +# 5 = SCHNELL (Sehr flüssig, gute Balance) +# Empfohlen für schnelle Server. +# +# 10 = FLÜSSIG (0,5s Verzögerung) +# Spart Ressourcen, fühlt sich noch schnell an. +# +# 20 = STANDARD (1 Sekunde) +# Standard-Wert, minimale Last. +# +# 30+ = SPARSAM (>1,5 Sekunden) +# Für sehr große Server mit schwacher Hardware. +# +sort_interval_ticks: 5 + +sort_interval_ticks: 5 + +# --------------------------------------------------- +# LIMITS FÜR SORTIERKISTEN (Optional) +# --------------------------------------------------- +# Maximale Anzahl an Sortierkisten pro Spielergruppe + +chest_limits: + default: 50 + vip: 100 + # Beispiel für weitere Gruppen: + # supporter: 150 + # admin: 200 + # --------------------------------------------------- # SCHILDFARBEN (Farbcodes wie im Chat) # &c = Rot, &a = Grün, &e = Gelb, &6 = Gold, &f = Weiß, &0 = Schwarz # --------------------------------------------------- + sign-colors: # Farben für die Eingangstruhe ([asc] / input) input: @@ -69,6 +137,7 @@ sign-colors: # SYSTEM NACHRICHTEN (Spieler-Feedback) # Platzhalter: %player%, %item%, %x%, %y%, %z%, %mode% # --------------------------------------------------- + messages: # --- FEHLERMELDUNGEN --- no-chest-near-sign: "&cKeine Truhe in der Nähe des Schildes!" @@ -88,4 +157,5 @@ messages: target-chest-full: "&cZieltruhe für %item% ist voll! Koordinaten: (%x%, %y%, %z%)" mode-changed: "&aModus gewechselt: &e%mode%" mode-public: "&aÖffentlich" - mode-private: "&cPrivat" \ No newline at end of file + mode-private: "&cPrivat" + diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index c61c217..30a5076 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: AutoSortChest -version: 1.9 +version: 2.0 main: com.viper.autosortchest.Main api-version: 1.21 authors: [M_Viper] @@ -7,12 +7,18 @@ description: Ein Plugin zum automatischen Sortieren von Items in Truhen commands: asc: description: AutoSortChest Befehle - usage: / [help|info|reload] + usage: / [help|info|reload|import|export] aliases: [autosortchest] permissions: autosortchest.reload: description: Erlaubt das Neuladen der Konfiguration mit /asc reload default: op + autosortchest.import: + description: Erlaubt den Import von players.yml nach MySQL mit /asc import + default: op + autosortchest.export: + description: Erlaubt den Export von MySQL nach players.yml mit /asc export + default: op autosortchest.bypass: description: Erlaubt das Abbauen von ASC-Schildern ohne Shift-Taste und unabhängig vom Besitzer default: op