4 Commits
1.0.1 ... 1.0.4

19 changed files with 2819 additions and 327 deletions

View File

@@ -5,7 +5,7 @@
<groupId>me.viper</groupId>
<artifactId>Team</artifactId>
<version>1.0.0</version>
<version>1.0.2</version>
<name>TeamPlugin</name>
<description>Erweitertes konfigurierbares Team-Plugin (Spigot 1.21.x)</description>

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.");
}

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);
sender.sendMessage(Utils.replace(LangManager.get("player_added"), "%player%", name, "%rank%", rank));
return true;
AuditLog.log(AuditLog.ADD, senderName(sender), name + " \u2192 " + rank);
sender.sendMessage(Utils.color(Utils.replace(LangManager.get("player_added"), "%player%", name, "%rank%", rank)));
}
if (sub.equals("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;
}
// ── 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)
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 (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("settings")) {
if (!(sender instanceof Player) || !sender.hasPermission("teamplugin.admin")) {
sender.sendMessage(LangManager.get("no_permission"));
return true;
}
SettingsGUI.openSettings((Player) sender);
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 (sub.equals("backup")) {
if (!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);
}
// ── backup ────────────────────────────────────────────────
case C_BACKUP -> {
if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; }
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;
sender.sendMessage(Utils.color(file != null
? Utils.replace(LangManager.get("backup_created"), "%file%", file)
: LangManager.get("prefix") + "\u00a7cBackup failed."));
}
if (sub.equals("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")) + "§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;
// ── 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 (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"));
return true;
}
sender.sendMessage(LangManager.get("backups_list_title"));
list.forEach(s -> sender.sendMessage("§7- §e" + s));
return true;
}
// Player: open Backup GUI
Player p = (Player) sender;
if (!p.hasPermission("teamplugin.admin")) {
p.sendMessage(LangManager.get("no_permission"));
return true;
}
// ── 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("unknown_command"));
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);
}
default -> sender.sendMessage(Utils.color(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;
@@ -11,96 +13,461 @@ import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
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 {
// rows mapping: we will place ranks on rows 1..4 (slots 9-17, 18-26, 27-35, 36-44)
private static final int[] rows = {1, 2, 3, 4};
private static final int GUI_SIZE = 54;
public static void openTeamGUI(Player player) {
private static final int[] OVERVIEW_SLOTS_FALLBACK = {13, 22, 31, 40, 11, 15, 29, 33};
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;
/** UUID → current page on the rank page they have open */
private static final Map<UUID, Integer> RANK_PAGE = new HashMap<>();
// ─────────────────────────────────────────────────────────────────
// 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();
FileConfiguration data = DataManager.getData();
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);
// background filler
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);
List<String> ranks = cfg.getStringList("ranks");
for (int i = 0; i < ranks.size() && i < rows.length; i++) {
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 rowStart = rows[i] * 9;
int count = Math.min(members.size(), 9);
if (count == 0) {
// show placeholder in middle
ItemStack empty = createInfoItem("§7Kein/e " + rank, List.of("§7Keine Mitglieder"));
inv.setItem(rowStart + 4, empty);
continue;
int slot = resolveOverviewSlot(cfg, rank, i);
if (slot < 0 || slot >= GUI_SIZE) continue;
inv.setItem(slot, createRankOverviewBlock(rank));
}
player.openInventory(inv);
}
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));
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);
}
}
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);
}
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;
}
// ─────────────────────────────────────────────────────────────────
// 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) {
if (meta == null) return skull;
@SuppressWarnings("deprecation")
OfflinePlayer off = Bukkit.getOfflinePlayer(name);
meta.setOwningPlayer(off);
applyHeadMeta(meta, name, rank);
skull.setItemMeta(meta);
return skull;
}
// rank display & prefix aus config
String rankDisplay = cfg.getString("rank-settings." + rank + ".display", rank);
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", "");
// Anzeige-Name: optional Prefix (farbcodes erlauben) + Spielername (in blau)
String displayName = (rankPrefix == null ? "" : rankPrefix + " ") + "&b" + name;
String displayName = rankPrefix.isEmpty() ? "&b" + name : rankPrefix + " &b" + name;
meta.setDisplayName(Utils.color(displayName.trim()));
List<String> lore = new ArrayList<>();
// Rang-Zeile (farbig, aus config display nutzen)
String rankLine = Utils.replace(LangManager.get("tooltip_rank"), "%rank%", rankDisplay);
lore.add(Utils.color(rankLine));
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)));
// Join-Datum falls vorhanden
String iso = DataManager.getData().getString("JoinDates." + name, "");
String joinLine = Utils.replace(LangManager.get("tooltip_joined"), "%joindate%", Utils.prettifyIso(iso));
lore.add(Utils.color(joinLine));
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);
skull.setItemMeta(meta);
}
// ─────────────────────────────────────────────────────────────────
// 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();
@@ -112,11 +479,7 @@ public class TeamGUI {
return item;
}
/**
* Liefert den (ungefärbten) GUI-Titel aus der config (z.B. "&8» &bTeam Übersicht").
* Listener sollte Utils.color(...) darauf anwenden bevor er mit e.getView().getTitle() vergleicht.
*/
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 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);
}
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);
}
/** 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();
}
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 "";
try {
Instant inst = Instant.parse(iso);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
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;

View File

@@ -1,60 +1,192 @@
# ---------- GUI ----------
gui:
size: 54 # feste Größe
title: "&8» &bTeam Übersicht"
background: GRAY_STAINED_GLASS_PANE
# ══════════════════════════════════════════════════════
# TeamPlugin config.yml
# GUI-Größe ist IMMER 54 und NICHT konfigurierbar.
# ══════════════════════════════════════════════════════
# ---------- Ranks ----------
# Reihenfolge der Ränge (nur Namen, die in rank-settings definiert sind)
# ── Sprache / Language ───────────────────────────────────────────────
# de → Deutsche Befehle & Nachrichten (/team hinzufügen, entfernen …)
# en → English commands & messages (/team add, del …)
#
# Beide Sprachen werden IMMER erkannt diese Einstellung steuert nur
# die Tab-Completion und die Fehlermeldungen.
# Nach einer Änderung: /team neuladen bzw. /team reload
language: de
# Ränge in dieser Reihenfolge (bestimmt auch die Navigations-Pfeile)
ranks:
- Owner
- Developer
- Admin
- Moderator
- Supporter
- Builder
- Eventler
- Mediateam
# Pro Rang Darstellung (display = wie der Rang im GUI angezeigt wird, prefix = optional am Spielernamen)
# Du kannst hier &-Farbcodes benutzen, z.B. &c für rot, &6 für gold, &e gelb etc.
# ── GUI-Grundeinstellungen ──────────────────────────────────────────
gui:
title: "&8» &bServer Team"
rank_page_title: "&8» &bTeam &7| "
background: "GRAY_STAINED_GLASS_PANE"
# ── Rang-Einstellungen ──────────────────────────────────────────────
#
# Jeder Rang kann folgende Felder haben:
#
# display Anzeigename im GUI (Farb-Codes mit & oder &#RRGGBB)
# slot Slot im Übersichts-GUI (0-53). Pflichtfeld für >8 Ränge.
# Ohne Angabe: Fallback-Layout (bis 8 Ränge automatisch).
# material Block-Material für das Übersichts-Icon
# skull_texture Eigener Skull (URL / 64-Hex-Hash / Base64 eyJ...)
# Wenn gesetzt, wird material ignoriert.
# prefix Präfix vor dem Spielernamen auf der Rang-Seite
# glass Hintergrundfarbe auf der Rang-Seite (optional)
# member_slots Liste von Slots (0-44), in denen Mitglieder auf der
# Rang-Seite platziert werden. Ohne Angabe: Auto-Zentrierung.
#
# ─── Übersichts-Layout (Slot-Nummern) ─────
# Zeile 0: 0 1 2 3 4 5 6 7 8
# Zeile 1: 9 10 11 12 13 14 15 16 17
# Zeile 2: 18 19 20 21 22 23 24 25 26
# Zeile 3: 27 28 29 30 31 32 33 34 35
# Zeile 4: 36 37 38 39 40 41 42 43 44
# Zeile 5: 45 46 47 48 49 50 51 52 53 ← Navigation (Rang-Seite)
#
# Aktuelles Layout mit 8 Rängen (3 oben | 2 Mitte | 3 unten):
#
# · · · O · D · A · Zeile 1: 10=Owner, 13=Developer, 16=Admin
# · · M · · · S · · Zeile 2: 19=Moderator, 25=Supporter
# · B · · · · · E · M Zeile 3: 28=Builder, 34=Eventler, 36=Mediateam
#
rank-settings:
# ══════════════════════════════════════════════════════════════════
# skull_texture Formate (alle drei werden unterstützt):
#
# A) Volle URL → http://textures.minecraft.net/texture/<HASH>
# B) 64-Hex → nur den <HASH>-Teil (64 Zeichen)
# C) Base64 → eyJ... (von minecraft-heads.com → „For Developers")
#
# Wenn skull_texture gesetzt ist, wird „material" ignoriert.
# Quelle für Skulls: https://minecraft-heads.com/custom-heads
# ══════════════════════════════════════════════════════════════════
Owner:
display: "&cOwner"
prefix: "&c[Owner]"
display: "&4&lOwner"
slot: 13 # Mitte Zeile 1
material: BEACON # Fallback, falls skull_texture entfernt wird
# Goldene Krone minecraft-heads.com » Decoration » Crown
skull_texture: "http://textures.minecraft.net/texture/91b0a5bbc697c0c42a6cf1b9c4c4435b07322fce5bb27d82b6930843e5ab7a09"
prefix: "&4[Owner]"
glass: RED_STAINED_GLASS_PANE
# member_slots nicht angegeben → automatisch zentriert
Developer:
display: "&9&lDeveloper"
slot: 11 # Links Zeile 1
material: COMMAND_BLOCK
# Terminal / Code-Skull minecraft-heads.com » Decoration » Computer
skull_texture: "http://textures.minecraft.net/texture/2e019aa96ea6bba412cbcbadb3e68301fc0410e52ca6caea851ae86f5cb74683"
prefix: "&9[Dev]"
glass: BLUE_STAINED_GLASS_PANE
Admin:
display: "&6Admin"
prefix: "&6[Admin]"
display: "&c&lAdmin"
slot: 15 # Rechts Zeile 1
material: NETHER_STAR
# Rotes Schild-Symbol minecraft-heads.com » Decoration » Shield
skull_texture: "http://textures.minecraft.net/texture/57c66f5a4b408005b336da6676e8f6a2a67eea315fb7e91360acc047802fa320"
prefix: "&c[Admin]"
glass: ORANGE_STAINED_GLASS_PANE
Moderator:
display: "&eModerator"
prefix: "&e[Mod]"
display: "&6&lModerator"
slot: 20 # Links Zeile 2
material: IRON_SWORD
# Hammer / Gavel minecraft-heads.com » Decoration » Gavel
skull_texture: "http://textures.minecraft.net/texture/b67a0625780c0c0e1bb7a8c5189b0109d4ded4148aa2739435804064b8fa16ab"
prefix: "&6[Mod]"
glass: YELLOW_STAINED_GLASS_PANE
Supporter:
display: "&bSupporter"
prefix: "&b[Supp]"
display: "&a&lSupporter"
slot: 24 # Rechts Zeile 2
material: EMERALD
# Grünes Herz / Hilfe-Symbol minecraft-heads.com » Decoration » Heart (green)
skull_texture: "http://textures.minecraft.net/texture/eff7b97e6465534f38b2afb4051c949f2673a64f1453ef698659424aa5c4ef1f"
prefix: "&a[Sup]"
glass: LIME_STAINED_GLASS_PANE
# ---------- Admin GUI Buttons ----------
admin-buttons:
- key: edit_ranks
material: PAPER
slot: 11
title: "&aRänge bearbeiten"
lore:
- "&7Bearbeite 'ranks' in config.yml"
- "&7und benutze &e/team settings -> Plugin neu laden"
- key: reload
material: BARRIER
slot: 13
title: "&cPlugin neu laden"
lore:
- "&7Lädt Config, Lang & Daten neu"
- key: backup
material: CHEST
slot: 15
title: "&eBackup erstellen"
lore:
- "&7Erstellt eine Sicherung von data.yml"
Builder:
display: "&e&lBuilder"
slot: 29 # Links Zeile 3
material: BRICKS
# Steinblock / Hammer minecraft-heads.com » Decoration » Bricks
skull_texture: "http://textures.minecraft.net/texture/56b1d4947763917814ae1632b820696098927895aaaf124cd29eb35856aaa5b9"
prefix: "&e[Builder]"
glass: YELLOW_STAINED_GLASS_PANE
member_slots: [10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25]
# Builder haben oft viele Mitglieder → feste Slots für saubere Anordnung
# ---------- Backup Settings ----------
Eventler:
display: "&d&lEventler"
slot: 33 # Rechts Zeile 3
material: FIREWORK_ROCKET
# Feuerwerk-Rakete minecraft-heads.com » Decoration » Firework
skull_texture: "http://textures.minecraft.net/texture/ead2632ee5564512e6cc498dd809363bda9da518d36885acbbcdc9fb55a"
prefix: "&d[Event]"
glass: MAGENTA_STAINED_GLASS_PANE
Mediateam:
display: "&b&lMediateam"
slot: 31 # Mitte Zeile 3
material: SPYGLASS
# Kamera / Aufnahme minecraft-heads.com » Decoration » Camera
skull_texture: "http://textures.minecraft.net/texture/97cafa39dbee0fac4355f99a712ba699f88d6d297828b40789c361f8d8fead61"
prefix: "&b[Media]"
glass: CYAN_STAINED_GLASS_PANE
# ── Status-Anzeige ──────────────────────────────────────────────────
status:
online: "&a🟢 Online"
offline: "&c🔴 Offline"
# ── Beitritt-Datum speichern ─────────────────────────────────────────
storeJoinDate: true
# ── Backup-Einstellungen ─────────────────────────────────────────────
backup:
enabled: true
folder: backups
keep: 10 # wie viele Backups behalten werden
keep: 10
# ---------- Misc ----------
storeJoinDate: true
# ── Admin-Buttons im Settings-GUI ───────────────────────────────────
admin-buttons:
- key: reload
slot: 11
material: CLOCK
title: "&aPlugin neu laden"
lore:
- "&7Lädt Config & Daten neu"
- key: backup_now
slot: 13
material: CHEST
title: "&eBackup erstellen"
lore:
- "&7Erstellt sofort ein Backup"
- key: backup
slot: 15
material: BOOK
title: "&bBackups anzeigen"
lore:
- "&7Öffnet die Backup-Liste"
- key: edit_ranks
slot: 22
material: WRITABLE_BOOK
title: "&6Ränge bearbeiten"
lore:
- "&7Bearbeite ranks & rank-settings"
- "&7in der config.yml"

View File

@@ -0,0 +1,153 @@
prefix: "§8[§bTeam§8] §7"
# ── Subcommand-Namen (für Tab-Completion und Usage-Texte) ────────────
# Diese Namen werden als Befehle erkannt. Ändere sie nicht ohne Grund.
cmd_add: "hinzufügen"
cmd_del: "entfernen"
cmd_move: "verschieben"
cmd_settings: "einstellungen"
cmd_backup: "sicherung"
cmd_restore: "wiederherstellen"
cmd_backups: "sicherungen"
cmd_reload: "neuladen"
cmd_info: "info"
cmd_search: "suchen"
cmd_apply: "bewerben"
cmd_apply_list: "liste"
cmd_apply_accept: "annehmen"
cmd_apply_deny: "ablehnen"
cmd_log: "protokoll"
cmd_mailbox: "postfach"
# ── Allgemein ────────────────────────────────────────────────────────
no_permission: "%prefix%§cDazu hast du keine Berechtigung!"
only_player: "%prefix%§cNur Spieler können diesen Befehl benutzen!"
unknown_command: "%prefix%§cUnbekannter Befehl."
# ── /team hinzufügen / entfernen ─────────────────────────────────────
add_usage: "%prefix%§cVerwendung: /team hinzufügen <Spieler> <Rang>"
del_usage: "%prefix%§cVerwendung: /team entfernen <Spieler>"
player_added: "%prefix%§a%player% wurde zu §e%rank% §ahinzugefügt!"
player_removed: "%prefix%§a%player% wurde entfernt!"
player_not_found: "%prefix%§c%player% wurde nicht gefunden!"
# ── /team verschieben ────────────────────────────────────────────────
player_moved: "%prefix%§a%player% wurde von §e%old% §anach §e%new% §averschoben."
# ── /team suchen ─────────────────────────────────────────────────────
search_results: "%prefix%§bSuchergebnisse für §e%query%§b:"
search_no_results: "%prefix%§cKeine Teammitglieder mit §e%query% §cgefunden."
# ── /team info ───────────────────────────────────────────────────────
info_usage: "%prefix%§cVerwendung: /team info <Spieler>"
info_header: "%prefix%§bInfo für §e%player%§8:"
info_rank: "%prefix%§7Rang§8: %rank%"
info_joined: "%prefix%§7Beigetreten§8: §e%joindate%"
info_status: "%prefix%§7Status§8: %status%"
info_not_team: "%prefix%§c%player% ist kein Teammitglied."
# ── /team bewerben ───────────────────────────────────────────────────
apply_sent: "%prefix%§aDeine Bewerbung für §e%rank% §awurde eingereicht!"
apply_already: "%prefix%§cDu hast bereits eine offene Bewerbung."
apply_already_member: "%prefix%§cDu bist bereits im Team."
apply_admin_notify: "%prefix%§e%player% §7hat sich für §b%rank% §7beworben. §8(/team bewerben liste)"
apply_accepted: "%prefix%§aBewerbung von §e%player% §afür §e%rank% §aangenommen."
apply_denied: "%prefix%§cBewerbung von §e%player% §cabgelehnt."
apply_you_accepted: "%prefix%§aDeine Bewerbung für §e%rank% §awurde angenommen. Willkommen im Team!"
apply_you_denied: "%prefix%§cDeine Bewerbung für §e%rank% §cwurde leider abgelehnt."
# ── GUI: Übersichts-Titel (kommt aus config.yml → gui.title) ─────────
# team_gui_title: nicht verwendet
settings_gui_title: "%prefix%§cTeam Einstellungen"
# ── Settings-GUI-Buttons ─────────────────────────────────────────────
settings_edit_ranks: "§aRänge bearbeiten"
settings_edit_ranks_lore:
- "§7Bearbeite die Ränge in der config.yml"
- "§7und nutze §e/team einstellungen §7→ Neuladen"
settings_reload: "§cPlugin neu laden"
settings_reload_lore:
- "§7Klicke um das Plugin neu zu laden"
settings_backup: "§eBackups verwalten"
settings_backup_lore:
- "§7Öffnet die Backup-Übersicht"
# ── Rang-Seite: Navigation ────────────────────────────────────────────
nav_prev_label: "§7« %rank%"
nav_home_label: "§fHauptmenü"
nav_next_label: "%rank% §7»"
nav_prev_lore: "§7Vorherige Seite"
nav_home_lore: "§7Zurück zur Übersicht"
nav_next_lore: "§7Nächste Seite"
page_prev_label: "§7← Vorherige Seite"
page_next_label: "§7Nächste Seite →"
# ── Spieler-Kopf Tooltip ─────────────────────────────────────────────
tooltip_rank: "§7Rang§8: §e%rank%"
tooltip_joined: "§7Beigetreten§8: §e%joindate%"
# ── Nachrichten (Kopf-Klick & Postfach) ──────────────────────────────
msg_head_hint: "§e▶ Klicke zum Schreiben einer Nachricht"
msg_enter_message: "%prefix%§eNachricht an §b%player%§e tippen:"
msg_reply_prompt: "%prefix%§eAntwort an §b%player%§e tippen:"
msg_cancel_hint: "%prefix%§7Tippe §c§labbrechen §7zum Abbrechen."
msg_cancel_word: "abbrechen"
msg_cancelled: "%prefix%§7Nachricht abgebrochen."
msg_format: "%prefix%§b%sender% §8» §f%message%"
msg_sent_online: "%prefix%§aNachricht an §b%player% §ageschickt!"
msg_sent_offline: "%prefix%§e%player% §7ist offline. Nachricht wird beim nächsten Login zugestellt."
msg_offline_header: "%prefix%§eDu hast §b%count% §eoffline Nachricht(en) erhalten:"
msg_mailbox_hint: "%prefix%§7Tippe §b/team postfach §7um deine Nachrichten zu lesen."
# ── Postfach-GUI ─────────────────────────────────────────────────────
mailbox_title: "%prefix%§bPostfach"
mailbox_read_title: "%prefix%§bNachricht lesen"
mailbox_prev_page: "§7← Vorherige Seite"
mailbox_next_page: "§7Nächste Seite →"
mailbox_close: "§cSchließen"
mailbox_delete_btn: "§c§lLöschen"
mailbox_reply_btn: "§a§lAntworten an %player%"
mailbox_back_btn: "§7← Zurück"
mail_deleted: "%prefix%§7Nachricht gelöscht."
# ── Backup ───────────────────────────────────────────────────────────
backup_created: "%prefix%§aSicherung erstellt: §e%file%"
backup_restore_success: "%prefix%§aSicherung wiederhergestellt: §e%file%"
backup_not_found: "%prefix%§cSicherung nicht gefunden: §e%file%"
backups_list_title: "%prefix%§bVerfügbare Sicherungen:"
no_backups: "%prefix%§7Keine Sicherungen vorhanden."
# ── Misc ─────────────────────────────────────────────────────────────
plugin_reloaded: "%prefix%§aPlugin erfolgreich neu geladen!"
# ── Bewerbungs-GUI ────────────────────────────────────────────────────
# Admin-GUI: Bewerbungen verwalten
apply_gui_selection_title: "%prefix%§bBewerbungen verwalten"
apply_gui_detail_title: "%prefix%§bBewerbung prüfen"
# Buttons im Admin-Detail-View
apply_accepted_btn: "Annehmen"
apply_denied_btn: "Ablehnen"
apply_deny_lore: "Bewerbung ablehnen und entfernen"
# Usage-Hinweis für Spieler (kein GUI mehr)
apply_usage: "Verwendung: /team bewerben <Rang> [Grund]"
apply_gui_close: "§cSchließen"
apply_gui_close_lore: "§7Bewerbung abbrechen"
apply_gui_members_label: "Mitglieder"
apply_gui_already_member: "Du bist bereits Mitglied"
apply_gui_already_applied: "Bewerbung bereits eingereicht"
apply_gui_click_to_apply: "Klicken zum Bewerben"
apply_gui_write_text: "§e§lPersönlichen Text hinzufügen"
apply_gui_edit_text: "§e§lText bearbeiten"
apply_gui_write_lore: "§7Schreibe warum du ins Team möchtest"
apply_gui_no_text: "Kein persönlicher Text"
apply_gui_no_text_lore: "Optional du kannst auch ohne Text einreichen"
apply_gui_text_preview_title: "Dein Text:"
apply_gui_text_edit_hint: "Klicke auf das Buch um den Text zu ändern"
apply_gui_cancel: "§c§lAbbrechen"
apply_gui_cancel_lore: "§7Zurück zur Rang-Auswahl"
apply_gui_submit: "§a§lBewerbung einreichen"
apply_gui_submit_no_text_note: "Kein persönlicher Text trotzdem einreichen"
apply_gui_submit_lore: "Klicken zum Absenden"
apply_gui_type_prompt: "%prefix%§eSchreibe deinen persönlichen Text in den Chat:"
apply_gui_text_too_long: "%prefix%§cText zu lang! Maximal §e%max% §cZeichen."
apply_gui_text_saved: "%prefix%§aText gespeichert!"

View File

@@ -0,0 +1,152 @@
prefix: "§8[§bTeam§8] §7"
# ── Subcommand names (for tab-completion and usage texts) ────────────
cmd_add: "add"
cmd_del: "del"
cmd_move: "move"
cmd_settings: "settings"
cmd_backup: "backup"
cmd_restore: "restore"
cmd_backups: "backups"
cmd_reload: "reload"
cmd_info: "info"
cmd_search: "search"
cmd_apply: "apply"
cmd_apply_list: "list"
cmd_apply_accept: "accept"
cmd_apply_deny: "deny"
cmd_log: "log"
cmd_mailbox: "mailbox"
# ── General ──────────────────────────────────────────────────────────
no_permission: "%prefix%§cYou don't have permission to do that!"
only_player: "%prefix%§cOnly players can use this command!"
unknown_command: "%prefix%§cUnknown command."
# ── /team add / del ───────────────────────────────────────────────────
add_usage: "%prefix%§cUsage: /team add <player> <rank>"
del_usage: "%prefix%§cUsage: /team del <player>"
player_added: "%prefix%§a%player% was added to §e%rank%§a!"
player_removed: "%prefix%§a%player% was removed!"
player_not_found: "%prefix%§c%player% was not found!"
# ── /team move ────────────────────────────────────────────────────────
player_moved: "%prefix%§a%player% was moved from §e%old% §ato §e%new%§a."
# ── /team search ──────────────────────────────────────────────────────
search_results: "%prefix%§bSearch results for §e%query%§b:"
search_no_results: "%prefix%§cNo team members found matching §e%query%§c."
# ── /team info ────────────────────────────────────────────────────────
info_usage: "%prefix%§cUsage: /team info <player>"
info_header: "%prefix%§bInfo for §e%player%§8:"
info_rank: "%prefix%§7Rank§8: %rank%"
info_joined: "%prefix%§7Joined§8: §e%joindate%"
info_status: "%prefix%§7Status§8: %status%"
info_not_team: "%prefix%§c%player% is not a team member."
# ── /team apply ───────────────────────────────────────────────────────
apply_sent: "%prefix%§aYour application for §e%rank% §ahas been submitted!"
apply_already: "%prefix%§cYou already have a pending application."
apply_already_member: "%prefix%§cYou are already in the team."
apply_admin_notify: "%prefix%§e%player% §7applied for §b%rank%§7. §8(/team apply list)"
apply_accepted: "%prefix%§aApplication of §e%player% §afor §e%rank% §aaccepted."
apply_denied: "%prefix%§cApplication of §e%player% §cdenied."
apply_you_accepted: "%prefix%§aYour application for §e%rank% §awas accepted. Welcome to the team!"
apply_you_denied: "%prefix%§cYour application for §e%rank% §cwas unfortunately denied."
# ── GUI titles ────────────────────────────────────────────────────────
# team_gui_title: not used comes from config.yml → gui.title
settings_gui_title: "%prefix%§cTeam Settings"
# ── Settings GUI buttons ──────────────────────────────────────────────
settings_edit_ranks: "§aEdit ranks"
settings_edit_ranks_lore:
- "§7Edit ranks in config.yml"
- "§7and use §e/team settings §7→ Reload"
settings_reload: "§cReload plugin"
settings_reload_lore:
- "§7Click to reload the plugin"
settings_backup: "§eManage backups"
settings_backup_lore:
- "§7Opens the backup overview"
# ── Rank page navigation ──────────────────────────────────────────────
nav_prev_label: "§7« %rank%"
nav_home_label: "§fMain menu"
nav_next_label: "%rank% §7»"
nav_prev_lore: "§7Previous page"
nav_home_lore: "§7Back to overview"
nav_next_lore: "§7Next page"
page_prev_label: "§7← Previous page"
page_next_label: "§7Next page →"
# ── Player head tooltip ───────────────────────────────────────────────
tooltip_rank: "§7Rank§8: §e%rank%"
tooltip_joined: "§7Joined§8: §e%joindate%"
# ── Messages (head click & mailbox) ──────────────────────────────────
msg_head_hint: "§e▶ Click to send a message"
msg_enter_message: "%prefix%§eType your message to §b%player%§e:"
msg_reply_prompt: "%prefix%§eType your reply to §b%player%§e:"
msg_cancel_hint: "%prefix%§7Type §c§lcancel §7to abort."
msg_cancel_word: "cancel"
msg_cancelled: "%prefix%§7Message cancelled."
msg_format: "%prefix%§b%sender% §8» §f%message%"
msg_sent_online: "%prefix%§aMessage sent to §b%player%§a!"
msg_sent_offline: "%prefix%§e%player% §7is offline. Message will be delivered on next login."
msg_offline_header: "%prefix%§eYou have §b%count% §eoffline message(s):"
msg_mailbox_hint: "%prefix%§7Type §b/team mailbox §7to read your messages."
# ── Mailbox GUI ───────────────────────────────────────────────────────
mailbox_title: "%prefix%§bMailbox"
mailbox_read_title: "%prefix%§bRead message"
mailbox_prev_page: "§7← Previous page"
mailbox_next_page: "§7Next page →"
mailbox_close: "§cClose"
mailbox_delete_btn: "§c§lDelete"
mailbox_reply_btn: "§a§lReply to %player%"
mailbox_back_btn: "§7← Back"
mail_deleted: "%prefix%§7Message deleted."
# ── Backup ────────────────────────────────────────────────────────────
backup_created: "%prefix%§aBackup created: §e%file%"
backup_restore_success: "%prefix%§aBackup restored: §e%file%"
backup_not_found: "%prefix%§cBackup not found: §e%file%"
backups_list_title: "%prefix%§bAvailable backups:"
no_backups: "%prefix%§7No backups available."
# ── Misc ──────────────────────────────────────────────────────────────
plugin_reloaded: "%prefix%§aPlugin successfully reloaded!"
# ── Application GUI ───────────────────────────────────────────────────
# Admin GUI: manage applications
apply_gui_selection_title: "%prefix%§bManage applications"
apply_gui_detail_title: "%prefix%§bReview application"
# Buttons in the admin detail view
apply_accepted_btn: "Accept"
apply_denied_btn: "Deny"
apply_deny_lore: "Deny and remove this application"
# Usage hint for players (no GUI anymore)
apply_usage: "Usage: /team apply <rank> [reason]"
apply_gui_close: "§cClose"
apply_gui_close_lore: "§7Cancel application"
apply_gui_members_label: "Members"
apply_gui_already_member: "You are already a member"
apply_gui_already_applied: "Application already submitted"
apply_gui_click_to_apply: "Click to apply"
apply_gui_write_text: "§e§lAdd personal text"
apply_gui_edit_text: "§e§lEdit text"
apply_gui_write_lore: "§7Write why you want to join the team"
apply_gui_no_text: "No personal text"
apply_gui_no_text_lore: "Optional you can submit without a message"
apply_gui_text_preview_title: "Your message:"
apply_gui_text_edit_hint: "Click the book to change your text"
apply_gui_cancel: "§c§lCancel"
apply_gui_cancel_lore: "§7Back to rank selection"
apply_gui_submit: "§a§lSubmit application"
apply_gui_submit_no_text_note: "No personal text submit anyway"
apply_gui_submit_lore: "Click to send"
apply_gui_type_prompt: "%prefix%§eType your personal message in chat:"
apply_gui_text_too_long: "%prefix%§cText too long! Maximum §e%max% §ccharacters."
apply_gui_text_saved: "%prefix%§aText saved!"

View File

@@ -1,5 +1,5 @@
name: Team
version: 1.0.0
version: 1.0.2
main: me.viper.teamplugin.Main
api-version: 1.21
author: Viper
@@ -8,7 +8,7 @@ description: Erweiterbares Team-Plugin mit GUI, Backup/Restore und vielen Config
commands:
team:
description: Team GUI & Verwaltung
usage: /team [add|del|settings|backup|restore|backups]
usage: /team [add|del|info|reload|settings|backup|restore|backups]
permission: teamplugin.use
permissions: