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 a18f754..fd0925b 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 @@ -43,8 +43,8 @@ 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 = 3, total = 60, tabSizeMax = 60; - private int configuredTabSize = 0; // 0 = auto-detect aus BungeeCord + private int rows = ROWS, columns = 6, total = 120, tabSizeMax = 180; + private int configuredTabSize = 180; // Default: 180 (9 Spalten möglich) private UUID[] fakeUuids; // Skin-Cache (pro Spieler) @@ -113,6 +113,7 @@ public class TablistModule implements Module, Listener { private Plugin plugin; private ScheduledTask updateTask; private Method sendPacketQueuedMethod; + private java.lang.reflect.Field tabListHandlerField; // BungeeCords interner Tab-Handler @Override public String getName() { return "TablistModule"; } @@ -129,13 +130,19 @@ public class TablistModule implements Module, Listener { } 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; + int maxCols = fbSize / ROWS; // kein hartes Limit mehr – tab_size entscheidet + tabSizeMax = fbSize; rows = ROWS; columns = Math.max(3, maxCols); 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); + // BungeeCords internen tabListHandler-Field finden und nullbar machen + try { + tabListHandlerField = uc.getDeclaredField("tabListHandler"); + tabListHandlerField.setAccessible(true); + } catch (Exception ignored) {} plugin.getLogger().info("[TablistModule] sendPacketQueued gefunden."); } catch (Exception e) { plugin.getLogger().severe("[TablistModule] sendPacketQueued NICHT gefunden: " + e.getMessage()); @@ -163,14 +170,19 @@ public class TablistModule implements Module, Listener { private void initGridSize() { int tabSize = 60; - try { - for (ListenerInfo li : ProxyServer.getInstance().getConfig().getListeners()) { - 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; + // FIX: configuredTabSize (aus tablist.properties) hat Vorrang. + // Nur wenn nicht gesetzt, per Reflection aus BungeeCord lesen. + if (configuredTabSize > 0) { + tabSize = 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 = tabSize; rows = ROWS; boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode); @@ -179,11 +191,13 @@ public class TablistModule implements Module, Listener { 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().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!"); } private void initUuids() { @@ -199,7 +213,9 @@ public class TablistModule implements Module, Listener { 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); + disableBungeeTabHandler(p); ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { + disableBungeeTabHandler(p); updateTablist(p); ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 2L, TimeUnit.SECONDS); }, 2L, TimeUnit.SECONDS); @@ -212,8 +228,10 @@ 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); + 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); @@ -269,10 +287,12 @@ public class TablistModule implements Module, Listener { } else { int serverCount = getServerOrder().size(); int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount); - newColumns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSizeMax / ROWS)); + int effectiveTabMax = configuredTabSize > 0 ? configuredTabSize : tabSizeMax; + newColumns = Math.max(hasInfo ? 2 : 1, Math.min(needed, effectiveTabMax / ROWS)); } int newTotal = ROWS * newColumns; - if (newColumns == columns && newTotal == total) return; + // 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) {} } @@ -286,6 +306,7 @@ public class TablistModule implements Module, Listener { private void updateTablist(ProxiedPlayer viewer) { if (viewer == null || !viewer.isConnected()) return; try { + // DEBUG: zeige aktuelle Grid-Werte im Chat (wird nach erstem Fix entfernt) 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); @@ -307,8 +328,11 @@ public class TablistModule implements Module, Listener { viewer.setTabHeader( new net.md_5.bungee.api.chat.TextComponent(hComps), new net.md_5.bungee.api.chat.TextComponent(fComps)); - hideRealPlayers(viewer); + // Erst Slots senden (ADD_PLAYER), DANN echte Spieler verstecken (UPDATE_LISTED=false). + // Umgekehrte Reihenfolge führt zu "Ignoring player info update for unknown player" + // weil der Client UPDATE_LISTED für unbekannte UUIDs ignoriert. sendSlots(viewer, buildItems(viewer)); + hideRealPlayers(viewer); } catch (Exception ex) { plugin.getLogger().warning("[TablistModule] " + viewer.getName() + ": " + ex.getMessage()); } @@ -363,8 +387,11 @@ public class TablistModule implements Module, Listener { for (int i = 0; i < total; i++) { texts[i] = " "; skins[i] = EMPTY_SKIN; pings[i] = 0; } boolean compact = "compact".equalsIgnoreCase(layoutMode); - // Ob der Spalten-Header einen Slot belegt (= "full") oder nicht (= "none"/"small") - boolean useSlotHeader = "full".equalsIgnoreCase(columnHeaderMode); + // Ob der Spalten-Header einen Slot belegt: + // "full" = explizit aktiviert, "none"/"small" = früher deaktiviert. + // FIX: Im Server-Modus immer den Servernamen in Zeile 0 schreiben, + // sonst weiß der Spieler nicht welche Spalte welcher Server ist. + boolean useSlotHeader = !"none".equalsIgnoreCase(columnHeaderMode); // Info-Spalte (nur classic) if (!compact) { @@ -476,51 +503,73 @@ public class TablistModule implements Module, Listener { private void hideRealPlayers(ProxiedPlayer viewer) { if (sendPacketQueuedMethod == null) return; try { + // Echte Spieler: nur listed=false setzen (KEIN Remove, sonst geht der Skin verloren!) + // Der Client kennt sie bereits per ADD_PLAYER von BungeeCord → UPDATE_LISTED reicht. Collection online = ProxyServer.getInstance().getPlayers(); + if (!online.isEmpty()) { + PlayerListItemUpdate playerPkt = new PlayerListItemUpdate(); + playerPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); + Item[] playerItems = new Item[online.size()]; + int i = 0; + for (ProxiedPlayer p : online) { + Item it = new Item(); it.setUuid(p.getUniqueId()); it.setListed(false); + playerItems[i++] = it; + } + playerPkt.setItems(playerItems); + sendPacketQueuedMethod.invoke(viewer, playerPkt); + } - // Sammle alle UUIDs die versteckt werden sollen: - // 1. Echte Spieler - // 2. BungeeCord-interne Server-Eintraege (werden von BC selbst in die Tablist geschrieben) - List toHide = new ArrayList<>(); - for (ProxiedPlayer p : online) toHide.add(p.getUniqueId()); - - // BungeeCord schreibt fuer jeden Server einen eigenen Eintrag mit einer - // deterministischen UUID (nameUUIDFromBytes des Server-Namens). - // Diese auch auf listed=false setzen damit die grossen Spalten-Header verschwinden. + // Server-UUIDs (BungeeCord schreibt pro Server 2 Einträge in die Tablist): + // Diese per PlayerListItemRemove entfernen – das funktioniert auch für + // UUIDs die der Client noch nicht kennt, ohne Skin-Schaden. + List serverUuids = new ArrayList<>(); for (String srvName : ProxyServer.getInstance().getServers().keySet()) { try { - UUID srvUuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + srvName).getBytes(StandardCharsets.UTF_8)); - toHide.add(srvUuid); - UUID srvUuid2 = UUID.nameUUIDFromBytes(srvName.getBytes(StandardCharsets.UTF_8)); - toHide.add(srvUuid2); + serverUuids.add(UUID.nameUUIDFromBytes(("OfflinePlayer:" + srvName).getBytes(StandardCharsets.UTF_8))); + serverUuids.add(UUID.nameUUIDFromBytes(srvName.getBytes(StandardCharsets.UTF_8))); } catch (Exception ignored) {} } - - if (toHide.isEmpty()) return; - PlayerListItemUpdate pkt = new PlayerListItemUpdate(); - pkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); - Item[] items = new Item[toHide.size()]; - int idx = 0; - for (UUID uuid : toHide) { - Item it = new Item(); it.setUuid(uuid); it.setListed(false); items[idx++] = it; + if (!serverUuids.isEmpty()) { + try { + Class removeClass = Class.forName("net.md_5.bungee.protocol.packet.PlayerListItemRemove"); + Object removePkt = removeClass.getDeclaredConstructor().newInstance(); + removeClass.getMethod("setUuids", UUID[].class).invoke(removePkt, + (Object) serverUuids.toArray(new UUID[0])); + sendPacketQueuedMethod.invoke(viewer, removePkt); + } catch (Exception ignored) { + // Fallback: UPDATE_LISTED=false (Warnungen im Client-Log sind harmlos) + PlayerListItemUpdate srvPkt = new PlayerListItemUpdate(); + srvPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); + Item[] srvItems = new Item[serverUuids.size()]; + for (int j = 0; j < serverUuids.size(); j++) { + Item it = new Item(); it.setUuid(serverUuids.get(j)); it.setListed(false); + srvItems[j] = it; + } + srvPkt.setItems(srvItems); + sendPacketQueuedMethod.invoke(viewer, srvPkt); + } } - pkt.setItems(items); - sendPacketQueuedMethod.invoke(viewer, pkt); } catch (Exception e) { plugin.getLogger().warning("[TablistModule] hideRealPlayers: " + e.getMessage()); } } @SuppressWarnings("unchecked") private void sendSlots(ProxiedPlayer viewer, Item[] items) { if (sendPacketQueuedMethod == null) return; - PlayerListItemUpdate pkt = new PlayerListItemUpdate(); - pkt.setActions(EnumSet.of( - PlayerListItemUpdate.Action.ADD_PLAYER, - PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME, - 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()); } + 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). + PlayerListItemUpdate pkt = new PlayerListItemUpdate(); + pkt.setActions(EnumSet.of( + PlayerListItemUpdate.Action.ADD_PLAYER, + PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME, + PlayerListItemUpdate.Action.UPDATE_LISTED, + PlayerListItemUpdate.Action.UPDATE_LATENCY)); + pkt.setItems(items); + sendPacketQueuedMethod.invoke(viewer, pkt); + } catch (Exception e) { plugin.getLogger().warning("[TablistModule] sendSlots: " + e.getMessage()); } } private void removeFakeSlots(ProxiedPlayer viewer) { @@ -544,6 +593,34 @@ public class TablistModule implements Module, Listener { // ── Helpers ──────────────────────────────────────────────────────────────── + /** + * Ersetzt BungeeCords internen tabListHandler durch einen No-Op Proxy. + * null setzen crasht BungeeCord (NPE in DownstreamBridge). + * Ein leerer Proxy ignoriert alle Aufrufe lautlos. + */ + private void disableBungeeTabHandler(ProxiedPlayer player) { + 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; + + Object noopProxy = java.lang.reflect.Proxy.newProxyInstance( + tabListClass.getClassLoader(), + new Class[]{ tabListClass }, + (proxy, method, args) -> { + Class ret = method.getReturnType(); + if (ret == boolean.class || ret == Boolean.class) return false; + if (ret == int.class || ret == Integer.class) return 0; + if (ret == long.class || ret == Long.class) return 0L; + return null; + } + ); + tabListHandlerField.set(player, noopProxy); + } catch (Exception ignored) {} + } + /** * ── ULTIMATE: Gibt das konfigurierte Server-Symbol für den Spieler zurück. * Leer wenn kein Symbol für den aktuellen Server definiert ist. @@ -1207,4 +1284,4 @@ public class TablistModule implements Module, Listener { } } -} +} \ No newline at end of file