Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-05-10 15:06:25 +02:00
parent 2c3050c87a
commit a32f087353
9 changed files with 1957 additions and 34 deletions

View File

@@ -7,6 +7,7 @@ import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.module.ModuleManager;
import net.viper.status.modules.economy.EconomyModule;
import net.viper.status.modules.tablist.TablistModule;
import net.viper.status.modules.scoreboard.ScoreboardModule;
import net.viper.status.modules.antibot.AntiBotModule;
import net.viper.status.modules.network.NetworkInfoModule;
import net.viper.status.modules.AutoMessage.AutoMessageModule;
@@ -111,6 +112,7 @@ public class StatusAPI extends Plugin implements Runnable {
moduleManager.registerModule(new ServerSwitcherModule());
moduleManager.registerModule(new EconomyModule());
moduleManager.registerModule(new TablistModule());
moduleManager.registerModule(new ScoreboardModule());
try {
Class<?> forumBridge = Class.forName("net.viper.status.modules.forum.ForumBridgeModule");
@@ -122,6 +124,20 @@ public class StatusAPI extends Plugin implements Runnable {
moduleManager.enableAll(this);
// FIX: ScoreboardModule mit NetworkInfoModule verbinden (TPS-Fallback)
try {
net.viper.status.modules.scoreboard.ScoreboardModule sbMod =
(net.viper.status.modules.scoreboard.ScoreboardModule) moduleManager.getModule("ScoreboardModule");
net.viper.status.modules.network.NetworkInfoModule nimMod =
(net.viper.status.modules.network.NetworkInfoModule) moduleManager.getModule("NetworkInfoModule");
if (sbMod != null && nimMod != null) {
sbMod.setNetworkInfoModule(nimMod);
getLogger().info("[StatusAPI] ScoreboardModule → NetworkInfoModule TPS-Fallback verbunden.");
}
} catch (Exception e) {
getLogger().warning("[StatusAPI] TPS-Fallback konnte nicht verbunden werden: " + e.getMessage());
}
// WebServer starten
shuttingDown = false;
requestExecutor = Executors.newFixedThreadPool(4, r -> {
@@ -528,6 +544,8 @@ public class StatusAPI extends Plugin implements Runnable {
playerMap.put("last_seen", ps.lastSeen);
playerMap.put("playtime", ps.getPlaytimeWithCurrentSession());
playerMap.put("joins", ps.joins);
playerMap.put("kills", ps.kills);
playerMap.put("deaths", ps.deaths);
playerMap.put("online", ProxyServer.getInstance().getPlayer(ps.uuid) != null);
// Balance direkt aus MySQL (serverübergreifend)
EconomyModule ecoModPlayer = (EconomyModule) moduleManager.getModule("EconomyModule");
@@ -683,6 +701,79 @@ public class StatusAPI extends Plugin implements Runnable {
return;
}
// POST /stats/update Kills und Deaths eines Spielers aktualisieren (von Backend-Server via StatusAPIBridge)
if ("POST".equalsIgnoreCase(method) && "/stats/update".equalsIgnoreCase(pathOnly)) {
String body = readBody(in, headers);
StatsModule statsModUpd = (StatsModule) moduleManager.getModule("StatsModule");
if (statsModUpd == null) {
sendHttpResponse(out, "{\"success\":false,\"error\":\"stats_module_unavailable\"}", 503);
return;
}
String uuidStr = extractJsonString(body, "uuid");
String nameStr = extractJsonString(body, "name");
PlayerStats psUpd = resolvePlayer(uuidStr, nameStr, statsModUpd);
if (psUpd == null) {
// Spieler noch nicht bekannt → ignorieren (er hat sich noch nicht eingeloggt)
sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
return;
}
String killsStr = extractJsonString(body, "kills");
String deathsStr = extractJsonString(body, "deaths");
synchronized (psUpd) {
try { if (killsStr != null && !killsStr.isEmpty()) psUpd.kills = Integer.parseInt(killsStr.trim()); } catch (Exception ignored) {}
try { if (deathsStr != null && !deathsStr.isEmpty()) psUpd.deaths = Integer.parseInt(deathsStr.trim()); } catch (Exception ignored) {}
}
sendHttpResponse(out, "{\"success\":true}", 200);
return;
}
// POST /scoreboard/health Leben eines Spielers aktualisieren (von StatusAPIBridge)
if ("POST".equalsIgnoreCase(method) && "/scoreboard/health".equalsIgnoreCase(pathOnly)) {
String body = readBody(in, headers);
String uuidStr = extractJsonString(body, "uuid");
String healthStr = extractJsonString(body, "health");
if (uuidStr != null && !uuidStr.isEmpty() && healthStr != null && !healthStr.isEmpty()) {
try {
UUID hUuid = UUID.fromString(uuidStr.trim());
double health = Double.parseDouble(healthStr.trim());
net.viper.status.modules.scoreboard.ScoreboardModule.playerHealth.put(hUuid, health);
} catch (Exception ignored) {}
}
sendHttpResponse(out, "{\"success\":true}", 200);
return;
}
// POST /scoreboard/compass Himmelsrichtung eines Spielers (von StatusAPIBridge)
if ("POST".equalsIgnoreCase(method) && "/scoreboard/compass".equalsIgnoreCase(pathOnly)) {
String body = readBody(in, headers);
String uuidStr = extractJsonString(body, "uuid");
String compassStr = extractJsonString(body, "compass");
if (uuidStr != null && !uuidStr.isEmpty() && compassStr != null && !compassStr.isEmpty()) {
try {
UUID cUuid = UUID.fromString(uuidStr.trim());
net.viper.status.modules.scoreboard.ScoreboardModule.playerCompass.put(cUuid, compassStr.trim());
} catch (Exception ignored) {}
}
sendHttpResponse(out, "{\"success\":true}", 200);
return;
}
// POST /scoreboard/tps TPS des Spieler-Servers (von StatusAPIBridge)
if ("POST".equalsIgnoreCase(method) && "/scoreboard/tps".equalsIgnoreCase(pathOnly)) {
String body = readBody(in, headers);
String uuidStr = extractJsonString(body, "uuid");
String tpsStr = extractJsonString(body, "tps");
if (uuidStr != null && !uuidStr.isEmpty() && tpsStr != null && !tpsStr.isEmpty()) {
try {
UUID tUuid = UUID.fromString(uuidStr.trim());
double tps = Double.parseDouble(tpsStr.trim());
net.viper.status.modules.scoreboard.ScoreboardModule.playerTps.put(tUuid, tps);
} catch (Exception ignored) {}
}
sendHttpResponse(out, "{\"success\":true}", 200);
return;
}
// POST /player/world Welt eines Spielers aktualisieren (von StatusAPIBridge)
if ("POST".equalsIgnoreCase(method) && "/player/world".equalsIgnoreCase(pathOnly)) {
String body = readBody(in, headers);
@@ -697,6 +788,35 @@ public class StatusAPI extends Plugin implements Runnable {
return;
}
// POST /player/data Koordinaten, Gamemode, Exp, Food, Speed
if ("POST".equalsIgnoreCase(method) && "/player/data".equalsIgnoreCase(pathOnly)) {
String body = readBody(in, headers);
String uuidStr = extractJsonString(body, "uuid");
if (uuidStr != null && !uuidStr.isEmpty()) {
try {
UUID uid = UUID.fromString(uuidStr.trim());
String xS = extractJsonString(body, "x");
String yS = extractJsonString(body, "y");
String zS = extractJsonString(body, "z");
String gm = extractJsonString(body, "gamemode");
String expS= extractJsonString(body, "exp");
String fdS = extractJsonString(body, "food");
String spS = extractJsonString(body, "speed");
String wld = extractJsonString(body, "world");
if (xS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerX.put(uid, (int)Double.parseDouble(xS));
if (yS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerY.put(uid, (int)Double.parseDouble(yS));
if (zS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerZ.put(uid, (int)Double.parseDouble(zS));
if (gm != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerGamemode.put(uid, gm);
if (expS!= null) net.viper.status.modules.scoreboard.ScoreboardModule.playerExp.put(uid, (int)Double.parseDouble(expS));
if (fdS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerFood.put(uid, (int)Double.parseDouble(fdS));
if (spS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerSpeed.put(uid, Double.parseDouble(spS));
if (wld != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerWorld.put(uid, wld);
} catch (Exception ignored) {}
}
sendHttpResponse(out, "{\"success\":true}", 200);
return;
}
// GET Status-Endpunkt
if (inputLine.startsWith("GET")) {
Map<String, Object> data = new LinkedHashMap<>();
@@ -775,6 +895,8 @@ public class StatusAPI extends Plugin implements Runnable {
if (ps != null) {
playerInfo.put("playtime", ps.getPlaytimeWithCurrentSession());
playerInfo.put("joins", ps.joins);
playerInfo.put("kills", ps.kills);
playerInfo.put("deaths", ps.deaths);
playerInfo.put("first_seen", ps.firstSeen);
playerInfo.put("last_seen", ps.lastSeen);
// Balance direkt aus MySQL (serverübergreifend)

View File

@@ -47,6 +47,7 @@ public class AutoMessageModule implements Module {
public void onEnable(Plugin plugin) {
this.api = (StatusAPI) plugin;
loadSettings();
ensureMessagesFileExists();
if (!enabled) return;
@@ -59,6 +60,36 @@ public class AutoMessageModule implements Module {
cancelTask();
}
private void ensureMessagesFileExists() {
File dataFolder = api.getDataFolder();
if (!dataFolder.exists()) dataFolder.mkdirs();
File target = new File(dataFolder, fileName);
if (target.exists()) return;
// Datei aus den Plugin-Ressourcen kopieren
try (java.io.InputStream in = api.getResourceAsStream(fileName)) {
if (in != null) {
Files.copy(in, target.toPath());
api.getLogger().info("[AutoMessage] " + fileName + " wurde aus den Ressourcen erstellt.");
return;
}
} catch (IOException e) {
api.getLogger().warning("[AutoMessage] Konnte " + fileName + " nicht aus Ressourcen kopieren: " + e.getMessage());
}
// Fallback: leere Datei mit Hinweis anlegen
try {
Files.write(target.toPath(),
("# AutoMessage eine Nachricht pro Zeile\n" +
"# Farben mit & oder §-Codes, z.B. &aGrüner Text\n" +
"# Kommentarzeilen (# ...) und Leerzeilen werden ignoriert\n").getBytes(StandardCharsets.UTF_8));
api.getLogger().info("[AutoMessage] " + fileName + " wurde als leere Vorlage erstellt.");
} catch (IOException e) {
api.getLogger().severe("[AutoMessage] Konnte " + fileName + " nicht erstellen: " + e.getMessage());
}
}
private void loadSettings() {
Properties props = api.getVerifyProperties();
enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));

View File

@@ -65,6 +65,9 @@ public class NetworkInfoModule implements Module {
private long lastPlayerAlertAt = 0L;
private long lastTpsAlertAt = 0L;
private volatile double currentProxyTps = 20.0D;
/** FIX: Öffentlicher Getter damit ScoreboardModule als TPS-Fallback darauf zugreifen kann */
public double getProxyTps() { return currentProxyTps; }
private long lastTpsSampleAtMs = 0L;
private ScheduledTask alertTask;
private ScheduledTask tpsSamplerTask;

File diff suppressed because it is too large Load Diff

View File

@@ -53,7 +53,7 @@ public class ServerSwitcherModule implements Module {
private List<String> aliases = new ArrayList<>(Arrays.asList("wechsel", "switch"));
private List<String> serverWhitelist = new ArrayList<>();
private String colorHeader = "&8&m---&r &6&lServer-Menue &8&m---";
private String colorHeader = "&8&m---&r &6&lServer-Menü &8&m---";
private String colorEntry = "&7>> &e";
private String colorOnline = "&a";
private String colorOffline = "&c";

View File

@@ -11,6 +11,10 @@ public class PlayerStats {
public long currentSessionStart;
public int joins;
// Combat
public int kills;
public int deaths;
// Economy
public double balance;
public double totalEarned;
@@ -34,6 +38,8 @@ public class PlayerStats {
this.totalPlaytime = 0;
this.currentSessionStart = 0;
this.joins = 0;
this.kills = 0;
this.deaths = 0;
this.balance = 0.0;
this.totalEarned = 0.0;
this.totalSpent = 0.0;
@@ -70,11 +76,35 @@ public class PlayerStats {
return totalPlaytime;
}
/**
* Format: uuid|name|firstSeen|lastSeen|totalPlaytime|currentSessionStart|joins|
* kills|deaths|
* balance|totalEarned|totalSpent|transactionsCount|
* bansCount|mutesCount|warnsCount|lastPunishmentAt|lastPunishmentType|punishmentScore
*
* HINWEIS: kills/deaths wurden in Version 1.17.1 als Felder 7 und 8 eingefügt.
* fromLine() ist rückwärtskompatibel (alte Dateien ohne kills/deaths werden 0 gesetzt).
*/
public synchronized String toLine() {
String safeType = (lastPunishmentType == null ? "" : lastPunishmentType).replace("|", "_");
return uuid + "|" + name.replace("|", "_") + "|" + firstSeen + "|" + lastSeen + "|" + totalPlaytime + "|" + currentSessionStart + "|" + joins
+ "|" + balance + "|" + totalEarned + "|" + totalSpent + "|" + transactionsCount
+ "|" + bansCount + "|" + mutesCount + "|" + warnsCount + "|" + lastPunishmentAt + "|" + safeType + "|" + punishmentScore;
return uuid + "|" + name.replace("|", "_")
+ "|" + firstSeen
+ "|" + lastSeen
+ "|" + totalPlaytime
+ "|" + currentSessionStart
+ "|" + joins
+ "|" + kills
+ "|" + deaths
+ "|" + balance
+ "|" + totalEarned
+ "|" + totalSpent
+ "|" + transactionsCount
+ "|" + bansCount
+ "|" + mutesCount
+ "|" + warnsCount
+ "|" + lastPunishmentAt
+ "|" + safeType
+ "|" + punishmentScore;
}
public static PlayerStats fromLine(String line) {
@@ -84,26 +114,63 @@ public class PlayerStats {
UUID uuid = UUID.fromString(parts[0]);
String name = parts[1];
PlayerStats ps = new PlayerStats(uuid, name);
ps.firstSeen = Long.parseLong(parts[2]);
ps.lastSeen = Long.parseLong(parts[3]);
ps.totalPlaytime = Long.parseLong(parts[4]);
ps.firstSeen = Long.parseLong(parts[2]);
ps.lastSeen = Long.parseLong(parts[3]);
ps.totalPlaytime = Long.parseLong(parts[4]);
ps.currentSessionStart = Long.parseLong(parts[5]);
ps.joins = Integer.parseInt(parts[6]);
// Economy (felder 7-10)
if (parts.length >= 11) {
try { ps.balance = Double.parseDouble(parts[7]); } catch (Exception ignored) {}
try { ps.totalEarned = Double.parseDouble(parts[8]); } catch (Exception ignored) {}
try { ps.totalSpent = Double.parseDouble(parts[9]); } catch (Exception ignored) {}
try { ps.transactionsCount = Integer.parseInt(parts[10]); } catch (Exception ignored) {}
ps.joins = Integer.parseInt(parts[6]);
// Erkennung ob altes Format (ohne kills/deaths, Economy ab Index 7)
// oder neues Format (kills/deaths ab Index 7, Economy ab Index 9).
// Altes Format hat 17 Felder (Index 0-16), neues hat 19 (Index 0-18).
// Heuristik: Wenn parts[7] ein gültiger Integer ist UND parts[9] wie eine
// Gleitkommazahl aussieht → neues Format. Sonst altes Format.
boolean newFormat = false;
if (parts.length >= 19) {
try {
Integer.parseInt(parts[7]); // kills
Integer.parseInt(parts[8]); // deaths
Double.parseDouble(parts[9]); // balance (Gleitkomma)
newFormat = true;
} catch (Exception ignored) {}
}
// Punishments (felder 11-16)
if (parts.length >= 17) {
try { ps.bansCount = Integer.parseInt(parts[11]); } catch (Exception ignored) {}
try { ps.mutesCount = Integer.parseInt(parts[12]); } catch (Exception ignored) {}
try { ps.warnsCount = Integer.parseInt(parts[13]); } catch (Exception ignored) {}
try { ps.lastPunishmentAt = Long.parseLong(parts[14]); } catch (Exception ignored) {}
ps.lastPunishmentType = parts[15];
try { ps.punishmentScore = Integer.parseInt(parts[16]); } catch (Exception ignored) {}
if (newFormat) {
// Neues Format: kills ab [7], deaths ab [8], economy ab [9]
try { ps.kills = Integer.parseInt(parts[7]); } catch (Exception ignored) {}
try { ps.deaths = Integer.parseInt(parts[8]); } catch (Exception ignored) {}
if (parts.length >= 13) {
try { ps.balance = Double.parseDouble(parts[9]); } catch (Exception ignored) {}
try { ps.totalEarned = Double.parseDouble(parts[10]); } catch (Exception ignored) {}
try { ps.totalSpent = Double.parseDouble(parts[11]); } catch (Exception ignored) {}
try { ps.transactionsCount = Integer.parseInt(parts[12]); } catch (Exception ignored) {}
}
if (parts.length >= 19) {
try { ps.bansCount = Integer.parseInt(parts[13]); } catch (Exception ignored) {}
try { ps.mutesCount = Integer.parseInt(parts[14]); } catch (Exception ignored) {}
try { ps.warnsCount = Integer.parseInt(parts[15]); } catch (Exception ignored) {}
try { ps.lastPunishmentAt = Long.parseLong(parts[16]); } catch (Exception ignored) {}
ps.lastPunishmentType = parts[17];
try { ps.punishmentScore = Integer.parseInt(parts[18]); } catch (Exception ignored) {}
}
} else {
// Altes Format (kompatibel): kills/deaths = 0, Economy ab [7]
ps.kills = 0;
ps.deaths = 0;
if (parts.length >= 11) {
try { ps.balance = Double.parseDouble(parts[7]); } catch (Exception ignored) {}
try { ps.totalEarned = Double.parseDouble(parts[8]); } catch (Exception ignored) {}
try { ps.totalSpent = Double.parseDouble(parts[9]); } catch (Exception ignored) {}
try { ps.transactionsCount = Integer.parseInt(parts[10]); } catch (Exception ignored) {}
}
if (parts.length >= 17) {
try { ps.bansCount = Integer.parseInt(parts[11]); } catch (Exception ignored) {}
try { ps.mutesCount = Integer.parseInt(parts[12]); } catch (Exception ignored) {}
try { ps.warnsCount = Integer.parseInt(parts[13]); } catch (Exception ignored) {}
try { ps.lastPunishmentAt = Long.parseLong(parts[14]); } catch (Exception ignored) {}
ps.lastPunishmentType = parts[15];
try { ps.punishmentScore = Integer.parseInt(parts[16]); } catch (Exception ignored) {}
}
}
return ps;
} catch (Exception e) {

View File

@@ -10,11 +10,20 @@ import net.viper.status.module.Module;
import java.util.concurrent.TimeUnit;
/**
* StatsModule: Kümmert sich eigenständig um das Tracking der Spielerdaten.
* Implementiert Module (für das Lifecycle) und Listener (für die Events).
* StatsModule: Tracking von Spielerdaten (Playtime, Joins, Kills, Deaths).
*
* Fixes:
* - BUG-1: Crash-Recovery für currentSessionStart (verhindert falsche Spielzeit nach Absturz)
* - BUG-2: kills / deaths werden jetzt getrackt und per POST /stats/update aktualisiert
*/
public class StatsModule implements Module, Listener {
/**
* Maximale Sessionlänge nach einem Crash noch gutschreiben (24 Stunden).
* Längere Differenzen sind unrealistisch → werden ignoriert, currentSessionStart = 0 gesetzt.
*/
private static final long MAX_SESSION_SECONDS = 86_400L;
private StatsManager manager;
private StatsStorage storage;
@@ -25,21 +34,64 @@ public class StatsModule implements Module, Listener {
@Override
public void onEnable(Plugin plugin) {
// Initialisierung
manager = new StatsManager();
storage = new StatsStorage(plugin.getDataFolder());
// Laden
try {
storage.load(manager);
} catch (Exception e) {
plugin.getLogger().warning("Fehler beim Laden der Stats: " + e.getMessage());
}
// Event Listener registrieren
// -----------------------------------------------------------------------
// FIX BUG-1: Crash-Recovery offene Sessions bereinigen.
//
// Bei normalem Shutdown setzt onDisable() currentSessionStart = 0 und speichert.
// Bei einem Crash (kill -9, OOM, etc.) passiert das nicht. Beim nächsten Start
// sind alle Spieler offline, aber currentSessionStart enthält noch den alten
// Timestamp. getPlaytimeWithCurrentSession() würde dann fälschlicherweise
// (now - alter_crash_timestamp) zur Spielzeit addieren → massiv falscher Wert.
//
// Fix: Nach dem Laden jeden Eintrag prüfen. Falls currentSessionStart > 0:
// - Plausible Differenz (≤ MAX_SESSION_SECONDS) → als echte Zeit gutschreiben
// - Unplausibel (> MAX_SESSION_SECONDS) → verwerfen, nur zurücksetzen
// - In beiden Fällen: currentSessionStart = 0 setzen
// -----------------------------------------------------------------------
long now = System.currentTimeMillis() / 1000L;
int recovered = 0;
for (PlayerStats ps : manager.all()) {
synchronized (ps) {
if (ps.currentSessionStart > 0) {
long delta = now - ps.currentSessionStart;
if (delta > 0 && delta <= MAX_SESSION_SECONDS) {
ps.totalPlaytime += delta;
recovered++;
} else if (delta > MAX_SESSION_SECONDS) {
plugin.getLogger().warning(
"[StatsModule] Unplausibler currentSessionStart für " + ps.name
+ " (delta=" + delta + "s > " + MAX_SESSION_SECONDS + "s). "
+ "Session wird ohne Gutschrift zurückgesetzt."
);
}
ps.currentSessionStart = 0;
}
}
}
if (recovered > 0) {
plugin.getLogger().info(
"[StatsModule] Crash-Recovery: " + recovered + " offene Session(en) bereinigt und gespeichert."
);
try {
storage.save(manager);
} catch (Exception e) {
plugin.getLogger().warning("Fehler beim Speichern nach Crash-Recovery: " + e.getMessage());
}
}
// -----------------------------------------------------------------------
plugin.getProxy().getPluginManager().registerListener(plugin, this);
// Auto-Save Task (alle 5 Minuten)
// Auto-Save alle 5 Minuten
plugin.getProxy().getScheduler().schedule(plugin, () -> {
try {
storage.save(manager);
@@ -52,16 +104,13 @@ public class StatsModule implements Module, Listener {
@Override
public void onDisable(Plugin plugin) {
if (manager != null && storage != null) {
// Laufende Sessions beenden vor dem Speichern
long now = System.currentTimeMillis() / 1000L;
for (PlayerStats ps : manager.all()) {
synchronized (ps) {
if (ps.currentSessionStart > 0) {
long delta = now - ps.currentSessionStart;
if (delta > 0) {
ps.totalPlaytime += delta;
}
ps.currentSessionStart = 0; // Session beenden
if (delta > 0) ps.totalPlaytime += delta;
ps.currentSessionStart = 0;
}
}
}
@@ -73,7 +122,6 @@ public class StatsModule implements Module, Listener {
}
}
// Öffentlicher Zugriff für den WebServer
public StatsManager getManager() {
return manager;
}

View File

@@ -10,6 +10,12 @@ softdepend:
- Geyser-BungeeCord
commands:
# ── ScoreboardModule ──────────────────────────────────────
scoreboard:
description: Scoreboard ein-/ausblenden oder zwischen Player/Admin wechseln
usage: /scoreboard [hide|show|player|admin]
aliases: [sb, togglesb]
# ── Economy ───────────────────────────────────────────────
pay:
description: Überweise Geld an einen Spieler (auch offline)

View File

@@ -0,0 +1,115 @@
# ScoreboardModule Konfiguration
# Platzhalter Spieler: %player% %rank% %money% %server% %compass% %health% %hearts% %date%
# %ping% %online% %maxplayers% %time% %playtime% %news%
# %x% %y% %z% %world% %gamemode% %exp% %food% %foodsym% %speed%
# Platzhalter Admin: %tps% %ram% %proxymem% %uptime% %servers%
# Gradient: %gradient:FARBE1:FARBE2:TEXT% (beliebig viele Farb-Stopps)
# Sonstiges: %line%
# Farben: &-Codes und Hex &#FF6600
scoreboard.enabled=true
# Update-Intervall in Millisekunden - MINIMUM 250! (500 = 0.5s empfohlen)
scoreboard.update_interval=500
scoreboard.title=&lViper Network
scoreboard.admin_title=&l[Admin] Panel
# Laufschrift leer lassen zum Deaktivieren
scoreboard.ticker.text=
scoreboard.ticker.width=26
scoreboard.ticker.speed=1
scoreboard.rainbow.enabled=true
# wave = fließende Farbwelle | chars = Regenbogen pro Buchstabe | line = eine Farbe
scoreboard.rainbow.mode=wave
# Wellengeschwindigkeit: 1=sehr langsam, 10=normal, 50=schnell, 100=sehr schnell
scoreboard.rainbow.speed=10
# Farben: Hex (#RRGGBB oder &#RRGGBB) oder Minecraft-Codes (&0-&f) kommagetrennt
# Leer lassen = voller HSB-Regenbogen
scoreboard.rainbow.colors=#FF0000,#FF6600,#FFFF00,#00FF00,#00FFFF,#0000FF,#FF00FF
scoreboard.admin_permission=statusapi.scoreboard.admin
scoreboard.time_format=HH:mm
scoreboard.date_format=dd.MM.yyyy
scoreboard.timezone=Europe/Berlin
scoreboard.money_format=#,##0.00
scoreboard.money_decimal_separator=,
# ===================================================
# SEPARATOR wird als %line% Placeholder genutzt
# Wähle einen Stil oder erstelle deinen eigenen:
#
# scoreboard.separator=&8&m-------------------- (Standard)
# scoreboard.separator=&8&m==================== (Doppelt)
# scoreboard.separator=&8&m~~~~~~~~~~~~~~~~~~~~ (Wellig)
# scoreboard.separator=&8&m.................... (Punkte)
# scoreboard.separator=&8&m──────────────────── (Dünn)
# scoreboard.separator=&8&m════════════════════ (Dick)
# scoreboard.separator=&8◆◇◆◇◆◇◆◇◆◇◆◇◆◇◆◇◆◇◆◇ (Diamanten)
# scoreboard.separator=%gradient:&8:&7:────────────────────% (Gradient)
# scoreboard.separator=%gradient:#FF0000:#0000FF:────────────────────% (Farbig)
# scoreboard.separator= (Leer/unsichtbar)
# ===================================================
scoreboard.separator=&8&m--------------------
# ===================================================
# NEWS-TICKER erscheint als %news% Placeholder
# Leer lassen zum Deaktivieren
# ===================================================
scoreboard.news.text=&eWillkommen auf Viper Network!
scoreboard.news.prefix=&8[&6News&8] &r
scoreboard.news.width=26
# Geschwindigkeit: 1=langsam, 2=normal, 3=schnell
scoreboard.news.speed=1
# Sekunden pro Rotation (0 = kein Wechsel)
scoreboard.rotation_interval=4
# ===================================================
# ZEILEN max 15 sichtbar
# Rotation pro Zeile:
# scoreboard.lines.N = Variante 1 (immer sichtbar / nur Variante)
# scoreboard.lines.N.2 = Variante 2 (wechselt alle rotation_interval Sekunden)
# scoreboard.lines.N.3 = Variante 3 usw.
# Gradient: %gradient:FARBE1:FARBE2:TEXT%
# ===================================================
scoreboard.lines.1=%line%
scoreboard.lines.2=%gradient:&b:&f:&b:&l> Player Info:%
scoreboard.lines.3=&7%rank% &f%player%
scoreboard.lines.4=
scoreboard.lines.5=&7Spielzeit: &f%playtime%
scoreboard.lines.5.2=&7Leben: &c%health%
scoreboard.lines.5.3=&7Hunger: &#8B4513%foodsym%
scoreboard.lines.6=
scoreboard.lines.7=%gradient:&b:&f:&b:&l> Money:%
scoreboard.lines.8=&a$%money%
scoreboard.lines.9=
scoreboard.lines.10=%gradient:&b:&f:&b:&l> Server Info:%
scoreboard.lines.11=&f%server%
scoreboard.lines.11.2=&7Ping: &f%ping%ms &8| &7Online: &f%online%
scoreboard.lines.12=
scoreboard.lines.13=%news%
scoreboard.lines.14=%line%
scoreboard.lines.15=&7%compass%
# ===================================================
# ADMIN-ZEILEN
# ===================================================
scoreboard.admin_lines.1=%line%
scoreboard.admin_lines.2=%gradient:&b:&f:&b:&l> Player Info:%
scoreboard.admin_lines.3=&7%rank% &f%player%
scoreboard.admin_lines.4=&7Gamemode: &f%gamemode%
scoreboard.admin_lines.5=&7Leben: &c%health%
scoreboard.admin_lines.5.2=&7Hunger: &#8B4513%foodsym%
scoreboard.admin_lines.6=
scoreboard.admin_lines.7=%gradient:&b:&f:&b:&l> Server Info:%
scoreboard.admin_lines.8=&f%server% &8| &7RAM: &e%ram%
scoreboard.admin_lines.8.2=&7Proxy: &f%uptime%
scoreboard.admin_lines.9=
scoreboard.admin_lines.10=&7TPS: &a%tps%
scoreboard.admin_lines.11=
scoreboard.admin_lines.12=&7Spieler: &f%online% &8| &7%maxplayers%
scoreboard.admin_lines.13=%news%
scoreboard.admin_lines.14=%line%
scoreboard.admin_lines.15=&7%compass%
scoreboard.admin_lines.15.2=&7Pos: X:&f%x% &7Y:&f%y% &7Z:&f%z%