Update from Git Manager GUI

This commit is contained in:
2026-03-27 13:55:23 +01:00
parent 26aad1c54b
commit ec6c9f69db
3 changed files with 680 additions and 123 deletions

View File

@@ -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 <Spieler> &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 <player> &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 <player>"
: "Verwendung: /asc list <Spieler>"));
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<String> filter = trashChestManager.getFilter(trashOwnerDirect);
List<org.bukkit.inventory.ItemStack> 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);

View File

@@ -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.<uuid>.trash-chest.world / x / y / z
* players.<uuid>.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.<uuid>.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<UUID, Location> trashChestLocations = new HashMap<>();
/** UUID → Filter-Liste */
private final Map<UUID, List<String>> trashFilterLists = new HashMap<>();
/** UUID → Truhen-Location */
private final Map<UUID, Location> trashChestLocations = new HashMap<>();
/** UUID → Filter-Liste (echte ItemStacks, Menge immer 1) */
private final Map<UUID, List<ItemStack>> trashFilterLists = new HashMap<>();
/** Location-Key → Besitzer-UUID */
private final Map<String, UUID> locationToOwner = new HashMap<>();
private final Map<String, UUID> locationToOwner = new HashMap<>();
/** Spieler-UUID → Truhen-Besitzer-UUID (offene GUIs) */
private final Map<UUID, UUID> openGuiOwners = new HashMap<>();
private final Map<UUID, UUID> openGuiOwners = new HashMap<>();
/** Spieler-UUID → aktuelle GUI-Seite (0-basiert) */
private final Map<UUID, Integer> 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<String> serializeFilter(List<ItemStack> items) {
List<String> 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<ItemStack> 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<ItemStack> 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<UUID, Location> 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<UUID, Location> 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<String> filter = trashFilterLists.getOrDefault(uuid, new ArrayList<>());
// Leerer Filter = keine Items werden weitergeleitet (Mülltruhe deaktiviert)
return !filter.isEmpty() && filter.contains(mat.name());
List<ItemStack> 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<String> filter = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>());
// Leerer Filter = keine Items löschen (Mülltruhe deaktiviert bis Items konfiguriert sind)
List<ItemStack> 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<String> 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<ItemStack> 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<ItemStack> 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<String> 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<ItemStack> getFilter(UUID uuid) {
return trashFilterLists.getOrDefault(uuid, new ArrayList<>());
}
public List<String> 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<ItemStack> 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<String> 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<ItemStack> filter = trashFilterLists.getOrDefault(ownerUUID, new ArrayList<>());
// Gültige Items vorfiltern
List<ItemStack> 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<String> lore = new ArrayList<>();
// ── Verzauberungen (normale Items) ─────────────────────────────
Map<Enchantment, Integer> enchants = meta.getEnchants();
if (!enchants.isEmpty()) {
for (Map.Entry<Enchantment, Integer> 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<Enchantment, Integer> stored = esm.getStoredEnchants();
if (!stored.isEmpty()) {
for (Map.Entry<Enchantment, Integer> 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<PotionEffect> 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<PotionEffect> 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<ItemStack> 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<ItemStack> 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.&lt;type&gt;.&lt;line&gt; 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 (110). */
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);
}
}

View File

@@ -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: /<command> [help|info|reload|import|export]
usage: /<command> [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 <Spieler> um Truhen-Statistiken einzusehen
default: op
autosortchest.limit.<gruppe>:
description: >
Limits fuer eine benutzerdefinierte Gruppe aus der config.yml.