patch lobby chat

This commit is contained in:
Alex Vanderveen
2026-09-06 03:31:56 -04:00
parent ecc121ade6
commit 0f1fd021a8
5 changed files with 154 additions and 4 deletions
+12
View File
@@ -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),
+9
View File
@@ -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);
+36
View File
@@ -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;
+96 -3
View File
@@ -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;