From 17d79956b104bb14e3d34007e8a1e1985b871e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 17 Oct 2026 21:51:11 +0200 Subject: [PATCH] Bossbar: kurzes Zustandswort statt Kicktitel, Platz kompakt --- build.gradle | 2 +- .../mhsl/craftattack/limboqueue/Config.java | 36 +++++++++++++++++-- .../craftattack/limboqueue/Dispatcher.java | 32 ++++++++++++----- .../craftattack/limboqueue/LimboQueue.java | 2 +- src/main/resources/config.properties | 10 +++++- .../limboqueue/KickClassifierTest.java | 22 ++++++++++++ 6 files changed, 90 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 270d32c..8a58d9a 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'eu.mhsl.craftattack' -version = '1.0.0' +version = '1.1.0' repositories { mavenCentral() diff --git a/src/main/java/eu/mhsl/craftattack/limboqueue/Config.java b/src/main/java/eu/mhsl/craftattack/limboqueue/Config.java index 79e7e85..1835160 100644 --- a/src/main/java/eu/mhsl/craftattack/limboqueue/Config.java +++ b/src/main/java/eu/mhsl/craftattack/limboqueue/Config.java @@ -7,7 +7,9 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; @@ -26,8 +28,18 @@ record Config( List backoffSeconds, String priorityPermission, Set temporaryTitles, - Set temporaryKeys + Set temporaryKeys, + Map stateLabels, + String unreachableLabel, + String unknownLabel, + String waitingLabel, + String nextLabel ) { + private static final String DEFAULT_STATE_LABELS = "Server closed=Neustart läuft|Server is restarting=Neustart läuft" + + "|Server für Spieler nicht verfügbar=Wartung|Hohe Serverauslastung=Server voll" + + "|Interner Serverfehler=Störung|Nutzerdaten nicht geladen=Anmeldung läuft" + + "|Connection throttled! Please wait before reconnecting.=Gleich geht's weiter"; + static Config load(Path dataDirectory) throws IOException { Path file = dataDirectory.resolve("config.properties"); if (Files.notExists(file)) { @@ -56,7 +68,12 @@ record Config( split(p.getProperty("backoff-seconds", "5|10|30")).stream().map(Long::parseLong).toList(), p.getProperty("priority-permission", "limboqueue.priority").trim(), Set.copyOf(split(p.getProperty("temporary-titles", ""))), - Set.copyOf(split(p.getProperty("temporary-keys", ""))) + Set.copyOf(split(p.getProperty("temporary-keys", ""))), + labels(p.getProperty("state-labels", DEFAULT_STATE_LABELS)), + p.getProperty("unreachable-label", "Neustart läuft").trim(), + p.getProperty("unknown-label", "Bitte warten").trim(), + p.getProperty("waiting-label", "Warteschlange").trim(), + p.getProperty("next-label", "Du bist als Nächstes dran").trim() ); if (config.intervalMs() < 100) throw new IllegalArgumentException("interval-ms muss mindestens 100 sein"); if (config.perTick() < 1) throw new IllegalArgumentException("per-tick muss mindestens 1 sein"); @@ -65,12 +82,27 @@ record Config( return config; } + /** Kurzes Wort für einen Kicktitel, z. B. „Wartung“. */ + String stateLabel(String kickTitle) { + return this.stateLabels.getOrDefault(kickTitle, this.unknownLabel); + } + /** Wartezeit nach dem n-ten Fehlschlag (ab 1); danach gilt der letzte Wert. */ long backoffMillis(int failures) { int index = Math.min(Math.max(failures, 1), this.backoffSeconds.size()) - 1; return this.backoffSeconds.get(index) * 1000; } + private static Map labels(String value) { + Map labels = new HashMap<>(); + for (String entry : split(value)) { + int separator = entry.lastIndexOf('='); + if (separator <= 0) throw new IllegalArgumentException("state-labels: '" + entry + "' braucht Titel=Wort"); + labels.put(entry.substring(0, separator).trim(), entry.substring(separator + 1).trim()); + } + return Map.copyOf(labels); + } + private static List split(String value) { return Arrays.stream(value.split("\\|")) .map(String::trim) diff --git a/src/main/java/eu/mhsl/craftattack/limboqueue/Dispatcher.java b/src/main/java/eu/mhsl/craftattack/limboqueue/Dispatcher.java index 8b5e71e..b3db4f9 100644 --- a/src/main/java/eu/mhsl/craftattack/limboqueue/Dispatcher.java +++ b/src/main/java/eu/mhsl/craftattack/limboqueue/Dispatcher.java @@ -7,6 +7,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; import org.slf4j.Logger; import java.util.Map; @@ -142,21 +143,24 @@ final class Dispatcher { continue; } - String name = this.config.targetDisplayName(); - String place = "Platz " + snapshot.position() + " von " + snapshot.size(); Component text; - BossBar.Color color = BossBar.Color.YELLOW; - if (snapshot.state() == WaitingQueue.State.CONNECTING) { - text = Component.text("Du wirst mit " + name + " verbunden ...", NamedTextColor.GREEN); + BossBar.Color color; + boolean paused = snapshot.state() == WaitingQueue.State.PAUSED; + if (snapshot.state() == WaitingQueue.State.CONNECTING || (reachable && !paused && snapshot.position() == 1)) { + text = Component.text(this.config.nextLabel(), NamedTextColor.GREEN); color = BossBar.Color.GREEN; } else if (!reachable) { - text = Component.text(name + " ist gerade nicht erreichbar · " + place, NamedTextColor.RED); + text = this.withPlace(this.config.unreachableLabel(), NamedTextColor.RED, snapshot); color = BossBar.Color.RED; - } else if (snapshot.state() == WaitingQueue.State.PAUSED && snapshot.lastReason() != null) { - text = Component.text(snapshot.lastReason() + " · " + place, NamedTextColor.RED); + } else if (paused) { + String label = snapshot.lastReason() == null + ? this.config.unknownLabel() + : this.config.stateLabel(snapshot.lastReason()); + text = this.withPlace(label, NamedTextColor.RED, snapshot); color = BossBar.Color.RED; } else { - text = Component.text("Warteschlange für " + name + " · " + place, NamedTextColor.YELLOW); + text = this.withPlace(this.config.waitingLabel(), NamedTextColor.YELLOW, snapshot); + color = BossBar.Color.YELLOW; } float progress = snapshot.size() <= 1 ? 1f : 1f - (float) (snapshot.position() - 1) / snapshot.size(); @@ -171,6 +175,16 @@ final class Dispatcher { } } + /** „Wartung · Platz 3 / 40“: Zustand farbig, Platz fett, Rest grau. */ + private Component withPlace(String label, NamedTextColor color, WaitingQueue.Snapshot snapshot) { + return Component.text() + .append(Component.text(label, color)) + .append(Component.text(" · Platz ", NamedTextColor.GRAY)) + .append(Component.text(snapshot.position(), NamedTextColor.WHITE, TextDecoration.BOLD)) + .append(Component.text(" / " + snapshot.size(), NamedTextColor.GRAY)) + .build(); + } + private boolean isOn(Player player, RegisteredServer server) { return player.getCurrentServer() .map(connection -> connection.getServerInfo().getName().equalsIgnoreCase(server.getServerInfo().getName())) diff --git a/src/main/java/eu/mhsl/craftattack/limboqueue/LimboQueue.java b/src/main/java/eu/mhsl/craftattack/limboqueue/LimboQueue.java index e4239e9..9d88196 100644 --- a/src/main/java/eu/mhsl/craftattack/limboqueue/LimboQueue.java +++ b/src/main/java/eu/mhsl/craftattack/limboqueue/LimboQueue.java @@ -15,7 +15,7 @@ import java.util.concurrent.TimeUnit; @Plugin( id = "limbo-queue", name = "limbo-queue", - version = "1.0.0", + version = "1.1.0", description = "Warteschlange im Limbo: holt Spieler nacheinander auf den Zielserver zurück", authors = {"MHSL"} ) diff --git a/src/main/resources/config.properties b/src/main/resources/config.properties index 57608e9..21b032c 100644 --- a/src/main/resources/config.properties +++ b/src/main/resources/config.properties @@ -4,9 +4,17 @@ # Server, auf dem gewartet wird, und Ziel der Warteschlange (Namen aus velocity.toml) limbo=limbo target=craftattack -# Name des Ziels in der Bossbar +# Name des Ziels (Log und Bossbar) target-display-name=CraftAttack +# Bossbar: kurzes Wort für den Zustand statt des ganzen Kicktitels. +# state-labels: Kicktitel=Wort; Titel ohne Eintrag zeigen unknown-label +state-labels=Server closed=Neustart läuft|Server is restarting=Neustart läuft|Server für Spieler nicht verfügbar=Wartung|Hohe Serverauslastung=Server voll|Interner Serverfehler=Störung|Nutzerdaten nicht geladen=Anmeldung läuft|Connection throttled! Please wait before reconnecting.=Gleich geht's weiter +unreachable-label=Neustart läuft +unknown-label=Bitte warten +waiting-label=Warteschlange +next-label=Du bist als Nächstes dran + # Alle interval-ms wird verbunden: höchstens per-tick Spieler, nie mehr als max-in-flight gleichzeitig interval-ms=1000 per-tick=1 diff --git a/src/test/java/eu/mhsl/craftattack/limboqueue/KickClassifierTest.java b/src/test/java/eu/mhsl/craftattack/limboqueue/KickClassifierTest.java index 8e79963..284b698 100644 --- a/src/test/java/eu/mhsl/craftattack/limboqueue/KickClassifierTest.java +++ b/src/test/java/eu/mhsl/craftattack/limboqueue/KickClassifierTest.java @@ -13,6 +13,7 @@ import java.util.Properties; import static eu.mhsl.craftattack.limboqueue.KickClassifier.Kind.FINAL; import static eu.mhsl.craftattack.limboqueue.KickClassifier.Kind.TEMPORARY; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; class KickClassifierTest { private final KickClassifier classifier = defaultClassifier(); @@ -85,6 +86,27 @@ class KickClassifierTest { assertEquals("", KickClassifier.title(Component.text("\n \n"))); } + @Test + void zustandswoerterFuerDieBossbar() throws IOException { + Properties properties = new Properties(); + try (var in = KickClassifierTest.class.getResourceAsStream("/config.properties")) { + properties.load(new InputStreamReader(in, StandardCharsets.UTF_8)); + } + Config fromFile = Config.fromProperties(properties); + Config defaults = Config.fromProperties(new Properties()); + for (Config config : new Config[]{fromFile, defaults}) { + assertEquals("Wartung", config.stateLabel("Server für Spieler nicht verfügbar")); + assertEquals("Neustart läuft", config.stateLabel("Server closed")); + assertEquals("Gleich geht's weiter", config.stateLabel("Connection throttled! Please wait before reconnecting.")); + assertEquals("Bitte warten", config.stateLabel("irgendwas")); + } + // jeder vorübergehende Titel hat ein eigenes Wort, und die Vorlage stimmt mit den Standardwerten überein + for (String title : fromFile.temporaryTitles()) { + assertNotEquals("Bitte warten", fromFile.stateLabel(title), title); + } + assertEquals(defaults.stateLabels(), fromFile.stateLabels()); + } + private KickClassifier.Kind classify(Component reason) { return this.classifier.classify(Optional.of(reason)); }