Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-05-08 09:14:47 +02:00
parent 42e80980a3
commit 23cbb557e5
4 changed files with 467 additions and 262 deletions

View File

@@ -50,6 +50,9 @@ public class StatusAPI extends Plugin implements Runnable {
// Welt pro Spieler (UUID -> Weltname), wird von StatusAPIBridge gepusht
public static final ConcurrentHashMap<UUID, String> playerWorlds = new ConcurrentHashMap<>();
// Kontostand pro Spieler (UUID -> Balance), wird von StatusAPIBridge gepusht
public static final ConcurrentHashMap<UUID, Double> playerBalances = new ConcurrentHashMap<>();
private volatile Thread thread;
private volatile ServerSocket serverSocket;
private volatile boolean shuttingDown = false;
@@ -605,6 +608,8 @@ public class StatusAPI extends Plugin implements Runnable {
try {
double newBal = Double.parseDouble(balStr);
ecoModUpd.getManager().setBalance(ecoUpdUuid, newBal);
// Auch in der Tablist-Map speichern
playerBalances.put(ecoUpdUuid, newBal);
} catch (NumberFormatException ignored) {}
}
// Stats-Felder (total_earned etc.) im Cache aktualisieren, falls vorhanden

View File

@@ -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.setActions(actions);
pkt.setItems(items);
try { sendPacketQueuedMethod.invoke(viewer, pkt); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] sendPacketQueued: " + e.getMessage()); }
boolean isNew = initializedViewers.add(viewer.getUniqueId());
if (isNew) {
// Erstes Mal: ADD_PLAYER + UPDATE_DISPLAY_NAME + UPDATE_LISTED
PlayerListItemUpdate addPkt = new PlayerListItemUpdate();
addPkt.setActions(EnumSet.of(
PlayerListItemUpdate.Action.ADD_PLAYER,
PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME,
PlayerListItemUpdate.Action.UPDATE_LISTED));
addPkt.setItems(items);
try { sendPacketQueuedMethod.invoke(viewer, addPkt); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] ADD_PLAYER: " + e.getMessage()); return; }
} else {
// Folgeupdate: nur DisplayName + Listed aktualisieren (kein Flackern)
PlayerListItemUpdate updPkt = new PlayerListItemUpdate();
updPkt.setActions(EnumSet.of(
PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME,
PlayerListItemUpdate.Action.UPDATE_LISTED));
updPkt.setItems(items);
try { sendPacketQueuedMethod.invoke(viewer, updPkt); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] UPDATE_DISPLAY_NAME: " + e.getMessage()); return; }
}
// Ping immer separat senden
PlayerListItemUpdate pingPkt = new PlayerListItemUpdate();
pingPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LATENCY));
pingPkt.setItems(items);
try { sendPacketQueuedMethod.invoke(viewer, pingPkt); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] UPDATE_LATENCY: " + e.getMessage()); }
}
private void removeFakeSlots(ProxiedPlayer viewer) {
if (sendPacketQueuedMethod == null || fakeUuids == null) return;
try {
Class<?> cls = Class.forName("net.md_5.bungee.protocol.packet.PlayerListItemRemove");
Object pkt = cls.getDeclaredConstructor().newInstance();
Class<?> cls = Class.forName("net.md_5.bungee.protocol.packet.PlayerListItemRemove");
Object pkt = cls.getDeclaredConstructor().newInstance();
cls.getMethod("setUuids", UUID[].class).invoke(pkt, (Object) fakeUuids.clone());
sendPacketQueuedMethod.invoke(viewer, pkt);
} catch (Exception e) {
@@ -433,17 +475,30 @@ public class TablistModule implements Module, Listener {
}
}
// ══════════════════════════════════════════════════════════════════════════
// Helpers
// ══════════════════════════════════════════════════════════════════════════
// ── Helpers ────────────────────────────────────────────────────────────────
private int set(String[] arr, int base, int row, String text) {
if (base + row < total) arr[base + row] = text == null ? " " : text;
return row + 1;
}
private net.md_5.bungee.protocol.data.Property[] getPlayerSkin(ProxiedPlayer player) {
try {
Object pending = player.getPendingConnection();
net.md_5.bungee.connection.LoginResult profile =
(net.md_5.bungee.connection.LoginResult)
pending.getClass().getMethod("getLoginProfile").invoke(pending);
if (profile != null && profile.getProperties() != null) return profile.getProperties();
} catch (Exception ignored) {}
return new net.md_5.bungee.protocol.data.Property[0];
}
private List<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);
labelServer = p.getProperty("tablist.info.label.server", labelServer);
labelWorld = p.getProperty("tablist.info.label.world", labelWorld);
labelTime = p.getProperty("tablist.info.label.time", labelTime);
labelTeamspeak = p.getProperty("tablist.info.label.teamspeak", labelTeamspeak);
valueTeamspeak = p.getProperty("tablist.info.value.teamspeak", valueTeamspeak);
colorSrvHeader = p.getProperty("tablist.color.server_header", colorSrvHeader);
timeFormat = p.getProperty("tablist.time_format", timeFormat);
try { sdf = new SimpleDateFormat(timeFormat); }
catch (Exception e) { sdf = new SimpleDateFormat("HH:mm:ss / h:mm a"); }
compactHeader1 = p.getProperty("tablist.compact.header.line1", compactHeader1);
compactHeader2 = p.getProperty("tablist.compact.header.line2", compactHeader2);
compactHeader3 = p.getProperty("tablist.compact.header.line3", compactHeader3);
compactHeader2Spacer = Boolean.parseBoolean(p.getProperty("tablist.compact.header.line2.spacer", "false"));
compactHeader3Spacer = Boolean.parseBoolean(p.getProperty("tablist.compact.header.line3.spacer", "false"));
compactFooter1 = p.getProperty("tablist.compact.footer.line1", compactFooter1);
compactFooter2 = p.getProperty("tablist.compact.footer.line2", compactFooter2);
compactFooter3 = p.getProperty("tablist.compact.footer.line3", compactFooter3);
compactFooter4 = p.getProperty("tablist.compact.footer.line4", compactFooter4);
compactFooter1Spacer = Boolean.parseBoolean(p.getProperty("tablist.compact.footer.line1.spacer", "false"));
compactFooter4Spacer = Boolean.parseBoolean(p.getProperty("tablist.compact.footer.line4.spacer", "false"));
colorSrvHeader = p.getProperty("tablist.color.server_header", colorSrvHeader);
timeFormat = p.getProperty("tablist.time_format", timeFormat);
timeZone = p.getProperty("tablist.timezone", timeZone);
try {
sdf = new SimpleDateFormat(timeFormat);
sdf.setTimeZone(java.util.TimeZone.getTimeZone(timeZone));
} catch (Exception e) {
sdf = new SimpleDateFormat("HH:mm:ss / h:mm a");
sdf.setTimeZone(java.util.TimeZone.getTimeZone("Europe/Berlin"));
}
// Info-Eintraege laden
infoEntries.clear();
String orderRaw = p.getProperty("tablist.info.order",
"website,name,rank,server,world,time,teamspeak").trim();
for (String id : orderRaw.split(",")) {
id = id.trim();
if (id.isEmpty()) continue;
boolean enabled = Boolean.parseBoolean(p.getProperty("tablist.info." + id + ".enabled", "true"));
String label = p.getProperty("tablist.info." + id + ".label", "");
String type = p.getProperty("tablist.info." + id + ".type", "custom");
String value = p.getProperty("tablist.info." + id + ".value", "");
infoEntries.add(new InfoEntry(label, type, value, enabled));
}
// Fallback wenn keine Eintraege konfiguriert
if (infoEntries.isEmpty()) {
infoEntries.add(new InfoEntry("&b&lWebsite:", "website", "&fviper-network.de", true));
infoEntries.add(new InfoEntry("&b&lName:", "name", "", true));
infoEntries.add(new InfoEntry("&b&lRank:", "rank", "", true));
infoEntries.add(new InfoEntry("&b&lServer:", "server", "", true));
infoEntries.add(new InfoEntry("&b&lWorld:", "world", "", true));
infoEntries.add(new InfoEntry("&b&lTime:", "time", "", true));
infoEntries.add(new InfoEntry("&b&lTeamspeak:", "teamspeak", "&fts.viper-network.de", true));
}
rankOrder.clear();
String rankRaw = p.getProperty("tablist.rank_order", "").trim();
if (!rankRaw.isEmpty())
for (String s : rankRaw.split(",")) { String t = s.trim(); if (!t.isEmpty()) rankOrder.add(t.toLowerCase()); }
serverOrder.clear();
String raw = p.getProperty("tablist.server_order", "").trim();
if (!raw.isEmpty()) {
if (!raw.isEmpty())
for (String s : raw.split(",")) { String t = s.trim(); if (!t.isEmpty()) serverOrder.add(t.toLowerCase()); }
}
hiddenServers.clear();
String hiddenRaw = p.getProperty("tablist.hidden_servers", "").trim();
if (!hiddenRaw.isEmpty()) {
if (!hiddenRaw.isEmpty())
for (String s : hiddenRaw.split(",")) { String t = s.trim().toLowerCase(); if (!t.isEmpty()) hiddenServers.add(t); }
}
}
}

View File

@@ -1,6 +1,6 @@
name: StatusAPI
main: net.viper.status.StatusAPI
version: 4.1.0
version: 4.1.1
author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung

View File

@@ -17,6 +17,8 @@ broadcast.format=%prefixColored% %messageColored%
# ===========================
statusapi.port=9191
# ===========================
# WORDPRESS / VERIFY EINSTELLUNGEN
# ===========================
@@ -79,6 +81,7 @@ automessage.prefix=
# Der Name der Datei, in der die Nachrichten stehen (liegt im Plugin-Ordner)
automessage.file=messages.txt
# ===========================
# ECONOMY (Serverübergreifendes Geld)
# ===========================