diff --git a/lib/recomp-net b/lib/recomp-net index c58f125a..b0751b7b 160000 --- a/lib/recomp-net +++ b/lib/recomp-net @@ -1 +1 @@ -Subproject commit c58f125a3cc3468fc84012cf4eeb50cc0b62c379 +Subproject commit b0751b7b8f8d19a8b3629f46c6d9eb3672dc113b diff --git a/runtime/include/psx_lobby_client.h b/runtime/include/psx_lobby_client.h index 7c9a4eb8..0831dbb3 100644 --- a/runtime/include/psx_lobby_client.h +++ b/runtime/include/psx_lobby_client.h @@ -106,6 +106,11 @@ typedef struct PsxLobbyMember { /* Account key behind this seat; "" for a guest. See * PsxLobbyOnlinePlayer.account. */ char account[PSX_LOBBY_ID_LEN]; + /* Host-relay room: this guest's latest path_report ("direct" | "fail" | + * "relay"), "" when none, and whether the server still trusts it at start + * (recomp-net-server WS_LOBBY.md "Host relay"). */ + char path[8]; + int path_fresh; } PsxLobbyMember; /* @@ -174,6 +179,11 @@ typedef struct PsxLobbyMatchCaps { int input_delay; /* recomp-net delay frames (D) */ int input_prediction; /* invent runway frames (P); rollback only */ int force_input_relay; /* 0/1 — server input relay (vs P2P) */ + /* 0/1 -- host: ask for the HOST RELAY (match_caps.relay = "host"): the + * host carries the match on its own UDP port and guests dial it; the + * 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; 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. */ @@ -223,6 +233,11 @@ typedef struct PsxLobbyJoinInfo { /* Launch: the host watches from the gallery but runs the match from * session slot 0 (pad muted); player seats sit at lobby seat + 1. */ int host_spectates; + /* Launch: transport "host" -- no relay_endpoint; the host carries the + * match on host_endpoint (its advertised public port); guests dial it. + * force_input_relay is 0. The host binds bind_hostport and accepts the + * guest (2 seats) or hubs (3+). */ + int transport_host; /* 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]; @@ -348,6 +363,17 @@ const PsxLobbyMatchCaps *psx_lobby_match_caps(void); /* Host: push updated caps while in lobby (clears ready via lobby_update). */ int psx_lobby_set_match_caps(const PsxLobbyMatchCaps *caps); +/* Host relay (recomp_net/host_relay.h), driven from psx_lobby_pump while + * seated online in a room whose caps ask for it: the host holds the game port + * (UPnP / NAT-PMP / STUN), advertises it with set_host_endpoint and answers + * probes; guests probe it and send path_report. The pref is the HOST's + * default for rooms it creates (recomp-ui owns the setting; default on). */ +void psx_lobby_set_relay_host_pref(int on); +int psx_lobby_relay_host_pref(void); +struct RNetHostRelayStatus; +int psx_lobby_host_relay_status(struct RNetHostRelayStatus *out); +void psx_lobby_host_relay_release_port(void); + /* Live member table from lobby_update (and create/join). */ int psx_lobby_member_count(void); int psx_lobby_member_get(int index, PsxLobbyMember *out); diff --git a/runtime/include/psx_netplay.h b/runtime/include/psx_netplay.h index dff35309..124d4542 100644 --- a/runtime/include/psx_netplay.h +++ b/runtime/include/psx_netplay.h @@ -79,6 +79,10 @@ typedef struct PsxNetplayConfig { /* Rollback invent runway (phase_lock / P). Clamped 2..16. Unused in delay-sync. */ int input_prediction; int force_input_relay; /* 1 = lobby UDP SFU star (from launch) */ + /* 1 = the launch said transport "host": an online room whose host + * carries the match on bind_hostport; guests dial peer_hostport. Takes + * the LAN / hub transport path, not the server-relay one. */ + int transport_host; 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. */ diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 1b3261be..4078fdcd 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -62,6 +62,7 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "psx_netplay_rb.h" #include "psx_selfcheck.h" #include "psx_lobby_client.h" +#include "recomp_net/host_relay.h" /* RNetHostRelayStatus for the launcher relay line */ #include "netplay_bios_settle.h" #include "netplay_exit_reason.h" #include "host_time.h" @@ -8497,6 +8498,11 @@ namespace { * relay_endpoint. force_turn is a rollback delay-floor hint only. */ int g_lnch_force_input_relay = 0; int g_lnch_force_turn = 0; + /* Host relay (2026-10-01): online rooms this build hosts ask the server + * for transport "host" (match_caps.relay). recomp-ui owns the setting + * (Network Settings / Lobby Settings, persisted there) and pushes it + * through relay_host_set; default on. */ + int g_lnch_relay_host = 1; /* Lobby default on; host “Disable Rollback” clears this → delay_sync. */ int g_lnch_rollback = 1; int g_lnch_multitap_analog = 1; @@ -10419,6 +10425,7 @@ namespace { if (caps.input_prediction < 2) caps.input_prediction = 2; if (caps.input_prediction > 16) caps.input_prediction = 16; caps.force_input_relay = g_lnch_force_input_relay != 0; + caps.relay_host = g_lnch_relay_host != 0; caps.force_turn = g_lnch_force_turn != 0; caps.rollback = g_lnch_rollback != 0; caps.multitap_analog = g_lnch_multitap_analog != 0; @@ -10461,6 +10468,7 @@ namespace { if (caps.input_prediction < 2) caps.input_prediction = 2; if (caps.input_prediction > 16) caps.input_prediction = 16; caps.force_input_relay = g_lnch_force_input_relay != 0; + caps.relay_host = g_lnch_relay_host != 0; caps.force_turn = g_lnch_force_turn != 0; caps.rollback = g_lnch_rollback != 0; caps.multitap_analog = g_lnch_multitap_analog != 0; @@ -10914,6 +10922,69 @@ namespace { } return g_lnch_force_turn; } + /* Host relay preference and live state (recomp_launcher.h relay_host_*). */ + int ae_np_relay_host_get(void*) { + if (g_lnch_hosting_lan || g_lnch_joined_lan) return 0; + if (psx_lobby_in_lobby()) { + const PsxLobbyMatchCaps* caps = psx_lobby_match_caps(); + if (caps && caps->valid) return caps->relay_host ? 1 : 0; + } + return g_lnch_relay_host; + } + int ae_np_relay_host_set(void*, int on) { + g_lnch_relay_host = on ? 1 : 0; + psx_lobby_set_relay_host_pref(g_lnch_relay_host); + 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 (g_lnch_hosting_lan || g_lnch_joined_lan || !psx_lobby_in_lobby()) return 0; + RNetHostRelayStatus st; + if (!psx_lobby_host_relay_status(&st)) return 0; + if (st.role == 1) { + int guests = 0, proven = 0; + const int n = psx_lobby_member_count(); + for (int i = 0; i < n; ++i) { + PsxLobbyMember mem; + if (!psx_lobby_member_get(i, &mem) || mem.is_spectator) continue; + if (std::strcmp(mem.player_id, psx_lobby_host_player_id()) == 0) continue; + guests++; + if (std::strcmp(mem.path, "direct") == 0 && mem.path_fresh) proven++; + } + if (!st.port.done) + std::snprintf(out, out_cap, "You carry the match. Opening UDP port %u (%s)...", + (unsigned)st.port.local_port, + st.port.stage[0] ? st.port.stage : "starting"); + else if (!st.port.endpoint[0]) + std::snprintf(out, out_cap, "%s", st.port.detail); + else + std::snprintf(out, out_cap, + "You carry the match at %s (%s). Guests who can reach you: %d of %d%s", + st.port.endpoint, st.port.how, proven, guests, + guests && proven < guests + ? ". Any guest who cannot sends the match through the lobby " + "server's relay." + : "."); + return 1; + } + if (st.role == 2) { + if (st.probing || !st.last_report[0]) + std::snprintf(out, out_cap, "The host carries the match. Checking you can reach %s...", + st.probed[0] ? st.probed : "the host"); + else if (std::strcmp(st.last_report, "direct") == 0) + std::snprintf(out, out_cap, "The host carries the match; you reach it directly (%s).", + st.probed); + else + std::snprintf(out, out_cap, + "The host's port did not answer (%s): the lobby server will relay " + "this match. Retrying.", st.probed); + return 1; + } + return 0; + } int ae_np_force_turn_set(void*, int force) { if (g_lnch_hosting_lan || g_lnch_joined_lan) return 0; /* LAN/Direct IP — no online delay-floor hint */ @@ -12830,13 +12901,18 @@ namespace { for (int i = 0; i < RECOMP_LAUNCHER_NETPLAY_MAX_MEMBERS + 1; ++i) out->slot_port[i] = plan.port[i]; } - /* §108: online launch always SFU. Prefer caps.force_input_relay from + /* Online launch: the server's relay (SFU) unless the launch said + * transport "host" (the host carries the match; 2026-10-01 amends + * §108's "always SFU"). Prefer caps.force_input_relay from the * relay_endpoint rewrite; also infer when host==guest advertise. */ out->force_input_relay = - (g_lnch_hosting_lan || g_lnch_joined_lan) + (g_lnch_hosting_lan || g_lnch_joined_lan || ji->transport_host) ? 0 : (caps->force_input_relay ? 1 : 0); - if (!out->force_input_relay && !g_lnch_hosting_lan && +#if defined(RECOMP_LAUNCHER_HAS_HOST_RELAY) + out->transport_host = ji->transport_host ? 1 : 0; +#endif + if (!out->force_input_relay && !ji->transport_host && !g_lnch_hosting_lan && !g_lnch_joined_lan && ji->host_endpoint[0] && ji->guest_endpoint[0] && std::strcmp(ji->host_endpoint, ji->guest_endpoint) == 0) { @@ -13047,6 +13123,11 @@ namespace { /* Append-only members past the positional initializer. */ #if defined(RECOMP_LAUNCHER_HAS_NETPLAY_HANDOFF) g_lnch_netplay_callbacks.ingest_launch = ae_np_ingest_launch; +#endif +#if defined(RECOMP_LAUNCHER_HAS_HOST_RELAY) + 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 g_lnch_netplay_callbacks.memcard_offer_set = ae_np_memcard_offer_set; g_lnch_netplay_callbacks.guest_memcard_get = ae_np_guest_memcard_get; @@ -15248,6 +15329,9 @@ int main(int argc, char** argv) { net_cfg.input_delay = ls.netplay_launch.input_delay; net_cfg.input_prediction = ls.netplay_launch.input_prediction; 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; +#endif net_cfg.force_turn = ls.netplay_launch.force_turn ? 1 : 0; net_cfg.rollback = ls.netplay_launch.rollback ? 1 : 0; net_cfg.guest_memcard = ls.netplay_launch.guest_memcard ? 1 : 0; @@ -17140,6 +17224,9 @@ soft_return_lobby: net_cfg.input_delay = ls.netplay_launch.input_delay; net_cfg.input_prediction = ls.netplay_launch.input_prediction; 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; +#endif net_cfg.force_turn = ls.netplay_launch.force_turn ? 1 : 0; net_cfg.rollback = ls.netplay_launch.rollback ? 1 : 0; /* Same fold as the first-boot path: rematch must not lose diff --git a/runtime/src/psx_lobby_client.c b/runtime/src/psx_lobby_client.c index a1f9efb0..b8e43a26 100644 --- a/runtime/src/psx_lobby_client.c +++ b/runtime/src/psx_lobby_client.c @@ -44,6 +44,10 @@ int psx_lobby_create(const char *a, const char *b, const char *c, const char *d int psx_lobby_join(const char *a, const char *b, const char *c) { (void)a; (void)b; (void)c; return -1; } int psx_lobby_leave(void) { return -1; } +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) {} 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; } @@ -183,6 +187,7 @@ void psx_lobby_clear_launch_pending(void) {} #include "rnet_ws.h" #include "rnet_sha1.h" #include "recomp_net/address.h" +#include "recomp_net/host_relay.h" #include "recomp_net/ice.h" #include "recomp_net/ice_rtt.h" #include "recomp_net/lan_beacon.h" @@ -1098,6 +1103,74 @@ static void match_caps_clear(PsxLobbyMatchCaps *c) c->guest_memcard = 1; } +/* ---- host relay (recomp_net/host_relay.h) ------------------------------- + * The same wiring as recomp-net's own lobby client (rnet_lobby_client.c): + * this file is psxrecomp's copy of that client, so the orchestration object + * is recomp-net's and only the glue lives here. */ +static RNetHostRelay *g_host_relay; +static int g_relay_host_pref = 1; + +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; } + +static int host_relay_send(const char *json, void *ctx) +{ + (void)ctx; + if (!psx_lobby_connected() || !g_lc.in_lobby) return -1; + queue_send(json); + flush_pending(); + return 0; +} + +static int lobby_bind_port(void) +{ + const char *colon = strrchr(g_lc.my_bind, ':'); + const int port = colon ? atoi(colon + 1) : 0; + return port > 0 && port < 65536 ? port : 0; +} + +/* Every pump: active only while seated in an online room whose published + * caps ask for the host relay (the host's caps are echoed back to it, so one + * rule serves both roles) and before the launch, which releases the port for + * the game. A spectator is not on the relay's path and reports nothing. */ +static void host_relay_step(void) +{ + RNetHostRelayView v; + if (!g_host_relay) { + g_host_relay = rnet_host_relay_create(); + if (!g_host_relay) return; + } + memset(&v, 0, sizeof(v)); + 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; + 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; + v.send_json = host_relay_send; + rnet_host_relay_update(g_host_relay, &v); +} + +int psx_lobby_host_relay_status(struct RNetHostRelayStatus *out) +{ + RNetHostRelayStatus st; + if (!out) return 0; + rnet_host_relay_status(g_host_relay, &st); + *out = st; + return st.role != 0; +} + +void psx_lobby_host_relay_release_port(void) +{ + rnet_host_relay_release_port(g_host_relay); +} + +static void host_relay_leave(void) +{ + rnet_host_relay_leave(g_host_relay); +} + 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); @@ -1481,6 +1554,12 @@ static void parse_match_caps_object(const char *obj, PsxLobbyMatchCaps *out) if (out->input_prediction < 2) out->input_prediction = 2; if (out->input_prediction > 16) out->input_prediction = 16; out->force_input_relay = json_get_bool(obj, "force_input_relay", 0); + { + char relay[12]; + relay[0] = '\0'; + json_get_str(obj, "relay", relay, sizeof(relay)); + out->relay_host = strcmp(relay, "host") == 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); @@ -1538,7 +1617,7 @@ static int append_match_caps_json(char *dst, size_t dst_cap, const PsxLobbyMatch return snprintf(dst, dst_cap, ",\"match_caps\":{\"v\":1,\"aspect_num\":%d,\"aspect_den\":%d," "\"turbo_loads\":%s,\"bios_hle\":%s,\"fast_boot\":%s," - "\"auto_skip_fmv\":%s,\"input_delay\":%d,\"input_prediction\":%d," + "\"auto_skip_fmv\":%s,\"input_delay\":%d,\"input_prediction\":%d,%s" "\"force_input_relay\":%s,\"force_turn\":%s,\"rollback\":%s," "\"multitap_analog\":%s,\"guest_memcard\":%s," "\"guest_memcard_active\":%s," @@ -1551,6 +1630,8 @@ 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, + /* only when asked: absent reads as "the SFU, as always" */ + caps->relay_host ? "\"relay\":\"host\"," : "", caps->force_input_relay ? "true" : "false", caps->force_turn ? "true" : "false", caps->rollback ? "true" : "false", @@ -1629,7 +1710,7 @@ static int using_server_input_relay(const PsxLobbyJoinInfo *j) static void fill_peer_bind_from_join(void) { PsxLobbyJoinInfo *j = &g_lc.join; - const int force_relay = using_server_input_relay(j); + const int force_relay = !j->transport_host && using_server_input_relay(j); const int seats = j->player_count >= 2 ? j->player_count : j->max_slots; const int host_hub = (g_lc.is_host && seats >= 3 && !force_relay) ? 1 : 0; memset(j->bind_hostport, 0, sizeof(j->bind_hostport)); @@ -1642,9 +1723,12 @@ static void fill_peer_bind_from_join(void) j->host_endpoint, j->guest_endpoint, NULL); } else if (g_lc.is_host) { strncpy(j->bind_hostport, g_lc.my_bind, sizeof(j->bind_hostport) - 1); - if (!host_hub) { + if (!host_hub && !j->transport_host) { /* 2P P2P: dial guest when they advertised a fixed port. Online - * guests often join with :0 — leave peer empty (accept-first). */ + * guests often join with :0 — leave peer empty (accept-first). + * Host relay: the guest's advertised address is its LAN bind, + * which this host cannot dial across the guest's NAT; the guest + * dials us (it proved it can), so accept-first always. */ if (j->guest_endpoint[0] && !endpoint_port_is_zero(j->guest_endpoint)) strncpy(j->peer_hostport, j->guest_endpoint, sizeof(j->peer_hostport) - 1); } @@ -1724,6 +1808,10 @@ static int parse_seat_array(const char *json, const char *key, int is_spectator, g_lc.members[n].slot = json_get_int(chunk, "slot", n); json_get_str(chunk, "player_id", g_lc.members[n].player_id, sizeof(g_lc.members[n].player_id)); + g_lc.members[n].path[0] = '\0'; + json_get_str(chunk, "path", g_lc.members[n].path, + sizeof(g_lc.members[n].path)); + g_lc.members[n].path_fresh = json_get_bool(chunk, "path_fresh", 0) ? 1 : 0; json_get_str(chunk, "display_name", g_lc.members[n].display_name, sizeof(g_lc.members[n].display_name)); g_lc.members[n].ready = json_get_bool(chunk, "ready", 0); @@ -2960,10 +3048,19 @@ static void handle_server_json(const char *json) } if (strcmp(op, "launch") == 0) { char relay_endpoint[PSX_LOBBY_ENDPOINT_LEN]; + char transport_kind[16]; json_get_str(json, "host_endpoint", g_lc.join.host_endpoint, sizeof(g_lc.join.host_endpoint)); json_get_str(json, "guest_endpoint", g_lc.join.guest_endpoint, sizeof(g_lc.join.guest_endpoint)); relay_endpoint[0] = '\0'; json_get_str(json, "relay_endpoint", relay_endpoint, sizeof(relay_endpoint)); + /* "host": the host carries the match (WS_LOBBY.md "Host relay"): no + * relay_endpoint, host_endpoint is the host's advertised port. The + * waiting-room socket on that port goes now so the game can bind it; + * the router mapping stays for the game. */ + transport_kind[0] = '\0'; + json_get_str(json, "transport", transport_kind, sizeof(transport_kind)); + g_lc.join.transport_host = strcmp(transport_kind, "host") == 0 ? 1 : 0; + rnet_host_relay_release_port(g_host_relay); 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); @@ -3000,7 +3097,8 @@ static void handle_server_json(const char *json) * Legacy ice_p2p from an old server is refused (need SFU). */ { char transport[24]; - const int force_relay = using_server_input_relay(&g_lc.join); + const int force_relay = + !g_lc.join.transport_host && using_server_input_relay(&g_lc.join); const int seats = g_lc.join.player_count >= 2 ? g_lc.join.player_count : g_lc.join.max_slots; const int host_hub = @@ -3582,6 +3680,7 @@ int psx_lobby_connect(const char *ws_url) void psx_lobby_disconnect(void) { + host_relay_leave(); /* Never block the UI on DNS/connect — cancel and let pump reap. */ lobby_cancel_connect_async(); automatch_on_connection_reset(); @@ -3934,6 +4033,9 @@ void psx_lobby_pump(void) { char buf[4096]; automatch_probe_poll(); + /* 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(); #if defined(_WIN32) int n; #else @@ -4198,6 +4300,7 @@ 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 */ queue_send("{\"op\":\"leave\"}"); flush_pending(); g_lc.ice_rtt_suspended = 0; diff --git a/runtime/src/psx_netplay.c b/runtime/src/psx_netplay.c index 2b37a104..acbd8f45 100644 --- a/runtime/src/psx_netplay.c +++ b/runtime/src/psx_netplay.c @@ -3594,6 +3594,17 @@ static int resolve_use_ice(const PsxNetplayConfig *cfg) * 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 (cfg->force_input_relay || in_motk_room) { if (!cfg->peer_hostport || !cfg->peer_hostport[0]) { fprintf(stderr,