From 0df4d7777ed7ab891a7c12b3b94b7daf40a33b73 Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Thu, 29 Oct 2026 19:45:15 +0100 Subject: [PATCH 1/2] feat(chat): add clickable proxy region and MOTD actions --- .../command/InteractiveCommandMessage.kt | 55 +++++++++++++ .../proxy/velocity/command/MotdCommand.kt | 24 ++++-- .../proxy/velocity/command/ProxyCommand.kt | 27 ++++--- .../proxy/velocity/command/RegionCommand.kt | 23 ++++-- .../gg/grounds/proxy/messages.properties | 30 +++---- .../gg/grounds/proxy/messages_de.properties | 30 +++---- .../gg/grounds/proxy/messages_es.properties | 30 +++---- .../gg/grounds/proxy/messages_fr.properties | 30 +++---- .../command/InteractiveCommandMessageTest.kt | 80 +++++++++++++++++++ 9 files changed, 245 insertions(+), 84 deletions(-) create mode 100644 velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessage.kt create mode 100644 velocity/src/test/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessageTest.kt diff --git a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessage.kt b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessage.kt new file mode 100644 index 0000000..bd75ae3 --- /dev/null +++ b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessage.kt @@ -0,0 +1,55 @@ +package gg.grounds.proxy.velocity.command + +import gg.grounds.i18n.MessageKey +import gg.grounds.i18n.Translations +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.event.HoverEvent + +internal data class CommandAction(val command: String, val runsImmediately: Boolean) { + companion object { + fun run(command: String) = CommandAction(command, runsImmediately = true) + + fun suggest(command: String) = CommandAction(command, runsImmediately = false) + } +} + +/** Makes a localized action label discoverable without showing its command syntax in chat. */ +internal fun interactiveCommand( + message: Component, + action: CommandAction, + hint: String, +): Component = + message + .clickEvent( + if (action.runsImmediately) ClickEvent.runCommand(action.command) + else ClickEvent.suggestCommand(action.command) + ) + .hoverEvent(HoverEvent.showText(Component.text(hint))) + +/** + * Sends an interactive localized system message through the normal renderer, keeping its category, + * level, prefix, and palette intact. + */ +internal fun Translations.sendInteractive( + audience: Audience, + key: MessageKey, + action: CommandAction, + hint: String, + vararg arguments: Pair, +) { + sendRendered(audience, key, interactiveCommand(render(key, audience, *arguments), action, hint)) +} + +/** + * Read-only MOTD help can run immediately; edits and the destructive reset stay in the input box. + */ +internal fun motdHelpAction(usage: String): CommandAction { + val command = usage.substringBefore(" <").substringBefore(" [") + return when (command) { + "/motd", + "/motd preview" -> CommandAction.run(command) + else -> CommandAction.suggest(command + if ('<' in usage) " " else "") + } +} diff --git a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/MotdCommand.kt b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/MotdCommand.kt index c86a168..fd2ce9a 100644 --- a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/MotdCommand.kt +++ b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/MotdCommand.kt @@ -98,10 +98,11 @@ class MotdCommand( private fun set(source: CommandSource, text: String) { if (text.isEmpty()) { - messages.send( + messages.sendInteractive( source, ProxySystemMessage.MOTD_USAGE_SET, - "usage" to "/motd set ", + CommandAction.suggest("/motd set "), + "/motd set ", ) return } @@ -117,10 +118,11 @@ class MotdCommand( private fun import(source: CommandSource, idOrUrl: String) { if (idOrUrl.isEmpty()) { - messages.send( + messages.sendInteractive( source, ProxySystemMessage.MOTD_USAGE_IMPORT, - "usage" to "/motd import ", + CommandAction.suggest("/motd import "), + "/motd import ", ) return } @@ -161,16 +163,22 @@ class MotdCommand( private fun maxPlayers(source: CommandSource, argument: String) { val document = manager.current() if (document == null) { - messages.send(source, ProxySystemMessage.MOTD_REQUIRED_FOR_MAX_PLAYERS) + messages.sendInteractive( + source, + ProxySystemMessage.MOTD_REQUIRED_FOR_MAX_PLAYERS, + CommandAction.suggest("/motd set "), + "/motd set ", + ) return } val value = when { argument.isEmpty() -> { - messages.send( + messages.sendInteractive( source, ProxySystemMessage.MOTD_USAGE_MAX_PLAYERS, - "usage" to "/motd maxplayers ", + CommandAction.suggest("/motd maxplayers "), + "/motd maxplayers ", ) return } @@ -237,7 +245,7 @@ class MotdCommand( ProxySystemMessage.MOTD_HELP_RESET to "/motd reset", ) .forEach { (key, usage) -> - messages.send(source, key, "usage" to usage, "newline" to "") + messages.sendInteractive(source, key, motdHelpAction(usage), usage) } messages.send( source, diff --git a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/ProxyCommand.kt b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/ProxyCommand.kt index 5c2a1c4..2680a79 100644 --- a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/ProxyCommand.kt +++ b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/ProxyCommand.kt @@ -71,16 +71,25 @@ class ProxyCommand( ?.proxies ?.sortedBy { it.proxyId } ?.forEach { entry -> - messages.send( - source, - if (entry.proxyId == proxyId) ProxySystemMessage.PROXY_ENTRY_CURRENT - else ProxySystemMessage.PROXY_ENTRY, - "proxy" to entry.proxyId, - "region" to (entry.region ?: "—"), - "count" to entry.players, - ) + val arguments = + arrayOf( + "proxy" to entry.proxyId, + "region" to (entry.region ?: "—"), + "count" to entry.players, + ) + if (entry.proxyId == proxyId) { + messages.send(source, ProxySystemMessage.PROXY_ENTRY_CURRENT, *arguments) + } else { + messages.sendInteractive( + source, + ProxySystemMessage.PROXY_ENTRY, + CommandAction.run("/proxy ${entry.proxyId}"), + "/proxy ${entry.proxyId}", + *arguments, + ) + } } - messages.send(source, ProxySystemMessage.PROXY_USAGE, "syntax" to "/proxy ") + messages.send(source, ProxySystemMessage.PROXY_USAGE) } override fun suggest(invocation: SimpleCommand.Invocation): List { diff --git a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/RegionCommand.kt b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/RegionCommand.kt index 1c06f92..5ae5a56 100644 --- a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/RegionCommand.kt +++ b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/command/RegionCommand.kt @@ -90,14 +90,23 @@ class RegionCommand( messages.send(player, ProxySystemMessage.REGION_HEADER) catalog.regions.forEach { region -> val isHere = here != null && region.code.equals(here, true) - messages.send( - player, - if (isHere) ProxySystemMessage.REGION_ENTRY_CURRENT - else ProxySystemMessage.REGION_ENTRY, - "region" to region.code, - ) + if (isHere) { + messages.send( + player, + ProxySystemMessage.REGION_ENTRY_CURRENT, + "region" to region.code, + ) + } else { + messages.sendInteractive( + player, + ProxySystemMessage.REGION_ENTRY, + CommandAction.run("/region ${region.code}"), + "/region ${region.code}", + "region" to region.code, + ) + } } - messages.send(player, ProxySystemMessage.REGION_USAGE, "syntax" to "/region ") + messages.send(player, ProxySystemMessage.REGION_USAGE) } override fun suggest(invocation: SimpleCommand.Invocation): List { diff --git a/velocity/src/main/resources/gg/grounds/proxy/messages.properties b/velocity/src/main/resources/gg/grounds/proxy/messages.properties index bc147e8..e42ce9d 100644 --- a/velocity/src/main/resources/gg/grounds/proxy/messages.properties +++ b/velocity/src/main/resources/gg/grounds/proxy/messages.properties @@ -18,9 +18,9 @@ region.already-here=You are already in . region.unavailable=Region switching is unavailable right now. region.moving=Moving you to ... region.header=--- Regions --- -region.entry= +region.entry=Move to region.entry.current= (you are here) -region.usage=Use to move. +region.usage=Click a region to move. party.transfer.following=Following the party leader to ... motd.unknown-subcommand=Unknown subcommand ''. motd.not-set=No MOTD is set; the proxy default is being served. @@ -32,16 +32,16 @@ motd.changed-at=changed at: motd.max-players=max players: motd.max-players.default=max players: proxy default motd.raw=raw: -motd.usage.set=Usage: +motd.usage.set=Set MOTD motd.updated=MOTD updated. -motd.usage.import=Usage: +motd.usage.import=Import MOTD motd.fetching=Fetching from motd.gg... motd.import-failed=Import failed: motd.icon-skipped=The motd.gg document has a server icon; it was not imported (the network icon is served from the CDN). motd.imported=MOTD imported from motd.gg/. motd.imported.named=MOTD imported from motd.gg/ (). -motd.max-players.required=No MOTD is set — run /motd set first; max players is stored alongside it. -motd.usage.max-players=Usage: +motd.max-players.required=No MOTD is set. Set MOTD first; max players is stored alongside it. +motd.usage.max-players=Set maximum players motd.invalid-player-count='' is not a player count. motd.max-players.cleared=Max players cleared; the proxy value is used again. motd.max-players.set=Max players set to . @@ -49,13 +49,13 @@ motd.removed=MOTD removed; the proxy default is served again. motd.nothing-to-remove=There was no MOTD to remove. motd.remove-failed=Could not remove the MOTD: motd.nothing-to-preview=Nothing to preview: no MOTD is set. -motd.help.header=--- /motd --- -motd.help.show= — show the MOTD in use -motd.help.set= — change it; starts the second line -motd.help.import= — take the design from motd.gg -motd.help.max-players= — the number right of the slash -motd.help.preview= — render it here without changing anything -motd.help.reset= — remove it and serve the proxy default +motd.help.header=--- MOTD --- +motd.help.show=Show current MOTD +motd.help.set=Change MOTD; <newline> starts the second line +motd.help.import=Import a design from motd.gg +motd.help.max-players=Set maximum players +motd.help.preview=Preview MOTD without changing it +motd.help.reset=Remove MOTD and use the proxy default motd.placeholders=Placeholders: motd.save-failed=Could not save the MOTD: motd.invalid-mini-message=That is not valid MiniMessage: @@ -75,6 +75,6 @@ proxy.moving=Looking for proxy ... proxy.arrived=You are now on . proxy.not-reached=Did not reach after attempts; you are on . proxy.current=You are on . -proxy.entry= (): +proxy.entry=Switch to (): proxy.entry.current= (): (you are here) -proxy.usage=Use to switch. +proxy.usage=Click a proxy to switch. diff --git a/velocity/src/main/resources/gg/grounds/proxy/messages_de.properties b/velocity/src/main/resources/gg/grounds/proxy/messages_de.properties index 268b72c..617febb 100644 --- a/velocity/src/main/resources/gg/grounds/proxy/messages_de.properties +++ b/velocity/src/main/resources/gg/grounds/proxy/messages_de.properties @@ -16,9 +16,9 @@ region.already-here=Du bist bereits in . region.unavailable=Der Regionswechsel ist derzeit nicht verfügbar. region.moving=Du wirst nach verschoben... region.header=--- Regionen --- -region.entry= +region.entry=Zu wechseln region.entry.current= (du bist hier) -region.usage=Nutze , um zu wechseln. +region.usage=Klicke auf eine Region, um zu wechseln. party.transfer.following=Folge dem Party-Leader zu ... motd.unknown-subcommand=Unbekannter Unterbefehl ''. motd.not-set=Keine MOTD gesetzt; der Proxy-Standard wird angezeigt. @@ -30,16 +30,16 @@ motd.changed-at=geändert am: motd.max-players=maximale Spieler: motd.max-players.default=maximale Spieler: Proxy-Standard motd.raw=Rohtext: -motd.usage.set=Verwendung: +motd.usage.set=MOTD setzen motd.updated=MOTD aktualisiert. -motd.usage.import=Verwendung: +motd.usage.import=MOTD importieren motd.fetching=Von motd.gg wird geladen... motd.import-failed=Import fehlgeschlagen: motd.icon-skipped=Das motd.gg-Dokument enthält ein Server-Icon; es wurde nicht importiert (das Netzwerk-Icon kommt vom CDN). motd.imported=MOTD von motd.gg/ importiert. motd.imported.named=MOTD von motd.gg/ () importiert. -motd.max-players.required=Keine MOTD gesetzt — führe zuerst /motd set aus; die maximale Spielerzahl wird damit gespeichert. -motd.usage.max-players=Verwendung: +motd.max-players.required=Keine MOTD gesetzt. Setze zuerst eine MOTD; die maximale Spielerzahl wird damit gespeichert. +motd.usage.max-players=Maximale Spielerzahl setzen motd.invalid-player-count='' ist keine Spielerzahl. motd.max-players.cleared=Maximale Spielerzahl entfernt; der Proxy-Wert wird wieder verwendet. motd.max-players.set=Maximale Spielerzahl auf gesetzt. @@ -47,13 +47,13 @@ motd.removed=MOTD entfernt; der Proxy-Standard wird wieder angezeigt.Es gab keine MOTD zum Entfernen. motd.remove-failed=MOTD konnte nicht entfernt werden: motd.nothing-to-preview=Keine Vorschau möglich: Es ist keine MOTD gesetzt. -motd.help.header=--- /motd --- -motd.help.show= — verwendete MOTD anzeigen -motd.help.set= — ändern; beginnt die zweite Zeile -motd.help.import= — Design von motd.gg übernehmen -motd.help.max-players= — Zahl rechts vom Schrägstrich -motd.help.preview= — hier anzeigen, ohne etwas zu ändern -motd.help.reset= — entfernen und Proxy-Standard verwenden +motd.help.header=--- MOTD --- +motd.help.show=Aktuelle MOTD anzeigen +motd.help.set=MOTD ändern; <newline> beginnt die zweite Zeile +motd.help.import=Design von motd.gg importieren +motd.help.max-players=Maximale Spielerzahl setzen +motd.help.preview=MOTD anzeigen, ohne etwas zu ändern +motd.help.reset=MOTD entfernen und Proxy-Standard verwenden motd.placeholders=Platzhalter: motd.save-failed=MOTD konnte nicht gespeichert werden: motd.invalid-mini-message=Das ist kein gültiges MiniMessage: @@ -73,6 +73,6 @@ proxy.moving=Suche Proxy ... proxy.arrived=Du bist jetzt auf . proxy.not-reached= nach Versuchen nicht erreicht; du bist auf . proxy.current=Du bist auf . -proxy.entry= (): +proxy.entry=Zu wechseln (): proxy.entry.current= (): (du bist hier) -proxy.usage=Nutze , um zu wechseln. +proxy.usage=Klicke auf einen Proxy, um zu wechseln. diff --git a/velocity/src/main/resources/gg/grounds/proxy/messages_es.properties b/velocity/src/main/resources/gg/grounds/proxy/messages_es.properties index ac57cfb..081b647 100644 --- a/velocity/src/main/resources/gg/grounds/proxy/messages_es.properties +++ b/velocity/src/main/resources/gg/grounds/proxy/messages_es.properties @@ -16,9 +16,9 @@ region.already-here=Ya estás en . region.unavailable=El cambio de región no está disponible ahora mismo. region.moving=Trasladándote a ... region.header=--- Regiones --- -region.entry= +region.entry=Mover a region.entry.current= (estás aquí) -region.usage=Usa para cambiar. +region.usage=Haz clic en una región para cambiar. party.transfer.following=Siguiendo al líder del grupo hasta ... motd.unknown-subcommand=Subcomando desconocido ''. motd.not-set=No hay MOTD; se está usando el valor predeterminado del proxy. @@ -30,16 +30,16 @@ motd.changed-at=modificado el: motd.max-players=máximo de jugadores: motd.max-players.default=máximo de jugadores: valor del proxy motd.raw=texto sin procesar: -motd.usage.set=Uso: +motd.usage.set=Establecer MOTD motd.updated=MOTD actualizado. -motd.usage.import=Uso: +motd.usage.import=Importar MOTD motd.fetching=Obteniendo desde motd.gg... motd.import-failed=Error de importación: motd.icon-skipped=El documento de motd.gg tiene un icono de servidor; no se importó (el icono de la red se sirve desde el CDN). motd.imported=MOTD importado desde motd.gg/. motd.imported.named=MOTD importado desde motd.gg/ (). -motd.max-players.required=No hay MOTD — ejecuta primero /motd set; el máximo de jugadores se guarda junto a él. -motd.usage.max-players=Uso: +motd.max-players.required=No hay MOTD. Establece primero una MOTD; el máximo de jugadores se guarda junto a ella. +motd.usage.max-players=Establecer máximo de jugadores motd.invalid-player-count='' no es un número de jugadores. motd.max-players.cleared=Máximo eliminado; se vuelve a usar el valor del proxy. motd.max-players.set=Máximo de jugadores establecido en . @@ -47,13 +47,13 @@ motd.removed=MOTD eliminado; se vuelve a usar el valor predeterminado d motd.nothing-to-remove=No había ningún MOTD que eliminar. motd.remove-failed=No se pudo eliminar el MOTD: motd.nothing-to-preview=No hay nada que previsualizar: no hay MOTD. -motd.help.header=--- /motd --- -motd.help.show= — mostrar el MOTD en uso -motd.help.set= — cambiarlo; inicia la segunda línea -motd.help.import= — tomar el diseño de motd.gg -motd.help.max-players= — el número a la derecha de la barra -motd.help.preview= — mostrarlo aquí sin cambiar nada -motd.help.reset= — eliminarlo y usar el valor del proxy +motd.help.header=--- MOTD --- +motd.help.show=Mostrar MOTD actual +motd.help.set=Cambiar MOTD; <newline> inicia la segunda línea +motd.help.import=Importar un diseño de motd.gg +motd.help.max-players=Establecer máximo de jugadores +motd.help.preview=Previsualizar MOTD sin cambiarlo +motd.help.reset=Eliminar MOTD y usar el valor del proxy motd.placeholders=Marcadores: motd.save-failed=No se pudo guardar el MOTD: motd.invalid-mini-message=No es un MiniMessage válido: @@ -73,6 +73,6 @@ proxy.moving=Buscando el proxy ... proxy.arrived=Ahora estás en . proxy.not-reached=No se alcanzó tras intentos; estás en . proxy.current=Estás en . -proxy.entry= (): +proxy.entry=Cambiar a (): proxy.entry.current= (): (estás aquí) -proxy.usage=Usa para cambiar. +proxy.usage=Haz clic en un proxy para cambiar. diff --git a/velocity/src/main/resources/gg/grounds/proxy/messages_fr.properties b/velocity/src/main/resources/gg/grounds/proxy/messages_fr.properties index 6dd1030..3ccf77e 100644 --- a/velocity/src/main/resources/gg/grounds/proxy/messages_fr.properties +++ b/velocity/src/main/resources/gg/grounds/proxy/messages_fr.properties @@ -16,9 +16,9 @@ region.already-here=Vous êtes déjà dans . region.unavailable=Le changement de région est indisponible pour le moment. region.moving=Transfert vers ... region.header=--- Régions --- -region.entry= +region.entry=Aller vers region.entry.current= (vous êtes ici) -region.usage=Utilisez pour changer. +region.usage=Cliquez sur une région pour changer. party.transfer.following=Vous suivez le chef du groupe vers ... motd.unknown-subcommand=Sous-commande inconnue ''. motd.not-set=Aucun MOTD n'est défini ; la valeur par défaut du proxy est utilisée. @@ -30,16 +30,16 @@ motd.changed-at=modifié le : motd.max-players=joueurs maximum : motd.max-players.default=joueurs maximum : valeur du proxy motd.raw=texte brut : -motd.usage.set=Utilisation : +motd.usage.set=Définir le MOTD motd.updated=MOTD mis à jour. -motd.usage.import=Utilisation : +motd.usage.import=Importer le MOTD motd.fetching=Récupération depuis motd.gg... motd.import-failed=Échec de l'importation : motd.icon-skipped=Le document motd.gg contient une icône de serveur ; elle n'a pas été importée (l'icône du réseau vient du CDN). motd.imported=MOTD importé depuis motd.gg/. motd.imported.named=MOTD importé depuis motd.gg/ (). -motd.max-players.required=Aucun MOTD n'est défini — exécutez d'abord /motd set ; le maximum est stocké avec celui-ci. -motd.usage.max-players=Utilisation : +motd.max-players.required=Aucun MOTD n'est défini. Définissez d'abord un MOTD ; le maximum est stocké avec lui. +motd.usage.max-players=Définir le nombre maximal de joueurs motd.invalid-player-count='' n'est pas un nombre de joueurs. motd.max-players.cleared=Maximum supprimé ; la valeur du proxy est de nouveau utilisée. motd.max-players.set=Maximum défini à . @@ -47,13 +47,13 @@ motd.removed=MOTD supprimé ; la valeur par défaut du proxy est de nou motd.nothing-to-remove=Il n'y avait aucun MOTD à supprimer. motd.remove-failed=Impossible de supprimer le MOTD : motd.nothing-to-preview=Aucun aperçu : aucun MOTD n'est défini. -motd.help.header=--- /motd --- -motd.help.show= — afficher le MOTD utilisé -motd.help.set= — le modifier ; commence la deuxième ligne -motd.help.import= — reprendre le design de motd.gg -motd.help.max-players= — le nombre à droite de la barre -motd.help.preview= — l'afficher ici sans rien modifier -motd.help.reset= — le supprimer et utiliser la valeur du proxy +motd.help.header=--- MOTD --- +motd.help.show=Afficher le MOTD actuel +motd.help.set=Modifier le MOTD ; <newline> commence la deuxième ligne +motd.help.import=Importer un design depuis motd.gg +motd.help.max-players=Définir le nombre maximal de joueurs +motd.help.preview=Aperçu du MOTD sans le modifier +motd.help.reset=Supprimer le MOTD et utiliser la valeur du proxy motd.placeholders=Variables : motd.save-failed=Impossible d'enregistrer le MOTD : motd.invalid-mini-message=Ce MiniMessage n'est pas valide : @@ -73,6 +73,6 @@ proxy.moving=Recherche du proxy ... proxy.arrived=Vous êtes maintenant sur . proxy.not-reached= introuvable après tentatives ; vous êtes sur . proxy.current=Vous êtes sur . -proxy.entry= () : +proxy.entry=Aller vers () : proxy.entry.current= () : (vous êtes ici) -proxy.usage=Utilisez pour changer. +proxy.usage=Cliquez sur un proxy pour changer. diff --git a/velocity/src/test/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessageTest.kt b/velocity/src/test/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessageTest.kt new file mode 100644 index 0000000..cb7e3cc --- /dev/null +++ b/velocity/src/test/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessageTest.kt @@ -0,0 +1,80 @@ +package gg.grounds.proxy.velocity.command + +import gg.grounds.i18n.Translations +import gg.grounds.proxy.velocity.ProxySystemMessage +import java.util.Locale +import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test + +class InteractiveCommandMessageTest { + + private val messages = + Translations.forBundle("gg.grounds.proxy.messages", javaClass.classLoader) + + @Test + fun `region entries run their exact region command in every supported locale`() { + for (locale in locales) { + val action = + interactiveCommand( + messages.render(ProxySystemMessage.REGION_ENTRY, locale, "region" to "eu"), + CommandAction.run("/region eu"), + "/region eu", + ) + + assertEquals( + ClickEvent.runCommand("/region eu"), + action.clickEvent(), + locale.toLanguageTag(), + ) + assertEquals("/region eu", hoverText(action), locale.toLanguageTag()) + assertFalse(plain(action).contains("/region"), locale.toLanguageTag()) + } + } + + @Test + fun `proxy entries run their exact network-wide proxy command`() { + val action = + interactiveCommand( + messages.render( + ProxySystemMessage.PROXY_ENTRY, + Locale.ENGLISH, + "proxy" to "eu-2", + "region" to "eu", + "count" to 12, + ), + CommandAction.run("/proxy eu-2"), + "/proxy eu-2", + ) + + assertEquals(ClickEvent.runCommand("/proxy eu-2"), action.clickEvent()) + assertEquals("/proxy eu-2", hoverText(action)) + } + + @Test + fun `motd help runs complete read-only commands and suggests edits`() { + val show = motdHelpAction("/motd") + val preview = motdHelpAction("/motd preview [MiniMessage]") + val set = motdHelpAction("/motd set ") + val reset = motdHelpAction("/motd reset") + + assertEquals(CommandAction.run("/motd"), show) + assertEquals(CommandAction.run("/motd preview"), preview) + assertEquals(CommandAction.suggest("/motd set "), set) + assertEquals(CommandAction.suggest("/motd reset"), reset) + } + + private fun hoverText(component: net.kyori.adventure.text.Component): String = + PlainTextComponentSerializer.plainText() + .serialize(component.hoverEvent()!!.value() as net.kyori.adventure.text.Component) + + private fun plain(component: net.kyori.adventure.text.Component): String = + PlainTextComponentSerializer.plainText().serialize(component) + + private companion object { + val locales = + listOf(Locale.ENGLISH, Locale.GERMAN, Locale.FRENCH, Locale.forLanguageTag("es")) + } +} From aa80f080aaee451257515c94604651d6e816abe6 Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Thu, 29 Oct 2026 19:47:48 +0100 Subject: [PATCH 2/2] fix(chat): preserve literal MOTD newline hints --- .../main/resources/gg/grounds/proxy/messages.properties | 2 +- .../resources/gg/grounds/proxy/messages_de.properties | 2 +- .../resources/gg/grounds/proxy/messages_es.properties | 2 +- .../resources/gg/grounds/proxy/messages_fr.properties | 2 +- .../velocity/command/InteractiveCommandMessageTest.kt | 9 +++++++++ 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/velocity/src/main/resources/gg/grounds/proxy/messages.properties b/velocity/src/main/resources/gg/grounds/proxy/messages.properties index e42ce9d..21623c5 100644 --- a/velocity/src/main/resources/gg/grounds/proxy/messages.properties +++ b/velocity/src/main/resources/gg/grounds/proxy/messages.properties @@ -51,7 +51,7 @@ motd.remove-failed=Could not remove the MOTD: motd.nothing-to-preview=Nothing to preview: no MOTD is set. motd.help.header=--- MOTD --- motd.help.show=Show current MOTD -motd.help.set=Change MOTD; <newline> starts the second line +motd.help.set=Change MOTD; \\ starts the second line motd.help.import=Import a design from motd.gg motd.help.max-players=Set maximum players motd.help.preview=Preview MOTD without changing it diff --git a/velocity/src/main/resources/gg/grounds/proxy/messages_de.properties b/velocity/src/main/resources/gg/grounds/proxy/messages_de.properties index 617febb..41a8fdc 100644 --- a/velocity/src/main/resources/gg/grounds/proxy/messages_de.properties +++ b/velocity/src/main/resources/gg/grounds/proxy/messages_de.properties @@ -49,7 +49,7 @@ motd.remove-failed=MOTD konnte nicht entfernt werden: motd.nothing-to-preview=Keine Vorschau möglich: Es ist keine MOTD gesetzt. motd.help.header=--- MOTD --- motd.help.show=Aktuelle MOTD anzeigen -motd.help.set=MOTD ändern; <newline> beginnt die zweite Zeile +motd.help.set=MOTD ändern; \\ beginnt die zweite Zeile motd.help.import=Design von motd.gg importieren motd.help.max-players=Maximale Spielerzahl setzen motd.help.preview=MOTD anzeigen, ohne etwas zu ändern diff --git a/velocity/src/main/resources/gg/grounds/proxy/messages_es.properties b/velocity/src/main/resources/gg/grounds/proxy/messages_es.properties index 081b647..5ca0fc6 100644 --- a/velocity/src/main/resources/gg/grounds/proxy/messages_es.properties +++ b/velocity/src/main/resources/gg/grounds/proxy/messages_es.properties @@ -49,7 +49,7 @@ motd.remove-failed=No se pudo eliminar el MOTD: motd.nothing-to-preview=No hay nada que previsualizar: no hay MOTD. motd.help.header=--- MOTD --- motd.help.show=Mostrar MOTD actual -motd.help.set=Cambiar MOTD; <newline> inicia la segunda línea +motd.help.set=Cambiar MOTD; \\ inicia la segunda línea motd.help.import=Importar un diseño de motd.gg motd.help.max-players=Establecer máximo de jugadores motd.help.preview=Previsualizar MOTD sin cambiarlo diff --git a/velocity/src/main/resources/gg/grounds/proxy/messages_fr.properties b/velocity/src/main/resources/gg/grounds/proxy/messages_fr.properties index 3ccf77e..e9d8b70 100644 --- a/velocity/src/main/resources/gg/grounds/proxy/messages_fr.properties +++ b/velocity/src/main/resources/gg/grounds/proxy/messages_fr.properties @@ -49,7 +49,7 @@ motd.remove-failed=Impossible de supprimer le MOTD : motd.nothing-to-preview=Aucun aperçu : aucun MOTD n'est défini. motd.help.header=--- MOTD --- motd.help.show=Afficher le MOTD actuel -motd.help.set=Modifier le MOTD ; <newline> commence la deuxième ligne +motd.help.set=Modifier le MOTD ; \\ commence la deuxième ligne motd.help.import=Importer un design depuis motd.gg motd.help.max-players=Définir le nombre maximal de joueurs motd.help.preview=Aperçu du MOTD sans le modifier diff --git a/velocity/src/test/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessageTest.kt b/velocity/src/test/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessageTest.kt index cb7e3cc..1330cc6 100644 --- a/velocity/src/test/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessageTest.kt +++ b/velocity/src/test/kotlin/gg/grounds/proxy/velocity/command/InteractiveCommandMessageTest.kt @@ -66,6 +66,15 @@ class InteractiveCommandMessageTest { assertEquals(CommandAction.suggest("/motd reset"), reset) } + @Test + fun `MOTD help preserves the literal newline tag in each locale`() { + locales.forEach { locale -> + val text = plain(messages.render(ProxySystemMessage.MOTD_HELP_SET, locale)) + org.junit.jupiter.api.Assertions.assertTrue(text.contains(""), text) + assertFalse(text.contains("<"), text) + } + } + private fun hoverText(component: net.kyori.adventure.text.Component): String = PlainTextComponentSerializer.plainText() .serialize(component.hoverEvent()!!.value() as net.kyori.adventure.text.Component)