Update from Git Manager GUI

This commit is contained in:
2026-03-24 08:54:30 +01:00
parent 6edf8f9157
commit e4b2ac32ca
18 changed files with 2748 additions and 265 deletions

View File

@@ -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;
}
}
}

View File

@@ -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 <rank> [reason] command only, no GUI
* Admins : /team apply (no args) opens ApplicationGUI
* /team apply accept <player>
* /team apply deny <player>
* /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<String, String> 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<String, String> 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<String> 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<String> activeApplySubs(boolean isAdmin) {
List<String> 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<String> 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<String> 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") + " <player> <rank>"); return true; }
String name = args[1], newRank = args[2];
List<String> 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") + " <file>"); 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 <dateiname>");
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<String> 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<String> 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") + " <n>"); return true; }
String query = args[1].toLowerCase();
List<String> 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<String> 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 <player>) → accept via command
// Admins (/team apply deny <player>) → deny via command
//
// Players (/team bewerben <rank> [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<String[]> 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] + " <player>");
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 <rank> [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<String> 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<String> 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<String> filter(List<String> src, String prefix) {
return src.stream().filter(s -> s.toLowerCase().startsWith(prefix.toLowerCase())).collect(Collectors.toList());
}
private List<String> onlinePlayers(String prefix) {
return Bukkit.getOnlinePlayers().stream().map(Player::getName)
.filter(n -> n.toLowerCase().startsWith(prefix.toLowerCase())).collect(Collectors.toList());
}
private List<String> allTeamMembers() {
var section = DataManager.getData().getConfigurationSection("Team");
if (section == null) return new ArrayList<>();
List<String> 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";
}
}

View File

@@ -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 <rank> [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<UUID, Integer> CURRENT_PAGE = new HashMap<>();
/** UUID → applicant name currently shown in the detail view */
private static final Map<UUID, String> VIEWING_PLAYER = new HashMap<>();
// ── Open: list view ───────────────────────────────────────────────
public static void openApplicationList(Player admin, int page) {
List<String[]> 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<String[]> 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<String> 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<String> 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<String> 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<String> wrapText(String text, int maxChars) {
List<String> 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;
}
}

View File

@@ -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<UUID, Integer> CURRENT_PAGE = new HashMap<>();
/** UUID → whose mailbox is open (player name) */
private static final Map<UUID, String> OPEN_FOR_PLAYER = new HashMap<>();
/** UUID → msgId of message currently being read */
private static final Map<UUID, String> 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<String[]> 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<String[]> 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<String> 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<String> 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<String> wrapText(String text, int maxChars) {
List<String> 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;
}
}

View File

@@ -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.<rank>.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.<rank>.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<UUID, Integer> RANK_PAGE = new HashMap<>();
List<String> 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<String> 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<String> 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<String> 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<String> ranks = cfg.getStringList("ranks");
int rankIdx = ranks.indexOf(rank);
if (rankIdx < 0) return;
int rankCount = ranks.size();
// All members for this rank
List<String> allMembers = DataManager.getData().getStringList("Team." + rank);
// Determine page capacity
List<Integer> 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<String> 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<Integer> 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<Integer> resolveMemberSlots(FileConfiguration cfg, String rank, int count) {
List<Integer> 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<String> 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<Integer> computeSlots(int count) {
List<Integer> 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<String> 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<String> 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<String> allMembers = DataManager.getData().getStringList("Team." + rank);
List<Integer> 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<String> pageMembers = allMembers.subList(start, end);
List<Integer> 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<String> 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<String> 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<String> 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; }
}
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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<String> 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<String[]> 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<String[]> getAllApplications() {
List<String[]> 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<String> 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<String> 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;
}
}

View File

@@ -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<String> 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<String> getEntries(int limit) {
ensureLoaded();
List<String> all = cfg.getStringList("entries");
int from = Math.max(0, all.size() - limit);
return new ArrayList<>(all.subList(from, all.size()));
}
}

View File

@@ -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<String> 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(); }
}
}

View File

@@ -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.<playerName> current inbox messages
* History.<playerName> 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<String> 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<String[]> getParsedMessages(String player) {
ensureLoaded();
List<String[]> 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<String> 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<String> 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<String> 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<String[]> getHistory(String player) {
ensureLoaded();
List<String[]> 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; }
}
}

View File

@@ -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<UUID, String> PENDING_INPUT = new HashMap<>();
private static final Map<UUID, String> 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);
}
}

View File

@@ -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<String, UUID> 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<UUID, Inventory> 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":"<ActualName>"}
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);
}
}

View File

@@ -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;
}
}
}