@@ -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 < InfoEntry > infoEntries = new ArrayList < > ( ) ;
private String timeFormat = " HH:mm:ss / h:mm a " ;
private String timeZone = " Europe/Berlin " ;
private SimpleDateFormat sdf ;
private List < String > serverOrder = new ArrayList < > ( ) ;
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 ──────────────────────────────────────────────────────────────────
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 < UUID > 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 < 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 ( ) ) ;
} , 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 < String > 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 < String > 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 < ProxiedPlayer > 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 < 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 ) {
@@ -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 < String > 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 . set Actions ( actions ) ;
pkt . setItems ( items ) ;
try { sendPacketQueuedMethod . invoke ( viewer , pkt ) ; }
catch ( Exception e ) { plugin . getLogger ( ) . warning ( " [TablistModule] sendPacketQueued: " + e . g etMessage ( ) ) ; }
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 . s etItems ( 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 < String > getServerOrder ( ) {
if ( ! serverOrder . isEmpty ( ) ) return new ArrayList < > ( serverOrder ) ;
List < String > 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 < 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 ; } }
/**
* 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 ;
}
// ══════════════════════════════════════════════════════════════════════════
// 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 < 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 ) ;
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 ) ;
labelServ er = 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 " ) ) ;
compactHeader3Spac er = 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 ) ; }
}
}
}