mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Integrate current ICE lobby transports and preserve settled LAN boot settings
This commit is contained in:
+39
-8
@@ -240,16 +240,47 @@ that identical frame; only what its window shows changes:
|
||||
|
||||
## Connectivity: ICE, TURN, SFU, LAN
|
||||
|
||||
### Online lobbies (WebSocket + SFU)
|
||||
### Online lobbies (WebSocket + host relay, SFU fallback)
|
||||
|
||||
Online matches go through the lobby at **netplay.retcomm.net**
|
||||
(WebSocket control plane). Match **UDP pad traffic** for online rooms uses an
|
||||
**SFU star** (selective forwarding unit on the lobby server): every peer sends
|
||||
to the SFU; the SFU fans out to other seats. Peers do **not** mesh each other
|
||||
for game data online.
|
||||
(WebSocket control plane). Match **UDP pad traffic** is a star; who is the
|
||||
hub depends on the launch the server sends (`transport`, `relay_via`):
|
||||
|
||||
Current lobby policy prefers **always SFU** for online starts so CGNAT /
|
||||
asymmetric NAT does not strand players on failed ICE attempts.
|
||||
| Launch | Hub | Transport in the engine |
|
||||
|--------|-----|-------------------------|
|
||||
| `transport "host"` + `relay_via "ice"` (default when the host's build has ICE) | **the host** | **ICE hub** — the ICE agents the waiting room already connected are adopted by the session (`rnet_session_start_ice_hub_adopt` on the host, `rnet_session_adopt_ice_agent` on a guest). No UDP port is bound or forwarded, nothing is dialled, no TURN. |
|
||||
| `transport "host"`, no `relay_via` (older server / host without ICE) | **the host** | LAN path on the host's advertised UDP port (UPnP / NAT-PMP / STUN, probed by guests). |
|
||||
| otherwise | the lobby server's **SFU** | LAN path to the server's relay endpoint. |
|
||||
|
||||
Host relay is a preference of the hosting player (launcher toggles, default
|
||||
on): `psx_lobby_set_relay_host_pref` publishes `match_caps.relay = "host"`;
|
||||
`psx_lobby_set_relay_via_ice_pref` adds `relay_via = "ice"` (only when the
|
||||
build can run ICE). The server falls back to its SFU unless every guest proved
|
||||
its path to the host.
|
||||
|
||||
**Host relay over ICE.** In the waiting room the host runs one ICE agent per
|
||||
seated guest (host = answerer, guests offer), signalled over the lobby
|
||||
`signal` op in its own type range (`130 + RNetSignalType`); a guest's proof is
|
||||
`path_report direct`. `psx_lobby_client.c` carries that glue (the proof logic
|
||||
is recomp-net's `RNetHostIce`). At launch the client captures the COMPLETED
|
||||
agents; `ae_np_clear_launch_pending` takes them (before the lobby launch is
|
||||
cleared) into `psx_netplay`'s stash and `psx_netplay_start` adopts them. A
|
||||
seat that is not connected, or a spectator in the room, **refuses the launch**:
|
||||
`join.last_error == "ice_not_connected"`, reason in
|
||||
`psx_lobby_ice_launch_error()`; a smaller room is never started.
|
||||
|
||||
Slot mapping: the lobby seat of each agent is mapped to its **session slot**
|
||||
from the launch's `port_of_slot` table (`psx_ice_hub_map_seats`,
|
||||
`netplay_ice_hub.c`), which already folds in `host_spectates` (slot 0 is the
|
||||
silent host, players start at slot 1) and moved seats (slots follow ascending
|
||||
lobby seat, the host is always slot 0). Every non-host session slot must get
|
||||
exactly one agent. `psx_transport_decide` guarantees an ICE-hub launch is
|
||||
never routed to the single-agent ICE path or the SFU, whatever else the config
|
||||
carries. Covered ROM-free by `netplay_ice_hub_test`.
|
||||
|
||||
Not on this path: spectators (gallery seats), and the waiting-room latency
|
||||
column in ICE mode (the per-pair RTT probe is disabled there so it cannot race
|
||||
the proof's `path_report`).
|
||||
|
||||
### ICE + TURN
|
||||
|
||||
@@ -262,7 +293,7 @@ libjuice is pulled as a **pinned URL tarball** (not `git clone`) so Retro
|
||||
AppImage / mismatched-libcurl hosts can still configure; offline builds can
|
||||
vendor `lib/recomp-net/third_party/libjuice` or set `-DRNET_LIBJUICE_ROOT`.
|
||||
“Force TURN” in the UI can raise delay floors for relay-heavy paths; it
|
||||
does not replace the SFU online architecture above.
|
||||
does not change which transport the launch selects (see Online lobbies).
|
||||
|
||||
### LAN / Direct IP (P2P star)
|
||||
|
||||
|
||||
+1
-1
Submodule lib/recomp-net updated: b1eab87bf3...bb6a616c44
@@ -91,6 +91,12 @@ if(BUILD_TESTING)
|
||||
add_executable(netplay_local_view_test tests/test_netplay_local_view.c)
|
||||
target_include_directories(netplay_local_view_test PRIVATE include)
|
||||
add_test(NAME netplay_local_view_test COMMAND netplay_local_view_test)
|
||||
# Host relay over ICE: transport choice + lobby seat -> session slot map.
|
||||
add_executable(netplay_ice_hub_test
|
||||
tests/test_netplay_ice_hub.c
|
||||
src/netplay_ice_hub.c)
|
||||
target_include_directories(netplay_ice_hub_test PRIVATE include)
|
||||
add_test(NAME netplay_ice_hub_test COMMAND netplay_ice_hub_test)
|
||||
add_executable(ws_scene_hold_test tests/test_ws_scene_hold.c)
|
||||
target_include_directories(ws_scene_hold_test PRIVATE include)
|
||||
add_test(NAME ws_scene_hold_test COMMAND ws_scene_hold_test)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/* netplay_ice_hub.h -- host relay over ICE: the pure decisions.
|
||||
*
|
||||
* Two questions the engine must answer identically on every peer, kept free of
|
||||
* recomp-net and of the lobby client so they can be tested without a ROM, a
|
||||
* socket, or an ICE agent:
|
||||
*
|
||||
* 1. Which transport does a launch use? psx_transport_decide
|
||||
* 2. Which SESSION slot does each connected guest agent serve?
|
||||
* psx_ice_hub_map_seats
|
||||
*
|
||||
* Doctrine: a launch with transport_ice_hub binds no UDP port and dials
|
||||
* nothing. It must never fall through to the single-agent ICE path (that is a
|
||||
* different negotiation, signalled over the legacy ICE types) or to the
|
||||
* server's SFU, even when the config also carries force_input_relay or an
|
||||
* explicit transport=ice. A seat that cannot be mapped refuses the launch
|
||||
* rather than starting a smaller room.
|
||||
*/
|
||||
#ifndef NETPLAY_ICE_HUB_H
|
||||
#define NETPLAY_ICE_HUB_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum PsxTransportChoice {
|
||||
PSX_TRANSPORT_ERROR_NO_PEER = -1, /* online / SFU launch with no endpoint */
|
||||
PSX_TRANSPORT_LAN_UDP = 0, /* bind + (hub | dial): LAN, host relay, SFU */
|
||||
PSX_TRANSPORT_SINGLE_ICE = 1, /* one negotiated agent (explicit ice only) */
|
||||
PSX_TRANSPORT_ICE_HUB = 2 /* adopt the waiting room's connected agents */
|
||||
} PsxTransportChoice;
|
||||
|
||||
/* cfg_transport: PsxNetplayConfig.transport (0 auto, 1 force ICE, 2 force LAN).
|
||||
* in_online_room: connected to the lobby and seated (a MotK online room).
|
||||
* ice_built: this build can run ICE agents.
|
||||
* An ICE hub launch under force-LAN is an error: the launch carries no
|
||||
* endpoint to bind or dial. */
|
||||
PsxTransportChoice psx_transport_decide(int cfg_transport, int transport_ice_hub,
|
||||
int transport_host, int force_input_relay,
|
||||
int in_online_room, int has_peer_endpoint,
|
||||
int ice_built);
|
||||
|
||||
#define PSX_ICE_HUB_MAX_SEATS 8
|
||||
|
||||
/* Why a mapping was refused (for the log / the launch error). */
|
||||
typedef enum PsxIceHubMapError {
|
||||
PSX_ICE_HUB_MAP_OK = 0,
|
||||
PSX_ICE_HUB_MAP_BAD_ARGS,
|
||||
PSX_ICE_HUB_MAP_SEAT_UNMAPPED, /* a lobby seat holds no session slot */
|
||||
PSX_ICE_HUB_MAP_SEAT_DUPLICATE, /* two agents for one lobby seat / slot */
|
||||
PSX_ICE_HUB_MAP_SEAT_MISSING /* a non-host session slot has no agent */
|
||||
} PsxIceHubMapError;
|
||||
|
||||
/* Map each connected guest's LOBBY seat to the SESSION slot it serves.
|
||||
*
|
||||
* port_of_slot[s] (s in 0..slot_count-1): the lobby seat session slot s
|
||||
* drives, -1 for none. Slot 0 is the host and is never an agent seat, so
|
||||
* the table is searched from slot 1. This is the table the launch already
|
||||
* carries (PsxNetplayConfig.port_of_slot): it folds in the host-in-gallery
|
||||
* offset (host_spectates: slot 0 has port -1, players start at slot 1) and
|
||||
* moved seats (slots follow ascending lobby seat, not lobby seat itself).
|
||||
* port_map_valid == 0: no table; identity, or lobby seat + 1 with
|
||||
* host_spectates (players sit one slot above their seat).
|
||||
*
|
||||
* Requires exactly slot_count - 1 distinct guest seats, every one mapped to a
|
||||
* distinct session slot in 1..slot_count-1: a session slot with no agent would
|
||||
* be a smaller room. Returns 0 and fills session_slots_out[i] (parallel to
|
||||
* lobby_slots[i]), else -1 with *err_out set. */
|
||||
int psx_ice_hub_map_seats(const int *port_of_slot, int port_map_valid,
|
||||
int slot_count, int host_spectates,
|
||||
const int *lobby_slots, int n,
|
||||
int *session_slots_out, PsxIceHubMapError *err_out);
|
||||
|
||||
const char *psx_ice_hub_map_error_text(PsxIceHubMapError e);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NETPLAY_ICE_HUB_H */
|
||||
@@ -208,6 +208,11 @@ typedef struct PsxLobbyMatchCaps {
|
||||
* server falls back to its relay unless every guest proved the path
|
||||
* (recomp_net/host_relay.h). Guests read it to know the room asks. */
|
||||
int relay_host;
|
||||
/* 0/1 -- with relay_host: match_caps.relay_via = "ice". The host carries
|
||||
* the match over the ICE agents the waiting room already connected (one
|
||||
* per guest); no port is forwarded or advertised. Published by the host
|
||||
* when psx_lobby_relay_via_ice() is on and this build has ICE. */
|
||||
int relay_via_ice;
|
||||
int force_turn; /* 0/1 — ICE relay-only (Force TURN for UDP) */
|
||||
int rollback; /* 0/1 — invent/rollback netplay (default on) */
|
||||
/* DualShock-on-multitap-tap hack (0/1). Host-authoritative for the match. */
|
||||
@@ -276,6 +281,14 @@ typedef struct PsxLobbyJoinInfo {
|
||||
* (peer-to-peer, signalled over the lobby). No endpoint is dialled and
|
||||
* force_input_relay is 0 (psx_lobby_launch_transport.h). */
|
||||
int transport_ice;
|
||||
/* Launch: transport "host" AND relay_via "ice". transport_host is ALSO 1
|
||||
* then, so test this first. Nothing is bound or dialled (bind_hostport is
|
||||
* the placeholder 0.0.0.0:0, peer_hostport empty): the match rides the
|
||||
* agents psx_lobby_ice_take_* hands over. A launch whose seats are not all
|
||||
* connected is refused in the client: launch_pending stays 0 and
|
||||
* last_error is "ice_not_connected" (psx_lobby_ice_launch_error() says
|
||||
* which seat). */
|
||||
int transport_ice_hub;
|
||||
/* A server code (need_password | bad_password | …) or, from
|
||||
* psx_lobby_set_last_error, one sentence the launcher shows as is. */
|
||||
char last_error[192];
|
||||
@@ -423,6 +436,37 @@ struct RNetHostRelayStatus;
|
||||
int psx_lobby_host_relay_status(struct RNetHostRelayStatus *out);
|
||||
void psx_lobby_host_relay_release_port(void);
|
||||
|
||||
/* Host relay over ICE (recomp_net/host_ice.h). The pref is the HOST's, default
|
||||
* on; it only publishes relay_via "ice" when the build can run ICE. */
|
||||
void psx_lobby_set_relay_via_ice_pref(int on);
|
||||
int psx_lobby_relay_via_ice_pref(void);
|
||||
/* 1 when this build can run ICE agents at all. */
|
||||
int psx_lobby_ice_available(void);
|
||||
struct RNetHostIceStatus;
|
||||
/* Waiting-room per-seat state; 1 while the ICE proof is running. */
|
||||
int psx_lobby_host_ice_status(struct RNetHostIceStatus *out);
|
||||
|
||||
/* Launch handover (transport_ice_hub). The agents are COMPLETED and owned by
|
||||
* the caller on success: give them to the session or release them with
|
||||
* psx_lobby_ice_destroy_agent. TAKE BEFORE psx_lobby_clear_launch_pending's
|
||||
* consumer finishes with the launch -- an untaken bundle is destroyed after
|
||||
* 60 s, at the next launch, or on leave. lobby_slot is the lobby's seat
|
||||
* namespace; the engine maps it to its own session slot. */
|
||||
struct RNetIceAgent;
|
||||
typedef struct PsxLobbyIceSeat {
|
||||
int lobby_slot;
|
||||
char player_id[PSX_LOBBY_ID_LEN];
|
||||
struct RNetIceAgent *agent;
|
||||
} PsxLobbyIceSeat;
|
||||
/* Host: n >= 1 (ownership moves to the caller) or -1. */
|
||||
int psx_lobby_ice_take_hub(PsxLobbyIceSeat *out, int max);
|
||||
/* Guest: the one agent connected to the host, or NULL. */
|
||||
struct RNetIceAgent *psx_lobby_ice_take_guest_agent(void);
|
||||
/* Why the last launch was refused / a take failed ("" when none). */
|
||||
const char *psx_lobby_ice_launch_error(void);
|
||||
void psx_lobby_ice_discard(void);
|
||||
void psx_lobby_ice_destroy_agent(struct RNetIceAgent *agent);
|
||||
|
||||
/* Live member table from lobby_update (and create/join). */
|
||||
int psx_lobby_member_count(void);
|
||||
int psx_lobby_member_get(int index, PsxLobbyMember *out);
|
||||
|
||||
@@ -98,6 +98,16 @@ typedef struct PsxNetplayConfig {
|
||||
* carries the match on bind_hostport; guests dial peer_hostport. Takes
|
||||
* the LAN / hub transport path, not the server-relay one. */
|
||||
int transport_host;
|
||||
/* 1 = the launch said transport "host" AND relay_via "ice" (host relay
|
||||
* over ICE). transport_host is ALSO 1 then; this is tested first. No UDP
|
||||
* port is bound and nothing is dialled (bind_hostport is a placeholder,
|
||||
* peer_hostport empty): the match runs over the ICE agents the waiting
|
||||
* room connected -- host: a hub over one agent per guest; guest: a 1:1
|
||||
* agent to the host. Neither the single-agent ICE path nor the SFU is
|
||||
* ever chosen for it. The agents come from psx_netplay_ice_stash_from_lobby
|
||||
* (taken when the launch was consumed) or, failing that, straight from the
|
||||
* lobby client. */
|
||||
int transport_ice_hub;
|
||||
int force_turn; /* 1 = ICE relay-only (Force TURN for UDP) */
|
||||
/* 0 = auto (MotK room → ICE, else LAN), 1 = force ICE, 2 = force LAN.
|
||||
* Env PSX_NET_TRANSPORT=lan|ice overrides. */
|
||||
@@ -186,13 +196,24 @@ int psx_netplay_local_view(uint32_t display_w, uint32_t display_h,
|
||||
|
||||
/*
|
||||
* Snapshot for diagnostic dumps (starvation_dump.jsonl meta, etc.).
|
||||
* arch_out: "off" | "p2p" | "host_relay" | "server_relay" (never NULL when
|
||||
* arch_out: "off" | "p2p" | "host_relay" | "ice_host_relay" | "server_relay" (never NULL when
|
||||
* arch_cap > 0). Returns 1 when netplay is/was configured this run.
|
||||
*/
|
||||
int psx_netplay_diag_snapshot(char *arch_out, size_t arch_cap,
|
||||
int *max_players_out, int *player_count_out);
|
||||
|
||||
int psx_netplay_start(const PsxNetplayConfig *cfg);
|
||||
|
||||
/* Host relay over ICE: take the launch's connected agents from the lobby
|
||||
* client and hold them until psx_netplay_start adopts them. Call where the
|
||||
* launch is consumed, BEFORE the lobby launch is cleared (the lobby destroys an
|
||||
* untaken bundle after 60 s). No-op unless the lobby's launch is an ICE hub
|
||||
* launch with a bundle pending. Returns 1 when agents are now held. */
|
||||
int psx_netplay_ice_stash_from_lobby(void);
|
||||
/* Release held agents (a launch that never started). Safe at any time. */
|
||||
void psx_netplay_ice_stash_discard(void);
|
||||
/* 1 when held agents await psx_netplay_start. */
|
||||
int psx_netplay_ice_stash_held(void);
|
||||
void psx_netplay_shutdown(void);
|
||||
/* Soft-return rematch / new lobby opponent: make host sim state match a cold
|
||||
* process peer. Call from session_reboot (rematch) and after BYE teardown.
|
||||
|
||||
@@ -493,6 +493,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_lobby_client.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/netplay_bios_settle.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/netplay_exit_reason.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/netplay_ice_hub.c
|
||||
${PSXRECOMP_ROOT}/recompiler/src/config_loader.cpp
|
||||
${PSXRECOMP_ROOT}/recompiler/src/ps1_exe_parser.cpp
|
||||
# (sljit Tier-2 in-process JIT backend removed 2026-07-15 — was disabled by
|
||||
|
||||
+126
-3
@@ -84,6 +84,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#if defined(PSX_HAS_RECOMP_NET)
|
||||
#include "recomp_net/auth.h"
|
||||
#include "recomp_net/host_relay.h" /* RNetHostRelayStatus for the launcher relay line */
|
||||
#include "recomp_net/host_ice.h" /* RNetHostIceStatus: host relay over ICE waiting-room line */
|
||||
#include "recomp_net/chat_filter.h" /* chat profanity mask, LAN rooms too */
|
||||
#endif
|
||||
#include "spu.h"
|
||||
@@ -10194,6 +10195,11 @@ namespace {
|
||||
* (Network Settings / Lobby Settings, persisted there) and pushes it
|
||||
* through relay_host_set; default on. */
|
||||
int g_lnch_relay_host = 1;
|
||||
/* Host relay over ICE (2026-10-03): with the host relay on, publish
|
||||
* relay_via "ice" so guests reach the host through ICE and nobody forwards
|
||||
* a port. Default on; the lobby client mirrors it (psx_lobby_set_
|
||||
* relay_via_ice_pref) and only publishes it when the build can run ICE. */
|
||||
int g_lnch_relay_via_ice = 1;
|
||||
/* Lobby default on; host “Disable Rollback” clears this → delay_sync. */
|
||||
int g_lnch_rollback = 1;
|
||||
int g_lnch_multitap_analog = 1;
|
||||
@@ -10758,6 +10764,9 @@ namespace {
|
||||
}
|
||||
|
||||
static void ae_np_lan_udp_close(void) {
|
||||
if (std::getenv("PSX_NET_LOBBY_TRACE"))
|
||||
std::fprintf(stderr, "LAN trace: close socket (host=%d guest=%d)\n",
|
||||
g_lnch_hosting_lan, g_lnch_joined_lan);
|
||||
ae_np_lan_sock_close(&g_lnch_lan_udp);
|
||||
for (int i = 0; i < kAeLanMaxSlots; ++i)
|
||||
g_lnch_lan_peer_ok[i] = false;
|
||||
@@ -11004,6 +11013,10 @@ namespace {
|
||||
}
|
||||
|
||||
static void ae_np_lan_udp_sendto(const sockaddr_in& to, const char* msg) {
|
||||
if (std::getenv("PSX_NET_LOBBY_TRACE") && msg)
|
||||
std::fprintf(stderr, "LAN trace: send %.*s to %08x:%u\n",
|
||||
(int)std::strcspn(msg, "\n"), msg,
|
||||
(unsigned)ntohl(to.sin_addr.s_addr), (unsigned)ntohs(to.sin_port));
|
||||
if (g_lnch_lan_udp == kAeLanSockInvalid || !msg) return;
|
||||
const int n = (int)std::strlen(msg);
|
||||
#ifdef _WIN32
|
||||
@@ -12901,11 +12914,87 @@ namespace {
|
||||
ae_np_push_match_caps(nullptr); /* no-op unless hosting online */
|
||||
return 0;
|
||||
}
|
||||
/* ICE-mode waiting-room text, from the agents' own state. Never says
|
||||
* "port": no port is held in this mode. 1 = `out` holds the line. */
|
||||
static const char* ae_np_ice_state_word(int state) {
|
||||
switch (state) {
|
||||
case RNET_ICE_STATE_COMPLETED: return "connected";
|
||||
case RNET_ICE_STATE_FAILED: return "failed";
|
||||
default: return "connecting";
|
||||
}
|
||||
}
|
||||
static void ae_np_ice_peer_text(const RNetHostIcePeerStatus& p, char* out, size_t cap) {
|
||||
if (p.state == RNET_ICE_STATE_COMPLETED && p.path[0])
|
||||
std::snprintf(out, cap, "connected (%s)", p.path);
|
||||
else
|
||||
std::snprintf(out, cap, "%s", ae_np_ice_state_word(p.state));
|
||||
}
|
||||
static int ae_np_ice_status_line(char* out, size_t out_cap) {
|
||||
RNetHostIceStatus st;
|
||||
std::memset(&st, 0, sizeof(st));
|
||||
if (!psx_lobby_host_ice_status(&st)) return 0;
|
||||
if (st.role == 2) {
|
||||
char w[40];
|
||||
if (st.peer_count < 1) {
|
||||
std::snprintf(out, out_cap, "Connecting to the host...");
|
||||
return 1;
|
||||
}
|
||||
ae_np_ice_peer_text(st.peer[0], w, sizeof(w));
|
||||
if (st.peer[0].state == RNET_ICE_STATE_FAILED)
|
||||
std::snprintf(out, out_cap,
|
||||
"Connection to the host failed. Check that your firewall "
|
||||
"allows the game, then rejoin.");
|
||||
else
|
||||
std::snprintf(out, out_cap, "Connection to the host: %s.", w);
|
||||
return 1;
|
||||
}
|
||||
if (st.peer_count < 1) {
|
||||
std::snprintf(out, out_cap, "Waiting for guests to connect through you...");
|
||||
return 1;
|
||||
}
|
||||
size_t n = 0;
|
||||
for (int i = 0; i < st.peer_count && i < RNET_HOST_ICE_MAX_PEERS; ++i) {
|
||||
char w[40];
|
||||
ae_np_ice_peer_text(st.peer[i], w, sizeof(w));
|
||||
const int k = std::snprintf(out + n, out_cap - n, "%sSeat %d: %s",
|
||||
i ? "\n" : "", st.peer[i].slot + 1, w);
|
||||
if (k < 0 || (size_t)k >= out_cap - n) break;
|
||||
n += (size_t)k;
|
||||
}
|
||||
if (st.completed < st.peer_count && n < out_cap)
|
||||
std::snprintf(out + n, out_cap - n,
|
||||
"\nPlay starts once every seat is connected. A seat that stays "
|
||||
"on failed can't reach you; check its firewall.");
|
||||
return 1;
|
||||
}
|
||||
int ae_np_relay_via_ice_get(void*) {
|
||||
if (g_lnch_hosting_lan || g_lnch_joined_lan || !psx_lobby_ice_available()) return 0;
|
||||
if (psx_lobby_in_lobby()) {
|
||||
const PsxLobbyMatchCaps* caps = psx_lobby_match_caps();
|
||||
if (caps && caps->valid) return (caps->relay_host && caps->relay_via_ice) ? 1 : 0;
|
||||
}
|
||||
return g_lnch_relay_via_ice;
|
||||
}
|
||||
int ae_np_relay_via_ice_set(void*, int on) {
|
||||
g_lnch_relay_via_ice = on ? 1 : 0;
|
||||
psx_lobby_set_relay_via_ice_pref(g_lnch_relay_via_ice);
|
||||
if (!g_lnch_hosting_lan && !g_lnch_joined_lan)
|
||||
ae_np_push_match_caps(nullptr); /* no-op unless hosting online */
|
||||
return 0;
|
||||
}
|
||||
int ae_np_relay_status(void*, char* out, size_t out_cap) {
|
||||
if (!out || !out_cap) return 0;
|
||||
out[0] = '\0';
|
||||
#if defined(PSX_HAS_RECOMP_NET)
|
||||
if (g_lnch_hosting_lan || g_lnch_joined_lan || !psx_lobby_in_lobby()) return 0;
|
||||
/* ICE mode replaces the port/reachability guidance entirely: the
|
||||
* legacy status would talk about a forwarded port this room never opens. */
|
||||
if (ae_np_ice_status_line(out, out_cap)) return 1;
|
||||
{
|
||||
const PsxLobbyMatchCaps* c = psx_lobby_match_caps();
|
||||
if (c && c->valid && c->relay_host && c->relay_via_ice)
|
||||
return 0; /* ICE room, agents not up yet: say nothing */
|
||||
}
|
||||
RNetHostRelayStatus st;
|
||||
if (!psx_lobby_host_relay_status(&st)) return 0;
|
||||
if (st.role == 1) {
|
||||
@@ -13144,6 +13233,10 @@ namespace {
|
||||
|
||||
/* Content negotiation: a guest accepts lobby datagrams (and so a
|
||||
* plan) only from the host endpoint it joined. */
|
||||
if (std::getenv("PSX_NET_LOBBY_TRACE"))
|
||||
std::fprintf(stderr, "LAN trace: receive %.*s from %08x:%u\n",
|
||||
(int)std::strcspn(buf, "\n"), buf,
|
||||
(unsigned)ntohl(from.sin_addr.s_addr), (unsigned)ntohs(from.sin_port));
|
||||
if (g_lnch_remote_lan && g_netplay_content_negotiation) {
|
||||
char host[64]; in_addr expected{};
|
||||
if (!ae_np_lan_endpoint_host(g_lnch_lan_endpoint, host, sizeof(host)) ||
|
||||
@@ -13661,7 +13754,8 @@ namespace {
|
||||
if (g_lnch_lan_start_plan_session == g_lnch_remote_lan_state.session_id &&
|
||||
!g_lnch_remote_lan_state.mod_caps_json.empty()) {
|
||||
PsxLobbyMatchCaps caps{};
|
||||
if (psx_lobby_decode_match_caps(g_lnch_remote_lan_state.mod_caps_json.c_str(), &caps))
|
||||
if (psx_lobby_decode_match_caps(g_lnch_remote_lan_state.mod_caps_json.c_str(), &caps) &&
|
||||
caps.session_bios[0])
|
||||
ae_np_set_session_bios_token(caps.session_bios, caps.session_bios_crc);
|
||||
}
|
||||
continue;
|
||||
@@ -14673,9 +14767,18 @@ namespace {
|
||||
: "openbios",
|
||||
session_crc);
|
||||
ae_np_log_settled_bios("LAN");
|
||||
ae_np_lan_sync_local_slot_memcard();
|
||||
g_lnch_lan_guest_memcard_active = ae_np_lan_guest_memcard_effective(state);
|
||||
if (g_netplay_content_negotiation) {
|
||||
PsxLobbyMatchCaps caps = ae_netplay_caps_from_settings(settings);
|
||||
/* The START plan must carry the same settled boot image as
|
||||
* its legacy fields. An empty BIOS in the plan cleared the
|
||||
* guest's START token, leaving it in the lobby while the
|
||||
* host waited forever for tick zero. */
|
||||
std::snprintf(caps.session_bios, sizeof(caps.session_bios),
|
||||
"%s", g_lnch_session_bios);
|
||||
caps.session_bios_crc = session_crc;
|
||||
caps.guest_memcard_active = g_lnch_lan_guest_memcard_active;
|
||||
char caps_wire[8192];
|
||||
if (!g_lnch_mod_plan_error.empty() ||
|
||||
!psx_lobby_encode_match_caps(caps_wire, sizeof(caps_wire), &caps)) return -1;
|
||||
@@ -14695,8 +14798,6 @@ namespace {
|
||||
if (delay > 20) delay = 20;
|
||||
if (pred < 2) pred = 2;
|
||||
if (pred > 16) pred = 16;
|
||||
ae_np_lan_sync_local_slot_memcard();
|
||||
g_lnch_lan_guest_memcard_active = ae_np_lan_guest_memcard_effective(state);
|
||||
std::fprintf(stdout, "psxrecomp: LAN guest memcard (P2 card as slot 2) = %s\n",
|
||||
g_lnch_lan_guest_memcard_active ? "on" : "off");
|
||||
char session_crc_text[16];
|
||||
@@ -14926,6 +15027,12 @@ namespace {
|
||||
return g_lnch_pending_direct_launch.enabled || psx_lobby_launch_pending();
|
||||
}
|
||||
void ae_np_clear_launch_pending(void*) {
|
||||
/* Host relay over ICE: the connected agents must be TAKEN before the
|
||||
* launch is cleared (the lobby destroys an untaken bundle after 60 s
|
||||
* and the game boots well after this). Held by psx_netplay until
|
||||
* psx_netplay_start adopts them. No-op for every other launch. */
|
||||
if (!g_lnch_pending_direct_launch.enabled && psx_lobby_launch_pending())
|
||||
(void)psx_netplay_ice_stash_from_lobby();
|
||||
g_lnch_pending_direct_launch = {};
|
||||
psx_lobby_clear_launch_pending();
|
||||
}
|
||||
@@ -14961,6 +15068,16 @@ namespace {
|
||||
return g_lnch_lan_endpoint.empty() ? nullptr : g_lnch_lan_endpoint.c_str();
|
||||
}
|
||||
|
||||
/* Host relay over ICE: transport "host" + relay_via "ice". Decided from the
|
||||
* lobby's launch (the same join info fill_launch read), and only for a
|
||||
* launch the lobby filled: a LAN / direct launch is never an ICE hub, and
|
||||
* transport_host is ALSO set for an ICE hub, so it gates the lobby read. */
|
||||
static int ae_np_launch_is_ice_hub(const RecompLauncherCNetplayLaunch& l) {
|
||||
if (!l.transport_host || g_lnch_hosting_lan || g_lnch_joined_lan) return 0;
|
||||
const PsxLobbyJoinInfo* ji = psx_lobby_join_info();
|
||||
return (ji && ji->ok && ji->transport_ice_hub) ? 1 : 0;
|
||||
}
|
||||
|
||||
int ae_np_fill_launch(void*, RecompLauncherCNetplayLaunch* out) {
|
||||
if (!out) return 0;
|
||||
if (g_lnch_pending_direct_launch.enabled) {
|
||||
@@ -15317,6 +15434,10 @@ namespace {
|
||||
g_lnch_netplay_callbacks.relay_host_get = ae_np_relay_host_get;
|
||||
g_lnch_netplay_callbacks.relay_host_set = ae_np_relay_host_set;
|
||||
g_lnch_netplay_callbacks.relay_status = ae_np_relay_status;
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_HOST_RELAY_ICE)
|
||||
g_lnch_netplay_callbacks.relay_via_ice_get = ae_np_relay_via_ice_get;
|
||||
g_lnch_netplay_callbacks.relay_via_ice_set = ae_np_relay_via_ice_set;
|
||||
#endif
|
||||
g_lnch_netplay_callbacks.memcard_offer_set = ae_np_memcard_offer_set;
|
||||
g_lnch_netplay_callbacks.guest_memcard_get = ae_np_guest_memcard_get;
|
||||
@@ -17744,6 +17865,7 @@ int main(int argc, char** argv) {
|
||||
net_cfg.force_input_relay = ls.netplay_launch.force_input_relay ? 1 : 0;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_HOST_RELAY)
|
||||
net_cfg.transport_host = ls.netplay_launch.transport_host ? 1 : 0;
|
||||
net_cfg.transport_ice_hub = ae_np_launch_is_ice_hub(ls.netplay_launch);
|
||||
#endif
|
||||
net_cfg.force_turn = ls.netplay_launch.force_turn ? 1 : 0;
|
||||
net_cfg.rollback = ls.netplay_launch.rollback ? 1 : 0;
|
||||
@@ -19834,6 +19956,7 @@ soft_return_lobby:
|
||||
net_cfg.force_input_relay = ls.netplay_launch.force_input_relay ? 1 : 0;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_HOST_RELAY)
|
||||
net_cfg.transport_host = ls.netplay_launch.transport_host ? 1 : 0;
|
||||
net_cfg.transport_ice_hub = ae_np_launch_is_ice_hub(ls.netplay_launch);
|
||||
#endif
|
||||
net_cfg.force_turn = ls.netplay_launch.force_turn ? 1 : 0;
|
||||
net_cfg.rollback = ls.netplay_launch.rollback ? 1 : 0;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/* netplay_ice_hub.c -- see netplay_ice_hub.h. No recomp-net, no sockets. */
|
||||
#include "netplay_ice_hub.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
PsxTransportChoice psx_transport_decide(int cfg_transport, int transport_ice_hub,
|
||||
int transport_host, int force_input_relay,
|
||||
int in_online_room, int has_peer_endpoint,
|
||||
int ice_built)
|
||||
{
|
||||
/* Hub first: it implies transport_host, and it is decided by the launch
|
||||
* alone. Neither force_input_relay nor an explicit transport=ice may
|
||||
* redirect it onto the SFU or the single-agent path. */
|
||||
if (transport_ice_hub) {
|
||||
if (cfg_transport == 2 || !ice_built) return PSX_TRANSPORT_ERROR_NO_PEER;
|
||||
return PSX_TRANSPORT_ICE_HUB;
|
||||
}
|
||||
if (cfg_transport == 2) return PSX_TRANSPORT_LAN_UDP;
|
||||
/* Legacy host relay (advertised UDP port): the LAN path, not the SFU. */
|
||||
if (transport_host) return PSX_TRANSPORT_LAN_UDP;
|
||||
if (force_input_relay || in_online_room)
|
||||
return has_peer_endpoint ? PSX_TRANSPORT_LAN_UDP : PSX_TRANSPORT_ERROR_NO_PEER;
|
||||
if (cfg_transport == 1 && ice_built) return PSX_TRANSPORT_SINGLE_ICE;
|
||||
return PSX_TRANSPORT_LAN_UDP;
|
||||
}
|
||||
|
||||
const char *psx_ice_hub_map_error_text(PsxIceHubMapError e)
|
||||
{
|
||||
switch (e) {
|
||||
case PSX_ICE_HUB_MAP_OK: return "ok";
|
||||
case PSX_ICE_HUB_MAP_BAD_ARGS: return "bad arguments";
|
||||
case PSX_ICE_HUB_MAP_SEAT_UNMAPPED: return "a connected seat holds no session slot";
|
||||
case PSX_ICE_HUB_MAP_SEAT_DUPLICATE: return "two connections for one seat";
|
||||
case PSX_ICE_HUB_MAP_SEAT_MISSING: return "a session slot has no connection";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
static int fail(PsxIceHubMapError *err_out, PsxIceHubMapError e)
|
||||
{
|
||||
if (err_out) *err_out = e;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int psx_ice_hub_map_seats(const int *port_of_slot, int port_map_valid,
|
||||
int slot_count, int host_spectates,
|
||||
const int *lobby_slots, int n,
|
||||
int *session_slots_out, PsxIceHubMapError *err_out)
|
||||
{
|
||||
int i, s;
|
||||
unsigned used = 0;
|
||||
if (err_out) *err_out = PSX_ICE_HUB_MAP_OK;
|
||||
if (!lobby_slots || !session_slots_out || n < 1 || n > PSX_ICE_HUB_MAX_SEATS ||
|
||||
slot_count < 2 || slot_count > 32 || (port_map_valid && !port_of_slot))
|
||||
return fail(err_out, PSX_ICE_HUB_MAP_BAD_ARGS);
|
||||
for (i = 0; i < n; ++i) {
|
||||
int found = -1;
|
||||
if (lobby_slots[i] < 0) return fail(err_out, PSX_ICE_HUB_MAP_SEAT_UNMAPPED);
|
||||
for (s = 0; s < i; ++s)
|
||||
if (lobby_slots[s] == lobby_slots[i])
|
||||
return fail(err_out, PSX_ICE_HUB_MAP_SEAT_DUPLICATE);
|
||||
if (port_map_valid) {
|
||||
for (s = 1; s < slot_count; ++s)
|
||||
if (port_of_slot[s] == lobby_slots[i]) { found = s; break; }
|
||||
} else {
|
||||
found = lobby_slots[i] + (host_spectates ? 1 : 0);
|
||||
if (found < 1 || found >= slot_count) found = -1;
|
||||
}
|
||||
if (found < 0) return fail(err_out, PSX_ICE_HUB_MAP_SEAT_UNMAPPED);
|
||||
if (used & (1u << found)) return fail(err_out, PSX_ICE_HUB_MAP_SEAT_DUPLICATE);
|
||||
used |= 1u << found;
|
||||
session_slots_out[i] = found;
|
||||
}
|
||||
/* Every non-host session slot needs its agent. */
|
||||
for (s = 1; s < slot_count; ++s)
|
||||
if (!(used & (1u << s))) return fail(err_out, PSX_ICE_HUB_MAP_SEAT_MISSING);
|
||||
return 0;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
@@ -52,6 +53,15 @@ void psx_lobby_set_relay_host_pref(int on) { (void)on; }
|
||||
int psx_lobby_relay_host_pref(void) { return 0; }
|
||||
int psx_lobby_host_relay_status(struct RNetHostRelayStatus *out) { (void)out; return 0; }
|
||||
void psx_lobby_host_relay_release_port(void) {}
|
||||
void psx_lobby_set_relay_via_ice_pref(int on) { (void)on; }
|
||||
int psx_lobby_relay_via_ice_pref(void) { return 0; }
|
||||
int psx_lobby_ice_available(void) { return 0; }
|
||||
int psx_lobby_host_ice_status(struct RNetHostIceStatus *out) { (void)out; return 0; }
|
||||
int psx_lobby_ice_take_hub(PsxLobbyIceSeat *out, int max) { (void)out; (void)max; return -1; }
|
||||
struct RNetIceAgent *psx_lobby_ice_take_guest_agent(void) { return NULL; }
|
||||
const char *psx_lobby_ice_launch_error(void) { return ""; }
|
||||
void psx_lobby_ice_discard(void) {}
|
||||
void psx_lobby_ice_destroy_agent(struct RNetIceAgent *agent) { (void)agent; }
|
||||
int psx_lobby_kick(int slot) { (void)slot; return -1; }
|
||||
void psx_lobby_set_allow_spectators(int allow) { (void)allow; }
|
||||
int psx_lobby_allow_spectators_pref(void) { return 0; }
|
||||
@@ -210,6 +220,7 @@ void psx_lobby_clear_launch_pending(void) {}
|
||||
#include "rnet_sha1.h"
|
||||
#include "recomp_net/address.h"
|
||||
#include "recomp_net/host_relay.h"
|
||||
#include "recomp_net/host_ice.h"
|
||||
#include "recomp_net/ice.h"
|
||||
#include "recomp_net/ice_rtt.h"
|
||||
#include "recomp_net/lan_beacon.h"
|
||||
@@ -1136,6 +1147,7 @@ static void match_caps_clear(PsxLobbyMatchCaps *c)
|
||||
* is recomp-net's and only the glue lives here. */
|
||||
static RNetHostRelay *g_host_relay;
|
||||
static int g_relay_host_pref = 1;
|
||||
static int room_relays_via_ice(void);
|
||||
|
||||
void psx_lobby_set_relay_host_pref(int on) { g_relay_host_pref = on ? 1 : 0; }
|
||||
int psx_lobby_relay_host_pref(void) { return g_relay_host_pref; }
|
||||
@@ -1171,7 +1183,8 @@ static void host_relay_step(void)
|
||||
v.is_host = g_lc.is_host ? 1 : 0;
|
||||
v.active = psx_lobby_connected() && g_lc.in_lobby && !g_lc.launch_pending &&
|
||||
!g_lc.join.local_is_spectator &&
|
||||
g_lc.match_caps.valid && g_lc.match_caps.relay_host;
|
||||
g_lc.match_caps.valid && g_lc.match_caps.relay_host &&
|
||||
!room_relays_via_ice(); /* ICE mode: no port, no set_host_endpoint */
|
||||
v.bind_port = (unsigned short)lobby_bind_port();
|
||||
if (v.is_host && !v.bind_port) v.active = 0;
|
||||
v.host_endpoint = g_lc.join.host_endpoint;
|
||||
@@ -1198,6 +1211,259 @@ static void host_relay_leave(void)
|
||||
rnet_host_relay_leave(g_host_relay);
|
||||
}
|
||||
|
||||
/* ---- host relay over ICE (recomp_net/host_ice.h) -------------------------
|
||||
* psxrecomp's copy of recomp-net's rnet_lobby_client.c wiring (decision: the
|
||||
* fork is not a thin copy -- own member table, spectator gallery, BIOS /
|
||||
* memcard offers, signal queue -- so the glue is ported here and the proof
|
||||
* logic is recomp-net's RNetHostIce, exactly as RNetHostRelay is above).
|
||||
*
|
||||
* The pref is the HOST's, default ON. In ICE mode the waiting-room proof is
|
||||
* one agent pair per guest instead of a probe of an advertised UDP port, so
|
||||
* host_relay_step stands down (no UPnP / STUN port, no set_host_endpoint). */
|
||||
static RNetHostIce *g_host_ice;
|
||||
static int g_relay_via_ice_pref = 1;
|
||||
|
||||
void psx_lobby_set_relay_via_ice_pref(int on) { g_relay_via_ice_pref = on ? 1 : 0; }
|
||||
int psx_lobby_relay_via_ice_pref(void) { return g_relay_via_ice_pref; }
|
||||
int psx_lobby_ice_available(void) { return rnet_host_ice_available(); }
|
||||
|
||||
/* The room's published caps ask for ICE as the host relay's path. */
|
||||
static int room_relays_via_ice(void)
|
||||
{
|
||||
return g_lc.match_caps.valid && g_lc.match_caps.relay_host &&
|
||||
g_lc.match_caps.relay_via_ice;
|
||||
}
|
||||
|
||||
/* Agents handed over at launch until the engine takes them (or the room /
|
||||
* launch is abandoned). Kept apart from g_lc so a refused launch can still say
|
||||
* why after the lobby state is reset. */
|
||||
typedef struct {
|
||||
int valid; /* a bundle awaits take */
|
||||
int is_host;
|
||||
int n;
|
||||
PsxLobbyIceSeat seat[RNET_HOST_ICE_MAX_PEERS]; /* host */
|
||||
RNetIceAgent *guest; /* guest: connected to the host */
|
||||
uint64_t born_ms;
|
||||
char error[200];
|
||||
} PsxIceLaunch;
|
||||
static PsxIceLaunch g_il;
|
||||
|
||||
static uint64_t ice_now_ms(void)
|
||||
{
|
||||
return (uint64_t)psx_host_mono_ms();
|
||||
}
|
||||
|
||||
static void ice_launch_discard(void)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < g_il.n; ++i)
|
||||
if (g_il.seat[i].agent) rnet_host_ice_destroy_agent(g_il.seat[i].agent);
|
||||
if (g_il.guest) rnet_host_ice_destroy_agent(g_il.guest);
|
||||
memset(&g_il, 0, offsetof(PsxIceLaunch, error));
|
||||
}
|
||||
|
||||
static void host_ice_leave(void)
|
||||
{
|
||||
ice_launch_discard();
|
||||
g_il.error[0] = '\0';
|
||||
rnet_host_ice_destroy(&g_host_ice);
|
||||
}
|
||||
|
||||
/* Lobby `signal` addressed to one player (the legacy sender broadcasts). */
|
||||
static int send_signal_to(const char *to_player_id, int type, int flag, const char *text)
|
||||
{
|
||||
char esc[4096];
|
||||
char to_esc[JSON_ESC_CAP(PSX_LOBBY_ID_LEN)];
|
||||
char lid_esc[JSON_ESC_CAP(PSX_LOBBY_ID_LEN)];
|
||||
char msg[4608];
|
||||
const char *lid;
|
||||
if (!psx_lobby_connected() || !g_lc.in_lobby) return -1;
|
||||
lid = g_lc.join.lobby_id[0] ? g_lc.join.lobby_id : "";
|
||||
json_escape(text ? text : "", esc, sizeof(esc));
|
||||
json_escape(to_player_id ? to_player_id : "", to_esc, sizeof(to_esc));
|
||||
json_escape(lid, lid_esc, sizeof(lid_esc));
|
||||
snprintf(msg, sizeof(msg),
|
||||
"{\"op\":\"signal\",\"lobby_id\":\"%s\",\"to_player_id\":\"%s\","
|
||||
"\"type\":%d,\"flag\":%d,\"text\":\"%s\"}",
|
||||
lid_esc, to_esc, type, flag, esc);
|
||||
if (g_lc.handshake_done && g_lc.fd >= 0)
|
||||
return rnet_ws_write_text(g_lc.fd, msg, 1) < 0 ? -1 : 0;
|
||||
queue_send(msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int host_ice_send_signal(const char *to, int type, int flag, const char *text,
|
||||
void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return send_signal_to(to, type, flag, text);
|
||||
}
|
||||
|
||||
/* Every pump, beside host_relay_step: the room decides which agents exist. */
|
||||
static void host_ice_step(void)
|
||||
{
|
||||
RNetHostIceView v;
|
||||
RNetHostIcePeer peers[RNET_HOST_ICE_MAX_PEERS];
|
||||
int n = 0, i;
|
||||
if (!g_host_ice) {
|
||||
g_host_ice = rnet_host_ice_create();
|
||||
if (!g_host_ice) return;
|
||||
}
|
||||
/* A bundle nobody took (the engine declined the launch) must not hold
|
||||
* sockets for the rest of the session. */
|
||||
if (g_il.valid && ice_now_ms() - g_il.born_ms > 60000u)
|
||||
ice_launch_discard();
|
||||
memset(&v, 0, sizeof(v));
|
||||
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() &&
|
||||
v.local_slot >= 0;
|
||||
if (v.active) {
|
||||
if (v.is_host) {
|
||||
for (i = 0; i < g_lc.member_count && n < RNET_HOST_ICE_MAX_PEERS; ++i) {
|
||||
const PsxLobbyMember *m = &g_lc.members[i];
|
||||
if (m->is_spectator || !m->player_id[0] ||
|
||||
strcmp(m->player_id, g_lc.player_id) == 0)
|
||||
continue;
|
||||
peers[n].player_id = m->player_id;
|
||||
peers[n].slot = m->slot;
|
||||
n++;
|
||||
}
|
||||
} else {
|
||||
const int hs = member_slot_for_player(g_lc.host_player_id);
|
||||
if (g_lc.host_player_id[0] && hs >= 0) {
|
||||
peers[0].player_id = g_lc.host_player_id;
|
||||
peers[0].slot = hs;
|
||||
n = 1;
|
||||
} else {
|
||||
v.active = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
v.peers = peers;
|
||||
v.peer_count = n;
|
||||
/* STUN only: hub agents never use TURN, so the Coturn mint is not used. */
|
||||
if (g_lc.turn.valid && g_lc.turn.stun_host[0]) {
|
||||
v.stun_host = g_lc.turn.stun_host;
|
||||
v.stun_port = (unsigned short)(g_lc.turn.stun_port > 0 ? g_lc.turn.stun_port : 3478);
|
||||
}
|
||||
v.send_signal = host_ice_send_signal;
|
||||
v.send_json = host_relay_send;
|
||||
rnet_host_ice_update(g_host_ice, &v);
|
||||
}
|
||||
|
||||
int psx_lobby_host_ice_status(struct RNetHostIceStatus *out)
|
||||
{
|
||||
if (!out) return 0;
|
||||
rnet_host_ice_status(g_host_ice, out);
|
||||
return out->role != 0;
|
||||
}
|
||||
|
||||
/* Build the handover from the agents the waiting room already connected. A
|
||||
* seat that is not COMPLETED is a refused launch with a reason, never a
|
||||
* smaller room. Spectators are not on the ICE path: any gallery seat refuses
|
||||
* the launch (host), and a spectating guest has no agent (it refuses itself). */
|
||||
static void ice_launch_capture(void)
|
||||
{
|
||||
int i;
|
||||
ice_launch_discard();
|
||||
g_il.error[0] = '\0';
|
||||
g_il.is_host = g_lc.is_host ? 1 : 0;
|
||||
g_il.born_ms = ice_now_ms();
|
||||
if (!g_host_ice) {
|
||||
snprintf(g_il.error, sizeof(g_il.error),
|
||||
"no ICE agents exist (this build cannot run ICE, or the room was "
|
||||
"not seated)");
|
||||
return;
|
||||
}
|
||||
if (g_il.is_host) {
|
||||
for (i = 0; i < g_lc.member_count; ++i) {
|
||||
const PsxLobbyMember *m = &g_lc.members[i];
|
||||
RNetIceAgent *a;
|
||||
if (!m->player_id[0] || strcmp(m->player_id, g_lc.player_id) == 0)
|
||||
continue;
|
||||
if (m->is_spectator) {
|
||||
snprintf(g_il.error, sizeof(g_il.error),
|
||||
"\"%s\" is a spectator; spectators are not on the ICE "
|
||||
"host-relay path",
|
||||
m->display_name[0] ? m->display_name : m->player_id);
|
||||
break;
|
||||
}
|
||||
if (g_il.n >= RNET_HOST_ICE_MAX_PEERS) {
|
||||
snprintf(g_il.error, sizeof(g_il.error), "too many guests for the ICE hub");
|
||||
break;
|
||||
}
|
||||
a = rnet_host_ice_take_completed(g_host_ice, m->player_id);
|
||||
if (!a) {
|
||||
snprintf(g_il.error, sizeof(g_il.error),
|
||||
"\"%s\" (seat %d) is not connected over ICE",
|
||||
m->display_name[0] ? m->display_name : m->player_id, m->slot);
|
||||
break;
|
||||
}
|
||||
g_il.seat[g_il.n].lobby_slot = m->slot;
|
||||
snprintf(g_il.seat[g_il.n].player_id, sizeof(g_il.seat[g_il.n].player_id),
|
||||
"%s", m->player_id);
|
||||
g_il.seat[g_il.n].agent = a;
|
||||
g_il.n++;
|
||||
}
|
||||
if (!g_il.error[0] && g_il.n == 0)
|
||||
snprintf(g_il.error, sizeof(g_il.error), "no guest is seated");
|
||||
} else {
|
||||
g_il.guest = rnet_host_ice_take_completed(g_host_ice, g_lc.host_player_id);
|
||||
if (!g_il.guest)
|
||||
snprintf(g_il.error, sizeof(g_il.error),
|
||||
"this client is not connected to the host over ICE");
|
||||
}
|
||||
if (g_il.error[0]) {
|
||||
char why[sizeof(g_il.error)];
|
||||
snprintf(why, sizeof(why), "%s", g_il.error);
|
||||
ice_launch_discard();
|
||||
snprintf(g_il.error, sizeof(g_il.error), "%s", why);
|
||||
return;
|
||||
}
|
||||
g_il.valid = 1;
|
||||
}
|
||||
|
||||
int psx_lobby_ice_take_hub(PsxLobbyIceSeat *out, int max)
|
||||
{
|
||||
int n;
|
||||
if (!out || max < 1) return -1;
|
||||
if (!g_il.valid || !g_il.is_host) {
|
||||
if (!g_il.error[0])
|
||||
snprintf(g_il.error, sizeof(g_il.error), "no ICE hub handover is pending");
|
||||
return -1;
|
||||
}
|
||||
if (max < g_il.n) {
|
||||
snprintf(g_il.error, sizeof(g_il.error), "output array too small for %d seats", g_il.n);
|
||||
return -1;
|
||||
}
|
||||
n = g_il.n;
|
||||
memcpy(out, g_il.seat, sizeof(out[0]) * (size_t)n);
|
||||
memset(&g_il, 0, offsetof(PsxIceLaunch, error)); /* ownership is the caller's */
|
||||
return n;
|
||||
}
|
||||
|
||||
RNetIceAgent *psx_lobby_ice_take_guest_agent(void)
|
||||
{
|
||||
RNetIceAgent *a;
|
||||
if (!g_il.valid || g_il.is_host || !g_il.guest) {
|
||||
if (!g_il.error[0])
|
||||
snprintf(g_il.error, sizeof(g_il.error), "no ICE guest handover is pending");
|
||||
return NULL;
|
||||
}
|
||||
a = g_il.guest;
|
||||
memset(&g_il, 0, offsetof(PsxIceLaunch, error));
|
||||
return a;
|
||||
}
|
||||
|
||||
const char *psx_lobby_ice_launch_error(void) { return g_il.error; }
|
||||
void psx_lobby_ice_discard(void) { ice_launch_discard(); }
|
||||
void psx_lobby_ice_destroy_agent(RNetIceAgent *agent)
|
||||
{
|
||||
if (agent) rnet_host_ice_destroy_agent(agent);
|
||||
}
|
||||
|
||||
static int json_extract_object(const char *json, const char *key, char *out, size_t out_cap);
|
||||
static void parse_match_caps_object(const char *obj, PsxLobbyMatchCaps *out);
|
||||
static void ingest_match_caps_from_json(const char *json);
|
||||
@@ -1734,6 +2000,12 @@ static void parse_match_caps_object(const char *obj, PsxLobbyMatchCaps *out)
|
||||
json_get_str(obj, "relay", relay, sizeof(relay));
|
||||
out->relay_host = strcmp(relay, "host") == 0 ? 1 : 0;
|
||||
}
|
||||
{
|
||||
char via[12];
|
||||
via[0] = '\0';
|
||||
json_get_str(obj, "relay_via", via, sizeof(via));
|
||||
out->relay_via_ice = strcmp(via, "ice") == 0 ? 1 : 0;
|
||||
}
|
||||
out->force_turn = json_get_bool(obj, "force_turn", 0);
|
||||
/* Absent field → delay-sync (older hosts). New hosts always publish explicit. */
|
||||
out->rollback = json_get_bool(obj, "rollback", 0);
|
||||
@@ -1820,7 +2092,12 @@ static int append_match_caps_json(char *dst, size_t dst_cap, const PsxLobbyMatch
|
||||
caps->auto_skip_fmv ? "true" : "false",
|
||||
caps->input_delay,
|
||||
caps->input_prediction,
|
||||
caps->relay_host ? "\"relay\":\"host\"," : "",
|
||||
/* only when asked: absent reads as "the SFU, as always" */
|
||||
caps->relay_host
|
||||
? ((g_relay_via_ice_pref && rnet_host_ice_available())
|
||||
? "\"relay\":\"host\",\"relay_via\":\"ice\","
|
||||
: "\"relay\":\"host\",")
|
||||
: "",
|
||||
caps->force_input_relay ? "true" : "false",
|
||||
caps->force_turn ? "true" : "false",
|
||||
caps->rollback ? "true" : "false",
|
||||
@@ -1929,7 +2206,11 @@ static void fill_peer_bind_from_join(void)
|
||||
const int host_hub = (g_lc.is_host && seats >= 3 && !force_relay) ? 1 : 0;
|
||||
memset(j->bind_hostport, 0, sizeof(j->bind_hostport));
|
||||
memset(j->peer_hostport, 0, sizeof(j->peer_hostport));
|
||||
if (force_relay) {
|
||||
if (j->transport_ice_hub) {
|
||||
/* Nothing to bind or dial: the agents are already connected. Kept
|
||||
* non-empty so a consumer's "usable bind" check passes. */
|
||||
strncpy(j->bind_hostport, "0.0.0.0:0", sizeof(j->bind_hostport) - 1);
|
||||
} else if (force_relay) {
|
||||
/* Everyone dials the lobby-server UDP relay — ephemeral local bind
|
||||
* (same as LAN guests) so same-PC multi-instance doesn't collide. */
|
||||
strncpy(j->bind_hostport, "0.0.0.0:0", sizeof(j->bind_hostport) - 1);
|
||||
@@ -3171,6 +3452,8 @@ static void handle_server_json(const char *json)
|
||||
}
|
||||
if (strcmp(op, "created") == 0) {
|
||||
psx_lobby_chat_clear();
|
||||
ice_launch_discard();
|
||||
g_lc.join.transport_ice_hub = 0;
|
||||
g_lc.in_lobby = 1;
|
||||
g_lc.is_host = 1;
|
||||
g_lc.join.ok = 1;
|
||||
@@ -3222,6 +3505,8 @@ static void handle_server_json(const char *json)
|
||||
(g_am.state == PSX_LOBBY_AUTOMATCH_ACCEPTED ||
|
||||
g_am.state == PSX_LOBBY_AUTOMATCH_FOUND);
|
||||
automatch_reset_queue_state();
|
||||
ice_launch_discard();
|
||||
g_lc.join.transport_ice_hub = 0;
|
||||
g_lc.in_lobby = 1;
|
||||
g_lc.is_host = 0;
|
||||
g_lc.join.ok = 1;
|
||||
@@ -3276,10 +3561,23 @@ static void handle_server_json(const char *json)
|
||||
g_lc.join.transport_host = strcmp(transport_kind, "host") == 0 ? 1 : 0;
|
||||
g_lc.join.transport_ice = strcmp(transport_kind, "ice") == 0 ? 1 : 0;
|
||||
rnet_host_relay_release_port(g_host_relay);
|
||||
ice_launch_discard(); /* a previous launch's agents are never reused */
|
||||
g_il.error[0] = '\0';
|
||||
g_lc.join.player_count = json_get_int(json, "player_count", g_lc.join.player_count);
|
||||
g_lc.join.max_slots = json_get_int(json, "max_slots", g_lc.join.max_slots);
|
||||
g_lc.join.session_id = (uint32_t)json_get_int(json, "session_id", (int)g_lc.join.session_id);
|
||||
ingest_match_caps_from_json(json);
|
||||
/* relay_via is decided by THIS launch alone (top level, or its
|
||||
* match_caps), never by caps carried over from an earlier message. */
|
||||
{
|
||||
char via[12];
|
||||
via[0] = '\0';
|
||||
json_get_str(json, "relay_via", via, sizeof(via));
|
||||
g_lc.match_caps.relay_via_ice = strcmp(via, "ice") == 0 ? 1 : 0;
|
||||
if (g_lc.match_caps.relay_via_ice) g_lc.match_caps.valid = 1;
|
||||
}
|
||||
g_lc.join.transport_ice_hub =
|
||||
(g_lc.join.transport_host && g_lc.match_caps.relay_via_ice) ? 1 : 0;
|
||||
/* Prefer explicit relay_endpoint when the server opened input relay.
|
||||
* Apply after caps ingest: omitted force_input_relay must not leave
|
||||
* hosts on the hub path while guests dial the relay. */
|
||||
@@ -3311,6 +3609,32 @@ static void handle_server_json(const char *json)
|
||||
}
|
||||
fill_peer_bind_from_join();
|
||||
parse_slots_array(json);
|
||||
if (g_lc.join.transport_ice_hub) {
|
||||
/* Host relay over ICE: no endpoints. Capture the connected agents
|
||||
* NOW, before the next pump's view (launch_pending) tears them
|
||||
* down; the engine takes them. A seat that is not connected is a
|
||||
* refused launch with a reason, never a smaller room. */
|
||||
ice_launch_capture();
|
||||
if (g_il.error[0]) {
|
||||
strncpy(g_lc.join.last_error, "ice_not_connected",
|
||||
sizeof(g_lc.join.last_error) - 1);
|
||||
g_lc.join.last_error[sizeof(g_lc.join.last_error) - 1] = '\0';
|
||||
g_lc.launch_pending = 0;
|
||||
fprintf(stderr, "psx_lobby: launch refused - %s\n", g_il.error);
|
||||
fflush(stderr);
|
||||
return;
|
||||
}
|
||||
g_lc.join.last_error[0] = '\0';
|
||||
g_lc.launch_pending = 1;
|
||||
g_lc.ice_rtt_suspended = 1;
|
||||
lobby_ice_rtt_close();
|
||||
lobby_rtt_close();
|
||||
psx_lobby_clear_signals();
|
||||
g_lc.ice_signal_accept = 1;
|
||||
rnet_lan_beacon_close(&g_lan_beacon_pub);
|
||||
lobby_host_advertise_reset();
|
||||
return;
|
||||
}
|
||||
/* §108: online start always SFU — every peer dials relay_endpoint.
|
||||
* Legacy ice_p2p from an old server is refused (need SFU). */
|
||||
{
|
||||
@@ -3437,6 +3761,26 @@ static void handle_server_json(const char *json)
|
||||
* rnet_rtt_probe (peer path). REPORT still accepted from peers. */
|
||||
if (type == PSX_LOBBY_SIG_RTT_PING || type == PSX_LOBBY_SIG_RTT_PONG)
|
||||
return;
|
||||
if (rnet_host_ice_sig_is_ours(type)) {
|
||||
/* Host-relay-over-ICE waiting-room agents. Dispatched by the
|
||||
* sender's player id, which the SERVER stamps (a client cannot
|
||||
* choose it); only a seated player is a peer, and the seat is
|
||||
* looked up here, never read from the payload. Never queued for
|
||||
* the legacy session-ICE drain. */
|
||||
const int from_slot = member_slot_for_player(from);
|
||||
int from_spec = 0, mi;
|
||||
for (mi = 0; mi < g_lc.member_count; ++mi)
|
||||
if (strcmp(g_lc.members[mi].player_id, from) == 0)
|
||||
from_spec = g_lc.members[mi].is_spectator ? 1 : 0;
|
||||
if (!g_host_ice || !from[0] || from_slot < 0 || from_spec) {
|
||||
fprintf(stderr, "psx_lobby: host-ice signal from \"%s\" refused "
|
||||
"(not a seated player)\n", from);
|
||||
return;
|
||||
}
|
||||
(void)rnet_host_ice_push_signal(g_host_ice, from, from_slot, type, flag,
|
||||
text_buf);
|
||||
return;
|
||||
}
|
||||
if (type == PSX_LOBBY_SIG_RTT_REPORT) {
|
||||
int slot = rtt_index_for_slot(member_slot_for_player(from));
|
||||
int ms = (int)strtol(text_buf, NULL, 10);
|
||||
@@ -3493,6 +3837,7 @@ static void handle_server_json(const char *json)
|
||||
if (strcmp(op, "lobby_closed") == 0 || strcmp(op, "left") == 0 ||
|
||||
strcmp(op, "kicked") == 0) {
|
||||
psx_lobby_chat_clear();
|
||||
ice_launch_discard();
|
||||
/* Leaving a room must never leave a queue ticket looking live, and
|
||||
* no seat means no automatch room either. */
|
||||
automatch_reset_queue_state();
|
||||
@@ -3913,6 +4258,7 @@ int psx_lobby_connect(const char *ws_url)
|
||||
void psx_lobby_disconnect(void)
|
||||
{
|
||||
host_relay_leave();
|
||||
host_ice_leave(); /* agents + any launch bundle */
|
||||
/* Never block the UI on DNS/connect — cancel and let pump reap. */
|
||||
lobby_cancel_connect_async();
|
||||
automatch_on_connection_reset();
|
||||
@@ -4129,8 +4475,11 @@ static void lobby_ice_rtt_tick(void)
|
||||
RNetIceState st;
|
||||
char path[16];
|
||||
|
||||
/* Host relay over ICE: the host-ice agents ARE the waiting-room ICE
|
||||
* session and own the path_report; a second per-pair probe would race it
|
||||
* (and its "relay"/"fail" reports would undo the proof). */
|
||||
if (!g_lc.in_lobby || g_lc.launch_pending || g_lc.ice_rtt_suspended ||
|
||||
g_lc.member_count < 2) {
|
||||
g_lc.member_count < 2 || room_relays_via_ice()) {
|
||||
lobby_ice_rtt_close();
|
||||
return;
|
||||
}
|
||||
@@ -4268,6 +4617,7 @@ void psx_lobby_pump(void)
|
||||
/* Host relay: hold / probe / report to match the room. Before the
|
||||
* connected() check so a dropped WS releases the port the same pump. */
|
||||
host_relay_step();
|
||||
host_ice_step();
|
||||
#if defined(_WIN32)
|
||||
int n;
|
||||
#else
|
||||
@@ -4546,6 +4896,8 @@ int psx_lobby_join(const char *lobby_id, const char *password, const char *guest
|
||||
int psx_lobby_leave(void)
|
||||
{
|
||||
host_relay_leave(); /* unmap the router, close the port, forget reports */
|
||||
ice_launch_discard();
|
||||
g_il.error[0] = '\0';
|
||||
queue_send("{\"op\":\"leave\"}");
|
||||
flush_pending();
|
||||
g_lc.ice_rtt_suspended = 0;
|
||||
|
||||
+239
-46
@@ -7,6 +7,7 @@
|
||||
#include "netplay_load_probe.h"
|
||||
#include "netplay_sim_pad_cache.h"
|
||||
#include "netplay_local_view.h"
|
||||
#include "netplay_ice_hub.h"
|
||||
|
||||
#include "host_time.h"
|
||||
#include "memcard.h"
|
||||
@@ -53,10 +54,10 @@
|
||||
#include "psx_cycles.h"
|
||||
#include "psx_scheduler.h"
|
||||
#include "spu.h"
|
||||
#if defined(PSX_HAS_LOBBY_CLIENT)
|
||||
/* Unconditional: the lobby client TU defines no-op stubs when it is off, and
|
||||
* the host-relay-over-ICE handover below calls it either way. */
|
||||
#include "psx_lobby_client.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef PSX_MAX_PLAYERS
|
||||
#define PSX_MAX_PLAYERS 2
|
||||
@@ -142,6 +143,7 @@ void psx_netplay_config_defaults(PsxNetplayConfig *cfg)
|
||||
cfg->force_input_relay = 0;
|
||||
cfg->force_turn = 0;
|
||||
cfg->transport = 0;
|
||||
cfg->transport_ice_hub = 0;
|
||||
cfg->guest_memcard = 0;
|
||||
cfg->host_spectates = 0;
|
||||
cfg->session_id = 1;
|
||||
@@ -488,6 +490,9 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
|
||||
(void)cfg;
|
||||
return -1;
|
||||
}
|
||||
int psx_netplay_ice_stash_from_lobby(void) { return 0; }
|
||||
void psx_netplay_ice_stash_discard(void) {}
|
||||
int psx_netplay_ice_stash_held(void) { return 0; }
|
||||
void psx_netplay_shutdown(void) {}
|
||||
void psx_netplay_cold_reset(void) {}
|
||||
void psx_netplay_stage_local(const PsxNetPad *pad) { (void)pad; }
|
||||
@@ -637,6 +642,7 @@ typedef struct {
|
||||
PsxNetplayLoadProbe load_probe; /* guest's verified local LOAD probe/apply */
|
||||
/* Transport / ICE / diag (MotK online path). */
|
||||
int use_ice;
|
||||
int ice_hub; /* match runs over adopted waiting-room agents */
|
||||
int ice_has_turn;
|
||||
int force_input_relay;
|
||||
int is_host;
|
||||
@@ -3398,6 +3404,7 @@ int psx_netplay_is_running(void)
|
||||
const char *psx_netplay_transport_name(void)
|
||||
{
|
||||
if (!psx_netplay_active()) return "none";
|
||||
if (g_np.ice_hub) return "ice_hub";
|
||||
return g_np.use_ice ? "ice" : "lan";
|
||||
}
|
||||
|
||||
@@ -3712,7 +3719,9 @@ static void np_diag_capture(const PsxNetplayConfig *cfg, int slots)
|
||||
const char *arch = "p2p";
|
||||
int players;
|
||||
if (!cfg) return;
|
||||
if (cfg->force_input_relay)
|
||||
if (cfg->transport_ice_hub)
|
||||
arch = "ice_host_relay";
|
||||
else if (cfg->force_input_relay)
|
||||
arch = "server_relay";
|
||||
else if (slots >= 3)
|
||||
arch = "host_relay";
|
||||
@@ -3774,6 +3783,10 @@ static void drain_lobby_signals(void)
|
||||
if (!g_np.session) return;
|
||||
while (psx_lobby_poll_signal(&type, &flag, text, sizeof(text))) {
|
||||
RNetSignal sig;
|
||||
/* Host relay over ICE: the adopted agents are frozen at COMPLETED.
|
||||
* Nothing may be pushed into them; a stray legacy ICE signal is
|
||||
* consumed and dropped. */
|
||||
if (g_np.ice_hub) continue;
|
||||
memset(&sig, 0, sizeof(sig));
|
||||
/* Peers emit LOCAL_*; push_signal expects REMOTE_* for SDP/candidates. */
|
||||
if (type == (int)RNET_SIGNAL_LOCAL_SDP)
|
||||
@@ -3790,35 +3803,29 @@ static void drain_lobby_signals(void)
|
||||
static void drain_lobby_signals(void) {}
|
||||
#endif
|
||||
|
||||
static int resolve_use_ice(const PsxNetplayConfig *cfg)
|
||||
/* Transport for this launch: 0 = LAN UDP (bind + hub / dial), 1 = one
|
||||
* negotiated ICE agent, 2 = the waiting room's connected agents (host relay
|
||||
* over ICE), -1 = refuse. The decision itself is netplay_ice_hub.c (tested
|
||||
* without a ROM); this adds the room lookup and the log lines. */
|
||||
static int resolve_transport(const PsxNetplayConfig *cfg)
|
||||
{
|
||||
int in_motk_room = 0;
|
||||
int ice_built = 0;
|
||||
PsxTransportChoice c;
|
||||
|
||||
if (cfg->transport == 2) return 0; /* force LAN */
|
||||
#if defined(PSX_HAS_LOBBY_CLIENT)
|
||||
in_motk_room = psx_lobby_connected() && psx_lobby_in_lobby();
|
||||
#endif
|
||||
|
||||
/* §108: MotK/BPE online lobbies always use lobby UDP SFU — LAN dial to
|
||||
* relay_endpoint (or equal host/guest advertise). Match ICE / ice_p2p
|
||||
* selection was removed; waiting-room ICE RTT may still run for delay
|
||||
* hints only. Direct IP / LAN file lobby (no MotK seat) stays LAN UDP. */
|
||||
/* Host relay (2026-10-01): an online room whose host carries the match
|
||||
* launches transport "host" -- no server relay. Host: bind the advertised
|
||||
* port, peer empty (accept-first / hub); guest: dial host_endpoint. That
|
||||
* is the LAN path below, so it is excluded from the SFU rule here. */
|
||||
if (cfg->transport_host) {
|
||||
fprintf(stderr,
|
||||
"psx_netplay: host relay — LAN transport (%s)\n",
|
||||
cfg->peer_hostport && cfg->peer_hostport[0] ? "dial host" : "host binds");
|
||||
fflush(stderr);
|
||||
return 0;
|
||||
}
|
||||
#if defined(RNET_ENABLE_ICE)
|
||||
ice_built = 1;
|
||||
#endif
|
||||
#if defined(PSX_HAS_LOBBY_CLIENT) && defined(RNET_ENABLE_ICE)
|
||||
/* The server started this room on ICE (2 players the host relay cannot
|
||||
* carry): peer-to-peer over STUN/TURN, signalled over the lobby socket.
|
||||
* Same-NAT peers advertise equal endpoints; that is not a relay. */
|
||||
if (in_motk_room && !cfg->force_input_relay) {
|
||||
* Same-NAT peers advertise equal endpoints; that is not a relay. A host
|
||||
* relay over ICE launch (hub) or a legacy host relay is decided below. */
|
||||
if (in_motk_room && !cfg->force_input_relay && !cfg->transport_ice_hub &&
|
||||
!cfg->transport_host) {
|
||||
const PsxLobbyJoinInfo *ji = psx_lobby_join_info();
|
||||
if (ji && ji->transport_ice) {
|
||||
fprintf(stderr, "psx_netplay: lobby launch transport=ice — ICE peer-to-peer\n");
|
||||
@@ -3827,29 +3834,171 @@ static int resolve_use_ice(const PsxNetplayConfig *cfg)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (cfg->force_input_relay || in_motk_room) {
|
||||
if (!cfg->peer_hostport || !cfg->peer_hostport[0]) {
|
||||
fprintf(stderr,
|
||||
"psx_netplay: MotK online / force_input_relay needs a "
|
||||
"peer or SFU endpoint (empty peer)\n");
|
||||
fflush(stderr);
|
||||
return -1;
|
||||
}
|
||||
fprintf(stderr,
|
||||
"psx_netplay: server input relay — LAN transport to %s\n",
|
||||
cfg->peer_hostport);
|
||||
/* §108: MotK/BPE online lobbies use the lobby UDP SFU unless the launch
|
||||
* says otherwise. Host relay (2026-10-01): transport "host" -- the host
|
||||
* carries the match on its advertised UDP port (the LAN path). Host relay
|
||||
* over ICE (2026-10-03): transport "host" + relay_via "ice" -- no port, the
|
||||
* match rides the waiting room's connected agents (psx_transport_decide
|
||||
* keeps it off the SFU and off the single-agent ICE path). Direct IP / LAN
|
||||
* file lobby (no MotK seat) stays LAN UDP. */
|
||||
c = psx_transport_decide(cfg->transport, cfg->transport_ice_hub,
|
||||
cfg->transport_host, cfg->force_input_relay,
|
||||
in_motk_room,
|
||||
cfg->peer_hostport && cfg->peer_hostport[0],
|
||||
ice_built);
|
||||
switch (c) {
|
||||
case PSX_TRANSPORT_ICE_HUB:
|
||||
fprintf(stderr, "psx_netplay: host relay over ICE - adopting the "
|
||||
"waiting room's connected agents (no UDP bind, no dial)\n");
|
||||
fflush(stderr);
|
||||
return 2;
|
||||
case PSX_TRANSPORT_SINGLE_ICE:
|
||||
return 1;
|
||||
case PSX_TRANSPORT_ERROR_NO_PEER:
|
||||
if (cfg->transport_ice_hub)
|
||||
fprintf(stderr, "psx_netplay: host relay over ICE refused (%s)\n",
|
||||
cfg->transport == 2 ? "PSX_NET_TRANSPORT=lan forces UDP"
|
||||
: "this build has no ICE");
|
||||
else
|
||||
fprintf(stderr, "psx_netplay: MotK online / force_input_relay needs a "
|
||||
"peer or SFU endpoint (empty peer)\n");
|
||||
fflush(stderr);
|
||||
return -1;
|
||||
case PSX_TRANSPORT_LAN_UDP:
|
||||
default:
|
||||
if (cfg->transport_host)
|
||||
fprintf(stderr, "psx_netplay: host relay - LAN transport (%s)\n",
|
||||
cfg->peer_hostport && cfg->peer_hostport[0] ? "dial host"
|
||||
: "host binds");
|
||||
else if ((cfg->force_input_relay || in_motk_room) && cfg->transport != 2)
|
||||
fprintf(stderr, "psx_netplay: server input relay - LAN transport to %s\n",
|
||||
cfg->peer_hostport);
|
||||
fflush(stderr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(RNET_ENABLE_ICE)
|
||||
/* Explicit ICE only outside MotK online (rare). */
|
||||
if (cfg->transport == 1)
|
||||
return 1;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Agents taken from the lobby when the launch was consumed, until start()
|
||||
* adopts them. Outside g_np: shutdown wipes g_np, and this must survive the
|
||||
* gap between the launcher closing and the session being created. */
|
||||
typedef struct {
|
||||
int n; /* host: seats held */
|
||||
PsxLobbyIceSeat seat[PSX_ICE_HUB_MAX_SEATS];
|
||||
struct RNetIceAgent *guest; /* guest: agent held */
|
||||
int is_host;
|
||||
} NpIceStash;
|
||||
static NpIceStash g_np_ice_stash;
|
||||
|
||||
void psx_netplay_ice_stash_discard(void)
|
||||
{
|
||||
#if defined(PSX_HAS_LOBBY_CLIENT)
|
||||
int i;
|
||||
for (i = 0; i < g_np_ice_stash.n; ++i)
|
||||
psx_lobby_ice_destroy_agent(g_np_ice_stash.seat[i].agent);
|
||||
if (g_np_ice_stash.guest)
|
||||
psx_lobby_ice_destroy_agent(g_np_ice_stash.guest);
|
||||
#endif
|
||||
memset(&g_np_ice_stash, 0, sizeof(g_np_ice_stash));
|
||||
}
|
||||
|
||||
int psx_netplay_ice_stash_held(void)
|
||||
{
|
||||
return g_np_ice_stash.n > 0 || g_np_ice_stash.guest != NULL;
|
||||
}
|
||||
|
||||
int psx_netplay_ice_stash_from_lobby(void)
|
||||
{
|
||||
#if defined(PSX_HAS_LOBBY_CLIENT)
|
||||
const PsxLobbyJoinInfo *ji = psx_lobby_join_info();
|
||||
if (!ji || !ji->transport_ice_hub || !psx_lobby_launch_pending())
|
||||
return 0;
|
||||
psx_netplay_ice_stash_discard();
|
||||
g_np_ice_stash.is_host = psx_lobby_is_host() ? 1 : 0;
|
||||
if (g_np_ice_stash.is_host) {
|
||||
const int n = psx_lobby_ice_take_hub(g_np_ice_stash.seat,
|
||||
PSX_ICE_HUB_MAX_SEATS);
|
||||
if (n < 1) {
|
||||
fprintf(stderr, "psx_netplay: ICE hub handover failed: %s\n",
|
||||
psx_lobby_ice_launch_error());
|
||||
fflush(stderr);
|
||||
return 0;
|
||||
}
|
||||
g_np_ice_stash.n = n;
|
||||
} else {
|
||||
g_np_ice_stash.guest = psx_lobby_ice_take_guest_agent();
|
||||
if (!g_np_ice_stash.guest) {
|
||||
fprintf(stderr, "psx_netplay: ICE guest handover failed: %s\n",
|
||||
psx_lobby_ice_launch_error());
|
||||
fflush(stderr);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Host relay over ICE: take ownership of the connected agents and bind each to
|
||||
* the SESSION slot it serves. Failure destroys everything held and leaves one
|
||||
* sentence for the lobby's status line; never a smaller room. On success the
|
||||
* caller owns the agents (adopt, or rnet_host_ice_destroy_agent them). */
|
||||
static int ice_hub_acquire(const PsxNetplayConfig *cfg, int slots, int local,
|
||||
RNetIceAdoptSeat *seats_out, int *n_out,
|
||||
struct RNetIceAgent **guest_out)
|
||||
{
|
||||
char why[192];
|
||||
why[0] = '\0';
|
||||
*n_out = 0;
|
||||
*guest_out = NULL;
|
||||
if (cfg->spectator) {
|
||||
snprintf(why, sizeof(why),
|
||||
"spectators are not on the ICE host-relay path");
|
||||
} else if (!psx_netplay_ice_stash_held() && !psx_netplay_ice_stash_from_lobby()) {
|
||||
snprintf(why, sizeof(why), "no ICE connection was handed over for this launch");
|
||||
} else if (g_np_ice_stash.is_host) {
|
||||
int lobby_slots[PSX_ICE_HUB_MAX_SEATS];
|
||||
int session_slots[PSX_ICE_HUB_MAX_SEATS];
|
||||
PsxIceHubMapError err = PSX_ICE_HUB_MAP_OK;
|
||||
int i;
|
||||
if (local != 0) {
|
||||
snprintf(why, sizeof(why), "host launch with a non-host seat (%d)", local);
|
||||
} else {
|
||||
for (i = 0; i < g_np_ice_stash.n; ++i)
|
||||
lobby_slots[i] = g_np_ice_stash.seat[i].lobby_slot;
|
||||
if (psx_ice_hub_map_seats(cfg->port_of_slot, cfg->port_map_valid, slots,
|
||||
cfg->host_spectates, lobby_slots,
|
||||
g_np_ice_stash.n, session_slots, &err) != 0) {
|
||||
snprintf(why, sizeof(why), "cannot seat the connected players: %s",
|
||||
psx_ice_hub_map_error_text(err));
|
||||
} else {
|
||||
for (i = 0; i < g_np_ice_stash.n; ++i) {
|
||||
seats_out[i].slot = session_slots[i];
|
||||
seats_out[i].agent = g_np_ice_stash.seat[i].agent;
|
||||
fprintf(stderr, "psx_netplay: ICE hub lobby seat %d -> session slot %d\n",
|
||||
lobby_slots[i], session_slots[i]);
|
||||
}
|
||||
*n_out = g_np_ice_stash.n;
|
||||
memset(&g_np_ice_stash, 0, sizeof(g_np_ice_stash)); /* caller owns */
|
||||
fflush(stderr);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (local < 1)
|
||||
snprintf(why, sizeof(why), "guest launch holding the host seat");
|
||||
else {
|
||||
*guest_out = g_np_ice_stash.guest;
|
||||
memset(&g_np_ice_stash, 0, sizeof(g_np_ice_stash));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
psx_netplay_ice_stash_discard();
|
||||
fprintf(stderr, "psx_netplay: host relay over ICE refused: %s\n", why);
|
||||
fflush(stderr);
|
||||
psx_lobby_set_last_error(why);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int psx_netplay_start(const PsxNetplayConfig *cfg)
|
||||
{
|
||||
@@ -3860,6 +4009,11 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
|
||||
int slots;
|
||||
int local;
|
||||
int use_ice;
|
||||
int transport;
|
||||
int ice_hub;
|
||||
RNetIceAdoptSeat hub_agents[PSX_ICE_HUB_MAX_SEATS];
|
||||
int hub_seats = 0;
|
||||
struct RNetIceAgent *guest_agent = NULL;
|
||||
|
||||
if (!cfg || !cfg->enabled) return -1;
|
||||
if (psx_mod_netplay_profile() && !psx_mod_netplay_is_active()) {
|
||||
@@ -3975,9 +4129,13 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
|
||||
in_player = cfg->input_player;
|
||||
if (in_player < 0 || in_player >= PSX_MAX_PLAYERS) in_player = 0;
|
||||
|
||||
use_ice = resolve_use_ice(cfg);
|
||||
if (use_ice < 0)
|
||||
transport = resolve_transport(cfg);
|
||||
if (transport < 0)
|
||||
return -4;
|
||||
use_ice = (transport == 1);
|
||||
ice_hub = (transport == 2);
|
||||
if (ice_hub && !ice_hub_acquire(cfg, slots, local, hub_agents, &hub_seats, &guest_agent))
|
||||
return -5;
|
||||
|
||||
memset(&host, 0, sizeof(host));
|
||||
host.sample_local = host_sample_local;
|
||||
@@ -3989,7 +4147,15 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
|
||||
#endif
|
||||
|
||||
g_np.session = rnet_session_create(&rcfg, &host);
|
||||
if (!g_np.session) return -2;
|
||||
if (!g_np.session) {
|
||||
if (ice_hub) {
|
||||
int i;
|
||||
for (i = 0; i < hub_seats; ++i)
|
||||
psx_lobby_ice_destroy_agent(hub_agents[i].agent);
|
||||
if (guest_agent) psx_lobby_ice_destroy_agent(guest_agent);
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
if (use_ice) {
|
||||
#if defined(RNET_ENABLE_ICE)
|
||||
@@ -4156,6 +4322,31 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
|
||||
#endif
|
||||
}
|
||||
|
||||
if (ice_hub) {
|
||||
/* No socket, no dial, no signals: the agents are already COMPLETED and
|
||||
* are frozen on adoption (never push signals to them). */
|
||||
int rc;
|
||||
if (hub_seats > 0) {
|
||||
rc = rnet_session_start_ice_hub_adopt(g_np.session, hub_agents, hub_seats);
|
||||
} else {
|
||||
rc = rnet_session_adopt_ice_agent(g_np.session, guest_agent);
|
||||
}
|
||||
if (rc != 0) {
|
||||
int i;
|
||||
/* -1: nothing adopted, the agents are still ours. */
|
||||
for (i = 0; i < hub_seats; ++i)
|
||||
psx_lobby_ice_destroy_agent(hub_agents[i].agent);
|
||||
if (guest_agent) psx_lobby_ice_destroy_agent(guest_agent);
|
||||
fprintf(stderr, "psx_netplay: ICE adopt failed; the match is not started\n");
|
||||
fflush(stderr);
|
||||
psx_lobby_set_last_error("The connection to the host was lost before the "
|
||||
"match started.");
|
||||
rnet_session_destroy(g_np.session);
|
||||
g_np.session = NULL;
|
||||
return -6;
|
||||
}
|
||||
}
|
||||
|
||||
if (!use_ice) {
|
||||
/* Host-as-relay: slot 0 with 3+ seats and no dial peer. */
|
||||
#if PSX_MAX_PLAYERS >= 3
|
||||
@@ -4193,6 +4384,7 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
|
||||
* freezes local_slot + slot_count at create. Starting with zeroed g_np made
|
||||
* every peer seal as slot 0 and export the wrong seat (VS-select hang). */
|
||||
g_np.use_ice = use_ice ? 1 : 0;
|
||||
g_np.ice_hub = ice_hub ? 1 : 0;
|
||||
g_np.slot_count = (int)rcfg.slot_count;
|
||||
g_np_slot_count = g_np.slot_count;
|
||||
g_np.local_slot = (int)rcfg.local_slot;
|
||||
@@ -4368,10 +4560,11 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
|
||||
fprintf(stderr,
|
||||
"psx_netplay: started transport=%s slot=%d input_player=%d session=%u "
|
||||
"delay=%u force_input_relay=%d force_turn=%d bind=%s peer=%s\n",
|
||||
use_ice ? "ice" : "lan", g_np.local_slot, g_np.input_player,
|
||||
ice_hub ? "ice_hub" : (use_ice ? "ice" : "lan"), g_np.local_slot,
|
||||
g_np.input_player,
|
||||
(unsigned)rcfg.session_id, (unsigned)rcfg.input_delay,
|
||||
g_np.force_input_relay, cfg->force_turn ? 1 : 0, cfg->bind_hostport,
|
||||
use_ice ? "(ice)" : cfg->peer_hostport);
|
||||
(use_ice || ice_hub) ? "(ice)" : cfg->peer_hostport);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/* Host relay over ICE: transport choice and lobby-seat -> session-slot map.
|
||||
* ROM-free, socket-free. Plain checks, not assert(): Release builds define
|
||||
* NDEBUG. */
|
||||
#include "netplay_ice_hub.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond) do { if (!(cond)) { \
|
||||
fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \
|
||||
++failures; } } while (0)
|
||||
|
||||
/* psx_transport_decide(cfg_transport, hub, host, force_input_relay,
|
||||
* in_online_room, has_peer, ice_built) */
|
||||
static void test_transport(void)
|
||||
{
|
||||
/* The hub is decided by the launch alone: neither force_input_relay, an
|
||||
* online room, nor an explicit transport=ice may move it to the SFU or the
|
||||
* single-agent ICE path. transport_host is ALSO 1 for a hub launch. */
|
||||
CHECK(psx_transport_decide(0, 1, 1, 0, 1, 0, 1) == PSX_TRANSPORT_ICE_HUB);
|
||||
CHECK(psx_transport_decide(0, 1, 1, 1, 1, 1, 1) == PSX_TRANSPORT_ICE_HUB);
|
||||
CHECK(psx_transport_decide(1, 1, 1, 0, 1, 0, 1) == PSX_TRANSPORT_ICE_HUB);
|
||||
CHECK(psx_transport_decide(0, 1, 1, 0, 0, 0, 1) == PSX_TRANSPORT_ICE_HUB);
|
||||
/* Forced LAN, or a build without ICE: refuse, never fall back to a UDP
|
||||
* path the launch has no endpoint for. */
|
||||
CHECK(psx_transport_decide(2, 1, 1, 0, 1, 0, 1) == PSX_TRANSPORT_ERROR_NO_PEER);
|
||||
CHECK(psx_transport_decide(0, 1, 1, 0, 1, 0, 0) == PSX_TRANSPORT_ERROR_NO_PEER);
|
||||
|
||||
/* Legacy paths are unchanged. */
|
||||
CHECK(psx_transport_decide(0, 0, 1, 0, 1, 0, 1) == PSX_TRANSPORT_LAN_UDP); /* host relay, host binds */
|
||||
CHECK(psx_transport_decide(0, 0, 1, 0, 1, 1, 1) == PSX_TRANSPORT_LAN_UDP); /* host relay, guest dials */
|
||||
CHECK(psx_transport_decide(0, 0, 0, 1, 1, 1, 1) == PSX_TRANSPORT_LAN_UDP); /* SFU */
|
||||
CHECK(psx_transport_decide(0, 0, 0, 1, 1, 0, 1) == PSX_TRANSPORT_ERROR_NO_PEER); /* SFU, no endpoint */
|
||||
CHECK(psx_transport_decide(0, 0, 0, 0, 1, 0, 1) == PSX_TRANSPORT_ERROR_NO_PEER); /* online, no endpoint */
|
||||
CHECK(psx_transport_decide(2, 0, 0, 1, 1, 0, 1) == PSX_TRANSPORT_LAN_UDP); /* forced LAN */
|
||||
CHECK(psx_transport_decide(0, 0, 0, 0, 0, 1, 1) == PSX_TRANSPORT_LAN_UDP); /* direct IP */
|
||||
CHECK(psx_transport_decide(1, 0, 0, 0, 0, 0, 1) == PSX_TRANSPORT_SINGLE_ICE);
|
||||
CHECK(psx_transport_decide(1, 0, 0, 0, 0, 0, 0) == PSX_TRANSPORT_LAN_UDP);
|
||||
}
|
||||
|
||||
static int map(const int *port, int valid, int slots, int hs, const int *lobby, int n,
|
||||
int *out, PsxIceHubMapError *err)
|
||||
{
|
||||
return psx_ice_hub_map_seats(port, valid, slots, hs, lobby, n, out, err);
|
||||
}
|
||||
|
||||
static void test_mapping(void)
|
||||
{
|
||||
int out[PSX_ICE_HUB_MAX_SEATS];
|
||||
PsxIceHubMapError e;
|
||||
|
||||
/* 2P: host seat 0, guest seat 1. */
|
||||
{
|
||||
const int port[] = { 0, 1 };
|
||||
const int lobby[] = { 1 };
|
||||
CHECK(map(port, 1, 2, 0, lobby, 1, out, &e) == 0 && out[0] == 1);
|
||||
}
|
||||
/* 3P in any arrival order: parallel output. */
|
||||
{
|
||||
const int port[] = { 0, 1, 2 };
|
||||
const int lobby[] = { 2, 1 };
|
||||
CHECK(map(port, 1, 3, 0, lobby, 2, out, &e) == 0);
|
||||
CHECK(out[0] == 2 && out[1] == 1);
|
||||
}
|
||||
/* Host holds lobby seat 2 (moved): it is still session slot 0, and the
|
||||
* guests follow in ascending seat order -- slot != seat. */
|
||||
{
|
||||
const int port[] = { 2, 0, 1 };
|
||||
const int lobby[] = { 0, 1 };
|
||||
CHECK(map(port, 1, 3, 0, lobby, 2, out, &e) == 0);
|
||||
CHECK(out[0] == 1 && out[1] == 2);
|
||||
}
|
||||
/* Host in the gallery (host_spectates): slot 0 drives no seat, players
|
||||
* start at slot 1. */
|
||||
{
|
||||
const int port[] = { -1, 0, 1 };
|
||||
const int lobby[] = { 0, 1 };
|
||||
CHECK(map(port, 1, 3, 1, lobby, 2, out, &e) == 0);
|
||||
CHECK(out[0] == 1 && out[1] == 2);
|
||||
}
|
||||
/* Sparse room (seats 0 and 3 occupied). */
|
||||
{
|
||||
const int port[] = { 0, 3 };
|
||||
const int lobby[] = { 3 };
|
||||
CHECK(map(port, 1, 2, 0, lobby, 1, out, &e) == 0 && out[0] == 1);
|
||||
}
|
||||
/* The host's own seat is never an agent seat (search starts at slot 1). */
|
||||
{
|
||||
const int port[] = { 0, 1 };
|
||||
const int lobby[] = { 0 };
|
||||
CHECK(map(port, 1, 2, 0, lobby, 1, out, &e) == -1);
|
||||
CHECK(e == PSX_ICE_HUB_MAP_SEAT_UNMAPPED);
|
||||
}
|
||||
/* A session slot with no agent is a smaller room: refused. */
|
||||
{
|
||||
const int port[] = { 0, 1, 2 };
|
||||
const int lobby[] = { 1 };
|
||||
CHECK(map(port, 1, 3, 0, lobby, 1, out, &e) == -1);
|
||||
CHECK(e == PSX_ICE_HUB_MAP_SEAT_MISSING);
|
||||
}
|
||||
/* A connected seat the launch does not seat. */
|
||||
{
|
||||
const int port[] = { 0, 1 };
|
||||
const int lobby[] = { 5 };
|
||||
CHECK(map(port, 1, 2, 0, lobby, 1, out, &e) == -1);
|
||||
CHECK(e == PSX_ICE_HUB_MAP_SEAT_UNMAPPED);
|
||||
}
|
||||
/* Two agents for one seat. */
|
||||
{
|
||||
const int port[] = { 0, 1, 2 };
|
||||
const int lobby[] = { 1, 1 };
|
||||
CHECK(map(port, 1, 3, 0, lobby, 2, out, &e) == -1);
|
||||
CHECK(e == PSX_ICE_HUB_MAP_SEAT_DUPLICATE);
|
||||
}
|
||||
/* Bad arguments never write a mapping the caller could adopt. */
|
||||
{
|
||||
const int lobby[] = { 1 };
|
||||
CHECK(map(NULL, 1, 2, 0, lobby, 1, out, &e) == -1);
|
||||
CHECK(e == PSX_ICE_HUB_MAP_BAD_ARGS);
|
||||
CHECK(map(NULL, 0, 2, 0, lobby, 0, out, &e) == -1);
|
||||
CHECK(map(NULL, 0, 1, 0, lobby, 1, out, &e) == -1);
|
||||
}
|
||||
/* No table: identity, or seat + 1 with the host in the gallery. */
|
||||
{
|
||||
const int lobby[] = { 1, 2 };
|
||||
CHECK(map(NULL, 0, 3, 0, lobby, 2, out, &e) == 0);
|
||||
CHECK(out[0] == 1 && out[1] == 2);
|
||||
}
|
||||
{
|
||||
const int lobby[] = { 0, 1 };
|
||||
CHECK(map(NULL, 0, 3, 1, lobby, 2, out, &e) == 0);
|
||||
CHECK(out[0] == 1 && out[1] == 2);
|
||||
}
|
||||
CHECK(psx_ice_hub_map_error_text(PSX_ICE_HUB_MAP_SEAT_MISSING) != NULL);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_transport();
|
||||
test_mapping();
|
||||
if (failures) {
|
||||
fprintf(stderr, "netplay_ice_hub_test: %d failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("netplay_ice_hub_test: ok\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user