From 0f1fd021a808a2aa12b89fa94712ec690ba4116c Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sun, 6 Sep 2026 03:31:56 -0400 Subject: [PATCH] patch lobby chat --- docs/NETPLAY.md | 12 ++++ lib/recomp-net | 2 +- runtime/include/psx_lobby_client.h | 9 +++ runtime/src/main.cpp | 36 +++++++++++ runtime/src/psx_lobby_client.c | 99 +++++++++++++++++++++++++++++- 5 files changed, 154 insertions(+), 4 deletions(-) diff --git a/docs/NETPLAY.md b/docs/NETPLAY.md index 6a61f836..9d8d68d4 100644 --- a/docs/NETPLAY.md +++ b/docs/NETPLAY.md @@ -42,6 +42,18 @@ remains useful for debugging and for hosts that prefer fixed lag. ## Seats vs session slots +Lobby chat is masked for profanity and slurs on every path: the server +filters relayed lines, and every client runs recomp-net's +`rnet_chat_filter_apply` on each line it puts in its ring -- so LAN rooms +(host-relayed `MOTK5 CHAT`) are filtered without a server. The list is +`lib/recomp-net/data/chat_filter_words.txt`; `RNET_CHAT_FILTER=0` disables +the client pass. + +The lobby browser also carries a per-game server chat (server op +`server_chat`, relayed to every client listed for the same title) and lists +only the players online for this title; both are online-only, since a LAN +room has no server. The same emoji and profanity handling applies. + Players can move themselves in the lobby and ask to swap seats (online via the server's `seat_move` / `seat_swap_request` / `seat_swap_answer`; on LAN via `MOTK5 SEATMOVE` / `SWAPASK` / `SWAPANS` / `SWAPRES` relayed by the host), diff --git a/lib/recomp-net b/lib/recomp-net index e9c8e937..30772f6e 160000 --- a/lib/recomp-net +++ b/lib/recomp-net @@ -1 +1 @@ -Subproject commit e9c8e9377ce073f73a77489902067bd5f9895055 +Subproject commit 30772f6e78845b937c270e4af869a6b989e91b07 diff --git a/runtime/include/psx_lobby_client.h b/runtime/include/psx_lobby_client.h index cabef39b..9242daa1 100644 --- a/runtime/include/psx_lobby_client.h +++ b/runtime/include/psx_lobby_client.h @@ -68,6 +68,9 @@ typedef struct PsxLobbyOnlinePlayer { /* First 8 characters of the player's connection id: enough to tell * "which row is me" without publishing whole ids to browsers. */ char tag[12]; + /* The title that player is browsing for; rows of other titles are + * dropped at parse time when this client has a game identity. */ + char game_name[PSX_LOBBY_NAME_LEN]; } PsxLobbyOnlinePlayer; #define PSX_LOBBY_MAX_ONLINE 64 @@ -398,6 +401,12 @@ void psx_lobby_seat_swap_clear(void); /* Lobby chat. send: 0 when queued (the line appears via the server echo). * count/get read the ring, oldest first; cleared on create/join/leave. */ int psx_lobby_send_chat(const char *text); + +/* Server chat: per-game, outside any room (op server_chat). Its own ring, + * cleared on disconnect; lines are masked on arrival like lobby chat. */ +int psx_lobby_send_server_chat(const char *text); +int psx_lobby_server_chat_count(void); +int psx_lobby_server_chat_get(int index, PsxLobbyChatMsg *out); int psx_lobby_chat_count(void); int psx_lobby_chat_get(int index, PsxLobbyChatMsg *out); void psx_lobby_chat_clear(void); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 65176729..5ab2d000 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -56,6 +56,9 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "psx_netplay_rb.h" #include "psx_selfcheck.h" #include "psx_lobby_client.h" +#if defined(PSX_HAS_RECOMP_NET) +#include "recomp_net/chat_filter.h" /* chat profanity mask, LAN rooms too */ +#endif #include "spu.h" #include "audio_trace.h" #include "spu_shadow.h" @@ -8084,6 +8087,11 @@ namespace { std::snprintf(m.player_id, sizeof(m.player_id), "%s", player_id ? player_id : ""); std::snprintf(m.from, sizeof(m.from), "%s", from ? from : ""); std::snprintf(m.text, sizeof(m.text), "%s", text); + /* A LAN room has no server to mask for it: every peer masks the + * line as it lands in the ring, the host included. */ +#if defined(PSX_HAS_RECOMP_NET) + if (!is_system) (void)rnet_chat_filter_apply(m.text, sizeof(m.text)); +#endif m.is_system = is_system ? 1 : 0; ++g_lnch_lan_chat_seq; } @@ -10032,6 +10040,31 @@ namespace { out->seq = msg.seq; return 1; } + /* Server chat: per-game, online only. A LAN room has no server and no + * wider audience, so the panel is hidden there (send refuses, count 0). */ + int ae_np_server_chat_send(void*, const char* text) { + if (g_lnch_hosting_lan || g_lnch_joined_lan || !psx_lobby_connected()) return -1; + char line[256]; + ae_np_chat_sanitize(text, line, sizeof(line)); + if (!line[0]) return -1; + return psx_lobby_send_server_chat(line); + } + int ae_np_server_chat_count(void*) { + if (g_lnch_hosting_lan || g_lnch_joined_lan) return 0; + return psx_lobby_server_chat_count(); + } + int ae_np_server_chat_get(void*, int index, RecompLauncherCNetplayChatMessage* out) { + if (!out) return 0; + std::memset(out, 0, sizeof(*out)); + PsxLobbyChatMsg msg{}; + if (!psx_lobby_server_chat_get(index, &msg)) return 0; + std::snprintf(out->from, sizeof(out->from), "%s", msg.from); + std::snprintf(out->text, sizeof(out->text), "%s", msg.text); + out->is_local = msg.is_local; + out->is_system = msg.is_system; + out->seq = msg.seq; + return 1; + } int ae_np_guest_memcard_set(void*, int allow) { if (g_lnch_joined_lan) return -1; if (!g_lnch_hosting_lan && psx_lobby_in_lobby() && !psx_lobby_is_host()) @@ -12014,6 +12047,9 @@ namespace { g_lnch_netplay_callbacks.host_can_spectate = ae_np_host_can_spectate; g_lnch_netplay_callbacks.online_count = ae_np_online_count; g_lnch_netplay_callbacks.online_get = ae_np_online_get; + g_lnch_netplay_callbacks.server_chat_send = ae_np_server_chat_send; + g_lnch_netplay_callbacks.server_chat_count = ae_np_server_chat_count; + g_lnch_netplay_callbacks.server_chat_get = ae_np_server_chat_get; g_lnch_netplay_callbacks.seat_move_self = ae_np_seat_move_self; g_lnch_netplay_callbacks.seat_swap_request = ae_np_seat_swap_request; g_lnch_netplay_callbacks.seat_swap_incoming = ae_np_seat_swap_incoming; diff --git a/runtime/src/psx_lobby_client.c b/runtime/src/psx_lobby_client.c index 73c17954..43da3dfe 100644 --- a/runtime/src/psx_lobby_client.c +++ b/runtime/src/psx_lobby_client.c @@ -74,6 +74,9 @@ int psx_lobby_set_match_caps(const PsxLobbyMatchCaps *c) { (void)c; return -1; int psx_lobby_member_count(void) { return 0; } int psx_lobby_member_get(int index, PsxLobbyMember *out) { (void)index; (void)out; return 0; } int psx_lobby_member_latency_ms(int slot) { (void)slot; return -1; } +int psx_lobby_send_server_chat(const char *text) { (void)text; return -1; } +int psx_lobby_server_chat_count(void) { return 0; } +int psx_lobby_server_chat_get(int index, PsxLobbyChatMsg *out) { (void)index; (void)out; return 0; } void psx_lobby_resume_waiting_room_rtt(void) {} int psx_lobby_member_is_host(const PsxLobbyMember *member) { @@ -149,6 +152,7 @@ void psx_lobby_clear_launch_pending(void) {} #include "recomp_net/ice_rtt.h" #include "recomp_net/lan_beacon.h" #include "recomp_net/rtt_probe.h" +#include "recomp_net/chat_filter.h" #include "host_time.h" #if defined(_WIN32) @@ -226,6 +230,11 @@ typedef struct { int chat_head; int chat_count; uint32_t chat_seq; + /* Server (per-game) chat: a second ring with its own sequence. */ + PsxLobbyChatMsg schat[PSX_LOBBY_CHAT_RING]; + int schat_head; + int schat_count; + uint32_t schat_seq; char pending_tx[8][2048]; int pending_n; /* Inbound ICE signals (WS op:signal). */ @@ -1319,6 +1328,13 @@ static void lobby_list_parse_players(const char *json) sizeof(g_lc.online[n].lobby_name)); g_lc.online[n].hosting = json_get_bool(chunk, "hosting", 0); json_get_str(chunk, "tag", g_lc.online[n].tag, sizeof(g_lc.online[n].tag)); + json_get_str(chunk, "game_name", g_lc.online[n].game_name, + sizeof(g_lc.online[n].game_name)); + /* Players of another title are not "online" for this one. A row + * with no title yet (a client that has not listed) is kept. */ + if (g_lc.filter_game_name[0] && g_lc.online[n].game_name[0] && + strcmp(g_lc.online[n].game_name, g_lc.filter_game_name) != 0) + { p = end; continue; } if (g_lc.online[n].display_name[0]) ++n; p = end; } @@ -1785,12 +1801,39 @@ static void chat_push(const char *player_id, const char *from, const char *text, snprintf(m->player_id, sizeof(m->player_id), "%s", player_id ? player_id : ""); snprintf(m->from, sizeof(m->from), "%s", from ? from : ""); snprintf(m->text, sizeof(m->text), "%s", text); + /* Masked on arrival, whatever relayed it: the server already did this, + * an older server did not, and the rule is that nothing unmasked is + * ever shown. */ + if (!is_system) (void)rnet_chat_filter_apply(m->text, sizeof(m->text)); m->is_system = is_system ? 1 : 0; m->is_local = (!is_system && g_lc.player_id[0] && player_id && strcmp(player_id, g_lc.player_id) == 0) ? 1 : 0; m->seq = ++g_lc.chat_seq; } +static void schat_push(const char *player_id, const char *from, const char *text) +{ + PsxLobbyChatMsg *m; + int idx; + if (!text || !text[0]) return; + if (g_lc.schat_count < PSX_LOBBY_CHAT_RING) { + idx = (g_lc.schat_head + g_lc.schat_count) % PSX_LOBBY_CHAT_RING; + g_lc.schat_count++; + } else { + idx = g_lc.schat_head; + g_lc.schat_head = (g_lc.schat_head + 1) % PSX_LOBBY_CHAT_RING; + } + m = &g_lc.schat[idx]; + memset(m, 0, sizeof(*m)); + snprintf(m->player_id, sizeof(m->player_id), "%s", player_id ? player_id : ""); + snprintf(m->from, sizeof(m->from), "%s", from ? from : ""); + snprintf(m->text, sizeof(m->text), "%s", text); + (void)rnet_chat_filter_apply(m->text, sizeof(m->text)); + m->is_local = (g_lc.player_id[0] && player_id && + strcmp(player_id, g_lc.player_id) == 0) ? 1 : 0; + m->seq = ++g_lc.schat_seq; +} + void psx_lobby_chat_clear(void) { g_lc.chat_head = 0; @@ -1805,9 +1848,15 @@ static void handle_server_json(const char *json) json_get_str(json, "op", op, sizeof(op)); if (strcmp(op, "welcome") == 0) { json_get_str(json, "player_id", g_lc.player_id, sizeof(g_lc.player_id)); - if (g_lc.display_name[0]) { - char msg[256]; - snprintf(msg, sizeof(msg), "{\"op\":\"hello\",\"display_name\":\"%s\"}", g_lc.display_name); + { + /* Say who we are AND what we are playing in the first message: + * the server scopes players-online and server chat by title, and + * waiting for a `list` to tell it left a client that chatted + * first with no title at all. Title only -- never the version. */ + char msg[512]; + snprintf(msg, sizeof(msg), + "{\"op\":\"hello\",\"display_name\":\"%s\",\"game_name\":\"%s\"}", + g_lc.display_name, g_lc.filter_game_name); queue_send(msg); } queue_list_request(); @@ -2197,6 +2246,19 @@ static void handle_server_json(const char *json) g_lc.swap_out = json_get_bool(json, "accept", 0) ? 2 : -1; return; } + if (strcmp(op, "server_chat") == 0) { + char text[PSX_LOBBY_CHAT_TEXT_LEN]; + char from_id[PSX_LOBBY_ID_LEN]; + char from[PSX_LOBBY_NAME_LEN]; + text[0] = '\0'; + from_id[0] = '\0'; + from[0] = '\0'; + json_get_str(json, "text", text, sizeof(text)); + json_get_str(json, "from_player_id", from_id, sizeof(from_id)); + json_get_str(json, "from", from, sizeof(from)); + schat_push(from_id, from, text); + return; + } if (strcmp(op, "chat") == 0) { char text[PSX_LOBBY_CHAT_TEXT_LEN]; char from_id[PSX_LOBBY_ID_LEN]; @@ -3573,6 +3635,37 @@ int psx_lobby_chat_count(void) return g_lc.chat_count; } +int psx_lobby_send_server_chat(const char *text) +{ + char esc[PSX_LOBBY_CHAT_TEXT_LEN * 2 + 8]; + char msg[PSX_LOBBY_CHAT_TEXT_LEN * 2 + 256]; + int n; + if (!psx_lobby_connected()) return -1; + if (!text || !text[0]) return -1; + json_escape(text, esc, sizeof(esc)); + /* Carry the title on the line itself: the server scopes by it, and this + * works even against a server that has not seen our `list` yet. */ + n = snprintf(msg, sizeof(msg), + "{\"op\":\"server_chat\",\"game_name\":\"%s\",\"text\":\"%s\"}", + g_lc.filter_game_name, esc); + if (n < 0 || (size_t)n >= sizeof(msg)) return -1; + queue_send(msg); + flush_pending(); + return 0; +} + +int psx_lobby_server_chat_count(void) +{ + return g_lc.schat_count; +} + +int psx_lobby_server_chat_get(int index, PsxLobbyChatMsg *out) +{ + if (!out || index < 0 || index >= g_lc.schat_count) return 0; + *out = g_lc.schat[(g_lc.schat_head + index) % PSX_LOBBY_CHAT_RING]; + return 1; +} + int psx_lobby_chat_get(int index, PsxLobbyChatMsg *out) { if (!out || index < 0 || index >= g_lc.chat_count) return 0;