From ecc121ade650f37b7a88cf6759809e5b8259759d Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sun, 6 Sep 2026 02:22:57 -0400 Subject: [PATCH] lobby browser: spectator column, players online, gallery latency - lobby_list rows carry allow_spectators / max_spectators / spectator_count (browser shows "No" or "1/4"), and every lobby_list carries a players array: everyone on the hub with country, room and a short connection tag so a client can pick out its own row without publishing whole ids. - Spectator latency: the round-trip table was keyed by seat and bounds- checked against its size, so gallery seats (numbered from 64) were always dropped. Seats now map to table cells, player seats first. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01V1dT6h1ZshMVUUVKTPxsCD --- runtime/include/psx_lobby_client.h | 21 +++++++ runtime/src/main.cpp | 28 +++++++++ runtime/src/psx_lobby_client.c | 98 +++++++++++++++++++++++++++--- 3 files changed, 139 insertions(+), 8 deletions(-) diff --git a/runtime/include/psx_lobby_client.h b/runtime/include/psx_lobby_client.h index da837a72..cabef39b 100644 --- a/runtime/include/psx_lobby_client.h +++ b/runtime/include/psx_lobby_client.h @@ -51,8 +51,26 @@ typedef struct PsxLobbyRow { int latency_ms; /* Host's country (alpha-2) from the server's GeoIP; "" unknown. */ char host_country[4]; + /* Gallery: 0 = none; else how many watch out of how many seats. */ + int allow_spectators; + int max_spectators; + int spectator_count; } PsxLobbyRow; +/* One player connected to the hub (the lobby_list `players` array). */ +typedef struct PsxLobbyOnlinePlayer { + char player_id[PSX_LOBBY_ID_LEN]; + char display_name[PSX_LOBBY_NAME_LEN]; + char country[4]; + char lobby_id[PSX_LOBBY_ID_LEN]; + char lobby_name[PSX_LOBBY_NAME_LEN]; + int hosting; + /* 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]; +} PsxLobbyOnlinePlayer; +#define PSX_LOBBY_MAX_ONLINE 64 + typedef struct PsxLobbyMember { /* Seat index in the shared namespace: a player seat, or * spectator_slot_base + gallery index. Pass it back to kick / move as-is. */ @@ -219,6 +237,9 @@ void psx_lobby_set_max_slots(int max_slots); void psx_lobby_request_list(void); int psx_lobby_list_count(void); int psx_lobby_list_get(int index, PsxLobbyRow *out); +/* Players connected to the hub, refreshed with every lobby_list. */ +int psx_lobby_online_count(void); +int psx_lobby_online_get(int index, PsxLobbyOnlinePlayer *out); /* * Create lobby. host_bind e.g. "0.0.0.0:7777". password may be NULL/empty. diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 2a2b030f..65176729 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -10831,6 +10831,32 @@ namespace { out->latency_ms = row.latency_ms; std::snprintf(out->host_country, sizeof(out->host_country), "%s", row.host_country); + out->allow_spectators = row.allow_spectators; + out->max_spectators = row.max_spectators; + out->spectator_count = row.spectator_count; + return 1; + } + + /* Players online: the hub's `players` list. LAN rooms have no hub, so + * a LAN-only session reports nobody rather than guessing. */ + int ae_np_online_count(void*) { + return psx_lobby_connected() ? psx_lobby_online_count() : 0; + } + int ae_np_online_get(void*, int index, RecompLauncherCNetplayOnlinePlayer* out) { + if (!out) return 0; + PsxLobbyOnlinePlayer p{}; + if (!psx_lobby_online_get(index, &p)) return 0; + std::memset(out, 0, sizeof(*out)); + std::snprintf(out->display_name, sizeof(out->display_name), "%s", p.display_name); + std::snprintf(out->country, sizeof(out->country), "%s", p.country); + std::snprintf(out->lobby_name, sizeof(out->lobby_name), "%s", p.lobby_name); + out->in_lobby = p.lobby_id[0] != '\0'; + out->hosting = p.hosting; + /* Which row is us: the hub tags each row with the first characters + * of its connection id. A name match would mark every namesake. */ + const char* me = psx_lobby_player_id(); + out->is_local = me && me[0] && p.tag[0] && + std::strncmp(me, p.tag, std::strlen(p.tag)) == 0; return 1; } @@ -11986,6 +12012,8 @@ namespace { g_lnch_netplay_callbacks.chat_count = ae_np_chat_count; g_lnch_netplay_callbacks.chat_get = ae_np_chat_get; 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.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 7fab9401..73c17954 100644 --- a/runtime/src/psx_lobby_client.c +++ b/runtime/src/psx_lobby_client.c @@ -197,6 +197,8 @@ typedef struct { size_t ws_pending_len; PsxLobbyRow list[PSX_LOBBY_MAX_LIST]; int list_count; + PsxLobbyOnlinePlayer online[PSX_LOBBY_MAX_ONLINE]; + int online_count; int in_lobby; int is_host; char host_player_id[PSX_LOBBY_ID_LEN]; @@ -916,6 +918,22 @@ static void member_rtt_clear(void) g_lc.rtt_next_ping_ms = 0; } +/* member_rtt_ms has a cell per seat in BOTH tables, but a gallery seat is + * numbered from the server's spectator base (64+), not from the array. Map + * a seat to its cell; -1 for a seat neither table has. Without this every + * spectator's report was dropped as out of range and the gallery showed no + * latency at all. */ +static int rtt_index_for_slot(int slot) +{ + int base; + if (slot < 0) return -1; + if (slot < PSX_LOBBY_MAX_PLAYERS) return slot; + base = psx_lobby_spectator_slot_base(); + if (base > 0 && slot >= base && slot < base + PSX_LOBBY_MAX_SPECTATORS) + return PSX_LOBBY_MAX_PLAYERS + (slot - base); + return -1; +} + static int member_slot_for_player(const char *player_id) { int i; @@ -1259,6 +1277,66 @@ static int json_get_int(const char *json, const char *key, int def) return (int)strtol(p + 1, NULL, 10); } +static int json_get_bool(const char *json, const char *key, int def); + +/* The `players` array of a lobby_list: everyone on the hub. Flat objects, + * so each one is cut out by brace depth and read with the key getters. An + * older server has no such array and the count simply goes to zero. */ +static void lobby_list_parse_players(const char *json) +{ + const char *p = strstr(json, "\"players\""); + int n = 0; + g_lc.online_count = 0; + if (!p) return; + p = strchr(p, '['); + if (!p) return; + ++p; + while (*p && n < PSX_LOBBY_MAX_ONLINE) { + const char *obj, *end; + int depth = 0; + char chunk[512]; + size_t len; + while (*p && *p != '{' && *p != ']') ++p; + if (*p != '{') break; + obj = end = p; + do { + if (*end == '{') ++depth; + else if (*end == '}') --depth; + ++end; + } while (*end && depth > 0); + len = (size_t)(end - obj); + if (len >= sizeof(chunk)) len = sizeof(chunk) - 1; + memcpy(chunk, obj, len); + chunk[len] = '\0'; + memset(&g_lc.online[n], 0, sizeof(g_lc.online[n])); + json_get_str(chunk, "display_name", g_lc.online[n].display_name, + sizeof(g_lc.online[n].display_name)); + json_get_str(chunk, "country", g_lc.online[n].country, + sizeof(g_lc.online[n].country)); + json_get_str(chunk, "lobby_id", g_lc.online[n].lobby_id, + sizeof(g_lc.online[n].lobby_id)); + json_get_str(chunk, "lobby_name", g_lc.online[n].lobby_name, + 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)); + if (g_lc.online[n].display_name[0]) ++n; + p = end; + } + g_lc.online_count = n; +} + +int psx_lobby_online_count(void) +{ + return g_lc.online_count; +} + +int psx_lobby_online_get(int index, PsxLobbyOnlinePlayer *out) +{ + if (!out || index < 0 || index >= g_lc.online_count) return 0; + *out = g_lc.online[index]; + return 1; +} + static int json_get_bool(const char *json, const char *key, int def) { char pat[80]; @@ -1783,6 +1861,7 @@ static void handle_server_json(const char *json) if (strcmp(op, "lobby_list") == 0) { const char *p = strstr(json, "\"lobbies\""); int n = 0; + lobby_list_parse_players(json); /* Keep prior RTTs across server list pushes; Refresh re-probes. * Invalidate when host_endpoint or lan_endpoints change. */ char prev_ids[PSX_LOBBY_MAX_LIST][PSX_LOBBY_ID_LEN]; @@ -1885,6 +1964,9 @@ static void handle_server_json(const char *json) g_lc.list[n].has_password = json_get_bool(chunk, "has_password", 0); json_get_str(chunk, "host_country", g_lc.list[n].host_country, sizeof(g_lc.list[n].host_country)); + g_lc.list[n].allow_spectators = json_get_bool(chunk, "allow_spectators", 0); + g_lc.list[n].max_spectators = json_get_int(chunk, "max_spectators", 0); + g_lc.list[n].spectator_count = json_get_int(chunk, "spectator_count", 0); json_get_str(chunk, "host_endpoint", g_lc.list[n].host_endpoint, sizeof(g_lc.list[n].host_endpoint)); g_lc.list[n].lan_count = json_parse_str_array( @@ -2142,12 +2224,11 @@ static void handle_server_json(const char *json) if (type == PSX_LOBBY_SIG_RTT_PING || type == PSX_LOBBY_SIG_RTT_PONG) return; if (type == PSX_LOBBY_SIG_RTT_REPORT) { - int slot = member_slot_for_player(from); + int slot = rtt_index_for_slot(member_slot_for_player(from)); int ms = (int)strtol(text_buf, NULL, 10); /* Max with local measure — never let an optimistic peer REPORT * undercut delay provisioning (Force TURN / asymmetric ICE). */ - if (slot >= 0 && slot < PSX_LOBBY_MAX_MEMBERS && ms >= 0 && - ms <= 60000) { + if (slot >= 0 && ms >= 0 && ms <= 60000) { if (g_lc.member_rtt_ms[slot] < 0 || ms > g_lc.member_rtt_ms[slot]) g_lc.member_rtt_ms[slot] = ms; @@ -2743,8 +2824,8 @@ static void lobby_rtt_store_for_peer(const char *peer_id, int ms) int slot; if (ms < 0 || !peer_id || !peer_id[0]) return; - slot = member_slot_for_player(peer_id); - if (slot < 0 || slot >= PSX_LOBBY_MAX_MEMBERS) + slot = rtt_index_for_slot(member_slot_for_player(peer_id)); + if (slot < 0) return; if (g_lc.member_rtt_ms[slot] < 0 || ms > g_lc.member_rtt_ms[slot]) g_lc.member_rtt_ms[slot] = ms; @@ -3350,13 +3431,14 @@ int psx_lobby_member_get(int index, PsxLobbyMember *out) int psx_lobby_member_latency_ms(int slot) { - int local; - if (slot < 0 || slot >= PSX_LOBBY_MAX_MEMBERS) + int local, idx; + idx = rtt_index_for_slot(slot); + if (idx < 0) return -1; local = local_member_slot(); if (local >= 0 && slot == local) return -1; /* never show self-RTT */ - return g_lc.member_rtt_ms[slot]; + return g_lc.member_rtt_ms[idx]; } int psx_lobby_member_is_host(const PsxLobbyMember *member)