Bossbar: kurzes Zustandswort statt Kicktitel, Platz kompakt
This commit is contained in:
1 parent
7111e382a5
commit
17d79956b1
6 files changed
+90
-14
No files matched your search
@@ -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"}
|
||||
)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user