Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd3567d157 | |||
| 98d08b627d | |||
| 114b0e83d7 | |||
| df4ee6c2e9 | |||
| 6b232f5cb1 | |||
| e4b2ac32ca |
2
pom.xml
2
pom.xml
@@ -5,7 +5,7 @@
|
||||
|
||||
<groupId>me.viper</groupId>
|
||||
<artifactId>Team</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.5</version>
|
||||
<name>TeamPlugin</name>
|
||||
<description>Erweitertes konfigurierbares Team-Plugin (Spigot 1.21.x)</description>
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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 me.viper.teamplugin.util.ConfigUpdater;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public class Main extends JavaPlugin {
|
||||
@@ -15,15 +16,24 @@ 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
|
||||
ConfigUpdater.update(); // fehlende Keys automatisch ergänzen
|
||||
if (!new java.io.File(getDataFolder(), "lang_de.yml").exists()) saveResource("lang_de.yml", false);
|
||||
if (!new java.io.File(getDataFolder(), "lang_en.yml").exists()) saveResource("lang_en.yml", false);
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,137 +1,507 @@
|
||||
// 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.ConfigUpdater;
|
||||
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";
|
||||
private static final String C_STATUS = "status";
|
||||
|
||||
// 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),
|
||||
Map.entry("status", C_STATUS),
|
||||
Map.entry("offline", C_STATUS)
|
||||
);
|
||||
|
||||
// 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"),
|
||||
LangManager.getCmd("cmd_status")
|
||||
);
|
||||
}
|
||||
|
||||
private static List<String> activeApplySubs(boolean isAdmin) {
|
||||
List<String> subs = new ArrayList<>();
|
||||
if (isAdmin) {
|
||||
subs.add(LangManager.getCmd("cmd_apply_list"));
|
||||
subs.add(LangManager.getCmd("cmd_apply_accept"));
|
||||
subs.add(LangManager.getCmd("cmd_apply_deny"));
|
||||
} else {
|
||||
// Players see the available ranks as completions for applying
|
||||
subs.addAll(Main.getInstance().getConfig().getStringList("ranks"));
|
||||
}
|
||||
return subs;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// COMMAND EXECUTION
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
|
||||
|
||||
if (args.length == 0) {
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage(LangManager.get("only_player"));
|
||||
return true;
|
||||
}
|
||||
TeamGUI.openTeamGUI((Player) sender);
|
||||
if (!(sender instanceof Player p)) { sender.sendMessage(LangManager.get("only_player")); return true; }
|
||||
TeamGUI.openTeamGUI(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
String sub = args[0].toLowerCase();
|
||||
String sub = resolve(args[0]);
|
||||
|
||||
if (sub.equals("add")) {
|
||||
if (!sender.hasPermission("teamplugin.admin")) {
|
||||
sender.sendMessage(LangManager.get("no_permission"));
|
||||
return true;
|
||||
}
|
||||
if (args.length < 3) {
|
||||
sender.sendMessage(LangManager.get("add_usage"));
|
||||
return true;
|
||||
}
|
||||
String name = args[1];
|
||||
String rank = args[2];
|
||||
switch (sub) {
|
||||
|
||||
List<String> ranks = Main.getInstance().getConfig().getStringList("ranks");
|
||||
if (ranks == null || !ranks.contains(rank)) {
|
||||
sender.sendMessage(Utils.color(LangManager.get("prefix")) + "§cRang §e" + rank + " §cexistiert nicht!");
|
||||
return true;
|
||||
// ── add ───────────────────────────────────────────────────
|
||||
case C_ADD -> {
|
||||
if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; }
|
||||
if (args.length < 3) { sender.sendMessage(LangManager.get("add_usage")); return true; }
|
||||
String name = args[1], rank = args[2];
|
||||
List<String> validRanks = Main.getInstance().getConfig().getStringList("ranks");
|
||||
if (!validRanks.contains(rank)) {
|
||||
sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7cRang/Rank \u00a7e" + rank + " \u00a7cdoes not exist!"); return true;
|
||||
}
|
||||
DataManager.addMember(rank, name);
|
||||
AuditLog.log(AuditLog.ADD, senderName(sender), name + " \u2192 " + rank);
|
||||
sender.sendMessage(Utils.color(Utils.replace(LangManager.get("player_added"), "%player%", name, "%rank%", rank)));
|
||||
}
|
||||
|
||||
DataManager.addMember(rank, name);
|
||||
sender.sendMessage(Utils.replace(LangManager.get("player_added"), "%player%", name, "%rank%", rank));
|
||||
return true;
|
||||
}
|
||||
// ── del ───────────────────────────────────────────────────
|
||||
case C_DEL -> {
|
||||
if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; }
|
||||
if (args.length < 2) { sender.sendMessage(LangManager.get("del_usage")); return true; }
|
||||
String name = args[1];
|
||||
String oldRank = findRank(name);
|
||||
boolean removed = DataManager.removeMember(name);
|
||||
if (removed) AuditLog.log(AuditLog.REMOVE, senderName(sender), name + " (was: " + (oldRank != null ? oldRank : "?") + ")");
|
||||
sender.sendMessage(Utils.color(removed
|
||||
? Utils.replace(LangManager.get("player_removed"), "%player%", name)
|
||||
: Utils.replace(LangManager.get("player_not_found"), "%player%", name)));
|
||||
}
|
||||
|
||||
if (sub.equals("del")) {
|
||||
if (!sender.hasPermission("teamplugin.admin")) {
|
||||
sender.sendMessage(LangManager.get("no_permission"));
|
||||
return true;
|
||||
// ── move ──────────────────────────────────────────────────
|
||||
case C_MOVE -> {
|
||||
if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; }
|
||||
if (args.length < 3) { sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7cUsage: /team " + LangManager.getCmd("cmd_move") + " <player> <rank>"); return true; }
|
||||
String name = args[1], newRank = args[2];
|
||||
List<String> validRanks = Main.getInstance().getConfig().getStringList("ranks");
|
||||
if (!validRanks.contains(newRank)) { sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7cRank \u00a7e" + newRank + " \u00a7cdoes not exist!"); return true; }
|
||||
String oldRank = findRank(name);
|
||||
if (oldRank == null) { sender.sendMessage(Utils.color(Utils.replace(LangManager.get("player_not_found"), "%player%", name))); return true; }
|
||||
if (oldRank.equals(newRank)) { sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7e" + name + " \u00a7cis already in \u00a7e" + newRank + "\u00a7c!"); return true; }
|
||||
DataManager.removeMember(name);
|
||||
DataManager.addMember(newRank, name);
|
||||
AuditLog.log(AuditLog.MOVE, senderName(sender), name + ": " + oldRank + " \u2192 " + newRank);
|
||||
sender.sendMessage(Utils.color(Utils.replace(LangManager.get("player_moved"), "%player%", name, "%old%", oldRank, "%new%", newRank)));
|
||||
}
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage(LangManager.get("del_usage"));
|
||||
return true;
|
||||
}
|
||||
String name = args[1];
|
||||
boolean removed = DataManager.removeMember(name);
|
||||
if (removed)
|
||||
sender.sendMessage(Utils.replace(LangManager.get("player_removed"), "%player%", name));
|
||||
else
|
||||
sender.sendMessage(Utils.replace(LangManager.get("player_not_found"), "%player%", name));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("settings")) {
|
||||
if (!(sender instanceof Player) || !sender.hasPermission("teamplugin.admin")) {
|
||||
sender.sendMessage(LangManager.get("no_permission"));
|
||||
return true;
|
||||
// ── settings ──────────────────────────────────────────────
|
||||
case C_SETTINGS -> {
|
||||
if (!(sender instanceof Player p) || !p.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; }
|
||||
SettingsGUI.openSettings(p);
|
||||
}
|
||||
SettingsGUI.openSettings((Player) sender);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("backup")) {
|
||||
if (!sender.hasPermission("teamplugin.admin")) {
|
||||
sender.sendMessage(LangManager.get("no_permission"));
|
||||
return true;
|
||||
// ── backup ────────────────────────────────────────────────
|
||||
case C_BACKUP -> {
|
||||
if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; }
|
||||
String file = BackupManager.createBackup();
|
||||
sender.sendMessage(Utils.color(file != null
|
||||
? Utils.replace(LangManager.get("backup_created"), "%file%", file)
|
||||
: LangManager.get("prefix") + "\u00a7cBackup failed."));
|
||||
}
|
||||
String file = BackupManager.createBackup();
|
||||
if (file != null) sender.sendMessage(Utils.replace(LangManager.get("backup_created"), "%file%", file));
|
||||
else sender.sendMessage(Utils.color(LangManager.get("prefix")) + "§cBackup fehlgeschlagen.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("restore")) {
|
||||
if (!sender.hasPermission("teamplugin.admin")) {
|
||||
sender.sendMessage(LangManager.get("no_permission"));
|
||||
return true;
|
||||
// ── restore ───────────────────────────────────────────────
|
||||
case C_RESTORE -> {
|
||||
if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; }
|
||||
if (args.length < 2) { sender.sendMessage(Utils.color(LangManager.get("prefix")) + "\u00a7cUsage: /team " + LangManager.getCmd("cmd_restore") + " <file>"); return true; }
|
||||
boolean ok = BackupManager.restoreBackup(args[1]);
|
||||
sender.sendMessage(Utils.color(ok
|
||||
? Utils.replace(LangManager.get("backup_restore_success"), "%file%", args[1])
|
||||
: Utils.replace(LangManager.get("backup_not_found"), "%file%", args[1])));
|
||||
}
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage(Utils.color(LangManager.get("prefix")) + "§cVerwendung: /team restore <dateiname>");
|
||||
return true;
|
||||
}
|
||||
String file = args[1];
|
||||
boolean ok = BackupManager.restoreBackup(file);
|
||||
if (ok) sender.sendMessage(Utils.replace(LangManager.get("backup_restore_success"), "%file%", file));
|
||||
else sender.sendMessage(Utils.replace(LangManager.get("backup_not_found"), "%file%", file));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("backups")) {
|
||||
if (!(sender instanceof Player)) {
|
||||
// Console: list backups in chat
|
||||
List<String> list = BackupManager.listBackups();
|
||||
if (list.isEmpty()) {
|
||||
sender.sendMessage(LangManager.get("no_backups"));
|
||||
// ── backups ───────────────────────────────────────────────
|
||||
case C_BACKUPS -> {
|
||||
if (sender instanceof Player p) {
|
||||
if (!p.hasPermission("teamplugin.admin")) { p.sendMessage(LangManager.get("no_permission")); return true; }
|
||||
BackupGUI.openBackupGUI(p);
|
||||
} else {
|
||||
List<String> list = BackupManager.listBackups();
|
||||
if (list.isEmpty()) { sender.sendMessage(Utils.color(LangManager.get("no_backups"))); return true; }
|
||||
sender.sendMessage(Utils.color(LangManager.get("backups_list_title")));
|
||||
list.forEach(s -> sender.sendMessage("\u00a77- \u00a7e" + s));
|
||||
}
|
||||
}
|
||||
|
||||
// ── reload ────────────────────────────────────────────────
|
||||
case C_RELOAD -> {
|
||||
if (!sender.hasPermission("teamplugin.admin")) { sender.sendMessage(LangManager.get("no_permission")); return true; }
|
||||
Main.getInstance().reloadConfig();
|
||||
ConfigUpdater.update();
|
||||
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 = PresenceManager.isShownAsOnline(infoName);
|
||||
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)));
|
||||
}
|
||||
|
||||
// ── status ────────────────────────────────────────────────
|
||||
case C_STATUS -> {
|
||||
if (!(sender instanceof Player p)) { sender.sendMessage(LangManager.get("only_player")); return true; }
|
||||
|
||||
if (findRank(p.getName()) == null) {
|
||||
p.sendMessage(Utils.color(LangManager.get("status_only_team")));
|
||||
return true;
|
||||
}
|
||||
sender.sendMessage(LangManager.get("backups_list_title"));
|
||||
list.forEach(s -> sender.sendMessage("§7- §e" + s));
|
||||
return true;
|
||||
|
||||
String mode = (args.length >= 2) ? args[1].toLowerCase() : "toggle";
|
||||
if (mode.equals("status")) {
|
||||
boolean forced = PresenceManager.isForcedOffline(p.getName());
|
||||
p.sendMessage(Utils.color(forced
|
||||
? LangManager.get("status_state_forced")
|
||||
: LangManager.get("status_state_normal")));
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean newForced = !PresenceManager.isForcedOffline(p.getName());
|
||||
|
||||
PresenceManager.setForcedOffline(p.getName(), newForced);
|
||||
p.sendMessage(Utils.color(newForced
|
||||
? LangManager.get("status_enabled")
|
||||
: LangManager.get("status_disabled")));
|
||||
}
|
||||
|
||||
// Player: open Backup GUI
|
||||
Player p = (Player) sender;
|
||||
if (!p.hasPermission("teamplugin.admin")) {
|
||||
p.sendMessage(LangManager.get("no_permission"));
|
||||
return true;
|
||||
// ── 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));
|
||||
}
|
||||
}
|
||||
BackupGUI.openBackupGUI(p);
|
||||
return true;
|
||||
|
||||
// ── 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;
|
||||
}
|
||||
|
||||
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")));
|
||||
}
|
||||
|
||||
sender.sendMessage(LangManager.get("unknown_command"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// TAB COMPLETION (shows only active-language names)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
|
||||
if (args.length == 1) return filter(activeSubs(), args[0]);
|
||||
|
||||
String sub = resolve(args[0]);
|
||||
|
||||
if (args.length == 2) return switch (sub) {
|
||||
case C_ADD -> onlinePlayers(args[1]);
|
||||
case C_DEL, C_INFO, C_MOVE -> filter(allTeamMembers(), args[1]);
|
||||
case C_RESTORE -> filter(BackupManager.listBackups(), args[1]);
|
||||
case C_SEARCH -> filter(allTeamMembers(), args[1]);
|
||||
case C_LOG -> List.of("10", "25", "50");
|
||||
case C_APPLY -> filter(activeApplySubs(sender.hasPermission("teamplugin.admin")), args[1]);
|
||||
case C_STATUS -> filter(List.of("toggle", "status"), 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";
|
||||
}
|
||||
}
|
||||
326
src/main/java/me/viper/teamplugin/gui/ApplicationGUI.java
Normal file
326
src/main/java/me/viper/teamplugin/gui/ApplicationGUI.java
Normal 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;
|
||||
}
|
||||
}
|
||||
313
src/main/java/me/viper/teamplugin/gui/MailboxGUI.java
Normal file
313
src/main/java/me/viper/teamplugin/gui/MailboxGUI.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import me.viper.teamplugin.Main;
|
||||
import me.viper.teamplugin.manager.BackupManager;
|
||||
import me.viper.teamplugin.manager.DataManager;
|
||||
import me.viper.teamplugin.manager.LangManager;
|
||||
import me.viper.teamplugin.manager.PresenceManager;
|
||||
import me.viper.teamplugin.util.ConfigUpdater;
|
||||
import me.viper.teamplugin.util.Utils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
@@ -59,6 +61,11 @@ public class SettingsGUI {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: show status toggle even if not configured in admin-buttons.
|
||||
if (!hasButtonKey(buttons, "status_toggle")) {
|
||||
inv.setItem(24, createStatusToggleItem(player));
|
||||
}
|
||||
|
||||
player.openInventory(inv);
|
||||
}
|
||||
|
||||
@@ -89,6 +96,7 @@ public class SettingsGUI {
|
||||
}
|
||||
case "reload" -> {
|
||||
Main.getInstance().reloadConfig();
|
||||
ConfigUpdater.update();
|
||||
LangManager.setup();
|
||||
DataManager.reloadData();
|
||||
LangManager.save();
|
||||
@@ -113,13 +121,32 @@ public class SettingsGUI {
|
||||
else p.sendMessage(Utils.color(LangManager.get("prefix")) + "§cBackup fehlgeschlagen.");
|
||||
p.closeInventory();
|
||||
}
|
||||
case "status_toggle" -> {
|
||||
boolean next = !PresenceManager.isForcedOffline(p.getName());
|
||||
PresenceManager.setForcedOffline(p.getName(), next);
|
||||
p.sendMessage(Utils.color(next
|
||||
? LangManager.get("status_enabled")
|
||||
: LangManager.get("status_disabled")));
|
||||
openSettings(p);
|
||||
}
|
||||
default -> {
|
||||
p.sendMessage(Utils.color(LangManager.get("prefix")) + "§cUnbekannter Button: " + key);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback click handler for the built-in status toggle item.
|
||||
if (isStatusToggleTitle(title)) {
|
||||
boolean next = !PresenceManager.isForcedOffline(p.getName());
|
||||
PresenceManager.setForcedOffline(p.getName(), next);
|
||||
p.sendMessage(Utils.color(next
|
||||
? LangManager.get("status_enabled")
|
||||
: LangManager.get("status_disabled")));
|
||||
openSettings(p);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,4 +155,37 @@ public class SettingsGUI {
|
||||
public static String getGuiTitle() {
|
||||
return LangManager.get("settings_gui_title");
|
||||
}
|
||||
|
||||
private static boolean hasButtonKey(List<?> buttons, String key) {
|
||||
if (buttons == null || key == null) return false;
|
||||
for (Object o : buttons) {
|
||||
if (!(o instanceof Map<?, ?> map)) continue;
|
||||
Object raw = map.get("key");
|
||||
if (raw instanceof String s && s.equalsIgnoreCase(key)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ItemStack createStatusToggleItem(Player player) {
|
||||
boolean forced = PresenceManager.isForcedOffline(player.getName());
|
||||
Material mat = forced ? Material.REDSTONE_TORCH : Material.LIME_DYE;
|
||||
|
||||
ItemStack item = new ItemStack(mat);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(Utils.color(LangManager.get("settings_status_toggle_title")));
|
||||
meta.setLore(List.of(
|
||||
Utils.color(Utils.replace(LangManager.get("settings_status_toggle_lore_state"), "%state%",
|
||||
forced ? LangManager.get("settings_status_state_offline") : LangManager.get("settings_status_state_online"))),
|
||||
Utils.color(LangManager.get("settings_status_toggle_lore_vanish")),
|
||||
Utils.color(LangManager.get("settings_status_toggle_lore_click"))
|
||||
));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private static boolean isStatusToggleTitle(String title) {
|
||||
return title != null && title.equals(Utils.color(LangManager.get("settings_status_toggle_title")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ 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.manager.PresenceManager;
|
||||
import me.viper.teamplugin.util.SkinResolver;
|
||||
import me.viper.teamplugin.util.Utils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
@@ -13,106 +16,497 @@ import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
import org.bukkit.profile.PlayerProfile;
|
||||
import org.bukkit.profile.PlayerTextures;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* TeamGUI – two-layer navigation with optional pagination on rank pages.
|
||||
*
|
||||
* Layer 1 Overview – one configurable icon per rank.
|
||||
* Slot read from rank-settings.<rank>.slot (config.yml).
|
||||
* Clicking opens the rank's member page.
|
||||
*
|
||||
* Layer 2 Rank Page – shows player heads.
|
||||
* If members exceed capacity → pagination.
|
||||
* Footer layout (slots 45-53):
|
||||
* 45 ← prev rank (wraps)
|
||||
* 46 ← prev page (hidden if page 0)
|
||||
* 49 home
|
||||
* 52 → next page (hidden if last page)
|
||||
* 53 → next rank (wraps)
|
||||
*
|
||||
* Page capacity:
|
||||
* rank-settings.<rank>.member_slots defined → capacity = list size
|
||||
* otherwise → gui.members_per_page (default 40)
|
||||
*
|
||||
* GUI size is always 54 and NOT configurable.
|
||||
*/
|
||||
public class TeamGUI {
|
||||
|
||||
// Zeilenpositionen für Ränge
|
||||
private static final int[] rows = {1, 2, 3, 4};
|
||||
private static final int GUI_SIZE = 54;
|
||||
|
||||
public static void openTeamGUI(Player player) {
|
||||
FileConfiguration cfg = Main.getInstance().getConfig();
|
||||
FileConfiguration data = DataManager.getData();
|
||||
private static final int[] OVERVIEW_SLOTS_FALLBACK = {13, 22, 31, 40, 11, 15, 29, 33};
|
||||
|
||||
int size = cfg.getInt("gui.size", 54);
|
||||
String title = Utils.color(cfg.getString("gui.title", "&8» &bTeam Übersicht"));
|
||||
Inventory inv = Bukkit.createInventory(null, size, title);
|
||||
public static final int NAV_PREV = 45;
|
||||
public static final int NAV_PREV_PAGE = 46;
|
||||
public static final int NAV_HOME = 49;
|
||||
public static final int NAV_NEXT_PAGE = 52;
|
||||
public static final int NAV_NEXT = 53;
|
||||
|
||||
// Hintergrund-Glas setzen
|
||||
Material bgMat = Material.valueOf(cfg.getString("gui.background", "GRAY_STAINED_GLASS_PANE"));
|
||||
ItemStack filler = new ItemStack(bgMat);
|
||||
ItemMeta fm = filler.getItemMeta();
|
||||
if (fm != null) {
|
||||
fm.setDisplayName(" ");
|
||||
filler.setItemMeta(fm);
|
||||
}
|
||||
for (int i = 0; i < inv.getSize(); i++) inv.setItem(i, filler);
|
||||
/** UUID → current page on the rank page they have open */
|
||||
private static final Map<UUID, Integer> RANK_PAGE = new HashMap<>();
|
||||
|
||||
List<String> ranks = cfg.getStringList("ranks");
|
||||
for (int i = 0; i < ranks.size() && i < rows.length; i++) {
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// PUBLIC ENTRY POINTS
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public static void openTeamGUI(Player player) { openOverview(player); }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// LAYER 1 – OVERVIEW
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public static void openOverview(Player player) {
|
||||
FileConfiguration cfg = Main.getInstance().getConfig();
|
||||
List<String> ranks = cfg.getStringList("ranks");
|
||||
|
||||
Inventory inv = Bukkit.createInventory(null, GUI_SIZE, Utils.color(getGuiTitle()));
|
||||
fillBackground(inv, cfg.getString("gui.background", "GRAY_STAINED_GLASS_PANE"));
|
||||
|
||||
for (int i = 0; i < ranks.size(); i++) {
|
||||
String rank = ranks.get(i);
|
||||
List<String> members = data.getStringList("Team." + rank);
|
||||
if (members == null) members = new ArrayList<>();
|
||||
int slot = resolveOverviewSlot(cfg, rank, i);
|
||||
if (slot < 0 || slot >= GUI_SIZE) continue;
|
||||
inv.setItem(slot, createRankOverviewBlock(rank));
|
||||
}
|
||||
|
||||
int rowStart = rows[i] * 9;
|
||||
int count = Math.min(members.size(), 9);
|
||||
if (count == 0) {
|
||||
ItemStack empty = createInfoItem("§7Kein/e " + rank, List.of("§7Keine Mitglieder"));
|
||||
inv.setItem(rowStart + 4, empty);
|
||||
continue;
|
||||
}
|
||||
|
||||
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));
|
||||
// Info-Item
|
||||
if (cfg.getBoolean("gui.info-item.enabled", false)) {
|
||||
int infoSlot = cfg.getInt("gui.info-item.slot", 49);
|
||||
if (infoSlot >= 0 && infoSlot < GUI_SIZE) {
|
||||
String skullTex = cfg.getString("gui.info-item.skull_texture", "");
|
||||
ItemStack infoItem = (skullTex != null && !skullTex.isEmpty())
|
||||
? buildCustomSkull(skullTex)
|
||||
: new ItemStack(parseMaterial(cfg.getString("gui.info-item.material", "BOOK"), Material.BOOK));
|
||||
String infoDisplay = cfg.getString("gui.info-item.display", "&eInfo");
|
||||
List<String> infoLore = cfg.getStringList("gui.info-item.lore")
|
||||
.stream().map(Utils::color).collect(Collectors.toList());
|
||||
ItemMeta infoMeta = infoItem.getItemMeta();
|
||||
if (infoMeta != null) {
|
||||
infoMeta.setDisplayName(Utils.color(infoDisplay));
|
||||
infoMeta.setLore(infoLore);
|
||||
infoItem.setItemMeta(infoMeta);
|
||||
}
|
||||
inv.setItem(infoSlot, infoItem);
|
||||
}
|
||||
}
|
||||
|
||||
player.openInventory(inv);
|
||||
}
|
||||
|
||||
private static ItemStack createPlayerHead(String name, String rank) {
|
||||
FileConfiguration cfg = Main.getInstance().getConfig();
|
||||
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;
|
||||
}
|
||||
|
||||
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
||||
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) {
|
||||
OfflinePlayer off = Bukkit.getOfflinePlayer(name);
|
||||
meta.setOwningPlayer(off);
|
||||
|
||||
// Rank aus config
|
||||
String rankDisplay = cfg.getString("rank-settings." + rank + ".display", rank);
|
||||
String rankPrefix = cfg.getString("rank-settings." + rank + ".prefix", "");
|
||||
|
||||
// Name + Prefix
|
||||
String displayName = (rankPrefix == null ? "" : rankPrefix + " ") + "&b" + name;
|
||||
meta.setDisplayName(Utils.color(displayName.trim()));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Rang
|
||||
String rankLine = Utils.replace(LangManager.get("tooltip_rank"), "%rank%", rankDisplay);
|
||||
lore.add(Utils.color(rankLine));
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// LAYER 2 – RANK PAGE (with pagination)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// Online-/Offline-Status aus config
|
||||
String statusOnline = cfg.getString("status.online", "&a🟢 Online");
|
||||
String statusOffline = cfg.getString("status.offline", "&c🔴 Offline");
|
||||
boolean isOnline = off.isOnline();
|
||||
String statusLine = isOnline ? statusOnline : statusOffline;
|
||||
lore.add(Utils.color("&7Status: " + statusLine));
|
||||
public static void openRankPage(Player player, String rank) {
|
||||
openRankPage(player, rank, 0);
|
||||
}
|
||||
|
||||
// Join-Datum, falls vorhanden
|
||||
String iso = DataManager.getData().getString("JoinDates." + name, "");
|
||||
if (iso != null && !iso.isEmpty()) {
|
||||
String joinLine = Utils.replace(LangManager.get("tooltip_joined"), "%joindate%", Utils.prettifyIso(iso));
|
||||
lore.add(Utils.color(joinLine));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
meta.setLore(lore);
|
||||
skull.setItemMeta(meta);
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Info-Item click
|
||||
if (cfg.getBoolean("gui.info-item.enabled", false)
|
||||
&& slot == cfg.getInt("gui.info-item.slot", 49)) {
|
||||
String action = cfg.getString("gui.info-item.on-click", "");
|
||||
if (action != null && !action.isEmpty()) {
|
||||
player.closeInventory();
|
||||
if (action.startsWith("cmd:")) {
|
||||
player.performCommand(action.substring(4));
|
||||
} else {
|
||||
player.sendMessage(Utils.color(action));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void handleRankPageClick(Player player, int slot, String currentRank) {
|
||||
List<String> ranks = Main.getInstance().getConfig().getStringList("ranks");
|
||||
int idx = ranks.indexOf(currentRank);
|
||||
int size = ranks.size();
|
||||
if (idx < 0 || size == 0) return;
|
||||
|
||||
int page = RANK_PAGE.getOrDefault(player.getUniqueId(), 0);
|
||||
|
||||
switch (slot) {
|
||||
case NAV_HOME -> openOverview(player);
|
||||
case NAV_PREV -> openRankPage(player, ranks.get((idx - 1 + size) % size));
|
||||
case NAV_NEXT -> openRankPage(player, ranks.get((idx + 1) % size));
|
||||
case NAV_PREV_PAGE -> openRankPage(player, currentRank, page - 1);
|
||||
case NAV_NEXT_PAGE -> openRankPage(player, currentRank, page + 1);
|
||||
default -> {
|
||||
// Member head click → message flow
|
||||
String targetName = getMemberAtSlot(currentRank, slot, page);
|
||||
if (targetName != null && !targetName.equalsIgnoreCase(player.getName())) {
|
||||
player.closeInventory();
|
||||
MessageManager.startInput(player, targetName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// HEAD → MEMBER LOOKUP
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the member name at the given slot on a rank page (accounting for page offset),
|
||||
* or null if the slot holds no member head.
|
||||
*/
|
||||
public static String getMemberAtSlot(String rank, int slot, int page) {
|
||||
FileConfiguration cfg = Main.getInstance().getConfig();
|
||||
List<String> allMembers = DataManager.getData().getStringList("Team." + rank);
|
||||
|
||||
List<Integer> configuredSlots = cfg.getIntegerList("rank-settings." + rank + ".member_slots")
|
||||
.stream().filter(s -> s >= 0 && s < 45).collect(Collectors.toList());
|
||||
int capacity = configuredSlots.isEmpty()
|
||||
? Math.min(cfg.getInt("gui.members_per_page", 40), 45)
|
||||
: configuredSlots.size();
|
||||
|
||||
int start = page * capacity;
|
||||
int end = Math.min(start + capacity, allMembers.size());
|
||||
List<String> pageMembers = allMembers.subList(start, end);
|
||||
List<Integer> memberSlots = resolveMemberSlots(cfg, rank, pageMembers.size());
|
||||
|
||||
for (int i = 0; i < pageMembers.size() && i < memberSlots.size(); i++) {
|
||||
if (memberSlots.get(i) == slot) return pageMembers.get(i);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// TITLE HELPERS
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public static String getGuiTitle() {
|
||||
return Main.getInstance().getConfig().getString("gui.title", "&8\u00bb &bTeam \u00dcbersicht");
|
||||
}
|
||||
|
||||
public static String getRankPageTitle(String rank) {
|
||||
FileConfiguration cfg = Main.getInstance().getConfig();
|
||||
String display = cfg.getString("rank-settings." + rank + ".display", rank);
|
||||
String prefix = cfg.getString("gui.rank_page_title", "&8\u00bb &bTeam &7| ");
|
||||
return prefix + display;
|
||||
}
|
||||
|
||||
public static boolean isOverviewTitle(String coloredTitle) {
|
||||
return coloredTitle.equals(Utils.color(getGuiTitle()));
|
||||
}
|
||||
|
||||
public static String extractRankFromTitle(String coloredTitle) {
|
||||
for (String rank : Main.getInstance().getConfig().getStringList("ranks")) {
|
||||
if (coloredTitle.equals(Utils.color(getRankPageTitle(rank)))) return rank;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// HEAD FACTORIES
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private static ItemStack createPlayerHeadSync(String name, String rank) {
|
||||
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
||||
if (meta == null) return skull;
|
||||
@SuppressWarnings("deprecation")
|
||||
OfflinePlayer off = Bukkit.getOfflinePlayer(name);
|
||||
meta.setOwningPlayer(off);
|
||||
applyHeadMeta(meta, name, rank);
|
||||
skull.setItemMeta(meta);
|
||||
return skull;
|
||||
}
|
||||
|
||||
private static ItemStack createPlayerHeadWithUUID(String name, String rank, UUID uuid) {
|
||||
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
||||
if (meta == null) return skull;
|
||||
OfflinePlayer off = (uuid != null) ? Bukkit.getOfflinePlayer(uuid)
|
||||
: Bukkit.getOfflinePlayer(name); //noinspection deprecation
|
||||
meta.setOwningPlayer(off);
|
||||
applyHeadMeta(meta, name, rank);
|
||||
skull.setItemMeta(meta);
|
||||
return skull;
|
||||
}
|
||||
|
||||
private static void applyHeadMeta(SkullMeta meta, String name, String rank) {
|
||||
FileConfiguration cfg = Main.getInstance().getConfig();
|
||||
String rankPrefix = cfg.getString("rank-settings." + rank + ".prefix", "");
|
||||
String displayName = rankPrefix.isEmpty() ? "&b" + name : rankPrefix + " &b" + name;
|
||||
meta.setDisplayName(Utils.color(displayName.trim()));
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
String rankDisplay = cfg.getString("rank-settings." + rank + ".display", rank);
|
||||
lore.add(Utils.color(Utils.replace(LangManager.get("tooltip_rank"), "%rank%", rankDisplay)));
|
||||
|
||||
String statusOnline = cfg.getString("status.online", "&a\uD83D\uDFE2 Online");
|
||||
String statusOffline = cfg.getString("status.offline", "&c\uD83D\uDD34 Offline");
|
||||
boolean online = PresenceManager.isShownAsOnline(name);
|
||||
lore.add(Utils.color("&7Status&8: " + (online ? statusOnline : statusOffline)));
|
||||
|
||||
String iso = DataManager.getData().getString("JoinDates." + name, "");
|
||||
if (iso != null && !iso.isEmpty())
|
||||
lore.add(Utils.color(Utils.replace(LangManager.get("tooltip_joined"),
|
||||
"%joindate%", Utils.prettifyIso(iso))));
|
||||
|
||||
lore.add("");
|
||||
lore.add(Utils.color(LangManager.get("msg_head_hint")));
|
||||
meta.setLore(lore);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// CUSTOM SKULL
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private static ItemStack buildCustomSkull(String texture) {
|
||||
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
|
||||
if (texture == null || texture.isEmpty()) return skull;
|
||||
try {
|
||||
String url = resolveTextureUrl(texture);
|
||||
if (url == null) {
|
||||
Main.getInstance().getLogger().warning("[TeamGUI] Cannot resolve texture: " + texture);
|
||||
return skull;
|
||||
}
|
||||
PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID(), "CustomSkull");
|
||||
PlayerTextures textures = profile.getTextures();
|
||||
textures.setSkin(new java.net.URL(url));
|
||||
profile.setTextures(textures);
|
||||
SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
||||
if (meta != null) { meta.setOwnerProfile(profile); skull.setItemMeta(meta); }
|
||||
} catch (Exception e) {
|
||||
Main.getInstance().getLogger().warning("[TeamGUI] Skull error: " + e.getMessage());
|
||||
}
|
||||
return skull;
|
||||
}
|
||||
|
||||
private static String resolveTextureUrl(String texture) {
|
||||
if (texture == null || texture.isEmpty()) return null;
|
||||
if (texture.startsWith("http://") || texture.startsWith("https://")) return texture;
|
||||
if (texture.matches("[0-9a-fA-F]{64}")) return "http://textures.minecraft.net/texture/" + texture;
|
||||
if (texture.startsWith("eyJ")) {
|
||||
try {
|
||||
String json = new String(Base64.getDecoder().decode(texture), StandardCharsets.UTF_8);
|
||||
int s = json.indexOf("\"url\":\"");
|
||||
if (s < 0) return null;
|
||||
s += 7;
|
||||
int e = json.indexOf('"', s);
|
||||
return (e > s) ? json.substring(s, e) : null;
|
||||
} catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// BACKGROUND / GENERIC HELPERS
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private static void fillBackground(Inventory inv, String matStr) {
|
||||
ItemStack filler = createFiller(parseMaterial(matStr, Material.GRAY_STAINED_GLASS_PANE));
|
||||
for (int i = 0; i < inv.getSize(); i++) inv.setItem(i, filler);
|
||||
}
|
||||
|
||||
private static ItemStack createFiller(Material mat) {
|
||||
ItemStack item = new ItemStack(mat);
|
||||
ItemMeta m = item.getItemMeta();
|
||||
if (m != null) { m.setDisplayName(" "); item.setItemMeta(m); }
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ItemStack createNavItem(Material mat, String name, List<String> lore) {
|
||||
ItemStack item = new ItemStack(mat);
|
||||
ItemMeta m = item.getItemMeta();
|
||||
if (m != null) { m.setDisplayName(name); m.setLore(lore); item.setItemMeta(m); }
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ItemStack createInfoItem(String name, List<String> lore) {
|
||||
ItemStack item = new ItemStack(Material.PAPER);
|
||||
ItemMeta m = item.getItemMeta();
|
||||
ItemMeta m = item.getItemMeta();
|
||||
if (m != null) {
|
||||
m.setDisplayName(Utils.color(name));
|
||||
m.setLore(lore.stream().map(Utils::color).toList());
|
||||
@@ -121,7 +515,7 @@ public class TeamGUI {
|
||||
return item;
|
||||
}
|
||||
|
||||
public static String getGuiTitle() {
|
||||
return Main.getInstance().getConfig().getString("gui.title", "&8» &bTeam Übersicht");
|
||||
private static Material parseMaterial(String s, Material fallback) {
|
||||
try { return Material.valueOf(s); } catch (Exception e) { return fallback; }
|
||||
}
|
||||
}
|
||||
49
src/main/java/me/viper/teamplugin/listener/ChatListener.java
Normal file
49
src/main/java/me/viper/teamplugin/listener/ChatListener.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
85
src/main/java/me/viper/teamplugin/manager/AuditLog.java
Normal file
85
src/main/java/me/viper/teamplugin/manager/AuditLog.java
Normal 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()));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DataManager {
|
||||
private static final String FORCE_OFFLINE_PATH = "Status.force-offline";
|
||||
|
||||
private static File file;
|
||||
private static FileConfiguration data;
|
||||
|
||||
@@ -94,6 +96,7 @@ public class DataManager {
|
||||
}
|
||||
if (removed) {
|
||||
data.set("JoinDates." + name, null);
|
||||
data.set(FORCE_OFFLINE_PATH + "." + normalizeName(name), null);
|
||||
save();
|
||||
}
|
||||
}
|
||||
@@ -177,4 +180,17 @@ public class DataManager {
|
||||
public static String getJoinDate(String name) {
|
||||
return data.getString("JoinDates." + name, "");
|
||||
}
|
||||
|
||||
public static boolean isForcedOffline(String name) {
|
||||
return data.getBoolean(FORCE_OFFLINE_PATH + "." + normalizeName(name), false);
|
||||
}
|
||||
|
||||
public static void setForcedOffline(String name, boolean forcedOffline) {
|
||||
data.set(FORCE_OFFLINE_PATH + "." + normalizeName(name), forcedOffline ? true : null);
|
||||
save();
|
||||
}
|
||||
|
||||
private static String normalizeName(String name) {
|
||||
return name == null ? "" : name.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,23 +6,95 @@ import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class LangManager {
|
||||
private static File file;
|
||||
|
||||
private static File file;
|
||||
private static FileConfiguration cfg;
|
||||
private static String loadedLanguage;
|
||||
|
||||
// ── Setup ─────────────────────────────────────────────────────────
|
||||
|
||||
public static void setup() {
|
||||
file = new File(Main.getInstance().getDataFolder(), "lang.yml");
|
||||
if (!file.exists()) {
|
||||
Main.getInstance().saveResource("lang.yml", false);
|
||||
}
|
||||
cfg = YamlConfiguration.loadConfiguration(file);
|
||||
syncLanguageFiles();
|
||||
|
||||
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 + ")");
|
||||
}
|
||||
|
||||
private static void syncLanguageFiles() {
|
||||
syncLanguageFile("lang_de.yml");
|
||||
syncLanguageFile("lang_en.yml");
|
||||
}
|
||||
|
||||
private static void syncLanguageFile(String fileName) {
|
||||
Main plugin = Main.getInstance();
|
||||
File target = new File(plugin.getDataFolder(), fileName);
|
||||
|
||||
if (!target.exists()) {
|
||||
plugin.saveResource(fileName, false);
|
||||
plugin.getLogger().info("[LangManager] Created missing file: " + fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
YamlConfiguration onDisk = YamlConfiguration.loadConfiguration(target);
|
||||
YamlConfiguration defaults = loadLangDefaults(plugin, fileName);
|
||||
if (defaults == null) return;
|
||||
|
||||
List<String> added = new ArrayList<>();
|
||||
for (String path : defaults.getKeys(true)) {
|
||||
if (defaults.isConfigurationSection(path)) continue;
|
||||
if (!onDisk.isSet(path)) {
|
||||
onDisk.set(path, defaults.get(path));
|
||||
added.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (added.isEmpty()) return;
|
||||
|
||||
try {
|
||||
onDisk.save(target);
|
||||
plugin.getLogger().info("[LangManager] " + fileName + ": " + added.size()
|
||||
+ " missing key(s) added.");
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("[LangManager] Failed to save " + fileName + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static YamlConfiguration loadLangDefaults(Main plugin, String fileName) {
|
||||
try (InputStream in = plugin.getResource(fileName)) {
|
||||
if (in == null) {
|
||||
plugin.getLogger().warning("[LangManager] Missing bundled language file: " + fileName);
|
||||
return null;
|
||||
}
|
||||
return YamlConfiguration.loadConfiguration(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("[LangManager] Failed to load bundled " + fileName + ": " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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 +102,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(); }
|
||||
}
|
||||
}
|
||||
144
src/main/java/me/viper/teamplugin/manager/MailboxManager.java
Normal file
144
src/main/java/me/viper/teamplugin/manager/MailboxManager.java
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package me.viper.teamplugin.manager;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.metadata.MetadataValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Zentraler Präsenz-Status für Team-Anzeige.
|
||||
* Spieler gelten als offline, wenn sie wirklich offline sind,
|
||||
* manuell auf offline gesetzt wurden oder im Vanish sind.
|
||||
*/
|
||||
public final class PresenceManager {
|
||||
|
||||
private PresenceManager() {}
|
||||
|
||||
public static boolean isShownAsOnline(String playerName) {
|
||||
if (playerName == null || playerName.isEmpty()) return false;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
OfflinePlayer off = Bukkit.getOfflinePlayer(playerName);
|
||||
if (!off.isOnline()) return false;
|
||||
|
||||
Player player = off.getPlayer();
|
||||
if (player == null) return false;
|
||||
|
||||
if (DataManager.isForcedOffline(playerName)) return false;
|
||||
return !isVanished(player);
|
||||
}
|
||||
|
||||
public static boolean isForcedOffline(String playerName) {
|
||||
return DataManager.isForcedOffline(playerName);
|
||||
}
|
||||
|
||||
public static void setForcedOffline(String playerName, boolean forcedOffline) {
|
||||
DataManager.setForcedOffline(playerName, forcedOffline);
|
||||
}
|
||||
|
||||
private static boolean isVanished(Player player) {
|
||||
if (player == null) return false;
|
||||
|
||||
// Common metadata keys used by vanish plugins.
|
||||
// Important: check metadata BOOLEAN value, not just key existence.
|
||||
if (hasTrueMetadata(player, "vanished")
|
||||
|| hasTrueMetadata(player, "vanish")
|
||||
|| hasTrueMetadata(player, "essentials.vanish")
|
||||
|| hasTrueMetadata(player, "supervanish")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback for plugins that toggle entity invisibility directly.
|
||||
return player.isInvisible();
|
||||
}
|
||||
|
||||
private static boolean hasTrueMetadata(Player player, String key) {
|
||||
List<MetadataValue> values = player.getMetadata(key);
|
||||
for (MetadataValue value : values) {
|
||||
if (value != null && value.asBoolean()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
135
src/main/java/me/viper/teamplugin/util/ConfigUpdater.java
Normal file
135
src/main/java/me/viper/teamplugin/util/ConfigUpdater.java
Normal file
@@ -0,0 +1,135 @@
|
||||
package me.viper.teamplugin.util;
|
||||
|
||||
import me.viper.teamplugin.Main;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Versionierte config.yml-Migration:
|
||||
* – Es werden niemals pauschal alle fehlenden Keys ergänzt.
|
||||
* – Ergänzt werden nur explizit definierte, neue Migrations-Keys pro Version.
|
||||
* – User-verwaltete Rangbereiche werden nie automatisch zurückgeschrieben.
|
||||
*/
|
||||
public class ConfigUpdater {
|
||||
|
||||
private static final String CONFIG_VERSION_PATH = "config-version";
|
||||
private static final Set<String> MIGRATION_EXCLUDED_ROOTS = Set.of(
|
||||
"ranks",
|
||||
"rank-settings"
|
||||
);
|
||||
|
||||
/**
|
||||
* Nur hier eingetragene Pfade werden bei einem Upgrade auf die jeweilige Version ergänzt.
|
||||
* Format: targetVersion -> List<configPath>
|
||||
*
|
||||
* Wichtig: Nur wirklich neue Keys eintragen, niemals bestehende User-Bereiche.
|
||||
*/
|
||||
private static final Map<Integer, List<String>> MIGRATION_ADDITIONS = Map.of(
|
||||
1, List.of()
|
||||
);
|
||||
|
||||
public static void update() {
|
||||
Main plugin = Main.getInstance();
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
|
||||
YamlConfiguration defaults = loadDefaults(plugin);
|
||||
if (defaults == null) return;
|
||||
|
||||
YamlConfiguration onDisk = YamlConfiguration.loadConfiguration(configFile);
|
||||
int defaultVersion = Math.max(1, defaults.getInt(CONFIG_VERSION_PATH, 1));
|
||||
int diskVersion = onDisk.getInt(CONFIG_VERSION_PATH, 0);
|
||||
|
||||
if (diskVersion > defaultVersion) {
|
||||
plugin.getLogger().warning("[Config] config.yml hat eine neuere Version (" + diskVersion
|
||||
+ ") als dieses Plugin (" + defaultVersion + ").");
|
||||
return;
|
||||
}
|
||||
|
||||
if (diskVersion >= defaultVersion) {
|
||||
// Bereits aktuell: keine Migration ausführen.
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> added = new ArrayList<>();
|
||||
|
||||
// Migrationen inkrementell je Versionsschritt anwenden.
|
||||
for (int targetVersion = diskVersion + 1; targetVersion <= defaultVersion; targetVersion++) {
|
||||
List<String> additions = MIGRATION_ADDITIONS.getOrDefault(targetVersion, List.of());
|
||||
for (String path : additions) {
|
||||
addPathIfMissing(plugin, onDisk, defaults, path, added);
|
||||
}
|
||||
}
|
||||
|
||||
// Nach erfolgreicher Migration Version anheben.
|
||||
onDisk.set(CONFIG_VERSION_PATH, defaultVersion);
|
||||
|
||||
try {
|
||||
onDisk.save(configFile);
|
||||
plugin.getLogger().info("[Config] Migration " + diskVersion + " -> " + defaultVersion
|
||||
+ " abgeschlossen; " + added.size() + " fehlende Einstellung(en) ergänzt.");
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("[Config] Fehler beim Speichern der config.yml: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// Neu gespeicherte Migration sofort in die Runtime laden.
|
||||
plugin.reloadConfig();
|
||||
}
|
||||
|
||||
private static YamlConfiguration loadDefaults(Main plugin) {
|
||||
try (InputStream ds = plugin.getResource("config.yml")) {
|
||||
if (ds == null) return null;
|
||||
return YamlConfiguration.loadConfiguration(new InputStreamReader(ds));
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("[Config] Fehler beim Laden der Default-config.yml: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPathIfMissing(Main plugin,
|
||||
YamlConfiguration onDisk,
|
||||
YamlConfiguration defaults,
|
||||
String path,
|
||||
List<String> added) {
|
||||
if (path == null || path.trim().isEmpty()) return;
|
||||
if (shouldSkipAutoMerge(path)) return;
|
||||
|
||||
if (!defaults.isSet(path)) {
|
||||
plugin.getLogger().warning("[Config] Migration-Key nicht in default config vorhanden: " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaults.isConfigurationSection(path)) {
|
||||
for (String relPath : defaults.getConfigurationSection(path).getKeys(true)) {
|
||||
String fullPath = path + "." + relPath;
|
||||
if (defaults.isConfigurationSection(fullPath)) continue;
|
||||
if (shouldSkipAutoMerge(fullPath)) continue;
|
||||
if (!onDisk.isSet(fullPath)) {
|
||||
onDisk.set(fullPath, defaults.get(fullPath));
|
||||
added.add(fullPath);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!onDisk.isSet(path)) {
|
||||
onDisk.set(path, defaults.get(path));
|
||||
added.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User-managed rank sections must not be recreated automatically.
|
||||
* Otherwise intentionally removed ranks reappear after /team reload.
|
||||
*/
|
||||
private static boolean shouldSkipAutoMerge(String path) {
|
||||
int dot = path.indexOf('.');
|
||||
String root = dot < 0 ? path : path.substring(0, dot);
|
||||
return MIGRATION_EXCLUDED_ROOTS.contains(root);
|
||||
}
|
||||
}
|
||||
129
src/main/java/me/viper/teamplugin/util/SkinResolver.java
Normal file
129
src/main/java/me/viper/teamplugin/util/SkinResolver.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -4,29 +4,61 @@ import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Utils {
|
||||
|
||||
// Matches &#RRGGBB hex color codes
|
||||
private static final Pattern HEX_PATTERN = Pattern.compile("&#([A-Fa-f0-9]{6})");
|
||||
|
||||
public static String formatIsoNow() {
|
||||
return DateTimeFormatter.ISO_INSTANT
|
||||
.withZone(ZoneId.systemDefault())
|
||||
.format(Instant.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an ISO timestamp into dd/MM/yyyy format.
|
||||
*/
|
||||
public static String prettifyIso(String iso) {
|
||||
if (iso == null || iso.isEmpty()) return "—";
|
||||
Instant inst = Instant.parse(iso);
|
||||
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
.withLocale(Locale.getDefault())
|
||||
.withZone(ZoneId.systemDefault());
|
||||
return fmt.format(inst);
|
||||
try {
|
||||
Instant inst = Instant.parse(iso);
|
||||
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy")
|
||||
.withLocale(Locale.getDefault())
|
||||
.withZone(ZoneId.systemDefault());
|
||||
return fmt.format(inst);
|
||||
} catch (Exception e) {
|
||||
return iso; // return as-is if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates & color codes and &#RRGGBB hex codes into Minecraft format codes.
|
||||
* Hex usage in config: &#FF5500 → §x§F§F§5§5§0§0
|
||||
*/
|
||||
public static String color(String s) {
|
||||
return s == null ? "" : s.replace("&", "§");
|
||||
if (s == null) return "";
|
||||
// Process hex colors first: &#RRGGBB → §x§R§R§G§G§B§B
|
||||
Matcher m = HEX_PATTERN.matcher(s);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (m.find()) {
|
||||
StringBuilder repl = new StringBuilder("§x");
|
||||
for (char c : m.group(1).toCharArray()) {
|
||||
repl.append('§').append(c);
|
||||
}
|
||||
m.appendReplacement(sb, repl.toString());
|
||||
}
|
||||
m.appendTail(sb);
|
||||
// Then translate remaining & codes
|
||||
return sb.toString().replace("&", "§");
|
||||
}
|
||||
|
||||
// Ersetze Platzhalter %player% %rank% %joindate%
|
||||
/**
|
||||
* Replaces placeholder pairs in a template string.
|
||||
* e.g. replace(template, "%player%", "Steve", "%rank%", "Admin")
|
||||
*/
|
||||
public static String replace(String template, String... pairs) {
|
||||
if (template == null) return "";
|
||||
String out = template;
|
||||
|
||||
@@ -1,64 +1,238 @@
|
||||
# ---------- GUI ----------
|
||||
gui:
|
||||
size: 54 # feste Größe
|
||||
title: "&8» &bTeam Übersicht"
|
||||
background: GRAY_STAINED_GLASS_PANE
|
||||
# ══════════════════════════════════════════════════════
|
||||
# TeamPlugin – config.yml
|
||||
# GUI-Größe ist IMMER 54 und NICHT konfigurierbar.
|
||||
# ══════════════════════════════════════════════════════
|
||||
|
||||
status:
|
||||
online: "&a🟢 Online"
|
||||
offline: "&c🔴 Offline"
|
||||
# Konfigurationsversion für automatische Migrationen
|
||||
# Hinweis: Nur explizit in ConfigUpdater definierte neue Keys werden ergänzt.
|
||||
# Es gibt kein pauschales Wiederherstellen fehlender Einträge.
|
||||
config-version: 1
|
||||
|
||||
# ---------- 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"
|
||||
|
||||
# ── Info-Item im Übersichts-GUI ─────────────────────────────────
|
||||
# Zeigt dem Spieler, wie er sich bewerben kann.
|
||||
# slot → Slot-Nummer im Übersichts-GUI (0-53)
|
||||
# material → Beliebiges Vanilla-Material (z. B. BOOK, PAPER, MAP)
|
||||
# display → Anzeigename (& Farbcodes erlaubt)
|
||||
# lore → Zeilenliste (& Farbcodes erlaubt)
|
||||
#
|
||||
# on-click → Aktion beim Klicken (optional):
|
||||
# Leer lassen / weglassen → kein Klick-Effekt
|
||||
# Präfix „cmd:" → Befehl ausführen (als Spieler, ohne /)
|
||||
# Beispiel: cmd:team bewerben
|
||||
# Kein Präfix → Chat-Nachricht senden
|
||||
# Beispiel: "&eBenutze /team bewerben um dich zu bewerben!"
|
||||
#
|
||||
# skull_texture → Skull statt material verwenden (optional)
|
||||
# Gleiche Formate wie bei rank-settings (URL / 64-Hex / Base64 eyJ...)
|
||||
# Wenn gesetzt, wird material ignoriert.
|
||||
info-item:
|
||||
enabled: true
|
||||
slot: 49
|
||||
material: BOOK
|
||||
skull_texture: "http://textures.minecraft.net/texture/d01afe973c5482fdc71e6aa10698833c79c437f21308ea9a1a095746ec274a0f"
|
||||
display: "&e&lBewirb dich!"
|
||||
lore:
|
||||
- "&7Möchtest du Teil unseres Teams werden?"
|
||||
- ""
|
||||
- "&eBefehl&8: &f/team bewerben"
|
||||
- ""
|
||||
- "&7Fülle die Bewerbung aus und schicke sie ab."
|
||||
- "&7Wir melden uns so schnell wie möglich!"
|
||||
on-click: "cmd:team bewerben"
|
||||
|
||||
# ── 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"
|
||||
|
||||
- key: status_toggle
|
||||
slot: 24
|
||||
material: LIME_DYE
|
||||
title: "&bStatus umschalten"
|
||||
lore:
|
||||
- "&7Schaltet deinen Team-Status"
|
||||
- "&7zwischen online/offline um"
|
||||
- "&8(Vanish wird automatisch als offline erkannt)"
|
||||
167
src/main/resources/lang_de.yml
Normal file
167
src/main/resources/lang_de.yml
Normal file
@@ -0,0 +1,167 @@
|
||||
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"
|
||||
cmd_status: "status"
|
||||
|
||||
# ── 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 status ────────────────────────────────────────────────────
|
||||
status_only_team: "%prefix%§cNur Teammitglieder können diesen Status setzen."
|
||||
status_state_forced: "%prefix%§7Du wirst aktuell als §coffline §7angezeigt."
|
||||
status_state_normal: "%prefix%§7Du wirst aktuell als §aonline §7angezeigt (sofern nicht vanished)."
|
||||
status_enabled: "%prefix%§aOffline-Modus aktiviert. Du wirst in Team-Infos als offline angezeigt."
|
||||
status_disabled: "%prefix%§aOffline-Modus deaktiviert. Du wirst wieder normal angezeigt."
|
||||
|
||||
# ── /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"
|
||||
settings_status_toggle_title: "&bStatus umschalten"
|
||||
settings_status_toggle_lore_state: "&7Aktuell: %state%"
|
||||
settings_status_toggle_lore_vanish: "&8(Bei Vanish immer offline)"
|
||||
settings_status_toggle_lore_click: "&eKlicken zum Umschalten"
|
||||
settings_status_state_online: "&aOnline"
|
||||
settings_status_state_offline: "&cOffline"
|
||||
|
||||
# ── 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!"
|
||||
166
src/main/resources/lang_en.yml
Normal file
166
src/main/resources/lang_en.yml
Normal file
@@ -0,0 +1,166 @@
|
||||
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"
|
||||
cmd_status: "status"
|
||||
|
||||
# ── 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 status ────────────────────────────────────────────────────
|
||||
status_only_team: "%prefix%§cOnly team members can change this status."
|
||||
status_state_forced: "%prefix%§7You are currently shown as §coffline§7."
|
||||
status_state_normal: "%prefix%§7You are currently shown as §aonline§7 (unless vanished)."
|
||||
status_enabled: "%prefix%§aOffline mode enabled. You will be shown as offline in team info."
|
||||
status_disabled: "%prefix%§aOffline mode disabled. You will be shown normally again."
|
||||
|
||||
# ── /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"
|
||||
settings_status_toggle_title: "&bToggle status"
|
||||
settings_status_toggle_lore_state: "&7Current: %state%"
|
||||
settings_status_toggle_lore_vanish: "&8(Always offline while vanished)"
|
||||
settings_status_toggle_lore_click: "&eClick to toggle"
|
||||
settings_status_state_online: "&aOnline"
|
||||
settings_status_state_offline: "&cOffline"
|
||||
|
||||
# ── 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!"
|
||||
@@ -1,14 +1,14 @@
|
||||
name: Team
|
||||
version: 1.0.0
|
||||
version: 1.0.5
|
||||
main: me.viper.teamplugin.Main
|
||||
api-version: 1.21
|
||||
author: Viper
|
||||
author: M_Viper
|
||||
description: Erweiterbares Team-Plugin mit GUI, Backup/Restore und vielen Config-Optionen
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user