diff --git a/StatusAPI/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java b/StatusAPI/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java index a410f75..beed418 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java @@ -132,6 +132,7 @@ public class ScoreboardModule implements Module, Listener { private Method sendPkt; private boolean ready = false; + private static final String[] ENTRIES = { "§0","§1","§2","§3","§4","§5","§6","§7", "§8","§9","§a","§b","§c","§d","§e", @@ -345,6 +346,7 @@ public class ScoreboardModule implements Module, Listener { } private void tickAll() { + // Nametags (Prefix über dem Kopf) periodisch aktualisieren for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { if (!p.isConnected()) continue; UUID id = p.getUniqueId(); @@ -1578,6 +1580,11 @@ public class ScoreboardModule implements Module, Listener { "scoreboard.news.width=20\n" + "scoreboard.news.speed=1\n" + "\n" + + "# ===================================================\n" + + "# NAMETAG - Prefix ueber dem Spieler-Kopf\n" + + "# ===================================================\n" + + "nametag.enabled=true\n" + + "\n" + "scoreboard.rotation_interval=4\n" + "# ===================================================\n" + "# ZEILEN - max 15 sichtbar\n" + @@ -1747,10 +1754,12 @@ public class ScoreboardModule implements Module, Listener { supporterLineMap.clear(); loadLineMap(map, "scoreboard.supporter_lines.", supporterLineMap); + plugin.getLogger().info("[ScoreboardModule] " + playerLineMap.size() + " Player-Zeilen, " + adminLineMap.size() + " Admin-Zeilen, " - + supporterLineMap.size() + " Supporter-Zeilen. RotInterval=" + rotationInterval + "s"); + + supporterLineMap.size() + " Supporter-Zeilen. RotInterval=" + rotationInterval + "s" +); } /** @@ -1867,7 +1876,8 @@ public class ScoreboardModule implements Module, Listener { event.getSuggestions().addAll(suggestions); } - private class ScoreboardToggleCommand extends Command { + + private class ScoreboardToggleCommand extends Command { ScoreboardToggleCommand() { super("scoreboard", null, "sb", "togglesb"); 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 9662045..eb53c2b 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 @@ -3,7 +3,6 @@ package net.viper.status.modules.tablist; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.event.PlayerDisconnectEvent; @@ -30,7 +29,6 @@ import java.util.concurrent.TimeUnit; public class TablistModule implements Module, Listener { private static final String CONFIG_FILE = "tablist.properties"; - // ── NEU: Server-Symbol-Config ────────────────────────────────────────────── // Leerer Skin (grauer Kopf) für Platzhalter-Slots private static final net.md_5.bungee.protocol.data.Property[] EMPTY_SKIN = { @@ -41,29 +39,18 @@ public class TablistModule implements Module, Listener { ) }; - // 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 = 6, total = 120, tabSizeMax = 180; - private int configuredTabSize = 180; // Default: 180 (9 Spalten möglich) - private int configuredColumns = 0; // 0 = automatisch, >0 = direkt nutzen + private int rows = ROWS, columns = 6, total = 120, tabSizeMax = 0; + private int configuredTabSize = 0; + private int configuredColumns = 0; private UUID[] fakeUuids; - // Skin-Cache (pro Spieler) private final ConcurrentHashMap skinCache = new ConcurrentHashMap<>(); - - // ── NEU: Server-Symbol-Map (serverName lowercase → colored symbol string) ─ private final Map serverSymbols = new LinkedHashMap<>(); - // ── NEU: Spalten-Header-Modus ────────────────────────────────────────────── - // "full" → bisheriges Verhalten: Spalten-Header belegt Zeile 0 (große Markierung) - // "none" → kein Header; Zeile 0 ist frei für Spieler oder bleibt leer - // "small" → kein Slot-Header, aber der Spalten-Name erscheint im Tab-Header/Footer - // (empfohlen für MuckiDEE: kleine Markierungen bleiben, große weg) - private String columnHeaderMode = "none"; // default: keine großen Markierungen - // player_display: "server" = Server-basiert (default) | "custom" = alle zusammen, links→rechts nach Rang + private String columnHeaderMode = "none"; private String playerDisplayMode = "server"; - // Config private boolean enabled = true; private int updateInterval = 5; private String layoutMode = "compact"; @@ -75,12 +62,15 @@ 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 compactHeader1 = "&6&lViper Network &8• &7%online% Spieler online"; + private String compactHeader1 = "&6&lViper Network &8• &2Hallo, &a%player%&7! &6Schön dass du da bist!"; private String compactHeader2 = ""; private String compactHeader3 = ""; private boolean compactHeader1Spacer = false; private boolean compactHeader2Spacer = false; private boolean compactHeader3Spacer = false; + private String compactHeader4 = ""; + private boolean compactHeader4Spacer = 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%"; @@ -95,7 +85,7 @@ public class TablistModule implements Module, Listener { private boolean compactFooter6Spacer = false; private String colorSrvHeader = "&6&l"; - private boolean showFooterServerList = true; // tablist.compact.footer.serverlist=true/false + private boolean showFooterServerList = true; private String timeFormat = "HH:mm:ss / h:mm a"; private String timeZone = "Europe/Berlin"; private SimpleDateFormat sdf; @@ -103,18 +93,16 @@ public class TablistModule implements Module, Listener { private Set hiddenServers = new HashSet<>(); private List rankOrder = new ArrayList<>(); - // 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 infoEntries = new ArrayList<>(); - // State private Plugin plugin; private ScheduledTask updateTask; private Method sendPacketQueuedMethod; - private java.lang.reflect.Field tabListHandlerField; // BungeeCords interner Tab-Handler + private java.lang.reflect.Field tabListHandlerField; @Override public String getName() { return "TablistModule"; } @@ -129,10 +117,12 @@ public class TablistModule implements Module, Listener { try { initGridSize(); } catch (Exception e) { - plugin.getLogger().warning("[TablistModule] initGridSize Fehler: " + e.getMessage() + " – nutze Fallback 3x20"); - int fbSize = configuredTabSize > 0 ? configuredTabSize : 60; - int maxCols = fbSize / ROWS; // kein hartes Limit mehr – tab_size entscheidet - tabSizeMax = fbSize; rows = ROWS; columns = Math.max(3, maxCols); total = ROWS * columns; + plugin.getLogger().warning("[TablistModule] initGridSize Fehler: " + e.getMessage()); + int fbSize = configuredTabSize > 0 ? configuredTabSize : 120; + int maxCols = fbSize / ROWS; + tabSizeMax = fbSize; rows = ROWS; + columns = configuredColumns > 0 ? configuredColumns : Math.max(6, maxCols); + total = ROWS * columns; } initUuids(); try { @@ -170,39 +160,26 @@ public class TablistModule implements Module, Listener { } private void initGridSize() { - int tabSize = 60; - // FIX: configuredTabSize (aus tablist.properties) hat Vorrang. - // Nur wenn nicht gesetzt, per Reflection aus BungeeCord lesen. + // configuredTabSize aus loadConfig() hat immer Vorrang. if (configuredTabSize > 0) { - tabSize = configuredTabSize; + tabSizeMax = configuredTabSize; } else { - try { - for (ListenerInfo li : ProxyServer.getInstance().getConfig().getListeners()) { - try { Object v = li.getClass().getMethod("getTabListSize").invoke(li); - if (v instanceof Number && ((Number)v).intValue() > 0) { tabSize = ((Number)v).intValue(); break; } - } catch (Exception ignored) {} - } - } catch (Exception ignored) {} + tabSizeMax = 400; // Sicherer Fallback } - tabSizeMax = tabSize; rows = ROWS; - boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode); + int serverCount = getServerOrder().size(); - int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount); + + // SIMPEL & SICHER: configuredColumns direkt nehmen, kein komplexes Berechnen if (configuredColumns > 0) { columns = configuredColumns; } else { - columns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSize / ROWS)); + // Kein Wert in config → automatisch: 1 Spalte pro Server, mindestens 6 + columns = Math.max(6, serverCount); } + total = ROWS * columns; - if (needed > tabSize / ROWS) { - plugin.getLogger().warning("[TablistModule] Nicht alle Server passen in die Tablist!"); - plugin.getLogger().warning("[TablistModule] LOESUNG: Setze in BungeeCord config.yml -> tab-list-size: " + (needed * ROWS)); - plugin.getLogger().warning("[TablistModule] Und in tablist.properties -> tablist.tab_size=" + (needed * ROWS)); - } - // Der Client zeigt exakt (tab-list-size / 20) Spalten — BungeeCord config.yml muss stimmen! - plugin.getLogger().info("[TablistModule] tab_size=" + tabSize + " -> " + columns + "x" + ROWS + "=" + total + " (" + serverCount + " Server)"); - plugin.getLogger().info("[TablistModule] Stelle sicher dass BungeeCord config.yml 'tab-list-size: " + tabSize + "' gesetzt ist!"); + plugin.getLogger().info("[TablistModule] Grid: " + columns + " Spalten × " + ROWS + " = " + total + " Slots | " + serverCount + " Server | tabSizeMax=" + tabSizeMax); } private void initUuids() { @@ -219,11 +196,16 @@ public class TablistModule implements Module, Listener { net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p); if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin); disableBungeeTabHandler(p); - ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { - disableBungeeTabHandler(p); - updateTablist(p); - ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 2L, TimeUnit.SECONDS); - }, 2L, TimeUnit.SECONDS); + // BungeeCord resettet tabListHandler nach PostLoginEvent intern nochmals. + // Deshalb: 0.5s, 1s, 2s nacheinander überschreiben. + long[] delays = {500L, 1000L, 2000L}; + for (long delayMs : delays) { + ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { + disableBungeeTabHandler(p); + updateTablist(p); + }, delayMs, TimeUnit.MILLISECONDS); + } + ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 3L, TimeUnit.SECONDS); } @EventHandler @@ -233,28 +215,23 @@ public class TablistModule implements Module, Listener { net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(switched); if (skin != null && skin.length > 0) skinCache.put(switched.getUniqueId(), skin); + // Sofort deaktivieren disableBungeeTabHandler(switched); - ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { - disableBungeeTabHandler(switched); - net.md_5.bungee.protocol.data.Property[] freshSkin = fetchSkin(switched); - if (freshSkin != null && freshSkin.length > 0) skinCache.put(switched.getUniqueId(), freshSkin); - - for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) { - try { removeFakeSlots(viewer); } catch (Exception ignored) {} - } - updateAll(); - + // BungeeCord setzt den tabListHandler nach ServerSwitch intern mehrfach zurück. + // Deshalb: 0.5s, 1s, 2s, 3s nacheinander überschreiben + tablist neu senden. + long[] delays = {500L, 1000L, 2000L, 3000L}; + for (long delayMs : delays) { 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); + disableBungeeTabHandler(switched); + net.md_5.bungee.protocol.data.Property[] freshSkin = fetchSkin(switched); + if (freshSkin != null && freshSkin.length > 0) skinCache.put(switched.getUniqueId(), freshSkin); for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) { try { removeFakeSlots(viewer); } catch (Exception ignored) {} } updateAll(); - }, 2L, TimeUnit.SECONDS); - - }, 1L, TimeUnit.SECONDS); + }, delayMs, TimeUnit.MILLISECONDS); + } } @EventHandler @@ -284,23 +261,13 @@ public class TablistModule implements Module, Listener { } private void recalculateGrid() { - boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode); - int newColumns; - if ("custom".equalsIgnoreCase(playerDisplayMode)) { - // Custom-Modus: immer 3 Spalten (konfigurierbar via tab_size) - newColumns = Math.min(3, tabSizeMax / ROWS); - } else { - int serverCount = getServerOrder().size(); - int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount); - if (configuredColumns > 0) { - newColumns = configuredColumns; - } else { - int effectiveTabMax = configuredTabSize > 0 ? configuredTabSize : tabSizeMax; - newColumns = Math.max(hasInfo ? 2 : 1, Math.min(needed, effectiveTabMax / ROWS)); - } - } + // tabSizeMax synchronisieren + if (configuredTabSize > 0) tabSizeMax = configuredTabSize; + + int serverCount = getServerOrder().size(); + int newColumns = configuredColumns > 0 ? configuredColumns : Math.max(6, serverCount); int newTotal = ROWS * newColumns; - // Nur abbrechen wenn Grid identisch UND fakeUuids bereits korrekt initialisiert + if (newColumns == columns && newTotal == total && fakeUuids != null && fakeUuids.length == newTotal) return; for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { try { removeFakeSlots(p); } catch (Exception ignored) {} @@ -309,7 +276,7 @@ public class TablistModule implements Module, Listener { columns = newColumns; total = newTotal; initUuids(); - plugin.getLogger().info("[TablistModule] Grid: " + columns + "x" + rows + "=" + total + " (" + getServerOrder().size() + " Server, Modus: " + playerDisplayMode + ")"); + plugin.getLogger().info("[TablistModule] Grid: " + columns + "x" + rows + "=" + total + " (" + serverCount + " Server)"); } private void updateTablist(ProxiedPlayer viewer) { @@ -354,6 +321,7 @@ public class TablistModule implements Module, Listener { appendLine(sb, compactHeader1, compactHeader1Spacer, 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); + appendLine(sb, compactHeader4, compactHeader4Spacer, viewer, srv, world, rank, time, balance, online); return sb.toString(); } @@ -565,11 +533,7 @@ public class TablistModule implements Module, Listener { private void sendSlots(ProxiedPlayer viewer, Item[] items) { if (sendPacketQueuedMethod == null) return; try { - // Alles in einem Paket – ADD_PLAYER muss UPDATE_LISTED enthalten - // damit der Client den Slot sofort als sichtbar markiert. - // Die "Ignoring unknown player" Warnung entsteht wenn hideRealPlayers() - // VOR sendSlots läuft und der Client die UUIDs noch nicht kennt. - // Fix: hideRealPlayers wird NACH sendSlots aufgerufen (siehe updateTablist). + // Paket 1: ADD_PLAYER + UPDATE_DISPLAY_NAME + UPDATE_LISTED + UPDATE_LATENCY PlayerListItemUpdate pkt = new PlayerListItemUpdate(); pkt.setActions(EnumSet.of( PlayerListItemUpdate.Action.ADD_PLAYER, @@ -578,6 +542,14 @@ public class TablistModule implements Module, Listener { PlayerListItemUpdate.Action.UPDATE_LATENCY)); pkt.setItems(items); sendPacketQueuedMethod.invoke(viewer, pkt); + + // Paket 2: Explizit UPDATE_LISTED=true für alle Fake-Slots nochmal senden. + // BungeeCord sendet nach ServerSwitch ein eigenes Tab-Paket das einige Slots + // auf listed=false setzt – dieses zweite Paket überschreibt das wieder. + PlayerListItemUpdate listedPkt = new PlayerListItemUpdate(); + listedPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); + listedPkt.setItems(items); // items haben alle listed=true gesetzt + sendPacketQueuedMethod.invoke(viewer, listedPkt); } catch (Exception e) { plugin.getLogger().warning("[TablistModule] sendSlots: " + e.getMessage()); } } @@ -611,10 +583,8 @@ public class TablistModule implements Module, Listener { if (tabListHandlerField == null) return; try { Class tabListClass = Class.forName("net.md_5.bungee.tab.TabList"); - // Wenn schon ein Proxy gesetzt ist, nichts tun - Object current = tabListHandlerField.get(player); - if (current != null && java.lang.reflect.Proxy.isProxyClass(current.getClass())) return; - + // IMMER neu setzen – BungeeCord resettet den tabListHandler nach jedem + // ServerSwitch intern, daher reicht ein einmaliges Setzen nicht aus. Object noopProxy = java.lang.reflect.Proxy.newProxyInstance( tabListClass.getClassLoader(), new Class[]{ tabListClass }, @@ -1225,6 +1195,8 @@ public class TablistModule implements Module, Listener { compactHeader1Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line1.spacer", "false")); compactHeader2Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line2.spacer", "false")); compactHeader3Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line3.spacer", "false")); + compactHeader4 = get.apply("tablist.compact.header.line4", compactHeader4); + compactHeader4Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line4.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); diff --git a/StatusAPI/src/main/resources/scoreboard.properties b/StatusAPI/src/main/resources/scoreboard.properties index efcc46e..186e465 100644 --- a/StatusAPI/src/main/resources/scoreboard.properties +++ b/StatusAPI/src/main/resources/scoreboard.properties @@ -138,3 +138,10 @@ scoreboard.admin_lines.13=&7Spieler: %online% &8/ &7%maxplayers% scoreboard.admin_lines.14=%line% scoreboard.admin_lines.15=&7%compass% scoreboard.admin_lines.15.2=&7Pos: X:&f%x% &7Y:&f%y% &7Z:&f%z% + +# =================================================== +# NAMETAG - Prefix ueber dem Spieler-Kopf +# =================================================== +# Zeigt den LuckPerms-Prefix ueber dem Spieler-Avatar an. +# Auf false setzen zum Deaktivieren. +nametag.enabled=true