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