diff --git a/StatusAPI/src/main/java/net/viper/status/StatusAPI.java b/StatusAPI/src/main/java/net/viper/status/StatusAPI.java index f1ed9a9..49443f1 100644 --- a/StatusAPI/src/main/java/net/viper/status/StatusAPI.java +++ b/StatusAPI/src/main/java/net/viper/status/StatusAPI.java @@ -50,6 +50,9 @@ public class StatusAPI extends Plugin implements Runnable { // Welt pro Spieler (UUID -> Weltname), wird von StatusAPIBridge gepusht public static final ConcurrentHashMap playerWorlds = new ConcurrentHashMap<>(); + // Kontostand pro Spieler (UUID -> Balance), wird von StatusAPIBridge gepusht + public static final ConcurrentHashMap playerBalances = new ConcurrentHashMap<>(); + private volatile Thread thread; private volatile ServerSocket serverSocket; private volatile boolean shuttingDown = false; @@ -605,6 +608,8 @@ public class StatusAPI extends Plugin implements Runnable { try { double newBal = Double.parseDouble(balStr); ecoModUpd.getManager().setBalance(ecoUpdUuid, newBal); + // Auch in der Tablist-Map speichern + playerBalances.put(ecoUpdUuid, newBal); } catch (NumberFormatException ignored) {} } // Stats-Felder (total_earned etc.) im Cache aktualisieren, falls vorhanden diff --git a/StatusAPI/src/main/java/net/viper/status/modules/tablist/TablistModule.java b/StatusAPI/src/main/java/net/viper/status/modules/tablist/TablistModule.java index 9d3b96d..f5e4037 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/tablist/TablistModule.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/tablist/TablistModule.java @@ -36,30 +36,24 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; -/** - * TablistModule fuer StatusAPI (BungeeCord 1.19.3+). - * - * Liest die tab_size aus der BungeeCord-Konfiguration und berechnet - * ROWS/COLUMNS dynamisch: - * tab_size=60 -> 3 Spalten x 20 Zeilen - * tab_size=80 -> 4 Spalten x 20 Zeilen - * - * Layout: - * Spalte 0 = Info (Website / Name / Rank / Server / World / Time / TS) - * Spalte 1 = Spieler auf Server 1 (Lobby) - * Spalte 2 = Spieler auf Server 2 - * Spalte 3 = Spieler auf Server 3 (nur bei tab_size=80) - */ public class TablistModule implements Module, Listener { private static final String CONFIG_FILE = "tablist.properties"; - // Wird beim Start dynamisch aus BungeeCord tab_size ermittelt - private int rows = 20; - private int columns = 4; - private int total = rows * columns; + // Leerer Skin (grauer Kopf) fuer Platzhalter-Slots – selber Skin wie TAB-Plugin + private static final net.md_5.bungee.protocol.data.Property[] EMPTY_SKIN = { + new net.md_5.bungee.protocol.data.Property( + "textures", + "ewogICJ0aW1lc3RhbXAiIDogMTY0NDcwNTExNjQ2OCwKICAicHJvZmlsZUlkIiA6ICJmZDQ3Y2I4YjgzNjQ0YmY3YWIyYmUxODZkYjI1ZmMwZCIsCiAgInByb2ZpbGVOYW1lIiA6ICJDVUNGTDEyIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2ZmOWJiOWU1NjEyNWM4MjI3Yjk0YmJkYTlmNmUwZjg2MjkzMWMyMjkyNTViYThmMTIwNWQxM2M0NGMxYmI1NjEiCiAgICB9CiAgfQp9", + "D24yzbg+aBETxe5e+acQR8xJwBkhf8+CdkNYi1ufu3NgXk6YK67dIij8o3QtMx/y3rR6xupRq7bKHUGGgkw+joCC/mtG6yDdLbD32s//VAhA+VVDbIQq/CJrJ8oYarerElTjOF08zxQCw8n97cfI10gkoZvdTDouRfTfQYIIo6vvG9kTGyAJv7mIriTvxE/nwP3m6WlwRmtKWOqDhiMRNoWwo9btCp5JTZR9HVFaZdsNQvh6gUmjBqHoKtr/xWOVveEhQ5mc8WZh0dAiiC3Astfr0VIx7HW1+xNu+Z7xvRMgbZ+SbKuRwotW2KHCN+BDymTbiQ3GBljjXDjwFao0sBHQ24DjafWQcuEEWNsDnhDHtmG3tKdvGQbZ1bYhh97EjRYKXG+eZKMrFGG4jr9oCg0JD3JMBc88Z0mJWyKzPF9B+klFocmrFBF/UgkQnzkNShfkpC6RjUfCymrnAFAoV6XBcznbKQzyKKAMeNE3LPFZ3iS2Tygbrqo2Sjmq9zGpjva04RxWHJ1oeKzROQkge0z96AOO7ChTFTXqnNnAjdkfW2TjK7pSIwS0vMGsUgm1C/amzMpZdJuI0FXFEzz1jhFi5cdwHXSQY1gVpa4VTLNQvu1xgcnbOVJaV0Ty+AebI2s6CLt6OcpI3QKY+KPlITuwj5HydMiQvfYldhiHPjc=" + ) + }; + + private int rows = 20; + private int columns = 4; + private int total = rows * columns; + private int tabSizeMax = 80; - // Fake-UUIDs – werden nach Bestimmung von total initialisiert private UUID[] fakeUuids; // ── Config ───────────────────────────────────────────────────────────────── @@ -73,27 +67,50 @@ 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 labelWebsite = "&b&lWebsite:"; - private String valueWebsite = "&fviper-network.de"; - private String labelName = "&b&lName:"; - private String labelRank = "&b&lRank:"; - private String labelServer = "&b&lServer:"; - private String labelWorld = "&b&lWorld:"; - private String labelTime = "&b&lTime:"; - private String labelTeamspeak = "&b&lTeamspeak:"; - private String valueTeamspeak = "&fts.viper-network.de"; private String colorSrvHeader = "&6&l"; - private String timeFormat = "HH:mm:ss / h:mm a"; + // 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 = ""; + private boolean compactHeader2Spacer = false; + private boolean compactHeader3Spacer = false; + private String compactFooter1 = ""; + private String compactFooter2 = "&7Zeit: &f%time% &8| &7Spieler: &f%online% &8| &7Ping: &f%ping%ms"; + private String compactFooter3 = "&7Kontostand: &a$%balance% &8| &7Server: &f%server% &8| &7Welt: &f%world%"; + private String compactFooter4 = ""; + 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 infoEntries = new ArrayList<>(); + + private String timeFormat = "HH:mm:ss / h:mm a"; + private String timeZone = "Europe/Berlin"; private SimpleDateFormat sdf; - private List serverOrder = new ArrayList<>(); + private List serverOrder = new ArrayList<>(); private Set hiddenServers = new HashSet<>(); + // Rang-Reihenfolge fuer Spieler-Sortierung (hoechster Rang zuerst) + private List rankOrder = new ArrayList<>(); // ── State ────────────────────────────────────────────────────────────────── private Plugin plugin; private ScheduledTask updateTask; private Method sendPacketQueuedMethod; - private int tabSizeMax = 80; // maximale Slots laut BungeeCord tab_size + // Spieler die bereits ADD_PLAYER erhalten haben – nur noch UPDATE nötig + private final Set initializedViewers = new HashSet<>(); // ══════════════════════════════════════════════════════════════════════════ @@ -106,20 +123,15 @@ public class TablistModule implements Module, Listener { loadConfig(); if (!enabled) { plugin.getLogger().info("[TablistModule] Deaktiviert."); return; } - // Tab-Size aus BungeeCord auslesen und ROWS/COLUMNS berechnen initGridSize(); - // Fake-UUIDs initialisieren fakeUuids = new UUID[total]; - for (int i = 0; i < total; i++) { + for (int i = 0; i < total; i++) fakeUuids[i] = new UUID(0xFFFEDEAD00000000L, (long) i); - } - // Reflection-Cache fuer sendPacketQueued try { - Class ucClass = Class.forName("net.md_5.bungee.UserConnection"); - sendPacketQueuedMethod = ucClass.getMethod("sendPacketQueued", - net.md_5.bungee.protocol.DefinedPacket.class); + Class uc = Class.forName("net.md_5.bungee.UserConnection"); + sendPacketQueuedMethod = uc.getMethod("sendPacketQueued", net.md_5.bungee.protocol.DefinedPacket.class); sendPacketQueuedMethod.setAccessible(true); } catch (Exception e) { plugin.getLogger().severe("[TablistModule] sendPacketQueued nicht gefunden: " + e.getMessage()); @@ -130,70 +142,47 @@ public class TablistModule implements Module, Listener { updateTask = ProxyServer.getInstance().getScheduler().schedule( plugin, this::updateAll, 2L, Math.max(1, updateInterval), TimeUnit.SECONDS); - // Server-Erkennung loggen ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { List 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()); }, 3L, TimeUnit.SECONDS); - plugin.getLogger().info("[TablistModule] Aktiviert. Grid=" + columns + "x" + rows - + " (total=" + total + "), Interval=" + updateInterval + "s"); + plugin.getLogger().info("[TablistModule] Aktiviert. Grid=" + columns + "x" + rows + ", Interval=" + updateInterval + "s"); } @Override public void onDisable(Plugin plugin) { if (updateTask != null) { updateTask.cancel(); updateTask = null; } for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - try { - removeFakeSlots(p); - p.setTabHeader(new TextComponent(""), new TextComponent("")); - } catch (Exception ignored) {} + try { removeFakeSlots(p); p.setTabHeader(new TextComponent(""), new TextComponent("")); } + catch (Exception ignored) {} } } - /** - * Liest tab_size aus dem ersten BungeeCord-Listener und berechnet ROWS/COLUMNS. - * Minecraft zeigt immer 20 Zeilen, die Spaltenanzahl ergibt sich aus tab_size/20. - * Fallback: 4 Spalten x 20 Zeilen = 80. - */ private void initGridSize() { int tabSize = 80; try { for (ListenerInfo li : ProxyServer.getInstance().getConfig().getListeners()) { - // getTabSize() nicht im API-Interface, daher Reflection try { - java.lang.reflect.Method m = li.getClass().getMethod("getTabSize"); - Object val = m.invoke(li); + Object val = li.getClass().getMethod("getTabSize").invoke(li); if (val instanceof Number && ((Number) val).intValue() > 0) { - tabSize = ((Number) val).intValue(); - break; + tabSize = ((Number) val).intValue(); break; } } catch (Exception ignored) {} } - } catch (Exception e) { - plugin.getLogger().warning("[TablistModule] Konnte tab_size nicht lesen, nutze 80."); - } - rows = 20; + } catch (Exception ignored) {} tabSizeMax = tabSize; - // Beim Start noch keine Server bekannt → maxColumns als Startwert, recalculateGrid korrigiert später - columns = Math.max(2, tabSize / rows); - total = rows * columns; - - // Sofort korrekt berechnen falls Server bereits bekannt + rows = 20; int serverCount = getServerOrder().size(); - if (serverCount > 0) { - int needed = 1 + serverCount; - columns = Math.max(2, Math.min(needed, tabSize / rows)); - total = rows * columns; - } - plugin.getLogger().info("[TablistModule] BungeeCord tab_size=" + tabSize - + " -> " + columns + " Spalten x " + rows + " Zeilen = " + total + " Slots"); + // +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); } - // ══════════════════════════════════════════════════════════════════════════ - // Events - // ══════════════════════════════════════════════════════════════════════════ + // ── Events ───────────────────────────────────────────────────────────────── @EventHandler public void onLogin(PostLoginEvent e) { if (!enabled) return; @@ -203,62 +192,63 @@ public class TablistModule implements Module, Listener { @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); } @EventHandler public void onDisconnect(PlayerDisconnectEvent e) { if (!enabled) return; + initializedViewers.remove(e.getPlayer().getUniqueId()); ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 1L, TimeUnit.SECONDS); } - // ══════════════════════════════════════════════════════════════════════════ - // Core - // ══════════════════════════════════════════════════════════════════════════ + // ── Core ─────────────────────────────────────────────────────────────────── private void updateAll() { recalculateGrid(); for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) updateTablist(p); } - /** - * Berechnet Spaltenanzahl dynamisch anhand der aktuell sichtbaren Server. - * Spalte 0 = Info, Spalten 1..n = Server - * Minimum: 2 Spalten (Info + 1 Server) - * Maximum: durch tab_size begrenzt - */ private void recalculateGrid() { int serverCount = getServerOrder().size(); - int needed = 1 + serverCount; // Info-Spalte + Server-Spalten - int maxColumns = tabSizeMax / rows; - int newColumns = Math.max(2, Math.min(needed, maxColumns)); + // 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; - if (newColumns == columns && newTotal == total) return; - - // Grid hat sich geändert – alte Fake-Slots bei allen Spielern entfernen for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { try { removeFakeSlots(p); } catch (Exception ignored) {} } + initializedViewers.clear(); columns = newColumns; total = newTotal; - - // Fake-UUIDs neu initialisieren fakeUuids = new UUID[total]; - for (int i = 0; i < total; i++) { + for (int i = 0; i < total; i++) fakeUuids[i] = new UUID(0xFFFEDEAD00000000L, (long) i); - } - - plugin.getLogger().info("[TablistModule] Grid: " - + columns + " Spalten x " + rows + " Zeilen = " + total - + " Slots (" + serverCount + " Server)"); + plugin.getLogger().info("[TablistModule] Grid: " + columns + "x" + rows + "=" + total + " (" + serverCount + " Server, layout=" + layoutMode + ")"); } private void updateTablist(ProxiedPlayer viewer) { if (viewer == null || !viewer.isConnected()) return; try { - String header = c(headerLine1) + "\n" + c(headerLine2) + "\n" + c(headerLine3); - String footer = c(footerLine1) + "\n" + c(footerLine2) + "\n" + c(footerLine3); + 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(); + + String header, footer; + if ("compact".equalsIgnoreCase(layoutMode)) { + header = buildCompactHeader(viewer, srv, world, rank, time, balance, online); + footer = buildCompactFooter(viewer, srv, world, rank, time, balance, online); + } else { + 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)); @@ -267,20 +257,155 @@ public class TablistModule implements Module, Listener { } } + 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); + appendLine(sb, compactHeader3, compactHeader3Spacer, viewer, srv, world, rank, time, balance, online); + return sb.toString(); + } + + 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 servers = getServerOrder(); + if (!servers.isEmpty()) { + StringBuilder serverLine = 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); + } + if (sb.length() > 0) sb.append("\n"); + sb.append(c(serverLine.toString())); + } + appendLine(sb, compactFooter2, false, viewer, srv, world, rank, time, balance, online); + appendLine(sb, compactFooter3, false, viewer, srv, world, rank, time, balance, online); + appendLine(sb, compactFooter4, compactFooter4Spacer, viewer, srv, world, rank, time, balance, online); + return sb.toString(); + } + /** - * Setzt alle echten Spieler-Slots auf listed=false damit sie in der Tablist - * unsichtbar werden und unsere Fake-Slots nicht verschieben. + * 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 + 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))); + } + } + + 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; + } + + // ── Spalte 0: Info (nur im classic Layout) ─────────────────────────── + 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) { + for (InfoEntry entry : infoEntries) { + if (!entry.enabled) continue; + if (row + 1 >= rows) break; + 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; + case "rank": val = "&f" + rank; break; + case "server": val = "&f" + srv; break; + case "world": val = "&f" + world; break; + case "time": val = "&f[" + time + "]"; break; + case "balance": val = "&f" + balance; break; + case "online": val = "&f" + online; break; + default: val = replacePlaceholders(entry.value, viewer, srv, world, rank, time, balance, online); break; + } + row = set(texts, base, row, c(val)); + row = set(texts, base, row, " "); + } + } + + // ── Server-Spieler Spalten ──────────────────────────────────────────── + List servers = getServerOrder(); + int startCol = compactMode ? 0 : 1; + for (int col = startCol; col < columns && (col - startCol) < servers.size(); col++) { + 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 sorted = sortPlayersByRank(new ArrayList<>(info.getPlayers())); + for (ProxiedPlayer p : sorted) { + 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; + row++; + } + } + } + + // Alle Slots listed=true – Layout bleibt erhalten + Item[] items = new Item[total]; + for (int i = 0; i < total; i++) { + Item item = new Item(); + item.setUuid(fakeUuids[i]); + item.setUsername(fakeName(i)); + item.setProperties(skins[i]); + item.setGamemode(0); + item.setPing(pings[i]); + item.setListed(true); + item.setDisplayName(new TextComponent(texts[i] == null || texts[i].isEmpty() ? " " : texts[i])); + items[i] = item; + } + return items; + } + + // ── Pakete ───────────────────────────────────────────────────────────────── + @SuppressWarnings("unchecked") private void hideRealPlayers(ProxiedPlayer viewer) { if (sendPacketQueuedMethod == null) return; try { java.util.Collection 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) { @@ -296,129 +421,46 @@ public class TablistModule implements Module, Listener { } } - // ══════════════════════════════════════════════════════════════════════════ - // Item-Matrix aufbauen - // ══════════════════════════════════════════════════════════════════════════ - - 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] = new net.md_5.bungee.protocol.data.Property[0]; - pings[i] = 0; // Fake-Ping fuer leere Slots - } - - // ── Spalte 0: Info ──────────────────────────────────────────────────── - int base = 0, row = 0; - row = set(texts, base, row, c(labelWebsite)); - row = set(texts, base, row, c(valueWebsite)); - row = set(texts, base, row, " "); - row = set(texts, base, row, c(labelName)); - row = set(texts, base, row, c("&f" + viewer.getName())); - row = set(texts, base, row, " "); - row = set(texts, base, row, c(labelRank)); - row = set(texts, base, row, c("&f" + getRank(viewer))); - row = set(texts, base, row, " "); - String srv = viewer.getServer() != null ? capitalize(viewer.getServer().getInfo().getName()) : "\u2014"; - row = set(texts, base, row, c(labelServer)); - row = set(texts, base, row, c("&f" + srv)); - row = set(texts, base, row, " "); - row = set(texts, base, row, c(labelWorld)); - String world = net.viper.status.StatusAPI.playerWorlds.getOrDefault(viewer.getUniqueId(), "world"); - row = set(texts, base, row, c("&f" + world)); - row = set(texts, base, row, " "); - row = set(texts, base, row, c(labelTime)); - row = set(texts, base, row, c("&f[" + sdf.format(new Date()) + "]")); - row = set(texts, base, row, " "); - row = set(texts, base, row, c(labelTeamspeak)); - row = set(texts, base, row, c(valueTeamspeak)); - - // ── Spalten 1 bis (columns-1): Server-Spieler ───────────────────────── - List servers = getServerOrder(); - for (int col = 1; col < columns && (col - 1) < servers.size(); col++) { - base = col * rows; - row = 0; - String sName = servers.get(col - 1); - row = set(texts, base, row, c(colorSrvHeader + capitalize(sName))); - - ServerInfo info = ProxyServer.getInstance().getServerInfo(sName); - if (info != null) { - for (ProxiedPlayer p : info.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); - pings[base + row] = Math.max(0, p.getPing()); - row++; - } - } - } - - // Items zusammenbauen - Item[] items = new Item[total]; - for (int i = 0; i < total; i++) { - Item item = new Item(); - item.setUuid(fakeUuids[i]); - item.setUsername(fakeName(i)); - item.setProperties(skins[i]); - item.setGamemode(0); - item.setPing(pings[i]); - item.setListed(true); - String text = texts[i]; - item.setDisplayName(new TextComponent(text == null || text.isEmpty() ? " " : text)); - items[i] = item; - } - return items; - } - - 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) { - 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(); - } - } catch (Exception ignored) {} - return new net.md_5.bungee.protocol.data.Property[0]; - } - - // ══════════════════════════════════════════════════════════════════════════ - // Pakete senden - // ══════════════════════════════════════════════════════════════════════════ - @SuppressWarnings("unchecked") private void sendSlots(ProxiedPlayer viewer, Item[] items) { if (sendPacketQueuedMethod == null) return; - PlayerListItemUpdate pkt = new PlayerListItemUpdate(); - EnumSet actions = EnumSet.of( - PlayerListItemUpdate.Action.ADD_PLAYER, - PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME, - PlayerListItemUpdate.Action.UPDATE_LISTED, - PlayerListItemUpdate.Action.UPDATE_LATENCY - ); - pkt.setActions(actions); - pkt.setItems(items); - try { sendPacketQueuedMethod.invoke(viewer, pkt); } - catch (Exception e) { plugin.getLogger().warning("[TablistModule] sendPacketQueued: " + e.getMessage()); } + + 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( + 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()); } } private void removeFakeSlots(ProxiedPlayer viewer) { if (sendPacketQueuedMethod == null || fakeUuids == null) return; try { - Class cls = Class.forName("net.md_5.bungee.protocol.packet.PlayerListItemRemove"); - Object pkt = cls.getDeclaredConstructor().newInstance(); + Class cls = Class.forName("net.md_5.bungee.protocol.packet.PlayerListItemRemove"); + Object pkt = cls.getDeclaredConstructor().newInstance(); cls.getMethod("setUuids", UUID[].class).invoke(pkt, (Object) fakeUuids.clone()); sendPacketQueuedMethod.invoke(viewer, pkt); } catch (Exception e) { @@ -433,17 +475,30 @@ public class TablistModule implements Module, Listener { } } - // ══════════════════════════════════════════════════════════════════════════ - // Helpers - // ══════════════════════════════════════════════════════════════════════════ + // ── 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) { + 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(); + } catch (Exception ignored) {} + return new net.md_5.bungee.protocol.data.Property[0]; + } private List getServerOrder() { if (!serverOrder.isEmpty()) return new ArrayList<>(serverOrder); List list = new ArrayList<>(); final String[] lobbyKey = { null }; - for (String key : ProxyServer.getInstance().getServers().keySet()) { + 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])) @@ -491,15 +546,82 @@ public class TablistModule implements Module, Listener { return ""; } - 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 sortPlayersByRank(List 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; + } - // ══════════════════════════════════════════════════════════════════════════ - // Config - // ══════════════════════════════════════════════════════════════════════════ + /** 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 balances = (java.util.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); + } + } catch (Exception ignored) {} + return "0.00"; + } + + // ── Config ───────────────────────────────────────────────────────────────── private void ensureConfigExists() { File f = new File(plugin.getDataFolder(), CONFIG_FILE); @@ -510,28 +632,66 @@ public class TablistModule implements Module, Listener { "# TablistModule Konfiguration\n" + "tablist.enabled=true\n" + "tablist.update_interval=5\n\n" + - "# Server-Spalten Reihenfolge (leer = Lobby zuerst, dann alle alphabetisch)\n" + - "# Beispiel: tablist.server_order=lobby,survival,citybuild\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.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" + "tablist.footer.line1=&8&m" + sep + "\n" + "tablist.footer.line2= &7Discord: &ediscord.viper-network.de &8| &7Shop: &eviper-network.de/shop\n" + "tablist.footer.line3=&8&m" + sep + "\n\n" + - "tablist.info.label.website=&b&lWebsite:\n" + - "tablist.info.value.website=&fviper-network.de\n" + - "tablist.info.label.name=&b&lName:\n" + - "tablist.info.label.rank=&b&lRank:\n" + - "tablist.info.label.server=&b&lServer:\n" + - "tablist.info.label.world=&b&lWorld:\n" + - "tablist.info.label.time=&b&lTime:\n" + - "tablist.info.label.teamspeak=&b&lTeamspeak:\n" + - "tablist.info.value.teamspeak=&fts.viper-network.de\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" + + "tablist.compact.header.line2.spacer=false\n" + + "tablist.compact.header.line3=\n" + + "tablist.compact.header.line3.spacer=false\n\n" + + "tablist.compact.footer.line1=\n" + + "tablist.compact.footer.line1.spacer=false\n" + + "tablist.compact.footer.line2=&7Zeit: &f%time% &8| &7Spieler: &f%online% &8| &7Ping: &f%ping%ms\n" + + "tablist.compact.footer.line2.spacer=false\n" + + "tablist.compact.footer.line3=&7Kontostand: &a$%balance% &8| &7Server: &f%server% &8| &7Welt: &f%world%\n" + + "tablist.compact.footer.line3.spacer=false\n" + + "tablist.compact.footer.line4=\n" + + "tablist.compact.footer.line4.spacer=false\n\n" + "tablist.color.server_header=&6&l\n" + - "tablist.time_format=HH:mm:ss / h:mm a\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" + + "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" + + "tablist.info.website.type=website\n" + + "tablist.info.website.value=&fviper-network.de\n\n" + + "tablist.info.name.enabled=true\n" + + "tablist.info.name.label=&b&lName:\n" + + "tablist.info.name.type=name\n\n" + + "tablist.info.rank.enabled=true\n" + + "tablist.info.rank.label=&b&lRank:\n" + + "tablist.info.rank.type=rank\n\n" + + "tablist.info.server.enabled=true\n" + + "tablist.info.server.label=&b&lServer:\n" + + "tablist.info.server.type=server\n\n" + + "tablist.info.world.enabled=true\n" + + "tablist.info.world.label=&b&lWorld:\n" + + "tablist.info.world.type=world\n\n" + + "tablist.info.time.enabled=true\n" + + "tablist.info.time.label=&b&lTime:\n" + + "tablist.info.time.type=time\n\n" + + "tablist.info.teamspeak.enabled=true\n" + + "tablist.info.teamspeak.label=&b&lTeamspeak:\n" + + "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()); } } @@ -546,34 +706,71 @@ public class TablistModule implements Module, Listener { } 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); - labelWebsite = p.getProperty("tablist.info.label.website", labelWebsite); - valueWebsite = p.getProperty("tablist.info.value.website", valueWebsite); - labelName = p.getProperty("tablist.info.label.name", labelName); - labelRank = p.getProperty("tablist.info.label.rank", labelRank); - labelServer = p.getProperty("tablist.info.label.server", labelServer); - labelWorld = p.getProperty("tablist.info.label.world", labelWorld); - labelTime = p.getProperty("tablist.info.label.time", labelTime); - labelTeamspeak = p.getProperty("tablist.info.label.teamspeak", labelTeamspeak); - valueTeamspeak = p.getProperty("tablist.info.value.teamspeak", valueTeamspeak); - colorSrvHeader = p.getProperty("tablist.color.server_header", colorSrvHeader); - timeFormat = p.getProperty("tablist.time_format", timeFormat); - try { sdf = new SimpleDateFormat(timeFormat); } - catch (Exception e) { sdf = new SimpleDateFormat("HH:mm:ss / h:mm a"); } + 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")); + } + + // Info-Eintraege laden + infoEntries.clear(); + String orderRaw = p.getProperty("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)); + } + // 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)); + infoEntries.add(new InfoEntry("&b&lRank:", "rank", "", true)); + infoEntries.add(new InfoEntry("&b&lServer:", "server", "", true)); + infoEntries.add(new InfoEntry("&b&lWorld:", "world", "", true)); + 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()) { + 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()) { + if (!hiddenRaw.isEmpty()) for (String s : hiddenRaw.split(",")) { String t = s.trim().toLowerCase(); if (!t.isEmpty()) hiddenServers.add(t); } - } } } diff --git a/StatusAPI/src/main/resources/plugin.yml b/StatusAPI/src/main/resources/plugin.yml index 667b0fa..bdb0b75 100644 --- a/StatusAPI/src/main/resources/plugin.yml +++ b/StatusAPI/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: StatusAPI main: net.viper.status.StatusAPI -version: 4.1.0 +version: 4.1.1 author: M_Viper description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule # Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung diff --git a/StatusAPI/src/main/resources/verify.properties b/StatusAPI/src/main/resources/verify.properties index 60d9149..019fa9e 100644 --- a/StatusAPI/src/main/resources/verify.properties +++ b/StatusAPI/src/main/resources/verify.properties @@ -17,6 +17,8 @@ broadcast.format=%prefixColored% %messageColored% # =========================== statusapi.port=9191 + + # =========================== # WORDPRESS / VERIFY EINSTELLUNGEN # =========================== @@ -79,6 +81,7 @@ automessage.prefix= # Der Name der Datei, in der die Nachrichten stehen (liegt im Plugin-Ordner) automessage.file=messages.txt + # =========================== # ECONOMY (Serverübergreifendes Geld) # ===========================