From 8720ba41bba071ef4cd36b4535d7e25fc02edbfb Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sun, 24 May 2026 19:43:52 +0000 Subject: [PATCH] Delete src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java via Git Manager GUI --- .../modules/scoreboard/ScoreboardModule.java | 1980 ----------------- 1 file changed, 1980 deletions(-) delete mode 100644 src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java diff --git a/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java b/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java deleted file mode 100644 index beed418..0000000 --- a/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java +++ /dev/null @@ -1,1980 +0,0 @@ -package net.viper.status.modules.scoreboard; - -import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.ProxyServer; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.api.event.PlayerDisconnectEvent; -import net.md_5.bungee.api.event.PostLoginEvent; -import net.md_5.bungee.api.event.ServerSwitchEvent; -import net.md_5.bungee.api.event.TabCompleteEvent; -import net.md_5.bungee.api.plugin.Command; -import net.md_5.bungee.api.plugin.Listener; -import net.md_5.bungee.api.plugin.Plugin; -import net.md_5.bungee.api.scheduler.ScheduledTask; -import net.md_5.bungee.event.EventHandler; -import net.md_5.bungee.protocol.packet.ScoreboardDisplay; -import net.md_5.bungee.protocol.packet.ScoreboardObjective; -import net.md_5.bungee.protocol.packet.ScoreboardObjective.HealthDisplay; -import net.md_5.bungee.protocol.packet.ScoreboardScore; -import net.md_5.bungee.protocol.packet.Team; -import net.md_5.bungee.protocol.packet.Team.NameTagVisibility; -import net.md_5.bungee.protocol.packet.Team.CollisionRule; -import net.md_5.bungee.protocol.util.Either; -import net.md_5.bungee.protocol.data.NumberFormat; -import net.viper.status.StatusAPI; -import net.viper.status.module.Module; - -import java.io.*; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryMXBean; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.text.DecimalFormat; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - -public class ScoreboardModule implements Module, Listener { - - private static final String CONFIG_FILE = "scoreboard.properties"; - private static final String OBJ_NAME = "vpsb"; - private static final String OBJ_NAME_ADMIN = "vpsbadmin"; - private static final String OBJ_NAME_SUPP = "vpsbsupp"; - - public static final ConcurrentHashMap playerHealth = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap playerCompass = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap playerTps = new ConcurrentHashMap<>(); - // Neue Placeholder-Daten - public static final ConcurrentHashMap playerX = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap playerY = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap playerZ = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap playerWorld = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap playerGamemode= new ConcurrentHashMap<>(); - public static final ConcurrentHashMap playerExp = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap playerFood = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap playerSpeed = new ConcurrentHashMap<>(); - - // ── TicketSystem Placeholder ────────────────────────────────────────────── - /** Eigene aktive Tickets des Spielers (OPEN + CLAIMED + FORWARDED) */ - public static final ConcurrentHashMap ticketMyOpen = new ConcurrentHashMap<>(); - /** Alle offenen Tickets gesamt (Status: OPEN) – für Supporter & Admin */ - public static final java.util.concurrent.atomic.AtomicInteger ticketTotalOpen = new java.util.concurrent.atomic.AtomicInteger(0); - /** Alle Tickets in Bearbeitung gesamt (Status: CLAIMED) – für Admin */ - public static final java.util.concurrent.atomic.AtomicInteger ticketTotalClaimed = new java.util.concurrent.atomic.AtomicInteger(0); - /** Positive Bewertungen gesamt – für Admin */ - public static final java.util.concurrent.atomic.AtomicInteger ticketRatingGood = new java.util.concurrent.atomic.AtomicInteger(0); - /** Negative Bewertungen gesamt – für Admin */ - public static final java.util.concurrent.atomic.AtomicInteger ticketRatingBad = new java.util.concurrent.atomic.AtomicInteger(0); - - private final ConcurrentHashMap joinTimes = new ConcurrentHashMap<>(); - /** Aktuell gerenderter Spieler – für PAPI-Auflösung in ph() */ - private UUID currentPlayerUuid = null; - // Spieler, die das Scoreboard ausgeblendet haben - /** FIX: Referenz auf NetworkInfoModule für TPS-Fallback */ - private net.viper.status.modules.network.NetworkInfoModule networkInfoModule = null; - - /** Wird von StatusAPI nach dem Registrieren aller Module aufgerufen */ - public void setNetworkInfoModule(net.viper.status.modules.network.NetworkInfoModule nim) { - this.networkInfoModule = nim; - } - - private final Set hiddenPlayers = ConcurrentHashMap.newKeySet(); - // Spieler, die manuell auf Player-Board gezwungen wurden (Override) - private final Set forcePlayerView = ConcurrentHashMap.newKeySet(); - // Spieler, die manuell auf Supporter-Board gezwungen wurden - private final Set forceSupporterView = ConcurrentHashMap.newKeySet(); - // Spieler, die manuell auf Admin-Board gezwungen wurden (ohne Perm) - private final Set forceAdminView = ConcurrentHashMap.newKeySet(); - - private boolean enabled = true; - private int updateInterval = 500; // Millisekunden - private int tickerSpeed = 1; - private boolean rainbowEnabled = true; - private String rainbowMode = "wave"; - // Wellen-Farben als RGB-Arrays (vorberechnet aus Config) - private int[][] waveColors = null; // null = HSB-Fallback - private float waveSpeed = 0.05f; // Bewegung pro Tick - private String title = "&6&lViper Network"; - private String adminTitle = "&c&l[Admin] &4&lPanel"; - private String supporterTitle = "&e&l[Support] &6&lPanel"; - private String adminPermission = "statusapi.scoreboard.admin"; - private String supporterPermission = "statusapi.scoreboard.supporter"; - private String timeFormat = "HH:mm"; - private String dateFormat = "dd.MM.yyyy"; - private String timeZone = "Europe/Berlin"; - private String moneyFormat = "#,##0.00"; - private String decimalSeparator = ","; - private String tickerText = " Viper Network "; - private String separator = "&8&m--------------------"; - private int tickerWidth = 24; - // Key = Zeilennummer (1-based), Value = Liste der Inhalte pro Page - // Zeilen mit nur 1 Eintrag rotieren nicht, Zeilen mit 2+ Eintraegen wechseln - private Map> playerLineMap = new LinkedHashMap<>(); - private Map> adminLineMap = new LinkedHashMap<>(); - private Map> supporterLineMap = new LinkedHashMap<>(); - private int rotationInterval = 4; // Sekunden pro Page - // News-Ticker - private String newsText = ""; - private String newsPrefix = "&8[&6News&8] &r"; - private int newsWidth = 20; - private int newsSpeed = 1; - private final ConcurrentHashMap newsPos = new ConcurrentHashMap<>(); - private int maxLineNum = 0; // hoechste definierte Zeilennummer - - private Plugin plugin; - private ScheduledTask updateTask; - private ScheduledTask titleTask; - private ScheduledTask newsTask; - private java.text.SimpleDateFormat sdf; - private java.text.SimpleDateFormat sdfDate; - private DecimalFormat df; - private Method sendPkt; - private boolean ready = false; - - - private static final String[] ENTRIES = { - "§0","§1","§2","§3","§4","§5","§6","§7", - "§8","§9","§a","§b","§c","§d","§e", - "§f§0","§f§1","§f§2","§f§3","§f§4" - }; - private static final int MAX_LINES = 15; // Minecraft Client zeigt max 15 Scoreboard-Einträge - - private final ConcurrentHashMap tickerPos = new ConcurrentHashMap<>(); - private final ConcurrentHashMap rainbowIdx = new ConcurrentHashMap<>(); - private final Set created = ConcurrentHashMap.newKeySet(); - private final Set createdAdmin = ConcurrentHashMap.newKeySet(); - private final Set createdSupporter = ConcurrentHashMap.newKeySet(); - - private static final ChatColor[] RAINBOW = { - ChatColor.RED, ChatColor.GOLD, ChatColor.YELLOW, - ChatColor.GREEN, ChatColor.AQUA, ChatColor.BLUE, ChatColor.LIGHT_PURPLE - }; - - // ── Compass Tape ───────────────────────────────────────────────────────── - // Kontinuierlicher Kompass mit Sub-Zeichen-Interpolation. - // Jeder Grad entspricht einem eigenen Slot → 360 Slots, kein Springen. - // Bridge sendet normYaw (0..360, float mit 1 Dezimalstelle). - private static final int COMPASS_SLOTS = 360; // 1 Slot = 1 Grad - private static final int COMPASS_WIN = 19; // sichtbare Slots (ungerade) - private static final int COMPASS_DEG_PER_SLOT = 9; // muss 90 teilen: 1,2,3,5,6,9,10,15,18,30,45,90 - // Labels an festen Grad-Positionen (N=0, E=90, S=180, W=270) - private static final int[] COMPASS_LABEL_DEG = { 0, 90, 180, 270 }; - private static final char[] COMPASS_LABEL_CH = { 'N', 'E', 'S', 'W' }; - - @Override public String getName() { return "ScoreboardModule"; } - - @Override - public void onEnable(Plugin plugin) { - this.plugin = plugin; - plugin.getLogger().info("[ScoreboardModule] Starte..."); - ensureConfigExists(); - loadConfig(); - if (!enabled) { plugin.getLogger().info("[ScoreboardModule] Deaktiviert."); return; } - try { - Class uc = Class.forName("net.md_5.bungee.UserConnection"); - sendPkt = uc.getMethod("sendPacketQueued", net.md_5.bungee.protocol.DefinedPacket.class); - sendPkt.setAccessible(true); - ready = true; - plugin.getLogger().info("[ScoreboardModule] Aktiviert. Interval=" + updateInterval + "ms (mind. 250 empfohlen)"); - } catch (Exception e) { - plugin.getLogger().severe("[ScoreboardModule] sendPacketQueued nicht gefunden: " + e); - return; - } - ProxyServer.getInstance().getPluginManager().registerListener(plugin, this); - ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ScoreboardToggleCommand()); - // updateInterval in ms für Daten-Updates (Kompass, Zeilen, etc.) - updateTask = ProxyServer.getInstance().getScheduler().schedule( - plugin, this::tickAll, updateInterval, updateInterval, TimeUnit.MILLISECONDS); - // Separater schneller Task nur für den Titel (Wave-Animation, 100ms = 10fps) - titleTask = ProxyServer.getInstance().getScheduler().schedule( - plugin, this::tickTitle, 100, 100, TimeUnit.MILLISECONDS); - // Separater Task für News-Ticker (100ms = flüssiges Scrollen) - newsTask = ProxyServer.getInstance().getScheduler().schedule( - plugin, this::tickNews, 100, 100, TimeUnit.MILLISECONDS); - } - - @Override - public void onDisable(Plugin plugin) { - if (updateTask != null) { updateTask.cancel(); updateTask = null; } - if (titleTask != null) { titleTask.cancel(); titleTask = null; } - if (newsTask != null) { newsTask.cancel(); newsTask = null; } - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - if (created.contains(p.getUniqueId())) removeObjectiveAndTeams(p, OBJ_NAME, "vt"); - if (createdAdmin.contains(p.getUniqueId())) removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); - if (createdSupporter.contains(p.getUniqueId())) removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); - } - created.clear(); createdAdmin.clear(); createdSupporter.clear(); tickerPos.clear(); rainbowIdx.clear(); - hiddenPlayers.clear(); forceAdminView.clear(); forcePlayerView.clear(); forceSupporterView.clear(); newsPos.clear(); - } - - @EventHandler - public void onJoin(PostLoginEvent e) { - if (!ready) return; - UUID id = e.getPlayer().getUniqueId(); - tickerPos.put(id, 0); - rainbowIdx.put(id, 0); - newsPos.put(id, 0); - joinTimes.put(id, System.currentTimeMillis()); - // Nur State initialisieren – onSwitch baut das Scoreboard auf - created.remove(id); - createdAdmin.remove(id); - createdSupporter.remove(id); - } - - @EventHandler - public void onSwitch(ServerSwitchEvent e) { - if (!ready) return; - ProxiedPlayer p = e.getPlayer(); - UUID id = p.getUniqueId(); - // Altes Objective sauber entfernen – tickAll übernimmt den Neuaufbau - if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); } - if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); } - if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); } - // Kein verzögerter sendAll-Call mehr – tickAll baut nach max. 500ms neu auf - } - - @EventHandler - public void onQuit(PlayerDisconnectEvent e) { - UUID id = e.getPlayer().getUniqueId(); - tickerPos.remove(id); rainbowIdx.remove(id); created.remove(id); createdAdmin.remove(id); createdSupporter.remove(id); - forcePlayerView.remove(id); forceSupporterView.remove(id); forceAdminView.remove(id); - playerHealth.remove(id); playerCompass.remove(id); playerTps.remove(id); - playerX.remove(id); playerY.remove(id); playerZ.remove(id); - playerWorld.remove(id); playerGamemode.remove(id); - playerExp.remove(id); playerFood.remove(id); playerSpeed.remove(id); - joinTimes.remove(id); hiddenPlayers.remove(id); forceAdminView.remove(id); forcePlayerView.remove(id); newsPos.remove(id); - } - - /** Schneller Task: aktualisiert News-Position und sendet nur die betroffene Team-Zeile */ - private void tickNews() { - if (newsText == null || newsText.isEmpty()) return; - String newsPlain = ChatColor.stripColor(c(newsText)); - int gap = 4; - int nCycle = Math.max(1, newsPlain.length()) + gap; - - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - if (!p.isConnected()) continue; - UUID id = p.getUniqueId(); - if (hiddenPlayers.contains(id)) continue; - boolean isAdmin = !forceSupporterView.contains(id) && !forcePlayerView.contains(id) - && (p.hasPermission(adminPermission) || forceAdminView.contains(id)); - boolean isSupporter = !isAdmin && (forceSupporterView.contains(id) || (!forcePlayerView.contains(id) && p.hasPermission(supporterPermission))); - Set activeCreated = isAdmin ? createdAdmin : isSupporter ? createdSupporter : created; - if (!activeCreated.contains(id)) continue; - - // Position vorrücken - int nOff = (newsPos.getOrDefault(id, 0) + newsSpeed) % nCycle; - newsPos.put(id, nOff); - - // Nur die News-Zeilen neu senden (nicht das ganze Scoreboard) - try { - String activeObjName = isAdmin ? OBJ_NAME_ADMIN : OBJ_NAME; - String newsStr = buildNewsTicker(nOff); - // Finde welche Zeilennummer(n) %news% enthält und sende nur diese - java.util.Map> lineMap = - isAdmin ? adminLineMap : isSupporter ? supporterLineMap : playerLineMap; - for (java.util.Map.Entry> entry : lineMap.entrySet()) { - boolean hasNews = false; - for (String v : entry.getValue()) { - if (v.contains("%news%")) { hasNews = true; break; } - } - if (!hasNews) continue; - int lineNum = entry.getKey(); - int lineIdx = lineNum - 1; // 0-based index for ENTRIES - if (lineIdx < 0 || lineIdx >= ENTRIES.length) continue; - - // Aktuellen Varianten-Index berechnen - int pageIdx = (rotationInterval > 0) - ? (int)((System.currentTimeMillis() / 1000) / rotationInterval) : 0; - java.util.List variants = entry.getValue(); - String tpl = variants.get(pageIdx % variants.size()); - if (!tpl.contains("%news%")) continue; - - String lineText = c(tpl.replace("%news%", newsStr)); - - // Team-Packet nur für diese Zeile senden - net.md_5.bungee.protocol.packet.Team team = new net.md_5.bungee.protocol.packet.Team(); - team.setName((isAdmin ? "vta" : isSupporter ? "vts" : "vt") + lineIdx); - team.setMode((byte) 2); // UPDATE - net.md_5.bungee.api.chat.TextComponent tc = - new net.md_5.bungee.api.chat.TextComponent(""); - for (net.md_5.bungee.api.chat.BaseComponent bc : buildComponents(lineText)) - tc.addExtra(bc); - team.setPrefix(Either.right(tc)); - team.setSuffix(Either.right(new net.md_5.bungee.api.chat.TextComponent(""))); - team.setDisplayName(Either.right(new net.md_5.bungee.api.chat.TextComponent(""))); - team.setNameTagVisibility(Either.right(net.md_5.bungee.protocol.packet.Team.NameTagVisibility.ALWAYS)); - team.setCollisionRule(Either.right(net.md_5.bungee.protocol.packet.Team.CollisionRule.ALWAYS)); - team.setColor(Optional.of(21)); - team.setFriendlyFire((byte) 3); - sendPkt.invoke(p, team); - } - } catch (Exception ignored) {} - } - } - - /** Schneller Task: aktualisiert nur den Objective-Titel für flüssige Wave-Animation */ - private void tickTitle() { - if (!rainbowEnabled || !"wave".equals(rainbowMode)) return; - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - if (!p.isConnected()) continue; - UUID id = p.getUniqueId(); - if (hiddenPlayers.contains(id)) continue; - boolean isAdmin = !forceSupporterView.contains(id) && !forcePlayerView.contains(id) - && (p.hasPermission(adminPermission) || forceAdminView.contains(id)); - boolean isSupporter = !isAdmin && (forceSupporterView.contains(id) || (!forcePlayerView.contains(id) && p.hasPermission(supporterPermission))); - Set activeCreated = isAdmin ? createdAdmin : isSupporter ? createdSupporter : created; - try { - int rIdx = (rainbowIdx.getOrDefault(id, 0) + 1) % 10000; - rainbowIdx.put(id, rIdx); - String activeObjName = isAdmin ? OBJ_NAME_ADMIN : isSupporter ? OBJ_NAME_SUPP : OBJ_NAME; - String rawTitle = isAdmin ? adminTitle : isSupporter ? supporterTitle : title; - String titleStr = rainbow(c(rawTitle), rIdx); - ScoreboardObjective obj = new ScoreboardObjective(); - obj.setName(activeObjName); - obj.setAction((byte) 2); // UPDATE_TITLE - net.md_5.bungee.api.chat.TextComponent tc = - new net.md_5.bungee.api.chat.TextComponent(""); - for (net.md_5.bungee.api.chat.BaseComponent bc : buildComponents(titleStr)) - tc.addExtra(bc); - obj.setValue(Either.right(tc)); - obj.setType(HealthDisplay.INTEGER); - sendPkt.invoke(p, obj); - } catch (Exception ignored) {} - } - } - - private void tickAll() { - // Nametags (Prefix über dem Kopf) periodisch aktualisieren - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - if (!p.isConnected()) continue; - UUID id = p.getUniqueId(); - try { - sendAll(p); - } catch (Exception e) { - plugin.getLogger().warning("[ScoreboardModule] " + p.getName() + ": " + e); - created.add(id); - } - } - } - - private void sendAll(ProxiedPlayer p) throws Exception { - if (!ready || !p.isConnected()) return; - UUID id = p.getUniqueId(); - - // Scoreboard ausgeblendet → Objective + Teams sauber entfernen (einmalig) und raus - if (hiddenPlayers.contains(id)) { - if (created.contains(id)) { - removeObjectiveAndTeams(p, OBJ_NAME, "vt"); - created.remove(id); - } - if (createdAdmin.contains(id)) { - removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); - createdAdmin.remove(id); - } - return; - } - - // forcePlayerView hat Vorrang vor Perm und forceAdminView - boolean isAdmin = !forceSupporterView.contains(id) && !forcePlayerView.contains(id) - && (p.hasPermission(adminPermission) || forceAdminView.contains(id)); - boolean isSupporter = !isAdmin && (forceSupporterView.contains(id) - || (!forcePlayerView.contains(id) && p.hasPermission(supporterPermission))); - String rawTicker = stripColors(tickerText); - int tLen = Math.max(1, rawTicker.length()); - int tOff = (tickerPos.getOrDefault(id, 0) + tickerSpeed) % tLen; - tickerPos.put(id, tOff); - int rIdx = rainbowIdx.getOrDefault(id, 0); // wird von tickTitle aktualisiert - - String pn = p.getName(); - String rank = getRank(p); - String money = getMoney(p); - String srvRaw = p.getServer() != null ? p.getServer().getInfo().getName() : "?"; - String srv = srvRaw.isEmpty() ? srvRaw - : Character.toUpperCase(srvRaw.charAt(0)) + srvRaw.substring(1); - String comp = buildCompass(playerCompass.getOrDefault(id, "0")); - String hp = formatHealth(playerHealth.getOrDefault(id, 20.0)); - String hpNum = String.valueOf((int) Math.ceil(playerHealth.getOrDefault(id, 20.0) / 2.0)); - String ping = String.valueOf(p.getPing()); - String online = String.valueOf(ProxyServer.getInstance().getOnlineCount()); - // FIX: getPlayerLimit() gibt -1 wenn kein Limit gesetzt → "∞" anzeigen - int rawLimit = ProxyServer.getInstance().getConfig().getPlayerLimit(); - if (rawLimit <= 0) { - // Listener-Limit als Fallback - try { - java.util.Iterator limIt = - ProxyServer.getInstance().getConfig().getListeners().iterator(); - if (limIt.hasNext()) { - int listenerMax = limIt.next().getMaxPlayers(); - if (listenerMax > 0) rawLimit = listenerMax; - } - } catch (Exception ignored) {} - } - String maxpl = rawLimit > 0 ? String.valueOf(rawLimit) : "∞"; - String tps = isAdmin ? getTps(id) : ""; - String ram = isAdmin ? getRam() : ""; - String time = sdf.format(new Date()); - String date = sdfDate.format(new Date()); - String playtime = formatPlaytime(id); - // Neue Placeholders - String xCoord = String.valueOf(playerX.getOrDefault(id, 0)); - String yCoord = String.valueOf(playerY.getOrDefault(id, 0)); - String zCoord = String.valueOf(playerZ.getOrDefault(id, 0)); - String world = playerWorld.getOrDefault(id, srv); - String gamemode = playerGamemode.getOrDefault(id, "?"); - String exp = String.valueOf(playerExp.getOrDefault(id, 0)); - String food = String.valueOf(playerFood.getOrDefault(id, 20)); - String foodSym = formatFood(playerFood.getOrDefault(id, 20)); - String speed = String.format("%.1f", playerSpeed.getOrDefault(id, 0.2) * 10); - String uptime = getUptime(); - // News: aktuellen Ticker-Stand lesen (wird von tickNews aktualisiert) - String news = buildNewsTicker(newsPos.getOrDefault(id, 0)); - String servers = String.valueOf(ProxyServer.getInstance().getServers().size()); - String proxymem = getRam(); - - // ── TicketSystem ────────────────────────────────────────────────────── - String ticketMyOpenStr = String.valueOf(ticketMyOpen.getOrDefault(id, 0)); - String ticketTotalOpenStr = String.valueOf(ticketTotalOpen.get()); - String ticketTotalClaimedStr = String.valueOf(ticketTotalClaimed.get()); - String ticketRatingGoodStr = String.valueOf(ticketRatingGood.get()); - String ticketRatingBadStr = String.valueOf(ticketRatingBad.get()); - int _tGood = ticketRatingGood.get(); - int _tBad = ticketRatingBad.get(); - String ticketRatingPctStr = (_tGood + _tBad == 0) ? "-" - : String.valueOf(Math.round(_tGood * 100.0 / (_tGood + _tBad))); - - // Per-Zeile Rotation: Zeilen mit mehreren Inhalten wechseln automatisch - Map> lineMap = isAdmin ? adminLineMap - : isSupporter ? supporterLineMap - : playerLineMap; - if (lineMap.isEmpty()) lineMap = playerLineMap; - // Aktueller Page-Index basierend auf Zeit - int pageIdx = (rotationInterval > 0) - ? (int)((System.currentTimeMillis() / 1000) / rotationInterval) - : 0; - // Zeilen aufbauen: fuer jede Zeilennummer den aktuellen Inhalt waehlen - int lineCount = lineMap.isEmpty() ? 0 - : lineMap.keySet().stream().mapToInt(Integer::intValue).max().orElse(0); - List srcLines = new ArrayList<>(); - for (int ln = 1; ln <= lineCount; ln++) { - List variants = lineMap.get(ln); - if (variants == null || variants.isEmpty()) { - srcLines.add(""); - } else { - // Zeile mit 1 Variante = immer gleich; mit 2+ = rotieren - int vi = pageIdx % variants.size(); - srcLines.add(variants.get(vi)); - } - } - List lines = new ArrayList<>(); - boolean hasTicker = !tickerText.isEmpty() && !isAdmin && !isSupporter; - if (hasTicker) lines.add(ticker(rawTicker, tOff, rIdx)); - // Maximale Inhaltszeilen: MAX_LINES insgesamt (Ticker zählt als eine) - currentPlayerUuid = id; // für PAPI-Auflösung in ph() - for (String tpl : srcLines) { - if (lines.size() >= MAX_LINES) break; - lines.add(c(ph(tpl, pn, rank, money, srv, comp, hp, hpNum, ping, online, maxpl, tps, ram, time, playtime, - xCoord, yCoord, zCoord, world, gamemode, exp, food, foodSym, speed, uptime, servers, proxymem, date, news, - ticketMyOpenStr, ticketTotalOpenStr, ticketTotalClaimedStr, - ticketRatingGoodStr, ticketRatingBadStr, ticketRatingPctStr))); - } - // Immer genau MAX_LINES Zeilen (Rest mit Leerzeilen auffüllen) - if (lines.size() > MAX_LINES) lines = new ArrayList<>(lines.subList(0, MAX_LINES)); - while (lines.size() < MAX_LINES) lines.add(" "); - - // Objective-Name und Titel je nach Rolle - String activeObjName = isAdmin ? OBJ_NAME_ADMIN - : isSupporter ? OBJ_NAME_SUPP - : OBJ_NAME; - String titleStr = isAdmin ? rainbow(c(adminTitle), rIdx) - : isSupporter ? rainbow(c(supporterTitle), rIdx) - : rainbow(c(title), rIdx); - - // Wenn Rolle wechselt: altes Objective sauber entfernen - if (isAdmin) { - if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); } - if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); } - } else if (isSupporter) { - if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); } - if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); } - } else { - if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); } - if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); } - } - - Set activeCreated = isAdmin ? createdAdmin : isSupporter ? createdSupporter : created; - - // Objective CREATE (einmalig) oder UPDATE_TITLE - boolean justCreated = !activeCreated.contains(id); - ScoreboardObjective obj = new ScoreboardObjective(); - obj.setName(activeObjName); - if (justCreated) { - obj.setAction((byte) 0); // CREATE - net.md_5.bungee.api.chat.TextComponent titleComp = - new net.md_5.bungee.api.chat.TextComponent(""); - for (net.md_5.bungee.api.chat.BaseComponent bc : buildComponents(titleStr)) - titleComp.addExtra(bc); - obj.setValue(Either.right(titleComp)); - obj.setType(HealthDisplay.INTEGER); - sendPkt.invoke(p, obj); - - ScoreboardDisplay disp = new ScoreboardDisplay(); - disp.setPosition(1); // Sidebar - disp.setName(activeObjName); - sendPkt.invoke(p, disp); - - activeCreated.add(id); - } else { - obj.setAction((byte) 2); // UPDATE_TITLE - net.md_5.bungee.api.chat.TextComponent titleComp2 = - new net.md_5.bungee.api.chat.TextComponent(""); - for (net.md_5.bungee.api.chat.BaseComponent bc : buildComponents(titleStr)) - titleComp2.addExtra(bc); - obj.setValue(Either.right(titleComp2)); - obj.setType(HealthDisplay.INTEGER); - sendPkt.invoke(p, obj); - } - - // Scores + Teams - for (int i = 0; i < MAX_LINES; i++) { - if (i < lines.size()) { - ScoreboardScore score = new ScoreboardScore(); - score.setItemName(ENTRIES[i]); - score.setScoreName(activeObjName); - score.setAction((byte) 0); - score.setValue(lines.size() - i); - // NumberFormat.BLANK versteckt die Zahlen rechts - try { - Class ntCls = Class.forName("net.md_5.bungee.protocol.data.NumberFormat$Type"); - Object blank = ntCls.getMethod("valueOf", String.class).invoke(null, "BLANK"); - Class nfCls = Class.forName("net.md_5.bungee.protocol.data.NumberFormat"); - Object nf = nfCls.getDeclaredConstructor(ntCls, Object.class).newInstance(blank, null); - score.getClass().getMethod("setNumberFormat", nfCls).invoke(score, nf); - } catch (Exception ignored) {} - sendPkt.invoke(p, score); - - Team team = new Team(); - team.setName((isAdmin ? "vta" : isSupporter ? "vts" : "vt") + i); - // CREATE wenn das Objective gerade frisch angelegt wurde, sonst UPDATE - team.setMode(justCreated ? (byte) 0 : (byte) 2); - // Hex-Farben: BaseComponent[] als Container-TextComponent verpacken - net.md_5.bungee.api.chat.BaseComponent[] prefixComp = - buildComponents(lines.get(i)); - net.md_5.bungee.api.chat.TextComponent prefixContainer = - new net.md_5.bungee.api.chat.TextComponent(""); - for (net.md_5.bungee.api.chat.BaseComponent bc : prefixComp) - prefixContainer.addExtra(bc); - team.setPrefix(Either.right(prefixContainer)); - team.setSuffix(Either.right(new net.md_5.bungee.api.chat.TextComponent(""))); - team.setDisplayName(Either.right(new net.md_5.bungee.api.chat.TextComponent(""))); - team.setNameTagVisibility(Either.right(NameTagVisibility.ALWAYS)); - team.setCollisionRule(Either.right(CollisionRule.ALWAYS)); - team.setColor(Optional.of(21)); // RESET - team.setFriendlyFire((byte) 3); - team.setPlayers(new String[]{ ENTRIES[i] }); - sendPkt.invoke(p, team); - } else { - // Leeren Score entfernen - try { - Class cls = Class.forName("net.md_5.bungee.protocol.packet.ScoreboardScoreReset"); - Object pkt = cls.getDeclaredConstructor().newInstance(); - cls.getMethod("setEntity", String.class).invoke(pkt, ENTRIES[i]); - cls.getMethod("setObjective", String.class).invoke(pkt, activeObjName); - sendPkt.invoke(p, pkt); - } catch (Exception ignored) {} - } - } - } - - // ── Ticker & Rainbow ───────────────────────────────────────────────────── - - private String ticker(String raw, int offset, int rIdx) { - if (raw.isEmpty()) return " "; - String doubled = raw + raw; - String visible = doubled.substring(offset, offset + Math.min(tickerWidth, raw.length())); - return rainbow(visible, rIdx); - } - - /** - * Wave-Effekt: jeder Buchstabe bekommt eine interpolierte Farbe. - * - * mode="wave" → Farbwelle mit konfigurierbaren Farben (smooth interpoliert) - * mode="chars" → klassisch mit RAINBOW-Array - * mode="line" → gesamter Text in einer Farbe - * - * idx steigt pro Tick → Welle wandert von links nach rechts. - */ - private String rainbow(String text, int idx) { - if (!rainbowEnabled || text == null || text.isEmpty()) return text == null ? " " : text; - String plain = ChatColor.stripColor(text); - if (plain.isEmpty()) return text; - - if ("wave".equals(rainbowMode)) { - StringBuilder sb = new StringBuilder(); - boolean bold = text.contains("§l") || text.contains("&l"); - boolean italic = text.contains("§o") || text.contains("&o"); - boolean underline = text.contains("§n") || text.contains("&n"); - boolean strike = text.contains("§m") || text.contains("&m"); - String fmt = (bold ? "§l" : "") + (italic ? "§o" : "") - + (underline ? "§n" : "") + (strike ? "§m" : ""); - // Sichtbare Zeichen zählen - int visLen = 0; - for (char c : plain.toCharArray()) if (c != ' ') visLen++; - int charIdx = 0; - for (int i = 0; i < plain.length(); i++) { - char ch = plain.charAt(i); - if (ch == ' ') { sb.append(' '); continue; } - // JAWa-Style: Hue gleichmäßig über alle Buchstaben verteilt, wandert pro Tick - float hue = ((float) charIdx / Math.max(visLen, 1) + idx * this.waveSpeed) % 1.0f; - if (hue < 0) hue += 1.0f; - int[] rgb = waveColors != null - ? interpolateWaveColor(hue) // konfigurierte Farben - : hsbWave(hue); // Sinus-Regenbogen (Default) - sb.append('§').append('x'); - sb.append('§').append(String.format("%02X", rgb[0]).charAt(0)); - sb.append('§').append(String.format("%02X", rgb[0]).charAt(1)); - sb.append('§').append(String.format("%02X", rgb[1]).charAt(0)); - sb.append('§').append(String.format("%02X", rgb[1]).charAt(1)); - sb.append('§').append(String.format("%02X", rgb[2]).charAt(0)); - sb.append('§').append(String.format("%02X", rgb[2]).charAt(1)); - sb.append(fmt); - sb.append(ch); - charIdx++; - } - return sb.toString(); - } - if ("chars".equals(rainbowMode)) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < plain.length(); i++) { - sb.append(RAINBOW[(idx + i) % RAINBOW.length]); - sb.append(plain.charAt(i)); - } - return sb.toString(); - } - // mode="line": ganzer Text in einer Farbe - return RAINBOW[idx % RAINBOW.length] + plain; - } - - /** - * Interpoliert zwischen den konfigurierten Wellen-Farben. - * pos = 0.0 .. 1.0 (Position in der Welle) - * Wenn keine Farben konfiguriert: HSB-Fallback (voller Regenbogen) - */ - /** - * Verarbeitet %gradient:FARBE1:FARBE2:TEXT% Placeholder. - * - * Beispiele: - * %gradient:#FF0000:#0000FF:Hallo Welt% - * %gradient:&c:&9:> Player Info:% - * %gradient:#FF0000:#FFFF00:#0000FF:Drei Farben% (beliebig viele Stopps) - * - * Auch mit Formatierung: - * %gradient:#00FFFF:#0000FF:&l> Player Info:% - * - * Der Text darf Farb-/Formatcodes enthalten – sie werden nach jedem - * Farbcode wiederhergestellt. - */ - private String applyGradients(String input) { - if (input == null || !input.contains("%gradient:")) return input; - StringBuilder result = new StringBuilder(); - int i = 0; - while (i < input.length()) { - int start = input.indexOf("%gradient:", i); - if (start < 0) { result.append(input.substring(i)); break; } - result.append(input, i, start); - int end = input.indexOf("%", start + 10); - if (end < 0) { result.append(input.substring(start)); break; } - // Parse: %gradient:C1:C2:...:TEXT% - String inner = input.substring(start + 10, end); - // Letzter Teil ist der Text, vorherige Teile sind Farben - // Text beginnt nach dem letzten ':' der eine Farbe abschließt - // Strategie: Teile von links lesen solange sie Farben sind - java.util.List stops = new java.util.ArrayList<>(); - int colonIdx = 0; - while (colonIdx < inner.length()) { - // Nächsten ':' suchen - int nextColon = inner.indexOf(':', colonIdx); - if (nextColon < 0) break; - String candidate = inner.substring(colonIdx, nextColon); - int[] rgb = parseGradientColor(candidate); - if (rgb != null) { - stops.add(rgb); - colonIdx = nextColon + 1; - } else { - break; - } - } - if (stops.size() < 2) { - // Nicht genug Farben – unveraendert lassen - result.append(input, start, end + 1); - i = end + 1; - continue; - } - String text = inner.substring(colonIdx); - // Formatcodes aus Text extrahieren - String plain = ChatColor.stripColor(c(text)); - boolean bold = text.contains("&l") || text.contains("\u00A7l"); - boolean italic = text.contains("&o") || text.contains("\u00A7o"); - boolean underline = text.contains("&n") || text.contains("\u00A7n"); - boolean strike = text.contains("&m") || text.contains("\u00A7m"); - String fmt = (bold ? "\u00A7l" : "") + (italic ? "\u00A7o" : "") - + (underline ? "\u00A7n" : "") + (strike ? "\u00A7m" : ""); - // Gradient auf sichtbare Zeichen anwenden - int visLen = 0; - for (char ch : plain.toCharArray()) if (ch != ' ') visLen++; - int charIdx = 0; - for (char ch : plain.toCharArray()) { - if (ch == ' ') { result.append(' '); continue; } - float pos = visLen <= 1 ? 0f : (float) charIdx / (visLen - 1); - int[] rgb = interpolateGradient(stops, pos); - result.append('\u00A7').append('x'); - result.append('\u00A7').append(String.format("%02X", rgb[0]).charAt(0)); - result.append('\u00A7').append(String.format("%02X", rgb[0]).charAt(1)); - result.append('\u00A7').append(String.format("%02X", rgb[1]).charAt(0)); - result.append('\u00A7').append(String.format("%02X", rgb[1]).charAt(1)); - result.append('\u00A7').append(String.format("%02X", rgb[2]).charAt(0)); - result.append('\u00A7').append(String.format("%02X", rgb[2]).charAt(1)); - result.append(fmt); - result.append(ch); - charIdx++; - } - i = end + 1; - } - return result.toString(); - } - - private int[] parseGradientColor(String s) { - s = s.trim(); - // Hex: #RRGGBB oder &#RRGGBB - if (s.startsWith("&#")) s = s.substring(1); - if (s.startsWith("#") && s.length() == 7) { - try { - return new int[]{ - Integer.parseInt(s.substring(1,3),16), - Integer.parseInt(s.substring(3,5),16), - Integer.parseInt(s.substring(5,7),16) - }; - } catch (Exception ignored) {} - } - // MC &-Code: &0-&f - if (s.startsWith("&") && s.length() == 2) { - return mcColorToRgb(s.charAt(1)); - } - return null; - } - - private int[] interpolateGradient(java.util.List stops, float pos) { - if (stops.size() == 1) return stops.get(0); - float scaled = pos * (stops.size() - 1); - int i0 = Math.min((int) scaled, stops.size() - 2); - int i1 = i0 + 1; - float t = scaled - i0; - return new int[]{ - (int)(stops.get(i0)[0] * (1-t) + stops.get(i1)[0] * t), - (int)(stops.get(i0)[1] * (1-t) + stops.get(i1)[1] * t), - (int)(stops.get(i0)[2] * (1-t) + stops.get(i1)[2] * t) - }; - } - - /** Sinus-basierter Regenbogen (JAWa-Style) – smooth, volle Farbpalette */ - private static int[] hsbWave(float hue) { - double h = hue * 2 * Math.PI; - int r = (int)(Math.sin(h) * 127.5 + 127.5); - int g = (int)(Math.sin(h + 2*Math.PI/3) * 127.5 + 127.5); - int b = (int)(Math.sin(h + 4*Math.PI/3) * 127.5 + 127.5); - return new int[]{ - Math.max(0, Math.min(255, r)), - Math.max(0, Math.min(255, g)), - Math.max(0, Math.min(255, b)) - }; - } - - /** Wandelt einen Minecraft &-Farbcode (0-9, a-f) in RGB um */ - private static int[] mcColorToRgb(char code) { - switch (Character.toLowerCase(code)) { - case '0': return new int[]{ 0, 0, 0}; // §0 Schwarz - case '1': return new int[]{ 0, 0, 170}; // §1 Dunkelblau - case '2': return new int[]{ 0, 170, 0}; // §2 Dunkelgrün - case '3': return new int[]{ 0, 170, 170}; // §3 Dunkeltürkis - case '4': return new int[]{170, 0, 0}; // §4 Dunkelrot - case '5': return new int[]{170, 0, 170}; // §5 Lila - case '6': return new int[]{255, 170, 0}; // §6 Gold - case '7': return new int[]{170, 170, 170}; // §7 Grau - case '8': return new int[]{ 85, 85, 85}; // §8 Dunkelgrau - case '9': return new int[]{ 85, 85, 255}; // §9 Blau - case 'a': return new int[]{ 85, 255, 85}; // §a Hellgrün - case 'b': return new int[]{ 85, 255, 255}; // §b Türkis - case 'c': return new int[]{255, 85, 85}; // §c Hellrot - case 'd': return new int[]{255, 85, 255}; // §d Hellviolett - case 'e': return new int[]{255, 255, 85}; // §e Gelb - case 'f': return new int[]{255, 255, 255}; // §f Weiß - default: return null; - } - } - - private int[] interpolateWaveColor(float pos) { - if (waveColors == null || waveColors.length == 0) { - // HSB-Fallback: voller Regenbogen - java.awt.Color c = java.awt.Color.getHSBColor(pos, 1.0f, 1.0f); - return new int[]{c.getRed(), c.getGreen(), c.getBlue()}; - } - int n = waveColors.length; - float scaled = pos * n; - int i0 = (int) scaled % n; - int i1 = (i0 + 1) % n; - float t = scaled - (int) scaled; // 0.0 .. 1.0 zwischen i0 und i1 - int r = (int)(waveColors[i0][0] * (1-t) + waveColors[i1][0] * t); - int g = (int)(waveColors[i0][1] * (1-t) + waveColors[i1][1] * t); - int b = (int)(waveColors[i0][2] * (1-t) + waveColors[i1][2] * t); - return new int[]{r, g, b}; - } - - // ── Placeholder ─────────────────────────────────────────────────────────── - - private String ph(String tpl, String player, String rank, String money, String server, - String compass, String health, String healthNum, String ping, String online, String maxplayers, - String tps, String ram, String time, String playtime, - String x, String y, String z, String world, String gamemode, - String exp, String food, String foodSymbol, String speed, - String uptime, String servers, String proxymem, String date, String news, - String ticketMyOpen, String ticketTotalOpen, String ticketTotalClaimed, - String ticketRatingGood, String ticketRatingBad, String ticketRatingPct) { - if (tpl == null) return " "; - // PAPI-Werte zuerst einsetzen; native Tokens überschreiben sie danach - String s = resolvePapiPlaceholders(tpl, currentPlayerUuid); - s = s - .replace("%player%", player) .replace("%rank%", rank) - .replace("%money%", money) .replace("%server%", server) - .replace("%compass%", compass) .replace("%health%", health) - .replace("%hearts%", healthNum) .replace("%ping%", ping) - .replace("%online%", online) .replace("%maxplayers%", maxplayers) - .replace("%tps%", tps) .replace("%ram%", ram) - .replace("%time%", time) .replace("%playtime%", playtime) - .replace("%x%", x) .replace("%y%", y) - .replace("%z%", z) .replace("%world%", world) - .replace("%gamemode%", gamemode) .replace("%exp%", exp) - .replace("%food%", food) .replace("%foodsym%", foodSymbol) - .replace("%speed%", speed) - .replace("%uptime%", uptime) .replace("%servers%", servers) - .replace("%proxymem%", proxymem) - .replace("%date%", date) - .replace("%news%", news) - // ── TicketSystem ────────────────────────────────────────────────── - .replace("%ticket_my_open%", ticketMyOpen) - .replace("%ticket_open%", ticketTotalOpen) - .replace("%ticket_claimed%", ticketTotalClaimed) - .replace("%ticket_rating_good%", ticketRatingGood) - .replace("%ticket_rating_bad%", ticketRatingBad) - .replace("%ticket_rating_pct%", ticketRatingPct); - s = applyGradients(s); - s = s.replace("%line%", c(separator)); - return s.isEmpty() ? " " : s; - } - - private static String resolvePapiPlaceholders(String text, UUID uuid) { - if (text == null || !text.contains("%")) return text; - if (uuid == null) return text; - java.util.Map papiMap = net.viper.status.StatusAPI.playerPapi.get(uuid); - if (papiMap == null || papiMap.isEmpty()) return text; - StringBuilder sb = new StringBuilder(); - int i = 0; - while (i < text.length()) { - int start = text.indexOf('%', i); - if (start < 0) { sb.append(text.substring(i)); break; } - int end = text.indexOf('%', start + 1); - if (end < 0) { sb.append(text.substring(i)); break; } - String token = text.substring(start + 1, end); - if (papiMap.containsKey(token)) { - sb.append(text, i, start); - sb.append(papiMap.get(token)); - i = end + 1; - } else { - sb.append(text, i, end + 1); - i = end + 1; - } - } - return sb.toString(); - } - - // ── Daten-Helfer ───────────────────────────────────────────────────────── - - private String getRank(ProxiedPlayer p) { - 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, p.getUniqueId()); - if (usr != null) { - Class qo = Class.forName("net.luckperms.api.query.QueryOptions"); - Object opts = qo.getMethod("defaultContextualOptions").invoke(null); - Object cache= usr.getClass().getMethod("getCachedData").invoke(usr); - Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts); - Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta); - if (pfx != null && !pfx.toString().isEmpty()) return pfx.toString(); - Object pg = usr.getClass().getMethod("getPrimaryGroup").invoke(usr); - if (pg != null) return pg.toString().toUpperCase(); - } - } catch (Exception ignored) {} - return "Spieler"; - } - - private String getMoney(ProxiedPlayer p) { - try { - Map bal = (Map) StatusAPI.class.getField("playerBalances").get(null); - Object v = bal.get(p.getUniqueId()); - if (v != null) return df.format(((Number) v).doubleValue()); - } catch (Exception ignored) {} - return "0" + decimalSeparator + "00"; - } - - private String formatHealth(double hp) { - int full = (int)(hp / 2.0); - int half = (hp % 2 >= 1) ? 1 : 0; - int empty = 10 - full - half; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < full; i++) sb.append("\u2665"); // ♥ voll - if (half > 0) sb.append("\u2665"); // ♥ halb - for (int i = 0; i < empty; i++) sb.append("\u2661"); // ♡ leer - return sb.length() > 0 ? sb.toString() : "\u2661"; - } - - private String formatFood(int food) { - // Food 0-20, je 2 Punkte = ein Symbol, max 10 Slots - int full = food / 2; - int half = food % 2; - int empty = 10 - full - half; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < full; i++) sb.append("\u25C6"); // ◆ voll - if (half > 0) sb.append("\u25C7"); // ◇ halb - for (int i = 0; i < empty; i++) sb.append("\u25C7"); // ◇ leer - return sb.toString(); - } - - private String formatPlaytime(UUID id) { - Long joinTime = joinTimes.get(id); - if (joinTime == null) return "00 00:00:00"; - long seconds = (System.currentTimeMillis() - joinTime) / 1000; - long days = seconds / 86400; - long hours = (seconds % 86400) / 3600; - long mins = (seconds % 3600) / 60; - long secs = seconds % 60; - return String.format("%02d %02d:%02d:%02d", days, hours, mins, secs); - } - - /** - * Baut den scrollenden Kompass-Balken. - * - * Akzeptiert zwei Formate: - * - Richtungstext: "N", "NE", "E", "SE", "S", "SW", "W", "NW" (Bridge-Altformat) - * - Yaw als Float: "-45.5" (Minecraft-Yaw: 0=S, 90=W, 180=N, -90=E) - * - * Beispiel-Output (Blick nach N): "- - - - &c&lN&r&7 - - - -" - */ - /** - * Baut den Kompass-Balken mit Sub-Grad-Auflösung. - * - * Das Fenster hat COMPASS_WIN Slots (z.B. 9). Jeder Slot entspricht genau - * 1 Grad auf dem Kreis (COMPASS_SLOTS = 360). Dadurch verschiebt sich der - * Balken bei jeder 1°-Änderung um genau eine Position – kein Springen. - * - * Jeder Slot zeigt: - * - 'N' / 'E' / 'S' / 'W' wenn sein Grad-Slot mit einem Himmelsrichtungs- - * Label übereinstimmt (±0°, kein Runden) - * - '|' für den Mittelpunkt (aktuelle Blickrichtung), - * falls kein Label genau trifft - * - '·' für alle anderen Slots - * - * Akzeptierte raw-Formate: - * Float-String "normYaw" (0..360): Bridge sendet normYaw = ((yaw%360)+360)%360 - * Richtungstext "N"/"NE"/"E"/…: Legacy-Fallback - */ - /** - * Kompass-Balken: WIN=15 Slots, 1°/Slot, Himmelsrichtungen farbig. - * - * Zeichen: - * '─' normaler Slot (grau, &8) - * N/E/S/W außerhalb Mitte: gelb &e - * Mitte mit Himmelsrichtung: rot+fett &c&l - * Mitte ohne Himmelsrichtung: rot+fett &c&l '|' - * - * Bridge sendet normYaw 0..360 (0 = Süden/MC-Konvention). - * Umrechnung: facingDeg = (normYaw + 180) % 360 → 0=N, 90=E, 180=S, 270=W - */ - private static final int SCOREBOARD_WIDTH = 26; // sichtbare Breite des Scoreboards - - private String buildCompass(String raw) { - if (raw == null || raw.isEmpty()) raw = "180"; - - float facingDeg; - try { - float normYaw = Float.parseFloat(raw); - facingDeg = (normYaw + 180.0f) % 360.0f; - } catch (NumberFormatException e) { - switch (raw.trim().toUpperCase()) { - case "N": facingDeg = 0; break; - case "NE": facingDeg = 45; break; - case "E": facingDeg = 90; break; - case "SE": facingDeg = 135; break; - case "S": facingDeg = 180; break; - case "SW": facingDeg = 225; break; - case "W": facingDeg = 270; break; - case "NW": facingDeg = 315; break; - default: return raw; - } - } - - int half = COMPASS_WIN / 2; - int totalSlots = COMPASS_SLOTS / COMPASS_DEG_PER_SLOT; - int centerSlot = (Math.round(facingDeg) / COMPASS_DEG_PER_SLOT) % totalSlots; - - StringBuilder sb = new StringBuilder(); - for (int i = -half; i <= half; i++) { - int slot = ((centerSlot + i) % totalSlots + totalSlots) % totalSlots; - int slotDeg = (slot * COMPASS_DEG_PER_SLOT) % COMPASS_SLOTS; - - // Himmelsrichtungs-Label? - char label = 0; - for (int k = 0; k < COMPASS_LABEL_DEG.length; k++) { - if (slotDeg == COMPASS_LABEL_DEG[k]) { label = COMPASS_LABEL_CH[k]; break; } - } - - if (i == 0) { - // Mitte: rot + fett; Himmelsrichtung oder '|' als Cursor - char marker = (label != 0) ? label : '|'; - sb.append("&c&l").append(marker).append("&r&8"); - } else if (label != 0) { - // Himmelsrichtung außerhalb Mitte: gelb, gut sichtbar - sb.append("&e").append(label).append("&8"); - } else { - sb.append('-'); // ASCII-Strich, sicher für alle MC-Versionen - } - } - // Kompass zentrieren: Leerzeichen links = (Scoreboard-Breite - Kompass-Breite) / 2 - int padding = Math.max(0, (26 - COMPASS_WIN) / 2); - StringBuilder padded = new StringBuilder(); - for (int p = 0; p < padding; p++) padded.append(' '); - padded.append(sb); - return padded.toString(); - } - - /** - * Baut den News-Ticker: Text gleitet von rechts nach links durch ein fixes Fenster. - * - * Das Fenster ist IMMER exakt newsWidth Zeichen breit – Scoreboard-Breite konstant. - * Text erscheint von rechts, läuft durch, verschwindet links. - * Dann Pause (Leerzeichen) bevor der Text wieder von rechts einläuft. - * - * newsPrefix ist optional – leer lassen in Config zum Deaktivieren. - */ - private String buildNewsTicker(int offset) { - if (newsText == null || newsText.isEmpty()) return ""; - String plain = ChatColor.stripColor(c(newsText)).trim(); - if (plain.isEmpty()) return ""; - String prefix = (newsPrefix != null && !newsPrefix.isEmpty()) ? c(newsPrefix) : ""; - int pfxLen = ChatColor.stripColor(prefix).length(); - int winWidth = Math.max(4, newsWidth - pfxLen); - - // Text + 4 Leerzeichen Pause → dann sofort wieder von rechts - // Zyklus = plain.length() + 4 - int gap = 4; - int cycleLen = plain.length() + gap; - int pos = offset % cycleLen; - - // Virtuelles Band: plain + 4 Leerzeichen, läuft zyklisch - // Fenster zeigt winWidth Zeichen aus dem Band - // Band-Position des ersten Fensterzeichens: pos - winWidth + 1 - StringBuilder window = new StringBuilder(); - for (int i = 0; i < winWidth; i++) { - int bandIdx = pos - winWidth + 1 + i; - if (bandIdx < 0 || bandIdx >= plain.length()) { - window.append(' '); - } else { - window.append(plain.charAt(bandIdx)); - } - } - return prefix + c("&f") + window.toString(); - } - - private static final long START_TIME = System.currentTimeMillis(); - - private String getUptime() { - long s = (System.currentTimeMillis() - START_TIME) / 1000; - return String.format("%02d:%02d:%02d", s / 3600, (s % 3600) / 60, s % 60); - } - - private String getTps(UUID id) { - // Primär: TPS vom Backend-Server (per POST /scoreboard/tps gesendet) - Double t = playerTps.get(id); - if (t != null) { - return new DecimalFormat("0.0").format(Math.min(20.0, t)); - } - // FIX: Fallback auf Proxy-eigene TPS aus NetworkInfoModule (immer verfügbar) - if (networkInfoModule != null && networkInfoModule.isEnabled()) { - double proxyTps = networkInfoModule.getProxyTps(); - return new DecimalFormat("0.0").format(Math.min(20.0, proxyTps)) + " (P)"; - } - return "N/A"; - } - - private String getRam() { - MemoryMXBean m = ManagementFactory.getMemoryMXBean(); - return (m.getHeapMemoryUsage().getUsed() / 1048576) - + "MB/" + (m.getHeapMemoryUsage().getMax() / 1048576) + "MB"; - } - - // ── Component Builder (Hex-Farb-Support für Scoreboard) ───────────────────── - - /** - * Wandelt einen bereits mit c() prozessierten String (§-Codes + §x§R§R§G§G§B§B) - * in BaseComponent[] um, die BungeeCord korrekt ins Scoreboard-Packet schreibt. - * - * Hex-Farben werden als echte net.md_5.bungee.api.ChatColor RGB-Instanzen gesetzt, - * nicht als Legacy-String – das ist der einzige Weg der in Sidebar-Packets funktioniert. - */ - private static net.md_5.bungee.api.chat.BaseComponent[] buildComponents(String text) { - if (text == null || text.isEmpty()) - return new net.md_5.bungee.api.chat.BaseComponent[]{ - new net.md_5.bungee.api.chat.TextComponent("")}; - - java.util.List parts = new java.util.ArrayList<>(); - net.md_5.bungee.api.ChatColor currentColor = net.md_5.bungee.api.ChatColor.WHITE; - boolean bold = false, italic = false, underline = false, strike = false, magic = false; - - int i = 0; - StringBuilder buf = new StringBuilder(); - - while (i < text.length()) { - char c = text.charAt(i); - - // §x§R§R§G§G§B§B – RGB Hex (14 Zeichen) - if (c == '§' && i + 13 <= text.length() - 1 && text.charAt(i+1) == 'x') { - // Flush buffer - if (buf.length() > 0) { - parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic)); - buf.setLength(0); - } - try { - // §x§R§R§G§G§B§B: chars at i+3,i+5,i+7,i+9,i+11,i+13 are the hex digits - String hex = "" + text.charAt(i+3) + text.charAt(i+5) - + text.charAt(i+7) + text.charAt(i+9) - + text.charAt(i+11) + text.charAt(i+13); - currentColor = net.md_5.bungee.api.ChatColor.of("#" + hex); - // Formatierungen NICHT zurücksetzen – Bold/Italic bleiben erhalten - } catch (Exception ignored) {} - i += 14; - continue; - } - - // §X – normale Farb/Format-Codes - if (c == '§' && i + 1 < text.length()) { - char code = Character.toLowerCase(text.charAt(i + 1)); - if (buf.length() > 0) { - parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic)); - buf.setLength(0); - } - switch (code) { - case 'r': currentColor = net.md_5.bungee.api.ChatColor.WHITE; - bold=false; italic=false; underline=false; strike=false; magic=false; break; - case 'l': bold = true; break; - case 'o': italic = true; break; - case 'n': underline = true; break; - case 'm': strike = true; break; - case 'k': magic = true; break; - default: - net.md_5.bungee.api.ChatColor col = - net.md_5.bungee.api.ChatColor.getByChar(code); - if (col != null) { - currentColor = col; - bold=false; italic=false; underline=false; strike=false; magic=false; - } - } - i += 2; - continue; - } - - buf.append(c); - i++; - } - if (buf.length() > 0) - parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic)); - - if (parts.isEmpty()) - return new net.md_5.bungee.api.chat.BaseComponent[]{ - new net.md_5.bungee.api.chat.TextComponent("")}; - return parts.toArray(new net.md_5.bungee.api.chat.BaseComponent[0]); - } - - private static net.md_5.bungee.api.chat.TextComponent makeComp( - String text, net.md_5.bungee.api.ChatColor color, - boolean bold, boolean italic, boolean underline, boolean strike, boolean magic) { - net.md_5.bungee.api.chat.TextComponent tc = new net.md_5.bungee.api.chat.TextComponent(text); - tc.setColor(color); - if (bold) tc.setBold(true); - if (italic) tc.setItalic(true); - if (underline) tc.setUnderlined(true); - if (strike) tc.setStrikethrough(true); - if (magic) tc.setObfuscated(true); - return tc; - } - - // ── Farb-Hilfsmethoden ──────────────────────────────────────────────────── - - // ══════════════════════════════════════════════════════════════════════════ - // Farb-Parser: Birdflop-kompatibel - // Unterstützte Formate (alle gleichzeitig nutzbar): - // - // &#RRGGBB → Pro-Zeichen Hex (Birdflop Standard-Output) - // {#RRGGBB} → Bracket-Format - // <#RRGGBB> → MiniMessage Kurzform - // → MiniMessage color-Tag - // → Farbverlauf (beliebig viele Farb-Stopps) - // → Text in Schattenfarbe - // → Formatierungen - // &l &o &n &m &k &r → Standard-Formatierungen - // ══════════════════════════════════════════════════════════════════════════ - - private static String c(String s) { - if (s == null) return " "; - s = parseMiniMessage(s); // MiniMessage-Tags (, , <#>, , usw.) - s = parseHexAmpersand(s); // &#RRGGBB und {#RRGGBB} - return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', s); - } - - private static String stripColors(String s) { - return s == null ? "" : net.md_5.bungee.api.ChatColor.stripColor(c(s)); - } - - // ── MiniMessage Haupt-Dispatcher ───────────────────────────────────────── - - private static String parseMiniMessage(String text) { - if (text == null || !text.contains("<")) return text == null ? "" : text; - // gradient-Tags als erstes, weil sie anderen Text enthalten können - text = parseGradientTags(text); - // shadow-Tags - text = parseShadowTags(text); - // Einfache Tags: , <#>, , , , , , - text = parseSimpleTags(text); - return text; - } - - // ── ────────────────────────────────────────── - - private static String parseGradientTags(String text) { - if (!text.contains(" suchen (mit Tiefenzähler für verschachtelte <...>) - int end = findClosingAngle(text, start + 1); - if (end < 0) { result.append(text, i, text.length()); break; } - String inner = text.substring(start + 1, end); // "gradient:#C1:#C2:TEXT" - result.append(applyGradientTag(inner)); - i = end + 1; - } - return result.toString(); - } - - /** - * Parst "gradient:#C1:#C2:#C3:TEXT" → eingefärbten Text. - * TEXT darf selbst wieder §-Codes oder &-Codes enthalten (z.B. &l für Bold). - */ - private static String applyGradientTag(String inner) { - // inner = "gradient:COLOR:COLOR:...:TEXT" - // Farben beginnen mit # oder mit & gefolgt von einem Hex-Code - java.util.List colors = new java.util.ArrayList<>(); - // Trenne am ersten Doppelpunkt nach "gradient" - int firstColon = inner.indexOf(':'); // nach "gradient" - if (firstColon < 0) return inner; - String rest = inner.substring(firstColon + 1); - - // Lese Farb-Stopps (jeder Teil beginnt mit #) - // TEXT ist alles ab dem ersten Teil der NICHT mit # beginnt - StringBuilder textSb = new StringBuilder(); - boolean inText = false; - String[] parts = rest.split(":", -1); - for (int p = 0; p < parts.length; p++) { - String part = parts[p]; - if (!inText && part.startsWith("#") && part.length() == 7) { - colors.add(part); - } else { - // Ab hier Text (inkl. Doppelpunkte wieder zusammensetzen) - inText = true; - if (textSb.length() > 0) textSb.append(":"); - textSb.append(part); - } - } - if (colors.size() < 2) return textSb.toString(); - - // Shadow-Tags im Text zuerst auflösen (können im Gradient-Text stecken) - String rawText = parseShadowTags(textSb.toString()); - return applyGradient(rawText, colors); - } - - private static String applyGradient(String text, java.util.List colorStops) { - if (text == null || text.isEmpty()) return text; - // §-Codes und &-Codes aus Text herausfiltern für Längenberechnung - String plain = text - .replaceAll("\u00A7[0-9a-fk-orx]", "") - .replaceAll("&[0-9a-fA-Fk-orK-OR]", "") - .replaceAll("\u00A7x(\u00A7[0-9a-fA-F]){6}", ""); // §x§R§R§G§G§B§B - int len = plain.length(); - if (len == 0) return text; - if (len == 1) return resolveColorToSection(colorStops.get(0)) + text; - - int[][] rgbStops = new int[colorStops.size()][3]; - for (int s = 0; s < colorStops.size(); s++) rgbStops[s] = hexToRgb(colorStops.get(s)); - - StringBuilder result = new StringBuilder(); - int charIdx = 0; - int ci = 0; - while (ci < text.length()) { - char ch = text.charAt(ci); - - // §x§R§R§G§G§B§B durchreichen (bereits aufgelöste Hex-Farbe z.B. von shadow) - if (ch == '\u00A7' && ci + 1 < text.length() && text.charAt(ci + 1) == 'x') { - // Lese die 12 folgenden Zeichen (§x + 6x §digit) - if (ci + 13 < text.length() + 1) { - result.append(text, ci, Math.min(ci + 14, text.length())); - ci = Math.min(ci + 14, text.length()); - } else { - result.append(ch); ci++; - } - continue; - } - // §-Formatcode durchreichen - if (ch == '\u00A7' && ci + 1 < text.length()) { - result.append(ch).append(text.charAt(ci + 1)); ci += 2; continue; - } - // &-Formatcode durchreichen - if (ch == '&' && ci + 1 < text.length() && "&0123456789abcdefABCDEFklmnorKLMNOR".indexOf(text.charAt(ci+1)) >= 0) { - result.append(ch).append(text.charAt(ci + 1)); ci += 2; continue; - } - - // Normales Zeichen → Farbe interpolieren - float t = len <= 1 ? 0f : (float) charIdx / (len - 1); - int segments = colorStops.size() - 1; - float scaled = t * segments; - int seg = Math.min((int) scaled, segments - 1); - float segT = scaled - seg; - int[] c1 = rgbStops[seg], c2 = rgbStops[seg + 1]; - int r = clamp((int)(c1[0] + (c2[0] - c1[0]) * segT)); - int g = clamp((int)(c1[1] + (c2[1] - c1[1]) * segT)); - int b = clamp((int)(c1[2] + (c2[2] - c1[2]) * segT)); - String hex = String.format("%02X%02X%02X", r, g, b); - appendHexSection(result, hex); - result.append(ch); - charIdx++; - ci++; - } - return result.toString(); - } - - // ── ───────────────────────────────────────────────── - - private static String parseShadowTags(String text) { - if (text == null || !text.contains("= 0 ? inner.indexOf(':', firstColon + 1) : -1; - if (firstColon < 0 || secondColon < 0) { result.append(text, i, end + 1); i = end + 1; continue; } - String colorPart = inner.substring(firstColon + 1, secondColon).trim(); - String content = inner.substring(secondColon + 1); - result.append(resolveColorToSection(colorPart)).append(content); - i = end + 1; - } - return result.toString(); - } - - // ── Einfache MiniMessage-Tags ───────────────────────────────────────────── - - private static String parseSimpleTags(String text) { - if (text == null || !text.contains("<")) return text == null ? "" : text; - // Ersetzungstabelle - text = text.replace("", "&l").replace("", "&r"); - text = text.replace("", "&o").replace("", "&r"); - text = text.replace("", "&n").replace("", "&r"); - text = text.replace("", "&m").replace("", "&r"); - text = text.replace("", "&k").replace("", "&r"); - text = text.replace("", "&r").replace("", ""); - // Closing-Tags entfernen (werden nach Verarbeitung nicht mehr benötigt) - text = text.replaceAll("", ""); - text = text.replaceAll("", ""); - text = text.replaceAll("", ""); - // und <#RRGGBB> - StringBuilder result = new StringBuilder(); - int i = 0; - while (i < text.length()) { - char ch = text.charAt(i); - if (ch != '<') { result.append(ch); i++; continue; } - // - if (text.startsWith("', i); - if (end > 0) { - String hex = text.substring(i + 7, end).trim(); - if (hex.startsWith("#") && hex.length() == 7 && hex.substring(1).matches("[0-9a-fA-F]{6}")) { - appendHexSection(result, hex.substring(1)); - i = end + 1; continue; - } - } - } - // <#RRGGBB> - if (text.startsWith("<#", i) && i + 9 <= text.length()) { - int end = text.indexOf('>', i); - if (end == i + 8) { - String hex = text.substring(i + 2, end); - if (hex.matches("[0-9a-fA-F]{6}")) { - appendHexSection(result, hex); - i = end + 1; continue; - } - } - } - result.append(ch); i++; - } - return result.toString(); - } - - // ── &#RRGGBB und {#RRGGBB} ─────────────────────────────────────────────── - - private static String parseHexAmpersand(String text) { - if (text == null) return ""; - if (!text.contains("&#") && !text.contains("{#")) return text; - StringBuilder sb = new StringBuilder(); - int i = 0; - while (i < text.length()) { - // &#RRGGBB - if (i + 7 < text.length() + 1 && i + 8 <= 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}")) { - appendHexSection(sb, hex); i += 8; continue; - } - } - // {#RRGGBB} - 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}")) { - appendHexSection(sb, hex); i += 9; continue; - } - } - } - sb.append(text.charAt(i++)); - } - return sb.toString(); - } - - // ── Hilfsmethoden ───────────────────────────────────────────────────────── - - private static void appendHexSection(StringBuilder sb, String hex) { - sb.append('\u00A7').append('x'); - for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch); - } - - private static String resolveColorToSection(String color) { - if (color == null) return ""; - color = color.trim(); - if (color.startsWith("#") && color.length() == 7 - && color.substring(1).matches("[0-9a-fA-F]{6}")) { - StringBuilder sb = new StringBuilder(); - appendHexSection(sb, color.substring(1)); - return sb.toString(); - } - if (color.startsWith("&") && color.length() == 2) return "\u00A7" + color.charAt(1); - return color; - } - - private static int[] hexToRgb(String color) { - String hex = color == null ? "" : color.trim(); - if (hex.startsWith("#")) hex = hex.substring(1); - if (hex.length() != 6) return new int[]{255, 255, 255}; - try { - return new int[]{ - Integer.parseInt(hex.substring(0,2), 16), - Integer.parseInt(hex.substring(2,4), 16), - Integer.parseInt(hex.substring(4,6), 16) - }; - } catch (Exception e) { return new int[]{255,255,255}; } - } - - private static int clamp(int v) { return Math.max(0, Math.min(255, v)); } - - /** - * Findet das schließende '>' für ein Tag das bei fromIndex beginnt. - * Berücksichtigt verschachtelte <...>. - */ - private static int findClosingAngle(String text, int fromIndex) { - int depth = 0; - for (int i = fromIndex; i < text.length(); i++) { - char ch = text.charAt(i); - if (ch == '<') depth++; - else if (ch == '>') { if (depth == 0) return i; depth--; } - } - return -1; - } - - - - - // ── Config ─────────────────────────────────────────────────────────────── - - private void ensureConfigExists() { - File f = new File(plugin.getDataFolder(), CONFIG_FILE); - if (f.exists()) return; - if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs(); - String content = - "# ScoreboardModule Konfiguration\n" + - "# Platzhalter Spieler: %player% %rank% %money% %server% %compass% %health% %hearts% %date%\n" + - "# %ping% %online% %maxplayers% %time% %playtime% %news%\n" + - "# %x% %y% %z% %world% %gamemode% %exp% %food% %foodsym% %speed%\n" + - "# Platzhalter Admin: %tps% %ram% %proxymem% %uptime% %servers%\n" + - "# Gradient: %gradient:FARBE1:FARBE2:TEXT%\n" + - "# Sonstiges: %line%\n" + - "# Farben: &-Codes und Hex &#FF6600\n" + - "\n" + - "scoreboard.enabled=true\n" + - "scoreboard.update_interval=500\n" + - "scoreboard.title=&lViper Network\n" + - "scoreboard.admin_title=&l[Admin] Panel\n" + - "scoreboard.supporter_title=&l[Support] Panel\n" + - "\n" + - "scoreboard.ticker.text=\n" + - "scoreboard.ticker.width=26\n" + - "scoreboard.ticker.speed=1\n" + - "\n" + - "scoreboard.rainbow.enabled=true\n" + - "# wave=fließende Welle, chars=Regenbogen pro Buchstabe, line=eine Farbe\n" + - "scoreboard.rainbow.mode=wave\n" + - "# Wellengeschwindigkeit: 1=sehr langsam, 10=normal, 50=schnell\n" + - "scoreboard.rainbow.speed=10\n" + - "# Leer = voller HSB-Regenbogen\n" + - "scoreboard.rainbow.colors=#FF0000,#FF6600,#FFFF00,#00FF00,#00FFFF,#0000FF,#FF00FF\n" + - "\n" + - "scoreboard.admin_permission=statusapi.scoreboard.admin\n" + - "scoreboard.supporter_permission=statusapi.scoreboard.supporter\n" + - "\n" + - "scoreboard.time_format=HH:mm\n" + - "scoreboard.date_format=dd.MM.yyyy\n" + - "scoreboard.timezone=Europe/Berlin\n" + - "scoreboard.money_format=#,##0.00\n" + - "scoreboard.money_decimal_separator=,\n" + - "\n" + - "# SEPARATOR – wird als %line% Placeholder genutzt\n" + - "scoreboard.separator=&8&m--------------------\n" + - "# News-Ticker (erscheint als %news% Placeholder)\n" + - "scoreboard.news.text=&eWillkommen auf Viper Network!\n" + - "scoreboard.news.prefix=&8[&6News&8] &r\n" + - "scoreboard.news.width=20\n" + - "scoreboard.news.speed=1\n" + - "\n" + - "# ===================================================\n" + - "# NAMETAG - Prefix ueber dem Spieler-Kopf\n" + - "# ===================================================\n" + - "nametag.enabled=true\n" + - "\n" + - "scoreboard.rotation_interval=4\n" + - "# ===================================================\n" + - "# ZEILEN - max 15 sichtbar\n" + - "# ===================================================\n" + - "scoreboard.lines.1=%line%\n" + - "scoreboard.lines.2=%gradient:&6:&f:&6:&l> Player Info:%\n" + - "scoreboard.lines.3=&7%rank% &f%player%\n" + - "scoreboard.lines.4=\n" + - "scoreboard.lines.5=&7Spielzeit: &f%playtime%\n" + - "scoreboard.lines.5.2=&7Leben: &c%health%\n" + - "scoreboard.lines.5.3=&7Hunger: B4513%foodsym%\n" + - "scoreboard.lines.6=\n" + - "scoreboard.lines.7=%gradient:&6:&f:&6:&l> Money:%\n" + - "scoreboard.lines.8=&a$%money%\n" + - "scoreboard.lines.9=\n" + - "scoreboard.lines.10=%gradient:&6:&f:&6:&l> Server Info:%\n" + - "scoreboard.lines.11=&f%server%\n" + - "scoreboard.lines.11.2=&7Ping: &f%ping%ms &8| &7Online: &f%online%\n" + - "scoreboard.lines.12=\n" + - "scoreboard.lines.13=%news%\n" + - "scoreboard.lines.14=%line%\n" + - "scoreboard.lines.15=&7%compass%\n" + - "# ===================================================\n" + - "# ADMIN-ZEILEN\n" + - "# ===================================================\n" + - "scoreboard.admin_lines.1=%line%\n" + - "scoreboard.admin_lines.2=%gradient:&6:&f:&6:&l> Player Info:%\n" + - "scoreboard.admin_lines.3=&7%rank% &f%player%\n" + - "scoreboard.admin_lines.4=&7Gamemode: &f%gamemode%\n" + - "scoreboard.admin_lines.5=&7Leben: &c%health%\n" + - "scoreboard.admin_lines.5.2=&7Hunger: B4513%foodsym%\n" + - "scoreboard.admin_lines.6=\n" + - "scoreboard.admin_lines.7=%gradient:&6:&f:&6:&l> Server Info:%\n" + - "scoreboard.admin_lines.8=&f%server% &8| &7RAM: &e%ram%\n" + - "scoreboard.admin_lines.8.2=&7Proxy: &f%uptime%\n" + - "scoreboard.admin_lines.9=\n" + - "scoreboard.admin_lines.10=&7TPS: &a%tps%\n" + - "scoreboard.admin_lines.11=\n" + - "scoreboard.admin_lines.12=&7Spieler: &f%online% &8| &7%maxplayers%\n" + - "scoreboard.admin_lines.13=%news%\n" + - "scoreboard.admin_lines.14=%line%\n" + - "scoreboard.admin_lines.15=&7%compass%\n" + - "scoreboard.admin_lines.15.2=&7Pos: X:&f%x% &7Y:&f%y% &7Z:&f%z%\n" + - "# ===================================================\n" + - "# SUPPORTER-ZEILEN\n" + - "# ===================================================\n" + - "scoreboard.supporter_lines.1=%line%\n" + - "scoreboard.supporter_lines.2=%gradient:&6:&f:&6:&l> Support Panel:%\n" + - "scoreboard.supporter_lines.3=&7%rank% &f%player%\n" + - "scoreboard.supporter_lines.4=&7Ping: &f%ping%ms &8| &7%server%\n" + - "scoreboard.supporter_lines.5=\n" + - "scoreboard.supporter_lines.6=%gradient:&6:&f:&6:&l> Tickets:%\n" + - "scoreboard.supporter_lines.7=&7Offen: &c%ticket_open%\n" + - "scoreboard.supporter_lines.8=&7Meine Tickets: &e%ticket_my_open%\n" + - "scoreboard.supporter_lines.9=\n" + - "scoreboard.supporter_lines.10=%gradient:&6:&f:&6:&l> Server Info:%\n" + - "scoreboard.supporter_lines.11=&7Online: &f%online% &8/ &7%maxplayers%\n" + - "scoreboard.supporter_lines.12=&7Zeit: &f%time%\n" + - "scoreboard.supporter_lines.13=\n" + - "scoreboard.supporter_lines.14=%line%\n" + - "scoreboard.supporter_lines.15=&7%compass%\n" + - ""; - try (OutputStream out = new FileOutputStream(f)) { - out.write(content.getBytes(StandardCharsets.UTF_8)); - } catch (Exception e) { - plugin.getLogger().warning("[ScoreboardModule] Config: " + e.getMessage()); - } - } - - private void loadConfig() { - File file = new File(plugin.getDataFolder(), CONFIG_FILE); - Map map = new LinkedHashMap<>(); - if (file.exists()) { - 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("[ScoreboardModule] Ladefehler: " + e.getMessage()); - } - } - java.util.function.BiFunction g = (k,d) -> map.getOrDefault(k, d); - enabled = Boolean.parseBoolean(g.apply("scoreboard.enabled", "true")); - updateInterval = Math.max(250, pi(g.apply("scoreboard.update_interval", "500"), 500)); - title = g.apply("scoreboard.title", "&6&lViper Network"); - adminTitle = g.apply("scoreboard.admin_title", "&c&l[Admin] &4&lPanel"); - supporterTitle = g.apply("scoreboard.supporter_title", "&e&l[Support] &6&lPanel"); - tickerText = g.apply("scoreboard.ticker.text", " Viper Network "); - tickerWidth = pi(g.apply("scoreboard.ticker.width", "26"), 26); - tickerSpeed = pi(g.apply("scoreboard.ticker.speed", "1"), 1); - rainbowEnabled = Boolean.parseBoolean(g.apply("scoreboard.rainbow.enabled", "true")); - rainbowMode = g.apply("scoreboard.rainbow.mode", "wave").trim().toLowerCase(); - // Wellen-Farben laden - // waveSpeed: Hue-Verschiebung pro Tick - // speed=1 → 0.005 pro Tick → bei 500ms-Interval ~100s pro Zyklus - // speed=10 → 0.05 pro Tick → ~10s pro Zyklus (empfohlen) - // speed=50 → 0.25 pro Tick → ~2s pro Zyklus - waveSpeed = Float.parseFloat(g.apply("scoreboard.rainbow.speed", "10")) * 0.005f; - String waveColorsStr = g.apply("scoreboard.rainbow.colors", ""); - if (!waveColorsStr.isEmpty()) { - String[] parts = waveColorsStr.split(","); - java.util.List cols = new java.util.ArrayList<>(); - for (String part : parts) { - String p = part.trim(); - // Hex-Farbe: #RRGGBB oder &#RRGGBB - if (p.startsWith("&#")) p = p.substring(1); - if (p.startsWith("#") && p.length() == 7) { - try { - int r = Integer.parseInt(p.substring(1,3),16); - int g2= Integer.parseInt(p.substring(3,5),16); - int b = Integer.parseInt(p.substring(5,7),16); - cols.add(new int[]{r,g2,b}); - } catch (Exception ignored) {} - // Minecraft &-Farbe: &0-&9, &a-&f - } else if (p.startsWith("&") && p.length() == 2) { - int[] rgb = mcColorToRgb(p.charAt(1)); - if (rgb != null) cols.add(rgb); - } - } - waveColors = cols.isEmpty() ? null : cols.toArray(new int[0][]); - } else { - waveColors = null; // HSB-Fallback - } - adminPermission = g.apply("scoreboard.admin_permission", "statusapi.scoreboard.admin"); - supporterPermission = g.apply("scoreboard.supporter_permission", "statusapi.scoreboard.supporter"); - timeFormat = g.apply("scoreboard.time_format", "HH:mm"); - dateFormat = g.apply("scoreboard.date_format", "dd.MM.yyyy"); - timeZone = g.apply("scoreboard.timezone", "Europe/Berlin"); - moneyFormat = g.apply("scoreboard.money_format", "#,##0.00"); - decimalSeparator = g.apply("scoreboard.money_decimal_separator",","); - separator = g.apply("scoreboard.separator", "&8&m--------------------"); - try { - sdf = new java.text.SimpleDateFormat(timeFormat); - sdf.setTimeZone(java.util.TimeZone.getTimeZone(timeZone)); - sdfDate = new java.text.SimpleDateFormat(dateFormat); - sdfDate.setTimeZone(java.util.TimeZone.getTimeZone(timeZone)); - } catch (Exception e) { - sdf = new java.text.SimpleDateFormat("HH:mm"); - sdfDate = new java.text.SimpleDateFormat("dd.MM.yyyy"); - } - try { - df = new DecimalFormat(moneyFormat); - java.text.DecimalFormatSymbols sym = df.getDecimalFormatSymbols(); - if (!decimalSeparator.isEmpty()) sym.setDecimalSeparator(decimalSeparator.charAt(0)); - df.setDecimalFormatSymbols(sym); - } catch (Exception e) { df = new DecimalFormat("#,##0.00"); } - rotationInterval = pi(g.apply("scoreboard.rotation_interval", "4"), 4); - newsText = g.apply("scoreboard.news.text", ""); - newsPrefix = g.apply("scoreboard.news.prefix", "&8[&6News&8] &r"); - newsWidth = pi(g.apply("scoreboard.news.width", "20"), 20); - newsSpeed = pi(g.apply("scoreboard.news.speed", "1"), 1); - - // Pro Zeile alle Varianten laden: - // scoreboard.lines.N = Zeile N, Variante 1 (immer sichtbar) - // scoreboard.lines.N.2 = Zeile N, Variante 2 (rotiert nach interval) - // scoreboard.lines.N.3 = Zeile N, Variante 3 usw. - playerLineMap.clear(); - loadLineMap(map, "scoreboard.lines.", playerLineMap); - adminLineMap.clear(); - loadLineMap(map, "scoreboard.admin_lines.", adminLineMap); - supporterLineMap.clear(); - loadLineMap(map, "scoreboard.supporter_lines.", supporterLineMap); - - - plugin.getLogger().info("[ScoreboardModule] " - + playerLineMap.size() + " Player-Zeilen, " - + adminLineMap.size() + " Admin-Zeilen, " - + supporterLineMap.size() + " Supporter-Zeilen. RotInterval=" + rotationInterval + "s" -); - } - - /** - * Liest pro Zeilennummer alle Varianten aus der Config. - * - * Format: - * prefix + N = Variante 1 fuer Zeile N (immer sichtbar wenn nur 1 Variante) - * prefix + N + ".2" = Variante 2 fuer Zeile N (rotiert mit Variante 1) - * prefix + N + ".3" = Variante 3 fuer Zeile N usw. - * - * Zeilen ohne .2/.3 bleiben immer gleich. - * Zeilen MIT .2/.3 wechseln automatisch alle rotationInterval Sekunden. - */ - private void loadLineMap(Map map, String prefix, - Map> result) { - // Hoechste Zeilennummer finden - int maxLine = 0; - for (String key : map.keySet()) { - if (!key.startsWith(prefix)) continue; - String rest = key.substring(prefix.length()); - int dot = rest.indexOf('.'); - String numStr = (dot >= 0) ? rest.substring(0, dot) : rest; - try { - int ln = Integer.parseInt(numStr); - if (ln > maxLine) maxLine = ln; - } catch (NumberFormatException ignored) {} - } - - for (int ln = 1; ln <= maxLine; ln++) { - List variants = new ArrayList<>(); - // Variante 1 (kein Suffix) - String v1 = map.get(prefix + ln); - if (v1 != null) variants.add(v1); - // Variante 2, 3, ... (Suffix .2, .3, ...) - for (int p = 2; p <= 20; p++) { - String vp = map.get(prefix + ln + "." + p); - if (vp == null) break; - variants.add(vp); - } - if (!variants.isEmpty()) result.put(ln, variants); - else result.put(ln, java.util.Collections.singletonList("")); - } - } - - private int pi(String s, int fb) { - try { return Integer.parseInt(s == null ? "" : s.trim()); } - catch (Exception e) { return fb; } - } - - /** - * Entfernt ein Scoreboard-Objective und alle zugehörigen Teams sauber vom Client. - * Muss aufgerufen werden bevor ein anderes Objective aktiviert wird, - * sonst crasht der Client beim erneuten Team-CREATE. - * - * @param p Spieler - * @param objName Objective-Name (z.B. "vpsb" oder "vpsbadmin") - * @param teamPrefix Team-Prefix (z.B. "vt" oder "vta") - */ - private void removeObjectiveAndTeams(ProxiedPlayer p, String objName, String teamPrefix) { - // 1. Alle Teams löschen (Mode 1 = REMOVE) - for (int i = 0; i < 15; i++) { - try { - Team team = new Team(); - team.setName(teamPrefix + i); - team.setMode((byte) 1); // REMOVE - sendPkt.invoke(p, team); - } catch (Exception ignored) {} - } - // 2. Objective entfernen (Action 1 = REMOVE) - try { - ScoreboardObjective rem = new ScoreboardObjective(); - rem.setName(objName); - rem.setAction((byte) 1); - sendPkt.invoke(p, rem); - } catch (Exception ignored) {} - } - - // ── /scoreboard Toggle-Command ──────────────────────────────────────────── - /** - * /scoreboard → Scoreboard ein-/ausblenden - * /scoreboard hide → Scoreboard ausblenden - * /scoreboard show → Scoreboard einblenden - * /scoreboard player → Player-Scoreboard anzeigen - * /scoreboard admin → Admin-Scoreboard anzeigen (Perm: statusapi.scoreboard.admin) - * - * Aliase: /sb, /togglesb - */ - private static final List SB_SUBS = Arrays.asList("hide", "show", "player", "admin", "supporter"); - - /** Tab-Completion für /scoreboard via TabCompleteEvent */ - @EventHandler - public void onTabComplete(TabCompleteEvent event) { - if (!(event.getSender() instanceof ProxiedPlayer)) return; - String cursor = event.getCursor(); - if (cursor == null) return; - String lower = cursor.toLowerCase(); - boolean match = lower.startsWith("/scoreboard ") || lower.startsWith("/sb ") - || lower.startsWith("/togglesb "); - if (!match) return; - - ProxiedPlayer p = (ProxiedPlayer) event.getSender(); - int spaceIdx = cursor.indexOf(' '); - String typed = spaceIdx >= 0 ? cursor.substring(spaceIdx + 1).toLowerCase() : ""; - - List suggestions = new ArrayList<>(); - for (String sub : SB_SUBS) { - // Supporter und Admin nur anzeigen wenn Berechtigung vorhanden - if (sub.equals("admin") && !p.hasPermission(adminPermission)) continue; - if (sub.equals("supporter") && !p.hasPermission(supporterPermission) - && !p.hasPermission(adminPermission)) continue; - if (sub.startsWith(typed)) suggestions.add(sub); - } - event.getSuggestions().clear(); - event.getSuggestions().addAll(suggestions); - } - - - private class ScoreboardToggleCommand extends Command { - - ScoreboardToggleCommand() { - super("scoreboard", null, "sb", "togglesb"); - } - - @Override - public void execute(CommandSender sender, String[] args) { - if (!(sender instanceof ProxiedPlayer)) { - sender.sendMessage(new net.md_5.bungee.api.chat.TextComponent( - ChatColor.RED + "Nur für Spieler.")); - return; - } - ProxiedPlayer p = (ProxiedPlayer) sender; - UUID id = p.getUniqueId(); - - String sub = (args.length > 0) ? args[0].toLowerCase() : "toggle"; - - switch (sub) { - case "hide": - if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); } - if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); } - if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); } - hiddenPlayers.add(id); - p.sendMessage(msg("&7Scoreboard &causgeblendet&7. (/sb zum Einblenden)")); - break; - - case "show": - if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); } - if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); } - if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); } - hiddenPlayers.remove(id); - p.sendMessage(msg("&7Scoreboard &aeingeblendet&7.")); - break; - - case "player": - if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); } - if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); } - if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); } - forcePlayerView.add(id); - forceAdminView.remove(id); - forceSupporterView.remove(id); - hiddenPlayers.remove(id); - p.sendMessage(msg("&7Zeige &eSpieler&7-Scoreboard.")); - break; - - case "supporter": - if (!p.hasPermission(supporterPermission) && !p.hasPermission(adminPermission)) { - p.sendMessage(msg("&cKeine Berechtigung.")); - return; - } - if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); } - if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); } - if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); } - forceSupporterView.add(id); - forceAdminView.remove(id); - forcePlayerView.remove(id); - hiddenPlayers.remove(id); - p.sendMessage(msg("&7Zeige &6Supporter&7-Scoreboard.")); - break; - - case "admin": - if (!p.hasPermission(adminPermission)) { - p.sendMessage(msg("&cKeine Berechtigung.")); - return; - } - if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); } - if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); } - if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); } - forceAdminView.add(id); - forcePlayerView.remove(id); - forceSupporterView.remove(id); - hiddenPlayers.remove(id); - p.sendMessage(msg("&7Zeige &cAdmin&7-Scoreboard.")); - break; - - default: // "toggle" - if (hiddenPlayers.contains(id)) { - if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); } - if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); } - if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); } - hiddenPlayers.remove(id); - p.sendMessage(msg("&7Scoreboard &aeingeblendet&7.")); - } else { - if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); } - if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); } - if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); } - hiddenPlayers.add(id); - p.sendMessage(msg("&7Scoreboard &causgeblendet&7. (/sb zum Einblenden)")); - } - break; - } - } - - private net.md_5.bungee.api.chat.TextComponent msg(String text) { - return new net.md_5.bungee.api.chat.TextComponent(c(text)); - } - } - - -} \ No newline at end of file