diff --git a/src/main/java/com/viper/autosortchest/Main.java b/src/main/java/com/viper/autosortchest/Main.java index 0e35910..a96e293 100644 --- a/src/main/java/com/viper/autosortchest/Main.java +++ b/src/main/java/com/viper/autosortchest/Main.java @@ -368,6 +368,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&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" + + "&f- &b/asc list &f- Zeigt Truhen-Übersicht eines Spielers (Admin).\n" + "&6&l========================"; private static final String HELP_EN = @@ -403,6 +404,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { "&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" + + "&f- &b/asc list &f- Shows chest overview of a player (Admin).\n" + "&6&l========================"; private static final String INFO_DE = @@ -1673,9 +1675,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { case "target.line1": fallback = config.getString("sign-colors.target.line3", "&f"); break; case "target.line2": fallback = config.getString("sign-colors.target.line4", "&1"); break; case "target.line3": fallback = config.getString("sign-colors.target.line4", "&1"); break; - case "full.line1": fallback = config.getString("sign-colors.full.line3", "&c"); break; - case "full.line2": fallback = config.getString("sign-colors.full.line4", "&1"); break; - case "full.line3": fallback = config.getString("sign-colors.full.line4", "&1"); break; + case "full.line1": fallback = config.getString("sign-colors.full.line1", "&c"); break; + case "full.line2": fallback = config.getString("sign-colors.full.line2", "&4"); break; + case "full.line3": fallback = config.getString("sign-colors.full.line3", "&e"); break; case "rest.line1": fallback = config.getString("sign-colors.rest.line4", "&1"); break; case "rest.line2": fallback = config.getString("sign-colors.rest.line2", "&0"); break; case "rest.line3": fallback = "&f"; break; @@ -2080,8 +2082,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { if (!isPlayer) { if (args.length == 0 || (!args[0].equalsIgnoreCase("reload") && !args[0].equalsIgnoreCase("import") - && !args[0].equalsIgnoreCase("export"))) { - sender.sendMessage(ChatColor.RED + "Dieser Befehl ist nur für Spieler! (Konsole: reload, import, export)"); + && !args[0].equalsIgnoreCase("export") + && !args[0].equalsIgnoreCase("list"))) { + sender.sendMessage(ChatColor.RED + "Dieser Befehl ist nur für Spieler! (Konsole: reload, import, export, list)"); return true; } } @@ -2180,6 +2183,149 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return true; } + // ------------------------------------------------------- + // /asc list [Spieler] – Admin-Truhen-Übersicht + // ------------------------------------------------------- + if (args[0].equalsIgnoreCase("list")) { + if (!sender.hasPermission("autosortchest.list")) { + sender.sendMessage(getMessage("no-permission")); + return true; + } + + boolean isEn = lang.equalsIgnoreCase("en"); + + if (args.length < 2) { + sender.sendMessage(ChatColor.RED + (isEn + ? "Usage: /asc list " + : "Verwendung: /asc list ")); + return true; + } + + String targetName = args[1]; + + // Spieler suchen (online bevorzugt, dann offline) + OfflinePlayer target = null; + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.getName() != null && p.getName().equalsIgnoreCase(targetName)) { + target = p; + break; + } + } + if (target == null) { + @SuppressWarnings("deprecation") + OfflinePlayer offline = Bukkit.getOfflinePlayer(targetName); + if (offline != null && offline.hasPlayedBefore()) { + target = offline; + } + } + if (target == null) { + sender.sendMessage(ChatColor.RED + (isEn + ? "Player '" + targetName + "' was not found!" + : "Spieler '" + targetName + "' wurde nicht gefunden!")); + return true; + } + + final OfflinePlayer finalTarget = target; + final String uuidStr = finalTarget.getUniqueId().toString(); + final String displayName = finalTarget.getName() != null ? finalTarget.getName() : targetName; + + // Zählen (async, da ggf. DB-Abfragen) + new BukkitRunnable() { + @Override + public void run() { + int inputCount, targetCount, restCount, trashCount; + + if (mysqlEnabled && mysqlManager != null) { + inputCount = mysqlManager.getInputChests(uuidStr).size(); + targetCount = mysqlManager.getTargetChests(uuidStr).size(); + restCount = mysqlManager.countRestChests(uuidStr); + trashCount = mysqlManager.getTrashChest(uuidStr, serverName) != null ? 1 : 0; + } else { + String inputPath = "players." + uuidStr + ".input-chests"; + inputCount = (playerData.contains(inputPath) && playerData.isConfigurationSection(inputPath)) + ? playerData.getConfigurationSection(inputPath).getKeys(false).size() : 0; + + String tPath = "players." + uuidStr + ".target-chests"; + int tc = 0; + if (playerData.contains(tPath) && playerData.isConfigurationSection(tPath)) { + for (String item : playerData.getConfigurationSection(tPath).getKeys(false)) { + String itemBase = tPath + "." + item; + if (playerData.contains(itemBase + ".world")) { + tc++; + } else if (playerData.isConfigurationSection(itemBase)) { + tc += playerData.getConfigurationSection(itemBase).getKeys(false).size(); + } + } + } + targetCount = tc; + + String restPath = "players." + uuidStr + ".rest-chests"; + restCount = (playerData.contains(restPath) && playerData.isConfigurationSection(restPath)) + ? playerData.getConfigurationSection(restPath).getKeys(false).size() : 0; + + trashCount = playerData.contains("players." + uuidStr + ".trash-chest.world") ? 1 : 0; + } + + // Limits bestimmen + // Spieler ist offline → Limits können nicht per Permission geprüft werden + // → nur die Anzahl anzeigen, kein " / X" + final boolean isOffline = !finalTarget.isOnline(); + + String inputMax, targetMax, restMax; + final String trashMax = "1"; + + if (isOffline || !chestLimitsEnabled) { + // Offline: kein Limit anzeigen + // Limits deaktiviert: unbegrenzt + inputMax = isOffline ? null : "*"; + targetMax = isOffline ? null : "*"; + restMax = isOffline ? null : "*"; + } else { + Player onlineTarget = (Player) finalTarget; + if (onlineTarget.isOp() || onlineTarget.hasPermission("autosortchest.limit.bypass")) { + inputMax = "*"; + targetMax = "*"; + restMax = "*"; + } else { + int iMax = getChestLimitForPlayer(onlineTarget, "input"); + int tMax = getChestLimitForPlayer(onlineTarget, "target"); + int rMax = getChestLimitForPlayer(onlineTarget, "rest"); + inputMax = (iMax == Integer.MAX_VALUE) ? "*" : String.valueOf(iMax); + targetMax = (tMax == Integer.MAX_VALUE) ? "*" : String.valueOf(tMax); + restMax = (rMax == Integer.MAX_VALUE) ? "*" : String.valueOf(rMax); + } + } + + // Sprachabhängige Label + final String labelPlayer = isEn ? "Player: " : "Spieler: "; + final String labelTarget = isEn ? "Target: " : "Ziel: "; + final String labelTrash = isEn ? "Trash: " : "Müll: "; + + final String fInputMax = inputMax, fTargetMax = targetMax, fRestMax = restMax; + final int fIn = inputCount, fTa = targetCount, fRe = restCount, fTr = trashCount; + new BukkitRunnable() { + @Override + public void run() { + sender.sendMessage(ChatColor.GOLD + "================================"); + sender.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "==== AutoSortChest Info ===="); + sender.sendMessage(ChatColor.YELLOW + labelPlayer + ChatColor.WHITE + displayName + + (isOffline ? ChatColor.GRAY + " (offline)" : "")); + sender.sendMessage(ChatColor.YELLOW + "Input: " + ChatColor.WHITE + + (fInputMax != null ? fIn + " / " + fInputMax : String.valueOf(fIn))); + sender.sendMessage(ChatColor.YELLOW + labelTarget + ChatColor.WHITE + + (fTargetMax != null ? fTa + " / " + fTargetMax : String.valueOf(fTa))); + sender.sendMessage(ChatColor.YELLOW + "Rest: " + ChatColor.WHITE + + (fRestMax != null ? fRe + " / " + fRestMax : String.valueOf(fRe))); + sender.sendMessage(ChatColor.YELLOW + labelTrash + ChatColor.WHITE + + (isOffline ? String.valueOf(fTr) : fTr + " / " + trashMax)); + sender.sendMessage(ChatColor.GOLD + "================================"); + } + }.runTask(Main.this); + } + }.runTaskAsynchronously(this); + return true; + } + // ------------------------------------------------------- // /asc import – YAML → MySQL // ------------------------------------------------------- @@ -2971,13 +3117,20 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor { return; } // Filter-Info in der Action-Bar anzeigen - List filter = trashChestManager.getFilter(trashOwnerDirect); + List filter = trashChestManager.getFilter(trashOwnerDirect); String filterInfo; if (filter.isEmpty()) { filterInfo = getMessage("trash-info-empty"); } else { String itemList = String.join(", ", - filter.stream().map(TrashChestManager::formatMaterialName).collect(java.util.stream.Collectors.toList())); + filter.stream() + .map(fi -> { + if (fi.hasItemMeta() && fi.getItemMeta().hasDisplayName()) { + return ChatColor.stripColor(fi.getItemMeta().getDisplayName()); + } + return TrashChestManager.formatMaterialName(fi.getType().name()); + }) + .collect(java.util.stream.Collectors.toList())); filterInfo = getMessage("trash-info-filter").replace("%items%", itemList); } player.sendMessage(filterInfo); diff --git a/src/main/java/com/viper/autosortchest/TrashChestManager.java b/src/main/java/com/viper/autosortchest/TrashChestManager.java index bde4380..f5b0515 100644 --- a/src/main/java/com/viper/autosortchest/TrashChestManager.java +++ b/src/main/java/com/viper/autosortchest/TrashChestManager.java @@ -7,6 +7,7 @@ import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Chest; import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -14,16 +15,26 @@ import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.inventory.meta.SuspiciousStewMeta; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; import org.bukkit.profile.PlayerProfile; import org.bukkit.profile.PlayerTextures; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,38 +43,48 @@ import java.util.UUID; /** * Verwaltet Mülltruhen für das AutoSortChest-Plugin. * - * Speicherung: - * MySQL (wenn aktiv): - * asc_trash_chests : uuid, world, x, y, z, server - * asc_trash_items : uuid, item - * YAML (Fallback): - * players..trash-chest.world / x / y / z - * players..trash-items (StringList) + * Filter-Logik: + * Jedes Item wird exakt erkannt (Typ + Verzauberungen + Name + Lore). + * Ein verzaubertes Item ist ein eigener Filter-Eintrag. + * Vergleich über ItemStack.isSimilar() – ignoriert nur die Stapelmenge. + * + * Speicherung (Base64-serialisierte ItemStacks): + * MySQL: asc_trash_items (item-Spalte = Base64-String) + * YAML: players..trash-items (Liste von Base64-Strings) + * + * Legacy-Fallback: + * Alte Material-Name-Einträge (z.B. "IRON_SWORD") werden beim Laden + * automatisch als normaler ItemStack ohne Meta importiert. */ public class TrashChestManager { private static final String SKULL_TEXTURE = - "http://textures.minecraft.net/texture/942e7fb9b8eae22d55e32b8222f38eca7b2c41948b15d769b716d80f9d113611"; + "http://textures.minecraft.net/texture/32518d04f9c06c95dd0edad617abb93d3d8657f01e659079d330cca6f65bccf7"; - /** Gibt den sprachabhängigen GUI-Titel zurück (Farbe aus chest-titles.trash). */ private String getGuiTitle() { - boolean isEn = "en".equalsIgnoreCase(plugin.getConfig().getString("language", "de")); String colorPrefix = getChestTitleColor(); - String label = isEn ? "Configure Trash Chest" : "Mülltruhe konfigurieren"; + String label = isEnglish() ? "Configure Trash Chest" : "Mülltruhe konfigurieren"; return colorPrefix + ChatColor.BOLD + label; } + private boolean isEnglish() { + return "en".equalsIgnoreCase(plugin.getConfig().getString("language", "de")); + } + private final Main plugin; - /** UUID → Truhen-Location (In-Memory-Cache für diesen Server) */ - private final Map trashChestLocations = new HashMap<>(); - /** UUID → Filter-Liste */ - private final Map> trashFilterLists = new HashMap<>(); + /** UUID → Truhen-Location */ + private final Map trashChestLocations = new HashMap<>(); + /** UUID → Filter-Liste (echte ItemStacks, Menge immer 1) */ + private final Map> trashFilterLists = new HashMap<>(); /** Location-Key → Besitzer-UUID */ - private final Map locationToOwner = new HashMap<>(); + private final Map locationToOwner = new HashMap<>(); /** Spieler-UUID → Truhen-Besitzer-UUID (offene GUIs) */ - private final Map openGuiOwners = new HashMap<>(); + private final Map openGuiOwners = new HashMap<>(); + /** Spieler-UUID → aktuelle GUI-Seite (0-basiert) */ + private final Map playerPages = new HashMap<>(); + private static final int ITEMS_PER_PAGE = 45; private BukkitTask autoTrashTask = null; public TrashChestManager(Main plugin) { @@ -72,6 +93,63 @@ public class TrashChestManager { plugin.getServer().getPluginManager().registerEvents(new GuiListener(), plugin); } + // ══════════════════════════════════════════════════════════════════════════ + // SERIALISIERUNG (Base64 ↔ ItemStack) + // ══════════════════════════════════════════════════════════════════════════ + + /** Serialisiert einen ItemStack (Menge = 1) in einen Base64-String. */ + private static String itemToBase64(ItemStack item) { + try { + ItemStack copy = item.clone(); + copy.setAmount(1); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BukkitObjectOutputStream dataOut = new BukkitObjectOutputStream(out); + dataOut.writeObject(copy); + dataOut.close(); + return Base64.getEncoder().encodeToString(out.toByteArray()); + } catch (Exception e) { + return null; + } + } + + /** Deserialisiert einen Base64-String zurück in einen ItemStack. Null bei Fehler. */ + private static ItemStack itemFromBase64(String data) { + try { + byte[] bytes = Base64.getDecoder().decode(data); + ByteArrayInputStream in = new ByteArrayInputStream(bytes); + BukkitObjectInputStream dataIn = new BukkitObjectInputStream(in); + ItemStack item = (ItemStack) dataIn.readObject(); + dataIn.close(); + return item; + } catch (Exception e) { + return null; + } + } + + /** + * Konvertiert einen gespeicherten String in einen ItemStack. + * Unterstützt Base64 (neu) und rohe Materialnamen (Legacy). + */ + private static ItemStack parseFilterEntry(String entry) { + if (entry == null || entry.isEmpty()) return null; + // Base64 versuchen + ItemStack fromBase64 = itemFromBase64(entry); + if (fromBase64 != null) return fromBase64; + // Legacy: reiner Materialname + Material mat = Material.matchMaterial(entry); + return (mat != null && mat != Material.AIR) ? new ItemStack(mat, 1) : null; + } + + /** Konvertiert die interne Filter-Liste in Base64-Strings zur Speicherung. */ + private static List serializeFilter(List items) { + List result = new ArrayList<>(); + for (ItemStack item : items) { + String b64 = itemToBase64(item); + if (b64 != null) result.add(b64); + } + return result; + } + // ══════════════════════════════════════════════════════════════════════════ // LADEN // ══════════════════════════════════════════════════════════════════════════ @@ -94,10 +172,17 @@ public class TrashChestManager { UUID uuid = UUID.fromString((String) row.get("uuid")); World world = Bukkit.getWorld((String) row.get("world")); if (world == null) continue; - Location loc = new Location(world, (int) row.get("x"), (int) row.get("y"), (int) row.get("z")); + Location loc = new Location(world, + (int) row.get("x"), (int) row.get("y"), (int) row.get("z")); trashChestLocations.put(uuid, loc); locationToOwner.put(locKey(loc), uuid); - trashFilterLists.put(uuid, new ArrayList<>(db.getTrashItems(uuid.toString()))); + + List items = new ArrayList<>(); + for (String entry : db.getTrashItems(uuid.toString())) { + ItemStack parsed = parseFilterEntry(entry); + if (parsed != null) items.add(parsed); + } + trashFilterLists.put(uuid, items); } catch (IllegalArgumentException ignored) {} } } @@ -113,10 +198,18 @@ public class TrashChestManager { World world = Bukkit.getWorld(data.getString(base + ".world")); if (world == null) continue; Location loc = new Location(world, - data.getInt(base + ".x"), data.getInt(base + ".y"), data.getInt(base + ".z")); + data.getInt(base + ".x"), + data.getInt(base + ".y"), + data.getInt(base + ".z")); trashChestLocations.put(uuid, loc); locationToOwner.put(locKey(loc), uuid); - trashFilterLists.put(uuid, new ArrayList<>(data.getStringList("players." + uuidStr + ".trash-items"))); + + List items = new ArrayList<>(); + for (String entry : data.getStringList("players." + uuidStr + ".trash-items")) { + ItemStack parsed = parseFilterEntry(entry); + if (parsed != null) items.add(parsed); + } + trashFilterLists.put(uuid, items); } catch (IllegalArgumentException ignored) {} } } @@ -145,7 +238,8 @@ public class TrashChestManager { } else { db.removeTrashChest(uuidStr); } - db.setTrashItems(uuidStr, trashFilterLists.getOrDefault(uuid, new ArrayList<>())); + db.setTrashItems(uuidStr, serializeFilter( + trashFilterLists.getOrDefault(uuid, new ArrayList<>()))); } private void saveYaml(UUID uuid) { @@ -162,7 +256,7 @@ public class TrashChestManager { data.set("players." + uuidStr + ".trash-chest", null); } data.set("players." + uuidStr + ".trash-items", - trashFilterLists.getOrDefault(uuid, new ArrayList<>())); + serializeFilter(trashFilterLists.getOrDefault(uuid, new ArrayList<>()))); plugin.savePlayerDataPublic(); } @@ -194,37 +288,45 @@ public class TrashChestManager { } } - public UUID getTrashChestOwner(Location loc) { - return locationToOwner.get(locKey(loc)); - } - - public Location getTrashChestLocation(UUID uuid) { - return trashChestLocations.get(uuid); - } - - public Map getAllTrashChests() { - return new HashMap<>(trashChestLocations); - } + public UUID getTrashChestOwner(Location loc) { return locationToOwner.get(locKey(loc)); } + public Location getTrashChestLocation(UUID uuid) { return trashChestLocations.get(uuid); } + public Map getAllTrashChests() { return new HashMap<>(trashChestLocations); } + /** + * Schneller Typ-Check: prüft ob ein Material überhaupt im Filter vorkommt. + * Wird vom Sortier-System benutzt (kein isSimilar nötig, nur Typ). + */ public boolean isTrashItem(UUID uuid, Material mat) { if (!trashChestLocations.containsKey(uuid)) return false; - List filter = trashFilterLists.getOrDefault(uuid, new ArrayList<>()); - // Leerer Filter = keine Items werden weitergeleitet (Mülltruhe deaktiviert) - return !filter.isEmpty() && filter.contains(mat.name()); + List filter = trashFilterLists.getOrDefault(uuid, new ArrayList<>()); + if (filter.isEmpty()) return false; + for (ItemStack fi : filter) { + if (fi.getType() == mat) return true; + } + return false; } // ══════════════════════════════════════════════════════════════════════════ // ITEM-VERARBEITUNG // ══════════════════════════════════════════════════════════════════════════ + /** + * Löscht alle Items aus der Truhen-Inventory die exakt einem Filter-Eintrag entsprechen. + * Vergleich: ItemStack.isSimilar() → Typ + Verzauberungen + Name + Lore müssen passen. + * Die Stapelmenge wird beim Vergleich ignoriert. + */ public void processTrashChestInventory(UUID ownerUUID, Inventory inv) { - List filter = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>()); - // Leerer Filter = keine Items löschen (Mülltruhe deaktiviert bis Items konfiguriert sind) + List filter = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>()); if (filter.isEmpty()) return; for (int i = 0; i < inv.getSize(); i++) { ItemStack item = inv.getItem(i); if (item == null || item.getType() == Material.AIR) continue; - if (filter.contains(item.getType().name())) inv.setItem(i, null); + for (ItemStack filterItem : filter) { + if (filterItem.isSimilar(item)) { + inv.setItem(i, null); + break; + } + } } } @@ -238,34 +340,76 @@ public class TrashChestManager { // FILTER-LISTE // ══════════════════════════════════════════════════════════════════════════ - public boolean addToFilter(UUID uuid, Material mat) { - List filter = trashFilterLists.computeIfAbsent(uuid, k -> new ArrayList<>()); - if (filter.contains(mat.name())) return false; - filter.add(mat.name()); + /** + * Fügt ein Item exakt (inkl. Verzauberungen, Name, Lore) zum Filter hinzu. + * Menge wird auf 1 normiert. Duplikate (isSimilar) werden abgelehnt. + * + * @return true wenn neu hinzugefügt, false wenn bereits vorhanden + */ + public boolean addToFilter(UUID uuid, ItemStack item) { + ItemStack normalized = item.clone(); + normalized.setAmount(1); + + List filter = trashFilterLists.computeIfAbsent(uuid, k -> new ArrayList<>()); + for (ItemStack existing : filter) { + if (existing.isSimilar(normalized)) return false; // Duplikat + } + + filter.add(normalized); + String b64 = itemToBase64(normalized); + if (b64 != null) { + if (plugin.isMysqlEnabled() && plugin.getMysqlManager() != null) { + plugin.getMysqlManager().addTrashItem(uuid.toString(), b64); + } else { + saveTrashChest(uuid); + } + } + return true; + } + + /** + * Entfernt den ersten Filter-Eintrag der isSimilar zum übergebenen Item ist. + * + * @return true wenn ein Eintrag entfernt wurde + */ + public boolean removeFromFilter(UUID uuid, ItemStack item) { + List filter = trashFilterLists.get(uuid); + if (filter == null) return false; + + ItemStack toRemove = null; + for (ItemStack existing : filter) { + if (existing.isSimilar(item)) { toRemove = existing; break; } + } + if (toRemove == null) return false; + + filter.remove(toRemove); if (plugin.isMysqlEnabled() && plugin.getMysqlManager() != null) { - plugin.getMysqlManager().addTrashItem(uuid.toString(), mat.name()); + // Komplette Liste neu schreiben (kein "remove single by item" in MySQLManager) + plugin.getMysqlManager().setTrashItems(uuid.toString(), serializeFilter(filter)); } else { saveTrashChest(uuid); } return true; } - public boolean removeFromFilter(UUID uuid, Material mat) { - List filter = trashFilterLists.get(uuid); - if (filter == null) return false; - boolean removed = filter.remove(mat.name()); - if (removed) { - if (plugin.isMysqlEnabled() && plugin.getMysqlManager() != null) { - plugin.getMysqlManager().removeTrashItem(uuid.toString(), mat.name()); - } else { - saveTrashChest(uuid); - } - } - return removed; + public List getFilter(UUID uuid) { + return trashFilterLists.getOrDefault(uuid, new ArrayList<>()); } - public List getFilter(UUID uuid) { - return trashFilterLists.getOrDefault(uuid, new ArrayList<>()); + /** + * Entfernt einen Filter-Eintrag direkt per Index (0-basiert). + * Wird vom GUI-Listener genutzt um das modifizierte Display-Item sicher zu entfernen. + */ + public boolean removeFromFilterByIndex(UUID uuid, int index) { + List filter = trashFilterLists.get(uuid); + if (filter == null || index < 0 || index >= filter.size()) return false; + filter.remove(index); + if (plugin.isMysqlEnabled() && plugin.getMysqlManager() != null) { + plugin.getMysqlManager().setTrashItems(uuid.toString(), serializeFilter(filter)); + } else { + saveTrashChest(uuid); + } + return true; } // ══════════════════════════════════════════════════════════════════════════ @@ -300,57 +444,198 @@ public class TrashChestManager { // ══════════════════════════════════════════════════════════════════════════ public void openConfigGui(Player player, UUID ownerUUID) { - openGuiOwners.put(player.getUniqueId(), ownerUUID); - Inventory gui = Bukkit.createInventory(null, 54, getGuiTitle()); + openConfigGui(player, ownerUUID, playerPages.getOrDefault(player.getUniqueId(), 0)); + } - List filter = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>()); - int displaySlot = 0; - for (String matName : filter) { - if (displaySlot >= 45) break; - Material mat = Material.matchMaterial(matName); - if (mat == null || mat == Material.AIR) continue; - ItemStack display = new ItemStack(mat, 1); - ItemMeta meta = display.getItemMeta(); - if (meta != null) { - meta.setDisplayName(getSignColor("trash", "line1") + formatMaterialName(matName)); - meta.setLore(Arrays.asList(getSignColor("trash", "line4") + "Rechtsklick: Entfernen")); - display.setItemMeta(meta); - } - gui.setItem(displaySlot++, display); + public void openConfigGui(Player player, UUID ownerUUID, int page) { + openGuiOwners.put(player.getUniqueId(), ownerUUID); + boolean isEn = isEnglish(); + + List filter = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>()); + + // Gültige Items vorfiltern + List validItems = new ArrayList<>(); + for (ItemStack fi : filter) { + if (fi != null && fi.getType() != Material.AIR) validItems.add(fi); } + int totalPages = Math.max(1, (int) Math.ceil(validItems.size() / (double) ITEMS_PER_PAGE)); + if (page < 0) page = 0; + if (page >= totalPages) page = totalPages - 1; + playerPages.put(player.getUniqueId(), page); + + Inventory gui = Bukkit.createInventory(null, 54, getGuiTitle()); + + // ── Items der aktuellen Seite ──────────────────────────────────────── + int startIndex = page * ITEMS_PER_PAGE; + int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, validItems.size()); + for (int i = startIndex; i < endIndex; i++) { + ItemStack filterItem = validItems.get(i); + // Clone mit Menge 1 + ItemStack display = filterItem.clone(); + display.setAmount(1); + + ItemMeta meta = display.getItemMeta(); + if (meta != null) { + // Anzeige-Namen setzen wenn kein eigener vorhanden + if (!meta.hasDisplayName()) { + meta.setDisplayName(getSignColor("trash", "line1") + "" + ChatColor.BOLD + + formatMaterialName(filterItem.getType().name())); + } + + List lore = new ArrayList<>(); + + // ── Verzauberungen (normale Items) ───────────────────────────── + Map enchants = meta.getEnchants(); + if (!enchants.isEmpty()) { + for (Map.Entry entry : enchants.entrySet()) { + String enchName = formatEnchantmentName(entry.getKey().getKey().getKey()); + lore.add(ChatColor.AQUA + enchName + " " + toRoman(entry.getValue())); + } + lore.add(""); + } + + // ── Verzauberungen (Zauberbücher: EnchantmentStorageMeta) ─────── + if (meta instanceof EnchantmentStorageMeta esm) { + Map stored = esm.getStoredEnchants(); + if (!stored.isEmpty()) { + for (Map.Entry entry : stored.entrySet()) { + String enchName = formatEnchantmentName(entry.getKey().getKey().getKey()); + lore.add(ChatColor.AQUA + enchName + " " + toRoman(entry.getValue())); + } + lore.add(""); + } + } + + // ── Tränkeffekte (Trank, Wurftrank, Pfeil: PotionMeta) ────────── + if (meta instanceof PotionMeta pm) { + List effects = pm.getCustomEffects(); + if (!effects.isEmpty()) { + for (PotionEffect effect : effects) { + lore.add(ChatColor.LIGHT_PURPLE + formatEffectName(effect.getType()) + + " " + toRoman(effect.getAmplifier() + 1) + + ChatColor.GRAY + " (" + formatDuration(effect.getDuration()) + ")"); + } + lore.add(""); + } + } + + // ── Seltsame Suppe (SuspiciousStewMeta) ───────────────────────── + if (meta instanceof SuspiciousStewMeta ssm) { + List effects = ssm.getCustomEffects(); + if (!effects.isEmpty()) { + for (PotionEffect effect : effects) { + lore.add(ChatColor.LIGHT_PURPLE + formatEffectName(effect.getType()) + + " " + toRoman(effect.getAmplifier() + 1) + + ChatColor.GRAY + " (" + formatDuration(effect.getDuration()) + ")"); + } + lore.add(""); + } + } + + // ── Bestehende Item-Lore ───────────────────────────────────────── + if (meta.hasLore()) { + lore.addAll(meta.getLore()); + lore.add(""); + } + + lore.add(ChatColor.RED + (isEn ? "▶ Right-click: Remove" : "▶ Rechtsklick: Entfernen")); + meta.setLore(lore); + display.setItemMeta(meta); + } + gui.setItem(i - startIndex, display); + } + + // ── Trennleiste (Zeile 6) ───────────────────────────────────────────── ItemStack filler = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); ItemMeta fillerMeta = filler.getItemMeta(); if (fillerMeta != null) { fillerMeta.setDisplayName(" "); filler.setItemMeta(fillerMeta); } for (int i = 45; i <= 53; i++) gui.setItem(i, filler.clone()); - ItemStack modeInfo = new ItemStack(filter.isEmpty() ? Material.BARRIER : Material.WATER_BUCKET); + // ── Vorherige Seite (Slot 45) ────────────────────────────────────────── + if (page > 0) { + ItemStack prev = new ItemStack(Material.ARROW); + ItemMeta prevMeta = prev.getItemMeta(); + if (prevMeta != null) { + prevMeta.setDisplayName(ChatColor.YELLOW + "" + ChatColor.BOLD + + (isEn ? "◀ Previous Page" : "◀ Vorherige Seite")); + prevMeta.setLore(Arrays.asList(ChatColor.GRAY + + (isEn ? "Page " : "Seite ") + page + (isEn ? " of " : " von ") + totalPages)); + prev.setItemMeta(prevMeta); + } + gui.setItem(45, prev); + } + + // ── Seitenanzeige (Slot 46) ──────────────────────────────────────────── + ItemStack pageInfo = new ItemStack(Material.PAPER); + ItemMeta pageMeta = pageInfo.getItemMeta(); + if (pageMeta != null) { + pageMeta.setDisplayName(ChatColor.WHITE + "" + ChatColor.BOLD + + (isEn ? "Page " : "Seite ") + (page + 1) + " / " + totalPages); + pageMeta.setLore(Arrays.asList(ChatColor.GRAY + "" + validItems.size() + + (isEn ? " items in filter" : " Items im Filter"))); + pageInfo.setItemMeta(pageMeta); + } + gui.setItem(46, pageInfo); + + // ── Nächste Seite (Slot 47) ──────────────────────────────────────────── + if (page < totalPages - 1) { + ItemStack next = new ItemStack(Material.ARROW); + ItemMeta nextMeta = next.getItemMeta(); + if (nextMeta != null) { + nextMeta.setDisplayName(ChatColor.YELLOW + "" + ChatColor.BOLD + + (isEn ? "Next Page ▶" : "Nächste Seite ▶")); + nextMeta.setLore(Arrays.asList(ChatColor.GRAY + + (isEn ? "Page " : "Seite ") + (page + 2) + (isEn ? " of " : " von ") + totalPages)); + next.setItemMeta(nextMeta); + } + gui.setItem(47, next); + } + + // ── Modus-Info (Slot 48) ─────────────────────────────────────────────── + ItemStack modeInfo = new ItemStack(filter.isEmpty() ? Material.BARRIER : Material.HOPPER); ItemMeta modeMeta = modeInfo.getItemMeta(); if (modeMeta != null) { if (filter.isEmpty()) { - modeMeta.setDisplayName(getSignColor("trash", "line2") + "" + ChatColor.BOLD + "Modus: Deaktiviert"); + modeMeta.setDisplayName(ChatColor.RED + "" + ChatColor.BOLD + + (isEn ? "✗ Status: Disabled" : "✗ Status: Deaktiviert")); modeMeta.setLore(Arrays.asList( - ChatColor.GRAY + "Kein Filter gesetzt –", - ChatColor.GRAY + "Items werden NICHT gelöscht.", - getSignColor("trash", "line1") + "Füge Items hinzu um die Mülltruhe", - getSignColor("trash", "line1") + "zu aktivieren.")); + ChatColor.GRAY + (isEn ? "No filter set –" : "Kein Filter gesetzt –"), + ChatColor.GRAY + (isEn ? "items will NOT be deleted." : "Items werden NICHT gelöscht."), + ChatColor.YELLOW + (isEn + ? "Add items to activate the trash chest." + : "Füge Items hinzu um die Mülltruhe zu aktivieren."))); } else { - modeMeta.setDisplayName(getSignColor("trash", "line4") + "" + ChatColor.BOLD + "Modus: Filter aktiv"); + modeMeta.setDisplayName(ChatColor.GREEN + "" + ChatColor.BOLD + + (isEn ? "✔ Status: Active" : "✔ Status: Aktiv")); modeMeta.setLore(Arrays.asList( - ChatColor.GRAY + "Nur gefilterte Items", - ChatColor.GRAY + "werden gelöscht.")); + ChatColor.GRAY + (isEn + ? "Items are matched exactly:" + : "Items werden exakt verglichen:"), + ChatColor.GRAY + (isEn + ? "Type + enchantments + name must match." + : "Typ + Verzauberungen + Name müssen übereinstimmen."))); } modeInfo.setItemMeta(modeMeta); } gui.setItem(48, modeInfo); + // ── Item hinzufügen (Slot 49) ────────────────────────────────────────── ItemStack addBtn = new ItemStack(Material.LIME_STAINED_GLASS_PANE); ItemMeta addMeta = addBtn.getItemMeta(); if (addMeta != null) { - addMeta.setDisplayName(getSignColor("trash", "line1") + "" + ChatColor.BOLD + "Item hinzufügen"); + addMeta.setDisplayName(ChatColor.GREEN + "" + ChatColor.BOLD + + (isEn ? "✚ Add Item" : "✚ Item hinzufügen")); addMeta.setLore(Arrays.asList( - ChatColor.GRAY + "Item in die Hand nehmen", - ChatColor.GRAY + "und diesen Button klicken.")); + ChatColor.GRAY + (isEn + ? "Hold the exact item in your main hand" + : "Genau das Item in die Haupthand nehmen"), + ChatColor.GRAY + (isEn + ? "and click this button." + : "und diesen Button klicken."), + ChatColor.YELLOW + (isEn + ? "Enchantments & name are saved exactly." + : "Verzauberungen & Name werden exakt gespeichert."))); addBtn.setItemMeta(addMeta); } gui.setItem(49, addBtn); @@ -363,6 +648,11 @@ public class TrashChestManager { } private ItemStack buildSkullButton() { + boolean isEn = isEnglish(); + String label = isEn ? "Empty Trash Chest" : "Mülltruhe leeren"; + String lore1 = isEn ? "Click to immediately" : "Klicken um alle Items"; + String lore2 = isEn ? "delete all items." : "sofort zu löschen."; + ItemStack skull; try { skull = new ItemStack(Material.PLAYER_HEAD); @@ -373,20 +663,16 @@ public class TrashChestManager { textures.setSkin(new URL(SKULL_TEXTURE)); profile.setTextures(textures); meta.setOwnerProfile(profile); - meta.setDisplayName(getChestTitleColor() + "" + ChatColor.BOLD + "Mülltruhe leeren"); - meta.setLore(Arrays.asList( - ChatColor.GRAY + "Klicken um alle Items", - ChatColor.GRAY + "sofort zu löschen.")); + meta.setDisplayName(getChestTitleColor() + "" + ChatColor.BOLD + label); + meta.setLore(Arrays.asList(ChatColor.GRAY + lore1, ChatColor.GRAY + lore2)); skull.setItemMeta(meta); } } catch (Exception e) { skull = new ItemStack(Material.RED_DYE); ItemMeta meta = skull.getItemMeta(); if (meta != null) { - meta.setDisplayName(getChestTitleColor() + "" + ChatColor.BOLD + "Mülltruhe leeren"); - meta.setLore(Arrays.asList( - ChatColor.GRAY + "Klicken um alle Items", - ChatColor.GRAY + "sofort zu löschen.")); + meta.setDisplayName(getChestTitleColor() + "" + ChatColor.BOLD + label); + meta.setLore(Arrays.asList(ChatColor.GRAY + lore1, ChatColor.GRAY + lore2)); skull.setItemMeta(meta); } } @@ -409,21 +695,39 @@ public class TrashChestManager { UUID ownerUUID = openGuiOwners.get(player.getUniqueId()); if (ownerUUID == null) return; + // ── Mülltruhe leeren ────────────────────────────────────────────── if (clickedSlot == 53) { clearTrashChest(ownerUUID); player.sendMessage(getMessage("trash-cleared")); return; } + // ── Vorherige Seite ─────────────────────────────────────────────── + if (clickedSlot == 45) { + int currentPage = playerPages.getOrDefault(player.getUniqueId(), 0); + if (currentPage > 0) openConfigGui(player, ownerUUID, currentPage - 1); + return; + } + + // ── Nächste Seite ───────────────────────────────────────────────── + if (clickedSlot == 47) { + int currentPage = playerPages.getOrDefault(player.getUniqueId(), 0); + List f = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>()); + int totalPages = Math.max(1, (int) Math.ceil(f.size() / (double) ITEMS_PER_PAGE)); + if (currentPage < totalPages - 1) openConfigGui(player, ownerUUID, currentPage + 1); + return; + } + + // ── Item hinzufügen ─────────────────────────────────────────────── if (clickedSlot == 49) { ItemStack inHand = player.getInventory().getItemInMainHand(); if (inHand.getType() == Material.AIR) { player.sendMessage(getMessage("no-item-in-hand")); return; } - Material mat = inHand.getType(); - if (addToFilter(ownerUUID, mat)) { - player.sendMessage(getMessage("trash-item-added").replace("%item%", formatMaterialName(mat.name()))); + if (addToFilter(ownerUUID, inHand)) { + player.sendMessage(getMessage("trash-item-added") + .replace("%item%", getItemDisplayName(inHand))); } else { player.sendMessage(getMessage("trash-item-already")); } @@ -433,12 +737,24 @@ public class TrashChestManager { if (clickedSlot >= 45) return; + // ── Rechtsklick: Item per Index entfernen ───────────────────────── + // WICHTIG: Display-Item hat modifizierte Lore → isSimilar() würde fehlschlagen. + // Stattdessen: Slot-Position → Filter-Index berechnen und direkt entfernen. if (event.isRightClick()) { ItemStack clicked = event.getCurrentItem(); if (clicked == null || clicked.getType() == Material.AIR) return; - Material mat = clicked.getType(); - if (removeFromFilter(ownerUUID, mat)) { - player.sendMessage(getMessage("trash-item-removed").replace("%item%", formatMaterialName(mat.name()))); + + int currentPage = playerPages.getOrDefault(player.getUniqueId(), 0); + int filterIndex = currentPage * ITEMS_PER_PAGE + clickedSlot; + + // Original-Item für die Chat-Nachricht holen (vor dem Entfernen) + List filterList = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>()); + String itemName = (filterIndex < filterList.size()) + ? getItemDisplayName(filterList.get(filterIndex)) + : getItemDisplayName(clicked); + + if (removeFromFilterByIndex(ownerUUID, filterIndex)) { + player.sendMessage(getMessage("trash-item-removed").replace("%item%", itemName)); openConfigGui(player, ownerUUID); } } @@ -447,7 +763,15 @@ public class TrashChestManager { @EventHandler public void onInventoryClose(InventoryCloseEvent event) { if (event.getPlayer() instanceof Player player) { - openGuiOwners.remove(player.getUniqueId()); + UUID playerUUID = player.getUniqueId(); + // 1-Tick Verzögerung: verhindert Löschung beim Seitenwechsel + Bukkit.getScheduler().runTaskLater(plugin, () -> { + String openTitle = player.getOpenInventory().getTitle(); + if (!getGuiTitle().equals(openTitle)) { + openGuiOwners.remove(playerUUID); + playerPages.remove(playerUUID); + } + }, 1L); } } } @@ -461,37 +785,39 @@ public class TrashChestManager { return org.bukkit.ChatColor.translateAlternateColorCodes('&', msg); } - /** - * Liest eine Schildfarbe aus sign-colors.<type>.<line> in der Config. - * Gibt übersetzten §-Code zurück (z.B. §6 für &6). - */ private String getSignColor(String type, String line) { String raw = plugin.getConfig().getString("sign-colors." + type + "." + line, "&f"); return ChatColor.translateAlternateColorCodes('&', raw); } - /** - * Liest nur den Farb-Präfix aus chest-titles.trash (ohne Titeltext). - * Gibt übersetzten §-Code zurück. - */ private String getChestTitleColor() { - boolean isEn = "en".equalsIgnoreCase(plugin.getConfig().getString("language", "de")); + boolean isEn = isEnglish(); String lang = isEn ? "en" : "de"; String full = plugin.getConfig().getString("chest-titles.trash." + lang, isEn ? "&4Trash Chest" : "&4Mülltruhe"); - // Nur die führenden &X / &l Codes extrahieren StringBuilder codes = new StringBuilder(); for (int i = 0; i + 1 < full.length(); i++) { if (full.charAt(i) == '&' && "0123456789abcdefklmnor".indexOf(full.charAt(i + 1)) >= 0) { codes.append(full, i, i + 2); - i++; // Zeichen überspringen + i++; } else { - break; // erster Nicht-Code-Zeichenblock → abbrechen + break; } } return ChatColor.translateAlternateColorCodes('&', codes.length() > 0 ? codes.toString() : "&4"); } + /** + * Lesbarer Anzeige-Name für Chat-Nachrichten. + * Nutzt Custom Display Name falls vorhanden, sonst formatierten Material-Namen. + */ + private String getItemDisplayName(ItemStack item) { + if (item.hasItemMeta() && item.getItemMeta().hasDisplayName()) { + return ChatColor.stripColor(item.getItemMeta().getDisplayName()); + } + return formatMaterialName(item.getType().name()); + } + private String locKey(Location loc) { return loc.getWorld().getName() + ":" + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ(); } @@ -509,4 +835,79 @@ public class TrashChestManager { } return sb.toString(); } + + /** Formatiert einen Enchantment-Key wie "sharpness" → "Sharpness". */ + private static String formatEnchantmentName(String key) { + if (key == null || key.isEmpty()) return ""; + String[] parts = key.split("_"); + StringBuilder sb = new StringBuilder(); + for (String part : parts) { + if (sb.length() > 0) sb.append(' '); + if (!part.isEmpty()) { + sb.append(Character.toUpperCase(part.charAt(0))); + if (part.length() > 1) sb.append(part.substring(1).toLowerCase()); + } + } + return sb.toString(); + } + + /** Formatiert einen PotionEffectType lesbar (z.B. "BLINDNESS" → "Blindheit" / "Blindness"). */ + private String formatEffectName(PotionEffectType type) { + boolean isEn = isEnglish(); + String key = type.getKey().getKey().toLowerCase(); + if (!isEn) { + // Deutsche Übersetzungen der häufigsten Effekte + switch (key) { + case "speed": return "Geschwindigkeit"; + case "slowness": return "Langsamkeit"; + case "haste": return "Eile"; + case "mining_fatigue": return "Schwere"; + case "strength": return "Stärke"; + case "instant_health": return "Sofortige Heilung"; + case "instant_damage": return "Sofortiger Schaden"; + case "jump_boost": return "Sprungkraft"; + case "nausea": return "Übelkeit"; + case "regeneration": return "Regeneration"; + case "resistance": return "Resistenz"; + case "fire_resistance": return "Feuerschutz"; + case "water_breathing": return "Wasseratmung"; + case "invisibility": return "Unsichtbarkeit"; + case "blindness": return "Blindheit"; + case "night_vision": return "Nachtsicht"; + case "hunger": return "Hunger"; + case "weakness": return "Schwäche"; + case "poison": return "Gift"; + case "wither": return "Wither"; + case "health_boost": return "Gesundheitsschub"; + case "absorption": return "Absorption"; + case "saturation": return "Sättigung"; + case "glowing": return "Leuchten"; + case "levitation": return "Schweben"; + case "luck": return "Glück"; + case "unluck": return "Pech"; + case "slow_falling": return "Langsamer Fall"; + case "conduit_power": return "Leitungskraft"; + case "dolphins_grace": return "Delfingnade"; + case "bad_omen": return "Schlechtes Omen"; + case "hero_of_the_village":return "Dorfheld"; + case "darkness": return "Dunkelheit"; + default: break; + } + } + return formatEnchantmentName(key); + } + + /** Wandelt Ticks in ein lesbares mm:ss-Format um (z.B. 220 → "00:11"). */ + private static String formatDuration(int ticks) { + int totalSeconds = ticks / 20; + int minutes = totalSeconds / 60; + int seconds = totalSeconds % 60; + return String.format("%02d:%02d", minutes, seconds); + } + + /** Konvertiert eine Zahl in römische Ziffern (1–10). */ + private static String toRoman(int level) { + String[] roman = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"}; + return (level >= 1 && level <= 10) ? roman[level] : String.valueOf(level); + } } \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index ee8cfcb..df20ed0 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: AutoSortChest -version: 2.5 +version: 2.6 main: com.viper.autosortchest.Main api-version: 1.21 authors: [M_Viper] @@ -7,7 +7,7 @@ description: Ein Plugin zum automatischen Sortieren von Items in Truhen commands: asc: description: AutoSortChest Befehle - usage: / [help|info|reload|import|export] + usage: / [help|info|reload|import|export|list] aliases: [autosortchest] permissions: autosortchest.use: @@ -31,6 +31,9 @@ permissions: autosortchest.admin: description: Erlaubt OPs/Admins Zugriff auf fremde AutoSortChest-Truhen default: op + autosortchest.list: + description: Erlaubt die Verwendung von /asc list um Truhen-Statistiken einzusehen + default: op autosortchest.limit.: description: > Limits fuer eine benutzerdefinierte Gruppe aus der config.yml.