Bossbar: kurzes Zustandswort statt Kicktitel, Platz kompakt

This commit is contained in:
MineTec committed 2026-10-17 21:51:11 +02:00
1 parent 7111e382a5
commit 17d79956b1
6 files changed
+90 -14

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
}
group = 'eu.mhsl.craftattack'
version = '1.0.0'
version = '1.1.0'
repositories {
mavenCentral()
@@ -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<Long> backoffSeconds,
String priorityPermission,
Set<String> temporaryTitles,
Set<String> temporaryKeys
Set<String> temporaryKeys,
Map<String, String> 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<String, String> labels(String value) {
Map<String, String> 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<String> split(String value) {
return Arrays.stream(value.split("\\|"))
.map(String::trim)
@@ -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()))
@@ -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"}
)
+9 -1
View File
@@ -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
@@ -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));
}