Server-Symbole nur für fremde Server, Tabliste als Spectator, Warteraum statt Limbo

This commit is contained in:
MineTec committed 2026-10-17 23:09:02 +02:00
1 parent 40edf64352
commit 5e7b57689c
10 files changed
+71 -50

No files matched your search

+6 -3
View File
@@ -8,8 +8,11 @@ Warum so herum: Velocity kann eine signierte Chat-Nachricht (ab 1.19.1) weder ä
ohne den Spieler zu trennen. Der Proxy lässt sie deshalb zum Backend durch, schaut nur zu und ohne den Spieler zu trennen. Der Proxy lässt sie deshalb zum Backend durch, schaut nur zu und
verteilt sie selbst. verteilt sie selbst.
- **Öffentlicher Chat** mit Server-Kürzel (`[Arena]`, `[Limbo]` …, der Hauptserver ohne), Namen - **Öffentlicher Chat:** Spieler auf einem anderen Server als man selbst bekommen ein dunkelgraues
von Spielern im Netzwerk werden golden als `@Name` hervorgehoben und hören einen Ton. Symbol vor den Namen (⛏ CraftAttack, ◎ Arena, ⌛ Warteraum, ⚄ Event, ♜ Weltmuseum; Hover: „… ist
gerade in der Arena“), Spieler vom eigenen Server keins. Gilt auch für /msg, Join/Leave und die
Tabliste. Einfache Zeichen ohne Klammern, damit sie nicht mit Rängen ([🌟], 👑, 🗡 …) verwechselt
werden. Namen von Spielern im Netzwerk werden golden als `@Name` hervorgehoben und hören einen Ton.
- **Name:** Rang-Prefixe schickt spawn (`DisplayName` → Kanal `craftattack:chat`, gilt nur solange - **Name:** Rang-Prefixe schickt spawn (`DisplayName` → Kanal `craftattack:chat`, gilt nur solange
der Spieler auf diesem Server ist), blau mit Permission `adminmarker`, 🎮 für Bedrock. der Spieler auf diesem Server ist), blau mit Permission `adminmarker`, 🎮 für Bedrock.
- **/msg** (`tell`, `w`, `whisper`), **/r**, **/adminchat** (Permission `admin`). - **/msg** (`tell`, `w`, `whisper`), **/r**, **/adminchat** (Permission `admin`).
@@ -21,7 +24,7 @@ verteilt sie selbst.
Velocity speichert sie in `plugins/chat-bridge/settings.properties` und gibt sie beim Join an spawn Velocity speichert sie in `plugins/chat-bridge/settings.properties` und gibt sie beim Join an spawn
zurück, damit die Arena dieselben Werte zeigt. Unbekannte Spieler übernimmt Velocity nur vom zurück, damit die Arena dieselben Werte zeigt. Unbekannte Spieler übernimmt Velocity nur vom
`settings-server` (craftattack), damit die Defaults der Arena keine echten Werte verdrängen. `settings-server` (craftattack), damit die Defaults der Arena keine echten Werte verdrängen.
- **Tabliste:** Spieler anderer Server stehen unten mit Server dahinter (`tab-tags`, sonst Chat-Kürzel). - **Tabliste:** Spieler anderer Server stehen unten, mit Symbol, grau und als Spectator (kursiv).
Die Einträge vom eigenen Server schickt weiter das Backend (Skins, Spielmodus, Prefixe). Abgleich Die Einträge vom eigenen Server schickt weiter das Backend (Skins, Spielmodus, Prefixe). Abgleich
alle 2 s und kurz nach Join, Wechsel und Quit. alle 2 s und kurz nach Join, Wechsel und Quit.
- **Log:** Chat, /msg, Adminchat und Mutes stehen im Velocity-Log (`[chat]`, `[msg]`, `[adminchat]`, `[mute]`). - **Log:** Chat, /msg, Adminchat und Mutes stehen im Velocity-Log (`[chat]`, `[msg]`, `[adminchat]`, `[mute]`).
+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
} }
group = 'eu.mhsl.craftattack' group = 'eu.mhsl.craftattack'
version = '1.1.0' version = '1.2.0'
repositories { repositories {
mavenCentral() mavenCentral()
@@ -58,13 +58,19 @@ final class Chat {
} }
Set<Player> mentioned = new LinkedHashSet<>(); Set<Player> mentioned = new LinkedHashSet<>();
Component highlighted = this.line(server, sender, this.body(message, mentioned)); Component highlighted = this.body(message, mentioned);
Component plain = this.line(server, sender, Component.text(message)); Component plain = Component.text(message);
Component name = this.names.clickable(sender);
for (Player player : this.proxy.getAllPlayers()) { for (Player player : this.proxy.getAllPlayers()) {
if (server != null && this.config.unsilencedServers().contains(server) && server.equals(serverOf(player))) continue; String viewerServer = serverOf(player);
if (server != null && this.config.unsilencedServers().contains(server) && server.equals(viewerServer)) continue;
ChatSettings.Values values = this.settings.of(player.getUniqueId()); ChatSettings.Values values = this.settings.of(player.getUniqueId());
player.sendMessage(values.highlightMentions() ? highlighted : plain); player.sendMessage(Component.text()
.append(this.names.marker(sender, viewerServer))
.append(name)
.append(Component.text(" > ", GRAY))
.append((values.highlightMentions() ? highlighted : plain).colorIfAbsent(SILVER)));
if (mentioned.contains(player) && !player.equals(sender) && values.mentionSound()) player.playSound(MENTION); if (mentioned.contains(player) && !player.equals(sender) && values.mentionSound()) player.playSound(MENTION);
} }
@@ -72,26 +78,18 @@ final class Chat {
} }
void announceJoin(Player player) { void announceJoin(Player player) {
this.announce(Component.text(">>> ", NamedTextColor.GREEN).append(this.names.clickable(player))); this.announce(player, Component.text(">>> ", NamedTextColor.GREEN));
} }
void announceLeave(Player player) { void announceLeave(Player player) {
this.announce(Component.text("<<< ", NamedTextColor.RED).append(this.names.clickable(player))); this.announce(player, Component.text("<<< ", NamedTextColor.RED));
} }
private void announce(Component line) { private void announce(Player subject, Component arrow) {
Component name = this.names.clickable(subject);
this.proxy.getAllPlayers().stream() this.proxy.getAllPlayers().stream()
.filter(player -> this.settings.of(player.getUniqueId()).joinAndLeave()) .filter(player -> this.settings.of(player.getUniqueId()).joinAndLeave())
.forEach(player -> player.sendMessage(line)); .forEach(player -> player.sendMessage(arrow.append(this.names.marker(subject, serverOf(player))).append(name)));
}
private Component line(@Nullable String server, Player sender, Component body) {
return Component.text()
.append(this.tag(server))
.append(this.names.clickable(sender))
.append(Component.text(" > ", GRAY))
.append(body.colorIfAbsent(SILVER))
.build();
} }
/** Meldet einem gestummten Spieler die Restzeit; true, wenn er gestummt ist. */ /** Meldet einem gestummten Spieler die Restzeit; true, wenn er gestummt ist. */
@@ -104,12 +102,6 @@ final class Chat {
return true; return true;
} }
private Component tag(@Nullable String server) {
String tag = server == null ? null : this.config.serverTags().get(server);
if (tag == null) return Component.empty();
return Component.text("[" + tag + "] ", NamedTextColor.DARK_GRAY);
}
/** Text der Nachricht; Namen von Spielern im Netzwerk werden golden als @Name hervorgehoben. */ /** Text der Nachricht; Namen von Spielern im Netzwerk werden golden als @Name hervorgehoben. */
private Component body(String message, Set<Player> mentioned) { private Component body(String message, Set<Player> mentioned) {
Map<String, Player> online = new HashMap<>(); Map<String, Player> online = new HashMap<>();
@@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit;
@Plugin( @Plugin(
id = "chat-bridge", id = "chat-bridge",
name = "chat-bridge", name = "chat-bridge",
version = "1.1.0", version = "1.2.0",
description = "Serverübergreifender Chat und Tabliste: öffentlicher Chat, /msg, Adminchat, Mute, Join/Leave", description = "Serverübergreifender Chat und Tabliste: öffentlicher Chat, /msg, Adminchat, Mute, Join/Leave",
authors = {"MHSL"}, authors = {"MHSL"},
dependencies = {@Dependency(id = "luckperms")} dependencies = {@Dependency(id = "luckperms")}
@@ -71,6 +71,6 @@ public final class ChatBridge {
this.proxy.getScheduler().buildTask(this, tabLists::refresh).repeat(TAB_REFRESH_SECONDS, TimeUnit.SECONDS).schedule(); this.proxy.getScheduler().buildTask(this, tabLists::refresh).repeat(TAB_REFRESH_SECONDS, TimeUnit.SECONDS).schedule();
Commands.register(this, this.proxy, this.logger, config, names, chat, privateMessages, mutes); Commands.register(this, this.proxy, this.logger, config, names, chat, privateMessages, mutes);
this.logger.info("chat-bridge aktiv, Kürzel {}, ohne Silencer {}", config.serverTags(), config.unsilencedServers()); this.logger.info("chat-bridge aktiv, Symbole {}, ohne Silencer {}", config.serverSymbols(), config.unsilencedServers());
} }
} }
@@ -19,8 +19,8 @@ import java.util.stream.Collectors;
* kommentierte Vorlage aus dem JAR geschrieben. * kommentierte Vorlage aus dem JAR geschrieben.
*/ */
record Config( record Config(
Map<String, String> serverTags, Map<String, String> serverSymbols,
Map<String, String> tabTags, Map<String, String> serverWhere,
Set<String> unsilencedServers, Set<String> unsilencedServers,
String settingsServer, String settingsServer,
String adminPermission, String adminPermission,
@@ -45,8 +45,8 @@ record Config(
static Config fromProperties(Properties p) { static Config fromProperties(Properties p) {
return new Config( return new Config(
pairs(p.getProperty("server-tags", "")), pairs(p.getProperty("server-symbols", "")),
pairs(p.getProperty("tab-tags", "")), pairs(p.getProperty("server-where", "")),
Set.copyOf(split(p.getProperty("unsilenced-servers", ""))), Set.copyOf(split(p.getProperty("unsilenced-servers", ""))),
p.getProperty("settings-server", "craftattack").trim(), p.getProperty("settings-server", "craftattack").trim(),
p.getProperty("admin-permission", "admin").trim(), p.getProperty("admin-permission", "admin").trim(),
@@ -44,6 +44,11 @@ final class Names {
/** Name ohne Klick, z. B. für die eigene Seite einer Privatnachricht. */ /** Name ohne Klick, z. B. für die eigene Seite einer Privatnachricht. */
Component plain(Player player) { Component plain(Player player) {
return this.plain(player, NamedTextColor.WHITE);
}
/** Name ohne Klick; {@code color} gilt für Nicht-Admins. */
Component plain(Player player, TextColor color) {
TextComponent.Builder name = Component.text(); TextComponent.Builder name = Component.text();
Prefix prefix = this.prefixes.get(player.getUniqueId()); Prefix prefix = this.prefixes.get(player.getUniqueId());
@@ -60,7 +65,7 @@ final class Names {
} }
boolean marked = player.hasPermission(this.config.markerPermission()); boolean marked = player.hasPermission(this.config.markerPermission());
name.append(Component.text(player.getUsername(), marked ? ADMIN : NamedTextColor.WHITE)); name.append(Component.text(player.getUsername(), marked ? ADMIN : color));
return name.build(); return name.build();
} }
@@ -71,6 +76,23 @@ final class Names {
.clickEvent(ClickEvent.suggestCommand("/msg " + player.getUsername() + " ")); .clickEvent(ClickEvent.suggestCommand("/msg " + player.getUsername() + " "));
} }
/**
* Symbol des Servers, auf dem {@code player} ist, samt Leerzeichen; leer, wenn der Betrachter auf
* demselben Server ist oder der Server kein Symbol hat.
*/
Component marker(Player player, @Nullable String viewerServer) {
String server = player.getCurrentServer().map(ServerConnection::getServerInfo).map(info -> info.getName()).orElse(null);
if (server == null || server.equals(viewerServer)) return Component.empty();
String symbol = this.config.serverSymbols().get(server);
if (symbol == null) return Component.empty();
String where = this.config.serverWhere().getOrDefault(server, "auf " + server);
return Component.text()
.append(Component.text(symbol, NamedTextColor.DARK_GRAY)
.hoverEvent(HoverEvent.showText(Component.text(player.getUsername() + " ist gerade " + where, NamedTextColor.GRAY))))
.append(Component.space())
.build();
}
/** Floodgate vergibt Bedrock-Spielern UUIDs, deren obere 64 Bit null sind. */ /** Floodgate vergibt Bedrock-Spielern UUIDs, deren obere 64 Bit null sind. */
static boolean isBedrock(UUID uuid) { static boolean isBedrock(UUID uuid) {
return uuid.getMostSignificantBits() == 0; return uuid.getMostSignificantBits() == 0;
@@ -149,11 +149,13 @@ final class PrivateMessages {
.append(PREFIX.clickEvent(ClickEvent.suggestCommand("/msg " + receiver.getUsername() + " "))) .append(PREFIX.clickEvent(ClickEvent.suggestCommand("/msg " + receiver.getUsername() + " ")))
.append(this.names.plain(sender)) .append(this.names.plain(sender))
.append(Component.text(" zu ", NamedTextColor.GRAY)) .append(Component.text(" zu ", NamedTextColor.GRAY))
.append(this.names.marker(receiver, Chat.serverOf(sender)))
.append(this.names.clickable(receiver)) .append(this.names.clickable(receiver))
.append(Component.text(" > ", NamedTextColor.GRAY)) .append(Component.text(" > ", NamedTextColor.GRAY))
.append(Component.text(message))); .append(Component.text(message)));
receiver.sendMessage(Component.text() receiver.sendMessage(Component.text()
.append(PREFIX.clickEvent(ClickEvent.suggestCommand("/msg " + sender.getUsername() + " "))) .append(PREFIX.clickEvent(ClickEvent.suggestCommand("/msg " + sender.getUsername() + " ")))
.append(this.names.marker(sender, Chat.serverOf(receiver)))
.append(this.names.clickable(sender)) .append(this.names.clickable(sender))
.append(Component.text(" zu ", NamedTextColor.GRAY)) .append(Component.text(" zu ", NamedTextColor.GRAY))
.append(this.names.plain(receiver)) .append(this.names.plain(receiver))
@@ -15,7 +15,8 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* Ergänzt die Tabliste jedes Spielers um die Spieler auf anderen Servern (grau, unten, mit Server). * Ergänzt die Tabliste jedes Spielers um die Spieler auf anderen Servern (grau, kursiv, unten, mit
* Server-Symbol).
* Die Einträge der Spieler auf dem eigenen Server schickt weiter das Backend (Skins, Spielmodus, * Die Einträge der Spieler auf dem eigenen Server schickt weiter das Backend (Skins, Spielmodus,
* Prefixe); die werden hier nie angefasst. * Prefixe); die werden hier nie angefasst.
* <p> * <p>
@@ -26,6 +27,8 @@ import java.util.concurrent.ConcurrentHashMap;
final class TabLists { final class TabLists {
/** Vanilla sortiert höhere Werte nach oben; Backends lassen 0 stehen. */ /** Vanilla sortiert höhere Werte nach oben; Backends lassen 0 stehen. */
private static final int REMOTE_ORDER = -1000; private static final int REMOTE_ORDER = -1000;
/** Der Client zeigt Spectator kursiv und blass; ohne Welt hat der Modus sonst keine Wirkung. */
private static final int SPECTATOR = 3;
private final ProxyServer proxy; private final ProxyServer proxy;
private final Config config; private final Config config;
@@ -52,7 +55,7 @@ final class TabLists {
if (other.equals(viewer) || otherServer == null || otherServer.equals(viewerServer)) continue; if (other.equals(viewer) || otherServer == null || otherServer.equals(viewerServer)) continue;
wanted.add(other.getUniqueId()); wanted.add(other.getUniqueId());
Component displayName = this.displayName(other, otherServer); Component displayName = this.displayName(other, viewerServer);
TabListEntry entry = tabList.getEntry(other.getUniqueId()).orElse(null); TabListEntry entry = tabList.getEntry(other.getUniqueId()).orElse(null);
if (entry == null || !ours.contains(other.getUniqueId())) { if (entry == null || !ours.contains(other.getUniqueId())) {
// Ein Rest-Eintrag vom Backend (gerade gewechselt) wird ersetzt // Ein Rest-Eintrag vom Backend (gerade gewechselt) wird ersetzt
@@ -62,13 +65,14 @@ final class TabLists {
.profile(other.getGameProfile()) .profile(other.getGameProfile())
.displayName(displayName) .displayName(displayName)
.latency((int) Math.min(other.getPing(), Integer.MAX_VALUE)) .latency((int) Math.min(other.getPing(), Integer.MAX_VALUE))
.gameMode(0) .gameMode(SPECTATOR)
.listed(true) .listed(true)
.listOrder(REMOTE_ORDER) .listOrder(REMOTE_ORDER)
.build()); .build());
ours.add(other.getUniqueId()); ours.add(other.getUniqueId());
} else { } else {
if (!Objects.equals(entry.getDisplayNameComponent().orElse(null), displayName)) entry.setDisplayName(displayName); if (!Objects.equals(entry.getDisplayNameComponent().orElse(null), displayName)) entry.setDisplayName(displayName);
if (entry.getGameMode() != SPECTATOR) entry.setGameMode(SPECTATOR);
int latency = (int) Math.min(other.getPing(), Integer.MAX_VALUE); int latency = (int) Math.min(other.getPing(), Integer.MAX_VALUE);
if (entry.getLatency() != latency) entry.setLatency(latency); if (entry.getLatency() != latency) entry.setLatency(latency);
} }
@@ -93,11 +97,11 @@ final class TabLists {
this.added.remove(viewer); this.added.remove(viewer);
} }
private Component displayName(Player player, String server) { /** Symbol des Servers, Name grau (Admins blau); der Spectator-Modus macht ihn zusätzlich kursiv. */
String tag = this.config.tabTags().getOrDefault(server, this.config.serverTags().getOrDefault(server, server)); private Component displayName(Player player, String viewerServer) {
return Component.text() return Component.text()
.append(this.names.plain(player)) .append(this.names.marker(player, viewerServer))
.append(Component.text(" · " + tag, NamedTextColor.DARK_GRAY)) .append(this.names.plain(player, NamedTextColor.GRAY))
.build(); .build();
} }
} }
+5 -7
View File
@@ -1,13 +1,11 @@
# chat-bridge: serverübergreifender Chat # chat-bridge: serverübergreifender Chat
# Listen werden mit | getrennt. Änderungen brauchen einen Neustart von Velocity. # Listen werden mit | getrennt. Änderungen brauchen einen Neustart von Velocity.
# Kürzel vor Nachrichten von diesem Server (Name aus velocity.toml=Kürzel). # Symbol vor Spielern, die auf einem anderen Server sind als man selbst (Chat, /msg, Join/Leave,
# Server ohne Eintrag (der Hauptserver) bekommen keins. # Tabliste), dunkelgrau, im Hover „<Name> ist gerade <server-where>“. Bewusst einfache Zeichen ohne
server-tags=arena=Arena|limbo=Limbo|event=Event|grand-event=Event|worldmuseum=Museum # Klammern, damit sie nicht mit Rängen ([🌟], 👑, 🗡 …) verwechselt werden. Server=Symbol.
server-symbols=craftattack=⛏|arena=◎|limbo=⌛|event=⚄|grand-event=⚄|worldmuseum=♜
# Tabliste: Spieler auf anderen Servern stehen unten mit diesem Namen dahinter. Ohne Eintrag gilt das server-where=craftattack=auf CraftAttack|arena=in der Arena|limbo=im Warteraum|event=beim Event|grand-event=beim Event|worldmuseum=im Weltmuseum
# Chat-Kürzel, sonst der Servername.
tab-tags=craftattack=CraftAttack
# Server, die ihren Chat nicht selbst unterdrücken (kein spawn-Plugin bzw. kein Silencer). # Server, die ihren Chat nicht selbst unterdrücken (kein spawn-Plugin bzw. kein Silencer).
# Deren Spieler sehen den Chat vom eigenen Server schon lokal und bekommen ihn von Velocity nicht # Deren Spieler sehen den Chat vom eigenen Server schon lokal und bekommen ihn von Velocity nicht
@@ -19,10 +19,10 @@ class ConfigTest {
properties.load(new InputStreamReader(in, StandardCharsets.UTF_8)); properties.load(new InputStreamReader(in, StandardCharsets.UTF_8));
} }
Config config = Config.fromProperties(properties); Config config = Config.fromProperties(properties);
assertEquals("Arena", config.serverTags().get("arena")); assertEquals("◎", config.serverSymbols().get("arena"));
assertEquals("im Warteraum", config.serverWhere().get("limbo"));
assertEquals(Set.of("grand-event", "worldmuseum"), config.unsilencedServers()); assertEquals(Set.of("grand-event", "worldmuseum"), config.unsilencedServers());
assertEquals("adminmarker", config.markerPermission()); assertEquals("adminmarker", config.markerPermission());
assertEquals("CraftAttack", config.tabTags().get("craftattack"));
assertEquals("craftattack", config.settingsServer()); assertEquals("craftattack", config.settingsServer());
} }