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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1dT6h1ZshMVUUVKTPxsCD
This commit is contained in:
Alex Vanderveen
2026-09-06 02:22:57 -04:00
parent 3d9930c5d3
commit ecc121ade6
3 changed files with 139 additions and 8 deletions
+21
View File
@@ -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.
+28
View File
@@ -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;
+90 -8
View File
@@ -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)