Update from Git Manager GUI

This commit is contained in:
2026-03-31 00:04:34 +02:00
parent 1e0765603d
commit b24fe51b3c
5 changed files with 993 additions and 154 deletions

View File

@@ -47,7 +47,7 @@ import java.util.stream.Collectors;
import com.viper.autosortchest.MySQLManager;
public class Main extends JavaPlugin implements Listener, CommandExecutor {
public class Main extends JavaPlugin implements Listener, CommandExecutor, org.bukkit.command.TabCompleter {
private boolean serverCrosslink = true;
@@ -138,9 +138,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
@Override
public void run() {
cleanMessageTracker();
flushPlayerData(); // FIX: Async-Flush alle 60s statt synchronem Save bei jeder Änderung
flushPlayerData(); // Async-Flush alle 30s
}
}.runTaskTimer(this, 20L * 60, 20L * 60);
}.runTaskTimer(this, 20L * 30, 20L * 30);
// ── BungeeCord NEU: Heartbeat alle 30 Sekunden (async) ────────────────
if (mysqlEnabled && mysqlManager != null && !serverName.isEmpty()) {
@@ -226,6 +226,10 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
// group → { "input", "rest", "target" } → limit
private Map<String, Map<String, Integer>> chestLimits;
private boolean chestLimitsEnabled = true;
// FIX: Cache für bereits migrierte Target-Einträge verhindert wiederholte YAML-Lookups
// im heißen Sort-Loop (isOldTargetFormat wird nach einmaliger Migration nie wieder true).
private final java.util.Set<String> migratedTargetItems = new java.util.HashSet<>();
private final Map<UUID, Map<Material, Long>> fullChestMessageTracker = new HashMap<>();
private static final long MESSAGE_COOLDOWN = 5 * 60 * 1000;
@@ -330,7 +334,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
private volatile boolean playerDataDirty = false;
private volatile boolean saveInProgress = false;
private static final String CONFIG_VERSION = "2.4";
private static final String CONFIG_VERSION = "2.5";
private boolean updateAvailable = false;
private String latestVersion = "";
@@ -369,6 +373,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
"&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" +
"&f- &b/asc autosign <input|ziel|rest|trash> [item|hand]\n" +
" &7Setzt automatisch ein ASC-Schild an die angeschaute Truhe.\n" +
" &7Beispiele: &b/asc autosign ziel IRON_ORE &7| &b/asc autosign ziel hand\n" +
"&6&l========================";
private static final String HELP_EN =
@@ -405,6 +412,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
"&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" +
"&f- &b/asc autosign <input|target|rest|trash> [item|hand]\n" +
" &7Automatically places an ASC sign on the chest you are looking at.\n" +
" &7Examples: &b/asc autosign target IRON_ORE &7| &b/asc autosign target hand\n" +
"&6&l========================";
private static final String INFO_DE =
@@ -663,6 +673,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
getServer().getPluginManager().registerEvents(this, this);
this.getCommand("asc").setExecutor(this);
this.getCommand("asc").setTabCompleter(this);
// Mülltruchen-Manager initialisieren
trashChestManager = new TrashChestManager(this);
@@ -734,6 +745,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
getLogger().info("AutoSortChest Plugin deaktiviert!");
}
@EventHandler
public void onPlayerQuit(org.bukkit.event.player.PlayerQuitEvent event) {
// Verhindert dauerhaftes Wachsen der openCustomInventories Map
openCustomInventories.remove(event.getPlayer().getUniqueId());
}
@EventHandler
public void onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent event) {
if (!updateAvailable) return;
@@ -991,6 +1008,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
}
private void loadPlayerData() {
migratedTargetItems.clear(); // Cache leeren damit Migrationen korrekt erkannt werden
if (playerDataFile == null) {
playerDataFile = new File(getDataFolder(), "players.yml");
}
@@ -1837,6 +1855,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
/** Prüft ob ein Target-Eintrag noch im alten Flat-Format gespeichert ist (direkt world/x/y/z unter item). */
private boolean isOldTargetFormat(UUID playerUUID, String itemName) {
// Cache-Hit: bereits migriert → sofort false zurückgeben ohne YAML-Zugriff
if (migratedTargetItems.contains(playerUUID + ":" + itemName)) return false;
String path = "players." + playerUUID + ".target-chests." + itemName;
return playerData.contains(path + ".world");
}
@@ -1862,6 +1882,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
playerData.set(base + ".0.y", y);
playerData.set(base + ".0.z", z);
playerData.set(base + ".0.public", pub);
// Im Cache markieren kein erneuter YAML-Lookup mehr nötig
migratedTargetItems.add(playerUUID + ":" + itemName);
savePlayerData();
if (isDebug()) getLogger().info("[YAML-Migration] target-chests." + itemName + " → Slot 0 für " + playerUUID);
}
@@ -1875,8 +1897,24 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
// Auto-Migrierung falls altes Format
if (isOldTargetFormat(playerUUID, itemName)) migrateOldTargetChestEntry(playerUUID, itemName);
if (!playerData.isConfigurationSection(base)) return result;
for (String slotKey : playerData.getConfigurationSection(base).getKeys(false)) {
String p = base + "." + slotKey;
// Slots mit Prio sammeln und nach prio aufsteigend sortieren (Prio 1 zuerst, 0 = nicht gesetzt → ganz hinten)
List<int[]> slotPrios = new ArrayList<>(); // [slot-index, prio]
List<String> slotKeys = new ArrayList<>(playerData.getConfigurationSection(base).getKeys(false));
for (String slotKey : slotKeys) {
int prio = playerData.getInt(base + "." + slotKey + ".prio", 0);
int slotIdx = 0;
try { slotIdx = Integer.parseInt(slotKey); } catch (NumberFormatException ignored) {}
slotPrios.add(new int[]{slotIdx, prio});
}
// Prio 1 zuerst, Prio 2 danach usw. — Prio 0 (nicht gesetzt) kommt ans Ende.
// Bei gleicher Prio: niedrigster Slot zuerst.
slotPrios.sort((a, b) -> {
int pa = a[1] == 0 ? Integer.MAX_VALUE : a[1];
int pb = b[1] == 0 ? Integer.MAX_VALUE : b[1];
return pa != pb ? Integer.compare(pa, pb) : Integer.compare(a[0], b[0]);
});
for (int[] sp : slotPrios) {
String p = base + "." + sp[0];
World w = Bukkit.getWorld(playerData.getString(p + ".world", ""));
if (w == null) continue;
result.add(new Location(w, playerData.getInt(p + ".x"), playerData.getInt(p + ".y"), playerData.getInt(p + ".z")));
@@ -2074,7 +2112,131 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!command.getName().equalsIgnoreCase("asc")) return false;
if (args.length == 0) {
// Keine Argumente → Hilfe anzeigen (kein Crash durch args[0])
String lang = config != null ? config.getString("language", "de") : "de";
if (sender instanceof Player p) {
String helpMessage = "en".equalsIgnoreCase(lang) ? HELP_EN : HELP_DE;
p.sendMessage(ChatColor.translateAlternateColorCodes('&', helpMessage).split("\n"));
} else {
sender.sendMessage(ChatColor.RED + "Verwendung: /asc [reload|import|export|list]");
}
return true;
}
// -------------------------------------------------------
// /asc priority <Nummer> Priorität für Zieltruhe setzen
// -------------------------------------------------------
if (args[0].equalsIgnoreCase("priority")) {
if (!(sender instanceof Player)) {
sender.sendMessage(getMessage("priority-player-only"));
return true;
}
Player player = (Player) sender;
if (!player.hasPermission("autosortchest.use")) {
player.sendMessage(getMessage("no-permission"));
return true;
}
if (args.length < 2) {
player.sendMessage(getMessage("priority-usage"));
return true;
}
int prio = 0;
try {
prio = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
player.sendMessage(getMessage("priority-invalid-number").replace("%input%", args[1]));
return true;
}
if (prio < 1 || prio > 20) {
player.sendMessage(getMessage("priority-out-of-range"));
return true;
}
// Block, den der Spieler anschaut (max. 5 Blöcke)
Block targetBlock = player.getTargetBlockExact(5);
if (targetBlock == null || !(targetBlock.getState() instanceof Sign)) {
player.sendMessage(getMessage("priority-no-sign"));
return true;
}
Sign sign = (Sign) targetBlock.getState();
// Prüfen, ob es ein ASC-Ziel-Schild ist
if (!isAscSign(sign, "target")) {
player.sendMessage(getMessage("priority-wrong-sign"));
return true;
}
// Zieltruhe zu diesem Schild finden (WallSign-API, nicht deprecated getData())
Block attached = null;
if (targetBlock.getBlockData() instanceof WallSign wallSignBD) {
attached = targetBlock.getRelative(wallSignBD.getFacing().getOppositeFace());
}
if (attached == null || !(attached.getState() instanceof Chest)) {
player.sendMessage(getMessage("priority-not-attached"));
return true;
}
Chest chest = (Chest) attached.getState();
Location chestLoc = chest.getLocation();
UUID playerUUID = player.getUniqueId();
// Item-Typ ermitteln:
// Normales Schild → Index 2 (Zeile 3) enthält den Item-Namen (z.B. "IRON_ORE")
// Clean-Schild → Item steht nicht auf dem Schild, per DB/YAML ermitteln
String itemType = null;
if (isCleanTargetSign(sign)) {
itemType = findItemForChestLocation(playerUUID, chestLoc);
if (itemType == null || itemType.isEmpty()) {
player.sendMessage(getMessage("priority-item-not-found"));
return true;
}
} else {
String line2 = sign.getLine(2); // Index 2 = dritte Zeile = Item-Name
if (line2 != null && !line2.trim().isEmpty()) {
itemType = ChatColor.stripColor(line2).trim().toUpperCase().replace(' ', '_');
}
}
if (itemType == null || itemType.isEmpty()) {
player.sendMessage(getMessage("priority-item-unknown"));
return true;
}
boolean found = false;
if (mysqlEnabled && mysqlManager != null) {
// MySQL: Zieltruhe suchen und Prio setzen
List<Map<String, Object>> targets = mysqlManager.getTargetChestsForItem(playerUUID.toString(), itemType);
for (Map<String, Object> t : targets) {
String w = (String) t.get("world");
int x = (int) t.get("x");
int y = (int) t.get("y");
int z = (int) t.get("z");
int slot = (int) t.get("slot");
if (w.equals(chestLoc.getWorld().getName()) && x == chestLoc.getBlockX() && y == chestLoc.getBlockY() && z == chestLoc.getBlockZ()) {
mysqlManager.setTargetChestPrio(playerUUID.toString(), itemType, slot, prio);
found = true;
break;
}
}
} else {
// YAML: Slot der Zieltruhe finden und Prio setzen
String base = "players." + playerUUID + ".target-chests." + itemType;
if (playerData.contains(base) && playerData.isConfigurationSection(base)) {
for (String slotKey : playerData.getConfigurationSection(base).getKeys(false)) {
String p = base + "." + slotKey;
if (playerData.getString(p + ".world", "").equals(chestLoc.getWorld().getName()) &&
playerData.getInt(p + ".x") == chestLoc.getBlockX() &&
playerData.getInt(p + ".y") == chestLoc.getBlockY() &&
playerData.getInt(p + ".z") == chestLoc.getBlockZ()) {
playerData.set(p + ".prio", prio);
savePlayerData();
flushPlayerData(); // sofort schreiben, nicht erst nach 30s
found = true;
break;
}
}
}
}
if (found) {
player.sendMessage(getMessage("priority-success").replace("%prio%", String.valueOf(prio)));
} else {
player.sendMessage(getMessage("priority-not-found"));
}
return true;
}
String lang = config != null ? config.getString("language", "de") : "de";
if (lang == null) lang = "de";
@@ -2084,7 +2246,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
&& !args[0].equalsIgnoreCase("import")
&& !args[0].equalsIgnoreCase("export")
&& !args[0].equalsIgnoreCase("list"))) {
sender.sendMessage(ChatColor.RED + "Dieser Befehl ist nur für Spieler! (Konsole: reload, import, export, list)");
sender.sendMessage(getMessage("console-only"));
return true;
}
}
@@ -2192,12 +2354,8 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
return true;
}
boolean isEn = lang.equalsIgnoreCase("en");
if (args.length < 2) {
sender.sendMessage(ChatColor.RED + (isEn
? "Usage: /asc list <player>"
: "Verwendung: /asc list <Spieler>"));
sender.sendMessage(getMessage("list-usage"));
return true;
}
@@ -2219,9 +2377,7 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
}
}
if (target == null) {
sender.sendMessage(ChatColor.RED + (isEn
? "Player '" + targetName + "' was not found!"
: "Spieler '" + targetName + "' wurde nicht gefunden!"));
sender.sendMessage(getMessage("list-player-not-found").replace("%name%", targetName));
return true;
}
@@ -2229,6 +2385,18 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
final String uuidStr = finalTarget.getUniqueId().toString();
final String displayName = finalTarget.getName() != null ? finalTarget.getName() : targetName;
// Labels vorab aus config lesen (thread-sicher, da Main-Thread)
final String cfgHeader = getMessage("list-header");
final String cfgTitle = getMessage("list-title");
final String cfgPlayerLabel = getMessage("list-player-label");
final String cfgOffline = getMessage("list-offline");
final String cfgInputLabel = getMessage("list-input-label");
final String cfgTargetLabel = getMessage("list-target-label");
final String cfgRestLabel = getMessage("list-rest-label");
final String cfgTrashLabel = getMessage("list-trash-label");
final String cfgUnlimited = getMessage("list-unlimited");
final String cfgFooter = getMessage("list-footer");
// Zählen (async, da ggf. DB-Abfragen)
new BukkitRunnable() {
@Override
@@ -2266,59 +2434,49 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
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 : "*";
inputMax = isOffline ? null : cfgUnlimited;
targetMax = isOffline ? null : cfgUnlimited;
restMax = isOffline ? null : cfgUnlimited;
} else {
Player onlineTarget = (Player) finalTarget;
if (onlineTarget.isOp() || onlineTarget.hasPermission("autosortchest.limit.bypass")) {
inputMax = "*";
targetMax = "*";
restMax = "*";
inputMax = cfgUnlimited;
targetMax = cfgUnlimited;
restMax = cfgUnlimited;
} 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);
inputMax = (iMax == Integer.MAX_VALUE) ? cfgUnlimited : String.valueOf(iMax);
targetMax = (tMax == Integer.MAX_VALUE) ? cfgUnlimited : String.valueOf(tMax);
restMax = (rMax == Integer.MAX_VALUE) ? cfgUnlimited : 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
sender.sendMessage(cfgHeader);
sender.sendMessage(cfgTitle);
sender.sendMessage(cfgPlayerLabel + ChatColor.WHITE + displayName
+ (isOffline ? cfgOffline : ""));
sender.sendMessage(cfgInputLabel + ChatColor.WHITE
+ (fInputMax != null ? fIn + " / " + fInputMax : String.valueOf(fIn)));
sender.sendMessage(ChatColor.YELLOW + labelTarget + ChatColor.WHITE
sender.sendMessage(cfgTargetLabel + ChatColor.WHITE
+ (fTargetMax != null ? fTa + " / " + fTargetMax : String.valueOf(fTa)));
sender.sendMessage(ChatColor.YELLOW + "Rest: " + ChatColor.WHITE
sender.sendMessage(cfgRestLabel + ChatColor.WHITE
+ (fRestMax != null ? fRe + " / " + fRestMax : String.valueOf(fRe)));
sender.sendMessage(ChatColor.YELLOW + labelTrash + ChatColor.WHITE
sender.sendMessage(cfgTrashLabel + ChatColor.WHITE
+ (isOffline ? String.valueOf(fTr) : fTr + " / " + trashMax));
sender.sendMessage(ChatColor.GOLD + "================================");
sender.sendMessage(cfgFooter);
}
}.runTask(Main.this);
}
@@ -2335,17 +2493,17 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
return true;
}
if (!mysqlEnabled || mysqlManager == null) {
sender.sendMessage(ChatColor.RED + "MySQL ist nicht aktiviert! Aktiviere MySQL in der config.yml zuerst.");
sender.sendMessage(getMessage("mysql-not-enabled-import"));
return true;
}
if (playerData == null || playerData.getConfigurationSection("players") == null
|| playerData.getConfigurationSection("players").getKeys(false).isEmpty()) {
sender.sendMessage(ChatColor.RED + "Die players.yml ist leer oder enthält keine Spielerdaten!");
sender.sendMessage(getMessage("yaml-empty"));
return true;
}
sender.sendMessage(ChatColor.YELLOW + "Importiere Daten aus players.yml nach MySQL...");
sender.sendMessage(ChatColor.GRAY + "Bestehende MySQL-Daten werden nicht überschrieben (REPLACE INTO).");
sender.sendMessage(getMessage("import-start"));
sender.sendMessage(getMessage("import-info"));
new BukkitRunnable() {
@Override
@@ -2439,11 +2597,11 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
final int fp = playerCount, fi = inputCount, ft = targetCount, fr = restCount;
Bukkit.getScheduler().runTask(Main.this, () -> {
sender.sendMessage(ChatColor.GREEN + "Import erfolgreich abgeschlossen!");
sender.sendMessage(ChatColor.GRAY + " Spieler: " + ChatColor.WHITE + fp);
sender.sendMessage(ChatColor.GRAY + " Eingangstruhen: " + ChatColor.WHITE + fi);
sender.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft);
sender.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr);
sender.sendMessage(getMessage("import-success"));
sender.sendMessage(getMessage("import-stats-players").replace("%players%", String.valueOf(fp)));
sender.sendMessage(getMessage("import-stats-input") .replace("%input%", String.valueOf(fi)));
sender.sendMessage(getMessage("import-stats-target").replace("%target%", String.valueOf(ft)));
sender.sendMessage(getMessage("import-stats-rest") .replace("%rest%", String.valueOf(fr)));
getLogger().info("Import durch " + sender.getName() + " abgeschlossen: "
+ fp + " Spieler, " + fi + " Input, " + ft + " Target, " + fr + " Rest.");
});
@@ -2461,12 +2619,12 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
return true;
}
if (!mysqlEnabled || mysqlManager == null) {
sender.sendMessage(ChatColor.RED + "MySQL ist nicht aktiviert! Der Export benötigt eine aktive MySQL-Verbindung.");
sender.sendMessage(getMessage("mysql-not-enabled-export"));
return true;
}
sender.sendMessage(ChatColor.YELLOW + "Exportiere Daten aus MySQL nach players.yml...");
sender.sendMessage(ChatColor.GRAY + "Ein Backup der aktuellen players.yml wird erstellt.");
sender.sendMessage(getMessage("export-start"));
sender.sendMessage(getMessage("export-info"));
new BukkitRunnable() {
@Override
@@ -2483,8 +2641,9 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
try {
java.nio.file.Files.copy(playerDataFile.toPath(), backupFile.toPath());
} catch (IOException e) {
final String errMsg = e.getMessage();
Bukkit.getScheduler().runTask(Main.this, () ->
sender.sendMessage(ChatColor.RED + "Backup fehlgeschlagen: " + e.getMessage()));
sender.sendMessage(getMessage("backup-failed").replace("%error%", errMsg)));
return;
}
}
@@ -2515,7 +2674,6 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
List<Map<String, Object>> targetChests = mysqlManager.getTargetChests(uuidString);
for (Map<String, Object> chest : targetChests) {
String item = (String) chest.get("item");
// FIX: export in new slotted format (slot 0)
String path = "players." + uuidString + ".target-chests." + item + ".0";
exportData.set(path + ".world", chest.get("world"));
exportData.set(path + ".x", chest.get("x"));
@@ -2546,24 +2704,25 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
Bukkit.getScheduler().runTask(Main.this, () -> {
playerData = finalExport;
sender.sendMessage(ChatColor.GREEN + "Export erfolgreich abgeschlossen!");
sender.sendMessage(getMessage("export-success"));
if (finalBackupName != null) {
sender.sendMessage(ChatColor.GRAY + " Backup: " + ChatColor.WHITE + finalBackupName);
sender.sendMessage(getMessage("export-backup").replace("%file%", finalBackupName));
} else {
sender.sendMessage(ChatColor.GRAY + " Backup: " + ChatColor.DARK_GRAY + "Übersprungen (players.yml war leer)");
sender.sendMessage(getMessage("export-backup-skipped"));
}
sender.sendMessage(ChatColor.GRAY + " Spieler: " + ChatColor.WHITE + fp);
sender.sendMessage(ChatColor.GRAY + " Eingangstruhen: " + ChatColor.WHITE + fi);
sender.sendMessage(ChatColor.GRAY + " Zieltruhen: " + ChatColor.WHITE + ft);
sender.sendMessage(ChatColor.GRAY + " Rest-Truhen: " + ChatColor.WHITE + fr);
sender.sendMessage(getMessage("import-stats-players").replace("%players%", String.valueOf(fp)));
sender.sendMessage(getMessage("import-stats-input") .replace("%input%", String.valueOf(fi)));
sender.sendMessage(getMessage("import-stats-target").replace("%target%", String.valueOf(ft)));
sender.sendMessage(getMessage("import-stats-rest") .replace("%rest%", String.valueOf(fr)));
getLogger().info("Export durch " + sender.getName() + " abgeschlossen: "
+ fp + " Spieler, " + fi + " Input, " + ft + " Target, " + fr + " Rest."
+ (finalBackupName != null ? " Backup: " + finalBackupName : " Kein Backup."));
});
} catch (Exception e) {
final String errMsg = e.getMessage();
Bukkit.getScheduler().runTask(Main.this, () ->
sender.sendMessage(ChatColor.RED + "Export fehlgeschlagen: " + e.getMessage()));
sender.sendMessage(getMessage("export-error").replace("%error%", errMsg)));
getLogger().warning("Export fehlgeschlagen: " + e.getMessage());
}
}
@@ -2571,6 +2730,25 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
return true;
}
// -------------------------------------------------------
// /asc autosign Schild automatisch an Truhe setzen
// -------------------------------------------------------
if (args[0].equalsIgnoreCase("autosign")) {
if (player == null) {
sender.sendMessage(getMessage("autosign-player-only"));
return true;
}
if (!player.hasPermission("autosortchest.use")) {
player.sendMessage(getMessage("no-permission"));
return true;
}
if (args.length < 2) {
player.sendMessage(getMessage("autosign-usage"));
return true;
}
return handleAutoSign(player, args);
}
// Unbekannter Befehl → Hilfe (nur für Spieler)
if (player == null) {
sender.sendMessage(ChatColor.RED + "Verwendung: /asc [reload|import|export]");
@@ -2582,6 +2760,416 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
return true;
}
// ═══════════════════════════════════════════════════════════════════════
// TAB COMPLETER
// ═══════════════════════════════════════════════════════════════════════
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (!command.getName().equalsIgnoreCase("asc")) return Collections.emptyList();
List<String> result = new ArrayList<>();
if (args.length == 1) {
// /asc <subcommand>
List<String> subs = new ArrayList<>(Arrays.asList(
"help", "info", "autosign", "priority"));
if (sender.hasPermission("autosortchest.reload")) subs.add("reload");
if (sender.hasPermission("autosortchest.import")) subs.add("import");
if (sender.hasPermission("autosortchest.export")) subs.add("export");
if (sender.hasPermission("autosortchest.list")) subs.add("list");
String partial = args[0].toLowerCase();
for (String s : subs) {
if (s.toLowerCase().startsWith(partial)) result.add(s);
}
Collections.sort(result);
} else if (args.length == 2) {
if (args[0].equalsIgnoreCase("autosign")) {
// /asc autosign <typ>
String lang = config != null ? config.getString("language", "de") : "de";
List<String> types;
if ("en".equalsIgnoreCase(lang)) {
types = Arrays.asList("input", "target", "rest", "trash");
} else {
types = Arrays.asList("input", "ziel", "rest", "trash");
}
String partial = args[1].toLowerCase();
for (String t : types) {
if (t.startsWith(partial)) result.add(t);
}
} else if (args[0].equalsIgnoreCase("list")) {
// /asc list <Spieler>
String partial = args[1].toLowerCase();
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.getName().toLowerCase().startsWith(partial)) result.add(p.getName());
}
} else if (args[0].equalsIgnoreCase("priority")) {
// /asc priority <Nummer>
String partial = args[1];
for (int i = 1; i <= 20; i++) {
String s = String.valueOf(i);
if (s.startsWith(partial)) result.add(s);
}
}
} else if (args.length == 3
&& args[0].equalsIgnoreCase("autosign")
&& (args[1].equalsIgnoreCase("ziel") || args[1].equalsIgnoreCase("target"))) {
// /asc autosign ziel <item|hand>
String partial = args[2].toLowerCase();
result.add("hand");
for (Material mat : Material.values()) {
if (mat.isAir() || !mat.isItem()) continue;
String name = mat.name().toLowerCase();
if (name.startsWith(partial)) {
result.add(name);
if (result.size() >= 60) break; // Lag-Schutz
}
}
result = result.stream()
.filter(s -> s.startsWith(partial))
.sorted()
.collect(Collectors.toList());
}
return result;
}
// ═══════════════════════════════════════════════════════════════════════
// /asc autosign Implementierung
// ═══════════════════════════════════════════════════════════════════════
/**
* Implementiert /asc autosign platziert automatisch ein ASC-Schild
* an der Truhe, auf die der Spieler gerade schaut (max. 5 Blöcke).
*
* Syntax: /asc autosign <input|ziel|rest|trash> [item|hand]
* input / rest / trash : kein weiteres Argument nötig
* ziel / target : [item] = Material-Name (z.B. IRON_ORE)
* [hand] = nimmt das Item in der Haupthand
* (kein Argument) = wie "hand"
*/
private boolean handleAutoSign(Player player, String[] args) {
// ── 1. Ziel-Block ermitteln ──────────────────────────────────────────
Block targetBlock = player.getTargetBlockExact(5);
if (targetBlock == null || !(targetBlock.getState() instanceof Chest)) {
player.sendMessage(getMessage("autosign-no-chest"));
return true;
}
if (isWorldBlacklisted(targetBlock.getWorld())) {
player.sendMessage(getMessage("world-blacklisted"));
return true;
}
// ── 2. Typ normalisieren ─────────────────────────────────────────────
String typRaw = args[1].toLowerCase();
switch (typRaw) {
case "target": typRaw = "ziel"; break;
case "müll": case "muell": typRaw = "trash"; break;
default: break;
}
if (!typRaw.equals("input") && !typRaw.equals("ziel")
&& !typRaw.equals("rest") && !typRaw.equals("trash")) {
player.sendMessage(getMessage("autosign-invalid-type"));
return true;
}
final String typ = typRaw;
// ── 3. Material für Zieltruhe ermitteln ──────────────────────────────
Material targetMaterial = null;
if (typ.equals("ziel")) {
if (args.length >= 3 && !args[2].equalsIgnoreCase("hand")) {
// Expliziter Material-Name angegeben
targetMaterial = Material.matchMaterial(args[2].toUpperCase());
if (targetMaterial == null || targetMaterial == Material.AIR) {
player.sendMessage(getMessage("autosign-unknown-item").replace("%item%", args[2]));
return true;
}
} else {
// Item in Haupthand verwenden
ItemStack handItem = player.getInventory().getItemInMainHand();
if (handItem == null || handItem.getType() == Material.AIR) {
player.sendMessage(getMessage("no-item-in-hand"));
return true;
}
targetMaterial = handItem.getType();
}
}
final Material finalMaterial = targetMaterial;
// ── 4. Freie Schildfläche an der Truhe suchen (Blickrichtung bevorzugen) ──
Chest chestState = (Chest) targetBlock.getState();
List<Block> chestBlocks = getChestBlocks(chestState);
org.bukkit.block.BlockFace[] faces = {
org.bukkit.block.BlockFace.NORTH, org.bukkit.block.BlockFace.SOUTH,
org.bukkit.block.BlockFace.EAST, org.bukkit.block.BlockFace.WEST
};
Block signBlock = null;
org.bukkit.block.BlockFace signFacing = null;
Block chestBlock = null;
// Blickrichtung des Spielers zur Truhe bestimmen (von Truhe zum Spieler!)
org.bukkit.util.Vector eye = player.getEyeLocation().toVector();
org.bukkit.util.Vector chest = targetBlock.getLocation().add(0.5, 0.5, 0.5).toVector();
org.bukkit.util.Vector dir = eye.clone().subtract(chest).normalize();
double maxDot = -2.0;
org.bukkit.block.BlockFace facingFace = null;
for (org.bukkit.block.BlockFace face : faces) {
org.bukkit.util.Vector faceVec = new org.bukkit.util.Vector(face.getModX(), face.getModY(), face.getModZ());
double dot = dir.dot(faceVec);
if (dot > maxDot) {
maxDot = dot;
facingFace = face;
}
}
// Zuerst versuchen, auf die Seite zu setzen, die der Spieler anschaut
outer:
for (Block cb : chestBlocks) {
Block adj = cb.getRelative(facingFace);
if (!(adj.getState() instanceof Sign existSign && isSignAttachedToChest(adj, cb)) &&
(adj.getType() == Material.AIR || adj.getType() == Material.CAVE_AIR || adj.getType() == Material.VOID_AIR)) {
signBlock = adj;
signFacing = facingFace;
chestBlock = cb;
break outer;
}
}
// Falls dort kein Platz ist, wie bisher freie Seite suchen
if (signBlock == null) {
outer2:
for (Block cb : chestBlocks) {
for (org.bukkit.block.BlockFace face : faces) {
Block adj = cb.getRelative(face);
if (adj.getState() instanceof Sign existSign && isSignAttachedToChest(adj, cb)) {
continue;
}
if (adj.getType() == Material.AIR || adj.getType() == Material.CAVE_AIR || adj.getType() == Material.VOID_AIR) {
signBlock = adj;
signFacing = face;
chestBlock = cb;
break outer2;
}
}
}
}
if (signBlock == null) {
player.sendMessage(getMessage("autosign-no-space"));
return true;
}
final Block finalChestBlock = chestBlock;
UUID playerUUID = player.getUniqueId();
// ── 5. Limit-Prüfung ────────────────────────────────────────────────
if (chestLimitsEnabled
&& !isAdmin(player)
&& !player.hasPermission("autosortchest.limit.bypass")) {
switch (typ) {
case "input": {
int maxInput = getChestLimitForPlayer(player, "input");
if (maxInput == 0) {
player.sendMessage(getMessage("limit-no-permission"));
return true;
}
int currentInput = autoSignCountInputChests(playerUUID);
boolean alreadyInput = autoSignIsAlreadyInputChest(playerUUID, finalChestBlock);
if (!alreadyInput && currentInput >= maxInput) {
player.sendMessage(getMessage("limit-input-reached")
.replace("%max%", String.valueOf(maxInput)));
return true;
}
break;
}
case "rest": {
int maxRest = getChestLimitForPlayer(player, "rest");
if (maxRest == 0) {
player.sendMessage(getMessage("limit-no-permission"));
return true;
}
int currentRest = autoSignCountRestChests(playerUUID);
if (currentRest >= maxRest) {
player.sendMessage(getMessage("limit-rest-reached")
.replace("%max%", String.valueOf(maxRest)));
return true;
}
break;
}
case "ziel": {
int maxChests = getChestLimitForPlayer(player, "target");
int maxPerItem = getChestLimitForPlayer(player, "target_per_item");
if (maxChests == 0) {
player.sendMessage(getMessage("limit-no-permission"));
return true;
}
Set<String> uniqueLocs = new HashSet<>();
int countForThisItem = 0;
String thisLocKey = finalChestBlock.getWorld().getName() + ":"
+ finalChestBlock.getX() + ":" + finalChestBlock.getY()
+ ":" + finalChestBlock.getZ();
if (mysqlEnabled && mysqlManager != null) {
for (Map<String, Object> map : mysqlManager.getTargetChests(playerUUID.toString())) {
String w = (String) map.get("world");
int tx = (int) map.get("x"), ty = (int) map.get("y"), tz = (int) map.get("z");
World bw = Bukkit.getWorld(w);
if (bw != null && bw.getBlockAt(tx, ty, tz).getState() instanceof Chest) {
uniqueLocs.add(w + ":" + tx + ":" + ty + ":" + tz);
if (finalMaterial.name().equals(map.get("item"))) countForThisItem++;
}
}
} else {
String basePath = "players." + playerUUID + ".target-chests";
if (playerData.contains(basePath)) {
for (String item : playerData.getConfigurationSection(basePath).getKeys(false)) {
for (Location slotLoc : getTargetChestSlotsYaml(playerUUID, item)) {
if (slotLoc == null || slotLoc.getWorld() == null) continue;
int tx = slotLoc.getBlockX(), ty = slotLoc.getBlockY(), tz = slotLoc.getBlockZ();
World bw = slotLoc.getWorld();
if (bw.getBlockAt(tx, ty, tz).getState() instanceof Chest) {
uniqueLocs.add(bw.getName() + ":" + tx + ":" + ty + ":" + tz);
if (finalMaterial.name().equals(item)) countForThisItem++;
}
}
}
}
}
boolean alreadyTarget = uniqueLocs.contains(thisLocKey);
if (!alreadyTarget && uniqueLocs.size() >= maxChests) {
player.sendMessage(getMessage("limit-target-reached")
.replace("%max%", String.valueOf(maxChests)));
return true;
}
if (!alreadyTarget && countForThisItem >= maxPerItem) {
player.sendMessage(getMessage("limit-target-per-item")
.replace("%max%", String.valueOf(maxPerItem))
.replace("%item%", finalMaterial.name()));
return true;
}
break;
}
case "trash": {
// Trash-Limit: kein eigenes Limit-System, nur Permission-Check
if (chestLimitsEnabled
&& getChestLimitForPlayer(player, "input") == 0
&& getChestLimitForPlayer(player, "target") == 0) {
player.sendMessage(getMessage("limit-no-permission"));
return true;
}
break;
}
}
}
// ── 6. Schild-Block setzen ───────────────────────────────────────────
signBlock.setType(Material.OAK_WALL_SIGN);
org.bukkit.block.data.type.WallSign wallSignData =
(org.bukkit.block.data.type.WallSign) signBlock.getBlockData();
wallSignData.setFacing(signFacing);
signBlock.setBlockData(wallSignData);
BlockState rawState = signBlock.getState();
if (!(rawState instanceof Sign)) {
player.sendMessage(getMessage("autosign-place-error"));
signBlock.setType(Material.AIR);
return true;
}
Sign sign = (Sign) rawState;
// ── 7. Schildtext + Registrierung ────────────────────────────────────
Location chestLoc = finalChestBlock.getLocation();
switch (typ) {
case "input": {
updateSignToCurrentStyle(sign, "input", null, player.getName(), false, false);
sign.update();
setInputChestLocation(playerUUID, chestLoc);
player.sendMessage(getMessage("input-chest-set"));
break;
}
case "ziel": {
String itemDisplay = TrashChestManager.formatMaterialName(finalMaterial.name());
if (itemDisplay.length() > 15) itemDisplay = itemDisplay.substring(0, 15);
updateSignToCurrentStyle(sign, "target", itemDisplay, player.getName(), false, false);
sign.update();
setTargetChestLocation(playerUUID, chestLoc, finalMaterial);
player.sendMessage(getMessage("target-chest-set").replace("%item%", finalMaterial.name()));
break;
}
case "rest": {
updateSignToCurrentStyle(sign, "rest", null, player.getName(), false, false);
sign.update();
setRestChestLocation(playerUUID, chestLoc);
player.sendMessage(getMessage("rest-chest-set"));
break;
}
case "trash": {
updateSignToCurrentStyle(sign, "trash", null, player.getName(), false, false);
sign.update();
trashChestManager.setTrashChestLocation(playerUUID, chestLoc);
player.sendMessage(getMessage("trash-chest-set"));
player.sendMessage(getMessage("trash-chest-hint"));
break;
}
}
return true;
}
// ── Hilfs-Methoden für Limit-Prüfung in handleAutoSign ───────────────────
private int autoSignCountInputChests(UUID playerUUID) {
if (mysqlEnabled && mysqlManager != null) {
return mysqlManager.getInputChests(playerUUID.toString()).size();
}
String basePath = "players." + playerUUID + ".input-chests";
return playerData.contains(basePath)
? playerData.getConfigurationSection(basePath).getKeys(false).size() : 0;
}
private boolean autoSignIsAlreadyInputChest(UUID playerUUID, Block chestBlock) {
if (mysqlEnabled && mysqlManager != null) {
return mysqlManager.getInputChests(playerUUID.toString()).stream().anyMatch(c -> {
String w = (String) c.get("world");
return w != null && w.equals(chestBlock.getWorld().getName())
&& (int) c.get("x") == chestBlock.getX()
&& (int) c.get("y") == chestBlock.getY()
&& (int) c.get("z") == chestBlock.getZ();
});
}
String basePath = "players." + playerUUID + ".input-chests";
if (playerData.contains(basePath)) {
for (String id : playerData.getConfigurationSection(basePath).getKeys(false)) {
String p = basePath + "." + id;
if (chestBlock.getWorld().getName().equals(playerData.getString(p + ".world"))
&& chestBlock.getX() == playerData.getInt(p + ".x")
&& chestBlock.getY() == playerData.getInt(p + ".y")
&& chestBlock.getZ() == playerData.getInt(p + ".z")) return true;
}
}
return false;
}
private int autoSignCountRestChests(UUID playerUUID) {
if (mysqlEnabled && mysqlManager != null) {
return mysqlManager.getRestChests(playerUUID.toString()).size();
}
String basePath = "players." + playerUUID + ".rest-chests";
if (playerData.contains(basePath) && playerData.isConfigurationSection(basePath)) {
return playerData.getConfigurationSection(basePath).getKeys(false).size();
}
return 0;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onSignChange(SignChangeEvent event) {
Player player = event.getPlayer();
@@ -3574,8 +4162,20 @@ public class Main extends JavaPlugin implements Listener, CommandExecutor {
}
}
// MySQL-Fallback: Besitzer anhand der Location direkt in der DB suchen.
// Notwendig wenn players.yml leer ist (MySQL-Modus) oder der Name-Lookup fehlschlug.
if (ownerUUID == null && mysqlEnabled && mysqlManager != null) {
ownerUUID = mysqlManager.findOwnerByLocation(
chestLoc.getWorld().getName(),
chestLoc.getBlockX(), chestLoc.getBlockY(), chestLoc.getBlockZ());
}
UUID uuidToDelete = (ownerUUID != null) ? ownerUUID : player.getUniqueId();
// YAML-Public-Cache immer leeren, unabhängig vom Typ (kein Leak nach Abbau)
removeFromYamlPublicCache(chestLoc.getWorld().getName(),
chestLoc.getBlockX(), chestLoc.getBlockY(), chestLoc.getBlockZ());
if (line1.equalsIgnoreCase("rest")) {
if (mysqlEnabled && mysqlManager != null) {
mysqlManager.removeRestChestByLocation(uuidToDelete.toString(),

View File

@@ -4,6 +4,36 @@ import java.sql.*;
import java.util.*;
public class MySQLManager {
/**
* Setzt die Priorität (prio) für eine Zieltruhe (target chest) anhand von uuid, item, slot.
* Legt die Spalte prio an, falls sie noch nicht existiert.
*/
public void setTargetChestPrio(String uuid, String item, int slot, int prio) {
ensureConnected();
// Spalte prio anlegen, falls sie fehlt
try (Statement st = connection.createStatement()) {
ResultSet rs = connection.getMetaData().getColumns(connection.getCatalog(), null, "asc_target_chests", "prio");
if (!rs.next()) {
st.execute("ALTER TABLE asc_target_chests ADD COLUMN prio INT DEFAULT 0;");
}
rs.close();
} catch (SQLException e) {
if (!e.getMessage().toLowerCase().contains("duplicate column")) {
e.printStackTrace();
}
}
// Prio setzen
try (PreparedStatement ps = connection.prepareStatement(
"UPDATE asc_target_chests SET prio=? WHERE uuid=? AND item=? AND slot=?;")) {
ps.setInt(1, prio);
ps.setString(2, uuid);
ps.setString(3, item);
ps.setInt(4, slot);
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public Connection getConnection() {
return connection;
}
@@ -598,12 +628,24 @@ public class MySQLManager {
return all.isEmpty() ? null : all.get(0);
}
/** Gibt ALLE Zieltruhen für ein bestimmtes Item zurück, sortiert nach slot. */
/** Gibt ALLE Zieltruhen für ein bestimmtes Item zurück, sortiert nach prio absteigend, dann slot aufsteigend. */
public List<Map<String, Object>> getTargetChestsForItem(String uuid, String item) {
ensureConnected();
List<Map<String, Object>> list = new ArrayList<>();
try (PreparedStatement ps = connection.prepareStatement(
"SELECT * FROM asc_target_chests WHERE uuid=? AND item=? ORDER BY slot ASC;")) {
// Prio 1 zuerst, Prio 2 danach usw. — Prio 0 (nicht gesetzt) kommt ans Ende.
// CASE WHEN vermeidet eine separate Abfrage ob die Spalte existiert.
String sql;
try {
if (columnExists("asc_target_chests", "prio")) {
sql = "SELECT * FROM asc_target_chests WHERE uuid=? AND item=? " +
"ORDER BY CASE WHEN COALESCE(prio,0)=0 THEN 999 ELSE COALESCE(prio,0) END ASC, slot ASC;";
} else {
sql = "SELECT * FROM asc_target_chests WHERE uuid=? AND item=? ORDER BY slot ASC;";
}
} catch (Exception e) {
sql = "SELECT * FROM asc_target_chests WHERE uuid=? AND item=? ORDER BY slot ASC;";
}
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, uuid);
ps.setString(2, item);
ResultSet rs = ps.executeQuery();
@@ -616,6 +658,7 @@ public class MySQLManager {
map.put("y", rs.getInt("y"));
map.put("z", rs.getInt("z"));
map.put("public", rs.getBoolean("public"));
try { map.put("prio", rs.getInt("prio")); } catch (SQLException ignored) { map.put("prio", 0); }
try { map.put("server", rs.getString("server")); }
catch (SQLException ignored) { map.put("server", ""); }
list.add(map);
@@ -1051,6 +1094,45 @@ public class MySQLManager {
// FIX: Einzelne UNION-Abfrage statt 3 separater Queries für isChestPublic()
// Reduziert Main-Thread-Blockierung bei MySQL um ~66%.
// ═══════════════════════════════════════════════════════════════════
/**
* Sucht den Besitzer einer Truhen-Location in ALLEN ASC-Tabellen (UNION).
* Wird im onBlockBreak-Fallback verwendet wenn der Name-Lookup keine UUID liefert
* (z.B. MySQL-Modus mit leerer players.yml).
*
* @return UUID des Besitzers oder null wenn nicht gefunden.
*/
public UUID findOwnerByLocation(String world, int x, int y, int z) {
ensureConnected();
String sql =
"(SELECT uuid FROM asc_input_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1) " +
"UNION ALL " +
"(SELECT uuid FROM asc_target_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1) " +
"UNION ALL " +
"(SELECT uuid FROM asc_rest_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1) " +
"UNION ALL " +
"(SELECT uuid FROM asc_trash_chests WHERE world=? AND x=? AND y=? AND z=? LIMIT 1) " +
"LIMIT 1";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
for (int i = 0; i < 4; i++) {
int base = i * 4;
ps.setString(base + 1, world);
ps.setInt (base + 2, x);
ps.setInt (base + 3, y);
ps.setInt (base + 4, z);
}
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String uuidStr = rs.getString("uuid");
rs.close();
try { return UUID.fromString(uuidStr); } catch (Exception ignored) { return null; }
}
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
/**
* Prüft ob eine Truhen-Location in IRGENDEINER Tabelle als public markiert ist.
* Kombiniert Input-, Target- und Rest-Tabelle in einer einzigen UNION-Abfrage.

View File

@@ -62,9 +62,32 @@ public class TrashChestManager {
"http://textures.minecraft.net/texture/32518d04f9c06c95dd0edad617abb93d3d8657f01e659079d330cca6f65bccf7";
private String getGuiTitle() {
String colorPrefix = getChestTitleColor();
String label = isEnglish() ? "Configure Trash Chest" : "Mülltruhe konfigurieren";
return colorPrefix + ChatColor.BOLD + label;
return getGuiText("title");
}
/**
* Liest einen einzelnen Text aus dem trash-gui-Abschnitt der config.yml.
* Wählt automatisch die richtige Sprache (de/en). Farbcodes werden übersetzt.
*/
private String getGuiText(String key) {
String lang = isEnglish() ? "en" : "de";
String val = plugin.getConfig().getString("trash-gui." + key + "." + lang);
if (val == null) val = plugin.getConfig().getString("trash-gui." + key + ".de", key);
return ChatColor.translateAlternateColorCodes('&', val);
}
/**
* Liest eine mehrzeilige Lore-Liste aus dem trash-gui-Abschnitt der config.yml.
* Wählt automatisch die richtige Sprache (de/en). Farbcodes werden übersetzt.
*/
private List<String> getGuiLore(String key) {
String lang = isEnglish() ? "en" : "de";
List<String> list = plugin.getConfig().getStringList("trash-gui." + key + "." + lang);
if (list.isEmpty()) list = plugin.getConfig().getStringList("trash-gui." + key + ".de");
List<String> result = new ArrayList<>(list.size());
for (String line : list)
result.add(ChatColor.translateAlternateColorCodes('&', line));
return result;
}
private boolean isEnglish() {
@@ -273,21 +296,44 @@ public class TrashChestManager {
saveTrashChest(uuid);
}
public void removeTrashChest(UUID uuid) {
Location loc = trashChestLocations.remove(uuid);
if (loc != null) locationToOwner.remove(locKey(loc));
/**
* Entfernt Mülltruhen-Eintrag für eine bestimmte Location (z.B. beim Abbau der Kiste).
* Falls mehrere Mülltruhen pro Spieler möglich sind, wird nur der passende Eintrag entfernt.
* Falls keine Location angegeben, wird wie bisher nach UUID gelöscht.
*/
public void removeTrashChest(UUID uuid, Location loc) {
trashChestLocations.remove(uuid);
locationToOwner.remove(locKey(loc));
trashFilterLists.remove(uuid);
if (plugin.isMysqlEnabled() && plugin.getMysqlManager() != null) {
plugin.getMysqlManager().removeTrashChest(uuid.toString());
plugin.getMysqlManager().removeAllTrashItems(uuid.toString());
} else {
FileConfiguration data = plugin.getPlayerData();
data.set("players." + uuid + ".trash-chest", null);
data.set("players." + uuid + ".trash-items", null);
String uuidStr = uuid.toString();
String base = "players." + uuidStr + ".trash-chest";
// Prüfe, ob die gespeicherte Location mit der zu entfernenden übereinstimmt
if (data.contains(base + ".world")) {
String w = data.getString(base + ".world");
int x = data.getInt(base + ".x");
int y = data.getInt(base + ".y");
int z = data.getInt(base + ".z");
if (w != null && loc != null &&
w.equals(loc.getWorld().getName()) &&
x == loc.getBlockX() && y == loc.getBlockY() && z == loc.getBlockZ()) {
data.set(base, null);
data.set("players." + uuidStr + ".trash-items", null);
}
}
plugin.savePlayerDataPublic();
}
}
// Für Kompatibilität: alter Aufruf ohne Location löscht wie bisher alles für die UUID
public void removeTrashChest(UUID uuid) {
removeTrashChest(uuid, trashChestLocations.get(uuid));
}
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); }
@@ -449,7 +495,6 @@ public class TrashChestManager {
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<>());
@@ -469,15 +514,14 @@ public class TrashChestManager {
// ── Items der aktuellen Seite ────────────────────────────────────────
int startIndex = page * ITEMS_PER_PAGE;
int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, validItems.size());
String removeHint = getGuiText("item-remove-hint");
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()));
@@ -507,7 +551,7 @@ public class TrashChestManager {
}
}
// ── Tränkeffekte (Trank, Wurftrank, Pfeil: PotionMeta) ──────────
// ── Tränkeffekte (PotionMeta) ────────────────────────────────────
if (meta instanceof PotionMeta pm) {
List<PotionEffect> effects = pm.getCustomEffects();
if (!effects.isEmpty()) {
@@ -539,7 +583,7 @@ public class TrashChestManager {
lore.add("");
}
lore.add(ChatColor.RED + (isEn ? "▶ Right-click: Remove" : "▶ Rechtsklick: Entfernen"));
lore.add(removeHint);
meta.setLore(lore);
display.setItemMeta(meta);
}
@@ -557,10 +601,10 @@ public class TrashChestManager {
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));
prevMeta.setDisplayName(getGuiText("btn-prev-title"));
prevMeta.setLore(Arrays.asList(getGuiText("page-nav-lore")
.replace("%page%", String.valueOf(page))
.replace("%total%", String.valueOf(totalPages))));
prev.setItemMeta(prevMeta);
}
gui.setItem(45, prev);
@@ -570,10 +614,11 @@ public class TrashChestManager {
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")));
pageMeta.setDisplayName(getGuiText("page-info-title")
.replace("%page%", String.valueOf(page + 1))
.replace("%total%", String.valueOf(totalPages)));
pageMeta.setLore(Arrays.asList(getGuiText("page-info-lore")
.replace("%count%", String.valueOf(validItems.size()))));
pageInfo.setItemMeta(pageMeta);
}
gui.setItem(46, pageInfo);
@@ -583,10 +628,10 @@ public class TrashChestManager {
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));
nextMeta.setDisplayName(getGuiText("btn-next-title"));
nextMeta.setLore(Arrays.asList(getGuiText("page-nav-lore")
.replace("%page%", String.valueOf(page + 2))
.replace("%total%", String.valueOf(totalPages))));
next.setItemMeta(nextMeta);
}
gui.setItem(47, next);
@@ -597,24 +642,11 @@ public class TrashChestManager {
ItemMeta modeMeta = modeInfo.getItemMeta();
if (modeMeta != null) {
if (filter.isEmpty()) {
modeMeta.setDisplayName(ChatColor.RED + "" + ChatColor.BOLD
+ (isEn ? "✗ Status: Disabled" : "✗ Status: Deaktiviert"));
modeMeta.setLore(Arrays.asList(
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.")));
modeMeta.setDisplayName(getGuiText("status-disabled-title"));
modeMeta.setLore(getGuiLore("status-disabled-lore"));
} else {
modeMeta.setDisplayName(ChatColor.GREEN + "" + ChatColor.BOLD
+ (isEn ? "✔ Status: Active" : "✔ Status: Aktiv"));
modeMeta.setLore(Arrays.asList(
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.")));
modeMeta.setDisplayName(getGuiText("status-active-title"));
modeMeta.setLore(getGuiLore("status-active-lore"));
}
modeInfo.setItemMeta(modeMeta);
}
@@ -624,18 +656,8 @@ public class TrashChestManager {
ItemStack addBtn = new ItemStack(Material.LIME_STAINED_GLASS_PANE);
ItemMeta addMeta = addBtn.getItemMeta();
if (addMeta != null) {
addMeta.setDisplayName(ChatColor.GREEN + "" + ChatColor.BOLD
+ (isEn ? "✚ Add Item" : "✚ Item hinzufügen"));
addMeta.setLore(Arrays.asList(
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.")));
addMeta.setDisplayName(getGuiText("btn-add-title"));
addMeta.setLore(getGuiLore("btn-add-lore"));
addBtn.setItemMeta(addMeta);
}
gui.setItem(49, addBtn);
@@ -648,10 +670,8 @@ 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.";
String label = getGuiText("btn-clear-title");
List<String> loreLines = getGuiLore("btn-clear-lore");
ItemStack skull;
try {
@@ -663,16 +683,16 @@ public class TrashChestManager {
textures.setSkin(new URL(SKULL_TEXTURE));
profile.setTextures(textures);
meta.setOwnerProfile(profile);
meta.setDisplayName(getChestTitleColor() + "" + ChatColor.BOLD + label);
meta.setLore(Arrays.asList(ChatColor.GRAY + lore1, ChatColor.GRAY + lore2));
meta.setDisplayName(label);
meta.setLore(loreLines);
skull.setItemMeta(meta);
}
} catch (Exception e) {
skull = new ItemStack(Material.RED_DYE);
ItemMeta meta = skull.getItemMeta();
if (meta != null) {
meta.setDisplayName(getChestTitleColor() + "" + ChatColor.BOLD + label);
meta.setLore(Arrays.asList(ChatColor.GRAY + lore1, ChatColor.GRAY + lore2));
meta.setDisplayName(label);
meta.setLore(loreLines);
skull.setItemMeta(meta);
}
}
@@ -790,23 +810,6 @@ public class TrashChestManager {
return ChatColor.translateAlternateColorCodes('&', raw);
}
private String getChestTitleColor() {
boolean isEn = isEnglish();
String lang = isEn ? "en" : "de";
String full = plugin.getConfig().getString("chest-titles.trash." + lang,
isEn ? "&4Trash Chest" : "&4Mülltruhe");
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++;
} else {
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.

View File

@@ -12,7 +12,7 @@
# ============================================================
# Version der Konfigurationsdatei bitte nicht ändern!
version: "2.3"
version: "2.5"
# Debug-Modus: true = Ausführliche Logs in der Konsole (nur zum Entwickeln)
debug: false
@@ -313,3 +313,157 @@ messages:
mode-changed: "&aModus gewechselt: &e%mode%"
mode-public: "&aÖffentlich"
mode-private: "&cPrivat"
# --- Priorität-Befehl (/asc priority) ---
# Platzhalter: %input% = eingegebene Zahl, %prio% = gesetzte Priorität
priority-player-only: "&cDieser Befehl ist nur für Spieler!"
priority-usage: "&cVerwendung: /asc priority <1-20>"
priority-invalid-number: "&cUngültige Zahl: &e%input%"
priority-out-of-range: "&cPriorität muss zwischen 1 und 20 liegen!"
priority-no-sign: "&cDu schaust auf kein Schild!"
priority-wrong-sign: "&cDas ist kein Ziel-Schild!"
priority-not-attached: "&cDas Schild ist nicht an einer Truhe befestigt!"
priority-item-unknown: "&cKonnte Item-Typ auf dem Schild nicht erkennen!"
priority-item-not-found: "&cKonnte Item-Typ für dieses Schild nicht finden!"
priority-success: "&aPriorität für Zieltruhe gesetzt: &e%prio%"
priority-not-found: "&cKonnte die Zieltruhe zu diesem Schild nicht finden!"
# --- AutoSign-Befehl (/asc autosign) ---
# Platzhalter: %item% = Item-Name
autosign-no-chest: "&cDu schaust auf keine Truhe! &7(max. 5 Blöcke)"
autosign-invalid-type:"&cUngültiger Typ! Nutze: input, ziel, rest, trash"
autosign-unknown-item:"&cUnbekanntes Item: &e%item%"
autosign-no-space: "&cKein freier Platz für ein Schild an dieser Truhe!"
autosign-place-error: "&cFehler beim Platzieren bitte manuell versuchen."
# --- /asc list Ausgabe ---
# Platzhalter: %name% = Spielername
list-usage: "&cVerwendung: /asc list <Spieler>"
list-player-not-found: "&cSpieler &e'%name%' &cwurde nicht gefunden!"
list-header: "&6================================"
list-title: "&6&l==== AutoSortChest Info ===="
list-player-label: "&eSpieler: "
list-offline: "&7 (offline)"
list-input-label: "&eInput: "
list-target-label: "&eZiel: "
list-rest-label: "&eRest: "
list-trash-label: "&eMüll: "
list-unlimited: "*"
list-footer: "&6================================"
# --- Konsolen-Hinweis (wenn Spieler-Befehl per Konsole aufgerufen) ---
console-only: "&cDieser Befehl ist nur für Spieler! (Konsole: reload, import, export, list)"
# ============================================================
# MÜLLTRUHEN-GUI
# ============================================================
# Alle Texte des Konfigurations-GUIs der Mülltruhe.
# Farbcodes: &0-&9, &a-&f | &l = Fett, &o = Kursiv
# Platzhalter: %page% = Seitennummer, %total% = Gesamtseiten, %count% = Anzahl Items
trash-gui:
# Fenstertitel
title:
de: "&4Mülltruhe konfigurieren"
en: "&4Configure Trash Chest"
# Hinweis unter jedem Filter-Item (Rechtsklick zum Entfernen)
item-remove-hint:
de: "&c▶ Rechtsklick: Entfernen"
en: "&c▶ Right-click: Remove"
# Navigations-Pfeile
btn-prev-title:
de: "&e&l◀ Vorherige Seite"
en: "&e&l◀ Previous Page"
btn-next-title:
de: "&e&lNächste Seite ▶"
en: "&e&lNext Page ▶"
# Seitenanzeige (Slot 46)
page-info-title:
de: "&f&lSeite %page% / %total%"
en: "&f&lPage %page% / %total%"
page-info-lore:
de: "&7%count% Items im Filter"
en: "&7%count% items in filter"
page-nav-lore:
de: "Seite %page% von %total%"
en: "Page %page% of %total%"
# Status-Anzeige: Deaktiviert (kein Filter gesetzt)
status-disabled-title:
de: "&c&l✗ Status: Deaktiviert"
en: "&c&l✗ Status: Disabled"
status-disabled-lore:
de:
- "&7Kein Filter gesetzt "
- "&7Items werden NICHT gelöscht."
- "&eItems hinzufügen um zu aktivieren."
en:
- "&7No filter set "
- "&7items will NOT be deleted."
- "&eAdd items to activate."
# Status-Anzeige: Aktiv (Filter gesetzt)
status-active-title:
de: "&a&l✔ Status: Aktiv"
en: "&a&l✔ Status: Active"
status-active-lore:
de:
- "&7Items werden exakt verglichen:"
- "&7Typ + Verzauberungen + Name."
en:
- "&7Items are matched exactly:"
- "&7Type + enchantments + name."
# Item hinzufügen (Slot 49)
btn-add-title:
de: "&a&l✚ Item hinzufügen"
en: "&a&l✚ Add Item"
btn-add-lore:
de:
- "&7Gewünschtes Item in die Haupthand nehmen"
- "&7und diesen Knopf klicken."
- "&eVerzauberungen & Name werden gespeichert."
en:
- "&7Hold the exact item in your main hand"
- "&7and click this button."
- "&eEnchantments & name are saved exactly."
# Mülltruhe leeren (Slot 53 Schädel-Button)
btn-clear-title:
de: "&4&lMülltruhe leeren"
en: "&4&lEmpty Trash Chest"
btn-clear-lore:
de:
- "&7Klicken um alle Items"
- "&7sofort zu löschen."
en:
- "&7Click to immediately"
- "&7delete all items."
# --- /asc import / export (Admin-Befehle) ---
mysql-not-enabled-import: "&cMySQL ist nicht aktiviert! Aktiviere MySQL in der config.yml zuerst."
mysql-not-enabled-export: "&cMySQL ist nicht aktiviert! Der Export benötigt eine aktive MySQL-Verbindung."
yaml-empty: "&cDie players.yml ist leer oder enthält keine Spielerdaten!"
import-start: "&eImportiere Daten aus players.yml nach MySQL..."
import-info: "&7Bestehende MySQL-Daten werden nicht überschrieben (REPLACE INTO)."
import-success: "&aImport erfolgreich abgeschlossen!"
# Platzhalter: %players%, %input%, %target%, %rest%
import-stats-players: "&7 Spieler: &f%players%"
import-stats-input: "&7 Eingangstruhen: &f%input%"
import-stats-target: "&7 Zieltruhen: &f%target%"
import-stats-rest: "&7 Rest-Truhen: &f%rest%"
export-start: "&eExportiere Daten aus MySQL nach players.yml..."
export-info: "&7Ein Backup der aktuellen players.yml wird erstellt."
export-success: "&aExport erfolgreich abgeschlossen!"
# Platzhalter: %file% = Backup-Dateiname
export-backup: "&7 Backup: &f%file%"
export-backup-skipped: "&7 Backup: &8Übersprungen (players.yml war leer)"
export-error: "&cExport fehlgeschlagen: &e%error%"
backup-failed: "&cBackup fehlgeschlagen: &e%error%"
# --- Autosign-Verwendungshinweis ---
autosign-player-only: "&cDieser Befehl ist nur für Spieler!"
autosign-usage: "&cVerwendung: /asc autosign <input|ziel|rest|trash> [item|hand]"

View File

@@ -1,5 +1,5 @@
name: AutoSortChest
version: 2.6
version: 2.7
main: com.viper.autosortchest.Main
api-version: 1.21
authors: [M_Viper]
@@ -7,11 +7,11 @@ description: Ein Plugin zum automatischen Sortieren von Items in Truhen
commands:
asc:
description: AutoSortChest Befehle
usage: /<command> [help|info|reload|import|export|list]
usage: /<command> [help|info|reload|import|export|list|autosign|priority]
aliases: [autosortchest]
permissions:
autosortchest.use:
description: Erlaubt das Erstellen von AutoSortChest-Schildern (Eingang, Ziel, Rest, Muelltruhe)
description: Erlaubt das Erstellen von AutoSortChest-Schildern (Eingang, Ziel, Rest, Muelltruhe) sowie die Verwendung von /asc autosign
default: true
autosortchest.reload:
description: Erlaubt das Neuladen der Konfiguration mit /asc reload