Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-05-09 11:23:27 +02:00
parent 2c2dd9c248
commit 2c3050c87a
15 changed files with 481 additions and 436 deletions

View File

@@ -53,6 +53,14 @@ public class StatusAPI extends Plugin implements Runnable {
// Kontostand pro Spieler (UUID -> Balance), wird von StatusAPIBridge gepusht
public static final ConcurrentHashMap<UUID, Double> playerBalances = new ConcurrentHashMap<>();
// Debug-Modus (aus verify.properties)
public static boolean DEBUG = false;
/** Gibt eine Info-Meldung nur im Debug-Modus aus */
public static void debugLog(Plugin plugin, String message) {
if (DEBUG) plugin.getLogger().info(message);
}
private volatile Thread thread;
private volatile ServerSocket serverSocket;
private volatile boolean shuttingDown = false;
@@ -83,6 +91,9 @@ public class StatusAPI extends Plugin implements Runnable {
port = 9191;
}
// Debug-Modus
DEBUG = verifyProperties != null && Boolean.parseBoolean(verifyProperties.getProperty("debug", "false"));
moduleManager = new ModuleManager();
// Module in korrekter Reihenfolge registrieren
@@ -192,8 +203,9 @@ public class StatusAPI extends Plugin implements Runnable {
File file = new File(getDataFolder(), "verify.properties");
verifyProperties = new Properties();
if (file.exists()) {
try (FileInputStream fis = new FileInputStream(file)) {
verifyProperties.load(fis);
try (java.io.InputStreamReader reader = new java.io.InputStreamReader(
new FileInputStream(file), StandardCharsets.UTF_8)) {
verifyProperties.load(reader);
}
} else {
getLogger().warning("verify.properties nicht gefunden.");

View File

@@ -121,7 +121,7 @@ public class AntiBotModule implements Module, Listener {
ensureSecurityLogFile();
if (!enabled) {
this.plugin.getLogger().info("[AntiBotModule] deaktiviert via " + CONFIG_FILE_NAME);
StatusAPI.debugLog(this.plugin, "[AntiBotModule] deaktiviert via " + CONFIG_FILE_NAME);
return;
}
@@ -129,7 +129,7 @@ public class AntiBotModule implements Module, Listener {
ProxyServer.getInstance().getPluginManager().registerCommand(this.plugin, new AntiBotCommand());
ProxyServer.getInstance().getScheduler().schedule(this.plugin, this::tick, 1, 1, TimeUnit.SECONDS);
this.plugin.getLogger().info("[AntiBotModule] aktiviert. maxCps=" + maxCps
this.plugin.getLogger().fine("[AntiBotModule] aktiviert. maxCps=" + maxCps
+ ", attackStartCps=" + attackStartCps + ", ip/min=" + ipConnectionsPerMinute);
}

View File

@@ -1,12 +1,20 @@
package net.viper.status.modules.broadcast;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.ChatColor;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.BaseComponent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.ClickEvent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.TextComponent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Listener;
import net.viper.status.module.Module;
@@ -57,7 +65,7 @@ public class BroadcastModule implements Module, Listener {
loadConfig();
if (!enabled) return;
try { plugin.getProxy().getPluginManager().registerListener(plugin, this); } catch (Throwable ignored) {}
plugin.getLogger().info("[BroadcastModule] aktiviert. Format: " + format);
plugin.getLogger().fine("[BroadcastModule] aktiviert. Format: " + format);
loadSchedules();
plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS);
}
@@ -142,7 +150,7 @@ public class BroadcastModule implements Module, Listener {
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
try { p.sendMessage(components); sent++; } catch (Throwable ignored) {}
}
plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message);
StatusAPI.debugLog(plugin, "[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message);
return true;
}
@@ -279,7 +287,7 @@ public class BroadcastModule implements Module, Listener {
}
}
scheduledByClientId.putAll(loaded);
plugin.getLogger().info("[BroadcastModule] " + loaded.size() + " geplante Broadcasts aus Datei wiederhergestellt.");
plugin.getLogger().fine("[BroadcastModule] geplante Broadcasts wiederhergestellt.");
}
public boolean scheduleBroadcast(long timestampMillis, String sourceName, String message, String type,
@@ -311,7 +319,7 @@ public class BroadcastModule implements Module, Listener {
prefix, prefixColor, bracketColor, messageColor, recur);
scheduledByClientId.put(id, sb);
saveSchedules();
plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id
StatusAPI.debugLog(plugin, "[BroadcastModule] Neue geplante Nachricht registriert: " + id
+ " @ " + dateFormat.format(new Date(timestampMillis)));
return true;
}
@@ -319,7 +327,7 @@ public class BroadcastModule implements Module, Listener {
public boolean cancelScheduled(String clientScheduleId) {
if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false;
ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId);
if (removed != null) { plugin.getLogger().info("[BroadcastModule] Schedule abgebrochen: " + clientScheduleId); saveSchedules(); return true; }
if (removed != null) { StatusAPI.debugLog(plugin, "[BroadcastModule] Schedule abgebrochen: " + clientScheduleId); saveSchedules(); return true; }
return false;
}
@@ -332,7 +340,7 @@ public class BroadcastModule implements Module, Listener {
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
ScheduledBroadcast sb = entry.getValue();
if (sb.nextRunMillis <= now) {
plugin.getLogger().info("[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ")");
StatusAPI.debugLog(plugin, "[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ")");
handleBroadcast(sb.sourceName, sb.message, sb.type, "", sb.prefix, sb.prefixColor, sb.bracketColor, sb.messageColor);
if (!"none".equalsIgnoreCase(sb.recur)) {
long next = computeNextMillis(sb.nextRunMillis, sb.recur);

View File

@@ -103,7 +103,7 @@ public class ChatConfig {
config = new Configuration();
}
parseConfig();
plugin.getLogger().info("[ChatModule] " + channels.size() + " Kanäle geladen.");
plugin.getLogger().fine("[ChatModule] " + channels.size() + " Kanäle geladen.");
}
private void parseConfig() {
@@ -392,7 +392,7 @@ public class ChatConfig {
try (java.io.FileWriter fw = new java.io.FileWriter(filterFile)) {
fw.write("# StatusAPI - Wort-Blacklist\n# words:\n# - beispielwort\nwords:\n");
}
plugin.getLogger().info("[ChatModule] filter.yml erstellt.");
plugin.getLogger().fine("[ChatModule] filter.yml erstellt.");
} catch (IOException e) { plugin.getLogger().warning("[ChatModule] Konnte filter.yml nicht erstellen: " + e.getMessage()); }
return;
}

View File

@@ -113,7 +113,7 @@ public class ChatModule implements Module, Listener {
// ChatLogger
if (config.isChatlogEnabled()) {
chatLogger = new ChatLogger(plugin.getDataFolder(), logger, config.getChatlogRetentionDays());
logger.info("[ChatModule] Chat-Log aktiviert (" + config.getChatlogRetentionDays() + " Tage Aufbewahrung).");
logger.fine("[ChatModule] Chat-Log aktiviert (" + config.getChatlogRetentionDays() + " Tage Aufbewahrung).");
}
// ReportManager
@@ -144,7 +144,7 @@ public class ChatModule implements Module, Listener {
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
registerCommands();
logger.info("[ChatModule] Aktiviert " + config.getChannels().size() + " Kanäle geladen.");
logger.fine("[ChatModule] Aktiviert " + config.getChannels().size() + " Kanäle geladen.");
}
@Override

View File

@@ -62,7 +62,7 @@ public class CommandBlockerModule implements Module, Listener {
}
});
this.plugin.getLogger().info("[CommandBlocker] aktiviert (" + blocked.size() + " Commands).");
this.plugin.getLogger().fine("[CommandBlocker] aktiviert (" + blocked.size() + " Commands).");
}

View File

@@ -48,7 +48,7 @@ public class CustomCommandModule implements Module, Listener {
// Hier casten wir 'Plugin' zu 'StatusAPI', da wir wissen, dass es das ist
this.plugin = (StatusAPI) plugin;
this.plugin.getLogger().info("Lade CustomCommandModule...");
this.plugin.getLogger().fine("Lade CustomCommandModule...");
reloadConfig();
if (this.config == null) {
this.config = new Configuration();

View File

@@ -2,6 +2,7 @@ package net.viper.status.modules.economy;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Plugin;
import java.sql.*;
@@ -23,6 +24,9 @@ public class EconomyDatabase {
this.log = plugin.getLogger();
HikariConfig cfg = new HikariConfig();
// HikariCP Startup-Logs unterdrücken
java.util.logging.Logger.getLogger("com.zaxxer.hikari").setLevel(java.util.logging.Level.WARNING);
java.util.logging.Logger.getLogger("com.zaxxer.hikari.HikariDataSource").setLevel(java.util.logging.Level.WARNING);
cfg.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database
+ "?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&useUnicode=true");
cfg.setUsername(user);
@@ -67,7 +71,7 @@ public class EconomyDatabase {
" `updated` BIGINT NOT NULL" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;")) {
ps.executeUpdate();
log.info("[Economy] MySQL verbunden Tabellen bereit.");
if (StatusAPI.DEBUG) log.info("[Economy] MySQL verbunden Tabellen bereit.");
} catch (SQLException e) {
log.severe("[Economy] Tabellen-Setup (bc_player_names) fehlgeschlagen: " + e.getMessage());
}

View File

@@ -1,5 +1,6 @@
package net.viper.status.modules.economy;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Plugin;
import java.util.UUID;
@@ -64,7 +65,7 @@ public class EconomyManager {
UUID uuid = UUID.fromString(formatted);
// Für künftige Lookups speichern
db.saveNameMapping(uuid, name);
plugin.getLogger().info("[Economy] Mojang-Lookup: " + name + "" + uuid);
StatusAPI.debugLog(plugin, "[Economy] Mojang-Lookup: " + name + "" + uuid);
return uuid;
} catch (Exception e) {
plugin.getLogger().warning("[Economy] Mojang-Lookup fehlgeschlagen für " + name + ": " + e.getMessage());

View File

@@ -57,14 +57,14 @@ public class EconomyModule implements Module {
plugin.getProxy().getPluginManager().registerCommand(plugin, new PayCommand(plugin, manager));
plugin.getProxy().getPluginManager().registerCommand(plugin, new EcoAdminCommand(plugin, manager));
plugin.getLogger().info("[Economy] EconomyModule aktiviert (start-balance: " + startBal + ").");
StatusAPI.debugLog(plugin, "[Economy] EconomyModule aktiviert (start-balance: " + startBal + ").");
}
@Override
public void onDisable(Plugin plugin) {
if (database != null) {
database.close();
plugin.getLogger().info("[Economy] MySQL-Verbindung geschlossen.");
StatusAPI.debugLog(plugin, "[Economy] MySQL-Verbindung geschlossen.");
}
}

View File

@@ -1,17 +1,30 @@
package net.viper.status.modules.forum;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.ChatColor;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.CommandSender;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.ProxyServer;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.ClickEvent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.HoverEvent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.TextComponent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Command;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Listener;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.StatusAPI;
import net.md_5.bungee.event.EventHandler;
import net.viper.status.module.Module;
@@ -46,7 +59,7 @@ public class ForumBridgeModule implements Module, Listener {
public void onEnable(Plugin plugin) {
this.plugin = plugin;
loadConfig(plugin);
if (!enabled) { plugin.getLogger().info("ForumBridgeModule ist deaktiviert."); return; }
if (!enabled) { StatusAPI.debugLog(plugin, "ForumBridgeModule ist deaktiviert."); return; }
storage = new ForumNotifStorage(plugin.getDataFolder(), plugin.getLogger());
storage.load();
@@ -60,12 +73,12 @@ public class ForumBridgeModule implements Module, Listener {
}, 10, 10, TimeUnit.MINUTES);
plugin.getProxy().getScheduler().schedule(plugin, () -> storage.purgeOld(30), 1, 24, TimeUnit.HOURS);
plugin.getLogger().info("ForumBridgeModule aktiviert.");
plugin.getLogger().fine("ForumBridgeModule aktiviert.");
}
@Override
public void onDisable(Plugin plugin) {
if (storage != null) { storage.save(); plugin.getLogger().info("Forum-Benachrichtigungen gespeichert."); }
if (storage != null) { storage.save(); StatusAPI.debugLog(plugin, "Forum-Benachrichtigungen gespeichert."); }
}
private void loadConfig(Plugin plugin) {

View File

@@ -1,18 +1,32 @@
package net.viper.status.modules.serverswitcher;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.ChatColor;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.CommandSender;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.ProxyServer;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.ClickEvent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.HoverEvent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.chat.TextComponent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.config.ServerInfo;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.event.TabCompleteEvent;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Command;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Listener;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.StatusAPI;
import net.md_5.bungee.event.EventHandler;
import net.viper.status.module.Module;
@@ -57,7 +71,7 @@ public class ServerSwitcherModule implements Module {
loadConfig();
if (!enabled) {
plugin.getLogger().info("[ServerSwitcherModule] Deaktiviert.");
StatusAPI.debugLog(plugin, "[ServerSwitcherModule] Deaktiviert.");
return;
}
@@ -67,7 +81,7 @@ public class ServerSwitcherModule implements Module {
ProxyServer.getInstance().getPluginManager().registerListener(plugin,
new GoTabListener());
plugin.getLogger().info("[ServerSwitcherModule] Aktiviert. Command: /" + commandName
plugin.getLogger().fine("[ServerSwitcherModule] Aktiviert. Command: /" + commandName
+ " | Aliases: " + aliases + " | Permission: " + permission);
}

View File

@@ -18,29 +18,19 @@ import net.md_5.bungee.protocol.packet.PlayerListItem.Item;
import net.md_5.bungee.protocol.packet.PlayerListItemUpdate;
import net.viper.status.module.Module;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.*;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class TablistModule implements Module, Listener {
private static final String CONFIG_FILE = "tablist.properties";
// Leerer Skin (grauer Kopf) fuer Platzhalter-Slots selber Skin wie TAB-Plugin
// Leerer Skin (grauer Kopf) für Platzhalter-Slots
private static final net.md_5.bungee.protocol.data.Property[] EMPTY_SKIN = {
new net.md_5.bungee.protocol.data.Property(
"textures",
@@ -49,16 +39,19 @@ public class TablistModule implements Module, Listener {
)
};
private int rows = 20;
private int columns = 4;
private int total = rows * columns;
private int tabSizeMax = 80;
// Grid rows ist IMMER 20 (Minecraft-Client-Layout: N Slots → ceil(N/20) Spalten à 20 Zeilen)
private static final int ROWS = 20;
private int rows = ROWS, columns = 3, total = 60, tabSizeMax = 60;
private int configuredTabSize = 0; // 0 = auto-detect aus BungeeCord
private UUID[] fakeUuids;
// ── Config ─────────────────────────────────────────────────────────────────
// Skin-Cache (pro Spieler)
private final ConcurrentHashMap<UUID, net.md_5.bungee.protocol.data.Property[]> skinCache = new ConcurrentHashMap<>();
// Config
private boolean enabled = true;
private int updateInterval = 5;
private String layoutMode = "compact";
private String headerLine1 = "&8&m" + rep('\u2501', 53);
private String headerLine2 = " &6&lViper Network";
@@ -67,12 +60,6 @@ public class TablistModule implements Module, Listener {
private String footerLine2 = " &7Discord: &ediscord.viper-network.de &8| &7Shop: &eviper-network.de/shop";
private String footerLine3 = "&8&m" + rep('\u2501', 53);
private String colorSrvHeader = "&6&l";
// Header/Footer Layout-Modus: "classic" oder "compact"
private String layoutMode = "classic";
// Compact-Layout Header/Footer
private String compactHeader1 = "&6&lViper Network &8• &7%online% Spieler online";
private String compactHeader2 = "";
private String compactHeader3 = "";
@@ -85,70 +72,61 @@ public class TablistModule implements Module, Listener {
private boolean compactFooter1Spacer = false;
private boolean compactFooter4Spacer = false;
// Konfigurierbare Info-Eintraege (Reihenfolge aus Config)
private static class InfoEntry {
String label;
String type; // website, name, rank, server, world, time, teamspeak, custom
String value; // fuer custom und statische Werte
boolean enabled;
InfoEntry(String label, String type, String value, boolean enabled) {
this.label = label; this.type = type; this.value = value; this.enabled = enabled;
}
}
private List<InfoEntry> infoEntries = new ArrayList<>();
private String colorSrvHeader = "&6&l";
private String timeFormat = "HH:mm:ss / h:mm a";
private String timeZone = "Europe/Berlin";
private SimpleDateFormat sdf;
private List<String> serverOrder = new ArrayList<>();
private Set<String> hiddenServers = new HashSet<>();
// Rang-Reihenfolge fuer Spieler-Sortierung (hoechster Rang zuerst)
private List<String> rankOrder = new ArrayList<>();
// ── State ──────────────────────────────────────────────────────────────────
// Info-Spalte
private static class InfoEntry {
String label, type, value; boolean enabled;
InfoEntry(String l, String t, String v, boolean e) { label=l; type=t; value=v; enabled=e; }
}
private List<InfoEntry> infoEntries = new ArrayList<>();
// State
private Plugin plugin;
private ScheduledTask updateTask;
private Method sendPacketQueuedMethod;
// Spieler die bereits ADD_PLAYER erhalten haben nur noch UPDATE nötig
private final Set<UUID> initializedViewers = new HashSet<>();
// ══════════════════════════════════════════════════════════════════════════
@Override public String getName() { return "TablistModule"; }
@Override
public void onEnable(Plugin plugin) {
this.plugin = plugin;
plugin.getLogger().info("[TablistModule] Starte...");
ensureConfigExists();
loadConfig();
plugin.getLogger().info("[TablistModule] Config geladen. Layout=" + layoutMode + " enabled=" + enabled);
if (!enabled) { plugin.getLogger().info("[TablistModule] Deaktiviert."); return; }
try {
initGridSize();
fakeUuids = new UUID[total];
for (int i = 0; i < total; i++)
fakeUuids[i] = new UUID(0xFFFEDEAD00000000L, (long) i);
} catch (Exception e) {
plugin.getLogger().warning("[TablistModule] initGridSize Fehler: " + e.getMessage() + " nutze Fallback 3x20");
int fbSize = configuredTabSize > 0 ? configuredTabSize : 60;
tabSizeMax = fbSize; rows = ROWS; columns = Math.min(Math.max(3, fbSize / ROWS), 8); total = ROWS * columns;
}
initUuids();
try {
Class<?> uc = Class.forName("net.md_5.bungee.UserConnection");
sendPacketQueuedMethod = uc.getMethod("sendPacketQueued", net.md_5.bungee.protocol.DefinedPacket.class);
sendPacketQueuedMethod.setAccessible(true);
plugin.getLogger().info("[TablistModule] sendPacketQueued gefunden.");
} catch (Exception e) {
plugin.getLogger().severe("[TablistModule] sendPacketQueued nicht gefunden: " + e.getMessage());
plugin.getLogger().severe("[TablistModule] sendPacketQueued NICHT gefunden: " + e.getMessage());
return;
}
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
updateTask = ProxyServer.getInstance().getScheduler().schedule(
plugin, this::updateAll, 2L, Math.max(1, updateInterval), TimeUnit.SECONDS);
updateTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 2L, Math.max(1, updateInterval), TimeUnit.SECONDS);
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
List<String> all = new ArrayList<>(ProxyServer.getInstance().getServers().keySet());
plugin.getLogger().info("[TablistModule] Alle BungeeCord-Server: " + all);
plugin.getLogger().info("[TablistModule] Tablist-Spalten (" + columns + "x" + rows + "): " + getServerOrder());
plugin.getLogger().info("[TablistModule] Alle BungeeCord-Server: " + new ArrayList<>(ProxyServer.getInstance().getServers().keySet()));
plugin.getLogger().info("[TablistModule] Tablist-Spalten: " + getServerOrder());
recalculateGrid();
}, 3L, TimeUnit.SECONDS);
plugin.getLogger().info("[TablistModule] Aktiviert. Grid=" + columns + "x" + rows + ", Interval=" + updateInterval + "s");
plugin.getLogger().info("[TablistModule] Aktiviert. Grid=" + columns + "x" + rows + " layout=" + layoutMode);
}
@Override
@@ -161,73 +139,132 @@ public class TablistModule implements Module, Listener {
}
private void initGridSize() {
int tabSize = 80;
int tabSize = 60;
try {
for (ListenerInfo li : ProxyServer.getInstance().getConfig().getListeners()) {
try {
Object val = li.getClass().getMethod("getTabSize").invoke(li);
if (val instanceof Number && ((Number) val).intValue() > 0) {
tabSize = ((Number) val).intValue(); break;
}
try { Object v = li.getClass().getMethod("getTabSize").invoke(li);
if (v instanceof Number && ((Number)v).intValue() > 0) { tabSize = ((Number)v).intValue(); break; }
} catch (Exception ignored) {}
}
} catch (Exception ignored) {}
if (configuredTabSize > 0) tabSize = configuredTabSize; // manuell gesetzt in tablist.properties
tabSizeMax = tabSize;
rows = 20;
rows = ROWS; // immer 20 Minecraft-Client-Pflicht
boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode);
int serverCount = getServerOrder().size();
// +1 fuer Info-Spalte
int needed = 1 + serverCount;
columns = Math.max(2, Math.min(needed, tabSize / rows));
total = rows * columns;
plugin.getLogger().info("[TablistModule] tab_size=" + tabSize + " -> " + columns + "x" + rows + "=" + total);
int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount);
// Spalten = benötigte Spalten, aber max was tab-size erlaubt (tab-size/20)
columns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSize / ROWS));
total = ROWS * columns;
if (needed > tabSize / ROWS) {
plugin.getLogger().warning("[TablistModule] Nicht alle Server passen in die Tablist! "
+ "Erhöhe tab-size in der BungeeCord config.yml auf mindestens " + (needed * ROWS)
+ " (aktuell: " + tabSize + ")");
}
plugin.getLogger().info("[TablistModule] tab_size=" + tabSize + " -> " + columns + "x" + ROWS + "=" + total + " (" + serverCount + " Server)");
}
private void initUuids() {
fakeUuids = new UUID[total];
for (int i = 0; i < total; i++) fakeUuids[i] = new UUID(0xFFFEDEAD00000000L, (long) i);
}
// ── Events ─────────────────────────────────────────────────────────────────
@EventHandler public void onLogin(PostLoginEvent e) {
@EventHandler
public void onLogin(PostLoginEvent e) {
if (!enabled) return;
ProxyServer.getInstance().getScheduler().schedule(plugin,
() -> updateTablist(e.getPlayer()), 3L, TimeUnit.SECONDS);
ProxiedPlayer p = e.getPlayer();
net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p);
if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin);
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
updateTablist(p);
// Nach 2s nochmals für alle damit der neue Spieler mit Kopf erscheint
ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 2L, TimeUnit.SECONDS);
}, 2L, TimeUnit.SECONDS);
}
@EventHandler public void onSwitch(ServerSwitchEvent e) {
@EventHandler
public void onSwitch(ServerSwitchEvent e) {
if (!enabled) return;
initializedViewers.remove(e.getPlayer().getUniqueId());
ProxyServer.getInstance().getScheduler().schedule(plugin,
() -> updateTablist(e.getPlayer()), 1L, TimeUnit.SECONDS);
ProxiedPlayer switched = e.getPlayer();
// Skin sofort cachen (noch auf dem alten Server, LoginProfile noch verfügbar)
net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(switched);
if (skin != null && skin.length > 0) skinCache.put(switched.getUniqueId(), skin);
// Nach 1s: alle Fake-Slots bei allen Viewern entfernen → erzwingt frisches ADD_PLAYER mit neuem Skin
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
// Skin nochmals versuchen (jetzt auf neuem Server)
net.md_5.bungee.protocol.data.Property[] freshSkin = fetchSkin(switched);
if (freshSkin != null && freshSkin.length > 0) skinCache.put(switched.getUniqueId(), freshSkin);
// Alle Slots bei allen Viewern entfernen
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
try { removeFakeSlots(viewer); } catch (Exception ignored) {}
}
@EventHandler public void onDisconnect(PlayerDisconnectEvent e) {
// Sofort neu aufbauen (kein weiterer Delay nötig da removeFakeSlots synchron ist)
updateAll();
// Nochmal nach 2s als Sicherheit
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
net.md_5.bungee.protocol.data.Property[] s2 = fetchSkin(switched);
if (s2 != null && s2.length > 0) skinCache.put(switched.getUniqueId(), s2);
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
try { removeFakeSlots(viewer); } catch (Exception ignored) {}
}
updateAll();
}, 2L, TimeUnit.SECONDS);
}, 1L, TimeUnit.SECONDS);
}
@EventHandler
public void onDisconnect(PlayerDisconnectEvent e) {
if (!enabled) return;
initializedViewers.remove(e.getPlayer().getUniqueId());
ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 1L, TimeUnit.SECONDS);
skinCache.remove(e.getPlayer().getUniqueId());
// Erst alle Fake-Slots entfernen, dann nach kurzer Pause neu aufbauen
// So verschwindet der Kopf des Spielers zuverlässig
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
try { removeFakeSlots(viewer); } catch (Exception ignored) {}
}
ProxyServer.getInstance().getScheduler().schedule(plugin,
this::updateAll, 1L, TimeUnit.SECONDS);
}, 1L, TimeUnit.SECONDS);
}
// ── Core ───────────────────────────────────────────────────────────────────
private void updateAll() {
recalculateGrid();
// Fehlende Skins nachladen
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
if (!skinCache.containsKey(p.getUniqueId())) {
net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p);
if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin);
}
}
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) updateTablist(p);
}
private void recalculateGrid() {
boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode);
int serverCount = getServerOrder().size();
// Im compact-Modus keine Info-Spalte, alle Spalten fuer Server
boolean hasInfoCol = !"compact".equalsIgnoreCase(layoutMode);
int needed = (hasInfoCol ? 1 : 0) + serverCount;
int newColumns = Math.max(hasInfoCol ? 2 : 1, Math.min(needed, tabSizeMax / rows));
int newTotal = rows * newColumns;
int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount);
// Spalten = benötigte Spalten, aber max was tab-size erlaubt (tab-size/20)
int newColumns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSizeMax / ROWS));
int newTotal = ROWS * newColumns;
if (newColumns == columns && newTotal == total) return;
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
try { removeFakeSlots(p); } catch (Exception ignored) {}
}
initializedViewers.clear();
rows = ROWS;
columns = newColumns;
total = newTotal;
fakeUuids = new UUID[total];
for (int i = 0; i < total; i++)
fakeUuids[i] = new UUID(0xFFFEDEAD00000000L, (long) i);
plugin.getLogger().info("[TablistModule] Grid: " + columns + "x" + rows + "=" + total + " (" + serverCount + " Server, layout=" + layoutMode + ")");
initUuids();
plugin.getLogger().info("[TablistModule] Grid: " + columns + "x" + rows + "=" + total + " (" + serverCount + " Server)");
}
private void updateTablist(ProxiedPlayer viewer) {
@@ -248,17 +285,17 @@ public class TablistModule implements Module, Listener {
header = c(headerLine1) + "\n" + c(headerLine2) + "\n" + c(headerLine3);
footer = c(footerLine1) + "\n" + c(footerLine2) + "\n" + c(footerLine3);
}
viewer.setTabHeader(new TextComponent(header), new TextComponent(footer));
hideRealPlayers(viewer);
sendSlots(viewer, buildItems(viewer));
} catch (Exception ex) {
plugin.getLogger().warning("[TablistModule] Fehler fuer " + viewer.getName() + ": " + ex.getMessage());
plugin.getLogger().warning("[TablistModule] " + viewer.getName() + ": " + ex.getMessage());
}
}
private String buildCompactHeader(ProxiedPlayer viewer, String srv, String world,
String rank, String time, String balance, int online) {
// ── Header / Footer ────────────────────────────────────────────────────────
private String buildCompactHeader(ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
StringBuilder sb = new StringBuilder();
appendLine(sb, compactHeader1, false, viewer, srv, world, rank, time, balance, online);
appendLine(sb, compactHeader2, compactHeader2Spacer, viewer, srv, world, rank, time, balance, online);
@@ -266,23 +303,20 @@ public class TablistModule implements Module, Listener {
return sb.toString();
}
private String buildCompactFooter(ProxiedPlayer viewer, String srv, String world,
String rank, String time, String balance, int online) {
private String buildCompactFooter(ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
StringBuilder sb = new StringBuilder();
appendLine(sb, compactFooter1, compactFooter1Spacer, viewer, srv, world, rank, time, balance, online);
// Automatische Server-Übersicht
List<String> servers = getServerOrder();
if (!servers.isEmpty()) {
StringBuilder serverLine = new StringBuilder();
StringBuilder sLine = new StringBuilder();
for (String sName : servers) {
ServerInfo info = ProxyServer.getInstance().getServerInfo(sName);
int count = info != null ? info.getPlayers().size() : 0;
if (serverLine.length() > 0) serverLine.append(" &8| ");
serverLine.append(c(colorSrvHeader)).append(capitalize(sName))
.append(" &8\u25cf &7").append(count);
ServerInfo si = ProxyServer.getInstance().getServerInfo(sName);
int cnt = si != null ? si.getPlayers().size() : 0;
if (sLine.length() > 0) sLine.append(" &8| ");
sLine.append(c(colorSrvHeader)).append(capitalize(sName)).append(" &8\u25cf &7").append(cnt);
}
if (sb.length() > 0) sb.append("\n");
sb.append(c(serverLine.toString()));
sb.append(c(sLine.toString()));
}
appendLine(sb, compactFooter2, false, viewer, srv, world, rank, time, balance, online);
appendLine(sb, compactFooter3, false, viewer, srv, world, rank, time, balance, online);
@@ -290,53 +324,34 @@ public class TablistModule implements Module, Listener {
return sb.toString();
}
/**
* Hängt eine Zeile an:
* - spacer=true + leer → fügt eine leere Abstandszeile ein
* - spacer=false + leer → Zeile wird komplett übersprungen
* - Text vorhanden → wird immer angezeigt
*/
private void appendLine(StringBuilder sb, String line, boolean spacer,
ProxiedPlayer viewer, String srv, String world, String rank,
String time, String balance, int online) {
boolean isEmpty = line == null || line.trim().isEmpty();
if (isEmpty && !spacer) return; // überspringen
private void appendLine(StringBuilder sb, String line, boolean spacer, ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
boolean empty = line == null || line.trim().isEmpty();
if (empty && !spacer) return;
if (sb.length() > 0) sb.append("\n");
if (isEmpty) {
sb.append(" "); // Abstandszeile
} else {
sb.append(c(replacePlaceholders(line, viewer, srv, world, rank, time, balance, online)));
}
sb.append(empty ? " " : c(replacePlaceholders(line, viewer, srv, world, rank, time, balance, online)));
}
// ── Items ──────────────────────────────────────────────────────────────────
private Item[] buildItems(ProxiedPlayer viewer) {
String[] texts = new String[total];
net.md_5.bungee.protocol.data.Property[][] skins = new net.md_5.bungee.protocol.data.Property[total][];
int[] pings = new int[total];
for (int i = 0; i < total; i++) {
texts[i] = " ";
skins[i] = EMPTY_SKIN;
pings[i] = 0;
}
for (int i = 0; i < total; i++) { texts[i] = " "; skins[i] = EMPTY_SKIN; pings[i] = 0; }
// ── Spalte 0: Info (nur im classic Layout) ───────────────────────────
boolean compact = "compact".equalsIgnoreCase(layoutMode);
// Info-Spalte (nur classic)
if (!compact) {
int base = 0, row = 0;
String srv = viewer.getServer() != null ? capitalize(viewer.getServer().getInfo().getName()) : "\u2014";
String world = net.viper.status.StatusAPI.playerWorlds.getOrDefault(viewer.getUniqueId(), "world");
String rank = getRank(viewer);
String time = sdf.format(new Date());
String balance = getBalance(viewer);
int online = ProxyServer.getInstance().getOnlineCount();
boolean compactMode = "compact".equalsIgnoreCase(layoutMode);
if (!compactMode) {
String rank = getRank(viewer); String time = sdf.format(new Date());
String balance = getBalance(viewer); int online = ProxyServer.getInstance().getOnlineCount();
for (InfoEntry entry : infoEntries) {
if (!entry.enabled) continue;
if (row + 1 >= rows) break;
if (entry.label != null && !entry.label.isEmpty()) {
if (!entry.enabled || row + 1 >= rows) continue;
if (entry.label != null && !entry.label.isEmpty())
row = set(texts, base, row, c(replacePlaceholders(entry.label, viewer, srv, world, rank, time, balance, online)));
}
String val;
switch (entry.type) {
case "name": val = "&f" + viewer.getName(); break;
@@ -353,34 +368,28 @@ public class TablistModule implements Module, Listener {
}
}
// ── Server-Spieler Spalten ────────────────────────────────────────────
// Server-Spalten
List<String> servers = getServerOrder();
int startCol = compactMode ? 0 : 1;
int startCol = compact ? 0 : 1;
for (int col = startCol; col < columns && (col - startCol) < servers.size(); col++) {
base = col * rows;
row = 0;
int base = col * rows, row = 0;
String sName = servers.get(col - startCol);
row = set(texts, base, row, c(colorSrvHeader + capitalize(sName)));
ServerInfo info = ProxyServer.getInstance().getServerInfo(sName);
if (info != null) {
// Spieler nach Rang-Reihenfolge sortieren
List<ProxiedPlayer> sorted = sortPlayersByRank(new ArrayList<>(info.getPlayers()));
for (ProxiedPlayer p : sorted) {
ServerInfo si = ProxyServer.getInstance().getServerInfo(sName);
if (si != null) {
for (ProxiedPlayer p : sortPlayersByRank(new ArrayList<>(si.getPlayers()))) {
if (row >= rows) break;
String prefix = getLuckPermsPrefix(p);
String display = prefix.isEmpty()
? c("&7" + p.getName())
: c(prefix + "&r " + p.getName());
set(texts, base, row, display);
skins[base + row] = getPlayerSkin(p);
int ping = p.getPing();
pings[base + row] = ping < 0 ? 1 : ping;
set(texts, base, row, prefix.isEmpty() ? c("&7" + p.getName()) : c(prefix + "&r " + p.getName()));
// Skin aus Cache immer aktuell
net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId());
skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN;
pings[base + row] = p.getPing() < 0 ? 1 : p.getPing();
row++;
}
}
}
// Alle Slots listed=true Layout bleibt erhalten
Item[] items = new Item[total];
for (int i = 0; i < total; i++) {
Item item = new Item();
@@ -402,58 +411,33 @@ public class TablistModule implements Module, Listener {
private void hideRealPlayers(ProxiedPlayer viewer) {
if (sendPacketQueuedMethod == null) return;
try {
java.util.Collection<ProxiedPlayer> online = ProxyServer.getInstance().getPlayers();
Collection<ProxiedPlayer> online = ProxyServer.getInstance().getPlayers();
if (online.isEmpty()) return;
PlayerListItemUpdate pkt = new PlayerListItemUpdate();
pkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED));
Item[] items = new Item[online.size()];
int idx = 0;
for (ProxiedPlayer p : online) {
Item item = new Item();
item.setUuid(p.getUniqueId());
item.setListed(false);
items[idx++] = item;
Item it = new Item(); it.setUuid(p.getUniqueId()); it.setListed(false); items[idx++] = it;
}
pkt.setItems(items);
sendPacketQueuedMethod.invoke(viewer, pkt);
} catch (Exception e) {
plugin.getLogger().warning("[TablistModule] hideRealPlayers: " + e.getMessage());
}
} catch (Exception e) { plugin.getLogger().warning("[TablistModule] hideRealPlayers: " + e.getMessage()); }
}
@SuppressWarnings("unchecked")
private void sendSlots(ProxiedPlayer viewer, Item[] items) {
if (sendPacketQueuedMethod == null) return;
boolean isNew = initializedViewers.add(viewer.getUniqueId());
if (isNew) {
// Erstes Mal: ADD_PLAYER + UPDATE_DISPLAY_NAME + UPDATE_LISTED
PlayerListItemUpdate addPkt = new PlayerListItemUpdate();
addPkt.setActions(EnumSet.of(
// Immer vollständiges ADD_PLAYER einfach und zuverlässig
PlayerListItemUpdate pkt = new PlayerListItemUpdate();
pkt.setActions(EnumSet.of(
PlayerListItemUpdate.Action.ADD_PLAYER,
PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME,
PlayerListItemUpdate.Action.UPDATE_LISTED));
addPkt.setItems(items);
try { sendPacketQueuedMethod.invoke(viewer, addPkt); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] ADD_PLAYER: " + e.getMessage()); return; }
} else {
// Folgeupdate: nur DisplayName + Listed aktualisieren (kein Flackern)
PlayerListItemUpdate updPkt = new PlayerListItemUpdate();
updPkt.setActions(EnumSet.of(
PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME,
PlayerListItemUpdate.Action.UPDATE_LISTED));
updPkt.setItems(items);
try { sendPacketQueuedMethod.invoke(viewer, updPkt); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] UPDATE_DISPLAY_NAME: " + e.getMessage()); return; }
}
// Ping immer separat senden
PlayerListItemUpdate pingPkt = new PlayerListItemUpdate();
pingPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LATENCY));
pingPkt.setItems(items);
try { sendPacketQueuedMethod.invoke(viewer, pingPkt); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] UPDATE_LATENCY: " + e.getMessage()); }
PlayerListItemUpdate.Action.UPDATE_LISTED,
PlayerListItemUpdate.Action.UPDATE_LATENCY));
pkt.setItems(items);
try { sendPacketQueuedMethod.invoke(viewer, pkt); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] sendSlots: " + e.getMessage()); }
}
private void removeFakeSlots(ProxiedPlayer viewer) {
@@ -477,37 +461,59 @@ public class TablistModule implements Module, Listener {
// ── Helpers ────────────────────────────────────────────────────────────────
private int set(String[] arr, int base, int row, String text) {
if (base + row < total) arr[base + row] = text == null ? " " : text;
return row + 1;
}
private net.md_5.bungee.protocol.data.Property[] getPlayerSkin(ProxiedPlayer player) {
private net.md_5.bungee.protocol.data.Property[] fetchSkin(ProxiedPlayer player) {
try {
Object pending = player.getPendingConnection();
net.md_5.bungee.connection.LoginResult profile =
(net.md_5.bungee.connection.LoginResult)
pending.getClass().getMethod("getLoginProfile").invoke(pending);
if (profile != null && profile.getProperties() != null) return profile.getProperties();
(net.md_5.bungee.connection.LoginResult) pending.getClass().getMethod("getLoginProfile").invoke(pending);
if (profile != null && profile.getProperties() != null && profile.getProperties().length > 0)
return profile.getProperties();
} catch (Exception ignored) {}
return new net.md_5.bungee.protocol.data.Property[0];
}
private List<String> getServerOrder() {
if (!serverOrder.isEmpty()) return new ArrayList<>(serverOrder);
List<String> list = new ArrayList<>();
final String[] lobbyKey = { null };
List<String> list;
if (!serverOrder.isEmpty()) {
list = new ArrayList<>(serverOrder);
// Versteckte Server auch aus manueller Liste entfernen
list.removeIf(s -> hiddenServers.contains(s.toLowerCase()));
} else {
list = new ArrayList<>();
final String[] lobbyKey = {null};
for (String key : ProxyServer.getInstance().getServers().keySet())
if (key.equalsIgnoreCase("lobby")) { lobbyKey[0] = key; break; }
if (lobbyKey[0] != null) list.add(lobbyKey[0]);
ProxyServer.getInstance().getServers().keySet().stream()
.filter(s -> lobbyKey[0] == null || !s.equalsIgnoreCase(lobbyKey[0]))
.filter(s -> !hiddenServers.contains(s.toLowerCase()))
.sorted(String.CASE_INSENSITIVE_ORDER)
.forEach(list::add);
.sorted(String.CASE_INSENSITIVE_ORDER).forEach(list::add);
}
return list;
}
private List<ProxiedPlayer> sortPlayersByRank(List<ProxiedPlayer> players) {
if (rankOrder.isEmpty()) return players;
players.sort((a, b) -> { int ia = getRankIndex(a), ib = getRankIndex(b);
return ia != ib ? Integer.compare(ia, ib) : a.getName().compareToIgnoreCase(b.getName()); });
return players;
}
private int getRankIndex(ProxiedPlayer player) {
try {
Class<?> prov = Class.forName("net.luckperms.api.LuckPermsProvider");
Object api = prov.getMethod("get").invoke(null);
Object um = api.getClass().getMethod("getUserManager").invoke(api);
Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId());
if (usr != null) {
Object pg = usr.getClass().getMethod("getPrimaryGroup").invoke(usr);
if (pg != null) { String g = pg.toString().toLowerCase();
for (int i = 0; i < rankOrder.size(); i++) if (rankOrder.get(i).equalsIgnoreCase(g)) return i; }
}
} catch (Exception ignored) {}
return rankOrder.size();
}
private String getRank(ProxiedPlayer player) {
try {
Class<?> prov = Class.forName("net.luckperms.api.LuckPermsProvider");
@@ -546,81 +552,67 @@ public class TablistModule implements Module, Listener {
return "";
}
/**
* Sortiert Spieler nach der konfigurierten Rang-Reihenfolge.
* Spieler mit hohem Rang (Index 0 in rankOrder) kommen zuerst.
* Spieler mit unbekanntem Rang kommen ans Ende, alphabetisch sortiert.
*/
private List<ProxiedPlayer> sortPlayersByRank(List<ProxiedPlayer> players) {
if (rankOrder.isEmpty()) return players;
players.sort((a, b) -> {
int idxA = getRankIndex(a);
int idxB = getRankIndex(b);
if (idxA != idxB) return Integer.compare(idxA, idxB);
return a.getName().compareToIgnoreCase(b.getName());
});
return players;
}
/** Gibt den Index des Spielers in der rankOrder-Liste zurück (niedrig = höher). */
private int getRankIndex(ProxiedPlayer player) {
try {
Class<?> prov = Class.forName("net.luckperms.api.LuckPermsProvider");
Object api = prov.getMethod("get").invoke(null);
Object um = api.getClass().getMethod("getUserManager").invoke(api);
Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId());
if (usr != null) {
Object pg = usr.getClass().getMethod("getPrimaryGroup").invoke(usr);
if (pg != null) {
String group = pg.toString().toLowerCase();
for (int i = 0; i < rankOrder.size(); i++) {
if (rankOrder.get(i).equalsIgnoreCase(group)) return i;
}
}
}
} catch (Exception ignored) {}
return rankOrder.size(); // unbekannter Rang ans Ende
}
private static String fakeName(int i) { return String.format("~vt%03d", i); }
private static String c(String s) { return ChatColor.translateAlternateColorCodes('&', s == null ? "" : s); }
private static String capitalize(String s) { return s == null || s.isEmpty() ? s : Character.toUpperCase(s.charAt(0)) + s.substring(1); }
private static String rep(char ch, int n) { StringBuilder sb = new StringBuilder(n); for (int i=0;i<n;i++) sb.append(ch); return sb.toString(); }
private int parseInt(String s, int fb) { try { return Integer.parseInt(s == null ? "" : s.trim()); } catch (Exception e) { return fb; } }
/**
* Ersetzt alle Platzhalter in einem Text:
* %player% %rank% %server% %world% %time% %balance% %ping% %online%
*/
private String replacePlaceholders(String text, ProxiedPlayer viewer,
String srv, String world, String rank,
String time, String balance, int online) {
if (text == null) return "";
return text
.replace("%player%", viewer.getName())
.replace("%rank%", rank)
.replace("%server%", srv)
.replace("%world%", world)
.replace("%time%", time)
.replace("%balance%", balance)
.replace("%ping%", String.valueOf(viewer.getPing()))
.replace("%online%", String.valueOf(online));
}
/** Liest den Kontostand aus der StatusAPI-Economy-Map (wird von StatusAPIBridge gepusht). */
private String getBalance(ProxiedPlayer player) {
try {
java.util.Map<?, ?> balances = (java.util.Map<?, ?>) net.viper.status.StatusAPI.class
.getField("playerBalances").get(null);
Map<?,?> balances = (Map<?,?>) net.viper.status.StatusAPI.class.getField("playerBalances").get(null);
Object val = balances.get(player.getUniqueId());
if (val != null) {
double d = ((Number) val).doubleValue();
return String.format("%,.2f", d);
}
if (val != null) return String.format("%,.2f", ((Number) val).doubleValue());
} catch (Exception ignored) {}
return "0.00";
}
private String replacePlaceholders(String text, ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
if (text == null) return "";
return text.replace("%player%", viewer.getName()).replace("%rank%", rank)
.replace("%server%", srv).replace("%world%", world).replace("%time%", time)
.replace("%balance%", balance).replace("%ping%", String.valueOf(viewer.getPing()))
.replace("%online%", String.valueOf(online));
}
private int set(String[] arr, int base, int row, String text) {
if (base + row < total) arr[base + row] = text == null ? " " : text; return row + 1;
}
private static String c(String s) {
if (s == null) return "";
s = replaceHexColors(s);
return ChatColor.translateAlternateColorCodes('&', s);
}
private static String replaceHexColors(String text) {
if (text == null || (!text.contains("&#") && !text.contains("{#"))) return text;
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < text.length()) {
if (i + 7 <= text.length() && text.charAt(i) == '&' && text.charAt(i+1) == '#') {
String hex = text.substring(i+2, i+8);
if (hex.matches("[0-9a-fA-F]{6}")) {
sb.append('\u00A7').append('x');
for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch);
i += 8; continue;
}
}
if (i + 8 < text.length() && text.charAt(i) == '{' && text.charAt(i+1) == '#') {
int end = text.indexOf('}', i+2);
if (end == i+8) {
String hex = text.substring(i+2, i+8);
if (hex.matches("[0-9a-fA-F]{6}")) {
sb.append('\u00A7').append('x');
for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch);
i += 9; continue;
}
}
}
sb.append(text.charAt(i)); i++;
}
return sb.toString();
}
private static String fakeName(int i) { return String.format("~vt%03d", i); }
private static String capitalize(String s){ return s==null||s.isEmpty()?s:Character.toUpperCase(s.charAt(0))+s.substring(1); }
private static String rep(char ch, int n) { StringBuilder sb=new StringBuilder(n); for(int i=0;i<n;i++) sb.append(ch); return sb.toString(); }
private int parseInt(String s, int fb) { try{return Integer.parseInt(s==null?"":s.trim());}catch(Exception e){return fb;} }
// ── Config ─────────────────────────────────────────────────────────────────
private void ensureConfigExists() {
@@ -631,16 +623,13 @@ public class TablistModule implements Module, Listener {
String content =
"# TablistModule Konfiguration\n" +
"tablist.enabled=true\n" +
"tablist.update_interval=5\n\n" +
"# Layout-Modus: classic (Trennlinien + Info-Spalte links) oder compact (wie SecretCraft)\n" +
"tablist.layout=classic\n\n" +
"# Server-Reihenfolge (leer = Lobby zuerst, dann alphabetisch)\n" +
"tablist.server_order=\n\n" +
"# Server die NICHT angezeigt werden (kommagetrennt, leer = alle anzeigen)\n" +
"tablist.hidden_servers=\n\n" +
"# Rang-Reihenfolge fuer Spieler-Sortierung (hoechster Rang zuerst, LuckPerms Gruppenname)\n" +
"tablist.tab_size=160\n" +
"tablist.update_interval=5\n" +
"# Layout-Modus: classic oder compact\n" +
"tablist.layout=compact\n" +
"tablist.server_order=\n" +
"tablist.hidden_servers=\n" +
"tablist.rank_order=owner,mod,primo,vip,scout,bewohner\n\n" +
"# ── Classic Layout ──────────────────────────────────────────────────\n" +
"tablist.header.line1=&8&m" + sep + "\n" +
"tablist.header.line2= &6&lViper Network\n" +
"tablist.header.line3=&8&m" + sep + "\n\n" +
@@ -649,9 +638,9 @@ public class TablistModule implements Module, Listener {
"tablist.footer.line3=&8&m" + sep + "\n\n" +
"# ── Compact Layout ──────────────────────────────────────────────────\n" +
"# Platzhalter: %player% %rank% %server% %world% %time% %balance% %ping% %online%\n" +
"# spacer=true: leere Zeile = sichtbarer Abstand | spacer=false: leere Zeile = wird uebersprungen\n" +
"tablist.compact.header.line1=&6&lViper Network &8• &2Hallo, &a%player%&7! &6Schön dass du da bist!\n" +
"tablist.compact.header.line2=&dCitybuild &8• &aSurvival &8• &eMinigames &3 Für jeden etwas dabei!\n" +
"# spacer=true: leere Zeile = Abstand | spacer=false: leere Zeile = überspringen\n" +
"tablist.compact.header.line1=&6&lViper Network &8• &2Hallo, &a%player%&7!\n" +
"tablist.compact.header.line2=&dCitybuild &8• &aSurvival &8• &eMinigames\n" +
"tablist.compact.header.line2.spacer=false\n" +
"tablist.compact.header.line3=\n" +
"tablist.compact.header.line3.spacer=false\n\n" +
@@ -666,8 +655,7 @@ public class TablistModule implements Module, Listener {
"tablist.color.server_header=&6&l\n" +
"tablist.time_format=HH:mm:ss / h:mm a\n" +
"tablist.timezone=Europe/Berlin\n\n" +
"# ── Info-Spalte (nur classic Layout) ────────────────────────────────\n" +
"# Platzhalter auch hier verfuegbar: %player% %balance% %ping% %online% usw.\n" +
"# ── Info-Spalte (nur classic) ────────────────────────────────────────\n" +
"tablist.info.order=website,name,rank,server,world,time,teamspeak\n\n" +
"tablist.info.website.enabled=true\n" +
"tablist.info.website.label=&b&lWebsite:\n" +
@@ -693,62 +681,75 @@ public class TablistModule implements Module, Listener {
"tablist.info.teamspeak.type=teamspeak\n" +
"tablist.info.teamspeak.value=&fts.viper-network.de\n";
try (OutputStream out = new FileOutputStream(f)) { out.write(content.getBytes(StandardCharsets.UTF_8)); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] Config-Fehler: " + e.getMessage()); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] Config: " + e.getMessage()); }
}
private void loadConfig() {
File file = new File(plugin.getDataFolder(), CONFIG_FILE);
Properties p = new Properties();
Map<String, String> map = new LinkedHashMap<>();
if (file.exists()) {
try (FileInputStream fis = new FileInputStream(file)) {
p.load(new InputStreamReader(fis, StandardCharsets.UTF_8));
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
int eq = line.indexOf('=');
if (eq < 1) continue;
map.put(line.substring(0, eq).trim(), line.substring(eq + 1));
}
} catch (Exception e) { plugin.getLogger().warning("[TablistModule] Ladefehler: " + e.getMessage()); }
}
enabled = Boolean.parseBoolean(p.getProperty("tablist.enabled", "true"));
updateInterval = parseInt(p.getProperty("tablist.update_interval", "5"), 5);
layoutMode = p.getProperty("tablist.layout", "classic").trim().toLowerCase();
headerLine1 = p.getProperty("tablist.header.line1", headerLine1);
headerLine2 = p.getProperty("tablist.header.line2", headerLine2);
headerLine3 = p.getProperty("tablist.header.line3", headerLine3);
footerLine1 = p.getProperty("tablist.footer.line1", footerLine1);
footerLine2 = p.getProperty("tablist.footer.line2", footerLine2);
footerLine3 = p.getProperty("tablist.footer.line3", footerLine3);
compactHeader1 = p.getProperty("tablist.compact.header.line1", compactHeader1);
compactHeader2 = p.getProperty("tablist.compact.header.line2", compactHeader2);
compactHeader3 = p.getProperty("tablist.compact.header.line3", compactHeader3);
compactHeader2Spacer = Boolean.parseBoolean(p.getProperty("tablist.compact.header.line2.spacer", "false"));
compactHeader3Spacer = Boolean.parseBoolean(p.getProperty("tablist.compact.header.line3.spacer", "false"));
compactFooter1 = p.getProperty("tablist.compact.footer.line1", compactFooter1);
compactFooter2 = p.getProperty("tablist.compact.footer.line2", compactFooter2);
compactFooter3 = p.getProperty("tablist.compact.footer.line3", compactFooter3);
compactFooter4 = p.getProperty("tablist.compact.footer.line4", compactFooter4);
compactFooter1Spacer = Boolean.parseBoolean(p.getProperty("tablist.compact.footer.line1.spacer", "false"));
compactFooter4Spacer = Boolean.parseBoolean(p.getProperty("tablist.compact.footer.line4.spacer", "false"));
colorSrvHeader = p.getProperty("tablist.color.server_header", colorSrvHeader);
timeFormat = p.getProperty("tablist.time_format", timeFormat);
timeZone = p.getProperty("tablist.timezone", timeZone);
try {
sdf = new SimpleDateFormat(timeFormat);
sdf.setTimeZone(java.util.TimeZone.getTimeZone(timeZone));
} catch (Exception e) {
sdf = new SimpleDateFormat("HH:mm:ss / h:mm a");
sdf.setTimeZone(java.util.TimeZone.getTimeZone("Europe/Berlin"));
}
java.util.function.BiFunction<String,String,String> get = (k,d) -> map.getOrDefault(k,d);
configuredTabSize = parseInt(get.apply("tablist.tab_size", "0"), 0);
enabled = Boolean.parseBoolean(get.apply("tablist.enabled", "true"));
updateInterval = parseInt(get.apply("tablist.update_interval", "5"), 5);
layoutMode = get.apply("tablist.layout", "compact").trim().toLowerCase();
headerLine1 = get.apply("tablist.header.line1", headerLine1);
headerLine2 = get.apply("tablist.header.line2", headerLine2);
headerLine3 = get.apply("tablist.header.line3", headerLine3);
footerLine1 = get.apply("tablist.footer.line1", footerLine1);
footerLine2 = get.apply("tablist.footer.line2", footerLine2);
footerLine3 = get.apply("tablist.footer.line3", footerLine3);
compactHeader1 = get.apply("tablist.compact.header.line1", compactHeader1);
compactHeader2 = get.apply("tablist.compact.header.line2", compactHeader2);
compactHeader3 = get.apply("tablist.compact.header.line3", compactHeader3);
compactHeader2Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line2.spacer", "false"));
compactHeader3Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line3.spacer", "false"));
compactFooter1 = get.apply("tablist.compact.footer.line1", compactFooter1);
compactFooter2 = get.apply("tablist.compact.footer.line2", compactFooter2);
compactFooter3 = get.apply("tablist.compact.footer.line3", compactFooter3);
compactFooter4 = get.apply("tablist.compact.footer.line4", compactFooter4);
compactFooter1Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line1.spacer", "false"));
compactFooter4Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line4.spacer", "false"));
colorSrvHeader = get.apply("tablist.color.server_header", colorSrvHeader);
timeFormat = get.apply("tablist.time_format", timeFormat);
timeZone = get.apply("tablist.timezone", timeZone);
try { sdf = new SimpleDateFormat(timeFormat); sdf.setTimeZone(java.util.TimeZone.getTimeZone(timeZone)); }
catch (Exception e) { sdf = new SimpleDateFormat("HH:mm:ss / h:mm a"); }
rankOrder.clear();
String rankRaw = get.apply("tablist.rank_order", "").trim();
if (!rankRaw.isEmpty()) for (String s : rankRaw.split(",")) { String t=s.trim(); if(!t.isEmpty()) rankOrder.add(t.toLowerCase()); }
serverOrder.clear();
String raw = get.apply("tablist.server_order", "").trim();
if (!raw.isEmpty()) for (String s : raw.split(",")) { String t=s.trim(); if(!t.isEmpty()) serverOrder.add(t.toLowerCase()); }
hiddenServers.clear();
String hRaw = get.apply("tablist.hidden_servers", "").trim();
if (!hRaw.isEmpty()) for (String s : hRaw.split(",")) { String t=s.trim().toLowerCase(); if(!t.isEmpty()) hiddenServers.add(t); }
// Info-Eintraege laden
infoEntries.clear();
String orderRaw = p.getProperty("tablist.info.order",
"website,name,rank,server,world,time,teamspeak").trim();
String orderRaw = get.apply("tablist.info.order", "website,name,rank,server,world,time,teamspeak").trim();
for (String id : orderRaw.split(",")) {
id = id.trim();
if (id.isEmpty()) continue;
boolean enabled = Boolean.parseBoolean(p.getProperty("tablist.info." + id + ".enabled", "true"));
String label = p.getProperty("tablist.info." + id + ".label", "");
String type = p.getProperty("tablist.info." + id + ".type", "custom");
String value = p.getProperty("tablist.info." + id + ".value", "");
infoEntries.add(new InfoEntry(label, type, value, enabled));
id = id.trim(); if (id.isEmpty()) continue;
boolean en = Boolean.parseBoolean(get.apply("tablist.info." + id + ".enabled", "true"));
String label = get.apply("tablist.info." + id + ".label", "");
String type = get.apply("tablist.info." + id + ".type", "custom");
String value = get.apply("tablist.info." + id + ".value", "");
infoEntries.add(new InfoEntry(label, type, value, en));
}
// Fallback wenn keine Eintraege konfiguriert
if (infoEntries.isEmpty()) {
infoEntries.add(new InfoEntry("&b&lWebsite:", "website", "&fviper-network.de", true));
infoEntries.add(new InfoEntry("&b&lName:", "name", "", true));
@@ -758,19 +759,5 @@ public class TablistModule implements Module, Listener {
infoEntries.add(new InfoEntry("&b&lTime:", "time", "", true));
infoEntries.add(new InfoEntry("&b&lTeamspeak:", "teamspeak", "&fts.viper-network.de", true));
}
rankOrder.clear();
String rankRaw = p.getProperty("tablist.rank_order", "").trim();
if (!rankRaw.isEmpty())
for (String s : rankRaw.split(",")) { String t = s.trim(); if (!t.isEmpty()) rankOrder.add(t.toLowerCase()); }
serverOrder.clear();
String raw = p.getProperty("tablist.server_order", "").trim();
if (!raw.isEmpty())
for (String s : raw.split(",")) { String t = s.trim(); if (!t.isEmpty()) serverOrder.add(t.toLowerCase()); }
hiddenServers.clear();
String hiddenRaw = p.getProperty("tablist.hidden_servers", "").trim();
if (!hiddenRaw.isEmpty())
for (String s : hiddenRaw.split(",")) { String t = s.trim().toLowerCase(); if (!t.isEmpty()) hiddenServers.add(t); }
}
}

View File

@@ -1,10 +1,16 @@
package net.viper.status.modules.verify;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.ChatColor;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.CommandSender;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.ProxyServer;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Command;
import net.viper.status.StatusAPI;
import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.module.Module;
@@ -38,7 +44,7 @@ public class VerifyModule implements Module {
public void onEnable(Plugin plugin) {
loadConfig(plugin);
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand());
plugin.getLogger().info("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
plugin.getLogger().fine("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
}
@Override
@@ -56,7 +62,7 @@ public class VerifyModule implements Module {
if (in == null) { plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR."); return; }
byte[] buffer = new byte[1024]; int length;
while ((length = in.read(buffer)) > 0) out.write(buffer, 0, length);
plugin.getLogger().info("Konfigurationsdatei '" + fileName + "' erstellt.");
StatusAPI.debugLog(plugin, "Konfigurationsdatei '" + fileName + "' erstellt.");
} catch (Exception e) { plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage()); return; }
}

View File

@@ -1,6 +1,6 @@
name: StatusAPI
main: net.viper.status.StatusAPI
version: 4.1.1
version: 4.1.0
author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung