From c47f7391e8bf7e8edef792da3df9fdf21294611e Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sun, 11 Oct 2026 16:16:33 -0400 Subject: [PATCH] fix netplay --- runtime/include/psx_lobby_client.h | 4 +- runtime/include/psx_netplay.h | 5 +++ runtime/runtime.cmake | 6 ++- runtime/src/main.cpp | 5 +++ runtime/src/psx_lobby_client.c | 11 ++++- runtime/src/psx_netplay.c | 31 +++++++++++++- runtime/tests/test_lobby_chat_ring.c | 63 ++++++++++++++++++++++++++++ 7 files changed, 121 insertions(+), 4 deletions(-) diff --git a/runtime/include/psx_lobby_client.h b/runtime/include/psx_lobby_client.h index a0960172..85b33d7a 100644 --- a/runtime/include/psx_lobby_client.h +++ b/runtime/include/psx_lobby_client.h @@ -10,7 +10,9 @@ extern "C" { #define PSX_LOBBY_ID_LEN 40 #define PSX_LOBBY_NAME_LEN 64 -#define PSX_LOBBY_VERSION_LEN 32 +/* Title compatibility IDs may contain a prefix and a 160-bit source hash. + * Preserve the full 63-byte ID for exact list/join comparisons. */ +#define PSX_LOBBY_VERSION_LEN 64 #define PSX_LOBBY_ENDPOINT_LEN 64 #define PSX_LOBBY_MAX_LIST 32 /* Player seats this framework can seat. Was what PSX_LOBBY_MAX_MEMBERS meant diff --git a/runtime/include/psx_netplay.h b/runtime/include/psx_netplay.h index 0d433c68..fe7a8906 100644 --- a/runtime/include/psx_netplay.h +++ b/runtime/include/psx_netplay.h @@ -354,6 +354,11 @@ void psx_netplay_wait_recv(int timeout_ms); void psx_netplay_admit_wait_info(char *stall_out, size_t stall_cap, uint32_t *sim_tick_out, int *lead_out); +/* One-line transport snapshot for a session stuck before RUNNING: ICE state, + * selected path, datagrams received, recv-queue drops and time since the last + * peer datagram. Writes "inactive" when no session exists. */ +void psx_netplay_link_diag(char *out, size_t cap); + /* Normalize for stabler cross-device blobs: unknown types become digital; * centred axes (sticks, wheel, twist) snap to 0x80 inside a deadzone; NeGcon * pressure bytes (ly/rx/ry) are kept exactly; digital centres everything. */ diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 6a6d8931..fb0b1988 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -118,6 +118,9 @@ endif() # release when no system package is available, so a default build does not # silently fall back to SDL2. SDL2 remains an explicit, fully supported A/B # backend. +# Dev-only: the lobby browser lists every version of this title instead of +# pinning to our exact game_version. Joining still requires an exact match. +option(PSX_LOBBY_DEV_LIST "Lobby browser ignores game_version (dev testing only)" OFF) set(PSX_SDL_BACKEND "SDL3" CACHE STRING "SDL backend (SDL3 or SDL2)") set_property(CACHE PSX_SDL_BACKEND PROPERTY STRINGS SDL3 SDL2) string(TOUPPER "${PSX_SDL_BACKEND}" _psx_sdl_backend) @@ -2002,7 +2005,8 @@ function(psxrecomp_add_runtime_target target) # target-wide compile line so Ninja does not rebuild every runtime + shard TU. set_source_files_properties( "${PSXRECOMP_ROOT}/runtime/src/psx_lobby_client.c" - PROPERTIES COMPILE_DEFINITIONS "PSX_GAME_VERSION=\"${PSXRT_GAME_VERSION}\"" + PROPERTIES COMPILE_DEFINITIONS + "PSX_GAME_VERSION=\"${PSXRT_GAME_VERSION}\";$<$:PSX_LOBBY_DEV_LIST=1>" ) set_source_files_properties( "${PSXRECOMP_ROOT}/runtime/src/crash_trace.c" diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index ab294b0f..cf33df28 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -6803,6 +6803,11 @@ static void netplay_barrier_admit(int override) { (unsigned long long)(now_ms - clock0), running ? "" : ", linking"); last_stall_log_ms = now_ms; + if (!running) { + char link[320]; + psx_netplay_link_diag(link, sizeof(link)); + std::fprintf(stderr, "psxrecomp: netplay link not up: %s\n", link); + } } psx_lobby_pump(); if (psx_netplay_input_desync(&dt, &lh, &rh)) { diff --git a/runtime/src/psx_lobby_client.c b/runtime/src/psx_lobby_client.c index 5928cf47..da9af852 100644 --- a/runtime/src/psx_lobby_client.c +++ b/runtime/src/psx_lobby_client.c @@ -1084,6 +1084,9 @@ static const char *effective_game_version(const char *override_ver) * unofficial / mismatched hosts; join still requires an exact version match. */ static int list_filter_version_strict(void) { +#ifdef PSX_LOBBY_DEV_LIST + return 0; +#endif const char *gv = effective_game_version(NULL); return gv && gv[0] && strcmp(gv, "dev") != 0; } @@ -1317,7 +1320,7 @@ static void host_ice_step(void) v.is_host = g_lc.is_host ? 1 : 0; v.local_slot = local_member_slot(); v.active = psx_lobby_connected() && g_lc.in_lobby && !g_lc.launch_pending && - !g_lc.join.local_is_spectator && room_relays_via_ice() && + !g_lc.ice_rtt_suspended && !g_lc.join.local_is_spectator && room_relays_via_ice() && v.local_slot >= 0; if (v.active) { if (v.is_host) { @@ -5492,6 +5495,12 @@ int psx_lobby_request_start(const PsxLobbyMatchCaps *match_caps) return -1; } } + /* The encoder selects the host relay's path from the local preference. + * Keep the waiting-room view on that same path while START is in flight: + * callers construct fresh caps and need not copy relay_via_ice. Clearing + * it here destroys the completed agents before LAUNCH can take them. */ + caps_local.relay_via_ice = caps_local.relay_host && + g_relay_via_ice_pref && rnet_host_ice_available(); g_lc.match_caps = caps_local; send = &caps_local; } diff --git a/runtime/src/psx_netplay.c b/runtime/src/psx_netplay.c index 30b310b3..329dc2f0 100644 --- a/runtime/src/psx_netplay.c +++ b/runtime/src/psx_netplay.c @@ -548,6 +548,10 @@ int psx_netplay_input_delay(void) { return 2; } int psx_netplay_catchup_budget(void) { return 0; } void psx_netplay_catchup_consume_frame(void) {} void psx_netplay_wait_recv(int timeout_ms) { (void)timeout_ms; } +void psx_netplay_link_diag(char *out, size_t cap) +{ + if (out && cap) snprintf(out, cap, "off"); +} void psx_netplay_admit_wait_info(char *stall_out, size_t stall_cap, uint32_t *sim_tick_out, int *lead_out) { @@ -4378,7 +4382,7 @@ int psx_netplay_start(const PsxNetplayConfig *cfg) } } - if (!use_ice) { + if (!use_ice && !ice_hub) { /* Host-as-relay: slot 0 with 3+ seats and no dial peer. */ #if PSX_MAX_PLAYERS >= 3 const int peer_empty = @@ -5680,6 +5684,31 @@ void psx_netplay_wait_recv(int timeout_ms) (void)rnet_session_wait_recv(g_np.session, timeout_ms); } +void psx_netplay_link_diag(char *out, size_t cap) +{ + RNetSessionStats st; + static const char *const k_state[] = { + "idle", "gathering", "connecting", "connected", "completed", "failed" + }; + unsigned si; + if (!out || !cap) return; + if (!psx_netplay_active() || !g_np.session) { + snprintf(out, cap, "inactive"); + return; + } + memset(&st, 0, sizeof(st)); + rnet_session_get_stats(g_np.session, &st); + si = (unsigned)st.ice_state; + snprintf(out, cap, + "ice=%s path=%s local=%s remote=%s rx=%u recv_drops=%u rx_age_ms=%llu", + si < sizeof(k_state) / sizeof(k_state[0]) ? k_state[si] : "?", + st.ice_path[0] ? st.ice_path : "-", + st.ice_local[0] ? st.ice_local : "-", + st.ice_remote[0] ? st.ice_remote : "-", + (unsigned)st.packets_rx, (unsigned)st.ice_recv_drops, + (unsigned long long)st.last_peer_rx_age_ms); +} + void psx_netplay_admit_wait_info(char *stall_out, size_t stall_cap, uint32_t *sim_tick_out, int *lead_out) { diff --git a/runtime/tests/test_lobby_chat_ring.c b/runtime/tests/test_lobby_chat_ring.c index 339cce84..5e96cd71 100644 --- a/runtime/tests/test_lobby_chat_ring.c +++ b/runtime/tests/test_lobby_chat_ring.c @@ -218,6 +218,67 @@ static void case_profile_settings_roundtrip(void) !strcmp(g_lc.match_caps.mods[0].feats, caps.mods[0].feats), "trusted settings survive the online lobby codec"); } +static void case_full_title_profile_version(void) +{ + static char version[64]; + static PSXModNetplayProfile profile = {0}; + char wire[768]; + memset(version, 'v', sizeof(version) - 1); + version[sizeof(version) - 1] = '\0'; + profile.plugin_id = "lobby.profile.test"; + profile.compatibility_id = version; + ck(psx_mod_register_netplay_profile(&profile), "register maximum-length title profile"); + psx_lobby_set_game_identity("Profile Fixture", version); + ck(!strcmp(g_lc.filter_game_version, version), "retain all 63 profile bytes in local metadata"); + /* Suppress discovery: this fixture checks the native codec/filter only. */ + g_list_rtt_active = 1; + snprintf(wire, sizeof(wire), "{\"op\":\"lobby_list\",\"lobbies\":[{" + "\"lobby_id\":\"profile-room\",\"name\":\"Profile Room\"," + "\"game_name\":\"Profile Fixture\",\"game_version\":\"%s\"}]}", version); + handle_server_json(wire); + ck(g_lc.list_count == 1, "matching full profile remains visible"); + ck(!strcmp(g_lc.list[0].game_version, version), "room metadata retains full profile"); + /* A difference beyond the former 31-byte prefix must still reject. */ + char *last = strstr(wire, version); + if (last) last[62] = 'w'; + ck(last != NULL, "locate profile in fixture"); + handle_server_json(wire); + ck(g_lc.list_count == 0, "reject profile differing only in final byte"); + g_list_rtt_active = 0; +} + +static void case_start_retains_relay_path(void) +{ + PsxLobbyMatchCaps caps = {0}, decoded; + char wire[4096]; + int prefer, host; + const int old_pref = g_relay_via_ice_pref; + printf(" START keeps the advertised relay path locally\n"); + for (prefer = 0; prefer < 2; ++prefer) { + for (host = 0; host < 2; ++host) { + memset(&g_lc, 0, sizeof(g_lc)); + /* An unhandshaken fixture queues START without touching a socket. */ + g_lc.connected = g_lc.in_lobby = g_lc.is_host = 1; + g_lc.fd = 0; + caps.valid = 1; + caps.relay_host = host; + caps.relay_via_ice = 0; /* fresh launcher caps, as in the regression */ + strcpy(caps.session_bios, "openbios"); + psx_lobby_set_relay_via_ice_pref(prefer); + ck(psx_lobby_request_start(&caps) == 0, "queue host START"); + ck(psx_lobby_encode_match_caps(wire, sizeof(wire), &caps) && + psx_lobby_decode_match_caps(wire, &decoded), "decode advertised START caps"); + ck(g_lc.match_caps.relay_via_ice == decoded.relay_via_ice, + "local ICE path matches the wire before LAUNCH arrives"); + ck(room_relays_via_ice() == (host && prefer && rnet_host_ice_available()), + "waiting-room agents remain active for an ICE host START"); + } + } + memset(&g_lc, 0, sizeof(g_lc)); + g_lc.fd = -1; + psx_lobby_set_relay_via_ice_pref(old_pref); +} + int main(void) { case_room_order(); @@ -226,6 +287,8 @@ int main(void) case_long_line_is_truncated_not_dropped(); case_mod_caps_roundtrip(); case_profile_settings_roundtrip(); + case_full_title_profile_version(); + case_start_retains_relay_path(); if (g_failures == 0) { printf("lobby_chat_ring_test: ok\n"); return 0;