diff --git a/README.md b/README.md index c032371..461d7ab 100644 --- a/README.md +++ b/README.md @@ -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 verteilt sie selbst. -- **Öffentlicher Chat** mit Server-Kürzel (`[Arena]`, `[Limbo]` …, der Hauptserver ohne), Namen - von Spielern im Netzwerk werden golden als `@Name` hervorgehoben und hören einen Ton. +- **Öffentlicher Chat:** Spieler auf einem anderen Server als man selbst bekommen ein dunkelgraues + 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 der Spieler auf diesem Server ist), blau mit Permission `adminmarker`, 🎮 für Bedrock. - **/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 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. -- **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 alle 2 s und kurz nach Join, Wechsel und Quit. - **Log:** Chat, /msg, Adminchat und Mutes stehen im Velocity-Log (`[chat]`, `[msg]`, `[adminchat]`, `[mute]`). diff --git a/build.gradle b/build.gradle index ca0f762..e1a7565 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'eu.mhsl.craftattack' -version = '1.1.0' +version = '1.2.0' repositories { mavenCentral() diff --git a/src/main/java/eu/mhsl/craftattack/chatbridge/Chat.java b/src/main/java/eu/mhsl/craftattack/chatbridge/Chat.java index c76178f..06e518b 100644 --- a/src/main/java/eu/mhsl/craftattack/chatbridge/Chat.java +++ b/src/main/java/eu/mhsl/craftattack/chatbridge/Chat.java @@ -58,13 +58,19 @@ final class Chat { } Set mentioned = new LinkedHashSet<>(); - Component highlighted = this.line(server, sender, this.body(message, mentioned)); - Component plain = this.line(server, sender, Component.text(message)); + Component highlighted = this.body(message, mentioned); + Component plain = Component.text(message); + Component name = this.names.clickable(sender); 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()); - 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); } @@ -72,26 +78,18 @@ final class Chat { } 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) { - 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() .filter(player -> this.settings.of(player.getUniqueId()).joinAndLeave()) - .forEach(player -> player.sendMessage(line)); - } - - 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(); + .forEach(player -> player.sendMessage(arrow.append(this.names.marker(subject, serverOf(player))).append(name))); } /** Meldet einem gestummten Spieler die Restzeit; true, wenn er gestummt ist. */ @@ -104,12 +102,6 @@ final class Chat { 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. */ private Component body(String message, Set mentioned) { Map online = new HashMap<>(); diff --git a/src/main/java/eu/mhsl/craftattack/chatbridge/ChatBridge.java b/src/main/java/eu/mhsl/craftattack/chatbridge/ChatBridge.java index 4596338..5ab4ce9 100644 --- a/src/main/java/eu/mhsl/craftattack/chatbridge/ChatBridge.java +++ b/src/main/java/eu/mhsl/craftattack/chatbridge/ChatBridge.java @@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit; @Plugin( id = "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", authors = {"MHSL"}, 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(); 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()); } } diff --git a/src/main/java/eu/mhsl/craftattack/chatbridge/Config.java b/src/main/java/eu/mhsl/craftattack/chatbridge/Config.java index 7dfe3cd..a7ef19d 100644 --- a/src/main/java/eu/mhsl/craftattack/chatbridge/Config.java +++ b/src/main/java/eu/mhsl/craftattack/chatbridge/Config.java @@ -19,8 +19,8 @@ import java.util.stream.Collectors; * kommentierte Vorlage aus dem JAR geschrieben. */ record Config( - Map serverTags, - Map tabTags, + Map serverSymbols, + Map serverWhere, Set unsilencedServers, String settingsServer, String adminPermission, @@ -45,8 +45,8 @@ record Config( static Config fromProperties(Properties p) { return new Config( - pairs(p.getProperty("server-tags", "")), - pairs(p.getProperty("tab-tags", "")), + pairs(p.getProperty("server-symbols", "")), + pairs(p.getProperty("server-where", "")), Set.copyOf(split(p.getProperty("unsilenced-servers", ""))), p.getProperty("settings-server", "craftattack").trim(), p.getProperty("admin-permission", "admin").trim(), diff --git a/src/main/java/eu/mhsl/craftattack/chatbridge/Names.java b/src/main/java/eu/mhsl/craftattack/chatbridge/Names.java index fdb2b7e..19baeb9 100644 --- a/src/main/java/eu/mhsl/craftattack/chatbridge/Names.java +++ b/src/main/java/eu/mhsl/craftattack/chatbridge/Names.java @@ -44,6 +44,11 @@ final class Names { /** Name ohne Klick, z. B. für die eigene Seite einer Privatnachricht. */ 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(); Prefix prefix = this.prefixes.get(player.getUniqueId()); @@ -60,7 +65,7 @@ final class Names { } 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(); } @@ -71,6 +76,23 @@ final class Names { .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. */ static boolean isBedrock(UUID uuid) { return uuid.getMostSignificantBits() == 0; diff --git a/src/main/java/eu/mhsl/craftattack/chatbridge/PrivateMessages.java b/src/main/java/eu/mhsl/craftattack/chatbridge/PrivateMessages.java index 3f2ca20..c27b7cf 100644 --- a/src/main/java/eu/mhsl/craftattack/chatbridge/PrivateMessages.java +++ b/src/main/java/eu/mhsl/craftattack/chatbridge/PrivateMessages.java @@ -149,11 +149,13 @@ final class PrivateMessages { .append(PREFIX.clickEvent(ClickEvent.suggestCommand("/msg " + receiver.getUsername() + " "))) .append(this.names.plain(sender)) .append(Component.text(" zu ", NamedTextColor.GRAY)) + .append(this.names.marker(receiver, Chat.serverOf(sender))) .append(this.names.clickable(receiver)) .append(Component.text(" > ", NamedTextColor.GRAY)) .append(Component.text(message))); receiver.sendMessage(Component.text() .append(PREFIX.clickEvent(ClickEvent.suggestCommand("/msg " + sender.getUsername() + " "))) + .append(this.names.marker(sender, Chat.serverOf(receiver))) .append(this.names.clickable(sender)) .append(Component.text(" zu ", NamedTextColor.GRAY)) .append(this.names.plain(receiver)) diff --git a/src/main/java/eu/mhsl/craftattack/chatbridge/TabLists.java b/src/main/java/eu/mhsl/craftattack/chatbridge/TabLists.java index 6fcb9cc..5e69247 100644 --- a/src/main/java/eu/mhsl/craftattack/chatbridge/TabLists.java +++ b/src/main/java/eu/mhsl/craftattack/chatbridge/TabLists.java @@ -15,7 +15,8 @@ import java.util.UUID; 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, * Prefixe); die werden hier nie angefasst. *

@@ -26,6 +27,8 @@ import java.util.concurrent.ConcurrentHashMap; final class TabLists { /** Vanilla sortiert höhere Werte nach oben; Backends lassen 0 stehen. */ 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 Config config; @@ -52,7 +55,7 @@ final class TabLists { if (other.equals(viewer) || otherServer == null || otherServer.equals(viewerServer)) continue; wanted.add(other.getUniqueId()); - Component displayName = this.displayName(other, otherServer); + Component displayName = this.displayName(other, viewerServer); TabListEntry entry = tabList.getEntry(other.getUniqueId()).orElse(null); if (entry == null || !ours.contains(other.getUniqueId())) { // Ein Rest-Eintrag vom Backend (gerade gewechselt) wird ersetzt @@ -62,13 +65,14 @@ final class TabLists { .profile(other.getGameProfile()) .displayName(displayName) .latency((int) Math.min(other.getPing(), Integer.MAX_VALUE)) - .gameMode(0) + .gameMode(SPECTATOR) .listed(true) .listOrder(REMOTE_ORDER) .build()); ours.add(other.getUniqueId()); } else { 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); if (entry.getLatency() != latency) entry.setLatency(latency); } @@ -93,11 +97,11 @@ final class TabLists { this.added.remove(viewer); } - private Component displayName(Player player, String server) { - String tag = this.config.tabTags().getOrDefault(server, this.config.serverTags().getOrDefault(server, server)); + /** Symbol des Servers, Name grau (Admins blau); der Spectator-Modus macht ihn zusätzlich kursiv. */ + private Component displayName(Player player, String viewerServer) { return Component.text() - .append(this.names.plain(player)) - .append(Component.text(" · " + tag, NamedTextColor.DARK_GRAY)) + .append(this.names.marker(player, viewerServer)) + .append(this.names.plain(player, NamedTextColor.GRAY)) .build(); } } diff --git a/src/main/resources/config.properties b/src/main/resources/config.properties index 861b552..428eba1 100644 --- a/src/main/resources/config.properties +++ b/src/main/resources/config.properties @@ -1,13 +1,11 @@ # chat-bridge: serverübergreifender Chat # Listen werden mit | getrennt. Änderungen brauchen einen Neustart von Velocity. -# Kürzel vor Nachrichten von diesem Server (Name aus velocity.toml=Kürzel). -# Server ohne Eintrag (der Hauptserver) bekommen keins. -server-tags=arena=Arena|limbo=Limbo|event=Event|grand-event=Event|worldmuseum=Museum - -# Tabliste: Spieler auf anderen Servern stehen unten mit diesem Namen dahinter. Ohne Eintrag gilt das -# Chat-Kürzel, sonst der Servername. -tab-tags=craftattack=CraftAttack +# Symbol vor Spielern, die auf einem anderen Server sind als man selbst (Chat, /msg, Join/Leave, +# Tabliste), dunkelgrau, im Hover „ ist gerade “. Bewusst einfache Zeichen ohne +# Klammern, damit sie nicht mit Rängen ([🌟], 👑, 🗡 …) verwechselt werden. Server=Symbol. +server-symbols=craftattack=⛏|arena=◎|limbo=⌛|event=⚄|grand-event=⚄|worldmuseum=♜ +server-where=craftattack=auf CraftAttack|arena=in der Arena|limbo=im Warteraum|event=beim Event|grand-event=beim Event|worldmuseum=im Weltmuseum # 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 diff --git a/src/test/java/eu/mhsl/craftattack/chatbridge/ConfigTest.java b/src/test/java/eu/mhsl/craftattack/chatbridge/ConfigTest.java index f31f8ec..bb68ee7 100644 --- a/src/test/java/eu/mhsl/craftattack/chatbridge/ConfigTest.java +++ b/src/test/java/eu/mhsl/craftattack/chatbridge/ConfigTest.java @@ -19,10 +19,10 @@ class ConfigTest { properties.load(new InputStreamReader(in, StandardCharsets.UTF_8)); } 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("adminmarker", config.markerPermission()); - assertEquals("CraftAttack", config.tabTags().get("craftattack")); assertEquals("craftattack", config.settingsServer()); }