diff --git a/src/main/java/me/viper/teamplugin/Main.java b/src/main/java/me/viper/teamplugin/Main.java index 0038ec7..0ff2a35 100644 --- a/src/main/java/me/viper/teamplugin/Main.java +++ b/src/main/java/me/viper/teamplugin/Main.java @@ -1,10 +1,10 @@ package me.viper.teamplugin; import me.viper.teamplugin.commands.TeamCommand; -import me.viper.teamplugin.gui.SettingsGUI; import me.viper.teamplugin.listener.InventoryListener; import me.viper.teamplugin.manager.DataManager; import me.viper.teamplugin.manager.LangManager; +import org.bukkit.command.PluginCommand; import org.bukkit.plugin.java.JavaPlugin; public class Main extends JavaPlugin { @@ -15,15 +15,23 @@ public class Main extends JavaPlugin { public void onEnable() { instance = this; - saveDefaultConfig(); // config.yml kopieren - saveResource("lang.yml", false); // lang.yml kopieren falls nicht vorhanden + saveDefaultConfig(); // config.yml + saveResource("lang_de.yml", false); // German lang file (only if absent) + saveResource("lang_en.yml", false); // English lang file (only if absent) LangManager.setup(); DataManager.setup(); - getCommand("team").setExecutor(new TeamCommand()); - getServer().getPluginManager().registerEvents(new InventoryListener(), this); + // Register command + tab completer + TeamCommand teamCommand = new TeamCommand(); + PluginCommand cmd = getCommand("team"); + if (cmd != null) { + cmd.setExecutor(teamCommand); + cmd.setTabCompleter(teamCommand); // ← enables tab completion + } + getServer().getPluginManager().registerEvents(new InventoryListener(), this); + getServer().getPluginManager().registerEvents(new me.viper.teamplugin.listener.ChatListener(), this); getLogger().info("TeamPlugin aktiviert."); } @@ -36,4 +44,4 @@ public class Main extends JavaPlugin { public static Main getInstance() { return instance; } -} +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/commands/TeamCommand.java b/src/main/java/me/viper/teamplugin/commands/TeamCommand.java index 6bffcf4..a828912 100644 --- a/src/main/java/me/viper/teamplugin/commands/TeamCommand.java +++ b/src/main/java/me/viper/teamplugin/commands/TeamCommand.java @@ -1,137 +1,474 @@ -// Datei: src/main/java/me/viper/teamplugin/commands/TeamCommand.java package me.viper.teamplugin.commands; +import me.viper.teamplugin.Main; +import me.viper.teamplugin.gui.ApplicationGUI; import me.viper.teamplugin.gui.BackupGUI; +import me.viper.teamplugin.gui.MailboxGUI; import me.viper.teamplugin.gui.SettingsGUI; import me.viper.teamplugin.gui.TeamGUI; -import me.viper.teamplugin.manager.BackupManager; -import me.viper.teamplugin.manager.DataManager; -import me.viper.teamplugin.manager.LangManager; -import me.viper.teamplugin.Main; +import me.viper.teamplugin.manager.*; import me.viper.teamplugin.util.Utils; +import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; -public class TeamCommand implements CommandExecutor { +/** + * TeamCommand – supports both DE and EN subcommands simultaneously. + * + * The active language (from config.yml → language) determines: + * - Which subcommand names appear in tab-completion + * - Which names are shown in usage / error messages + * + * Both DE and EN subcommand names are always recognised, so admins can + * switch languages without breaking muscle memory mid-session. + * + * Subcommand names are defined in lang_de.yml / lang_en.yml under + * cmd_* keys (e.g. cmd_add, cmd_del, ...). + * + * ── Application flow ───────────────────────────────────────────────── + * Players : /team bewerben [reason] – command only, no GUI + * Admins : /team apply (no args) – opens ApplicationGUI + * /team apply accept + * /team apply deny + * /team apply list – text list (also console) + */ +public class TeamCommand implements CommandExecutor, TabCompleter { + + // ── Internal canonical names (language-independent) ─────────────── + private static final String C_ADD = "add"; + private static final String C_DEL = "del"; + private static final String C_MOVE = "move"; + private static final String C_SETTINGS = "settings"; + private static final String C_BACKUP = "backup"; + private static final String C_RESTORE = "restore"; + private static final String C_BACKUPS = "backups"; + private static final String C_RELOAD = "reload"; + private static final String C_INFO = "info"; + private static final String C_SEARCH = "search"; + private static final String C_APPLY = "apply"; + private static final String C_LOG = "log"; + private static final String C_MAILBOX = "mailbox"; + + // apply sub-sub-commands + private static final String C_LIST = "list"; + private static final String C_ACCEPT = "accept"; + private static final String C_DENY = "deny"; + + // ── DE aliases (hardcoded so they always work regardless of lang) ── + private static final Map DE_ALIASES = Map.ofEntries( + Map.entry("hinzufuegen", C_ADD), + Map.entry("hinzufügen", C_ADD), + Map.entry("entfernen", C_DEL), + Map.entry("verschieben", C_MOVE), + Map.entry("einstellungen", C_SETTINGS), + Map.entry("sicherung", C_BACKUP), + Map.entry("wiederherstellen", C_RESTORE), + Map.entry("sicherungen", C_BACKUPS), + Map.entry("neuladen", C_RELOAD), + Map.entry("suchen", C_SEARCH), + Map.entry("bewerben", C_APPLY), + Map.entry("protokoll", C_LOG), + Map.entry("postfach", C_MAILBOX) + ); + + // apply sub-sub DE aliases + private static final Map DE_APPLY_SUB = Map.of( + "liste", C_LIST, + "annehmen", C_ACCEPT, + "ablehnen", C_DENY + ); + + // ───────────────────────────────────────────────────────────────── + // Resolve subcommand → canonical + // ───────────────────────────────────────────────────────────────── + + private static String resolve(String input) { + if (input == null) return ""; + String lower = input.toLowerCase(); + if (DE_ALIASES.containsKey(lower)) return DE_ALIASES.get(lower); + return lower; + } + + private static String resolveApplySub(String input) { + if (input == null) return ""; + String lower = input.toLowerCase(); + if (DE_APPLY_SUB.containsKey(lower)) return DE_APPLY_SUB.get(lower); + return lower; + } + + // ───────────────────────────────────────────────────────────────── + // Active-language subcommand names (for tab-completion & usage) + // ───────────────────────────────────────────────────────────────── + + private static List activeSubs() { + return List.of( + LangManager.getCmd("cmd_add"), + LangManager.getCmd("cmd_del"), + LangManager.getCmd("cmd_move"), + LangManager.getCmd("cmd_settings"), + LangManager.getCmd("cmd_backup"), + LangManager.getCmd("cmd_restore"), + LangManager.getCmd("cmd_backups"), + LangManager.getCmd("cmd_reload"), + LangManager.getCmd("cmd_info"), + LangManager.getCmd("cmd_search"), + LangManager.getCmd("cmd_apply"), + LangManager.getCmd("cmd_log"), + LangManager.getCmd("cmd_mailbox") + ); + } + + private static List activeApplySubs(boolean isAdmin) { + List subs = new ArrayList<>(); + if (isAdmin) { + subs.add(LangManager.getCmd("cmd_apply_list")); + subs.add(LangManager.getCmd("cmd_apply_accept")); + subs.add(LangManager.getCmd("cmd_apply_deny")); + } else { + // Players see the available ranks as completions for applying + subs.addAll(Main.getInstance().getConfig().getStringList("ranks")); + } + return subs; + } + + // ───────────────────────────────────────────────────────────────── + // COMMAND EXECUTION + // ───────────────────────────────────────────────────────────────── @Override public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { + if (args.length == 0) { - if (!(sender instanceof Player)) { - sender.sendMessage(LangManager.get("only_player")); - return true; - } - TeamGUI.openTeamGUI((Player) sender); + if (!(sender instanceof Player p)) { sender.sendMessage(LangManager.get("only_player")); return true; } + TeamGUI.openTeamGUI(p); return true; } - String sub = args[0].toLowerCase(); + String sub = resolve(args[0]); - if (sub.equals("add")) { - if (!sender.hasPermission("teamplugin.admin")) { - sender.sendMessage(LangManager.get("no_permission")); - return true; - } - if (args.length < 3) { - sender.sendMessage(LangManager.get("add_usage")); - return true; - } - String name = args[1]; - String rank = args[2]; + switch (sub) { - List ranks = Main.getInstance().getConfig().getStringList("ranks"); - if (ranks == null || !ranks.contains(rank)) { - sender.sendMessage(Utils.color(LangManager.get("prefix")) + "§cRang §e" + rank + " §cexistiert nicht!"); - return true; + // ── add ─────────────────────────────────────────────────── + case C_ADD -> { + if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; } + if (args.length < 3) { sender.sendMessage(LangManager.get("add_usage")); return true; } + String name = args[1], rank = args[2]; + List validRanks = Main.getInstance().getConfig().getStringList("ranks"); + if (!validRanks.contains(rank)) { + sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7cRang/Rank \u00a7e" + rank + " \u00a7cdoes not exist!"); return true; + } + DataManager.addMember(rank, name); + AuditLog.log(AuditLog.ADD, senderName(sender), name + " \u2192 " + rank); + sender.sendMessage(Utils.color(Utils.replace(LangManager.get("player_added"), "%player%", name, "%rank%", rank))); } - DataManager.addMember(rank, name); - sender.sendMessage(Utils.replace(LangManager.get("player_added"), "%player%", name, "%rank%", rank)); - return true; - } + // ── del ─────────────────────────────────────────────────── + case C_DEL -> { + if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; } + if (args.length < 2) { sender.sendMessage(LangManager.get("del_usage")); return true; } + String name = args[1]; + String oldRank = findRank(name); + boolean removed = DataManager.removeMember(name); + if (removed) AuditLog.log(AuditLog.REMOVE, senderName(sender), name + " (was: " + (oldRank != null ? oldRank : "?") + ")"); + sender.sendMessage(Utils.color(removed + ? Utils.replace(LangManager.get("player_removed"), "%player%", name) + : Utils.replace(LangManager.get("player_not_found"), "%player%", name))); + } - if (sub.equals("del")) { - if (!sender.hasPermission("teamplugin.admin")) { - sender.sendMessage(LangManager.get("no_permission")); - return true; + // ── move ────────────────────────────────────────────────── + case C_MOVE -> { + if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; } + if (args.length < 3) { sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7cUsage: /team " + LangManager.getCmd("cmd_move") + " "); return true; } + String name = args[1], newRank = args[2]; + List validRanks = Main.getInstance().getConfig().getStringList("ranks"); + if (!validRanks.contains(newRank)) { sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7cRank \u00a7e" + newRank + " \u00a7cdoes not exist!"); return true; } + String oldRank = findRank(name); + if (oldRank == null) { sender.sendMessage(Utils.color(Utils.replace(LangManager.get("player_not_found"), "%player%", name))); return true; } + if (oldRank.equals(newRank)) { sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7e" + name + " \u00a7cis already in \u00a7e" + newRank + "\u00a7c!"); return true; } + DataManager.removeMember(name); + DataManager.addMember(newRank, name); + AuditLog.log(AuditLog.MOVE, senderName(sender), name + ": " + oldRank + " \u2192 " + newRank); + sender.sendMessage(Utils.color(Utils.replace(LangManager.get("player_moved"), "%player%", name, "%old%", oldRank, "%new%", newRank))); } - if (args.length < 2) { - sender.sendMessage(LangManager.get("del_usage")); - return true; - } - String name = args[1]; - boolean removed = DataManager.removeMember(name); - if (removed) - sender.sendMessage(Utils.replace(LangManager.get("player_removed"), "%player%", name)); - else - sender.sendMessage(Utils.replace(LangManager.get("player_not_found"), "%player%", name)); - return true; - } - if (sub.equals("settings")) { - if (!(sender instanceof Player) || !sender.hasPermission("teamplugin.admin")) { - sender.sendMessage(LangManager.get("no_permission")); - return true; + // ── settings ────────────────────────────────────────────── + case C_SETTINGS -> { + if (!(sender instanceof Player p) || !p.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; } + SettingsGUI.openSettings(p); } - SettingsGUI.openSettings((Player) sender); - return true; - } - if (sub.equals("backup")) { - if (!sender.hasPermission("teamplugin.admin")) { - sender.sendMessage(LangManager.get("no_permission")); - return true; + // ── backup ──────────────────────────────────────────────── + case C_BACKUP -> { + if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; } + String file = BackupManager.createBackup(); + sender.sendMessage(Utils.color(file != null + ? Utils.replace(LangManager.get("backup_created"), "%file%", file) + : LangManager.get("prefix") + "\u00a7cBackup failed.")); } - String file = BackupManager.createBackup(); - if (file != null) sender.sendMessage(Utils.replace(LangManager.get("backup_created"), "%file%", file)); - else sender.sendMessage(Utils.color(LangManager.get("prefix")) + "§cBackup fehlgeschlagen."); - return true; - } - if (sub.equals("restore")) { - if (!sender.hasPermission("teamplugin.admin")) { - sender.sendMessage(LangManager.get("no_permission")); - return true; + // ── restore ─────────────────────────────────────────────── + case C_RESTORE -> { + if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; } + if (args.length < 2) { sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7cUsage: /team " + LangManager.getCmd("cmd_restore") + " "); return true; } + boolean ok = BackupManager.restoreBackup(args[1]); + sender.sendMessage(Utils.color(ok + ? Utils.replace(LangManager.get("backup_restore_success"), "%file%", args[1]) + : Utils.replace(LangManager.get("backup_not_found"), "%file%", args[1]))); } - if (args.length < 2) { - sender.sendMessage(Utils.color(LangManager.get("prefix")) + "§cVerwendung: /team restore "); - return true; - } - String file = args[1]; - boolean ok = BackupManager.restoreBackup(file); - if (ok) sender.sendMessage(Utils.replace(LangManager.get("backup_restore_success"), "%file%", file)); - else sender.sendMessage(Utils.replace(LangManager.get("backup_not_found"), "%file%", file)); - return true; - } - if (sub.equals("backups")) { - if (!(sender instanceof Player)) { - // Console: list backups in chat - List list = BackupManager.listBackups(); - if (list.isEmpty()) { - sender.sendMessage(LangManager.get("no_backups")); + // ── backups ─────────────────────────────────────────────── + case C_BACKUPS -> { + if (sender instanceof Player p) { + if (!p.hasPermission("teamplugin.admin")) { p.sendMessage(LangManager.get("no_permission")); return true; } + BackupGUI.openBackupGUI(p); + } else { + List list = BackupManager.listBackups(); + if (list.isEmpty()) { sender.sendMessage(Utils.color(LangManager.get("no_backups"))); return true; } + sender.sendMessage(Utils.color(LangManager.get("backups_list_title"))); + list.forEach(s -> sender.sendMessage("\u00a77- \u00a7e" + s)); + } + } + + // ── reload ──────────────────────────────────────────────── + case C_RELOAD -> { + if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; } + Main.getInstance().reloadConfig(); + LangManager.setup(); + DataManager.reloadData(); + MailboxManager.reload(); + AuditLog.reload(); + sender.sendMessage(Utils.color(LangManager.get("plugin_reloaded"))); + } + + // ── info ────────────────────────────────────────────────── + case C_INFO -> { + if (args.length < 2) { sender.sendMessage(Utils.color(LangManager.get("info_usage"))); return true; } + String infoName = args[1]; + String infoRank = findRank(infoName); + if (infoRank == null) { sender.sendMessage(Utils.color(Utils.replace(LangManager.get("info_not_team"), "%player%", infoName))); return true; } + String rankDisplay = Main.getInstance().getConfig().getString("rank-settings." + infoRank + ".display", infoRank); + String iso = DataManager.getJoinDate(infoName); + String joinDate = (iso != null && !iso.isEmpty()) ? Utils.prettifyIso(iso) : "\u00a77\u2014"; + boolean isOnline = Bukkit.getOfflinePlayer(infoName).isOnline(); + String statusOn = Main.getInstance().getConfig().getString("status.online", "&a\uD83D\uDFE2 Online"); + String statusOff = Main.getInstance().getConfig().getString("status.offline", "&c\uD83D\uDD34 Offline"); + sender.sendMessage(Utils.color(Utils.replace(LangManager.get("info_header"), "%player%", infoName))); + sender.sendMessage(Utils.color(Utils.replace(LangManager.get("info_rank"), "%rank%", rankDisplay))); + sender.sendMessage(Utils.color(Utils.replace(LangManager.get("info_joined"), "%joindate%", joinDate))); + sender.sendMessage(Utils.color(Utils.replace(LangManager.get("info_status"), "%status%", isOnline ? statusOn : statusOff))); + } + + // ── search ──────────────────────────────────────────────── + case C_SEARCH -> { + if (args.length < 2) { sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7cUsage: /team " + LangManager.getCmd("cmd_search") + " "); return true; } + String query = args[1].toLowerCase(); + List found = new ArrayList<>(); + var section = DataManager.getData().getConfigurationSection("Team"); + if (section != null) + for (String r : section.getKeys(false)) + for (String m : DataManager.getData().getStringList("Team." + r)) + if (m.toLowerCase().contains(query)) found.add(m + " \u00a78(\u00a77" + r + "\u00a78)"); + if (found.isEmpty()) { + sender.sendMessage(Utils.color(Utils.replace(LangManager.get("search_no_results"), "%query%", args[1]))); + } else { + sender.sendMessage(Utils.color(Utils.replace(LangManager.get("search_results"), "%query%", args[1]))); + String pre = Utils.color(LangManager.get("prefix")); + found.forEach(r -> sender.sendMessage(pre + "\u00a77- \u00a7e" + r)); + } + } + + // ── log ─────────────────────────────────────────────────── + case C_LOG -> { + if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; } + int count = 10; + if (args.length >= 2) { try { count = Math.min(50, Math.max(1, Integer.parseInt(args[1]))); } catch (NumberFormatException ignored) {} } + List entries = AuditLog.getEntries(count); + String pre = Utils.color(LangManager.get("prefix")); + if (entries.isEmpty()) { sender.sendMessage(pre + "\u00a77No audit entries yet."); return true; } + sender.sendMessage(pre + "\u00a7bAudit Log \u00a78(last " + entries.size() + ")\u00a78:"); + entries.forEach(e -> sender.sendMessage("\u00a78\u00bb \u00a77" + e)); + } + + // ── mailbox ─────────────────────────────────────────────── + case C_MAILBOX -> { + if (!(sender instanceof Player p)) { sender.sendMessage(LangManager.get("only_player")); return true; } + MailboxGUI.openMailbox(p); + } + + // ── apply ───────────────────────────────────────────────── + // + // Admins (/team apply) → opens ApplicationGUI (manage applications) + // Admins (/team apply list) → text list + // Admins (/team apply accept ) → accept via command + // Admins (/team apply deny ) → deny via command + // + // Players (/team bewerben [reason]) → submit application via command, NO GUI + // + case C_APPLY -> { + + // ── No extra argument ──────────────────────────────── + if (args.length < 2) { + // Admins: open the application management GUI + if (sender.hasPermission("teamplugin.admin")) { + if (!(sender instanceof Player p)) { sender.sendMessage(LangManager.get("only_player")); return true; } + ApplicationGUI.openApplicationList(p, 0); + } else { + // Players: show usage hint + if (!(sender instanceof Player p)) { sender.sendMessage(LangManager.get("only_player")); return true; } + p.sendMessage(Utils.color(LangManager.get("prefix")) + + "\u00a77" + LangManager.get("apply_usage")); + } return true; } - sender.sendMessage(LangManager.get("backups_list_title")); - list.forEach(s -> sender.sendMessage("§7- §e" + s)); - return true; + + String sub2 = resolveApplySub(args[1]); + + // ── list ───────────────────────────────────────────── + if (sub2.equals(C_LIST)) { + if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; } + List all = ApplicationManager.getAllApplications(); + String pre = Utils.color(LangManager.get("prefix")); + if (all.isEmpty()) { sender.sendMessage(pre + "\u00a77No pending applications."); return true; } + sender.sendMessage(pre + "\u00a7bPending applications \u00a78(" + all.size() + ")\u00a78:"); + for (String[] a : all) + sender.sendMessage("\u00a78\u00bb \u00a7e" + a[1] + " \u00a78\u2192 \u00a77" + a[0] + + " \u00a78| \u00a77" + Utils.prettifyIso(a[2]) + + (a[3].equals("-") ? "" : " \u00a78| \u00a77" + a[3])); + return true; + } + + // ── accept / deny ──────────────────────────────────── + if (sub2.equals(C_ACCEPT) || sub2.equals(C_DENY)) { + if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; } + if (args.length < 3) { + sender.sendMessage(Utils.color(LangManager.get("prefix")) + + "\u00a7cUsage: /team " + LangManager.getCmd("cmd_apply") + + " " + args[1] + " "); + return true; + } + String applicantName = args[2]; + String appliedRank = ApplicationManager.findApplication(applicantName); + if (appliedRank == null) { + sender.sendMessage(Utils.color(LangManager.get("prefix")) + + "\u00a7cNo application found for \u00a7e" + applicantName); + return true; + } + ApplicationManager.removeApplication(applicantName); + if (sub2.equals(C_ACCEPT)) { + DataManager.addMember(appliedRank, applicantName); + AuditLog.log(AuditLog.APPLY_ACCEPT, senderName(sender), applicantName + " \u2192 " + appliedRank); + sender.sendMessage(Utils.color(Utils.replace(LangManager.get("apply_accepted"), "%player%", applicantName, "%rank%", appliedRank))); + Player applicant = Bukkit.getPlayerExact(applicantName); + if (applicant != null) applicant.sendMessage(Utils.color(Utils.replace(LangManager.get("apply_you_accepted"), "%rank%", appliedRank))); + } else { + AuditLog.log(AuditLog.APPLY_DENY, senderName(sender), applicantName + " for " + appliedRank); + sender.sendMessage(Utils.color(Utils.replace(LangManager.get("apply_denied"), "%player%", applicantName))); + Player applicant = Bukkit.getPlayerExact(applicantName); + if (applicant != null) applicant.sendMessage(Utils.color(Utils.replace(LangManager.get("apply_you_denied"), "%rank%", appliedRank))); + } + return true; + } + + // ── Player submits application via command ──────────── + // /team bewerben [reason...] + if (!(sender instanceof Player p)) { sender.sendMessage(LangManager.get("only_player")); return true; } + + // Admins who accidentally type a rank: redirect to the GUI + if (p.hasPermission("teamplugin.admin")) { + ApplicationGUI.openApplicationList(p, 0); + return true; + } + + List validRanks = Main.getInstance().getConfig().getStringList("ranks"); + String targetRank = args[1]; + if (!validRanks.contains(targetRank)) { + p.sendMessage(Utils.color(LangManager.get("prefix")) + + "\u00a7c" + LangManager.get("apply_usage")); + return true; + } + + // Already a team member? + if (findRank(p.getName()) != null) { + p.sendMessage(Utils.color(LangManager.get("apply_already_member"))); + return true; + } + + String reason = args.length > 2 + ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) + : ""; + ApplicationManager.apply(p, targetRank, reason); } - // Player: open Backup GUI - Player p = (Player) sender; - if (!p.hasPermission("teamplugin.admin")) { - p.sendMessage(LangManager.get("no_permission")); - return true; - } - BackupGUI.openBackupGUI(p); - return true; + default -> sender.sendMessage(Utils.color(LangManager.get("unknown_command"))); } - - sender.sendMessage(LangManager.get("unknown_command")); return true; } -} + + // ───────────────────────────────────────────────────────────────── + // TAB COMPLETION (shows only active-language names) + // ───────────────────────────────────────────────────────────────── + + @Override + public List onTabComplete(CommandSender sender, Command cmd, String label, String[] args) { + if (args.length == 1) return filter(activeSubs(), args[0]); + + String sub = resolve(args[0]); + + if (args.length == 2) return switch (sub) { + case C_ADD -> onlinePlayers(args[1]); + case C_DEL, C_INFO, C_MOVE -> filter(allTeamMembers(), args[1]); + case C_RESTORE -> filter(BackupManager.listBackups(), args[1]); + case C_SEARCH -> filter(allTeamMembers(), args[1]); + case C_LOG -> List.of("10", "25", "50"); + case C_APPLY -> filter(activeApplySubs(sender.hasPermission("teamplugin.admin")), args[1]); + default -> new ArrayList<>(); + }; + + if (args.length == 3) { + if (sub.equals(C_ADD) || sub.equals(C_MOVE)) + return filter(Main.getInstance().getConfig().getStringList("ranks"), args[2]); + if (sub.equals(C_APPLY)) { + String sub2 = resolveApplySub(args[1]); + if (sub2.equals(C_ACCEPT) || sub2.equals(C_DENY)) + return filter(ApplicationManager.getApplicantNames(), args[2]); + } + } + return new ArrayList<>(); + } + + // ───────────────────────────────────────────────────────────────── + // HELPERS + // ───────────────────────────────────────────────────────────────── + + private List filter(List src, String prefix) { + return src.stream().filter(s -> s.toLowerCase().startsWith(prefix.toLowerCase())).collect(Collectors.toList()); + } + private List onlinePlayers(String prefix) { + return Bukkit.getOnlinePlayers().stream().map(Player::getName) + .filter(n -> n.toLowerCase().startsWith(prefix.toLowerCase())).collect(Collectors.toList()); + } + private List allTeamMembers() { + var section = DataManager.getData().getConfigurationSection("Team"); + if (section == null) return new ArrayList<>(); + List all = new ArrayList<>(); + for (String rank : section.getKeys(false)) all.addAll(DataManager.getData().getStringList("Team." + rank)); + return all; + } + private String findRank(String name) { + var section = DataManager.getData().getConfigurationSection("Team"); + if (section == null) return null; + for (String rank : section.getKeys(false)) + for (String m : DataManager.getData().getStringList("Team." + rank)) + if (m.equalsIgnoreCase(name)) return rank; + return null; + } + private static String senderName(CommandSender sender) { + return (sender instanceof Player p) ? p.getName() : "CONSOLE"; + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/gui/ApplicationGUI.java b/src/main/java/me/viper/teamplugin/gui/ApplicationGUI.java new file mode 100644 index 0000000..6c1446c --- /dev/null +++ b/src/main/java/me/viper/teamplugin/gui/ApplicationGUI.java @@ -0,0 +1,326 @@ +package me.viper.teamplugin.gui; + +import me.viper.teamplugin.Main; +import me.viper.teamplugin.manager.ApplicationManager; +import me.viper.teamplugin.manager.AuditLog; +import me.viper.teamplugin.manager.DataManager; +import me.viper.teamplugin.manager.LangManager; +import me.viper.teamplugin.util.Utils; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; + +import java.util.*; + +/** + * ApplicationGUI – Admin-only GUI for reviewing and managing pending applications. + * + * LIST VIEW (54 slots) /team apply [admin only] + * ┌───────────────────────────────────┐ + * │ Header (cyan glass) │ + * │ Applicant heads slots 9-44 │ one PLAYER_HEAD per pending application + * │ Footer: ←45 | close 49 | →53 │ + * └───────────────────────────────────┘ + * + * DETAIL VIEW (54 slots) + * ┌───────────────────────────────────┐ + * │ Background filler │ + * │ slot 13 Applicant head + info │ + * │ slot 20 Accept (GREEN) │ + * │ slot 24 Deny (RED) │ + * │ slot 49 Back │ + * └───────────────────────────────────┘ + * + * Players apply purely via command: /team bewerben [reason] + * No GUI is shown to the applicant. + */ +public class ApplicationGUI { + + private static final int ITEMS_PER_PAGE = 36; + + /** UUID → current list-view page */ + private static final Map CURRENT_PAGE = new HashMap<>(); + /** UUID → applicant name currently shown in the detail view */ + private static final Map VIEWING_PLAYER = new HashMap<>(); + + // ── Open: list view ─────────────────────────────────────────────── + + public static void openApplicationList(Player admin, int page) { + List all = ApplicationManager.getAllApplications(); // [rank, name, iso, reason] + int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / ITEMS_PER_PAGE)); + page = Math.max(0, Math.min(page, totalPages - 1)); + CURRENT_PAGE.put(admin.getUniqueId(), page); + + Inventory inv = Bukkit.createInventory(null, 54, Utils.color(getSelectionTitle())); + + // Header row + ItemStack hGlass = filler(Material.CYAN_STAINED_GLASS_PANE); + for (int i = 0; i < 9; i++) inv.setItem(i, hGlass); + + // Footer row + ItemStack fGlass = filler(Material.BLACK_STAINED_GLASS_PANE); + for (int i = 45; i < 54; i++) inv.setItem(i, fGlass); + + // Info item (slot 4) + inv.setItem(4, buildInfoItem(all.size(), page + 1, totalPages)); + + // Applicant items (slots 9-44) + int start = page * ITEMS_PER_PAGE; + for (int i = 0; i < ITEMS_PER_PAGE; i++) { + int idx = start + i; + if (idx >= all.size()) break; + inv.setItem(9 + i, buildApplicantItem(all.get(idx))); + } + + // Navigation + if (page > 0) inv.setItem(45, navItem(Material.ARROW, + Utils.color(LangManager.get("mailbox_prev_page")), List.of())); + inv.setItem(49, navItem(Material.BARRIER, + Utils.color(LangManager.get("apply_gui_close")), List.of())); + if (page + 1 < totalPages) inv.setItem(53, navItem(Material.ARROW, + Utils.color(LangManager.get("mailbox_next_page")), List.of())); + + admin.openInventory(inv); + } + + // ── Open: detail view ───────────────────────────────────────────── + + private static void openDetail(Player admin, String[] app) { + // app: [rank, name, iso, reason] + VIEWING_PLAYER.put(admin.getUniqueId(), app[1]); + + Inventory inv = Bukkit.createInventory(null, 54, Utils.color(getDetailTitle())); + + ItemStack bg = filler(Material.GRAY_STAINED_GLASS_PANE); + for (int i = 0; i < 54; i++) inv.setItem(i, bg); + + // Applicant head + full info (slot 13) + inv.setItem(13, buildDetailItem(app)); + + // Accept (slot 20) + String rankDisplay = Main.getInstance().getConfig() + .getString("rank-settings." + app[0] + ".display", app[0]); + inv.setItem(20, navItem(Material.GREEN_CONCRETE, + Utils.color("&a&l" + LangManager.get("apply_accepted_btn")), + List.of(Utils.color("&7" + app[1] + " &8→ " + rankDisplay)))); + + // Deny (slot 24) + inv.setItem(24, navItem(Material.RED_CONCRETE, + Utils.color("&c&l" + LangManager.get("apply_denied_btn")), + List.of(Utils.color("&7" + LangManager.get("apply_deny_lore"))))); + + // Back (slot 49) + inv.setItem(49, navItem(Material.ARROW, + Utils.color(LangManager.get("mailbox_back_btn")), List.of())); + + admin.openInventory(inv); + } + + // ── Click handlers ──────────────────────────────────────────────── + + public static void handleSelectionClick(Player admin, InventoryClickEvent e) { + e.setCancelled(true); + int slot = e.getRawSlot(); + ItemStack clicked = e.getCurrentItem(); + if (clicked == null || clicked.getType().isAir()) return; + + int page = CURRENT_PAGE.getOrDefault(admin.getUniqueId(), 0); + + switch (slot) { + case 45 -> openApplicationList(admin, page - 1); + case 49 -> { cleanup(admin.getUniqueId()); admin.closeInventory(); } + case 53 -> openApplicationList(admin, page + 1); + default -> { + if (slot >= 9 && slot <= 44) { + List all = ApplicationManager.getAllApplications(); + int idx = page * ITEMS_PER_PAGE + (slot - 9); + if (idx < all.size()) openDetail(admin, all.get(idx)); + } + } + } + } + + public static void handleDetailClick(Player admin, InventoryClickEvent e) { + e.setCancelled(true); + int slot = e.getRawSlot(); + ItemStack clicked = e.getCurrentItem(); + if (clicked == null || clicked.getType().isAir()) return; + + String applicantName = VIEWING_PLAYER.get(admin.getUniqueId()); + if (applicantName == null) { admin.closeInventory(); return; } + + int page = CURRENT_PAGE.getOrDefault(admin.getUniqueId(), 0); + String appliedRank = ApplicationManager.findApplication(applicantName); + + switch (slot) { + case 20 -> { // Accept + if (appliedRank != null) { + ApplicationManager.removeApplication(applicantName); + DataManager.addMember(appliedRank, applicantName); + AuditLog.log(AuditLog.APPLY_ACCEPT, admin.getName(), + applicantName + " \u2192 " + appliedRank); + admin.sendMessage(Utils.color(Utils.replace( + LangManager.get("apply_accepted"), + "%player%", applicantName, "%rank%", appliedRank))); + Player applicant = Bukkit.getPlayerExact(applicantName); + if (applicant != null) applicant.sendMessage(Utils.color( + Utils.replace(LangManager.get("apply_you_accepted"), + "%rank%", appliedRank))); + } + VIEWING_PLAYER.remove(admin.getUniqueId()); + openApplicationList(admin, page); + } + case 24 -> { // Deny + if (appliedRank != null) { + ApplicationManager.removeApplication(applicantName); + AuditLog.log(AuditLog.APPLY_DENY, admin.getName(), + applicantName + " for " + appliedRank); + admin.sendMessage(Utils.color(Utils.replace( + LangManager.get("apply_denied"), + "%player%", applicantName))); + Player applicant = Bukkit.getPlayerExact(applicantName); + if (applicant != null) applicant.sendMessage(Utils.color( + Utils.replace(LangManager.get("apply_you_denied"), + "%rank%", appliedRank))); + } + VIEWING_PLAYER.remove(admin.getUniqueId()); + openApplicationList(admin, page); + } + case 49 -> { // Back + VIEWING_PLAYER.remove(admin.getUniqueId()); + openApplicationList(admin, page); + } + } + } + + // ── Title helpers ───────────────────────────────────────────────── + + public static String getSelectionTitle() { + return LangManager.get("apply_gui_selection_title"); + } + + public static String getDetailTitle() { + return LangManager.get("apply_gui_detail_title"); + } + + public static boolean isSelectionTitle(String colored) { + return colored.equals(Utils.color(getSelectionTitle())); + } + + public static boolean isDetailTitle(String colored) { + return colored.equals(Utils.color(getDetailTitle())); + } + + // ── Cleanup ──────────────────────────────────────────────────────── + + public static void cleanup(UUID uuid) { + CURRENT_PAGE.remove(uuid); + VIEWING_PLAYER.remove(uuid); + } + + // ── Stubs kept so ChatListener compiles without changes ──────────── + // (The old player-text-input flow has been removed.) + public static boolean isAwaitingText(UUID uuid) { return false; } + public static boolean handleTextInput(Player player, String text) { return false; } + + // ── Item builders ───────────────────────────────────────────────── + + private static ItemStack buildInfoItem(int total, int page, int totalPages) { + ItemStack item = new ItemStack(Material.ENCHANTED_BOOK); + ItemMeta m = item.getItemMeta(); + if (m != null) { + m.setDisplayName(Utils.color("&b&lBewerbungen")); + m.setLore(List.of( + Utils.color("&7Ausstehend&8: &e" + total), + Utils.color("&7Seite &e" + page + " &7/ &e" + totalPages) + )); + item.setItemMeta(m); + } + return item; + } + + private static ItemStack buildApplicantItem(String[] app) { + // app: [rank, name, iso, reason] + ItemStack item = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) item.getItemMeta(); + if (meta != null) { + meta.setOwningPlayer(Bukkit.getOfflinePlayer(app[1])); + meta.setDisplayName(Utils.color("&e&l" + app[1])); + String rankDisplay = Main.getInstance().getConfig() + .getString("rank-settings." + app[0] + ".display", app[0]); + List lore = new ArrayList<>(); + lore.add(Utils.color("&7Rang&8: " + rankDisplay)); + lore.add(Utils.color("&7Datum&8: &f" + Utils.prettifyIso(app[2]))); + if (!app[3].equals("-")) lore.add(Utils.color("&7Grund&8: &f" + app[3])); + lore.add(""); + lore.add(Utils.color("&aKlicken zum Bearbeiten")); + meta.setLore(lore); + item.setItemMeta(meta); + } + return item; + } + + private static ItemStack buildDetailItem(String[] app) { + // app: [rank, name, iso, reason] + ItemStack item = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) item.getItemMeta(); + if (meta != null) { + meta.setOwningPlayer(Bukkit.getOfflinePlayer(app[1])); + meta.setDisplayName(Utils.color("&e&l" + app[1])); + String rankDisplay = Main.getInstance().getConfig() + .getString("rank-settings." + app[0] + ".display", app[0]); + List lore = new ArrayList<>(); + lore.add(Utils.color("&7Rang&8: " + rankDisplay)); + lore.add(Utils.color("&7Datum&8: &f" + Utils.prettifyIso(app[2]))); + lore.add(""); + if (app[3].equals("-")) { + lore.add(Utils.color("&8Kein Bewerbungstext angegeben")); + } else { + lore.add(Utils.color("&7Bewerbungstext&8:")); + for (String line : wrapText(app[3], 35)) + lore.add(Utils.color("&f " + line)); + } + meta.setLore(lore); + item.setItemMeta(meta); + } + return item; + } + + // ── Generic helpers ─────────────────────────────────────────────── + + private static ItemStack filler(Material mat) { + ItemStack item = new ItemStack(mat); + ItemMeta m = item.getItemMeta(); + if (m != null) { m.setDisplayName(" "); item.setItemMeta(m); } + return item; + } + + private static ItemStack navItem(Material mat, String name, List lore) { + ItemStack item = new ItemStack(mat); + ItemMeta m = item.getItemMeta(); + if (m != null) { m.setDisplayName(name); m.setLore(lore); item.setItemMeta(m); } + return item; + } + + private static List wrapText(String text, int maxChars) { + List lines = new ArrayList<>(); + String[] words = text.split(" "); + StringBuilder cur = new StringBuilder(); + for (String w : words) { + if (cur.length() > 0 && cur.length() + 1 + w.length() > maxChars) { + lines.add(cur.toString()); + cur = new StringBuilder(w); + } else { + if (cur.length() > 0) cur.append(' '); + cur.append(w); + } + } + if (cur.length() > 0) lines.add(cur.toString()); + return lines.isEmpty() ? List.of(text) : lines; + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/gui/MailboxGUI.java b/src/main/java/me/viper/teamplugin/gui/MailboxGUI.java new file mode 100644 index 0000000..2241a15 --- /dev/null +++ b/src/main/java/me/viper/teamplugin/gui/MailboxGUI.java @@ -0,0 +1,313 @@ +package me.viper.teamplugin.gui; + +import me.viper.teamplugin.manager.LangManager; +import me.viper.teamplugin.manager.MailboxManager; +import me.viper.teamplugin.manager.MessageManager; +import me.viper.teamplugin.util.Utils; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.*; + +/** + * MailboxGUI – two-view postfach for TeamPlugin. + * + * LIST VIEW (54 slots) + * ┌─────────────────────────┐ + * │ Header row (glass) │ slot 4 = mailbox info item + * │ Message items 9-44 │ WRITTEN_BOOK=unread, BOOK=read + * │ Footer nav 45/49/53 │ ← page | close | page → + * └─────────────────────────┘ + * + * READ VIEW (54 slots) + * ┌─────────────────────────┐ + * │ Background filler │ + * │ Full message at 22 │ + * │ Delete:20 Reply:24 │ + * │ Back:49 │ + * └─────────────────────────┘ + */ +public class MailboxGUI { + + private static final int ITEMS_PER_PAGE = 36; // rows 1-4, slots 9-44 + + /** UUID → current list-view page number */ + private static final Map CURRENT_PAGE = new HashMap<>(); + /** UUID → whose mailbox is open (player name) */ + private static final Map OPEN_FOR_PLAYER = new HashMap<>(); + /** UUID → msgId of message currently being read */ + private static final Map READING_ID = new HashMap<>(); + + // ── Open: list view ─────────────────────────────────────────────── + + /** Opens the caller's own mailbox at page 0. */ + public static void openMailbox(Player viewer) { + openMailbox(viewer, viewer.getName(), 0); + } + + /** Opens a specific player's mailbox at a given page (admins can inspect others). */ + public static void openMailbox(Player viewer, String targetPlayer, int page) { + List messages = MailboxManager.getParsedMessages(targetPlayer); + int totalPages = Math.max(1, (int) Math.ceil((double) messages.size() / ITEMS_PER_PAGE)); + page = Math.max(0, Math.min(page, totalPages - 1)); + + OPEN_FOR_PLAYER.put(viewer.getUniqueId(), targetPlayer); + CURRENT_PAGE.put(viewer.getUniqueId(), page); + + Inventory inv = Bukkit.createInventory(null, 54, Utils.color(getListTitle())); + + // Header row + ItemStack headerGlass = filler(Material.CYAN_STAINED_GLASS_PANE); + for (int i = 0; i < 9; i++) inv.setItem(i, headerGlass); + + // Footer row + ItemStack footerGlass = filler(Material.BLACK_STAINED_GLASS_PANE); + for (int i = 45; i < 54; i++) inv.setItem(i, footerGlass); + + // Info item (slot 4) + int unread = MailboxManager.getUnreadCount(targetPlayer); + inv.setItem(4, buildInfoItem(targetPlayer, unread, page + 1, totalPages)); + + // Message items (slots 9-44) + int start = page * ITEMS_PER_PAGE; + for (int i = 0; i < ITEMS_PER_PAGE; i++) { + int idx = start + i; + if (idx >= messages.size()) break; + inv.setItem(9 + i, buildMessageListItem(messages.get(idx))); + } + + // Navigation + if (page > 0) inv.setItem(45, navItem(Material.ARROW, + Utils.color(LangManager.get("mailbox_prev_page")), + List.of(Utils.color("&7Vorherige Seite")))); + + inv.setItem(49, navItem(Material.BARRIER, + Utils.color(LangManager.get("mailbox_close")), + List.of(Utils.color("&7Postfach schließen")))); + + if (page + 1 < totalPages) inv.setItem(53, navItem(Material.ARROW, + Utils.color(LangManager.get("mailbox_next_page")), + List.of(Utils.color("&7Nächste Seite")))); + + viewer.openInventory(inv); + } + + // ── Open: read view ─────────────────────────────────────────────── + + private static void openMessage(Player viewer, String[] parts) { + // parts: [id, from, isoTimestamp, read, text] + String msgId = parts[0]; + String from = parts[1]; + String date = Utils.prettifyIso(parts[2]); + String text = parts[4]; + + READING_ID.put(viewer.getUniqueId(), msgId); + + String targetPlayer = OPEN_FOR_PLAYER.getOrDefault(viewer.getUniqueId(), viewer.getName()); + MailboxManager.markRead(targetPlayer, msgId); + + Inventory inv = Bukkit.createInventory(null, 54, Utils.color(getReadTitle())); + + // Background + ItemStack bg = filler(Material.GRAY_STAINED_GLASS_PANE); + for (int i = 0; i < 54; i++) inv.setItem(i, bg); + + // Message paper at slot 22 + inv.setItem(22, buildFullMessageItem(from, date, text)); + + // Delete (slot 20) + inv.setItem(20, navItem(Material.RED_CONCRETE, + Utils.color(LangManager.get("mailbox_delete_btn")), + List.of(Utils.color("&7Nachricht endgültig löschen")))); + + // Reply (slot 24) + inv.setItem(24, navItem(Material.WRITABLE_BOOK, + Utils.color(Utils.replace(LangManager.get("mailbox_reply_btn"), "%player%", from)), + List.of(Utils.color("&7An &b" + from + " &7antworten")))); + + // Back (slot 49) + inv.setItem(49, navItem(Material.ARROW, + Utils.color(LangManager.get("mailbox_back_btn")), + List.of(Utils.color("&7Zurück zur Liste")))); + + viewer.openInventory(inv); + } + + // ── Click handlers ──────────────────────────────────────────────── + + public static void handleListClick(Player player, InventoryClickEvent e) { + e.setCancelled(true); + int slot = e.getRawSlot(); + ItemStack clicked = e.getCurrentItem(); + if (clicked == null || clicked.getType().isAir()) return; + + String target = OPEN_FOR_PLAYER.getOrDefault(player.getUniqueId(), player.getName()); + int page = CURRENT_PAGE.getOrDefault(player.getUniqueId(), 0); + + switch (slot) { + case 45 -> openMailbox(player, target, page - 1); + case 49 -> { + cleanup(player.getUniqueId()); + player.closeInventory(); + } + case 53 -> openMailbox(player, target, page + 1); + default -> { + if (slot >= 9 && slot <= 44) { + List messages = MailboxManager.getParsedMessages(target); + int idx = page * ITEMS_PER_PAGE + (slot - 9); + if (idx < messages.size()) openMessage(player, messages.get(idx)); + } + } + } + } + + public static void handleReadClick(Player player, InventoryClickEvent e) { + e.setCancelled(true); + int slot = e.getRawSlot(); + ItemStack clicked = e.getCurrentItem(); + if (clicked == null || clicked.getType().isAir()) return; + + String target = OPEN_FOR_PLAYER.getOrDefault(player.getUniqueId(), player.getName()); + String msgId = READING_ID.get(player.getUniqueId()); + int page = CURRENT_PAGE.getOrDefault(player.getUniqueId(), 0); + + switch (slot) { + case 20 -> { // delete + if (msgId != null) MailboxManager.delete(target, msgId); + READING_ID.remove(player.getUniqueId()); + player.sendMessage(Utils.color(LangManager.get("mail_deleted"))); + openMailbox(player, target, page); + } + case 24 -> { // reply + if (msgId != null) { + MailboxManager.getParsedMessages(target).stream() + .filter(a -> a[0].equals(msgId)) + .findFirst() + .ifPresent(parts -> { + cleanup(player.getUniqueId()); + player.closeInventory(); + MessageManager.startReply(player, parts[1]); + }); + } + } + case 49 -> { // back + READING_ID.remove(player.getUniqueId()); + openMailbox(player, target, page); + } + } + } + + // ── Title helpers ───────────────────────────────────────────────── + + public static String getListTitle() { + return LangManager.get("mailbox_title"); + } + + public static String getReadTitle() { + return LangManager.get("mailbox_read_title"); + } + + public static boolean isListTitle(String colored) { + return colored.equals(Utils.color(getListTitle())); + } + + public static boolean isReadTitle(String colored) { + return colored.equals(Utils.color(getReadTitle())); + } + + // ── State cleanup ───────────────────────────────────────────────── + + public static void cleanup(UUID uuid) { + CURRENT_PAGE.remove(uuid); + OPEN_FOR_PLAYER.remove(uuid); + READING_ID.remove(uuid); + } + + // ── Item builders ───────────────────────────────────────────────── + + private static ItemStack buildInfoItem(String owner, int unread, int page, int total) { + ItemStack item = new ItemStack(Material.ENCHANTED_BOOK); + ItemMeta m = item.getItemMeta(); + if (m != null) { + m.setDisplayName(Utils.color("&b&lPostfach &8– &7" + owner)); + m.setLore(List.of( + Utils.color("&7Ungelesen&8: &e" + unread), + Utils.color("&7Seite &e" + page + " &7/ &e" + total) + )); + item.setItemMeta(m); + } + return item; + } + + private static ItemStack buildMessageListItem(String[] parts) { + // parts: [id, from, iso, read, text] + boolean read = "true".equals(parts[3]); + ItemStack item = new ItemStack(read ? Material.BOOK : Material.WRITTEN_BOOK); + ItemMeta m = item.getItemMeta(); + if (m != null) { + m.setDisplayName(Utils.color((read ? "&7" : "&e&l") + "Von: " + parts[1])); + String preview = parts[4].length() > 38 ? parts[4].substring(0, 38) + "…" : parts[4]; + m.setLore(List.of( + Utils.color("&7" + Utils.prettifyIso(parts[2])), + Utils.color("&f" + preview), + "", + Utils.color(read ? "&8Gelesen" : "&eUngelesen – Klicken zum Lesen") + )); + item.setItemMeta(m); + } + return item; + } + + private static ItemStack buildFullMessageItem(String from, String date, String text) { + ItemStack item = new ItemStack(Material.PAPER); + ItemMeta m = item.getItemMeta(); + if (m != null) { + m.setDisplayName(Utils.color("&bVon: &e" + from + " &8| &7" + date)); + List lore = new ArrayList<>(); + lore.add(""); + for (String line : wrapText(text, 40)) lore.add(Utils.color("&f" + line)); + m.setLore(lore); + item.setItemMeta(m); + } + return item; + } + + // ── Generic helpers ─────────────────────────────────────────────── + + private static ItemStack filler(Material mat) { + ItemStack item = new ItemStack(mat); + ItemMeta m = item.getItemMeta(); + if (m != null) { m.setDisplayName(" "); item.setItemMeta(m); } + return item; + } + + private static ItemStack navItem(Material mat, String name, List lore) { + ItemStack item = new ItemStack(mat); + ItemMeta m = item.getItemMeta(); + if (m != null) { m.setDisplayName(name); m.setLore(lore); item.setItemMeta(m); } + return item; + } + + /** Word-wraps {@code text} at {@code maxChars} characters per line. */ + private static List wrapText(String text, int maxChars) { + List lines = new ArrayList<>(); + String[] words = text.split(" "); + StringBuilder cur = new StringBuilder(); + for (String w : words) { + if (cur.length() > 0 && cur.length() + 1 + w.length() > maxChars) { + lines.add(cur.toString()); + cur = new StringBuilder(w); + } else { + if (cur.length() > 0) cur.append(' '); + cur.append(w); + } + } + if (cur.length() > 0) lines.add(cur.toString()); + return lines.isEmpty() ? List.of(text) : lines; + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/gui/TeamGUI.java b/src/main/java/me/viper/teamplugin/gui/TeamGUI.java index 698d7f9..2eee4f4 100644 --- a/src/main/java/me/viper/teamplugin/gui/TeamGUI.java +++ b/src/main/java/me/viper/teamplugin/gui/TeamGUI.java @@ -3,6 +3,8 @@ package me.viper.teamplugin.gui; import me.viper.teamplugin.Main; import me.viper.teamplugin.manager.DataManager; import me.viper.teamplugin.manager.LangManager; +import me.viper.teamplugin.manager.MessageManager; +import me.viper.teamplugin.util.SkinResolver; import me.viper.teamplugin.util.Utils; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -13,106 +15,462 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; -import java.util.ArrayList; -import java.util.List; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; +/** + * TeamGUI – two-layer navigation with optional pagination on rank pages. + * + * Layer 1 Overview – one configurable icon per rank. + * Slot read from rank-settings..slot (config.yml). + * Clicking opens the rank's member page. + * + * Layer 2 Rank Page – shows player heads. + * If members exceed capacity → pagination. + * Footer layout (slots 45-53): + * 45 ← prev rank (wraps) + * 46 ← prev page (hidden if page 0) + * 49 home + * 52 → next page (hidden if last page) + * 53 → next rank (wraps) + * + * Page capacity: + * rank-settings..member_slots defined → capacity = list size + * otherwise → gui.members_per_page (default 40) + * + * GUI size is always 54 and NOT configurable. + */ public class TeamGUI { - // Zeilenpositionen für Ränge - private static final int[] rows = {1, 2, 3, 4}; + private static final int GUI_SIZE = 54; - public static void openTeamGUI(Player player) { - FileConfiguration cfg = Main.getInstance().getConfig(); - FileConfiguration data = DataManager.getData(); + private static final int[] OVERVIEW_SLOTS_FALLBACK = {13, 22, 31, 40, 11, 15, 29, 33}; - int size = cfg.getInt("gui.size", 54); - String title = Utils.color(cfg.getString("gui.title", "&8» &bTeam Übersicht")); - Inventory inv = Bukkit.createInventory(null, size, title); + public static final int NAV_PREV = 45; + public static final int NAV_PREV_PAGE = 46; + public static final int NAV_HOME = 49; + public static final int NAV_NEXT_PAGE = 52; + public static final int NAV_NEXT = 53; - // Hintergrund-Glas setzen - Material bgMat = Material.valueOf(cfg.getString("gui.background", "GRAY_STAINED_GLASS_PANE")); - ItemStack filler = new ItemStack(bgMat); - ItemMeta fm = filler.getItemMeta(); - if (fm != null) { - fm.setDisplayName(" "); - filler.setItemMeta(fm); - } - for (int i = 0; i < inv.getSize(); i++) inv.setItem(i, filler); + /** UUID → current page on the rank page they have open */ + private static final Map RANK_PAGE = new HashMap<>(); - List ranks = cfg.getStringList("ranks"); - for (int i = 0; i < ranks.size() && i < rows.length; i++) { + // ───────────────────────────────────────────────────────────────── + // PUBLIC ENTRY POINTS + // ───────────────────────────────────────────────────────────────── + + public static void openTeamGUI(Player player) { openOverview(player); } + + // ───────────────────────────────────────────────────────────────── + // LAYER 1 – OVERVIEW + // ───────────────────────────────────────────────────────────────── + + public static void openOverview(Player player) { + FileConfiguration cfg = Main.getInstance().getConfig(); + List ranks = cfg.getStringList("ranks"); + + Inventory inv = Bukkit.createInventory(null, GUI_SIZE, Utils.color(getGuiTitle())); + fillBackground(inv, cfg.getString("gui.background", "GRAY_STAINED_GLASS_PANE")); + + for (int i = 0; i < ranks.size(); i++) { String rank = ranks.get(i); - List members = data.getStringList("Team." + rank); - if (members == null) members = new ArrayList<>(); + int slot = resolveOverviewSlot(cfg, rank, i); + if (slot < 0 || slot >= GUI_SIZE) continue; + inv.setItem(slot, createRankOverviewBlock(rank)); + } + player.openInventory(inv); + } - int rowStart = rows[i] * 9; - int count = Math.min(members.size(), 9); - if (count == 0) { - ItemStack empty = createInfoItem("§7Kein/e " + rank, List.of("§7Keine Mitglieder")); - inv.setItem(rowStart + 4, empty); - continue; + private static int resolveOverviewSlot(FileConfiguration cfg, String rank, int index) { + if (cfg.isInt("rank-settings." + rank + ".slot")) + return cfg.getInt("rank-settings." + rank + ".slot"); + if (index < OVERVIEW_SLOTS_FALLBACK.length) + return OVERVIEW_SLOTS_FALLBACK[index]; + return -1; + } + + private static ItemStack createRankOverviewBlock(String rank) { + FileConfiguration cfg = Main.getInstance().getConfig(); + String displayRaw = cfg.getString("rank-settings." + rank + ".display", rank); + String skullTexture = cfg.getString("rank-settings." + rank + ".skull_texture", ""); + int memberCount = DataManager.getData().getStringList("Team." + rank).size(); + + ItemStack item = skullTexture.isEmpty() + ? new ItemStack(parseMaterial(cfg.getString("rank-settings." + rank + ".material", "STONE"), Material.STONE)) + : buildCustomSkull(skullTexture); + + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(Utils.color(displayRaw)); + List lore = new ArrayList<>(); + lore.add(Utils.color("&7Mitglieder&8: &e" + memberCount)); + lore.add(""); + lore.add(Utils.color("&7\u25ba &fKlicke zum \u00d6ffnen")); + meta.setLore(lore); + item.setItemMeta(meta); + } + return item; + } + + // ───────────────────────────────────────────────────────────────── + // LAYER 2 – RANK PAGE (with pagination) + // ───────────────────────────────────────────────────────────────── + + public static void openRankPage(Player player, String rank) { + openRankPage(player, rank, 0); + } + + public static void openRankPage(Player player, String rank, int page) { + FileConfiguration cfg = Main.getInstance().getConfig(); + List ranks = cfg.getStringList("ranks"); + int rankIdx = ranks.indexOf(rank); + if (rankIdx < 0) return; + + int rankCount = ranks.size(); + + // All members for this rank + List allMembers = DataManager.getData().getStringList("Team." + rank); + + // Determine page capacity + List configuredSlots = cfg.getIntegerList("rank-settings." + rank + ".member_slots") + .stream().filter(s -> s >= 0 && s < 45).collect(Collectors.toList()); + int capacity = configuredSlots.isEmpty() + ? Math.min(cfg.getInt("gui.members_per_page", 40), 45) + : configuredSlots.size(); + + int totalPages = Math.max(1, (int) Math.ceil((double) allMembers.size() / capacity)); + page = Math.max(0, Math.min(page, totalPages - 1)); + RANK_PAGE.put(player.getUniqueId(), page); + + // Slice for this page + int start = page * capacity; + int end = Math.min(start + capacity, allMembers.size()); + List members = allMembers.subList(start, end); + + String glassStr = cfg.getString("rank-settings." + rank + ".glass", + cfg.getString("gui.background", "GRAY_STAINED_GLASS_PANE")); + + Inventory inv = Bukkit.createInventory(null, GUI_SIZE, Utils.color(getRankPageTitle(rank))); + fillBackground(inv, glassStr); + + // Dark footer row + ItemStack navFiller = createFiller(Material.BLACK_STAINED_GLASS_PANE); + for (int i = 45; i < 54; i++) inv.setItem(i, navFiller); + + // ── Rank navigation (always visible) ──────────────────────── + String prevRank = ranks.get((rankIdx - 1 + rankCount) % rankCount); + String prevDisplay = cfg.getString("rank-settings." + prevRank + ".display", prevRank); + inv.setItem(NAV_PREV, createNavItem(Material.ARROW, + Utils.color(Utils.replace(LangManager.get("nav_prev_label"), "%rank%", prevDisplay)), + List.of(Utils.color(LangManager.get("nav_prev_lore"))))); + + inv.setItem(NAV_HOME, createNavItem(Material.NETHER_STAR, + Utils.color(LangManager.get("nav_home_label")), + List.of(Utils.color(LangManager.get("nav_home_lore"))))); + + String nextRank = ranks.get((rankIdx + 1) % rankCount); + String nextDisplay = cfg.getString("rank-settings." + nextRank + ".display", nextRank); + inv.setItem(NAV_NEXT, createNavItem(Material.ARROW, + Utils.color(Utils.replace(LangManager.get("nav_next_label"), "%rank%", nextDisplay)), + List.of(Utils.color(LangManager.get("nav_next_lore"))))); + + // ── Page navigation (only if multiple pages) ───────────────── + if (totalPages > 1) { + if (page > 0) inv.setItem(NAV_PREV_PAGE, createNavItem(Material.SPECTRAL_ARROW, + Utils.color(LangManager.get("page_prev_label")), + List.of(Utils.color("&7Seite " + page + " / " + totalPages)))); + + // Page indicator in home slot lore (update lore only) + ItemStack homeItem = inv.getItem(NAV_HOME); + if (homeItem != null) { + ItemMeta hm = homeItem.getItemMeta(); + if (hm != null) { + hm.setLore(List.of( + Utils.color(LangManager.get("nav_home_lore")), + Utils.color("&8Seite &7" + (page + 1) + " &8/ &7" + totalPages) + )); + homeItem.setItemMeta(hm); + } } - int startOffset = (9 - count) / 2; - for (int j = 0; j < count; j++) { - String name = members.get(j); - int slot = rowStart + startOffset + j; - if (slot >= 0 && slot < inv.getSize()) { - inv.setItem(slot, createPlayerHead(name, rank)); - } + if (page + 1 < totalPages) inv.setItem(NAV_NEXT_PAGE, createNavItem(Material.SPECTRAL_ARROW, + Utils.color(LangManager.get("page_next_label")), + List.of(Utils.color("&7Seite " + (page + 2) + " / " + totalPages)))); + } + + // ── Member heads ────────────────────────────────────────────── + List memberSlots = resolveMemberSlots(cfg, rank, members.size()); + + if (allMembers.isEmpty()) { + inv.setItem(22, createInfoItem("\u00a77Keine Mitglieder", + List.of("\u00a77Dieser Rang hat keine Mitglieder."))); + } else { + for (int i = 0; i < members.size() && i < memberSlots.size(); i++) { + int slot = memberSlots.get(i); + String memberName = members.get(i); + + inv.setItem(slot, createPlayerHeadSync(memberName, rank)); + + final int finalSlot = slot; + SkinResolver.resolveAndUpdate(memberName, inv, finalSlot, + (uuid, updatedInv) -> { + if (!player.isOnline()) return; + if (!player.getOpenInventory().getTitle() + .equals(Utils.color(getRankPageTitle(rank)))) return; + updatedInv.setItem(finalSlot, + createPlayerHeadWithUUID(memberName, rank, uuid)); + }); } } player.openInventory(inv); } - private static ItemStack createPlayerHead(String name, String rank) { - FileConfiguration cfg = Main.getInstance().getConfig(); + private static List resolveMemberSlots(FileConfiguration cfg, String rank, int count) { + List configured = cfg.getIntegerList("rank-settings." + rank + ".member_slots") + .stream().filter(s -> s >= 0 && s < 45).collect(Collectors.toList()); + return (!configured.isEmpty()) ? configured : computeSlots(count); + } - ItemStack skull = new ItemStack(Material.PLAYER_HEAD); - SkullMeta meta = (SkullMeta) skull.getItemMeta(); - if (meta != null) { - OfflinePlayer off = Bukkit.getOfflinePlayer(name); - meta.setOwningPlayer(off); - - // Rank aus config - String rankDisplay = cfg.getString("rank-settings." + rank + ".display", rank); - String rankPrefix = cfg.getString("rank-settings." + rank + ".prefix", ""); - - // Name + Prefix - String displayName = (rankPrefix == null ? "" : rankPrefix + " ") + "&b" + name; - meta.setDisplayName(Utils.color(displayName.trim())); - - List lore = new ArrayList<>(); - - // Rang - String rankLine = Utils.replace(LangManager.get("tooltip_rank"), "%rank%", rankDisplay); - lore.add(Utils.color(rankLine)); - - // Online-/Offline-Status aus config - String statusOnline = cfg.getString("status.online", "&a🟢 Online"); - String statusOffline = cfg.getString("status.offline", "&c🔴 Offline"); - boolean isOnline = off.isOnline(); - String statusLine = isOnline ? statusOnline : statusOffline; - lore.add(Utils.color("&7Status: " + statusLine)); - - // Join-Datum, falls vorhanden - String iso = DataManager.getData().getString("JoinDates." + name, ""); - if (iso != null && !iso.isEmpty()) { - String joinLine = Utils.replace(LangManager.get("tooltip_joined"), "%joindate%", Utils.prettifyIso(iso)); - lore.add(Utils.color(joinLine)); + private static List computeSlots(int count) { + List result = new ArrayList<>(); + if (count == 0) return result; + final int MAX = 45, COLS = 9, ROWS = 5; + int total = Math.min(count, MAX); + int perRow = Math.min((int) Math.ceil((double) total / ROWS), COLS); + int placed = 0; + for (int row = 0; row < ROWS && placed < total; row++) { + int inRow = Math.min(perRow, total - placed); + int offset = (COLS - inRow) / 2; + for (int col = 0; col < inRow && placed < total; col++) { + result.add(row * COLS + offset + col); + placed++; } + } + return result; + } - meta.setLore(lore); - skull.setItemMeta(meta); + // ───────────────────────────────────────────────────────────────── + // CLICK HANDLERS + // ───────────────────────────────────────────────────────────────── + + public static void handleOverviewClick(Player player, int slot) { + FileConfiguration cfg = Main.getInstance().getConfig(); + List ranks = cfg.getStringList("ranks"); + for (int i = 0; i < ranks.size(); i++) { + if (slot == resolveOverviewSlot(cfg, ranks.get(i), i)) { + openRankPage(player, ranks.get(i)); + return; + } + } + } + + public static void handleRankPageClick(Player player, int slot, String currentRank) { + List ranks = Main.getInstance().getConfig().getStringList("ranks"); + int idx = ranks.indexOf(currentRank); + int size = ranks.size(); + if (idx < 0 || size == 0) return; + + int page = RANK_PAGE.getOrDefault(player.getUniqueId(), 0); + + switch (slot) { + case NAV_HOME -> openOverview(player); + case NAV_PREV -> openRankPage(player, ranks.get((idx - 1 + size) % size)); + case NAV_NEXT -> openRankPage(player, ranks.get((idx + 1) % size)); + case NAV_PREV_PAGE -> openRankPage(player, currentRank, page - 1); + case NAV_NEXT_PAGE -> openRankPage(player, currentRank, page + 1); + default -> { + // Member head click → message flow + String targetName = getMemberAtSlot(currentRank, slot, page); + if (targetName != null && !targetName.equalsIgnoreCase(player.getName())) { + player.closeInventory(); + MessageManager.startInput(player, targetName); + } + } + } + } + + // ───────────────────────────────────────────────────────────────── + // HEAD → MEMBER LOOKUP + // ───────────────────────────────────────────────────────────────── + + /** + * Returns the member name at the given slot on a rank page (accounting for page offset), + * or null if the slot holds no member head. + */ + public static String getMemberAtSlot(String rank, int slot, int page) { + FileConfiguration cfg = Main.getInstance().getConfig(); + List allMembers = DataManager.getData().getStringList("Team." + rank); + + List configuredSlots = cfg.getIntegerList("rank-settings." + rank + ".member_slots") + .stream().filter(s -> s >= 0 && s < 45).collect(Collectors.toList()); + int capacity = configuredSlots.isEmpty() + ? Math.min(cfg.getInt("gui.members_per_page", 40), 45) + : configuredSlots.size(); + + int start = page * capacity; + int end = Math.min(start + capacity, allMembers.size()); + List pageMembers = allMembers.subList(start, end); + List memberSlots = resolveMemberSlots(cfg, rank, pageMembers.size()); + + for (int i = 0; i < pageMembers.size() && i < memberSlots.size(); i++) { + if (memberSlots.get(i) == slot) return pageMembers.get(i); + } + return null; + } + + // ───────────────────────────────────────────────────────────────── + // TITLE HELPERS + // ───────────────────────────────────────────────────────────────── + + public static String getGuiTitle() { + return Main.getInstance().getConfig().getString("gui.title", "&8\u00bb &bTeam \u00dcbersicht"); + } + + public static String getRankPageTitle(String rank) { + FileConfiguration cfg = Main.getInstance().getConfig(); + String display = cfg.getString("rank-settings." + rank + ".display", rank); + String prefix = cfg.getString("gui.rank_page_title", "&8\u00bb &bTeam &7| "); + return prefix + display; + } + + public static boolean isOverviewTitle(String coloredTitle) { + return coloredTitle.equals(Utils.color(getGuiTitle())); + } + + public static String extractRankFromTitle(String coloredTitle) { + for (String rank : Main.getInstance().getConfig().getStringList("ranks")) { + if (coloredTitle.equals(Utils.color(getRankPageTitle(rank)))) return rank; + } + return null; + } + + // ───────────────────────────────────────────────────────────────── + // HEAD FACTORIES + // ───────────────────────────────────────────────────────────────── + + private static ItemStack createPlayerHeadSync(String name, String rank) { + ItemStack skull = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) skull.getItemMeta(); + if (meta == null) return skull; + @SuppressWarnings("deprecation") + OfflinePlayer off = Bukkit.getOfflinePlayer(name); + meta.setOwningPlayer(off); + applyHeadMeta(meta, name, rank); + skull.setItemMeta(meta); + return skull; + } + + private static ItemStack createPlayerHeadWithUUID(String name, String rank, UUID uuid) { + ItemStack skull = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) skull.getItemMeta(); + if (meta == null) return skull; + OfflinePlayer off = (uuid != null) ? Bukkit.getOfflinePlayer(uuid) + : Bukkit.getOfflinePlayer(name); //noinspection deprecation + meta.setOwningPlayer(off); + applyHeadMeta(meta, name, rank); + skull.setItemMeta(meta); + return skull; + } + + private static void applyHeadMeta(SkullMeta meta, String name, String rank) { + FileConfiguration cfg = Main.getInstance().getConfig(); + String rankPrefix = cfg.getString("rank-settings." + rank + ".prefix", ""); + String displayName = rankPrefix.isEmpty() ? "&b" + name : rankPrefix + " &b" + name; + meta.setDisplayName(Utils.color(displayName.trim())); + + List lore = new ArrayList<>(); + String rankDisplay = cfg.getString("rank-settings." + rank + ".display", rank); + lore.add(Utils.color(Utils.replace(LangManager.get("tooltip_rank"), "%rank%", rankDisplay))); + + String statusOnline = cfg.getString("status.online", "&a\uD83D\uDFE2 Online"); + String statusOffline = cfg.getString("status.offline", "&c\uD83D\uDD34 Offline"); + @SuppressWarnings("deprecation") + boolean online = Bukkit.getOfflinePlayer(name).isOnline(); + lore.add(Utils.color("&7Status&8: " + (online ? statusOnline : statusOffline))); + + String iso = DataManager.getData().getString("JoinDates." + name, ""); + if (iso != null && !iso.isEmpty()) + lore.add(Utils.color(Utils.replace(LangManager.get("tooltip_joined"), + "%joindate%", Utils.prettifyIso(iso)))); + + lore.add(""); + lore.add(Utils.color(LangManager.get("msg_head_hint"))); + meta.setLore(lore); + } + + // ───────────────────────────────────────────────────────────────── + // CUSTOM SKULL + // ───────────────────────────────────────────────────────────────── + + private static ItemStack buildCustomSkull(String texture) { + ItemStack skull = new ItemStack(Material.PLAYER_HEAD); + if (texture == null || texture.isEmpty()) return skull; + try { + String url = resolveTextureUrl(texture); + if (url == null) { + Main.getInstance().getLogger().warning("[TeamGUI] Cannot resolve texture: " + texture); + return skull; + } + PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID(), "CustomSkull"); + PlayerTextures textures = profile.getTextures(); + textures.setSkin(new java.net.URL(url)); + profile.setTextures(textures); + SkullMeta meta = (SkullMeta) skull.getItemMeta(); + if (meta != null) { meta.setOwnerProfile(profile); skull.setItemMeta(meta); } + } catch (Exception e) { + Main.getInstance().getLogger().warning("[TeamGUI] Skull error: " + e.getMessage()); } return skull; } + private static String resolveTextureUrl(String texture) { + if (texture == null || texture.isEmpty()) return null; + if (texture.startsWith("http://") || texture.startsWith("https://")) return texture; + if (texture.matches("[0-9a-fA-F]{64}")) return "http://textures.minecraft.net/texture/" + texture; + if (texture.startsWith("eyJ")) { + try { + String json = new String(Base64.getDecoder().decode(texture), StandardCharsets.UTF_8); + int s = json.indexOf("\"url\":\""); + if (s < 0) return null; + s += 7; + int e = json.indexOf('"', s); + return (e > s) ? json.substring(s, e) : null; + } catch (IllegalArgumentException ignored) {} + } + return null; + } + + // ───────────────────────────────────────────────────────────────── + // BACKGROUND / GENERIC HELPERS + // ───────────────────────────────────────────────────────────────── + + private static void fillBackground(Inventory inv, String matStr) { + ItemStack filler = createFiller(parseMaterial(matStr, Material.GRAY_STAINED_GLASS_PANE)); + for (int i = 0; i < inv.getSize(); i++) inv.setItem(i, filler); + } + + private static ItemStack createFiller(Material mat) { + ItemStack item = new ItemStack(mat); + ItemMeta m = item.getItemMeta(); + if (m != null) { m.setDisplayName(" "); item.setItemMeta(m); } + return item; + } + + private static ItemStack createNavItem(Material mat, String name, List lore) { + ItemStack item = new ItemStack(mat); + ItemMeta m = item.getItemMeta(); + if (m != null) { m.setDisplayName(name); m.setLore(lore); item.setItemMeta(m); } + return item; + } + private static ItemStack createInfoItem(String name, List lore) { ItemStack item = new ItemStack(Material.PAPER); - ItemMeta m = item.getItemMeta(); + ItemMeta m = item.getItemMeta(); if (m != null) { m.setDisplayName(Utils.color(name)); m.setLore(lore.stream().map(Utils::color).toList()); @@ -121,7 +479,7 @@ public class TeamGUI { return item; } - public static String getGuiTitle() { - return Main.getInstance().getConfig().getString("gui.title", "&8» &bTeam Übersicht"); + private static Material parseMaterial(String s, Material fallback) { + try { return Material.valueOf(s); } catch (Exception e) { return fallback; } } -} +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/listener/ChatListener.java b/src/main/java/me/viper/teamplugin/listener/ChatListener.java new file mode 100644 index 0000000..ec4f817 --- /dev/null +++ b/src/main/java/me/viper/teamplugin/listener/ChatListener.java @@ -0,0 +1,49 @@ +package me.viper.teamplugin.listener; + +import me.viper.teamplugin.manager.MessageManager; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class ChatListener implements Listener { + + /** + * Intercepts chat messages from players who are in "awaiting message input" mode + * (mailbox reply / direct message flow). + * + * Note: AsyncPlayerChatEvent is deprecated in newer Paper versions in favour of + * AsyncChatEvent (adventure). If you use Paper 1.19+, swap the import and handler + * signature accordingly. + */ + @EventHandler(priority = EventPriority.HIGH) + public void onChat(AsyncPlayerChatEvent e) { + Player player = e.getPlayer(); + + if (!MessageManager.isAwaitingInput(player.getUniqueId())) return; + + // Cancel the public chat message and handle privately + e.setCancelled(true); + + // MessageManager.handleInput must run on the main thread (Bukkit API) + org.bukkit.Bukkit.getScheduler().runTask( + me.viper.teamplugin.Main.getInstance(), + () -> MessageManager.handleInput(player, e.getMessage()) + ); + } + + /** Deliver any stored offline messages when a team member logs in. */ + @EventHandler + public void onJoin(PlayerJoinEvent e) { + MessageManager.deliverPending(e.getPlayer()); + } + + /** Clean up if a sender disconnects while in input mode. */ + @EventHandler + public void onQuit(PlayerQuitEvent e) { + MessageManager.cancelInput(e.getPlayer().getUniqueId()); + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/listener/InventoryListener.java b/src/main/java/me/viper/teamplugin/listener/InventoryListener.java index 7f669ea..6696918 100644 --- a/src/main/java/me/viper/teamplugin/listener/InventoryListener.java +++ b/src/main/java/me/viper/teamplugin/listener/InventoryListener.java @@ -1,10 +1,12 @@ package me.viper.teamplugin.listener; import me.viper.teamplugin.gui.BackupGUI; +import me.viper.teamplugin.gui.ApplicationGUI; +import me.viper.teamplugin.gui.MailboxGUI; import me.viper.teamplugin.gui.SettingsGUI; import me.viper.teamplugin.gui.TeamGUI; -import me.viper.teamplugin.manager.LangManager; import me.viper.teamplugin.util.Utils; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.inventory.InventoryClickEvent; @@ -14,40 +16,82 @@ public class InventoryListener implements Listener { @EventHandler public void onInventoryClick(InventoryClickEvent e) { + if (!(e.getWhoClicked() instanceof Player player)) return; String title = e.getView().getTitle(); - String teamTitle = Utils.color(TeamGUI.getGuiTitle()); - String settingsTitle = Utils.color(SettingsGUI.getGuiTitle()); - String backupTitle = Utils.color(BackupGUI.getGuiTitle()); - - if (title.equals(teamTitle)) { + // ── Team Overview GUI ─────────────────────────────────────── + if (TeamGUI.isOverviewTitle(title)) { e.setCancelled(true); + if (isEmptyClick(e)) return; + TeamGUI.handleOverviewClick(player, e.getRawSlot()); return; } - if (title.equals(settingsTitle)) { + // ── Rank Member Page ──────────────────────────────────────── + String rank = TeamGUI.extractRankFromTitle(title); + if (rank != null) { + e.setCancelled(true); + if (isEmptyClick(e)) return; + TeamGUI.handleRankPageClick(player, e.getRawSlot(), rank); + return; + } + + // ── Settings GUI ──────────────────────────────────────────── + if (title.equals(Utils.color(SettingsGUI.getGuiTitle()))) { e.setCancelled(true); SettingsGUI.handleClick(e); return; } - if (title.equals(backupTitle)) { + // ── Backup GUI ────────────────────────────────────────────── + if (title.equals(Utils.color(BackupGUI.getGuiTitle()))) { e.setCancelled(true); BackupGUI.handleClick(e); return; } + + // ── Mailbox GUIs ───────────────────────────────────────────── + if (MailboxGUI.isListTitle(title)) { + e.setCancelled(true); + MailboxGUI.handleListClick(player, e); + return; + } + if (MailboxGUI.isReadTitle(title)) { + e.setCancelled(true); + MailboxGUI.handleReadClick(player, e); + return; + } + + // ── Application GUIs ───────────────────────────────────────── + if (ApplicationGUI.isSelectionTitle(title)) { + e.setCancelled(true); + ApplicationGUI.handleSelectionClick(player, e); + return; + } + if (ApplicationGUI.isDetailTitle(title)) { + e.setCancelled(true); + ApplicationGUI.handleDetailClick(player, e); + } } @EventHandler public void onInventoryDrag(InventoryDragEvent e) { String title = e.getView().getTitle(); - - String teamTitle = Utils.color(TeamGUI.getGuiTitle()); - String settingsTitle = Utils.color(SettingsGUI.getGuiTitle()); - String backupTitle = Utils.color(BackupGUI.getGuiTitle()); - - if (title.equals(teamTitle) || title.equals(settingsTitle) || title.equals(backupTitle)) { + if (TeamGUI.isOverviewTitle(title) + || TeamGUI.extractRankFromTitle(title) != null + || title.equals(Utils.color(SettingsGUI.getGuiTitle())) + || title.equals(Utils.color(BackupGUI.getGuiTitle())) + || MailboxGUI.isListTitle(title) + || MailboxGUI.isReadTitle(title) + || ApplicationGUI.isSelectionTitle(title) + || ApplicationGUI.isDetailTitle(title)) { e.setCancelled(true); } } -} + + // ──────────────────────────────────────────────────────────────── + + private boolean isEmptyClick(InventoryClickEvent e) { + return e.getCurrentItem() == null || e.getCurrentItem().getType().isAir(); + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/manager/ApplicationManager.java b/src/main/java/me/viper/teamplugin/manager/ApplicationManager.java new file mode 100644 index 0000000..85fbcc0 --- /dev/null +++ b/src/main/java/me/viper/teamplugin/manager/ApplicationManager.java @@ -0,0 +1,118 @@ +package me.viper.teamplugin.manager; + +import me.viper.teamplugin.Main; +import me.viper.teamplugin.util.Utils; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Manages team-rank applications stored in data.yml under "Applications". + * + * Entry format per rank list: "playerName|isoTimestamp|reason" + */ +public class ApplicationManager { + + // ── Submit ──────────────────────────────────────────────────────── + + /** + * Submits an application for {@code applicant} to join {@code rank}. + * Returns false if the player has already applied to any rank. + */ + public static boolean apply(Player applicant, String rank, String reason) { + // Duplicate check across all ranks + if (findApplication(applicant.getName()) != null) { + applicant.sendMessage(Utils.color(LangManager.get("apply_already"))); + return false; + } + + FileConfiguration data = DataManager.getData(); + String key = "Applications." + rank; + List list = new ArrayList<>(data.getStringList(key)); + list.add(applicant.getName() + "|" + Utils.formatIsoNow() + "|" + (reason.isEmpty() ? "-" : reason)); + data.set(key, list); + DataManager.save(); + + applicant.sendMessage(Utils.color( + Utils.replace(LangManager.get("apply_sent"), "%rank%", rank))); + + // Notify online admins + if (Main.getInstance().getConfig().getBoolean("apply.notify_admins", true)) { + String displayRank = Main.getInstance().getConfig() + .getString("rank-settings." + rank + ".display", rank); + String notify = Utils.color(Utils.replace( + LangManager.get("apply_admin_notify"), + "%player%", applicant.getName(), + "%rank%", displayRank)); + for (Player admin : Bukkit.getOnlinePlayers()) { + if (admin.hasPermission("teamplugin.admin")) admin.sendMessage(notify); + } + } + return true; + } + + // ── Query ───────────────────────────────────────────────────────── + + /** Returns parsed applications for a single rank: each entry is [name, iso, reason]. */ + public static List getApplications(String rank) { + return DataManager.getData().getStringList("Applications." + rank) + .stream() + .map(s -> s.split("\\|", 3)) + .filter(a -> a.length == 3) + .collect(Collectors.toList()); + } + + /** Returns all applications across all ranks: each entry is [rank, name, iso, reason]. */ + public static List getAllApplications() { + List all = new ArrayList<>(); + for (String rank : Main.getInstance().getConfig().getStringList("ranks")) { + for (String[] app : getApplications(rank)) { + all.add(new String[]{rank, app[0], app[1], app[2]}); + } + } + return all; + } + + /** Returns all player names that have a pending application. */ + public static List getApplicantNames() { + return getAllApplications().stream() + .map(a -> a[1]) + .collect(Collectors.toList()); + } + + /** Returns the rank a player has applied for, or null if not found. */ + public static String findApplication(String playerName) { + for (String rank : Main.getInstance().getConfig().getStringList("ranks")) { + boolean found = DataManager.getData() + .getStringList("Applications." + rank) + .stream() + .anyMatch(e -> e.split("\\|", 3)[0].equalsIgnoreCase(playerName)); + if (found) return rank; + } + return null; + } + + // ── Remove ──────────────────────────────────────────────────────── + + /** Removes the application for playerName from all ranks. Returns true if one was found. */ + public static boolean removeApplication(String playerName) { + FileConfiguration data = DataManager.getData(); + boolean removed = false; + for (String rank : Main.getInstance().getConfig().getStringList("ranks")) { + String key = "Applications." + rank; + List list = data.getStringList(key); + boolean changed = list.removeIf( + e -> e.split("\\|", 3)[0].equalsIgnoreCase(playerName)); + if (changed) { + data.set(key, list.isEmpty() ? null : list); + removed = true; + } + } + if (removed) DataManager.save(); + return removed; + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/manager/AuditLog.java b/src/main/java/me/viper/teamplugin/manager/AuditLog.java new file mode 100644 index 0000000..ba869c0 --- /dev/null +++ b/src/main/java/me/viper/teamplugin/manager/AuditLog.java @@ -0,0 +1,85 @@ +package me.viper.teamplugin.manager; + +import me.viper.teamplugin.Main; +import me.viper.teamplugin.util.Utils; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Writes and reads audit entries in log.yml. + * + * Entry format: + * 2026-03-08T12:00:00Z | ADD | by:AdminName | Steve → Owner + * + * Supported action strings (use the constants below for consistency): + * ADD, REMOVE, MOVE, APPLY_ACCEPT, APPLY_DENY + */ +public class AuditLog { + + public static final String ADD = "ADD"; + public static final String REMOVE = "REMOVE"; + public static final String MOVE = "MOVE"; + public static final String APPLY_ACCEPT = "APPLY_ACCEPT"; + public static final String APPLY_DENY = "APPLY_DENY"; + + private static File file; + private static FileConfiguration cfg; + + // ── File management ─────────────────────────────────────────────── + + private static void ensureLoaded() { + if (cfg != null) return; + file = new File(Main.getInstance().getDataFolder(), "log.yml"); + try { if (!file.exists()) file.createNewFile(); } + catch (IOException e) { e.printStackTrace(); } + cfg = YamlConfiguration.loadConfiguration(file); + } + + private static void save() { + try { if (cfg != null && file != null) cfg.save(file); } + catch (IOException e) { e.printStackTrace(); } + } + + public static void reload() { cfg = null; } + + // ── API ─────────────────────────────────────────────────────────── + + /** + * Appends an audit entry. + * + * @param action one of the constants above, e.g. AuditLog.ADD + * @param performedBy name of the command sender (player or "CONSOLE") + * @param detail human-readable summary, e.g. "Steve → Owner" + */ + public static void log(String action, String performedBy, String detail) { + ensureLoaded(); + String entry = Utils.formatIsoNow() + + " | " + action + + " | by:" + performedBy + + " | " + detail; + + List entries = new ArrayList<>(cfg.getStringList("entries")); + entries.add(entry); + + int max = Main.getInstance().getConfig().getInt("audit.keep", 500); + if (entries.size() > max) entries = entries.subList(entries.size() - max, entries.size()); + + cfg.set("entries", entries); + save(); + } + + /** + * Returns the last {@code limit} entries, most recent last. + */ + public static List getEntries(int limit) { + ensureLoaded(); + List all = cfg.getStringList("entries"); + int from = Math.max(0, all.size() - limit); + return new ArrayList<>(all.subList(from, all.size())); + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/manager/LangManager.java b/src/main/java/me/viper/teamplugin/manager/LangManager.java index 33efac4..77c8c60 100644 --- a/src/main/java/me/viper/teamplugin/manager/LangManager.java +++ b/src/main/java/me/viper/teamplugin/manager/LangManager.java @@ -9,20 +9,34 @@ import java.io.IOException; import java.util.List; public class LangManager { - private static File file; + + private static File file; private static FileConfiguration cfg; + private static String loadedLanguage; + + // ── Setup ───────────────────────────────────────────────────────── public static void setup() { - file = new File(Main.getInstance().getDataFolder(), "lang.yml"); - if (!file.exists()) { - Main.getInstance().saveResource("lang.yml", false); - } - cfg = YamlConfiguration.loadConfiguration(file); + String lang = Main.getInstance().getConfig().getString("language", "de").toLowerCase(); + String fileName = "lang_" + lang + ".yml"; + + // Both lang files are saved by Main.java on startup. + // Here we just load the correct one. + File target = new File(Main.getInstance().getDataFolder(), fileName); + file = target; + cfg = YamlConfiguration.loadConfiguration(file); + loadedLanguage = lang; + + Main.getInstance().getLogger().info( + "[LangManager] Loaded language: " + lang + " (" + fileName + ")"); } + // ── Getters ─────────────────────────────────────────────────────── + public static String get(String path) { if (cfg == null) setup(); - return cfg.getString(path, "Missing:" + path).replace("%prefix%", cfg.getString("prefix", "")); + String raw = cfg.getString(path, "Missing:" + path); + return raw.replace("%prefix%", cfg.getString("prefix", "")); } public static List getList(String path) { @@ -30,11 +44,23 @@ public class LangManager { return cfg.getStringList(path); } - public static void save() { - try { - if (cfg != null && file != null) cfg.save(file); - } catch (IOException e) { - e.printStackTrace(); - } + /** Returns the currently active language code ("de" or "en"). */ + public static String getLanguage() { + return loadedLanguage != null ? loadedLanguage : "de"; } -} + + /** + * Returns the configured subcommand name for a given key, e.g. + * getCmd("cmd_add") → "hinzufügen" (DE) or "add" (EN). + */ + public static String getCmd(String key) { + return get(key); + } + + // ── Save ────────────────────────────────────────────────────────── + + public static void save() { + try { if (cfg != null && file != null) cfg.save(file); } + catch (IOException e) { e.printStackTrace(); } + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/manager/MailboxManager.java b/src/main/java/me/viper/teamplugin/manager/MailboxManager.java new file mode 100644 index 0000000..087c3dd --- /dev/null +++ b/src/main/java/me/viper/teamplugin/manager/MailboxManager.java @@ -0,0 +1,144 @@ +package me.viper.teamplugin.manager; + +import me.viper.teamplugin.Main; +import me.viper.teamplugin.util.Utils; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Manages mailbox.yml – per-player inbox and message history. + * + * Entry format (pipe-separated, limit 5 so text may contain '|'): + * id|fromName|isoTimestamp|readBool|messageText + * + * Sections in mailbox.yml: + * Mailbox. – current inbox messages + * History. – all sent/received messages (last mail.history_max) + */ +public class MailboxManager { + + private static File file; + private static FileConfiguration cfg; + + // ── File management ─────────────────────────────────────────────── + + private static void ensureLoaded() { + if (cfg != null) return; + file = new File(Main.getInstance().getDataFolder(), "mailbox.yml"); + try { if (!file.exists()) file.createNewFile(); } + catch (IOException e) { e.printStackTrace(); } + cfg = YamlConfiguration.loadConfiguration(file); + } + + private static void save() { + try { if (cfg != null && file != null) cfg.save(file); } + catch (IOException e) { e.printStackTrace(); } + } + + public static void reload() { cfg = null; } + + // ── Store ───────────────────────────────────────────────────────── + + /** + * Stores a new message in the recipient's inbox and updates history for + * both parties. Returns the generated message ID. + */ + public static String store(String recipient, String fromName, String text) { + ensureLoaded(); + String id = String.valueOf(System.currentTimeMillis()); + String iso = Utils.formatIsoNow(); + String entry = id + "|" + fromName + "|" + iso + "|false|" + text; + + // Recipient inbox + List inbox = cfg.getStringList("Mailbox." + recipient); + inbox.add(entry); + cfg.set("Mailbox." + recipient, inbox); + + // History: recipient sees received message as unread + addToHistory(recipient, entry); + // History: sender sees their own message as already read (true) + addToHistory(fromName, id + "|" + fromName + "|" + iso + "|true|" + text); + + save(); + return id; + } + + // ── Read ────────────────────────────────────────────────────────── + + /** Returns parsed message arrays for a player's inbox, newest first. */ + public static List getParsedMessages(String player) { + ensureLoaded(); + List result = cfg.getStringList("Mailbox." + player).stream() + .map(s -> s.split("\\|", 5)) + .filter(a -> a.length == 5) + .collect(Collectors.toList()); + // Newest first (highest id = highest timestamp) + result.sort((a, b) -> Long.compare( + parseLong(b[0]), parseLong(a[0]))); + return result; + } + + public static int getUnreadCount(String player) { + ensureLoaded(); + return (int) getParsedMessages(player).stream() + .filter(a -> "false".equals(a[3])) + .count(); + } + + // ── Mutations ───────────────────────────────────────────────────── + + public static void markRead(String player, String msgId) { + ensureLoaded(); + List updated = cfg.getStringList("Mailbox." + player).stream() + .map(s -> { + String[] p = s.split("\\|", 5); + return (p.length == 5 && p[0].equals(msgId)) + ? p[0] + "|" + p[1] + "|" + p[2] + "|true|" + p[4] + : s; + }) + .collect(Collectors.toList()); + cfg.set("Mailbox." + player, updated); + save(); + } + + public static void delete(String player, String msgId) { + ensureLoaded(); + List remaining = cfg.getStringList("Mailbox." + player).stream() + .filter(s -> !s.startsWith(msgId + "|")) + .collect(Collectors.toList()); + cfg.set("Mailbox." + player, remaining.isEmpty() ? null : remaining); + save(); + } + + // ── History ─────────────────────────────────────────────────────── + + private static void addToHistory(String player, String entry) { + int max = Main.getInstance().getConfig().getInt("mail.history_max", 50); + List hist = new ArrayList<>(cfg.getStringList("History." + player)); + hist.add(entry); + if (hist.size() > max) hist = hist.subList(hist.size() - max, hist.size()); + cfg.set("History." + player, hist); + } + + public static List getHistory(String player) { + ensureLoaded(); + List result = cfg.getStringList("History." + player).stream() + .map(s -> s.split("\\|", 5)) + .filter(a -> a.length == 5) + .collect(Collectors.toList()); + result.sort((a, b) -> Long.compare(parseLong(b[0]), parseLong(a[0]))); + return result; + } + + // ── Helper ──────────────────────────────────────────────────────── + + private static long parseLong(String s) { + try { return Long.parseLong(s); } catch (NumberFormatException e) { return 0L; } + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/manager/MessageManager.java b/src/main/java/me/viper/teamplugin/manager/MessageManager.java new file mode 100644 index 0000000..b2bddb1 --- /dev/null +++ b/src/main/java/me/viper/teamplugin/manager/MessageManager.java @@ -0,0 +1,81 @@ +package me.viper.teamplugin.manager; + +import me.viper.teamplugin.util.Utils; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class MessageManager { + + private static final Map PENDING_INPUT = new HashMap<>(); + private static final Map PENDING_REPLY = new HashMap<>(); + + public static void startInput(Player sender, String targetName) { + PENDING_INPUT.put(sender.getUniqueId(), targetName); + sender.sendMessage(Utils.color( + Utils.replace(LangManager.get("msg_enter_message"), "%player%", targetName))); + sender.sendMessage(Utils.color(LangManager.get("msg_cancel_hint"))); + } + + public static void startReply(Player sender, String targetName) { + PENDING_REPLY.put(sender.getUniqueId(), targetName); + sender.sendMessage(Utils.color( + Utils.replace(LangManager.get("msg_reply_prompt"), "%player%", targetName))); + sender.sendMessage(Utils.color(LangManager.get("msg_cancel_hint"))); + } + + public static boolean isAwaitingInput(UUID uuid) { + return PENDING_INPUT.containsKey(uuid) || PENDING_REPLY.containsKey(uuid); + } + + public static boolean handleInput(Player sender, String text) { + String target = PENDING_INPUT.remove(sender.getUniqueId()); + if (target == null) target = PENDING_REPLY.remove(sender.getUniqueId()); + if (target == null) return false; + + if (text.equalsIgnoreCase(LangManager.get("msg_cancel_word"))) { + sender.sendMessage(Utils.color(LangManager.get("msg_cancelled"))); + return true; + } + deliver(sender, target, text); + return true; + } + + public static void cancelInput(UUID uuid) { + PENDING_INPUT.remove(uuid); + PENDING_REPLY.remove(uuid); + } + + private static void deliver(Player sender, String targetName, String text) { + MailboxManager.store(targetName, sender.getName(), text); + + Player target = Bukkit.getPlayerExact(targetName); + if (target != null && target.isOnline()) { + String formatted = Utils.color(Utils.replace( + LangManager.get("msg_format"), + "%sender%", sender.getName(), + "%message%", text)); + target.sendMessage(formatted); + sender.sendMessage(Utils.color( + Utils.replace(LangManager.get("msg_sent_online"), "%player%", targetName))); + } else { + sender.sendMessage(Utils.color( + Utils.replace(LangManager.get("msg_sent_offline"), "%player%", targetName))); + } + } + + public static void deliverPending(Player player) { + int unread = MailboxManager.getUnreadCount(player.getName()); + if (unread == 0) return; + Bukkit.getScheduler().runTaskLater( + me.viper.teamplugin.Main.getInstance(), () -> { + player.sendMessage(Utils.color(Utils.replace( + LangManager.get("msg_offline_header"), + "%count%", String.valueOf(unread)))); + player.sendMessage(Utils.color(LangManager.get("msg_mailbox_hint"))); + }, 20L); + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/util/SkinResolver.java b/src/main/java/me/viper/teamplugin/util/SkinResolver.java new file mode 100644 index 0000000..c562563 --- /dev/null +++ b/src/main/java/me/viper/teamplugin/util/SkinResolver.java @@ -0,0 +1,129 @@ +package me.viper.teamplugin.util; + +import me.viper.teamplugin.Main; +import org.bukkit.Bukkit; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.logging.Level; + +/** + * Resolves player UUIDs asynchronously via the Mojang API. + * + * Usage: + * SkinResolver.resolveAndUpdate(name, inv, slot, headBuilder); + * + * 1. The inventory slot is filled immediately with a placeholder head. + * 2. If the UUID is already cached the head is rebuilt on the same tick. + * 3. Otherwise a scheduler async task fetches the UUID from the Mojang API + * and updates the slot on the main thread once done. + */ +public class SkinResolver { + + /** In-memory UUID cache: lowercase name → UUID */ + private static final Map UUID_CACHE = new ConcurrentHashMap<>(); + + /** + * Resolves the UUID for `name`, then calls `callback` on the main thread + * with the UUID (or null on failure). + * The callback should rebuild the ItemStack and put it back in the slot. + * + * @param name Minecraft player name + * @param inv open inventory to update + * @param slot slot index in that inventory + * @param callback (UUID, Inventory) → void – called on the main thread + */ + public static void resolveAndUpdate(String name, + Inventory inv, + int slot, + BiConsumer callback) { + + String key = name.toLowerCase(); + + // Cache hit – update immediately on the current (main) thread + if (UUID_CACHE.containsKey(key)) { + callback.accept(UUID_CACHE.get(key), inv); + return; + } + + // Fall back to Mojang API (async) + Bukkit.getScheduler().runTaskAsynchronously(Main.getInstance(), () -> { + UUID uuid = fetchUUIDFromMojang(name); + if (uuid != null) UUID_CACHE.put(key, uuid); + + // Update slot on the main thread + Bukkit.getScheduler().runTask(Main.getInstance(), + () -> callback.accept(uuid, inv)); + }); + } + + /** + * Evicts `name` from the cache so the next call fetches a fresh UUID. + * Useful after a name-change is detected. + */ + public static void invalidate(String name) { + UUID_CACHE.remove(name.toLowerCase()); + } + + /** Clears the entire cache (e.g. on plugin reload). */ + public static void clearCache() { + UUID_CACHE.clear(); + } + + // ───────────────────────────────────────────────────────────────── + // Internal + // ───────────────────────────────────────────────────────────────── + + /** + * Blocking HTTP call to api.mojang.com – must be called from an async context. + * Returns null on any error or 404. + */ + private static UUID fetchUUIDFromMojang(String name) { + try { + URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + name); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(4000); + conn.setReadTimeout(4000); + conn.setRequestProperty("Accept", "application/json"); + + if (conn.getResponseCode() != 200) return null; + + String body = new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + // {"id":"<32-char-hex>","name":""} + String raw = extractJsonValue(body, "id"); + if (raw == null || raw.length() != 32) return null; + + // Insert dashes: 8-4-4-4-12 + String dashed = raw.replaceFirst( + "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", + "$1-$2-$3-$4-$5" + ); + return UUID.fromString(dashed); + + } catch (IOException | IllegalArgumentException e) { + Main.getInstance().getLogger().log(Level.WARNING, + "[SkinResolver] Could not fetch UUID for '" + name + "': " + e.getMessage()); + return null; + } + } + + /** Minimal JSON string value extractor (avoids requiring a JSON library). */ + private static String extractJsonValue(String json, String key) { + String search = "\"" + key + "\":\""; + int start = json.indexOf(search); + if (start < 0) return null; + start += search.length(); + int end = json.indexOf('"', start); + if (end < 0) return null; + return json.substring(start, end); + } +} \ No newline at end of file diff --git a/src/main/java/me/viper/teamplugin/util/Utils.java b/src/main/java/me/viper/teamplugin/util/Utils.java index cc3135e..9c45794 100644 --- a/src/main/java/me/viper/teamplugin/util/Utils.java +++ b/src/main/java/me/viper/teamplugin/util/Utils.java @@ -4,29 +4,61 @@ import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class Utils { + // Matches &#RRGGBB hex color codes + private static final Pattern HEX_PATTERN = Pattern.compile("&#([A-Fa-f0-9]{6})"); + public static String formatIsoNow() { return DateTimeFormatter.ISO_INSTANT .withZone(ZoneId.systemDefault()) .format(Instant.now()); } + /** + * Formats an ISO timestamp into dd/MM/yyyy format. + */ public static String prettifyIso(String iso) { if (iso == null || iso.isEmpty()) return "—"; - Instant inst = Instant.parse(iso); - DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withLocale(Locale.getDefault()) - .withZone(ZoneId.systemDefault()); - return fmt.format(inst); + try { + Instant inst = Instant.parse(iso); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy") + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()); + return fmt.format(inst); + } catch (Exception e) { + return iso; // return as-is if parsing fails + } } + /** + * Translates & color codes and &#RRGGBB hex codes into Minecraft format codes. + * Hex usage in config: &#FF5500 → §x§F§F§5§5§0§0 + */ public static String color(String s) { - return s == null ? "" : s.replace("&", "§"); + if (s == null) return ""; + // Process hex colors first: &#RRGGBB → §x§R§R§G§G§B§B + Matcher m = HEX_PATTERN.matcher(s); + StringBuffer sb = new StringBuffer(); + while (m.find()) { + StringBuilder repl = new StringBuilder("§x"); + for (char c : m.group(1).toCharArray()) { + repl.append('§').append(c); + } + m.appendReplacement(sb, repl.toString()); + } + m.appendTail(sb); + // Then translate remaining & codes + return sb.toString().replace("&", "§"); } - // Ersetze Platzhalter %player% %rank% %joindate% + /** + * Replaces placeholder pairs in a template string. + * e.g. replace(template, "%player%", "Steve", "%rank%", "Admin") + */ public static String replace(String template, String... pairs) { if (template == null) return ""; String out = template; @@ -35,4 +67,4 @@ public class Utils { } return out; } -} +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 45a26f5..911b596 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,64 +1,192 @@ -# ---------- GUI ---------- -gui: - size: 54 # feste Größe - title: "&8» &bTeam Übersicht" - background: GRAY_STAINED_GLASS_PANE +# ══════════════════════════════════════════════════════ +# TeamPlugin – config.yml +# GUI-Größe ist IMMER 54 und NICHT konfigurierbar. +# ══════════════════════════════════════════════════════ -status: - online: "&a🟢 Online" - offline: "&c🔴 Offline" +# ── Sprache / Language ─────────────────────────────────────────────── +# de → Deutsche Befehle & Nachrichten (/team hinzufügen, entfernen …) +# en → English commands & messages (/team add, del …) +# +# Beide Sprachen werden IMMER erkannt – diese Einstellung steuert nur +# die Tab-Completion und die Fehlermeldungen. +# Nach einer Änderung: /team neuladen bzw. /team reload +language: de -# ---------- Ranks ---------- -# Reihenfolge der Ränge (nur Namen, die in rank-settings definiert sind) +# Ränge in dieser Reihenfolge (bestimmt auch die Navigations-Pfeile) ranks: - Owner + - Developer - Admin - Moderator - Supporter + - Builder + - Eventler + - Mediateam -# Pro Rang Darstellung (display = wie der Rang im GUI angezeigt wird, prefix = optional am Spielernamen) -# Du kannst hier &-Farbcodes benutzen, z.B. &c für rot, &6 für gold, &e gelb etc. +# ── GUI-Grundeinstellungen ────────────────────────────────────────── +gui: + title: "&8» &bServer Team" + rank_page_title: "&8» &bTeam &7| " + background: "GRAY_STAINED_GLASS_PANE" + +# ── Rang-Einstellungen ────────────────────────────────────────────── +# +# Jeder Rang kann folgende Felder haben: +# +# display Anzeigename im GUI (Farb-Codes mit & oder &#RRGGBB) +# slot Slot im Übersichts-GUI (0-53). Pflichtfeld für >8 Ränge. +# Ohne Angabe: Fallback-Layout (bis 8 Ränge automatisch). +# material Block-Material für das Übersichts-Icon +# skull_texture Eigener Skull (URL / 64-Hex-Hash / Base64 eyJ...) +# Wenn gesetzt, wird material ignoriert. +# prefix Präfix vor dem Spielernamen auf der Rang-Seite +# glass Hintergrundfarbe auf der Rang-Seite (optional) +# member_slots Liste von Slots (0-44), in denen Mitglieder auf der +# Rang-Seite platziert werden. Ohne Angabe: Auto-Zentrierung. +# +# ─── Übersichts-Layout (Slot-Nummern) ───── +# Zeile 0: 0 1 2 3 4 5 6 7 8 +# Zeile 1: 9 10 11 12 13 14 15 16 17 +# Zeile 2: 18 19 20 21 22 23 24 25 26 +# Zeile 3: 27 28 29 30 31 32 33 34 35 +# Zeile 4: 36 37 38 39 40 41 42 43 44 +# Zeile 5: 45 46 47 48 49 50 51 52 53 ← Navigation (Rang-Seite) +# +# Aktuelles Layout mit 8 Rängen (3 oben | 2 Mitte | 3 unten): +# +# · · · O · D · A · Zeile 1: 10=Owner, 13=Developer, 16=Admin +# · · M · · · S · · Zeile 2: 19=Moderator, 25=Supporter +# · B · · · · · E · M Zeile 3: 28=Builder, 34=Eventler, 36=Mediateam +# rank-settings: + + # ══════════════════════════════════════════════════════════════════ + # skull_texture – Formate (alle drei werden unterstützt): + # + # A) Volle URL → http://textures.minecraft.net/texture/ + # B) 64-Hex → nur den -Teil (64 Zeichen) + # C) Base64 → eyJ... (von minecraft-heads.com → „For Developers") + # + # Wenn skull_texture gesetzt ist, wird „material" ignoriert. + # Quelle für Skulls: https://minecraft-heads.com/custom-heads + # ══════════════════════════════════════════════════════════════════ + Owner: - display: "&cOwner" - prefix: "&c[Owner]" + display: "&4&lOwner" + slot: 13 # Mitte Zeile 1 + material: BEACON # Fallback, falls skull_texture entfernt wird + # Goldene Krone – minecraft-heads.com » Decoration » Crown + skull_texture: "http://textures.minecraft.net/texture/91b0a5bbc697c0c42a6cf1b9c4c4435b07322fce5bb27d82b6930843e5ab7a09" + prefix: "&4[Owner]" + glass: RED_STAINED_GLASS_PANE + # member_slots nicht angegeben → automatisch zentriert + + Developer: + display: "&9&lDeveloper" + slot: 11 # Links Zeile 1 + material: COMMAND_BLOCK + # Terminal / Code-Skull – minecraft-heads.com » Decoration » Computer + skull_texture: "http://textures.minecraft.net/texture/2e019aa96ea6bba412cbcbadb3e68301fc0410e52ca6caea851ae86f5cb74683" + prefix: "&9[Dev]" + glass: BLUE_STAINED_GLASS_PANE + Admin: - display: "&6Admin" - prefix: "&6[Admin]" + display: "&c&lAdmin" + slot: 15 # Rechts Zeile 1 + material: NETHER_STAR + # Rotes Schild-Symbol – minecraft-heads.com » Decoration » Shield + skull_texture: "http://textures.minecraft.net/texture/57c66f5a4b408005b336da6676e8f6a2a67eea315fb7e91360acc047802fa320" + prefix: "&c[Admin]" + glass: ORANGE_STAINED_GLASS_PANE + Moderator: - display: "&eModerator" - prefix: "&e[Mod]" + display: "&6&lModerator" + slot: 20 # Links Zeile 2 + material: IRON_SWORD + # Hammer / Gavel – minecraft-heads.com » Decoration » Gavel + skull_texture: "http://textures.minecraft.net/texture/b67a0625780c0c0e1bb7a8c5189b0109d4ded4148aa2739435804064b8fa16ab" + prefix: "&6[Mod]" + glass: YELLOW_STAINED_GLASS_PANE + Supporter: - display: "&bSupporter" - prefix: "&b[Supp]" + display: "&a&lSupporter" + slot: 24 # Rechts Zeile 2 + material: EMERALD + # Grünes Herz / Hilfe-Symbol – minecraft-heads.com » Decoration » Heart (green) + skull_texture: "http://textures.minecraft.net/texture/eff7b97e6465534f38b2afb4051c949f2673a64f1453ef698659424aa5c4ef1f" + prefix: "&a[Sup]" + glass: LIME_STAINED_GLASS_PANE -# ---------- Admin GUI Buttons ---------- -admin-buttons: - - key: edit_ranks - material: PAPER - slot: 11 - title: "&aRänge bearbeiten" - lore: - - "&7Bearbeite 'ranks' in config.yml" - - "&7und benutze &e/team settings -> Plugin neu laden" - - key: reload - material: BARRIER - slot: 13 - title: "&cPlugin neu laden" - lore: - - "&7Lädt Config, Lang & Daten neu" - - key: backup - material: CHEST - slot: 15 - title: "&eBackup erstellen" - lore: - - "&7Erstellt eine Sicherung von data.yml" + Builder: + display: "&e&lBuilder" + slot: 29 # Links Zeile 3 + material: BRICKS + # Steinblock / Hammer – minecraft-heads.com » Decoration » Bricks + skull_texture: "http://textures.minecraft.net/texture/56b1d4947763917814ae1632b820696098927895aaaf124cd29eb35856aaa5b9" + prefix: "&e[Builder]" + glass: YELLOW_STAINED_GLASS_PANE + member_slots: [10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25] + # Builder haben oft viele Mitglieder → feste Slots für saubere Anordnung -# ---------- Backup Settings ---------- + Eventler: + display: "&d&lEventler" + slot: 33 # Rechts Zeile 3 + material: FIREWORK_ROCKET + # Feuerwerk-Rakete – minecraft-heads.com » Decoration » Firework + skull_texture: "http://textures.minecraft.net/texture/ead2632ee5564512e6cc498dd809363bda9da518d36885acbbcdc9fb55a" + prefix: "&d[Event]" + glass: MAGENTA_STAINED_GLASS_PANE + + Mediateam: + display: "&b&lMediateam" + slot: 31 # Mitte Zeile 3 + material: SPYGLASS + # Kamera / Aufnahme – minecraft-heads.com » Decoration » Camera + skull_texture: "http://textures.minecraft.net/texture/97cafa39dbee0fac4355f99a712ba699f88d6d297828b40789c361f8d8fead61" + prefix: "&b[Media]" + glass: CYAN_STAINED_GLASS_PANE + +# ── Status-Anzeige ────────────────────────────────────────────────── +status: + online: "&a🟢 Online" + offline: "&c🔴 Offline" + +# ── Beitritt-Datum speichern ───────────────────────────────────────── +storeJoinDate: true + +# ── Backup-Einstellungen ───────────────────────────────────────────── backup: enabled: true folder: backups - keep: 10 # wie viele Backups behalten werden + keep: 10 -# ---------- Misc ---------- -storeJoinDate: true +# ── Admin-Buttons im Settings-GUI ─────────────────────────────────── +admin-buttons: + - key: reload + slot: 11 + material: CLOCK + title: "&aPlugin neu laden" + lore: + - "&7Lädt Config & Daten neu" + + - key: backup_now + slot: 13 + material: CHEST + title: "&eBackup erstellen" + lore: + - "&7Erstellt sofort ein Backup" + + - key: backup + slot: 15 + material: BOOK + title: "&bBackups anzeigen" + lore: + - "&7Öffnet die Backup-Liste" + + - key: edit_ranks + slot: 22 + material: WRITABLE_BOOK + title: "&6Ränge bearbeiten" + lore: + - "&7Bearbeite ranks & rank-settings" + - "&7in der config.yml" \ No newline at end of file diff --git a/src/main/resources/lang_de.yml b/src/main/resources/lang_de.yml new file mode 100644 index 0000000..1587faf --- /dev/null +++ b/src/main/resources/lang_de.yml @@ -0,0 +1,153 @@ +prefix: "§8[§bTeam§8] §7" + +# ── Subcommand-Namen (für Tab-Completion und Usage-Texte) ──────────── +# Diese Namen werden als Befehle erkannt. Ändere sie nicht ohne Grund. +cmd_add: "hinzufügen" +cmd_del: "entfernen" +cmd_move: "verschieben" +cmd_settings: "einstellungen" +cmd_backup: "sicherung" +cmd_restore: "wiederherstellen" +cmd_backups: "sicherungen" +cmd_reload: "neuladen" +cmd_info: "info" +cmd_search: "suchen" +cmd_apply: "bewerben" +cmd_apply_list: "liste" +cmd_apply_accept: "annehmen" +cmd_apply_deny: "ablehnen" +cmd_log: "protokoll" +cmd_mailbox: "postfach" + +# ── Allgemein ──────────────────────────────────────────────────────── +no_permission: "%prefix%§cDazu hast du keine Berechtigung!" +only_player: "%prefix%§cNur Spieler können diesen Befehl benutzen!" +unknown_command: "%prefix%§cUnbekannter Befehl." + +# ── /team hinzufügen / entfernen ───────────────────────────────────── +add_usage: "%prefix%§cVerwendung: /team hinzufügen " +del_usage: "%prefix%§cVerwendung: /team entfernen " +player_added: "%prefix%§a%player% wurde zu §e%rank% §ahinzugefügt!" +player_removed: "%prefix%§a%player% wurde entfernt!" +player_not_found: "%prefix%§c%player% wurde nicht gefunden!" + +# ── /team verschieben ──────────────────────────────────────────────── +player_moved: "%prefix%§a%player% wurde von §e%old% §anach §e%new% §averschoben." + +# ── /team suchen ───────────────────────────────────────────────────── +search_results: "%prefix%§bSuchergebnisse für §e%query%§b:" +search_no_results: "%prefix%§cKeine Teammitglieder mit §e%query% §cgefunden." + +# ── /team info ─────────────────────────────────────────────────────── +info_usage: "%prefix%§cVerwendung: /team info " +info_header: "%prefix%§bInfo für §e%player%§8:" +info_rank: "%prefix%§7Rang§8: %rank%" +info_joined: "%prefix%§7Beigetreten§8: §e%joindate%" +info_status: "%prefix%§7Status§8: %status%" +info_not_team: "%prefix%§c%player% ist kein Teammitglied." + +# ── /team bewerben ─────────────────────────────────────────────────── +apply_sent: "%prefix%§aDeine Bewerbung für §e%rank% §awurde eingereicht!" +apply_already: "%prefix%§cDu hast bereits eine offene Bewerbung." +apply_already_member: "%prefix%§cDu bist bereits im Team." +apply_admin_notify: "%prefix%§e%player% §7hat sich für §b%rank% §7beworben. §8(/team bewerben liste)" +apply_accepted: "%prefix%§aBewerbung von §e%player% §afür §e%rank% §aangenommen." +apply_denied: "%prefix%§cBewerbung von §e%player% §cabgelehnt." +apply_you_accepted: "%prefix%§aDeine Bewerbung für §e%rank% §awurde angenommen. Willkommen im Team!" +apply_you_denied: "%prefix%§cDeine Bewerbung für §e%rank% §cwurde leider abgelehnt." + +# ── GUI: Übersichts-Titel (kommt aus config.yml → gui.title) ───────── +# team_gui_title: nicht verwendet +settings_gui_title: "%prefix%§cTeam Einstellungen" + +# ── Settings-GUI-Buttons ───────────────────────────────────────────── +settings_edit_ranks: "§aRänge bearbeiten" +settings_edit_ranks_lore: + - "§7Bearbeite die Ränge in der config.yml" + - "§7und nutze §e/team einstellungen §7→ Neuladen" +settings_reload: "§cPlugin neu laden" +settings_reload_lore: + - "§7Klicke um das Plugin neu zu laden" +settings_backup: "§eBackups verwalten" +settings_backup_lore: + - "§7Öffnet die Backup-Übersicht" + +# ── Rang-Seite: Navigation ──────────────────────────────────────────── +nav_prev_label: "§7« %rank%" +nav_home_label: "§fHauptmenü" +nav_next_label: "%rank% §7»" +nav_prev_lore: "§7Vorherige Seite" +nav_home_lore: "§7Zurück zur Übersicht" +nav_next_lore: "§7Nächste Seite" +page_prev_label: "§7← Vorherige Seite" +page_next_label: "§7Nächste Seite →" + +# ── Spieler-Kopf Tooltip ───────────────────────────────────────────── +tooltip_rank: "§7Rang§8: §e%rank%" +tooltip_joined: "§7Beigetreten§8: §e%joindate%" + +# ── Nachrichten (Kopf-Klick & Postfach) ────────────────────────────── +msg_head_hint: "§e▶ Klicke zum Schreiben einer Nachricht" +msg_enter_message: "%prefix%§eNachricht an §b%player%§e tippen:" +msg_reply_prompt: "%prefix%§eAntwort an §b%player%§e tippen:" +msg_cancel_hint: "%prefix%§7Tippe §c§labbrechen §7zum Abbrechen." +msg_cancel_word: "abbrechen" +msg_cancelled: "%prefix%§7Nachricht abgebrochen." +msg_format: "%prefix%§b%sender% §8» §f%message%" +msg_sent_online: "%prefix%§aNachricht an §b%player% §ageschickt!" +msg_sent_offline: "%prefix%§e%player% §7ist offline. Nachricht wird beim nächsten Login zugestellt." +msg_offline_header: "%prefix%§eDu hast §b%count% §eoffline Nachricht(en) erhalten:" +msg_mailbox_hint: "%prefix%§7Tippe §b/team postfach §7um deine Nachrichten zu lesen." + +# ── Postfach-GUI ───────────────────────────────────────────────────── +mailbox_title: "%prefix%§bPostfach" +mailbox_read_title: "%prefix%§bNachricht lesen" +mailbox_prev_page: "§7← Vorherige Seite" +mailbox_next_page: "§7Nächste Seite →" +mailbox_close: "§cSchließen" +mailbox_delete_btn: "§c§lLöschen" +mailbox_reply_btn: "§a§lAntworten an %player%" +mailbox_back_btn: "§7← Zurück" +mail_deleted: "%prefix%§7Nachricht gelöscht." + +# ── Backup ─────────────────────────────────────────────────────────── +backup_created: "%prefix%§aSicherung erstellt: §e%file%" +backup_restore_success: "%prefix%§aSicherung wiederhergestellt: §e%file%" +backup_not_found: "%prefix%§cSicherung nicht gefunden: §e%file%" +backups_list_title: "%prefix%§bVerfügbare Sicherungen:" +no_backups: "%prefix%§7Keine Sicherungen vorhanden." + +# ── Misc ───────────────────────────────────────────────────────────── +plugin_reloaded: "%prefix%§aPlugin erfolgreich neu geladen!" + +# ── Bewerbungs-GUI ──────────────────────────────────────────────────── +# Admin-GUI: Bewerbungen verwalten +apply_gui_selection_title: "%prefix%§bBewerbungen verwalten" +apply_gui_detail_title: "%prefix%§bBewerbung prüfen" +# Buttons im Admin-Detail-View +apply_accepted_btn: "Annehmen" +apply_denied_btn: "Ablehnen" +apply_deny_lore: "Bewerbung ablehnen und entfernen" +# Usage-Hinweis für Spieler (kein GUI mehr) +apply_usage: "Verwendung: /team bewerben [Grund]" +apply_gui_close: "§cSchließen" +apply_gui_close_lore: "§7Bewerbung abbrechen" +apply_gui_members_label: "Mitglieder" +apply_gui_already_member: "Du bist bereits Mitglied" +apply_gui_already_applied: "Bewerbung bereits eingereicht" +apply_gui_click_to_apply: "Klicken zum Bewerben" +apply_gui_write_text: "§e§lPersönlichen Text hinzufügen" +apply_gui_edit_text: "§e§lText bearbeiten" +apply_gui_write_lore: "§7Schreibe warum du ins Team möchtest" +apply_gui_no_text: "Kein persönlicher Text" +apply_gui_no_text_lore: "Optional – du kannst auch ohne Text einreichen" +apply_gui_text_preview_title: "Dein Text:" +apply_gui_text_edit_hint: "Klicke auf das Buch um den Text zu ändern" +apply_gui_cancel: "§c§lAbbrechen" +apply_gui_cancel_lore: "§7Zurück zur Rang-Auswahl" +apply_gui_submit: "§a§lBewerbung einreichen" +apply_gui_submit_no_text_note: "Kein persönlicher Text – trotzdem einreichen" +apply_gui_submit_lore: "Klicken zum Absenden" +apply_gui_type_prompt: "%prefix%§eSchreibe deinen persönlichen Text in den Chat:" +apply_gui_text_too_long: "%prefix%§cText zu lang! Maximal §e%max% §cZeichen." +apply_gui_text_saved: "%prefix%§aText gespeichert!" \ No newline at end of file diff --git a/src/main/resources/lang_en.yml b/src/main/resources/lang_en.yml new file mode 100644 index 0000000..5f22678 --- /dev/null +++ b/src/main/resources/lang_en.yml @@ -0,0 +1,152 @@ +prefix: "§8[§bTeam§8] §7" + +# ── Subcommand names (for tab-completion and usage texts) ──────────── +cmd_add: "add" +cmd_del: "del" +cmd_move: "move" +cmd_settings: "settings" +cmd_backup: "backup" +cmd_restore: "restore" +cmd_backups: "backups" +cmd_reload: "reload" +cmd_info: "info" +cmd_search: "search" +cmd_apply: "apply" +cmd_apply_list: "list" +cmd_apply_accept: "accept" +cmd_apply_deny: "deny" +cmd_log: "log" +cmd_mailbox: "mailbox" + +# ── General ────────────────────────────────────────────────────────── +no_permission: "%prefix%§cYou don't have permission to do that!" +only_player: "%prefix%§cOnly players can use this command!" +unknown_command: "%prefix%§cUnknown command." + +# ── /team add / del ─────────────────────────────────────────────────── +add_usage: "%prefix%§cUsage: /team add " +del_usage: "%prefix%§cUsage: /team del " +player_added: "%prefix%§a%player% was added to §e%rank%§a!" +player_removed: "%prefix%§a%player% was removed!" +player_not_found: "%prefix%§c%player% was not found!" + +# ── /team move ──────────────────────────────────────────────────────── +player_moved: "%prefix%§a%player% was moved from §e%old% §ato §e%new%§a." + +# ── /team search ────────────────────────────────────────────────────── +search_results: "%prefix%§bSearch results for §e%query%§b:" +search_no_results: "%prefix%§cNo team members found matching §e%query%§c." + +# ── /team info ──────────────────────────────────────────────────────── +info_usage: "%prefix%§cUsage: /team info " +info_header: "%prefix%§bInfo for §e%player%§8:" +info_rank: "%prefix%§7Rank§8: %rank%" +info_joined: "%prefix%§7Joined§8: §e%joindate%" +info_status: "%prefix%§7Status§8: %status%" +info_not_team: "%prefix%§c%player% is not a team member." + +# ── /team apply ─────────────────────────────────────────────────────── +apply_sent: "%prefix%§aYour application for §e%rank% §ahas been submitted!" +apply_already: "%prefix%§cYou already have a pending application." +apply_already_member: "%prefix%§cYou are already in the team." +apply_admin_notify: "%prefix%§e%player% §7applied for §b%rank%§7. §8(/team apply list)" +apply_accepted: "%prefix%§aApplication of §e%player% §afor §e%rank% §aaccepted." +apply_denied: "%prefix%§cApplication of §e%player% §cdenied." +apply_you_accepted: "%prefix%§aYour application for §e%rank% §awas accepted. Welcome to the team!" +apply_you_denied: "%prefix%§cYour application for §e%rank% §cwas unfortunately denied." + +# ── GUI titles ──────────────────────────────────────────────────────── +# team_gui_title: not used – comes from config.yml → gui.title +settings_gui_title: "%prefix%§cTeam Settings" + +# ── Settings GUI buttons ────────────────────────────────────────────── +settings_edit_ranks: "§aEdit ranks" +settings_edit_ranks_lore: + - "§7Edit ranks in config.yml" + - "§7and use §e/team settings §7→ Reload" +settings_reload: "§cReload plugin" +settings_reload_lore: + - "§7Click to reload the plugin" +settings_backup: "§eManage backups" +settings_backup_lore: + - "§7Opens the backup overview" + +# ── Rank page navigation ────────────────────────────────────────────── +nav_prev_label: "§7« %rank%" +nav_home_label: "§fMain menu" +nav_next_label: "%rank% §7»" +nav_prev_lore: "§7Previous page" +nav_home_lore: "§7Back to overview" +nav_next_lore: "§7Next page" +page_prev_label: "§7← Previous page" +page_next_label: "§7Next page →" + +# ── Player head tooltip ─────────────────────────────────────────────── +tooltip_rank: "§7Rank§8: §e%rank%" +tooltip_joined: "§7Joined§8: §e%joindate%" + +# ── Messages (head click & mailbox) ────────────────────────────────── +msg_head_hint: "§e▶ Click to send a message" +msg_enter_message: "%prefix%§eType your message to §b%player%§e:" +msg_reply_prompt: "%prefix%§eType your reply to §b%player%§e:" +msg_cancel_hint: "%prefix%§7Type §c§lcancel §7to abort." +msg_cancel_word: "cancel" +msg_cancelled: "%prefix%§7Message cancelled." +msg_format: "%prefix%§b%sender% §8» §f%message%" +msg_sent_online: "%prefix%§aMessage sent to §b%player%§a!" +msg_sent_offline: "%prefix%§e%player% §7is offline. Message will be delivered on next login." +msg_offline_header: "%prefix%§eYou have §b%count% §eoffline message(s):" +msg_mailbox_hint: "%prefix%§7Type §b/team mailbox §7to read your messages." + +# ── Mailbox GUI ─────────────────────────────────────────────────────── +mailbox_title: "%prefix%§bMailbox" +mailbox_read_title: "%prefix%§bRead message" +mailbox_prev_page: "§7← Previous page" +mailbox_next_page: "§7Next page →" +mailbox_close: "§cClose" +mailbox_delete_btn: "§c§lDelete" +mailbox_reply_btn: "§a§lReply to %player%" +mailbox_back_btn: "§7← Back" +mail_deleted: "%prefix%§7Message deleted." + +# ── Backup ──────────────────────────────────────────────────────────── +backup_created: "%prefix%§aBackup created: §e%file%" +backup_restore_success: "%prefix%§aBackup restored: §e%file%" +backup_not_found: "%prefix%§cBackup not found: §e%file%" +backups_list_title: "%prefix%§bAvailable backups:" +no_backups: "%prefix%§7No backups available." + +# ── Misc ────────────────────────────────────────────────────────────── +plugin_reloaded: "%prefix%§aPlugin successfully reloaded!" + +# ── Application GUI ─────────────────────────────────────────────────── +# Admin GUI: manage applications +apply_gui_selection_title: "%prefix%§bManage applications" +apply_gui_detail_title: "%prefix%§bReview application" +# Buttons in the admin detail view +apply_accepted_btn: "Accept" +apply_denied_btn: "Deny" +apply_deny_lore: "Deny and remove this application" +# Usage hint for players (no GUI anymore) +apply_usage: "Usage: /team apply [reason]" +apply_gui_close: "§cClose" +apply_gui_close_lore: "§7Cancel application" +apply_gui_members_label: "Members" +apply_gui_already_member: "You are already a member" +apply_gui_already_applied: "Application already submitted" +apply_gui_click_to_apply: "Click to apply" +apply_gui_write_text: "§e§lAdd personal text" +apply_gui_edit_text: "§e§lEdit text" +apply_gui_write_lore: "§7Write why you want to join the team" +apply_gui_no_text: "No personal text" +apply_gui_no_text_lore: "Optional – you can submit without a message" +apply_gui_text_preview_title: "Your message:" +apply_gui_text_edit_hint: "Click the book to change your text" +apply_gui_cancel: "§c§lCancel" +apply_gui_cancel_lore: "§7Back to rank selection" +apply_gui_submit: "§a§lSubmit application" +apply_gui_submit_no_text_note: "No personal text – submit anyway" +apply_gui_submit_lore: "Click to send" +apply_gui_type_prompt: "%prefix%§eType your personal message in chat:" +apply_gui_text_too_long: "%prefix%§cText too long! Maximum §e%max% §ccharacters." +apply_gui_text_saved: "%prefix%§aText saved!" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4ca502d..31d77ec 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: Team -version: 1.0.0 +version: 1.0.2 main: me.viper.teamplugin.Main api-version: 1.21 author: Viper @@ -8,11 +8,11 @@ description: Erweiterbares Team-Plugin mit GUI, Backup/Restore und vielen Config commands: team: description: Team GUI & Verwaltung - usage: /team [add|del|settings|backup|restore|backups] + usage: /team [add|del|info|reload|settings|backup|restore|backups] permission: teamplugin.use permissions: teamplugin.use: default: true teamplugin.admin: - default: op + default: op \ No newline at end of file